Allow developers to take additional fees from a swap (#46)
- Add a new swapWithDevFees function in the Whirlpool client class - Add new swapQuoteByInputTokenWithDevFees quote function
This commit is contained in:
parent
1acd1469bd
commit
0f592f267b
|
@ -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",
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum TokenErrorCode {
|
|||
}
|
||||
|
||||
export enum SwapErrorCode {
|
||||
InvalidDevFeePercentage = `InvalidDevFeePercentage`,
|
||||
InvalidSqrtPriceLimitDirection = `InvalidSqrtPriceLimitDirection`,
|
||||
SqrtPriceOutOfBounds = `SqrtPriceOutOfBounds`,
|
||||
ZeroTradableAmount = `ZeroTradableAmount`,
|
||||
|
|
|
@ -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<TransactionBuilder> {
|
||||
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<TransactionBuilder> {
|
||||
private async getSwapTx(
|
||||
input: SwapInput,
|
||||
wallet: PublicKey,
|
||||
initTxBuilder?: TransactionBuilder
|
||||
): Promise<TransactionBuilder> {
|
||||
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,
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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<DevFeeSwapQuote> {
|
||||
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;
|
||||
}
|
|
@ -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<SwapQuote> {
|
||||
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<SwapQuote> {
|
||||
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<SwapQuoteParam> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.`);
|
||||
}
|
||||
}
|
|
@ -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<TransactionBuilder>;
|
||||
swap: (input: SwapInput, wallet?: PublicKey) => Promise<TransactionBuilder>;
|
||||
|
||||
/**
|
||||
* 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<TransactionBuilder>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<InitPoolParams> => {
|
||||
const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context);
|
||||
const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context, tokenAIsNative);
|
||||
|
||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||
context.program.programId,
|
||||
|
|
|
@ -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<web3.PublicKey> {
|
||||
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,
|
||||
|
|
17
yarn.lock
17
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" "*"
|
||||
|
||||
|
|
Loading…
Reference in New Issue