diff --git a/sdk/package.json b/sdk/package.json index e0708c9..d60c36a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -7,14 +7,14 @@ "types": "dist/index.d.ts", "dependencies": { "@metaplex-foundation/mpl-token-metadata": "1.2.5", - "@orca-so/common-sdk": "~0.1.0", + "@orca-so/common-sdk": "~0.1.1", "@project-serum/anchor": "~0.25.0", "@solana/spl-token": "^0.1.8", "decimal.js": "^10.3.1", "tiny-invariant": "^1.2.0" }, "devDependencies": { - "@types/bn.js": "^5.1.0", + "@types/bn.js": "~5.1.0", "@types/decimal.js": "^7.4.0", "@types/jest": "^26.0.24", "@types/mocha": "^9.0.0", diff --git a/sdk/src/errors/errors.ts b/sdk/src/errors/errors.ts index 740e7cc..3e44149 100644 --- a/sdk/src/errors/errors.ts +++ b/sdk/src/errors/errors.ts @@ -11,6 +11,7 @@ export enum TokenErrorCode { } export enum SwapErrorCode { + InvalidDevFeePercentage = `InvalidDevFeePercentage`, InvalidSqrtPriceLimitDirection = `InvalidSqrtPriceLimitDirection`, SqrtPriceOutOfBounds = `SqrtPriceOutOfBounds`, ZeroTradableAmount = `ZeroTradableAmount`, diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index 0e36f20..be098e9 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -3,29 +3,31 @@ import { deriveATA, Percentage, resolveOrCreateATAs, + TokenUtil, TransactionBuilder, - ZERO, + ZERO } from "@orca-so/common-sdk"; -import { Address, translateAddress, BN } from "@project-serum/anchor"; +import { Address, BN, translateAddress } from "@project-serum/anchor"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import invariant from "tiny-invariant"; import { WhirlpoolContext } from "../context"; import { + closePositionIx, + decreaseLiquidityIx, + DevFeeSwapInput, IncreaseLiquidityInput, + increaseLiquidityIx, + initTickArrayIx, openPositionIx, openPositionWithMetadataIx, - initTickArrayIx, - increaseLiquidityIx, - decreaseLiquidityIx, - closePositionIx, - swapIx, SwapInput, + swapIx } from "../instructions"; -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 { decreaseLiquidityQuoteByLiquidityWithParams } from "../quotes/public"; +import { TokenAccountInfo, TokenInfo, WhirlpoolData, WhirlpoolRewardInfo } from "../types/public"; import { PDAUtil, TickArrayUtil, TickUtil } from "../utils/public"; -import { decreaseLiquidityQuoteByLiquidityWithParams, SwapQuote } from "../quotes/public"; +import { Whirlpool } from "../whirlpool-client"; import { getRewardInfos, getTokenVaultAccountInfos } from "./util"; export class WhirlpoolImpl implements Whirlpool { @@ -168,13 +170,47 @@ export class WhirlpoolImpl implements Whirlpool { ); } - async swap(quote: SwapQuote, sourceWallet?: Address) { + async swap(quote: SwapInput, sourceWallet?: Address) { const sourceWalletKey = sourceWallet ? AddressUtil.toPubKey(sourceWallet) : this.ctx.wallet.publicKey; return this.getSwapTx(quote, sourceWalletKey); } + async swapWithDevFees( + quote: DevFeeSwapInput, + devFeeWallet: PublicKey, + wallet?: PublicKey | undefined, + payer?: PublicKey | undefined + ): Promise { + const sourceWalletKey = wallet ? AddressUtil.toPubKey(wallet) : this.ctx.wallet.publicKey; + const payerKey = payer ? AddressUtil.toPubKey(payer) : this.ctx.wallet.publicKey; + const txBuilder = new TransactionBuilder( + this.ctx.provider.connection, + this.ctx.provider.wallet + ); + + if (!quote.devFeeAmount.eq(ZERO)) { + const inputToken = + quote.aToB === quote.amountSpecifiedIsInput ? this.getTokenAInfo() : this.getTokenBInfo(); + + txBuilder.addInstruction( + await TokenUtil.createSendTokensToWalletInstruction( + this.ctx.connection, + sourceWalletKey, + devFeeWallet, + inputToken.mint, + inputToken.decimals, + quote.devFeeAmount, + () => this.ctx.fetcher.getAccountRentExempt(), + payerKey + ) + ); + } + + return this.getSwapTx(quote, sourceWalletKey, txBuilder); + } + /** * Construct a transaction for opening an new position with optional metadata */ @@ -393,13 +429,17 @@ export class WhirlpoolImpl implements Whirlpool { return txBuilder; } - private async getSwapTx(input: SwapInput, wallet: PublicKey): Promise { + private async getSwapTx( + input: SwapInput, + wallet: PublicKey, + initTxBuilder?: TransactionBuilder + ): Promise { + invariant(input.amount.gt(ZERO), "swap amount must be more than zero."); const { amount, aToB } = input; const whirlpool = this.data; - const txBuilder = new TransactionBuilder( - this.ctx.provider.connection, - this.ctx.provider.wallet - ); + const txBuilder = + initTxBuilder ?? + new TransactionBuilder(this.ctx.provider.connection, this.ctx.provider.wallet); const [ataA, ataB] = await resolveOrCreateATAs( this.ctx.connection, diff --git a/sdk/src/instructions/swap-ix.ts b/sdk/src/instructions/swap-ix.ts index e813162..d261f3e 100644 --- a/sdk/src/instructions/swap-ix.ts +++ b/sdk/src/instructions/swap-ix.ts @@ -1,9 +1,8 @@ -import { TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; -import { Program } from "@project-serum/anchor"; -import { Whirlpool } from "../artifacts/whirlpool"; import { Instruction } from "@orca-so/common-sdk"; +import { BN, Program } from "@project-serum/anchor"; +import { TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; -import { BN } from "@project-serum/anchor"; +import { Whirlpool } from "../artifacts/whirlpool"; /** * Parameters and accounts to swap on a Whirlpool @@ -61,6 +60,25 @@ export type SwapInput = { tickArray2: PublicKey; }; +/** + * 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 devFeeAmount - FeeAmount (developer fees) charged on this swap + */ +export type DevFeeSwapInput = SwapInput & { + devFeeAmount: u64; +}; + /** * Perform a swap in this Whirlpool * diff --git a/sdk/src/quotes/public/dev-fee-swap-quote.ts b/sdk/src/quotes/public/dev-fee-swap-quote.ts new file mode 100644 index 0000000..5a34a96 --- /dev/null +++ b/sdk/src/quotes/public/dev-fee-swap-quote.ts @@ -0,0 +1,85 @@ +import { Percentage } from "@orca-so/common-sdk"; +import { Address } from "@project-serum/anchor"; +import { u64 } from "@solana/spl-token"; +import { AccountFetcher } from "../.."; +import { SwapErrorCode, WhirlpoolsError } from "../../errors/errors"; +import { Whirlpool } from "../../whirlpool-client"; +import { NormalSwapQuote, swapQuoteByInputToken } from "./swap-quote"; + +/** + * A collection of estimated values from quoting a swap that collects a developer-fee. + * @category Quotes + * @param estimatedAmountIn - Approximate number of input token swapped in the swap + * @param estimatedAmountOut - Approximate number of output token swapped in the swap + * @param estimatedEndTickIndex - Approximate tick-index the Whirlpool will land on after this swap + * @param estimatedEndSqrtPrice - Approximate sqrtPrice the Whirlpool will land on after this swap + * @param estimatedFeeAmount - Approximate feeAmount (all fees) charged on this swap + * @param estimatedSwapFeeAmount - Approximate feeAmount (LP + protocol fees) charged on this swap + * @param devFeeAmount - FeeAmount (developer fees) charged on this swap + */ +export type DevFeeSwapQuote = NormalSwapQuote & { + // NOTE: DevFeeSwaps supports input-token based swaps only as it is difficult + // to collect an exact % amount of dev-fees for output-token based swaps due to slippage. + // If there are third party requests in the future for this functionality, we can launch it + // but with the caveat that the % collected is only an estimate. + amountSpecifiedIsInput: true; + estimatedSwapFeeAmount: u64; + devFeeAmount: u64; +}; + +/** + * Get an estimated swap quote using input token amount while collecting dev fees. + * + * @category Quotes + * @param whirlpool - Whirlpool to perform the swap on + * @param inputTokenMint - PublicKey for the input token mint to swap with + * @param tokenAmount - The amount of input token to swap from + * @param slippageTolerance - The amount of slippage to account for in this quote + * @param programId - PublicKey for the Whirlpool ProgramId + * @param fetcher - AccountFetcher object to fetch solana accounts + * @param refresh - If true, fetcher would default to fetching the latest accounts + * @param devFeePercentage - The percentage amount to send to developer wallet prior to the swap. Percentage num/dem values has to match token decimal. + * @returns a SwapQuote object with slippage adjusted SwapInput parameters & estimates on token amounts, fee & end whirlpool states. + */ +export async function swapQuoteByInputTokenWithDevFees( + whirlpool: Whirlpool, + inputTokenMint: Address, + tokenAmount: u64, + slippageTolerance: Percentage, + programId: Address, + fetcher: AccountFetcher, + devFeePercentage: Percentage, + refresh: boolean +): Promise { + if (devFeePercentage.toDecimal().greaterThanOrEqualTo(1)) { + throw new WhirlpoolsError( + "Provided devFeePercentage must be less than 100%", + SwapErrorCode.InvalidDevFeePercentage + ); + } + + const devFeeAmount = tokenAmount + .mul(devFeePercentage.numerator) + .div(devFeePercentage.denominator); + + const slippageAdjustedQuote = await swapQuoteByInputToken( + whirlpool, + inputTokenMint, + tokenAmount.sub(devFeeAmount), + slippageTolerance, + programId, + fetcher, + refresh + ); + + const devFeeAdjustedQuote: DevFeeSwapQuote = { + ...slippageAdjustedQuote, + amountSpecifiedIsInput: true, + estimatedAmountIn: slippageAdjustedQuote.estimatedAmountIn.add(devFeeAmount), + estimatedFeeAmount: slippageAdjustedQuote.estimatedFeeAmount.add(devFeeAmount), + estimatedSwapFeeAmount: slippageAdjustedQuote.estimatedFeeAmount, + devFeeAmount, + }; + + return devFeeAdjustedQuote; +} diff --git a/sdk/src/quotes/public/swap-quote.ts b/sdk/src/quotes/public/swap-quote.ts index 1015a4f..9591340 100644 --- a/sdk/src/quotes/public/swap-quote.ts +++ b/sdk/src/quotes/public/swap-quote.ts @@ -1,15 +1,16 @@ +import { AddressUtil, Percentage } from "@orca-so/common-sdk"; import { Address, BN } from "@project-serum/anchor"; import { u64 } from "@solana/spl-token"; import invariant from "tiny-invariant"; -import { PoolUtil } from "../../utils/public/pool-utils"; import { SwapInput } from "../../instructions"; -import { WhirlpoolData, TickArray } from "../../types/public"; -import { AddressUtil, Percentage } from "@orca-so/common-sdk"; -import { TickArrayUtil, TokenType } from "../../utils/public"; -import { Whirlpool } from "../../whirlpool-client"; import { AccountFetcher } from "../../network/public"; -import { simulateSwap } from "../swap/swap-quote-impl"; +import { TickArray, WhirlpoolData } from "../../types/public"; +import { PoolUtil, TokenType } from "../../utils/public"; import { SwapUtils } from "../../utils/public/swap-utils"; +import { Whirlpool } from "../../whirlpool-client"; +import { simulateSwap } from "../swap/swap-quote-impl"; +import { checkIfAllTickArraysInitialized } from "../swap/swap-quote-utils"; +import { DevFeeSwapQuote } from "./dev-fee-swap-quote"; /** * @category Quotes @@ -32,6 +33,14 @@ export type SwapQuoteParam = { tickArrays: TickArray[]; }; +/** + * A collection of estimated values from quoting a swap. + * @category Quotes + * @link {BaseSwapQuote} + * @link {DevFeeSwapQuote} + */ +export type SwapQuote = NormalSwapQuote | DevFeeSwapQuote; + /** * A collection of estimated values from quoting a swap. * @category Quotes @@ -41,7 +50,7 @@ export type SwapQuoteParam = { * @param estimatedEndSqrtPrice - Approximate sqrtPrice the Whirlpool will land on after this swap * @param estimatedFeeAmount - Approximate feeAmount (all fees) charged on this swap */ -export type SwapQuote = { +export type NormalSwapQuote = { estimatedAmountIn: u64; estimatedAmountOut: u64; estimatedEndTickIndex: number; @@ -71,17 +80,17 @@ export async function swapQuoteByInputToken( fetcher: AccountFetcher, refresh: boolean ): Promise { - return swapQuoteByToken( + const params = await swapQuoteByToken( whirlpool, inputTokenMint, tokenAmount, - slippageTolerance, TokenType.TokenA, true, programId, fetcher, refresh ); + return swapQuoteWithParams(params, slippageTolerance); } /** @@ -109,17 +118,17 @@ export async function swapQuoteByOutputToken( fetcher: AccountFetcher, refresh: boolean ): Promise { - return swapQuoteByToken( + const params = await swapQuoteByToken( whirlpool, outputTokenMint, tokenAmount, - slippageTolerance, TokenType.TokenB, false, programId, fetcher, refresh ); + return swapQuoteWithParams(params, slippageTolerance); } /** @@ -130,7 +139,10 @@ export async function swapQuoteByOutputToken( * @param slippageTolerance - The amount of slippage to account for when generating the final quote. * @returns a SwapQuote object with slippage adjusted SwapInput parameters & estimates on token amounts, fee & end whirlpool states. */ -export function swapQuoteWithParams(params: SwapQuoteParam, slippageTolerance: Percentage) { +export function swapQuoteWithParams( + params: SwapQuoteParam, + slippageTolerance: Percentage +): SwapQuote { checkIfAllTickArraysInitialized(params.tickArrays); const quote = simulateSwap(params); @@ -138,11 +150,11 @@ export function swapQuoteWithParams(params: SwapQuoteParam, slippageTolerance: P const slippageAdjustedQuote: SwapQuote = { ...quote, ...SwapUtils.calculateSwapAmountsFromQuote( - params.tokenAmount, + quote.amount, quote.estimatedAmountIn, quote.estimatedAmountOut, slippageTolerance, - params.amountSpecifiedIsInput + quote.amountSpecifiedIsInput ), }; @@ -153,13 +165,12 @@ async function swapQuoteByToken( whirlpool: Whirlpool, inputTokenMint: Address, tokenAmount: u64, - slippageTolerance: Percentage, amountSpecifiedTokenType: TokenType, amountSpecifiedIsInput: boolean, programId: Address, fetcher: AccountFetcher, refresh: boolean -) { +): Promise { const whirlpoolData = whirlpool.getData(); const swapMintKey = AddressUtil.toPubKey(inputTokenMint); const swapTokenType = PoolUtil.getTokenType(whirlpoolData, swapMintKey); @@ -177,29 +188,13 @@ async function swapQuoteByToken( refresh ); - return swapQuoteWithParams( - { - whirlpoolData, - tokenAmount, - aToB, - amountSpecifiedIsInput, - sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), - otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput), - tickArrays, - }, - slippageTolerance - ); -} - -function checkIfAllTickArraysInitialized(tickArrays: TickArray[]) { - // Check if all the tick arrays have been initialized. - const uninitializedIndices = TickArrayUtil.getUninitializedArrays( - tickArrays.map((array) => array.data) - ); - if (uninitializedIndices.length > 0) { - const uninitializedArrays = uninitializedIndices - .map((index) => tickArrays[index].address.toBase58()) - .join(", "); - throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`); - } + return { + whirlpoolData, + tokenAmount, + aToB, + amountSpecifiedIsInput, + sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB), + otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput), + tickArrays, + }; } diff --git a/sdk/src/quotes/swap/swap-quote-utils.ts b/sdk/src/quotes/swap/swap-quote-utils.ts new file mode 100644 index 0000000..ea91f60 --- /dev/null +++ b/sdk/src/quotes/swap/swap-quote-utils.ts @@ -0,0 +1,14 @@ +import { TickArray, TickArrayUtil } from "../.."; + +export function checkIfAllTickArraysInitialized(tickArrays: TickArray[]) { + // Check if all the tick arrays have been initialized. + const uninitializedIndices = TickArrayUtil.getUninitializedArrays( + tickArrays.map((array) => array.data) + ); + if (uninitializedIndices.length > 0) { + const uninitializedArrays = uninitializedIndices + .map((index) => tickArrays[index].address.toBase58()) + .join(", "); + throw new Error(`TickArray addresses - [${uninitializedArrays}] need to be initialized.`); + } +} diff --git a/sdk/src/whirlpool-client.ts b/sdk/src/whirlpool-client.ts index 59a828f..44418b0 100644 --- a/sdk/src/whirlpool-client.ts +++ b/sdk/src/whirlpool-client.ts @@ -3,8 +3,8 @@ import { Address } from "@project-serum/anchor"; import { PublicKey } from "@solana/web3.js"; import { WhirlpoolContext } from "./context"; import { WhirlpoolClientImpl } from "./impl/whirlpool-client-impl"; +import { DevFeeSwapInput, SwapInput } from "./instructions"; import { AccountFetcher } from "./network/public"; -import { SwapQuote } from "./quotes/public"; import { DecreaseLiquidityInput, IncreaseLiquidityInput, @@ -201,11 +201,27 @@ export interface Whirlpool { /** * Perform a swap between tokenA and tokenB on this pool. * - * @param quote - A quote on the desired tokenIn and tokenOut for this swap. Use @link {swapQuote} to generate this object. + * @param input - A quote on the desired tokenIn and tokenOut for this swap. Use @link {swapQuote} to generate this object. * @param wallet - The wallet that tokens will be withdrawn and deposit into. If null, the WhirlpoolContext wallet is used. * @return a transaction that will perform the swap once executed. */ - swap: (quote: SwapQuote, wallet?: PublicKey) => Promise; + swap: (input: SwapInput, wallet?: PublicKey) => Promise; + + /** + * Collect a developer fee and perform a swap between tokenA and tokenB on this pool. + * + * @param input - A quote on the desired tokenIn and tokenOut for this swap. Use @link {swapQuote} to generate this object. + * @param devFeeWallet - The wallet that developer fees will be deposited into. + * @param wallet - The wallet that swap tokens will be withdrawn and deposit into. If null, the WhirlpoolContext wallet is used. + * @param payer - The wallet that will fund the cost needed to initialize the dev wallet token ATA accounts. If null, the WhirlpoolContext wallet is used. + * @return a transaction that will perform the swap once executed. + */ + swapWithDevFees: ( + input: DevFeeSwapInput, + devFeeWallet: PublicKey, + wallet?: PublicKey, + payer?: PublicKey + ) => Promise; } /** diff --git a/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts b/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts new file mode 100644 index 0000000..7ff372f --- /dev/null +++ b/sdk/tests/sdk/whirlpools/swap/swap-dev-fee.test.ts @@ -0,0 +1,529 @@ +import { Percentage, ZERO } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; +import { Address } from "@project-serum/anchor"; +import { u64 } from "@solana/spl-token"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import * as assert from "assert"; +import { + buildWhirlpoolClient, PriceMath, + swapQuoteByInputToken, + Whirlpool, + WhirlpoolContext +} from "../../../../src"; +import { SwapErrorCode, WhirlpoolsError } from "../../../../src/errors/errors"; +import { swapQuoteByInputTokenWithDevFees } from "../../../../src/quotes/public/dev-fee-swap-quote"; +import { + assertDevFeeQuotes, + assertDevTokenAmount, + assertQuoteAndResults, + TickSpacing +} from "../../../utils"; +import { + arrayTickIndexToTickIndex, + buildPosition, + setupSwapTest +} from "../../../utils/swap-test-utils"; +import { getVaultAmounts } from "../../../utils/whirlpools-test-utils"; + +describe("whirlpool-dev-fee-swap", () => { + const provider = anchor.AnchorProvider.local(); + anchor.setProvider(anchor.AnchorProvider.env()); + const program = anchor.workspace.Whirlpool; + const ctx = WhirlpoolContext.fromWorkspace(provider, program); + const client = buildWhirlpoolClient(ctx); + const tickSpacing = TickSpacing.SixtyFour; + const slippageTolerance = Percentage.fromFraction(0, 100); + + it("swap with dev-fee 0% equals swap", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const devWallet = Keypair.generate(); + const aToB = false; + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(0, 1000); // 0% + const inputTokenAmount = new u64(119500000); + const postFeeTokenAmount = inputTokenAmount.sub( + inputTokenAmount.mul(devFeePercentage.numerator).div(devFeePercentage.denominator) + ); + const whirlpoolData = await whirlpool.refreshData(); + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + true + ); + const postFeeInputTokenQuote = await swapQuoteByInputToken( + whirlpool, + whirlpoolData.tokenMintB, + postFeeTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + true + ); + const inputTokenQuoteWithDevFees = await swapQuoteByInputTokenWithDevFees( + whirlpool, + whirlpoolData.tokenMintB, + inputTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + devFeePercentage, + true + ); + assertDevFeeQuotes(inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees); + await ( + await whirlpool.swapWithDevFees(inputTokenQuoteWithDevFees, devWallet.publicKey) + ).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults(aToB, inputTokenQuote, newData, beforeVaultAmounts, afterVaultAmounts); + }); + + it("swap with dev-fee 0.1%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const devWallet = Keypair.generate(); + const aToB = false; + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(1, 1000); // 0.1% + const inputTokenAmount = new u64(1195000); + const postFeeTokenAmount = inputTokenAmount.sub( + inputTokenAmount.mul(devFeePercentage.numerator).div(devFeePercentage.denominator) + ); + + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = aToB ? whirlpoolData.tokenMintA : whirlpoolData.tokenMintB; + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const { inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees } = await getQuotes( + ctx, + whirlpool, + swapToken, + inputTokenAmount, + postFeeTokenAmount, + slippageTolerance, + devFeePercentage + ); + assertDevFeeQuotes(inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees); + await ( + await whirlpool.swapWithDevFees(inputTokenQuoteWithDevFees, devWallet.publicKey) + ).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults( + aToB, + postFeeInputTokenQuote, + newData, + beforeVaultAmounts, + afterVaultAmounts + ); + await assertDevTokenAmount(ctx, inputTokenQuoteWithDevFees, swapToken, devWallet.publicKey); + }); + + it("swap with dev-fee 1%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 22 }, tickSpacing); + const devWallet = Keypair.generate(); + const aToB = true; + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(1, 100); // 1% + const inputTokenAmount = new u64(119500000); + const postFeeTokenAmount = inputTokenAmount.sub( + inputTokenAmount.mul(devFeePercentage.numerator).div(devFeePercentage.denominator) + ); + + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = aToB ? whirlpoolData.tokenMintA : whirlpoolData.tokenMintB; + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const { inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees } = await getQuotes( + ctx, + whirlpool, + swapToken, + inputTokenAmount, + postFeeTokenAmount, + slippageTolerance, + devFeePercentage + ); + assertDevFeeQuotes(inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees); + await ( + await whirlpool.swapWithDevFees(inputTokenQuoteWithDevFees, devWallet.publicKey) + ).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults( + aToB, + postFeeInputTokenQuote, + newData, + beforeVaultAmounts, + afterVaultAmounts + ); + await assertDevTokenAmount(ctx, inputTokenQuoteWithDevFees, swapToken, devWallet.publicKey); + }); + + it("swap with input-token as NATIVE_MINT & dev-fee 1%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: 1, offsetIndex: 1 }, tickSpacing); + const aToB = true; + const tokenAIsNative = true; + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-16896, -11264, -5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(990_000_000) + ), + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 0, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(990_000_000) + ), + buildPosition( + // a + { arrayIndex: 0, offsetIndex: 22 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(1_990_000_000) + ), + buildPosition( + // a + { arrayIndex: 0, offsetIndex: 23 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(990_000_000) + ), + ], + }, tokenAIsNative); + + const { devWallet, balance: preDevWalletBalance } = await setupDevWallet(ctx, 10_000_000) + + const devFeePercentage = Percentage.fromFraction(1, 10000); // 0.01% + const inputTokenAmount = new u64(1_000_000_000); // Swap 1SOL + const postFeeTokenAmount = inputTokenAmount.sub( + inputTokenAmount.mul(devFeePercentage.numerator).div(devFeePercentage.denominator) + ); + + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = aToB ? whirlpoolData.tokenMintA : whirlpoolData.tokenMintB; + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + + const { inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees } = await getQuotes( + ctx, + whirlpool, + swapToken, + inputTokenAmount, + postFeeTokenAmount, + slippageTolerance, + devFeePercentage + ); + + assertDevFeeQuotes(inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees); + await ( + await whirlpool.swapWithDevFees(inputTokenQuoteWithDevFees, devWallet.publicKey) + ).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults( + aToB, + postFeeInputTokenQuote, + newData, + beforeVaultAmounts, + afterVaultAmounts + ); + await assertDevTokenAmount(ctx, inputTokenQuoteWithDevFees, swapToken, devWallet.publicKey, preDevWalletBalance); + }); + + it("swap with dev-fee 50%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const devWallet = Keypair.generate(); + const aToB = false; + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(500000, 1000000); // 50% + const inputTokenAmount = new u64(119500000); + const postFeeTokenAmount = inputTokenAmount.sub( + inputTokenAmount.mul(devFeePercentage.numerator).div(devFeePercentage.denominator) + ); + + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = aToB ? whirlpoolData.tokenMintA : whirlpoolData.tokenMintB; + const beforeVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + const { inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees } = await getQuotes( + ctx, + whirlpool, + swapToken, + inputTokenAmount, + postFeeTokenAmount, + slippageTolerance, + devFeePercentage + ); + assertDevFeeQuotes(inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees); + await ( + await whirlpool.swapWithDevFees(inputTokenQuoteWithDevFees, devWallet.publicKey) + ).buildAndExecute(); + + const newData = await whirlpool.refreshData(); + const afterVaultAmounts = await getVaultAmounts(ctx, whirlpoolData); + assertQuoteAndResults( + aToB, + postFeeInputTokenQuote, + newData, + beforeVaultAmounts, + afterVaultAmounts + ); + await assertDevTokenAmount(ctx, inputTokenQuoteWithDevFees, swapToken, devWallet.publicKey); + }); + + it("swap with dev-fee of 100%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(100, 100); // 100% + const inputTokenAmount = new u64(119500000); + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = whirlpoolData.tokenMintB; + + assert.rejects( + () => + swapQuoteByInputTokenWithDevFees( + whirlpool, + swapToken, + inputTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + devFeePercentage, + true + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.InvalidDevFeePercentage + ); + }); + + it("swap with dev-fee of 200%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(200, 100); // 200% + const inputTokenAmount = new u64(119500000); + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = whirlpoolData.tokenMintB; + + assert.rejects( + () => + swapQuoteByInputTokenWithDevFees( + whirlpool, + swapToken, + inputTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + devFeePercentage, + true + ), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.InvalidDevFeePercentage + ); + }); + + it("swap with a manual quote with dev-fee of 200%", async () => { + const currIndex = arrayTickIndexToTickIndex({ arrayIndex: -1, offsetIndex: 22 }, tickSpacing); + const devWallet = Keypair.generate(); + const whirlpool = await setupSwapTest({ + ctx, + client, + tickSpacing, + initSqrtPrice: PriceMath.tickIndexToSqrtPriceX64(currIndex), + initArrayStartTicks: [-5632, 0, 5632], + fundedPositions: [ + buildPosition( + // a + { arrayIndex: -1, offsetIndex: 10 }, + { arrayIndex: 1, offsetIndex: 23 }, + tickSpacing, + new anchor.BN(250_000_000) + ), + ], + }); + + const devFeePercentage = Percentage.fromFraction(200, 100); // 200% + const inputTokenAmount = new u64(119500000); + const whirlpoolData = await whirlpool.refreshData(); + const swapToken = whirlpoolData.tokenMintB; + + assert.rejects( + async () => + ( + await whirlpool.swapWithDevFees( + { + amount: new u64(10000), + devFeeAmount: new u64(30000), + amountSpecifiedIsInput: true, + aToB: true, + otherAmountThreshold: ZERO, + sqrtPriceLimit: ZERO, + tickArray0: PublicKey.default, + tickArray1: PublicKey.default, + tickArray2: PublicKey.default, + }, + devWallet.publicKey + ) + ).buildAndExecute(), + (err) => (err as WhirlpoolsError).errorCode === SwapErrorCode.InvalidDevFeePercentage + ); + }); +}); + +async function getQuotes( + ctx: WhirlpoolContext, + whirlpool: Whirlpool, + swapToken: Address, + inputTokenAmount: u64, + postFeeTokenAmount: u64, + slippageTolerance: Percentage, + devFeePercentage: Percentage +) { + const inputTokenQuote = await swapQuoteByInputToken( + whirlpool, + swapToken, + inputTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + true + ); + const postFeeInputTokenQuote = await swapQuoteByInputToken( + whirlpool, + swapToken, + postFeeTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + true + ); + const inputTokenQuoteWithDevFees = await swapQuoteByInputTokenWithDevFees( + whirlpool, + swapToken, + inputTokenAmount, + slippageTolerance, + ctx.program.programId, + ctx.fetcher, + devFeePercentage, + true + ); + + return { inputTokenQuote, postFeeInputTokenQuote, inputTokenQuoteWithDevFees }; +} + +async function setupDevWallet(ctx: WhirlpoolContext, airdrop: number) { + // Setup dev-wallet. Airdrop some tokens in or it'll be difficult to account for + // rent-tokens when we do assertion + const devWallet = Keypair.generate(); + const txn = await ctx.provider.connection.requestAirdrop(devWallet.publicKey, airdrop); + await ctx.provider.connection.confirmTransaction(txn); + const balance = await ctx.provider.connection.getBalance(devWallet.publicKey); + return { devWallet, balance } +} diff --git a/sdk/tests/utils/assert.ts b/sdk/tests/utils/assert.ts index 646a412..488a6d5 100644 --- a/sdk/tests/utils/assert.ts +++ b/sdk/tests/utils/assert.ts @@ -1,12 +1,15 @@ +import { deriveATA, ONE } from "@orca-so/common-sdk"; +import { BN, Program, web3 } from "@project-serum/anchor"; +import { AccountLayout, NATIVE_MINT } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; import * as assert from "assert"; -import { Program, web3, BN } from "@project-serum/anchor"; -import { AccountLayout } from "@solana/spl-token"; -import { TEST_TOKEN_PROGRAM_ID } from "./test-consts"; +import { SwapQuote, WhirlpoolContext } from "../../src"; import { Whirlpool } from "../../src/artifacts/whirlpool"; +import { DevFeeSwapQuote } from "../../src/quotes/public/dev-fee-swap-quote"; import { TickData, WhirlpoolData } from "../../src/types/public"; -import { SwapQuote } from "../../src"; +import { TEST_TOKEN_PROGRAM_ID } from "./test-consts"; +import { getTokenBalance } from "./token"; import { VaultAmounts } from "./whirlpools-test-utils"; -import { ONE } from "@orca-so/common-sdk"; export function assertInputOutputQuoteEqual( inputTokenQuote: SwapQuote, @@ -39,6 +42,71 @@ export function assertInputOutputQuoteEqual( ); } +export function assertDevFeeQuotes( + inputQuote: SwapQuote, + postFeeInputQuote: SwapQuote, + devFeeQuote: DevFeeSwapQuote +) { + assert.equal(inputQuote.aToB, devFeeQuote.aToB, "aToB not equal"); + assert.ok( + devFeeQuote.estimatedAmountIn.eq(inputQuote.estimatedAmountIn), + `the devFeeQuote's estimatedAmountIn ${devFeeQuote.estimatedAmountIn} should equal the normal quote's estimatedAmountIn ${inputQuote.estimatedAmountIn}` + ); + assert.ok( + devFeeQuote.estimatedAmountIn.eq( + postFeeInputQuote.estimatedAmountIn.add(devFeeQuote.devFeeAmount) + ), + `the devFeeQuote's estimatedAmountIn ${devFeeQuote.estimatedAmountIn} should equal the post-fee quote's estimatedAmountIn ${inputQuote.estimatedAmountIn} plus devFeeAmount ${devFeeQuote.devFeeAmount}` + ); + assert.ok( + postFeeInputQuote.estimatedAmountOut.sub(devFeeQuote.estimatedAmountOut).abs().lte(ONE), + `post-fee input estimatedAmountOut ${inputQuote.estimatedAmountOut} does not equal devFee quote estimatedAmountOut - ${devFeeQuote.estimatedAmountOut}` + ); + assert.equal( + postFeeInputQuote.estimatedEndTickIndex, + devFeeQuote.estimatedEndTickIndex, + "estimatedEndTickIndex not equal" + ); + assert.equal( + devFeeQuote.estimatedFeeAmount.toString(), + devFeeQuote.estimatedSwapFeeAmount.add(devFeeQuote.devFeeAmount).toString(), + "devFeeQuote estimatedFeeAmount is not the sum of estimatedSwapFeeAmount and devFeeAmount" + ); + assert.equal( + devFeeQuote.estimatedSwapFeeAmount.toString(), + postFeeInputQuote.estimatedFeeAmount.toString(), + "devFeeQuote's estimatedSwapFeeAmount should equal the quote's total swap fee (without dev fee)" + ); + assert.equal( + postFeeInputQuote.amountSpecifiedIsInput, + devFeeQuote.amountSpecifiedIsInput, + "amountSpecifiedIsInput not equal" + ); +} + +export async function assertDevTokenAmount( + ctx: WhirlpoolContext, + expectationQuote: DevFeeSwapQuote, + swapToken: PublicKey, + devWallet: PublicKey, + preDevWalletLamport = 0 +) { + + if (swapToken.equals(NATIVE_MINT)) { + const walletAmount = await ctx.provider.connection.getBalance(devWallet); + assert.equal(expectationQuote.devFeeAmount.toNumber() + preDevWalletLamport, walletAmount) + return; + } + + const tokenDevWalletAta = await deriveATA(devWallet, swapToken); + const afterDevWalletAmount = await getTokenBalance(ctx.provider, tokenDevWalletAta); + assert.equal( + expectationQuote.devFeeAmount, + afterDevWalletAmount, + "incorrect devFee amount sent to dev wallet." + ); +} + export function assertQuoteAndResults( aToB: boolean, quote: SwapQuote, diff --git a/sdk/tests/utils/init-utils.ts b/sdk/tests/utils/init-utils.ts index 7485996..537c0bc 100644 --- a/sdk/tests/utils/init-utils.ts +++ b/sdk/tests/utils/init-utils.ts @@ -1,37 +1,26 @@ +import { MathUtil, PDA } from "@orca-so/common-sdk"; import * as anchor from "@project-serum/anchor"; -import { - InitTickArrayParams, - OpenPositionParams, - InitPoolParams, - InitializeRewardParams, - TICK_ARRAY_SIZE, - WhirlpoolContext, - AccountFetcher, - InitConfigParams, - TickUtil, - PriceMath, - WhirlpoolIx, - PDAUtil, - toTx, -} from "../../src"; -import { - generateDefaultConfigParams, - generateDefaultInitFeeTierParams, - generateDefaultInitPoolParams, - generateDefaultInitTickArrayParams, - generateDefaultOpenPositionParams, -} from "./test-builders"; -import { PublicKey, Keypair } from "@solana/web3.js"; +import { u64 } from "@solana/spl-token"; +import { Keypair, PublicKey } from "@solana/web3.js"; import { createAndMintToAssociatedTokenAccount, createMint, mintToByAuthority, TickSpacing, - ZERO_BN, + ZERO_BN } from "."; -import { u64 } from "@solana/spl-token"; +import { + InitConfigParams, InitializeRewardParams, InitPoolParams, InitTickArrayParams, + OpenPositionParams, PDAUtil, PriceMath, TickUtil, TICK_ARRAY_SIZE, toTx, WhirlpoolClient, WhirlpoolContext, WhirlpoolIx +} from "../../src"; import { PoolUtil } from "../../src/utils/public/pool-utils"; -import { MathUtil, PDA } from "@orca-so/common-sdk"; +import { + generateDefaultConfigParams, + generateDefaultInitFeeTierParams, + generateDefaultInitPoolParams, + generateDefaultInitTickArrayParams, + generateDefaultOpenPositionParams +} from "./test-builders"; const defaultInitSqrtPrice = MathUtil.toX64_BN(new anchor.BN(5)); @@ -47,7 +36,8 @@ export async function buildTestPoolParams( tickSpacing: number, defaultFeeRate = 3000, initSqrtPrice = defaultInitSqrtPrice, - funder?: PublicKey + funder?: PublicKey, + tokenAIsNative = false ) { const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx); await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute(); @@ -65,7 +55,8 @@ export async function buildTestPoolParams( feeTierParams.feeTierPda.publicKey, tickSpacing, initSqrtPrice, - funder + funder, + tokenAIsNative ); return { configInitInfo, @@ -85,14 +76,16 @@ export async function initTestPool( ctx: WhirlpoolContext, tickSpacing: number, initSqrtPrice = defaultInitSqrtPrice, - funder?: Keypair + funder?: Keypair, + tokenAIsNative = false ) { const { configInitInfo, poolInitInfo, configKeypairs, feeTierParams } = await buildTestPoolParams( ctx, tickSpacing, 3000, initSqrtPrice, - funder?.publicKey + funder?.publicKey, + tokenAIsNative ); const tx = toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, poolInitInfo)); @@ -289,27 +282,37 @@ export async function initTestPoolWithTokens( ctx: WhirlpoolContext, tickSpacing: number, initSqrtPrice = defaultInitSqrtPrice, - mintAmount = new anchor.BN("15000000000") + mintAmount = new anchor.BN("15000000000"), + tokenAIsNative = false ) { const provider = ctx.provider; const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool( ctx, tickSpacing, - initSqrtPrice + initSqrtPrice, + undefined, + tokenAIsNative ); const { tokenMintA, tokenMintB, whirlpoolPda } = poolInitInfo; + + // Airdrop SOL into provider's wallet for SOL native token testing. + const connection = ctx.provider.connection; + await connection.requestAirdrop(ctx.provider.wallet.publicKey, 100_000_000_000_000) + const tokenAccountA = await createAndMintToAssociatedTokenAccount( provider, tokenMintA, mintAmount ); + const tokenAccountB = await createAndMintToAssociatedTokenAccount( provider, tokenMintB, mintAmount ); + return { poolInitInfo, configInitInfo, @@ -442,6 +445,31 @@ export interface FundedPositionInfo { tickArrayUpper: PublicKey; } +export async function fundPositionsWithClient( + client: WhirlpoolClient, + whirlpoolKey: PublicKey, + fundParams: FundedPositionParams[] +) { + const whirlpool = await client.getPool(whirlpoolKey, true); + const whirlpoolData = whirlpool.getData(); + await Promise.all(fundParams.map(async (param, idx) => { + const { tokenA, tokenB } = PoolUtil.getTokenAmountsFromLiquidity( + param.liquidityAmount, + whirlpoolData.sqrtPrice, + PriceMath.tickIndexToSqrtPriceX64(param.tickLowerIndex), + PriceMath.tickIndexToSqrtPriceX64(param.tickUpperIndex), + true + ); + + const { tx } = await whirlpool.openPosition(param.tickLowerIndex, param.tickUpperIndex, { + liquidityAmount: param.liquidityAmount, + tokenMaxA: tokenA, + tokenMaxB: tokenB + }); + await tx.buildAndExecute(); + })); +} + export async function fundPositions( ctx: WhirlpoolContext, poolInitInfo: InitPoolParams, diff --git a/sdk/tests/utils/swap-test-utils.ts b/sdk/tests/utils/swap-test-utils.ts index f943707..3c1f889 100644 --- a/sdk/tests/utils/swap-test-utils.ts +++ b/sdk/tests/utils/swap-test-utils.ts @@ -1,10 +1,10 @@ -import * as anchor from "@project-serum/anchor"; -import { PublicKey } from "@solana/web3.js"; import { Percentage } from "@orca-so/common-sdk"; +import * as anchor from "@project-serum/anchor"; import { u64 } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; import { TickSpacing } from "."; -import { WhirlpoolContext, WhirlpoolClient, Whirlpool, TICK_ARRAY_SIZE } from "../../src"; -import { FundedPositionParams, initTestPoolWithTokens, fundPositions } from "./init-utils"; +import { TICK_ARRAY_SIZE, Whirlpool, WhirlpoolClient, WhirlpoolContext } from "../../src"; +import { FundedPositionParams, fundPositionsWithClient, initTestPoolWithTokens } from "./init-utils"; export interface SwapTestPoolParams { ctx: WhirlpoolContext; @@ -29,19 +29,21 @@ export interface SwapTestSetup { tickArrayAddresses: PublicKey[]; } -export async function setupSwapTest(setup: SwapTestPoolParams) { +export async function setupSwapTest(setup: SwapTestPoolParams, tokenAIsNative = false) { const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = await initTestPoolWithTokens( setup.ctx, setup.tickSpacing, setup.initSqrtPrice, - setup.tokenMintAmount + setup.tokenMintAmount, + tokenAIsNative ); const whirlpool = await setup.client.getPool(whirlpoolPda.publicKey, true); await (await whirlpool.initTickArrayForTicks(setup.initArrayStartTicks))?.buildAndExecute(); - await fundPositions(setup.ctx, poolInitInfo, tokenAccountA, tokenAccountB, setup.fundedPositions); + await fundPositionsWithClient(setup.client, whirlpoolPda.publicKey, setup.fundedPositions) + return whirlpool; } diff --git a/sdk/tests/utils/test-builders.ts b/sdk/tests/utils/test-builders.ts index 4469cf7..69a5063 100644 --- a/sdk/tests/utils/test-builders.ts +++ b/sdk/tests/utils/test-builders.ts @@ -1,6 +1,6 @@ import { MathUtil, PDA, Percentage } from "@orca-so/common-sdk"; import { AnchorProvider } from "@project-serum/anchor"; -import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { Keypair, PublicKey } from "@solana/web3.js"; import Decimal from "decimal.js"; import { createAndMintToAssociatedTokenAccount, createMint } from "."; @@ -13,7 +13,7 @@ import { OpenPositionParams, PDAUtil, PriceMath, - Whirlpool, + Whirlpool } from "../../src"; import { WhirlpoolContext } from "../../src/context"; @@ -46,9 +46,9 @@ export const generateDefaultConfigParams = ( return { configInitInfo, configKeypairs }; }; -export const createInOrderMints = async (context: WhirlpoolContext) => { +export const createInOrderMints = async (context: WhirlpoolContext, tokenAIsNative = false) => { const provider = context.provider; - const tokenXMintPubKey = await createMint(provider); + const tokenXMintPubKey = tokenAIsNative ? NATIVE_MINT : await createMint(provider); const tokenYMintPubKey = await createMint(provider); let tokenAMintPubKey, tokenBMintPubKey; @@ -69,9 +69,10 @@ export const generateDefaultInitPoolParams = async ( feeTierKey: PublicKey, tickSpacing: number, initSqrtPrice = MathUtil.toX64(new Decimal(5)), - funder?: PublicKey + funder?: PublicKey, + tokenAIsNative = false ): Promise => { - const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context); + const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context, tokenAIsNative); const whirlpoolPda = PDAUtil.getWhirlpool( context.program.programId, diff --git a/sdk/tests/utils/token.ts b/sdk/tests/utils/token.ts index 59e6a7b..5948047 100644 --- a/sdk/tests/utils/token.ts +++ b/sdk/tests/utils/token.ts @@ -1,11 +1,14 @@ -import { deriveATA } from "@orca-so/common-sdk"; -import { BN, AnchorProvider, web3 } from "@project-serum/anchor"; +import { deriveATA, TransactionBuilder, ZERO } from "@orca-so/common-sdk"; +import { createWSOLAccountInstructions } from "@orca-so/common-sdk/dist/helpers/token-instructions"; +import { AnchorProvider, BN, web3 } from "@project-serum/anchor"; import { + AccountLayout, ASSOCIATED_TOKEN_PROGRAM_ID, AuthorityType, + NATIVE_MINT, Token, TOKEN_PROGRAM_ID, - u64, + u64 } from "@solana/spl-token"; import { TEST_TOKEN_PROGRAM_ID } from "./test-consts"; @@ -154,6 +157,20 @@ export async function createAndMintToAssociatedTokenAccount( ): Promise { const destinationWalletKey = destinationWallet ? destinationWallet : provider.wallet.publicKey; const payerKey = payer ? payer : provider.wallet.publicKey; + + // Workaround For SOL - just create a wSOL account to satisfy the rest of the test building pipeline. + // Tests who want to test with SOL will have to request their own airdrop. + if (mint.equals(NATIVE_MINT)) { + const rentExemption = await provider.connection.getMinimumBalanceForRentExemption( + AccountLayout.span + ); + const txBuilder = new TransactionBuilder(provider.connection, provider.wallet); + const { address: tokenAccount, ...ix } = createWSOLAccountInstructions(destinationWalletKey, ZERO, rentExemption); + txBuilder.addInstruction(ix); + await txBuilder.buildAndExecute(); + return tokenAccount; + } + const tokenAccount = await createAssociatedTokenAccount( provider, mint, diff --git a/yarn.lock b/yarn.lock index 76adcac..a5006d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,14 +565,15 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@orca-so/common-sdk@~0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.1.0.tgz#25050f18e95bab6a7ef9b10e2d3bd44699ce9836" - integrity sha512-WH6mjrrJyuUMq0DC41jWW6opEIq5Ip/UOf8h7z/gM9Zx+9156JwP/dm8N5GJTNnIG6GYFfhemyNNPj1y3U5LIw== +"@orca-so/common-sdk@~0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.1.1.tgz#18a3a562f1b57f970f2115f453c7e00aa05034cf" + integrity sha512-OKFwr5VgzLKmM/C6xQom4y1jxrihuhKXhDwYMbgPLJW4SllePOfS6SG5fp9/17SvyuWL/zna8Z6ZW2QHpX1Yxw== dependencies: "@project-serum/anchor" "~0.25.0" "@solana/spl-token" "0.1.8" decimal.js "^10.3.1" + tiny-invariant "^1.2.0" "@orca-so/whirlpool-client-sdk@0.0.7": version "0.0.7" @@ -747,10 +748,10 @@ dependencies: "@babel/types" "^7.3.0" -"@types/bn.js@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68" - integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== +"@types/bn.js@~5.1.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" + integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== dependencies: "@types/node" "*"