@marinade.finance/kamino-sdk
Version:
304 lines • 14.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrcaService = void 0;
const web3_js_1 = require("@solana/web3.js");
const decimal_js_1 = __importDefault(require("decimal.js"));
const whirlpool_sdk_1 = require("@orca-so/whirlpool-sdk");
const axios_1 = __importDefault(require("axios"));
const accounts_1 = require("../kamino-client/accounts");
const scope_sdk_1 = require("@hubbleprotocol/scope-sdk");
const whirpools_client_1 = require("../whirpools-client");
const utils_1 = require("../utils");
const programId_1 = require("../whirpools-client/programId");
class OrcaService {
_connection;
_cluster;
_orcaNetwork;
_orcaApiUrl;
_globalConfig;
constructor(connection, cluster, globalConfig) {
this._connection = connection;
this._cluster = cluster;
this._globalConfig = globalConfig;
this._orcaNetwork = cluster === 'mainnet-beta' ? whirlpool_sdk_1.OrcaNetwork.MAINNET : whirlpool_sdk_1.OrcaNetwork.DEVNET;
this._orcaApiUrl = `https://api.${cluster === 'mainnet-beta' ? 'mainnet' : 'devnet'}.orca.so`;
}
async getOrcaWhirlpools() {
return (await axios_1.default.get(`${this._orcaApiUrl}/v1/whirlpool/list`)).data;
}
/**
* Get token prices for a strategy - for use with orca sdk
* @param strategy
* @param prices
* @param collateralInfos
* @returns {Record<string, Decimal>} - token prices by mint string
* @private
*/
getTokenPrices(strategy, prices, collateralInfos) {
const tokensPrices = {};
const tokenA = collateralInfos[strategy.tokenACollateralId.toNumber()];
const tokenB = collateralInfos[strategy.tokenBCollateralId.toNumber()];
const rewardToken0 = collateralInfos[strategy.reward0CollateralId.toNumber()];
const rewardToken1 = collateralInfos[strategy.reward1CollateralId.toNumber()];
const rewardToken2 = collateralInfos[strategy.reward2CollateralId.toNumber()];
const aPrice = scope_sdk_1.Scope.getPriceFromScopeChain(tokenA.scopePriceChain, prices);
const bPrice = scope_sdk_1.Scope.getPriceFromScopeChain(tokenB.scopePriceChain, prices);
const reward0Price = strategy.reward0Decimals.toNumber() !== 0
? scope_sdk_1.Scope.getPriceFromScopeChain(rewardToken0.scopePriceChain, prices)
: null;
const reward1Price = strategy.reward1Decimals.toNumber() !== 0
? scope_sdk_1.Scope.getPriceFromScopeChain(rewardToken1.scopePriceChain, prices)
: null;
const reward2Price = strategy.reward2Decimals.toNumber() !== 0
? scope_sdk_1.Scope.getPriceFromScopeChain(rewardToken2.scopePriceChain, prices)
: null;
const [mintA, mintB] = [strategy.tokenAMint.toString(), strategy.tokenBMint.toString()];
const reward0 = collateralInfos[strategy.reward0CollateralId.toNumber()]?.mint?.toString();
const reward1 = collateralInfos[strategy.reward1CollateralId.toNumber()]?.mint?.toString();
const reward2 = collateralInfos[strategy.reward2CollateralId.toNumber()]?.mint?.toString();
tokensPrices[mintA] = aPrice;
tokensPrices[mintB] = bPrice;
if (reward0Price !== null) {
tokensPrices[reward0] = reward0Price;
}
if (reward1Price !== null) {
tokensPrices[reward1] = reward1Price;
}
if (reward2Price !== null) {
tokensPrices[reward2] = reward2Price;
}
return tokensPrices;
}
getPoolTokensPrices(pool, prices) {
const tokensPrices = {};
const tokens = [
pool.tokenMintA.toString(),
pool.tokenMintB.toString(),
pool.rewards[0].mint.toString(),
pool.rewards[1].mint.toString(),
pool.rewards[2].mint.toString(),
];
for (const mint of tokens) {
if (mint) {
const price = prices.find((x) => x.mint?.toString() === mint)?.price;
if (!price) {
throw new Error(`Could not get token ${mint} price`);
}
tokensPrices[mint] = price;
}
}
return tokensPrices;
}
async getPool(poolAddress) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._connection,
network: this._orcaNetwork,
});
return await orca.getPool(poolAddress);
}
async getStrategyWhirlpoolPoolAprApy(strategy, whirlpools, prices) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._connection,
network: this._orcaNetwork,
});
const scope = new scope_sdk_1.Scope(this._cluster, this._connection);
if (!prices) {
prices = await scope.getOraclePrices();
}
const position = await whirpools_client_1.Position.fetch(this._connection, strategy.position);
if (!position) {
throw new Error(`Position ${strategy.position} does not exist`);
}
const pool = await orca.getPool(strategy.pool);
if (!whirlpools) {
({ whirlpools } = await this.getOrcaWhirlpools());
}
const whirlpool = whirlpools?.find((x) => x.address === strategy.pool.toString());
if (!pool || !whirlpool) {
throw Error(`Could not get orca pool data for ${strategy.pool}`);
}
const priceRange = (0, utils_1.getStrategyPriceRangeOrca)(position.tickLowerIndex, position.tickUpperIndex, strategy, new decimal_js_1.default(pool.price.toString()));
if (priceRange.strategyOutOfRange) {
return {
...priceRange,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const lpFeeRate = pool.feePercentage;
const volume24hUsd = whirlpool?.volume?.day ?? new decimal_js_1.default(0);
const fee24Usd = new decimal_js_1.default(volume24hUsd).mul(lpFeeRate).toNumber();
const config = await accounts_1.GlobalConfig.fetch(this._connection, this._globalConfig);
if (!config) {
throw Error(`Could not fetch globalConfig with pubkey ${this._globalConfig}`);
}
const collateralInfos = await accounts_1.CollateralInfos.fetch(this._connection, config.tokenInfos);
if (!collateralInfos) {
throw Error('Could not fetch collateral infos');
}
const tokensPrices = this.getTokenPrices(strategy, prices, collateralInfos.infos);
const apr = (0, whirlpool_sdk_1.estimateAprsForPriceRange)(pool, tokensPrices, fee24Usd, position.tickLowerIndex, position.tickUpperIndex);
const totalApr = new decimal_js_1.default(apr.fee).add(apr.rewards[0]).add(apr.rewards[1]).add(apr.rewards[2]);
const feeApr = new decimal_js_1.default(apr.fee);
const rewardsApr = apr.rewards.map((r) => new decimal_js_1.default(r));
return {
totalApr,
totalApy: (0, utils_1.aprToApy)(totalApr, 365),
feeApr,
feeApy: (0, utils_1.aprToApy)(feeApr, 365),
rewardsApr,
rewardsApy: rewardsApr.map((x) => (0, utils_1.aprToApy)(x, 365)),
...priceRange,
};
}
// strongly recommended to pass lowestTick and highestTick because fetching the lowest and highest existent takes very long
async getWhirlpoolLiquidityDistribution(pool, keepOrder = true, lowestTick, highestTick) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._connection,
network: this._orcaNetwork,
});
const poolData = await orca.getPool(pool);
if (!poolData) {
throw new Error(`Could not get pool data for Whirlpool ${pool}`);
}
let lowestInitializedTick;
if (lowestTick) {
lowestInitializedTick = lowestTick;
}
else {
lowestInitializedTick = await orca.pool.getLowestInitializedTickArrayTickIndex(pool, poolData.tickSpacing);
}
let highestInitializedTick;
if (highestTick) {
highestInitializedTick = highestTick;
}
else {
highestInitializedTick = await orca.pool.getHighestInitializedTickArrayTickIndex(pool, poolData.tickSpacing);
}
const orcaLiqDistribution = await orca.pool.getLiquidityDistribution(pool, lowestInitializedTick, highestInitializedTick);
let liqDistribution = {
currentPrice: poolData.price,
currentTickIndex: poolData.tickCurrentIndex,
distribution: [],
};
orcaLiqDistribution.datapoints.forEach((entry) => {
let priceWithOrder = new decimal_js_1.default(entry.price);
if (!keepOrder) {
priceWithOrder = new decimal_js_1.default(1).div(priceWithOrder);
}
const liq = {
price: priceWithOrder,
liquidity: entry.liquidity,
tickIndex: entry.tickIndex,
};
liqDistribution.distribution.push(liq);
});
return liqDistribution;
}
async getWhirlpoolPositionAprApy(poolPubkey, priceLower, priceUpper, whirlpools, prices) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._connection,
network: this._orcaNetwork,
});
const scope = new scope_sdk_1.Scope(this._cluster, this._connection);
if (!prices) {
prices = await scope.getAllPrices();
}
const pool = await orca.getPool(poolPubkey);
if (!whirlpools) {
({ whirlpools } = await this.getOrcaWhirlpools());
}
const whirlpool = whirlpools?.find((x) => x.address === poolPubkey.toString());
if (!pool || !whirlpool) {
throw Error(`Could not get orca pool data for ${poolPubkey}`);
}
let strategyOutOfRange = false;
if (priceLower.gt(pool.price) || priceUpper.lt(pool.price)) {
strategyOutOfRange = true;
}
if (strategyOutOfRange) {
return {
priceLower,
priceUpper,
strategyOutOfRange,
poolPrice: pool.price,
rewardsApy: [],
rewardsApr: [],
feeApy: utils_1.ZERO,
feeApr: utils_1.ZERO,
totalApy: utils_1.ZERO,
totalApr: utils_1.ZERO,
};
}
const lpFeeRate = pool.feePercentage;
const volume24hUsd = whirlpool?.volume?.day ?? new decimal_js_1.default(0);
const fee24Usd = new decimal_js_1.default(volume24hUsd).mul(lpFeeRate).toNumber();
let tokensPrices = this.getPoolTokensPrices(pool, prices);
const tickLowerIndex = (0, whirlpool_sdk_1.getNearestValidTickIndexFromTickIndex)((0, whirlpool_sdk_1.priceToTickIndex)(priceLower, pool.tokenDecimalsA, pool.tokenDecimalsB), whirlpool.tickSpacing);
const tickUpperIndex = (0, whirlpool_sdk_1.getNearestValidTickIndexFromTickIndex)((0, whirlpool_sdk_1.priceToTickIndex)(priceUpper, pool.tokenDecimalsA, pool.tokenDecimalsB), whirlpool.tickSpacing);
const apr = (0, whirlpool_sdk_1.estimateAprsForPriceRange)(pool, tokensPrices, fee24Usd, tickLowerIndex, tickUpperIndex);
const totalApr = new decimal_js_1.default(apr.fee).add(apr.rewards[0]).add(apr.rewards[1]).add(apr.rewards[2]);
const feeApr = new decimal_js_1.default(apr.fee);
const rewardsApr = apr.rewards.map((r) => new decimal_js_1.default(r));
return {
totalApr,
totalApy: (0, utils_1.aprToApy)(totalApr, 365),
feeApr,
feeApy: (0, utils_1.aprToApy)(feeApr, 365),
rewardsApr,
rewardsApy: rewardsApr.map((x) => (0, utils_1.aprToApy)(x, 365)),
priceLower,
priceUpper,
poolPrice: pool.price,
strategyOutOfRange,
};
}
async getGenericPoolInfo(poolPubkey, whirlpools) {
const orca = new whirlpool_sdk_1.OrcaWhirlpoolClient({
connection: this._connection,
network: this._orcaNetwork,
});
const pool = await orca.getPool(poolPubkey);
if (!whirlpools) {
({ whirlpools } = await this.getOrcaWhirlpools());
}
const whirlpool = whirlpools?.find((x) => x.address === poolPubkey.toString());
if (!pool || !whirlpool) {
throw Error(`Could not get orca pool data for ${poolPubkey}`);
}
let poolInfo = {
dex: 'ORCA',
address: new web3_js_1.PublicKey(poolPubkey),
tokenMintA: pool.tokenMintA,
tokenMintB: pool.tokenMintB,
price: pool.price,
feeRate: pool.feePercentage,
volumeOnLast7d: whirlpool.volume ? new decimal_js_1.default(whirlpool.volume?.week) : undefined,
tvl: whirlpool.tvl ? new decimal_js_1.default(whirlpool.tvl) : undefined,
tickSpacing: new decimal_js_1.default(pool.tickSpacing),
// todo(Silviu): get real amount of positions
positions: new decimal_js_1.default(0),
};
return poolInfo;
}
async getPositionsCountByPool(pool) {
const rawPositions = await this._connection.getProgramAccounts(programId_1.WHIRLPOOL_PROGRAM_ID, {
commitment: 'confirmed',
filters: [
// account LAYOUT: https://github.com/orca-so/whirlpools/blob/main/programs/whirlpool/src/state/position.rs#L20
{ dataSize: 216 },
{ memcmp: { bytes: pool.toBase58(), offset: 8 } },
],
});
return rawPositions.length;
}
}
exports.OrcaService = OrcaService;
//# sourceMappingURL=OrcaService.js.map