From bdb267efe28af911878b66c66e076b9215f1b88c Mon Sep 17 00:00:00 2001 From: meep <33380758+odcheung@users.noreply.github.com> Date: Sat, 14 Jan 2023 21:17:40 +0800 Subject: [PATCH] New SwapUtil. getSwapParamsFromQuote to make calling WhirlpoolIx.swap simpler (#76) \ --- sdk/src/impl/whirlpool-impl.ts | 86 ++++++------------- .../composites/collect-all-txn.ts | 1 - sdk/src/instructions/composites/index.ts | 1 + sdk/src/instructions/composites/swap-async.ts | 67 +++++++++++++++ sdk/src/instructions/swap-ix.ts | 26 ++---- sdk/src/ix.ts | 8 +- sdk/src/types/public/ix-types.ts | 8 +- sdk/src/utils/public/swap-utils.ts | 53 +++++++++++- sdk/src/utils/public/tick-utils.ts | 35 +++++++- 9 files changed, 190 insertions(+), 95 deletions(-) create mode 100644 sdk/src/instructions/composites/swap-async.ts diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index bbe7d46..f6ace8f 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -21,8 +21,8 @@ import { initTickArrayIx, openPositionIx, openPositionWithMetadataIx, + swapAsync, SwapInput, - swapIx, } from "../instructions"; import { collectFeesQuote, @@ -181,11 +181,19 @@ export class WhirlpoolImpl implements Whirlpool { ); } - async swap(quote: SwapInput, sourceWallet?: Address) { + async swap(quote: SwapInput, sourceWallet?: Address): Promise { const sourceWalletKey = sourceWallet ? AddressUtil.toPubKey(sourceWallet) : this.ctx.wallet.publicKey; - return this.getSwapTx(quote, sourceWalletKey); + return swapAsync( + this.ctx, + { + swapInput: quote, + whirlpool: this, + wallet: sourceWalletKey, + }, + true + ); } async swapWithDevFees( @@ -219,7 +227,19 @@ export class WhirlpoolImpl implements Whirlpool { ); } - return this.getSwapTx(quote, sourceWalletKey, txBuilder); + const swapTxBuilder = await swapAsync( + this.ctx, + { + swapInput: quote, + whirlpool: this, + wallet: sourceWalletKey, + }, + true + ); + + txBuilder.addInstruction(swapTxBuilder.compressIx(true)); + + return txBuilder; } /** @@ -549,64 +569,6 @@ export class WhirlpoolImpl implements Whirlpool { return txBuilders; } - private async getSwapTx( - input: SwapInput, - wallet: PublicKey, - initTxBuilder?: TransactionBuilder - ): Promise { - invariant(input.amount.gt(ZERO), "swap amount must be more than zero."); - - // Check if all the tick arrays have been initialized. - const tickArrayAddresses = [input.tickArray0, input.tickArray1, input.tickArray2]; - const tickArrays = await this.ctx.fetcher.listTickArrays(tickArrayAddresses, true); - const uninitializedIndices = TickArrayUtil.getUninitializedArrays(tickArrays); - if (uninitializedIndices.length > 0) { - const uninitializedArrays = uninitializedIndices - .map((index) => tickArrayAddresses[index].toBase58()) - .join(", "); - throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`); - } - - const { amount, aToB } = input; - const whirlpool = this.data; - const txBuilder = - initTxBuilder ?? - new TransactionBuilder(this.ctx.provider.connection, this.ctx.provider.wallet); - - const [ataA, ataB] = await resolveOrCreateATAs( - this.ctx.connection, - wallet, - [ - { tokenMint: whirlpool.tokenMintA, wrappedSolAmountIn: aToB ? amount : ZERO }, - { tokenMint: whirlpool.tokenMintB, wrappedSolAmountIn: !aToB ? amount : ZERO }, - ], - () => this.ctx.fetcher.getAccountRentExempt() - ); - - const { address: tokenOwnerAccountA, ...tokenOwnerAccountAIx } = ataA; - const { address: tokenOwnerAccountB, ...tokenOwnerAccountBIx } = ataB; - - txBuilder.addInstruction(tokenOwnerAccountAIx); - txBuilder.addInstruction(tokenOwnerAccountBIx); - - const oraclePda = PDAUtil.getOracle(this.ctx.program.programId, this.address); - - txBuilder.addInstruction( - swapIx(this.ctx.program, { - ...input, - whirlpool: this.address, - tokenAuthority: wallet, - tokenOwnerAccountA, - tokenVaultA: whirlpool.tokenVaultA, - tokenOwnerAccountB, - tokenVaultB: whirlpool.tokenVaultB, - oracle: oraclePda.publicKey, - }) - ); - - return txBuilder; - } - private async refresh() { const account = await this.ctx.fetcher.getPool(this.address, true); if (!!account) { diff --git a/sdk/src/instructions/composites/collect-all-txn.ts b/sdk/src/instructions/composites/collect-all-txn.ts index b75425a..ebc1c4f 100644 --- a/sdk/src/instructions/composites/collect-all-txn.ts +++ b/sdk/src/instructions/composites/collect-all-txn.ts @@ -1,5 +1,4 @@ import { Instruction, TokenUtil, TransactionBuilder, ZERO } from "@orca-so/common-sdk"; -import { createWSOLAccountInstructions } from "@orca-so/common-sdk/dist/helpers/token-instructions"; import { Address } from "@project-serum/anchor"; import { NATIVE_MINT } from "@solana/spl-token"; import { PACKET_DATA_SIZE, PublicKey } from "@solana/web3.js"; diff --git a/sdk/src/instructions/composites/index.ts b/sdk/src/instructions/composites/index.ts index e109aa8..0d0c5e0 100644 --- a/sdk/src/instructions/composites/index.ts +++ b/sdk/src/instructions/composites/index.ts @@ -1,2 +1,3 @@ export * from "./collect-all-txn"; export * from "./collect-protocol-fees"; +export * from "./swap-async"; diff --git a/sdk/src/instructions/composites/swap-async.ts b/sdk/src/instructions/composites/swap-async.ts new file mode 100644 index 0000000..bba7325 --- /dev/null +++ b/sdk/src/instructions/composites/swap-async.ts @@ -0,0 +1,67 @@ +import { resolveOrCreateATAs, TransactionBuilder, ZERO } from "@orca-so/common-sdk"; +import { PublicKey } from "@solana/web3.js"; +import { SwapUtils, TickArrayUtil, Whirlpool, WhirlpoolContext } from "../.."; +import { SwapInput, swapIx } from "../swap-ix"; + +export type SwapAsyncParams = { + swapInput: SwapInput; + whirlpool: Whirlpool; + wallet: PublicKey; +}; + +/** + * Swap instruction builder method with resolveATA & additional checks. + * @param ctx - WhirlpoolContext object for the current environment. + * @param params - {@link SwapAsyncParams} + * @param refresh - If true, the network calls will always fetch for the latest values. + * @returns + */ +export async function swapAsync( + ctx: WhirlpoolContext, + params: SwapAsyncParams, + refresh: boolean +): Promise { + const { wallet, whirlpool, swapInput } = params; + const { aToB, amount } = swapInput; + const txBuilder = new TransactionBuilder(ctx.connection, ctx.wallet); + const tickArrayAddresses = [swapInput.tickArray0, swapInput.tickArray1, swapInput.tickArray2]; + + let uninitializedArrays = await TickArrayUtil.getUninitializedArraysString( + tickArrayAddresses, + ctx.fetcher, + refresh + ); + if (uninitializedArrays) { + throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`); + } + + const data = whirlpool.getData(); + const [resolvedAtaA, resolvedAtaB] = await resolveOrCreateATAs( + ctx.connection, + wallet, + [ + { tokenMint: data.tokenMintA, wrappedSolAmountIn: aToB ? amount : ZERO }, + { tokenMint: data.tokenMintB, wrappedSolAmountIn: !aToB ? amount : ZERO }, + ], + () => ctx.fetcher.getAccountRentExempt() + ); + const { address: ataAKey, ...tokenOwnerAccountAIx } = resolvedAtaA; + const { address: ataBKey, ...tokenOwnerAccountBIx } = resolvedAtaB; + txBuilder.addInstructions([tokenOwnerAccountAIx, tokenOwnerAccountBIx]); + const inputTokenAccount = aToB ? ataAKey : ataBKey; + const outputTokenAccount = aToB ? ataBKey : ataAKey; + + return txBuilder.addInstruction( + swapIx( + ctx.program, + SwapUtils.getSwapParamsFromQuote( + swapInput, + ctx, + whirlpool, + inputTokenAccount, + outputTokenAccount, + wallet + ) + ) + ); +} diff --git a/sdk/src/instructions/swap-ix.ts b/sdk/src/instructions/swap-ix.ts index d261f3e..efdd240 100644 --- a/sdk/src/instructions/swap-ix.ts +++ b/sdk/src/instructions/swap-ix.ts @@ -5,18 +5,10 @@ import { PublicKey } from "@solana/web3.js"; import { Whirlpool } from "../artifacts/whirlpool"; /** - * Parameters and accounts to swap on a Whirlpool + * Raw parameters and accounts to swap on a Whirlpool * * @category Instruction Types - * @param aToB - The direction of the swap. True if swapping from A to B. False if swapping from B to A. - * @param amountSpecifiedIsInput - Specifies the token the parameter `amount`represents. If true, the amount represents - * the input token of the swap. - * @param amount - The amount of input or output token to swap from (depending on amountSpecifiedIsInput). - * @param otherAmountThreshold - The maximum/minimum of input/output token to swap into (depending on amountSpecifiedIsInput). - * @param sqrtPriceLimit - The maximum/minimum price the swap will swap to. - * @param tickArray0 - PublicKey of the tick-array where the Whirlpool's currentTickIndex resides in - * @param tickArray1 - The next tick-array in the swap direction. If the swap will not reach the next tick-aray, input the same array as tickArray0. - * @param tickArray2 - The next tick-array in the swap direction after tickArray2. If the swap will not reach the next tick-aray, input the same array as tickArray1. + * @param swapInput - Parameters in {@link SwapInput} * @param whirlpool - PublicKey for the whirlpool that the position will be opened for. * @param tokenOwnerAccountA - PublicKey for the associated token account for tokenA in the collection wallet * @param tokenOwnerAccountB - PublicKey for the associated token account for tokenB in the collection wallet @@ -36,7 +28,7 @@ export type SwapParams = SwapInput & { }; /** - * Parameters to swap on a Whirlpool + * Parameters that describe the nature of a swap on a Whirlpool. * * @category Instruction Types * @param aToB - The direction of the swap. True if swapping from A to B. False if swapping from B to A. @@ -64,15 +56,7 @@ export type SwapInput = { * Parameters to swap on a Whirlpool with developer fees * * @category Instruction Types - * @param aToB - The direction of the swap. True if swapping from A to B. False if swapping from B to A. - * @param amountSpecifiedIsInput - Specifies the token the parameter `amount`represents. If true, the amount represents - * the input token of the swap. - * @param amount - The amount of input or output token to swap from (depending on amountSpecifiedIsInput). - * @param otherAmountThreshold - The maximum/minimum of input/output token to swap into (depending on amountSpecifiedIsInput). - * @param sqrtPriceLimit - The maximum/minimum price the swap will swap to. - * @param tickArray0 - PublicKey of the tick-array where the Whirlpool's currentTickIndex resides in - * @param tickArray1 - The next tick-array in the swap direction. If the swap will not reach the next tick-aray, input the same array as tickArray0. - * @param tickArray2 - The next tick-array in the swap direction after tickArray2. If the swap will not reach the next tick-aray, input the same array as tickArray1. + * @param swapInput - Parameters in {@link SwapInput} * @param devFeeAmount - FeeAmount (developer fees) charged on this swap */ export type DevFeeSwapInput = SwapInput & { @@ -95,7 +79,7 @@ export type DevFeeSwapInput = SwapInput & { * ### Parameters * @category Instructions * @param context - Context object containing services required to generate the instruction - * @param params - SwapParams object + * @param params - {@link SwapParams} * @returns - Instruction to perform the action. */ export function swapIx(program: Program, params: SwapParams): Instruction { diff --git a/sdk/src/ix.ts b/sdk/src/ix.ts index ebedb65..1c765ba 100644 --- a/sdk/src/ix.ts +++ b/sdk/src/ix.ts @@ -5,7 +5,7 @@ import { Whirlpool } from "./artifacts/whirlpool"; import * as ix from "./instructions"; /** - * Instruction set for the Whirlpools program. + * Instruction builders for the Whirlpools program. * * @category Core */ @@ -182,7 +182,7 @@ export class WhirlpoolIx { * * ### Parameters * @param program - program object containing services required to generate the instruction - * @param params - SwapParams object + * @param params - {@link SwapParams} * @returns - Instruction to perform the action. */ public static swapIx(program: Program, params: ix.SwapParams) { @@ -418,14 +418,16 @@ export class WhirlpoolIx { } /** + * DEPRECATED - use ${@link WhirlpoolClient} collectFeesAndRewardsForPositions function * A set of transactions to collect all fees and rewards from a list of positions. * + * @deprecated * @param ctx - WhirlpoolContext object for the current environment. * @param params - CollectAllPositionAddressParams object. * @param refresh - if true, will always fetch for the latest values on chain to compute. * @returns */ - public static collectAllForPositionsTxns( + public static async collectAllForPositionsTxns( ctx: WhirlpoolContext, params: ix.CollectAllPositionAddressParams, refresh: boolean diff --git a/sdk/src/types/public/ix-types.ts b/sdk/src/types/public/ix-types.ts index 3cd771a..4b76e93 100644 --- a/sdk/src/types/public/ix-types.ts +++ b/sdk/src/types/public/ix-types.ts @@ -1,8 +1,5 @@ export { ClosePositionParams, - CollectAllParams, - CollectAllPositionAddressParams, - CollectAllPositionParams, CollectFeesParams, CollectProtocolFeesParams, CollectRewardParams, @@ -31,3 +28,8 @@ export { SwapParams, UpdateFeesAndRewardsParams, } from "../../instructions/"; +export { + CollectAllParams, + CollectAllPositionAddressParams, + CollectAllPositionParams, +} from "../../instructions/composites"; diff --git a/sdk/src/utils/public/swap-utils.ts b/sdk/src/utils/public/swap-utils.ts index 0d50124..956fbbd 100644 --- a/sdk/src/utils/public/swap-utils.ts +++ b/sdk/src/utils/public/swap-utils.ts @@ -1,15 +1,19 @@ -import { ZERO, U64_MAX, Percentage } from "@orca-so/common-sdk"; +import { AddressUtil, Percentage, U64_MAX, ZERO } from "@orca-so/common-sdk"; +import { Address } from "@project-serum/anchor"; import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; +import { WhirlpoolContext } from "../.."; import { AccountFetcher } from "../../network/public"; import { - MIN_SQRT_PRICE, MAX_SQRT_PRICE, - WhirlpoolData, MAX_SWAP_TICK_ARRAYS, - TickArray, + MIN_SQRT_PRICE, SwapInput, + SwapParams, + TickArray, + WhirlpoolData, } from "../../types/public"; +import { Whirlpool } from "../../whirlpool-client"; import { adjustForSlippage } from "../math/token-math"; import { PDAUtil } from "./pda-utils"; import { PoolUtil } from "./pool-utils"; @@ -166,4 +170,45 @@ export class SwapUtils { }; } } + + /** + * Convert a quote object and WhirlpoolClient's {@link Whirlpool} object into a {@link SwapParams} type + * to be plugged into {@link WhirlpoolIx.swapIx}. + * + * @param quote - A {@link SwapQuote} type generated from {@link swapQuoteWithParams} + * @param ctx - {@link WhirlpoolContext} + * @param whirlpool - A {@link Whirlpool} object from WhirlpoolClient + * @param inputTokenAssociatedAddress - The public key for the ATA of the input token in the swap + * @param outputTokenAssociatedAddress - The public key for the ATA of the input token in the swap + * @param wallet - The token authority for this swap + * @returns A converted {@link SwapParams} generated from the input + */ + public static getSwapParamsFromQuote( + quote: SwapInput, + ctx: WhirlpoolContext, + whirlpool: Whirlpool, + inputTokenAssociatedAddress: Address, + outputTokenAssociatedAddress: Address, + wallet: PublicKey + ) { + const addr = whirlpool.getAddress(); + const data = whirlpool.getData(); + const aToB = quote.aToB; + const [inputTokenATA, outputTokenATA] = AddressUtil.toPubKeys([ + inputTokenAssociatedAddress, + outputTokenAssociatedAddress, + ]); + const oraclePda = PDAUtil.getOracle(ctx.program.programId, addr); + const params: SwapParams = { + whirlpool: whirlpool.getAddress(), + tokenOwnerAccountA: aToB ? inputTokenATA : outputTokenATA, + tokenOwnerAccountB: aToB ? outputTokenATA : inputTokenATA, + tokenVaultA: data.tokenVaultA, + tokenVaultB: data.tokenVaultB, + oracle: oraclePda.publicKey, + tokenAuthority: wallet, + ...quote, + }; + return params; + } } diff --git a/sdk/src/utils/public/tick-utils.ts b/sdk/src/utils/public/tick-utils.ts index b7b4007..cec939d 100644 --- a/sdk/src/utils/public/tick-utils.ts +++ b/sdk/src/utils/public/tick-utils.ts @@ -1,4 +1,5 @@ -import { PDA } from "@orca-so/common-sdk"; +import { AddressUtil, PDA } from "@orca-so/common-sdk"; +import { Address } from "@project-serum/anchor"; import { PublicKey } from "@solana/web3.js"; import invariant from "tiny-invariant"; import { AccountFetcher } from "../../network/public"; @@ -217,6 +218,38 @@ export class TickArrayUtil { }); } + /** + * Return a string containing all of the uninitialized arrays in the provided addresses. + * Useful for creating error messages. + * + * @param tickArrayAddrs - A list of tick-array addresses to verify. + * @param fetcher - {@link AccountFetcher} + * @param refresh - If true, always fetch the latest on-chain data + * @returns A string of all uninitialized tick array addresses, delimited by ",". Falsy value if all arrays are initialized. + */ + public static async getUninitializedArraysString( + tickArrayAddrs: Address[], + fetcher: AccountFetcher, + refresh: boolean + ) { + const taAddrs = AddressUtil.toPubKeys(tickArrayAddrs); + const tickArrayData = await fetcher.listTickArrays(taAddrs, refresh); + + // Verify tick arrays are initialized if the user provided them. + if (tickArrayData) { + const uninitializedIndices = TickArrayUtil.getUninitializedArrays(tickArrayData); + if (uninitializedIndices.length > 0) { + const uninitializedArrays = uninitializedIndices + .map((index) => taAddrs[index].toBase58()) + .join(", "); + + return uninitializedArrays; + } + } + + return null; + } + public static async getUninitializedArraysPDAs( ticks: number[], programId: PublicKey,