From 0c6049def41f1789078e6c7cd11958446fb3ba6f Mon Sep 17 00:00:00 2001 From: peroxy Date: Mon, 30 Oct 2023 16:24:59 +0100 Subject: [PATCH] Add price methods to kamino-sdk --- packages/kamino-sdk/src/Kamino.ts | 123 ++++++++++++++++-- .../src/models/EnrichedScopePrice.ts | 29 +++++ packages/kamino-sdk/tests/kamino.test.ts | 53 ++++++++ 3 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 packages/kamino-sdk/src/models/EnrichedScopePrice.ts diff --git a/packages/kamino-sdk/src/Kamino.ts b/packages/kamino-sdk/src/Kamino.ts index 8a84405e..ba1f3aa9 100644 --- a/packages/kamino-sdk/src/Kamino.ts +++ b/packages/kamino-sdk/src/Kamino.ts @@ -113,6 +113,7 @@ import { WithdrawAllAndCloseIxns, numberToReferencePriceType, stripTwapZeros, + getTokenNameFromCollateralInfo, } from './utils'; import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { @@ -243,8 +244,8 @@ import { } from './utils/transactions'; import { RouteInfo } from '@jup-ag/core'; import { SwapResponse } from '@jup-ag/api'; -import { StrategyPrices } from './models/StrategyPrices'; -import { getDefaultManualRebalanceFieldInfos, getManualRebalanceFieldInfos } from './rebalance_methods/manualRebalance'; +import { StrategyPrices } from './models'; +import { getDefaultManualRebalanceFieldInfos, getManualRebalanceFieldInfos } from './rebalance_methods'; import { deserializePricePercentageRebalanceFromOnchainParams, deserializePricePercentageRebalanceWithStateOverride, @@ -252,9 +253,8 @@ import { getPositionRangeFromPercentageRebalanceParams, getPricePercentageRebalanceFieldInfos, readPricePercentageRebalanceParamsFromStrategy, - readPricePercentageRebalanceStateFromStrategy, readRawPricePercentageRebalanceStateFromStrategy, -} from './rebalance_methods/pricePercentageRebalance'; +} from './rebalance_methods'; import { deserializePricePercentageWithResetRebalanceFromOnchainParams, deserializePricePercentageWithResetRebalanceWithStateOverride, @@ -263,7 +263,7 @@ import { getPricePercentageWithResetRebalanceFieldInfos, readPricePercentageWithResetRebalanceParamsFromStrategy, readRawPricePercentageWithResetRebalanceStateFromStrategy, -} from './rebalance_methods/pricePercentageWithResetRebalance'; +} from './rebalance_methods'; import { deserializeDriftRebalanceFromOnchainParams, deserializeDriftRebalanceWithStateOverride, @@ -272,7 +272,7 @@ import { getPositionRangeFromDriftParams, readDriftRebalanceParamsFromStrategy, readRawDriftRebalanceStateFromStrategy, -} from './rebalance_methods/driftRebalance'; +} from './rebalance_methods'; import { deserializeExpanderRebalanceWithStateOverride, deserializePeriodicRebalanceFromOnchainParams, @@ -311,6 +311,7 @@ import { getDefaultAutodriftRebalanceFieldInfos, getPositionRangeFromAutodriftParams, } from './rebalance_methods/autodriftRebalance'; +import { EnrichedScopePrice, KaminoPrices, OraclePricesAndCollateralInfos } from './models/EnrichedScopePrice'; export const KAMINO_IDL = KaminoIdl; export class Kamino { @@ -324,7 +325,6 @@ export class Kamino { private readonly _kaminoProgramId: PublicKey; private readonly _orcaService: OrcaService; private readonly _raydiumService: RaydiumService; - private readonly _jupService: JupService; /** * Create a new instance of the Kamino SDK class. @@ -368,7 +368,6 @@ export class Kamino { this._orcaService = new OrcaService(connection, cluster, this._globalConfig); this._raydiumService = new RaydiumService(connection, cluster); - this._jupService = new JupService(connection, cluster); } getConnection = () => this._connection; @@ -1707,7 +1706,111 @@ export class Kamino { }; }; - getAllPrices = (): Promise => this._scope.getOraclePrices(); + getAllOraclePrices = (): Promise => this._scope.getOraclePrices(); + + /** + * Get all Kamino token spot and twap prices + * @param oraclePrices (optional) Scope Oracle prices + * @param collateralInfos (optional) Kamino Collateral Infos + */ + getAllPrices = async (oraclePrices?: OraclePrices, collateralInfos?: CollateralInfo[]): Promise => { + const spotPrices: EnrichedScopePrice[] = []; + const twaps: EnrichedScopePrice[] = []; + ({ oraclePrices, collateralInfos } = await this.getOraclePricesAndCollateralInfos(oraclePrices, collateralInfos)); + for (const collateralInfo of collateralInfos) { + if (collateralInfo.scopePriceChain.some((x) => x > 0)) { + const spotPrice = await this._scope.getPriceFromChain(collateralInfo.scopePriceChain, oraclePrices); + spotPrices.push({ + price: spotPrice, + mint: collateralInfo.mint, + name: getTokenNameFromCollateralInfo(collateralInfo), + }); + + if (collateralInfo.scopeTwapPriceChain.some((x) => x > 0)) { + const twap = await this._scope.getPriceFromChain(collateralInfo.scopeTwapPriceChain, oraclePrices); + twaps.push({ price: twap, mint: collateralInfo.mint, name: getTokenNameFromCollateralInfo(collateralInfo) }); + } + } + } + return { spot: spotPrices, twap: twaps }; + }; + + private async getOraclePricesAndCollateralInfos( + oraclePrices?: OraclePrices, + collateralInfos?: CollateralInfo[] + ): Promise { + if (!oraclePrices) { + oraclePrices = await this.getAllOraclePrices(); + } + if (!collateralInfos) { + collateralInfos = await this.getCollateralInfos(); + } + return { oraclePrices, collateralInfos }; + } + + /** + * Get Kamino token spot price by mint + * @param mint token mint + * @param oraclePrices (optional) Scope Oracle prices + * @param collateralInfos (optional) Kamino Collateral Infos + */ + getPriceByMint = async ( + mint: PublicKey | string, + oraclePrices?: OraclePrices, + collateralInfos?: CollateralInfo[] + ): Promise => { + return (await this.getAllPrices(oraclePrices, collateralInfos)).spot.find( + (x) => x.mint.toString() === mint.toString() + ); + }; + + /** + * Get Kamino token TWAP by token mint + * @param mint token mint + * @param oraclePrices (optional) Scope Oracle prices + * @param collateralInfos (optional) Kamino Collateral Infos + */ + getTwapByMint = async ( + mint: PublicKey | string, + oraclePrices?: OraclePrices, + collateralInfos?: CollateralInfo[] + ): Promise => { + return (await this.getAllPrices(oraclePrices, collateralInfos)).twap.find( + (x) => x.mint.toString() === mint.toString() + ); + }; + + /** + * Get multiple Kamino token spot price by mint + * @param mints token mint + * @param oraclePrices (optional) Scope Oracle prices + * @param collateralInfos (optional) Kamino Collateral Infos + */ + getPricesByMints = async ( + mints: (PublicKey | string)[], + oraclePrices?: OraclePrices, + collateralInfos?: CollateralInfo[] + ): Promise<(EnrichedScopePrice | undefined)[]> => { + return (await this.getAllPrices(oraclePrices, collateralInfos)).spot.filter((x) => + mints.some((y) => y.toString() === x.mint.toString()) + ); + }; + + /** + * Get multiple Kamino token TWAP by token mint + * @param mints token mints + * @param oraclePrices (optional) Scope Oracle prices + * @param collateralInfos (optional) Kamino Collateral Infos + */ + getTwapsByMints = async ( + mints: (PublicKey | string)[], + oraclePrices?: OraclePrices, + collateralInfos?: CollateralInfo[] + ): Promise<(EnrichedScopePrice | undefined)[]> => { + return (await this.getAllPrices(oraclePrices, collateralInfos)).twap.filter((x) => + mints.some((y) => y.toString() === x.mint.toString()) + ); + }; /** * Get the prices of all tokens in the specified strategy, or null if the reward token does not exist @@ -4952,7 +5055,7 @@ export class Kamino { }; } if (isOrca) { - const prices = await this.getAllPrices(); + const prices = await this.getAllOraclePrices(); return this._orcaService.getStrategyWhirlpoolPoolAprApy(strategyState, orcaPools, prices); } if (isRaydium) { diff --git a/packages/kamino-sdk/src/models/EnrichedScopePrice.ts b/packages/kamino-sdk/src/models/EnrichedScopePrice.ts new file mode 100644 index 00000000..d30e9d71 --- /dev/null +++ b/packages/kamino-sdk/src/models/EnrichedScopePrice.ts @@ -0,0 +1,29 @@ +import Decimal from 'decimal.js'; +import { PublicKey } from '@solana/web3.js'; +import { OraclePrices } from '@hubbleprotocol/scope-sdk'; +import { CollateralInfo } from '../kamino-client/types'; + +export interface EnrichedScopePrice { + /** + * Price in USD + */ + price: Decimal; + /** + * Token mint + */ + mint: PublicKey; + /** + * Token name (as specified in collateral infos) + */ + name: string; +} + +export interface KaminoPrices { + spot: EnrichedScopePrice[]; + twap: EnrichedScopePrice[]; +} + +export interface OraclePricesAndCollateralInfos { + oraclePrices: OraclePrices; + collateralInfos: CollateralInfo[]; +} diff --git a/packages/kamino-sdk/tests/kamino.test.ts b/packages/kamino-sdk/tests/kamino.test.ts index c1099904..bda343b6 100644 --- a/packages/kamino-sdk/tests/kamino.test.ts +++ b/packages/kamino-sdk/tests/kamino.test.ts @@ -313,6 +313,59 @@ describe('Kamino SDK Tests', () => { expect(init).to.throw(Error); }); + it('should get all Kamino prices', async () => { + const kamino = new Kamino( + cluster, + connection, + fixtures.globalConfig, + fixtures.kaminoProgramId, + WHIRLPOOL_PROGRAM_ID, + LOCAL_RAYDIUM_PROGRAM_ID + ); + const prices = await kamino.getAllPrices(); + expect(prices).not.to.be.undefined; + expect(prices.spot).to.have.length(2); + + expect(prices.twap).to.have.length(2); + + const usdh = prices.spot.find((x) => x.mint.toString() === fixtures.newTokenMintA.toString()); + const usdc = prices.spot.find((x) => x.mint.toString() === fixtures.newTokenMintB.toString()); + const usdhTwap = prices.twap.find((x) => x.mint.toString() === fixtures.newTokenMintA.toString()); + const usdcTwap = prices.twap.find((x) => x.mint.toString() === fixtures.newTokenMintB.toString()); + expect(usdh).not.to.be.undefined; + expect(usdh!.name).to.be.equal('USDH'); + expect(usdh!.price.toNumber()).to.be.greaterThan(0); + expect(usdc).not.to.be.undefined; + expect(usdc!.name).to.be.equal('USDC'); + expect(usdc!.price.toNumber()).to.be.greaterThan(0); + expect(usdhTwap).not.to.be.undefined; + expect(usdhTwap!.name).to.be.equal('USDH'); + expect(usdhTwap!.price.toNumber()).to.be.greaterThan(0); + expect(usdcTwap).not.to.be.undefined; + expect(usdcTwap!.name).to.be.equal('USDC'); + expect(usdcTwap!.price.toNumber()).to.be.greaterThan(0); + }); + + it('should get Kamino price and twap by mint', async () => { + const kamino = new Kamino( + cluster, + connection, + fixtures.globalConfig, + fixtures.kaminoProgramId, + WHIRLPOOL_PROGRAM_ID, + LOCAL_RAYDIUM_PROGRAM_ID + ); + const usdh = await kamino.getPriceByMint(fixtures.newTokenMintA); + expect(usdh).not.to.be.undefined; + expect(usdh!.name).to.be.equal('USDH'); + expect(usdh!.price.toNumber()).to.be.greaterThan(0); + + const usdhTwap = await kamino.getTwapByMint(fixtures.newTokenMintA); + expect(usdhTwap).not.to.be.undefined; + expect(usdhTwap!.name).to.be.equal('USDH'); + expect(usdhTwap!.price.toNumber()).to.be.greaterThan(0); + }); + it('should get all strategies', async () => { let kamino = new Kamino( cluster,