diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index 075d3bd..fe735af 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -5,7 +5,7 @@ import { resolveOrCreateATAs, TokenUtil, TransactionBuilder, - ZERO, + ZERO } from "@orca-so/common-sdk"; import { Address, BN, translateAddress } from "@project-serum/anchor"; import { Keypair, PublicKey } from "@solana/web3.js"; @@ -21,7 +21,7 @@ import { openPositionIx, openPositionWithMetadataIx, SwapInput, - swapIx, + swapIx } from "../instructions"; import { decreaseLiquidityQuoteByLiquidityWithParams } from "../quotes/public"; import { TokenAccountInfo, TokenInfo, WhirlpoolData, WhirlpoolRewardInfo } from "../types/public"; @@ -436,7 +436,7 @@ export class WhirlpoolImpl implements Whirlpool { // Check if all the tick arrays have been initialized. const tickArrayAddresses = [input.tickArray0, input.tickArray1, input.tickArray2]; - const tickArrays = await this.fetcher.listTickArrays(tickArrayAddresses, true); + const tickArrays = await this.ctx.fetcher.listTickArrays(tickArrayAddresses, true); const uninitializedIndices = TickArrayUtil.getUninitializedArrays(tickArrays); if (uninitializedIndices.length > 0) { const uninitializedArrays = uninitializedIndices diff --git a/sdk/src/instructions/composites/collect-all-txn.ts b/sdk/src/instructions/composites/collect-all-txn.ts index fa4ed53..b82d8b1 100644 --- a/sdk/src/instructions/composites/collect-all-txn.ts +++ b/sdk/src/instructions/composites/collect-all-txn.ts @@ -3,7 +3,6 @@ import { createWSOLAccountInstructions } from "@orca-so/common-sdk/dist/helpers/ import { Address } from "@project-serum/anchor"; import { NATIVE_MINT } from "@solana/spl-token"; import { PACKET_DATA_SIZE, PublicKey } from "@solana/web3.js"; -import { updateFeesAndRewardsIx } from ".."; import { WhirlpoolContext } from "../.."; import { PositionImpl } from "../../impl/position-impl"; import { WhirlpoolIx } from "../../ix"; @@ -11,8 +10,9 @@ import { WhirlpoolData } from "../../types/public"; import { PDAUtil, PoolUtil, TickUtil } from "../../utils/public"; import { getAssociatedTokenAddressSync } from "../../utils/spl-token-utils"; import { convertListToMap } from "../../utils/txn-utils"; +import { getTokenMintsFromWhirlpools, resolveAtaForMints } from "../../utils/whirlpool-ata-utils"; import { Position } from "../../whirlpool-client"; -import { resolveAtaForWhirlpoolsIxs } from "./resolve-atas-ix"; +import { updateFeesAndRewardsIx } from "../update-fees-and-rewards-ix"; /** * Parameters to collect all fees and rewards from a list of positions. @@ -102,23 +102,20 @@ export async function collectAllForPositionsTxns( const whirlpoolDatas = await ctx.fetcher.listPools(whirlpoolAddrs, false); const whirlpools = convertListToMap(whirlpoolDatas, whirlpoolAddrs); - // TODO: Payer is not configurable here. Forced to use wallet + const accountExemption = await ctx.fetcher.getAccountRentExempt(); const { ataTokenAddresses: affliatedTokenAtaMap, resolveAtaIxs } = - await resolveAtaForWhirlpoolsIxs(ctx, { - whirlpools: whirlpoolDatas, + await resolveAtaForMints(ctx, { + mints: getTokenMintsFromWhirlpools(whirlpoolDatas), + accountExemption, receiver: receiverKey, payer: payerKey, }); const latestBlockhash = await ctx.connection.getLatestBlockhash("singleGossip"); - const accountExemption = await ctx.fetcher.getAccountRentExempt(); const txBuilders: TransactionBuilder[] = []; - let pendingTxBuilder = new TransactionBuilder(ctx.connection, ctx.wallet).addInstructions( - resolveAtaIxs - ); + let pendingTxBuilder = new TransactionBuilder(ctx.connection, ctx.wallet).addInstructions(resolveAtaIxs); let pendingTxBuilderTxSize = await pendingTxBuilder.txnSize({ latestBlockhash }); - let posIndex = 0; let reattempt = false; diff --git a/sdk/src/instructions/composites/index.ts b/sdk/src/instructions/composites/index.ts index b5d5b5e..69bdc87 100644 --- a/sdk/src/instructions/composites/index.ts +++ b/sdk/src/instructions/composites/index.ts @@ -1,2 +1 @@ export * from "./collect-all-txn"; -export * from "./resolve-atas-ix"; diff --git a/sdk/src/instructions/composites/resolve-atas-ix.ts b/sdk/src/instructions/composites/resolve-atas-ix.ts deleted file mode 100644 index 2c38964..0000000 --- a/sdk/src/instructions/composites/resolve-atas-ix.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Instruction, TokenUtil } from "@orca-so/common-sdk"; -import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; -import { WhirlpoolContext } from "../.."; -import { WhirlpoolData } from "../../types/public"; -import { getAssociatedTokenAddressSync } from "../../utils/spl-token-utils"; -import { convertListToMap } from "../../utils/txn-utils"; - -/** - * Parameters to resolve ATAs for affliated tokens in a list of Whirlpools - * - * @category Instruction Types - * @param whirlpools - The list of WhirlpoolData to generate affliated tokens for. - * @param destinationWallet - the wallet to generate ATAs against - * @param payer - The payer address that would pay for the creation of ATA addresses - */ -export type ResolveAtaInstructionParams = { - whirlpools: (WhirlpoolData | null)[]; - receiver?: PublicKey; - payer?: PublicKey; -}; - -/** - * An interface of mapping between tokenMint & ATA & the instruction set to initialize them. - * - * @category Instruction Types - * @param ataTokenAddresses - A record between the token mint & generated ATA addresses - * @param resolveAtaIxs - An array of instructions to initialize all uninitialized ATA token accounts for the list above. - */ -export type ResolvedATAInstructionSet = { - ataTokenAddresses: Record; - resolveAtaIxs: Instruction[]; -}; - -/** - * Build instructions to resolve ATAs (Associated Tokens Addresses) for affliated tokens in a list of Whirlpools. - * Affliated tokens are tokens that are part of the trade pair or reward in a Whirlpool. - * - * SOL tokens does not use the ATA program and therefore not handled. - * - * @param ctx - WhirlpoolContext object for the current environment. - - * @returns a ResolvedTokenAddressesIxSet containing the derived ATA addresses & ix set to initialize the accounts. - */ -export async function resolveAtaForWhirlpoolsIxs( - ctx: WhirlpoolContext, - params: ResolveAtaInstructionParams -): Promise { - const { whirlpools, receiver, payer } = params; - const receiverKey = receiver ?? ctx.wallet.publicKey; - const payerKey = payer ?? ctx.wallet.publicKey; - const { affliatedTokenAtaMap, affliatedTokensInfoMap } = await getAffliatedTokenAtas( - ctx, - whirlpools, - receiverKey - ); - - const tokensRequiringAtaResolve = Object.fromEntries( - Object.entries(affliatedTokensInfoMap) - .filter(([, account]) => !account) - .map(([mint]) => [mint, affliatedTokenAtaMap[mint]]) - ); - - const resolveAtaIxs: Instruction[] = []; - - Object.entries(tokensRequiringAtaResolve).forEach(([mint, ataKey]) => { - const createAtaInstruction = Token.createAssociatedTokenAccountInstruction( - ASSOCIATED_TOKEN_PROGRAM_ID, - TOKEN_PROGRAM_ID, - new PublicKey(mint), - ataKey, - receiverKey, - payerKey - ); - resolveAtaIxs.push({ - instructions: [createAtaInstruction], - cleanupInstructions: [], - signers: [], - }); - }); - - return { - ataTokenAddresses: affliatedTokenAtaMap, - resolveAtaIxs, - }; -} - -async function getAffliatedTokenAtas( - ctx: WhirlpoolContext, - whirlpoolDatas: (WhirlpoolData | null)[], - wallet: PublicKey -) { - const affliatedTokens = Array.from( - whirlpoolDatas.reduce>((accu, whirlpoolData) => { - if (whirlpoolData) { - const { tokenMintA, tokenMintB } = whirlpoolData; - if (!TokenUtil.isNativeMint(tokenMintA)) { - accu.add(tokenMintA.toBase58()); - } - - if (!TokenUtil.isNativeMint(tokenMintB)) { - accu.add(tokenMintB.toBase58()); - } - - const rewardInfos = whirlpoolData.rewardInfos; - rewardInfos.forEach((reward) => { - if (!reward.mint.equals(PublicKey.default)) { - accu.add(reward.mint.toBase58()); - } - }); - } - return accu; - }, new Set()) - ); - - const tokenMintInfoMap = convertListToMap( - await ctx.fetcher.listMintInfos(affliatedTokens, false), - affliatedTokens - ); - - // Derive associated addresses for all affliated spl-tokens - const affliatedTokenAtaMap = Object.fromEntries( - Object.keys(tokenMintInfoMap).map((addr) => [ - addr, - getAssociatedTokenAddressSync(addr, wallet.toBase58()), - ]) - ); - - return { - affliatedTokenAtaMap: affliatedTokenAtaMap, - affliatedTokensInfoMap: convertListToMap( - await ctx.fetcher.listTokenInfos(Object.values(affliatedTokenAtaMap), false), - Object.keys(affliatedTokenAtaMap) - ), - }; -} diff --git a/sdk/src/utils/whirlpool-ata-utils.ts b/sdk/src/utils/whirlpool-ata-utils.ts new file mode 100644 index 0000000..7d68a4f --- /dev/null +++ b/sdk/src/utils/whirlpool-ata-utils.ts @@ -0,0 +1,118 @@ +import { Instruction, resolveOrCreateATAs, TokenUtil } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { WhirlpoolContext } from ".."; +import { WhirlpoolData } from "../types/public"; +import { convertListToMap } from "./txn-utils"; + +/** + * Fetch a list of affliated tokens from a list of whirlpools + * + * SOL tokens does not use the ATA program and therefore not handled. + * @param whirlpoolDatas An array of whirlpoolData (from fetcher.listPools) + * @returns All the whirlpool, reward token mints in the given set of whirlpools + */ +export function getTokenMintsFromWhirlpools(whirlpoolDatas: (WhirlpoolData | null)[]) { + return Array.from( + whirlpoolDatas.reduce>((accu, whirlpoolData) => { + if (whirlpoolData) { + const { tokenMintA, tokenMintB } = whirlpoolData; + if (!TokenUtil.isNativeMint(tokenMintA)) { + accu.add(tokenMintA); + } + + if (!TokenUtil.isNativeMint(tokenMintB)) { + accu.add(tokenMintB); + } + + const rewardInfos = whirlpoolData.rewardInfos; + rewardInfos.forEach((reward) => { + if (!reward.mint.equals(PublicKey.default)) { + accu.add(reward.mint); + } + }); + } + return accu; + }, new Set()) + ); +} + +/** + * Parameters to resolve ATAs for affliated tokens in a list of Whirlpools + * + * @category Instruction Types + * @param mints - The list of mints to generate affliated tokens for. + * @param accountExemption - The value from the most recent getMinimumBalanceForRentExemption(). + * @param destinationWallet - the wallet to generate ATAs against + * @param payer - The payer address that would pay for the creation of ATA addresses + */ +export type ResolveAtaInstructionParams = { + mints: PublicKey[]; + accountExemption: number; + receiver?: PublicKey; + payer?: PublicKey; +}; + +/** + * An interface of mapping between tokenMint & ATA & the instruction set to initialize them. + * + * @category Instruction Types + * @param ataTokenAddresses - A record between the token mint & generated ATA addresses + * @param resolveAtaIxs - An array of instructions to initialize all uninitialized ATA token accounts for the list above. + */ +export type ResolvedATAInstructionSet = { + ataTokenAddresses: Record; + resolveAtaIxs: Instruction[]; +}; + +/** + * Build instructions to resolve ATAs (Associated Tokens Addresses) for affliated tokens in a list of Whirlpools. + * Affliated tokens are tokens that are part of the trade pair or reward in a Whirlpool. + * + * @param ctx - WhirlpoolContext object for the current environment. + * @param params - ResolveAtaInstructionParams + * @returns a ResolvedTokenAddressesIxSet containing the derived ATA addresses & ix set to initialize the accounts. + */ +export async function resolveAtaForMints( + ctx: WhirlpoolContext, + params: ResolveAtaInstructionParams +): Promise { + const { mints, receiver, payer, accountExemption } = params; + const receiverKey = receiver ?? ctx.wallet.publicKey; + const payerKey = payer ?? ctx.wallet.publicKey; + + const resolvedAtaResults = await resolveOrCreateATAs( + ctx.connection, + receiverKey, + mints.map((tokenMint) => { + return { tokenMint }; + }), + async () => accountExemption, + payerKey + ); + + // Convert the results back into the specified format + const { resolveAtaIxs, resolvedAtas } = resolvedAtaResults.reduce<{ + resolvedAtas: PublicKey[]; + resolveAtaIxs: Instruction[]; + }>( + (accu, curr) => { + const { address, ...ix } = curr; + accu.resolvedAtas.push(address); + + // TODO: common-sdk needs to have an easier way to check for empty instruction + if (ix.instructions.length) { + accu.resolveAtaIxs.push(ix); + } + return accu; + }, + { resolvedAtas: [], resolveAtaIxs: [] } + ); + + const affliatedTokenAtaMap = convertListToMap(resolvedAtas, mints.map((mint) => mint.toBase58())); + return { + ataTokenAddresses: affliatedTokenAtaMap, + resolveAtaIxs, + }; +} + +