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",
|
"types": "dist/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@metaplex-foundation/mpl-token-metadata": "1.2.5",
|
"@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",
|
"@project-serum/anchor": "~0.25.0",
|
||||||
"@solana/spl-token": "^0.1.8",
|
"@solana/spl-token": "^0.1.8",
|
||||||
"decimal.js": "^10.3.1",
|
"decimal.js": "^10.3.1",
|
||||||
"tiny-invariant": "^1.2.0"
|
"tiny-invariant": "^1.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bn.js": "^5.1.0",
|
"@types/bn.js": "~5.1.0",
|
||||||
"@types/decimal.js": "^7.4.0",
|
"@types/decimal.js": "^7.4.0",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/mocha": "^9.0.0",
|
"@types/mocha": "^9.0.0",
|
||||||
|
|
|
@ -11,6 +11,7 @@ export enum TokenErrorCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SwapErrorCode {
|
export enum SwapErrorCode {
|
||||||
|
InvalidDevFeePercentage = `InvalidDevFeePercentage`,
|
||||||
InvalidSqrtPriceLimitDirection = `InvalidSqrtPriceLimitDirection`,
|
InvalidSqrtPriceLimitDirection = `InvalidSqrtPriceLimitDirection`,
|
||||||
SqrtPriceOutOfBounds = `SqrtPriceOutOfBounds`,
|
SqrtPriceOutOfBounds = `SqrtPriceOutOfBounds`,
|
||||||
ZeroTradableAmount = `ZeroTradableAmount`,
|
ZeroTradableAmount = `ZeroTradableAmount`,
|
||||||
|
|
|
@ -3,29 +3,31 @@ import {
|
||||||
deriveATA,
|
deriveATA,
|
||||||
Percentage,
|
Percentage,
|
||||||
resolveOrCreateATAs,
|
resolveOrCreateATAs,
|
||||||
|
TokenUtil,
|
||||||
TransactionBuilder,
|
TransactionBuilder,
|
||||||
ZERO,
|
ZERO
|
||||||
} from "@orca-so/common-sdk";
|
} 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 { WhirlpoolContext } from "../context";
|
||||||
import {
|
import {
|
||||||
|
closePositionIx,
|
||||||
|
decreaseLiquidityIx,
|
||||||
|
DevFeeSwapInput,
|
||||||
IncreaseLiquidityInput,
|
IncreaseLiquidityInput,
|
||||||
|
increaseLiquidityIx,
|
||||||
|
initTickArrayIx,
|
||||||
openPositionIx,
|
openPositionIx,
|
||||||
openPositionWithMetadataIx,
|
openPositionWithMetadataIx,
|
||||||
initTickArrayIx,
|
|
||||||
increaseLiquidityIx,
|
|
||||||
decreaseLiquidityIx,
|
|
||||||
closePositionIx,
|
|
||||||
swapIx,
|
|
||||||
SwapInput,
|
SwapInput,
|
||||||
|
swapIx
|
||||||
} from "../instructions";
|
} 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 { 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 { PDAUtil, TickArrayUtil, TickUtil } from "../utils/public";
|
||||||
import { decreaseLiquidityQuoteByLiquidityWithParams, SwapQuote } from "../quotes/public";
|
import { Whirlpool } from "../whirlpool-client";
|
||||||
import { getRewardInfos, getTokenVaultAccountInfos } from "./util";
|
import { getRewardInfos, getTokenVaultAccountInfos } from "./util";
|
||||||
|
|
||||||
export class WhirlpoolImpl implements Whirlpool {
|
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
|
const sourceWalletKey = sourceWallet
|
||||||
? AddressUtil.toPubKey(sourceWallet)
|
? AddressUtil.toPubKey(sourceWallet)
|
||||||
: this.ctx.wallet.publicKey;
|
: this.ctx.wallet.publicKey;
|
||||||
return this.getSwapTx(quote, sourceWalletKey);
|
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
|
* Construct a transaction for opening an new position with optional metadata
|
||||||
*/
|
*/
|
||||||
|
@ -393,13 +429,17 @@ export class WhirlpoolImpl implements Whirlpool {
|
||||||
return txBuilder;
|
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 { amount, aToB } = input;
|
||||||
const whirlpool = this.data;
|
const whirlpool = this.data;
|
||||||
const txBuilder = new TransactionBuilder(
|
const txBuilder =
|
||||||
this.ctx.provider.connection,
|
initTxBuilder ??
|
||||||
this.ctx.provider.wallet
|
new TransactionBuilder(this.ctx.provider.connection, this.ctx.provider.wallet);
|
||||||
);
|
|
||||||
|
|
||||||
const [ataA, ataB] = await resolveOrCreateATAs(
|
const [ataA, ataB] = await resolveOrCreateATAs(
|
||||||
this.ctx.connection,
|
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 { 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 { PublicKey } from "@solana/web3.js";
|
||||||
import { BN } from "@project-serum/anchor";
|
import { Whirlpool } from "../artifacts/whirlpool";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters and accounts to swap on a Whirlpool
|
* Parameters and accounts to swap on a Whirlpool
|
||||||
|
@ -61,6 +60,25 @@ export type SwapInput = {
|
||||||
tickArray2: PublicKey;
|
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
|
* 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 { Address, BN } from "@project-serum/anchor";
|
||||||
import { u64 } from "@solana/spl-token";
|
import { u64 } from "@solana/spl-token";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { PoolUtil } from "../../utils/public/pool-utils";
|
|
||||||
import { SwapInput } from "../../instructions";
|
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 { 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 { 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
|
* @category Quotes
|
||||||
|
@ -32,6 +33,14 @@ export type SwapQuoteParam = {
|
||||||
tickArrays: TickArray[];
|
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.
|
* A collection of estimated values from quoting a swap.
|
||||||
* @category Quotes
|
* @category Quotes
|
||||||
|
@ -41,7 +50,7 @@ export type SwapQuoteParam = {
|
||||||
* @param estimatedEndSqrtPrice - Approximate sqrtPrice 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 estimatedFeeAmount - Approximate feeAmount (all fees) charged on this swap
|
||||||
*/
|
*/
|
||||||
export type SwapQuote = {
|
export type NormalSwapQuote = {
|
||||||
estimatedAmountIn: u64;
|
estimatedAmountIn: u64;
|
||||||
estimatedAmountOut: u64;
|
estimatedAmountOut: u64;
|
||||||
estimatedEndTickIndex: number;
|
estimatedEndTickIndex: number;
|
||||||
|
@ -71,17 +80,17 @@ export async function swapQuoteByInputToken(
|
||||||
fetcher: AccountFetcher,
|
fetcher: AccountFetcher,
|
||||||
refresh: boolean
|
refresh: boolean
|
||||||
): Promise<SwapQuote> {
|
): Promise<SwapQuote> {
|
||||||
return swapQuoteByToken(
|
const params = await swapQuoteByToken(
|
||||||
whirlpool,
|
whirlpool,
|
||||||
inputTokenMint,
|
inputTokenMint,
|
||||||
tokenAmount,
|
tokenAmount,
|
||||||
slippageTolerance,
|
|
||||||
TokenType.TokenA,
|
TokenType.TokenA,
|
||||||
true,
|
true,
|
||||||
programId,
|
programId,
|
||||||
fetcher,
|
fetcher,
|
||||||
refresh
|
refresh
|
||||||
);
|
);
|
||||||
|
return swapQuoteWithParams(params, slippageTolerance);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -109,17 +118,17 @@ export async function swapQuoteByOutputToken(
|
||||||
fetcher: AccountFetcher,
|
fetcher: AccountFetcher,
|
||||||
refresh: boolean
|
refresh: boolean
|
||||||
): Promise<SwapQuote> {
|
): Promise<SwapQuote> {
|
||||||
return swapQuoteByToken(
|
const params = await swapQuoteByToken(
|
||||||
whirlpool,
|
whirlpool,
|
||||||
outputTokenMint,
|
outputTokenMint,
|
||||||
tokenAmount,
|
tokenAmount,
|
||||||
slippageTolerance,
|
|
||||||
TokenType.TokenB,
|
TokenType.TokenB,
|
||||||
false,
|
false,
|
||||||
programId,
|
programId,
|
||||||
fetcher,
|
fetcher,
|
||||||
refresh
|
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.
|
* @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.
|
* @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);
|
checkIfAllTickArraysInitialized(params.tickArrays);
|
||||||
|
|
||||||
const quote = simulateSwap(params);
|
const quote = simulateSwap(params);
|
||||||
|
@ -138,11 +150,11 @@ export function swapQuoteWithParams(params: SwapQuoteParam, slippageTolerance: P
|
||||||
const slippageAdjustedQuote: SwapQuote = {
|
const slippageAdjustedQuote: SwapQuote = {
|
||||||
...quote,
|
...quote,
|
||||||
...SwapUtils.calculateSwapAmountsFromQuote(
|
...SwapUtils.calculateSwapAmountsFromQuote(
|
||||||
params.tokenAmount,
|
quote.amount,
|
||||||
quote.estimatedAmountIn,
|
quote.estimatedAmountIn,
|
||||||
quote.estimatedAmountOut,
|
quote.estimatedAmountOut,
|
||||||
slippageTolerance,
|
slippageTolerance,
|
||||||
params.amountSpecifiedIsInput
|
quote.amountSpecifiedIsInput
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -153,13 +165,12 @@ async function swapQuoteByToken(
|
||||||
whirlpool: Whirlpool,
|
whirlpool: Whirlpool,
|
||||||
inputTokenMint: Address,
|
inputTokenMint: Address,
|
||||||
tokenAmount: u64,
|
tokenAmount: u64,
|
||||||
slippageTolerance: Percentage,
|
|
||||||
amountSpecifiedTokenType: TokenType,
|
amountSpecifiedTokenType: TokenType,
|
||||||
amountSpecifiedIsInput: boolean,
|
amountSpecifiedIsInput: boolean,
|
||||||
programId: Address,
|
programId: Address,
|
||||||
fetcher: AccountFetcher,
|
fetcher: AccountFetcher,
|
||||||
refresh: boolean
|
refresh: boolean
|
||||||
) {
|
): Promise<SwapQuoteParam> {
|
||||||
const whirlpoolData = whirlpool.getData();
|
const whirlpoolData = whirlpool.getData();
|
||||||
const swapMintKey = AddressUtil.toPubKey(inputTokenMint);
|
const swapMintKey = AddressUtil.toPubKey(inputTokenMint);
|
||||||
const swapTokenType = PoolUtil.getTokenType(whirlpoolData, swapMintKey);
|
const swapTokenType = PoolUtil.getTokenType(whirlpoolData, swapMintKey);
|
||||||
|
@ -177,8 +188,7 @@ async function swapQuoteByToken(
|
||||||
refresh
|
refresh
|
||||||
);
|
);
|
||||||
|
|
||||||
return swapQuoteWithParams(
|
return {
|
||||||
{
|
|
||||||
whirlpoolData,
|
whirlpoolData,
|
||||||
tokenAmount,
|
tokenAmount,
|
||||||
aToB,
|
aToB,
|
||||||
|
@ -186,20 +196,5 @@ async function swapQuoteByToken(
|
||||||
sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB),
|
sqrtPriceLimit: SwapUtils.getDefaultSqrtPriceLimit(aToB),
|
||||||
otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput),
|
otherAmountThreshold: SwapUtils.getDefaultOtherAmountThreshold(amountSpecifiedIsInput),
|
||||||
tickArrays,
|
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.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { PublicKey } from "@solana/web3.js";
|
||||||
import { WhirlpoolContext } from "./context";
|
import { WhirlpoolContext } from "./context";
|
||||||
import { WhirlpoolClientImpl } from "./impl/whirlpool-client-impl";
|
import { WhirlpoolClientImpl } from "./impl/whirlpool-client-impl";
|
||||||
|
import { DevFeeSwapInput, SwapInput } from "./instructions";
|
||||||
import { AccountFetcher } from "./network/public";
|
import { AccountFetcher } from "./network/public";
|
||||||
import { SwapQuote } from "./quotes/public";
|
|
||||||
import {
|
import {
|
||||||
DecreaseLiquidityInput,
|
DecreaseLiquidityInput,
|
||||||
IncreaseLiquidityInput,
|
IncreaseLiquidityInput,
|
||||||
|
@ -201,11 +201,27 @@ export interface Whirlpool {
|
||||||
/**
|
/**
|
||||||
* Perform a swap between tokenA and tokenB on this pool.
|
* 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.
|
* @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.
|
* @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 * as assert from "assert";
|
||||||
import { Program, web3, BN } from "@project-serum/anchor";
|
import { SwapQuote, WhirlpoolContext } from "../../src";
|
||||||
import { AccountLayout } from "@solana/spl-token";
|
|
||||||
import { TEST_TOKEN_PROGRAM_ID } from "./test-consts";
|
|
||||||
import { Whirlpool } from "../../src/artifacts/whirlpool";
|
import { Whirlpool } from "../../src/artifacts/whirlpool";
|
||||||
|
import { DevFeeSwapQuote } from "../../src/quotes/public/dev-fee-swap-quote";
|
||||||
import { TickData, WhirlpoolData } from "../../src/types/public";
|
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 { VaultAmounts } from "./whirlpools-test-utils";
|
||||||
import { ONE } from "@orca-so/common-sdk";
|
|
||||||
|
|
||||||
export function assertInputOutputQuoteEqual(
|
export function assertInputOutputQuoteEqual(
|
||||||
inputTokenQuote: SwapQuote,
|
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(
|
export function assertQuoteAndResults(
|
||||||
aToB: boolean,
|
aToB: boolean,
|
||||||
quote: SwapQuote,
|
quote: SwapQuote,
|
||||||
|
|
|
@ -1,37 +1,26 @@
|
||||||
|
import { MathUtil, PDA } from "@orca-so/common-sdk";
|
||||||
import * as anchor from "@project-serum/anchor";
|
import * as anchor from "@project-serum/anchor";
|
||||||
import {
|
import { u64 } from "@solana/spl-token";
|
||||||
InitTickArrayParams,
|
import { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
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 {
|
import {
|
||||||
createAndMintToAssociatedTokenAccount,
|
createAndMintToAssociatedTokenAccount,
|
||||||
createMint,
|
createMint,
|
||||||
mintToByAuthority,
|
mintToByAuthority,
|
||||||
TickSpacing,
|
TickSpacing,
|
||||||
ZERO_BN,
|
ZERO_BN
|
||||||
} from ".";
|
} 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 { 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));
|
const defaultInitSqrtPrice = MathUtil.toX64_BN(new anchor.BN(5));
|
||||||
|
|
||||||
|
@ -47,7 +36,8 @@ export async function buildTestPoolParams(
|
||||||
tickSpacing: number,
|
tickSpacing: number,
|
||||||
defaultFeeRate = 3000,
|
defaultFeeRate = 3000,
|
||||||
initSqrtPrice = defaultInitSqrtPrice,
|
initSqrtPrice = defaultInitSqrtPrice,
|
||||||
funder?: PublicKey
|
funder?: PublicKey,
|
||||||
|
tokenAIsNative = false
|
||||||
) {
|
) {
|
||||||
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
|
const { configInitInfo, configKeypairs } = generateDefaultConfigParams(ctx);
|
||||||
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
|
await toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configInitInfo)).buildAndExecute();
|
||||||
|
@ -65,7 +55,8 @@ export async function buildTestPoolParams(
|
||||||
feeTierParams.feeTierPda.publicKey,
|
feeTierParams.feeTierPda.publicKey,
|
||||||
tickSpacing,
|
tickSpacing,
|
||||||
initSqrtPrice,
|
initSqrtPrice,
|
||||||
funder
|
funder,
|
||||||
|
tokenAIsNative
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
configInitInfo,
|
configInitInfo,
|
||||||
|
@ -85,14 +76,16 @@ export async function initTestPool(
|
||||||
ctx: WhirlpoolContext,
|
ctx: WhirlpoolContext,
|
||||||
tickSpacing: number,
|
tickSpacing: number,
|
||||||
initSqrtPrice = defaultInitSqrtPrice,
|
initSqrtPrice = defaultInitSqrtPrice,
|
||||||
funder?: Keypair
|
funder?: Keypair,
|
||||||
|
tokenAIsNative = false
|
||||||
) {
|
) {
|
||||||
const { configInitInfo, poolInitInfo, configKeypairs, feeTierParams } = await buildTestPoolParams(
|
const { configInitInfo, poolInitInfo, configKeypairs, feeTierParams } = await buildTestPoolParams(
|
||||||
ctx,
|
ctx,
|
||||||
tickSpacing,
|
tickSpacing,
|
||||||
3000,
|
3000,
|
||||||
initSqrtPrice,
|
initSqrtPrice,
|
||||||
funder?.publicKey
|
funder?.publicKey,
|
||||||
|
tokenAIsNative
|
||||||
);
|
);
|
||||||
|
|
||||||
const tx = toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, poolInitInfo));
|
const tx = toTx(ctx, WhirlpoolIx.initializePoolIx(ctx.program, poolInitInfo));
|
||||||
|
@ -289,27 +282,37 @@ export async function initTestPoolWithTokens(
|
||||||
ctx: WhirlpoolContext,
|
ctx: WhirlpoolContext,
|
||||||
tickSpacing: number,
|
tickSpacing: number,
|
||||||
initSqrtPrice = defaultInitSqrtPrice,
|
initSqrtPrice = defaultInitSqrtPrice,
|
||||||
mintAmount = new anchor.BN("15000000000")
|
mintAmount = new anchor.BN("15000000000"),
|
||||||
|
tokenAIsNative = false
|
||||||
) {
|
) {
|
||||||
const provider = ctx.provider;
|
const provider = ctx.provider;
|
||||||
|
|
||||||
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
|
const { poolInitInfo, configInitInfo, configKeypairs } = await initTestPool(
|
||||||
ctx,
|
ctx,
|
||||||
tickSpacing,
|
tickSpacing,
|
||||||
initSqrtPrice
|
initSqrtPrice,
|
||||||
|
undefined,
|
||||||
|
tokenAIsNative
|
||||||
);
|
);
|
||||||
|
|
||||||
const { tokenMintA, tokenMintB, whirlpoolPda } = poolInitInfo;
|
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(
|
const tokenAccountA = await createAndMintToAssociatedTokenAccount(
|
||||||
provider,
|
provider,
|
||||||
tokenMintA,
|
tokenMintA,
|
||||||
mintAmount
|
mintAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenAccountB = await createAndMintToAssociatedTokenAccount(
|
const tokenAccountB = await createAndMintToAssociatedTokenAccount(
|
||||||
provider,
|
provider,
|
||||||
tokenMintB,
|
tokenMintB,
|
||||||
mintAmount
|
mintAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
poolInitInfo,
|
poolInitInfo,
|
||||||
configInitInfo,
|
configInitInfo,
|
||||||
|
@ -442,6 +445,31 @@ export interface FundedPositionInfo {
|
||||||
tickArrayUpper: PublicKey;
|
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(
|
export async function fundPositions(
|
||||||
ctx: WhirlpoolContext,
|
ctx: WhirlpoolContext,
|
||||||
poolInitInfo: InitPoolParams,
|
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 { Percentage } from "@orca-so/common-sdk";
|
||||||
|
import * as anchor from "@project-serum/anchor";
|
||||||
import { u64 } from "@solana/spl-token";
|
import { u64 } from "@solana/spl-token";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
import { TickSpacing } from ".";
|
import { TickSpacing } from ".";
|
||||||
import { WhirlpoolContext, WhirlpoolClient, Whirlpool, TICK_ARRAY_SIZE } from "../../src";
|
import { TICK_ARRAY_SIZE, Whirlpool, WhirlpoolClient, WhirlpoolContext } from "../../src";
|
||||||
import { FundedPositionParams, initTestPoolWithTokens, fundPositions } from "./init-utils";
|
import { FundedPositionParams, fundPositionsWithClient, initTestPoolWithTokens } from "./init-utils";
|
||||||
|
|
||||||
export interface SwapTestPoolParams {
|
export interface SwapTestPoolParams {
|
||||||
ctx: WhirlpoolContext;
|
ctx: WhirlpoolContext;
|
||||||
|
@ -29,19 +29,21 @@ export interface SwapTestSetup {
|
||||||
tickArrayAddresses: PublicKey[];
|
tickArrayAddresses: PublicKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupSwapTest(setup: SwapTestPoolParams) {
|
export async function setupSwapTest(setup: SwapTestPoolParams, tokenAIsNative = false) {
|
||||||
const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = await initTestPoolWithTokens(
|
const { poolInitInfo, whirlpoolPda, tokenAccountA, tokenAccountB } = await initTestPoolWithTokens(
|
||||||
setup.ctx,
|
setup.ctx,
|
||||||
setup.tickSpacing,
|
setup.tickSpacing,
|
||||||
setup.initSqrtPrice,
|
setup.initSqrtPrice,
|
||||||
setup.tokenMintAmount
|
setup.tokenMintAmount,
|
||||||
|
tokenAIsNative
|
||||||
);
|
);
|
||||||
|
|
||||||
const whirlpool = await setup.client.getPool(whirlpoolPda.publicKey, true);
|
const whirlpool = await setup.client.getPool(whirlpoolPda.publicKey, true);
|
||||||
|
|
||||||
await (await whirlpool.initTickArrayForTicks(setup.initArrayStartTicks))?.buildAndExecute();
|
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;
|
return whirlpool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { MathUtil, PDA, Percentage } from "@orca-so/common-sdk";
|
import { MathUtil, PDA, Percentage } from "@orca-so/common-sdk";
|
||||||
import { AnchorProvider } from "@project-serum/anchor";
|
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 { Keypair, PublicKey } from "@solana/web3.js";
|
||||||
import Decimal from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
import { createAndMintToAssociatedTokenAccount, createMint } from ".";
|
import { createAndMintToAssociatedTokenAccount, createMint } from ".";
|
||||||
|
@ -13,7 +13,7 @@ import {
|
||||||
OpenPositionParams,
|
OpenPositionParams,
|
||||||
PDAUtil,
|
PDAUtil,
|
||||||
PriceMath,
|
PriceMath,
|
||||||
Whirlpool,
|
Whirlpool
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { WhirlpoolContext } from "../../src/context";
|
import { WhirlpoolContext } from "../../src/context";
|
||||||
|
|
||||||
|
@ -46,9 +46,9 @@ export const generateDefaultConfigParams = (
|
||||||
return { configInitInfo, configKeypairs };
|
return { configInitInfo, configKeypairs };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createInOrderMints = async (context: WhirlpoolContext) => {
|
export const createInOrderMints = async (context: WhirlpoolContext, tokenAIsNative = false) => {
|
||||||
const provider = context.provider;
|
const provider = context.provider;
|
||||||
const tokenXMintPubKey = await createMint(provider);
|
const tokenXMintPubKey = tokenAIsNative ? NATIVE_MINT : await createMint(provider);
|
||||||
const tokenYMintPubKey = await createMint(provider);
|
const tokenYMintPubKey = await createMint(provider);
|
||||||
|
|
||||||
let tokenAMintPubKey, tokenBMintPubKey;
|
let tokenAMintPubKey, tokenBMintPubKey;
|
||||||
|
@ -69,9 +69,10 @@ export const generateDefaultInitPoolParams = async (
|
||||||
feeTierKey: PublicKey,
|
feeTierKey: PublicKey,
|
||||||
tickSpacing: number,
|
tickSpacing: number,
|
||||||
initSqrtPrice = MathUtil.toX64(new Decimal(5)),
|
initSqrtPrice = MathUtil.toX64(new Decimal(5)),
|
||||||
funder?: PublicKey
|
funder?: PublicKey,
|
||||||
|
tokenAIsNative = false
|
||||||
): Promise<InitPoolParams> => {
|
): Promise<InitPoolParams> => {
|
||||||
const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context);
|
const [tokenAMintPubKey, tokenBMintPubKey] = await createInOrderMints(context, tokenAIsNative);
|
||||||
|
|
||||||
const whirlpoolPda = PDAUtil.getWhirlpool(
|
const whirlpoolPda = PDAUtil.getWhirlpool(
|
||||||
context.program.programId,
|
context.program.programId,
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { deriveATA } from "@orca-so/common-sdk";
|
import { deriveATA, TransactionBuilder, ZERO } from "@orca-so/common-sdk";
|
||||||
import { BN, AnchorProvider, web3 } from "@project-serum/anchor";
|
import { createWSOLAccountInstructions } from "@orca-so/common-sdk/dist/helpers/token-instructions";
|
||||||
|
import { AnchorProvider, BN, web3 } from "@project-serum/anchor";
|
||||||
import {
|
import {
|
||||||
|
AccountLayout,
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
AuthorityType,
|
AuthorityType,
|
||||||
|
NATIVE_MINT,
|
||||||
Token,
|
Token,
|
||||||
TOKEN_PROGRAM_ID,
|
TOKEN_PROGRAM_ID,
|
||||||
u64,
|
u64
|
||||||
} from "@solana/spl-token";
|
} from "@solana/spl-token";
|
||||||
import { TEST_TOKEN_PROGRAM_ID } from "./test-consts";
|
import { TEST_TOKEN_PROGRAM_ID } from "./test-consts";
|
||||||
|
|
||||||
|
@ -154,6 +157,20 @@ export async function createAndMintToAssociatedTokenAccount(
|
||||||
): Promise<web3.PublicKey> {
|
): Promise<web3.PublicKey> {
|
||||||
const destinationWalletKey = destinationWallet ? destinationWallet : provider.wallet.publicKey;
|
const destinationWalletKey = destinationWallet ? destinationWallet : provider.wallet.publicKey;
|
||||||
const payerKey = payer ? payer : 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(
|
const tokenAccount = await createAssociatedTokenAccount(
|
||||||
provider,
|
provider,
|
||||||
mint,
|
mint,
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -565,14 +565,15 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
"@orca-so/common-sdk@~0.1.0":
|
"@orca-so/common-sdk@~0.1.1":
|
||||||
version "0.1.0"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.1.0.tgz#25050f18e95bab6a7ef9b10e2d3bd44699ce9836"
|
resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.1.1.tgz#18a3a562f1b57f970f2115f453c7e00aa05034cf"
|
||||||
integrity sha512-WH6mjrrJyuUMq0DC41jWW6opEIq5Ip/UOf8h7z/gM9Zx+9156JwP/dm8N5GJTNnIG6GYFfhemyNNPj1y3U5LIw==
|
integrity sha512-OKFwr5VgzLKmM/C6xQom4y1jxrihuhKXhDwYMbgPLJW4SllePOfS6SG5fp9/17SvyuWL/zna8Z6ZW2QHpX1Yxw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@project-serum/anchor" "~0.25.0"
|
"@project-serum/anchor" "~0.25.0"
|
||||||
"@solana/spl-token" "0.1.8"
|
"@solana/spl-token" "0.1.8"
|
||||||
decimal.js "^10.3.1"
|
decimal.js "^10.3.1"
|
||||||
|
tiny-invariant "^1.2.0"
|
||||||
|
|
||||||
"@orca-so/whirlpool-client-sdk@0.0.7":
|
"@orca-so/whirlpool-client-sdk@0.0.7":
|
||||||
version "0.0.7"
|
version "0.0.7"
|
||||||
|
@ -747,10 +748,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
"@types/bn.js@^5.1.0":
|
"@types/bn.js@~5.1.0":
|
||||||
version "5.1.0"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68"
|
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682"
|
||||||
integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==
|
integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue