diff --git a/sdk/src/impl/util.ts b/sdk/src/impl/util.ts new file mode 100644 index 0000000..78ebf4c --- /dev/null +++ b/sdk/src/impl/util.ts @@ -0,0 +1,76 @@ +import BN from "bn.js"; +import { AccountFetcher, PoolUtil, TokenInfo } from ".."; +import { + WhirlpoolData, + WhirlpoolRewardInfo, + WhirlpoolRewardInfoData, + TokenAccountInfo, +} from "../types/public"; + +export async function getTokenMintInfos( + fetcher: AccountFetcher, + data: WhirlpoolData, + refresh: boolean +): Promise { + const mintA = data.tokenMintA; + const infoA = await fetcher.getMintInfo(mintA, refresh); + if (!infoA) { + throw new Error(`Unable to fetch MintInfo for mint - ${mintA}`); + } + const mintB = data.tokenMintB; + const infoB = await fetcher.getMintInfo(mintB, refresh); + if (!infoB) { + throw new Error(`Unable to fetch MintInfo for mint - ${mintB}`); + } + return [ + { mint: mintA, ...infoA }, + { mint: mintB, ...infoB }, + ]; +} + +export async function getRewardInfos( + fetcher: AccountFetcher, + data: WhirlpoolData, + refresh: boolean +): Promise { + const rewardInfos: WhirlpoolRewardInfo[] = []; + for (const rewardInfo of data.rewardInfos) { + rewardInfos.push(await getRewardInfo(fetcher, rewardInfo, refresh)); + } + return rewardInfos; +} + +async function getRewardInfo( + fetcher: AccountFetcher, + data: WhirlpoolRewardInfoData, + refresh: boolean +): Promise { + const rewardInfo = { ...data, initialized: false, vaultAmount: new BN(0) }; + if (PoolUtil.isRewardInitialized(data)) { + const vaultInfo = await fetcher.getTokenInfo(data.vault, refresh); + if (!vaultInfo) { + throw new Error(`Unable to fetch TokenAccountInfo for vault - ${data.vault}`); + } + rewardInfo.initialized = true; + rewardInfo.vaultAmount = vaultInfo.amount; + } + return rewardInfo; +} + +export async function getTokenVaultAccountInfos( + fetcher: AccountFetcher, + data: WhirlpoolData, + refresh: boolean +): Promise { + const vaultA = data.tokenVaultA; + const vaultInfoA = await fetcher.getTokenInfo(vaultA, refresh); + if (!vaultInfoA) { + throw new Error(`Unable to fetch TokenAccountInfo for vault - ${vaultA}`); + } + const vaultB = data.tokenVaultB; + const vaultInfoB = await fetcher.getTokenInfo(vaultB, refresh); + if (!vaultInfoB) { + throw new Error(`Unable to fetch TokenAccountInfo for vault - ${vaultB}`); + } + return [vaultInfoA, vaultInfoB]; +} diff --git a/sdk/src/impl/whirlpool-client-impl.ts b/sdk/src/impl/whirlpool-client-impl.ts index e8de60b..e94c6fa 100644 --- a/sdk/src/impl/whirlpool-client-impl.ts +++ b/sdk/src/impl/whirlpool-client-impl.ts @@ -2,9 +2,11 @@ import { AddressUtil } from "@orca-so/common-sdk"; import { Address } from "@project-serum/anchor"; import { WhirlpoolContext } from "../context"; import { AccountFetcher } from "../network/public"; -import { WhirlpoolData, TokenInfo } from "../types/public"; +import { WhirlpoolData } from "../types/public"; +import { PoolUtil } from "../utils/public"; import { WhirlpoolClient, Whirlpool, Position } from "../whirlpool-client"; import { PositionImpl } from "./position-impl"; +import { getRewardInfos, getTokenMintInfos, getTokenVaultAccountInfos } from "./util"; import { WhirlpoolImpl } from "./whirlpool-impl"; export class WhirlpoolClientImpl implements WhirlpoolClient { @@ -23,17 +25,69 @@ export class WhirlpoolClientImpl implements WhirlpoolClient { if (!account) { throw new Error(`Unable to fetch Whirlpool at address at ${poolAddress}`); } - const tokenInfos = await getTokenInfos(this.ctx.fetcher, account, false); + const tokenInfos = await getTokenMintInfos(this.ctx.fetcher, account, refresh); + const vaultInfos = await getTokenVaultAccountInfos(this.ctx.fetcher, account, refresh); + const rewardInfos = await getRewardInfos(this.ctx.fetcher, account, refresh); return new WhirlpoolImpl( this.ctx, this.ctx.fetcher, AddressUtil.toPubKey(poolAddress), tokenInfos[0], tokenInfos[1], + vaultInfos[0], + vaultInfos[1], + rewardInfos, account ); } + public async getPools(poolAddresses: Address[], refresh = false): Promise { + const accounts = (await this.ctx.fetcher.listPools(poolAddresses, refresh)).filter( + (account): account is WhirlpoolData => !!account + ); + if (accounts.length !== poolAddresses.length) { + throw new Error(`Unable to fetch all Whirlpools at addresses ${poolAddresses}`); + } + const tokenMints = new Set(); + const tokenAccounts = new Set(); + accounts.forEach((account) => { + tokenMints.add(account.tokenMintA.toBase58()); + tokenMints.add(account.tokenMintB.toBase58()); + tokenAccounts.add(account.tokenVaultA.toBase58()); + tokenAccounts.add(account.tokenVaultB.toBase58()); + account.rewardInfos.forEach((rewardInfo) => { + if (PoolUtil.isRewardInitialized(rewardInfo)) { + tokenAccounts.add(rewardInfo.vault.toBase58()); + } + }); + }); + await this.ctx.fetcher.listMintInfos(Array.from(tokenMints), refresh); + await this.ctx.fetcher.listTokenInfos(Array.from(tokenAccounts), refresh); + + const whirlpools: Whirlpool[] = []; + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i]; + const poolAddress = poolAddresses[i]; + const tokenInfos = await getTokenMintInfos(this.ctx.fetcher, account, false); + const vaultInfos = await getTokenVaultAccountInfos(this.ctx.fetcher, account, false); + const rewardInfos = await getRewardInfos(this.ctx.fetcher, account, false); + whirlpools.push( + new WhirlpoolImpl( + this.ctx, + this.ctx.fetcher, + AddressUtil.toPubKey(poolAddress), + tokenInfos[0], + tokenInfos[1], + vaultInfos[0], + vaultInfos[1], + rewardInfos, + account + ) + ); + } + return whirlpools; + } + public async getPosition(positionAddress: Address, refresh = false): Promise { const account = await this.ctx.fetcher.getPosition(positionAddress, refresh); if (!account) { @@ -47,24 +101,3 @@ export class WhirlpoolClientImpl implements WhirlpoolClient { ); } } - -async function getTokenInfos( - fetcher: AccountFetcher, - data: WhirlpoolData, - refresh: boolean -): Promise { - const mintA = data.tokenMintA; - const infoA = await fetcher.getMintInfo(mintA, refresh); - if (!infoA) { - throw new Error(`Unable to fetch MintInfo for mint - ${mintA}`); - } - const mintB = data.tokenMintB; - const infoB = await fetcher.getMintInfo(mintB, refresh); - if (!infoB) { - throw new Error(`Unable to fetch MintInfo for mint - ${mintB}`); - } - return [ - { mint: mintA, ...infoA }, - { mint: mintB, ...infoB }, - ]; -} diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index 75aa69e..0e36f20 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -19,13 +19,14 @@ import { swapIx, SwapInput, } from "../instructions"; -import { TokenInfo, WhirlpoolData } from "../types/public"; +import { TokenAccountInfo, TokenInfo, WhirlpoolData, WhirlpoolRewardInfo } from "../types/public"; import { Whirlpool } from "../whirlpool-client"; import { PublicKey, Keypair } from "@solana/web3.js"; import { AccountFetcher } from "../network/public"; import invariant from "tiny-invariant"; -import { PDAUtil, PriceMath, TickArrayUtil, TickUtil } from "../utils/public"; +import { PDAUtil, TickArrayUtil, TickUtil } from "../utils/public"; import { decreaseLiquidityQuoteByLiquidityWithParams, SwapQuote } from "../quotes/public"; +import { getRewardInfos, getTokenVaultAccountInfos } from "./util"; export class WhirlpoolImpl implements Whirlpool { private data: WhirlpoolData; @@ -35,6 +36,9 @@ export class WhirlpoolImpl implements Whirlpool { readonly address: PublicKey, readonly tokenAInfo: TokenInfo, readonly tokenBInfo: TokenInfo, + private tokenVaultAInfo: TokenAccountInfo, + private tokenVaultBInfo: TokenAccountInfo, + private rewardInfos: WhirlpoolRewardInfo[], data: WhirlpoolData ) { this.data = data; @@ -56,6 +60,18 @@ export class WhirlpoolImpl implements Whirlpool { return this.tokenBInfo; } + getTokenVaultAInfo(): TokenAccountInfo { + return this.tokenVaultAInfo; + } + + getTokenVaultBInfo(): TokenAccountInfo { + return this.tokenVaultBInfo; + } + + getRewardInfos(): WhirlpoolRewardInfo[] { + return this.rewardInfos; + } + async refreshData() { await this.refresh(); return this.data; @@ -422,7 +438,16 @@ export class WhirlpoolImpl implements Whirlpool { private async refresh() { const account = await this.fetcher.getPool(this.address, true); if (!!account) { + const rewardInfos = await getRewardInfos(this.fetcher, account, true); + const [tokenVaultAInfo, tokenVaultBInfo] = await getTokenVaultAccountInfos( + this.fetcher, + account, + true + ); this.data = account; + this.tokenVaultAInfo = tokenVaultAInfo; + this.tokenVaultBInfo = tokenVaultBInfo; + this.rewardInfos = rewardInfos; } } } diff --git a/sdk/src/types/public/client-types.ts b/sdk/src/types/public/client-types.ts index 809e47c..48cc320 100644 --- a/sdk/src/types/public/client-types.ts +++ b/sdk/src/types/public/client-types.ts @@ -1,6 +1,6 @@ import { PublicKey } from "@solana/web3.js"; -import { MintInfo } from "@solana/spl-token"; -import { TickArrayData } from "./anchor-types"; +import { AccountInfo, MintInfo, u64 } from "@solana/spl-token"; +import { TickArrayData, WhirlpoolRewardInfoData } from "./anchor-types"; /** * Extended MintInfo class to host token info. @@ -8,6 +8,13 @@ import { TickArrayData } from "./anchor-types"; */ export type TokenInfo = MintInfo & { mint: PublicKey }; +export type TokenAccountInfo = AccountInfo; + +export type WhirlpoolRewardInfo = WhirlpoolRewardInfoData & { + initialized: boolean; + vaultAmount: u64; +}; + /** * A wrapper class of a TickArray on a Whirlpool * @category WhirlpoolClient diff --git a/sdk/src/whirlpool-client.ts b/sdk/src/whirlpool-client.ts index fe7865c..59a828f 100644 --- a/sdk/src/whirlpool-client.ts +++ b/sdk/src/whirlpool-client.ts @@ -11,7 +11,7 @@ import { PositionData, WhirlpoolData, } from "./types/public"; -import { TokenInfo } from "./types/public/client-types"; +import { TokenAccountInfo, TokenInfo, WhirlpoolRewardInfo } from "./types/public/client-types"; /** * Helper class to help interact with Whirlpool Accounts with a simpler interface. @@ -38,6 +38,13 @@ export interface WhirlpoolClient { */ getPool: (poolAddress: Address, refresh?: boolean) => Promise; + /** + * Get a list of Whirlpool objects matching the provided list of addresses. + * @param poolAddresses the addresses of the Whirlpool accounts + * @return a list of Whirlpool objects to interact with + */ + getPools: (poolAddresses: Address[], refresh?: boolean) => Promise; + /** * Get a Position object to interact with the Position account at the given address. * @param positionAddress the address of the Position account @@ -92,6 +99,24 @@ export interface Whirlpool { */ getTokenBInfo: () => TokenInfo; + /** + * Get the TokenAccountInfo for token vault A of this pool. + * @return TokenAccountInfo for token vault A + */ + getTokenVaultAInfo: () => TokenAccountInfo; + + /** + * Get the TokenAccountInfo for token vault B of this pool. + * @return TokenAccountInfo for token vault B + */ + getTokenVaultBInfo: () => TokenAccountInfo; + + /** + * Get the WhirlpoolRewardInfos for this pool. + * @return Array of 3 WhirlpoolRewardInfos. However, not all of them may be initialized. Use the initialized field on WhirlpoolRewardInfo to check if the reward is active. + */ + getRewardInfos: () => WhirlpoolRewardInfo[]; + /** * Initialize a set of tick-arrays that encompasses the provided ticks. *