From bb47854d784ad5c6dd561db71786ce8b267bad52 Mon Sep 17 00:00:00 2001 From: codewithgun Date: Fri, 3 Jan 2025 15:38:31 +0800 Subject: [PATCH] wip --- ts-client/src/dlmm/helpers/derive.ts | 11 + ts-client/src/dlmm/index.ts | 73 ++- ts-client/src/test/sdk_token2022.test.ts | 778 ++++++++++++++++++++++- 3 files changed, 824 insertions(+), 38 deletions(-) diff --git a/ts-client/src/dlmm/helpers/derive.ts b/ts-client/src/dlmm/helpers/derive.ts index 1ae39262..4668401f 100644 --- a/ts-client/src/dlmm/helpers/derive.ts +++ b/ts-client/src/dlmm/helpers/derive.ts @@ -217,3 +217,14 @@ export function deriveEventAuthority(programId: PublicKey) { programId ); } + +export function deriveRewardVault( + lbPair: PublicKey, + rewardIndex: BN, + programId: PublicKey +) { + return PublicKey.findProgramAddressSync( + [lbPair.toBuffer(), rewardIndex.toArrayLike(Buffer, "le", 8)], + programId + ); +} diff --git a/ts-client/src/dlmm/index.ts b/ts-client/src/dlmm/index.ts index 2084c4c6..4c219325 100644 --- a/ts-client/src/dlmm/index.ts +++ b/ts-client/src/dlmm/index.ts @@ -2458,8 +2458,8 @@ export class DLMM { tokenYMint: this.lbPair.tokenYMint, binArrayBitmapExtension, sender: user, - tokenXProgram: TOKEN_PROGRAM_ID, - tokenYProgram: TOKEN_PROGRAM_ID, + tokenXProgram: this.tokenX.owner, + tokenYProgram: this.tokenY.owner, memoProgram: MEMO_PROGRAM_ID, }; @@ -4020,10 +4020,6 @@ export class DLMM { .div(new Decimal(outAmountWithoutSlippage.toString())) .mul(new Decimal(100)); - const minOutAmount = totalOutAmount - .mul(new BN(BASIS_POINT_MAX).sub(allowedSlippage)) - .div(new BN(BASIS_POINT_MAX)); - const endPrice = getPriceOfBinByBinId( activeId.toNumber(), this.lbPair.binStep @@ -4035,6 +4031,10 @@ export class DLMM { this.clock.epoch.toNumber() ).amount; + const minOutAmount = transferFeeExcludedAmountOut + .mul(new BN(BASIS_POINT_MAX).sub(allowedSlippage)) + .div(new BN(BASIS_POINT_MAX)); + return { consumedInAmount: transferFeeIncludedInAmount, outAmount: transferFeeExcludedAmountOut, @@ -4215,9 +4215,6 @@ export class DLMM { closeWrappedSOLIx && postInstructions.push(closeWrappedSOLIx); } - let swapForY = true; - if (outToken.equals(tokenXMint)) swapForY = false; - // TODO: needs some refinement in case binArray not yet initialized const binArrays: AccountMeta[] = binArraysPubkey.map((pubkey) => { return { @@ -4299,12 +4296,15 @@ export class DLMM { user, binArraysPubkey, }: SwapParams): Promise { - const { tokenXMint, tokenYMint, reserveX, reserveY, activeId, oracle } = - await this.program.account.lbPair.fetch(lbPair); - const preInstructions: TransactionInstruction[] = []; const postInstructions: Array = []; + const [inTokenProgram, outTokenProgram] = inToken.equals( + this.lbPair.tokenXMint + ) + ? [this.tokenX.owner, this.tokenY.owner] + : [this.tokenY.owner, this.tokenX.owner]; + const [ { ataPubKey: userTokenIn, ix: createInTokenAccountIx }, { ataPubKey: userTokenOut, ix: createOutTokenAccountIx }, @@ -4313,13 +4313,13 @@ export class DLMM { this.program.provider.connection, inToken, user, - this.tokenX.owner + inTokenProgram ), getOrCreateATAInstruction( this.program.provider.connection, outToken, user, - this.tokenY.owner + outTokenProgram ), ]); createInTokenAccountIx && preInstructions.push(createInTokenAccountIx); @@ -4342,9 +4342,6 @@ export class DLMM { closeWrappedSOLIx && postInstructions.push(closeWrappedSOLIx); } - let swapForY = true; - if (outToken.equals(tokenXMint)) swapForY = false; - // TODO: needs some refinement in case binArray not yet initialized const binArrays: AccountMeta[] = binArraysPubkey.map((pubkey) => { return { @@ -4361,10 +4358,10 @@ export class DLMM { .swap2(inAmount, minOutAmount, { slices }) .accounts({ lbPair, - reserveX, - reserveY, - tokenXMint, - tokenYMint, + reserveX: this.lbPair.reserveX, + reserveY: this.lbPair.reserveY, + tokenXMint: this.lbPair.tokenXMint, + tokenYMint: this.lbPair.tokenYMint, tokenXProgram: this.tokenX.owner, tokenYProgram: this.tokenY.owner, user, @@ -4373,7 +4370,7 @@ export class DLMM { binArrayBitmapExtension: this.binArrayBitmapExtension ? this.binArrayBitmapExtension.publicKey : null, - oracle, + oracle: this.lbPair.oracle, hostFeeIn: null, memoProgram: MEMO_PROGRAM_ID, }) @@ -4523,19 +4520,39 @@ export class DLMM { public async claimSwapFee({ owner, position, + binRange, }: { owner: PublicKey; position: LbPosition; + binRange?: { + minBinId: BN; + maxBinId: BN; + }; }): Promise { - const claimFeeTx = await this.createClaimSwapFeeMethod({ owner, position }); + const claimFeeTx = await this.createClaimSwapFeeMethod({ + owner, + position, + shouldIncludePretIx: true, + shouldIncludePostIx: true, + binRange, + }); + + const setCUIx = await getEstimatedComputeUnitIxWithBuffer( + this.program.provider.connection, + claimFeeTx.instructions, + owner + ); + + const instructions = [setCUIx, ...claimFeeTx.instructions]; const { blockhash, lastValidBlockHeight } = await this.program.provider.connection.getLatestBlockhash("confirmed"); + return new Transaction({ blockhash, lastValidBlockHeight, feePayer: owner, - }).add(claimFeeTx); + }).add(...instructions); } /** @@ -4548,7 +4565,8 @@ export class DLMM { public async claimAllSwapFee({ owner, positions, - }: { + }: // todo: range + { owner: PublicKey; positions: LbPosition[]; }): Promise { @@ -5519,6 +5537,7 @@ export class DLMM { }); }); + // TODO: Merge it into above iteration and allow quote chunk swap fee const { feeX, feeY } = await this.getClaimableSwapFee( program, position, @@ -5900,7 +5919,7 @@ export class DLMM { position: position.publicKey, rewardVault: rewardInfo.vault, rewardMint: rewardInfo.mint, - tokenProgram: TOKEN_PROGRAM_ID, + tokenProgram: this.rewards[i].owner, userTokenAccount: ataPubKey, memoProgram: MEMO_PROGRAM_ID, }) @@ -5956,12 +5975,14 @@ export class DLMM { this.program.provider.connection, this.tokenX.publicKey, walletToReceiveFee, + this.tokenX.owner, owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, walletToReceiveFee, + this.tokenY.owner, owner ), ]); diff --git a/ts-client/src/test/sdk_token2022.test.ts b/ts-client/src/test/sdk_token2022.test.ts index ffc54144..10b3e7e6 100644 --- a/ts-client/src/test/sdk_token2022.test.ts +++ b/ts-client/src/test/sdk_token2022.test.ts @@ -9,13 +9,13 @@ import { getAssociatedTokenAddressSync, getMintLen, getOrCreateAssociatedTokenAccount, - getTransferFeeAmount, mintTo, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, unpackAccount, } from "@solana/spl-token"; import { + AccountInfo, Cluster, Connection, Keypair, @@ -25,7 +25,7 @@ import { SystemProgram, Transaction, } from "@solana/web3.js"; -import { BN } from "bn.js"; +import BN from "bn.js"; import fs from "fs"; import { DLMM } from "../dlmm"; import { LBCLMM_PROGRAM_IDS } from "../dlmm/constants"; @@ -34,6 +34,7 @@ import { deriveLbPairWithPresetParamWithIndexKey, derivePermissionLbPair, derivePresetParameterWithIndex, + deriveRewardVault, deriveTokenBadge, toAmountsBothSideByStrategy, } from "../dlmm/helpers"; @@ -44,6 +45,7 @@ import { createTransferHookCounterProgram, TRANSFER_HOOK_COUNTER_PROGRAM_ID, } from "./external/program"; +import Decimal from "decimal.js"; const keypairBuffer = fs.readFileSync( "../keys/localnet/admin-bossj3JvwiNK7pvjr149DqdtJxf2gdygbcmEPTkb2F1.json", @@ -56,12 +58,15 @@ const keypair = Keypair.fromSecretKey( const btcDecimal = 6; const usdcDecimal = 6; +const metDecimal = 6; const BTCKeypair = Keypair.generate(); const USDCKeypair = Keypair.generate(); +const METKeypair = Keypair.generate(); const BTC2022 = BTCKeypair.publicKey; const USDC = USDCKeypair.publicKey; +const MET2022 = METKeypair.publicKey; const transferFeeBps = 500; // 5% const maxFee = BigInt(100_000) * BigInt(10 ** btcDecimal); @@ -76,6 +81,9 @@ const provider = new AnchorProvider( ); const program = new Program(IDL, LBCLMM_PROGRAM_IDS["localhost"], provider); +const widePositionKeypair = Keypair.generate(); +const tightPositionKeypair = Keypair.generate(); + type Opt = { cluster?: Cluster | "localhost"; programId?: PublicKey; @@ -146,7 +154,7 @@ describe("SDK token2022 test", () => { mintLen ); - const transaction = new Transaction() + const createBtcTx = new Transaction() .add( SystemProgram.createAccount({ fromPubkey: keypair.publicKey, @@ -186,7 +194,7 @@ describe("SDK token2022 test", () => { await sendAndConfirmTransaction( connection, - transaction, + createBtcTx, [keypair, BTCKeypair], { commitment: "confirmed" } ); @@ -231,6 +239,81 @@ describe("SDK token2022 test", () => { }, TOKEN_2022_PROGRAM_ID ); + + const createMetTx = new Transaction() + .add( + SystemProgram.createAccount({ + fromPubkey: keypair.publicKey, + newAccountPubkey: METKeypair.publicKey, + space: mintLen, + lamports: minLamports, + programId: TOKEN_2022_PROGRAM_ID, + }) + ) + .add( + createInitializeTransferFeeConfigInstruction( + MET2022, + keypair.publicKey, + keypair.publicKey, + transferFeeBps, + maxFee, + TOKEN_2022_PROGRAM_ID + ) + ) + .add( + createInitializeTransferHookInstruction( + MET2022, + keypair.publicKey, + TRANSFER_HOOK_COUNTER_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID + ) + ) + .add( + createInitializeMintInstruction( + MET2022, + metDecimal, + keypair.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction( + connection, + createMetTx, + [keypair, METKeypair], + { commitment: "confirmed" } + ); + + const userMetAccount = await getOrCreateAssociatedTokenAccount( + connection, + keypair, + MET2022, + keypair.publicKey, + true, + "confirmed", + { + commitment: "confirmed", + }, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const userMetAta = userMetAccount.address; + + await mintTo( + connection, + keypair, + MET2022, + userMetAta, + keypair, + BigInt(1_000_000_000) * BigInt(10 ** metDecimal), + [], + { + commitment: "confirmed", + }, + TOKEN_2022_PROGRAM_ID + ); }); // DLMM related setup @@ -274,6 +357,18 @@ describe("SDK token2022 test", () => { tokenMint: BTC2022, }) .rpc(); + + const [metTokenBadge] = deriveTokenBadge(MET2022, program.programId); + + await program.methods + .initializeTokenBadge() + .accounts({ + tokenBadge: metTokenBadge, + admin: keypair.publicKey, + systemProgram: SystemProgram.programId, + tokenMint: MET2022, + }) + .rpc(); }); it("getAllPresetParameters return created preset parameter 2", async () => { @@ -435,9 +530,33 @@ describe("SDK token2022 test", () => { }); }); - describe("Position", () => { - const widePositionKeypair = Keypair.generate(); - const tightPositionKeypair = Keypair.generate(); + describe("Position management", () => { + beforeAll(async () => { + const rewardIndex = new BN(0); + const rewardDuration = new BN(3600); + const funder = keypair.publicKey; + + const [rewardVault] = deriveRewardVault( + pairKey, + rewardIndex, + program.programId + ); + + const [tokenBadge] = deriveTokenBadge(MET2022, program.programId); + + await program.methods + .initializeReward(rewardIndex, rewardDuration, funder) + .accounts({ + lbPair: pairKey, + rewardMint: MET2022, + rewardVault, + admin: keypair.publicKey, + tokenBadge, + tokenProgram: TOKEN_2022_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .rpc(); + }); it("createEmptyPosition", async () => { const dlmm = await DLMM.create(connection, pairKey, opt); @@ -574,7 +693,9 @@ describe("SDK token2022 test", () => { binCount = positionData.upperBinId - positionData.lowerBinId + 1; expect(positionData.positionBinData.length).toBe(binCount); }); + }); + describe("Add liquidity", () => { it("Add liquidity by strategy", async () => { const totalXAmount = new BN(100_000).mul(new BN(10 ** btcDecimal)); const totalYAmount = new BN(100_000).mul(new BN(10 ** usdcDecimal)); @@ -612,22 +733,655 @@ describe("SDK token2022 test", () => { slippage: 0, }); - const beforeReserveXAccount = await connection.getAccountInfo( - dlmm.tokenX.reserve - ); + const [beforeReserveXAccount, beforeReserveYAccount] = + await connection.getMultipleAccountsInfo([ + dlmm.tokenX.reserve, + dlmm.tokenY.reserve, + ]); await sendAndConfirmTransaction(connection, addLiquidityTx, [keypair]); position = await dlmm.getPosition(widePositionKeypair.publicKey); - const afterReserveXAccount = await connection.getAccountInfo( - dlmm.tokenX.reserve + const [afterReserveXAccount, afterReserveYAccount] = + await connection.getMultipleAccountsInfo([ + dlmm.tokenX.reserve, + dlmm.tokenY.reserve, + ]); + + const [computedInAmountX, computedInAmountY] = computedInBinAmount.reduce( + ([totalXAmount, totalYAmount], { amountX, amountY }) => { + return [totalXAmount.add(amountX), totalYAmount.add(amountY)]; + }, + [new BN(0), new BN(0)] + ); + + expect(computedInAmountX.lte(totalXAmount)).toBeTruthy(); + expect(computedInAmountY.lte(totalYAmount)).toBeTruthy(); + + const beforeReserveX = unpackAccount( + dlmm.tokenX.reserve, + beforeReserveXAccount, + beforeReserveXAccount.owner + ); + + const beforeReserveY = unpackAccount( + dlmm.tokenY.reserve, + beforeReserveYAccount, + beforeReserveYAccount.owner + ); + + const afterReserveX = unpackAccount( + dlmm.tokenX.reserve, + afterReserveXAccount, + afterReserveXAccount.owner + ); + + const afterReserveY = unpackAccount( + dlmm.tokenY.reserve, + afterReserveYAccount, + afterReserveYAccount.owner + ); + + const reserveXReceivedAmount = + afterReserveX.amount - beforeReserveX.amount; + + const reserveYReceivedAmount = + afterReserveY.amount - beforeReserveY.amount; + + expect(new BN(reserveXReceivedAmount.toString()).toString()).toBe( + computedInAmountX.toString() + ); + + expect(new BN(reserveYReceivedAmount.toString()).toString()).toBe( + computedInAmountY.toString() + ); + + const positionXAmount = new BN(position.positionData.totalXAmount); + const positionYAmount = new BN(position.positionData.totalYAmount); + + const xDiff = computedInAmountX.sub(positionXAmount); + const yDiff = computedInAmountY.sub(positionYAmount); + + expect(xDiff.lte(new BN(1))).toBeTruthy(); + expect(yDiff.lte(new BN(1))).toBeTruthy(); + + expect(positionXAmount.add(xDiff).toString()).toBe( + computedInAmountX.toString() + ); + + expect(positionYAmount.add(yDiff).toString()).toBe( + computedInAmountY.toString() + ); + }); + + it("Initialize position and add liquidity by strategy", async () => { + const totalXAmount = new BN(100_000).mul(new BN(10 ** btcDecimal)); + const totalYAmount = new BN(100_000).mul(new BN(10 ** usdcDecimal)); + + const dlmm = await DLMM.create(connection, pairKey, opt); + + const minBinId = dlmm.lbPair.activeId - 30; + const maxBinId = dlmm.lbPair.activeId + 30; + + const activeBinInfo = await dlmm.getActiveBin(); + + const computedInBinAmount = toAmountsBothSideByStrategy( + dlmm.lbPair.activeId, + dlmm.lbPair.binStep, + minBinId, + maxBinId, + totalXAmount, + totalYAmount, + activeBinInfo.xAmount, + activeBinInfo.yAmount, + StrategyType.SpotImBalanced, + dlmm.tokenX.mint, + dlmm.tokenY.mint, + dlmm.clock + ); + + const initAndAddLiquidityTx = + await dlmm.initializePositionAndAddLiquidityByStrategy({ + positionPubKey: tightPositionKeypair.publicKey, + totalXAmount, + totalYAmount, + strategy: { + strategyType: StrategyType.SpotImBalanced, + minBinId, + maxBinId, + }, + slippage: 0, + user: keypair.publicKey, + }); + + const [beforeReserveXAccount, beforeReserveYAccount] = + await connection.getMultipleAccountsInfo([ + dlmm.tokenX.reserve, + dlmm.tokenY.reserve, + ]); + + await sendAndConfirmTransaction(connection, initAndAddLiquidityTx, [ + keypair, + tightPositionKeypair, + ]); + + const [afterReserveXAccount, afterReserveYAccount] = + await connection.getMultipleAccountsInfo([ + dlmm.tokenX.reserve, + dlmm.tokenY.reserve, + ]); + + await dlmm.refetchStates(); + + const position = await dlmm.getPosition(tightPositionKeypair.publicKey); + expect(position.positionData.lowerBinId).toBe(minBinId); + expect(position.positionData.upperBinId).toBe(maxBinId); + + const [computedInAmountX, computedInAmountY] = computedInBinAmount.reduce( + ([totalXAmount, totalYAmount], { amountX, amountY }) => { + return [totalXAmount.add(amountX), totalYAmount.add(amountY)]; + }, + [new BN(0), new BN(0)] ); + expect(computedInAmountX.lte(totalXAmount)).toBeTruthy(); + expect(computedInAmountY.lte(totalYAmount)).toBeTruthy(); + const beforeReserveX = unpackAccount( dlmm.tokenX.reserve, beforeReserveXAccount, beforeReserveXAccount.owner ); + + const beforeReserveY = unpackAccount( + dlmm.tokenY.reserve, + beforeReserveYAccount, + beforeReserveYAccount.owner + ); + + const afterReserveX = unpackAccount( + dlmm.tokenX.reserve, + afterReserveXAccount, + afterReserveXAccount.owner + ); + + const afterReserveY = unpackAccount( + dlmm.tokenY.reserve, + afterReserveYAccount, + afterReserveYAccount.owner + ); + + const reserveXReceivedAmount = + afterReserveX.amount - beforeReserveX.amount; + + const reserveYReceivedAmount = + afterReserveY.amount - beforeReserveY.amount; + + expect(new BN(reserveXReceivedAmount.toString()).toString()).toBe( + computedInAmountX.toString() + ); + + expect(new BN(reserveYReceivedAmount.toString()).toString()).toBe( + computedInAmountY.toString() + ); + + const positionXAmount = new BN(position.positionData.totalXAmount); + const positionYAmount = new BN(position.positionData.totalYAmount); + + const xDiff = computedInAmountX.sub(positionXAmount); + const yDiff = computedInAmountY.sub(positionYAmount); + + expect(xDiff.lte(new BN(1))).toBeTruthy(); + expect(yDiff.lte(new BN(1))).toBeTruthy(); + + expect(positionXAmount.add(xDiff).toString()).toBe( + computedInAmountX.toString() + ); + + expect(positionYAmount.add(yDiff).toString()).toBe( + computedInAmountY.toString() + ); + }); + }); + + describe("Swap", () => { + it("Swap quote X into Y and execute swap", async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + const inAmount = new BN(100_000).mul(new BN(10 ** btcDecimal)); + const swapForY = true; + + const bidBinArrays = await dlmm.getBinArrayForSwap(swapForY, 3); + const quoteResult = dlmm.swapQuote( + inAmount, + swapForY, + new BN(0), + bidBinArrays, + false + ); + + const swapTx = await dlmm.swap({ + inAmount, + inToken: dlmm.tokenX.publicKey, + outToken: dlmm.tokenY.publicKey, + minOutAmount: quoteResult.minOutAmount, + lbPair: pairKey, + user: keypair.publicKey, + binArraysPubkey: bidBinArrays.map((b) => b.publicKey), + }); + + const [beforeUserXAccount, beforeUserYAccount] = + await connection.getMultipleAccountsInfo([ + getAssociatedTokenAddressSync( + dlmm.tokenX.publicKey, + keypair.publicKey, + true, + dlmm.tokenX.owner + ), + getAssociatedTokenAddressSync( + dlmm.tokenY.publicKey, + keypair.publicKey, + true, + dlmm.tokenY.owner + ), + ]); + + await sendAndConfirmTransaction(connection, swapTx, [keypair]); + + const [afterUserXAccount, afterUserYAccount] = + await connection.getMultipleAccountsInfo([ + getAssociatedTokenAddressSync( + dlmm.tokenX.publicKey, + keypair.publicKey, + true, + dlmm.tokenX.owner + ), + getAssociatedTokenAddressSync( + dlmm.tokenY.publicKey, + keypair.publicKey, + true, + dlmm.tokenY.owner + ), + ]); + + const beforeUserX = unpackAccount( + dlmm.tokenX.publicKey, + beforeUserXAccount, + beforeUserXAccount.owner + ); + + const beforeUserY = unpackAccount( + dlmm.tokenY.publicKey, + beforeUserYAccount, + beforeUserYAccount.owner + ); + + const afterUserX = unpackAccount( + dlmm.tokenX.publicKey, + afterUserXAccount, + afterUserXAccount.owner + ); + + const afterUserY = unpackAccount( + dlmm.tokenY.publicKey, + afterUserYAccount, + afterUserYAccount.owner + ); + + const consumedXAmount = new BN( + (beforeUserX.amount - afterUserX.amount).toString() + ); + const receivedYAmount = new BN( + (afterUserY.amount - beforeUserY.amount).toString() + ); + + expect(consumedXAmount.toString()).toBe( + quoteResult.consumedInAmount.toString() + ); + expect(receivedYAmount.toString()).toBe(quoteResult.outAmount.toString()); + }); + + it("Swap quote Y into X and execute swap", async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + const inAmount = new BN(100_000).mul(new BN(10 ** usdcDecimal)); + const swapForY = false; + + const askBinArrays = await dlmm.getBinArrayForSwap(swapForY, 3); + const quoteResult = dlmm.swapQuote( + inAmount, + swapForY, + new BN(0), + askBinArrays, + false + ); + + const swapTx = await dlmm.swap({ + inAmount, + inToken: dlmm.tokenY.publicKey, + outToken: dlmm.tokenX.publicKey, + minOutAmount: quoteResult.minOutAmount, + lbPair: pairKey, + user: keypair.publicKey, + binArraysPubkey: askBinArrays.map((b) => b.publicKey), + }); + + const [beforeUserXAccount, beforeUserYAccount] = + await connection.getMultipleAccountsInfo([ + getAssociatedTokenAddressSync( + dlmm.tokenX.publicKey, + keypair.publicKey, + true, + dlmm.tokenX.owner + ), + getAssociatedTokenAddressSync( + dlmm.tokenY.publicKey, + keypair.publicKey, + true, + dlmm.tokenY.owner + ), + ]); + + await sendAndConfirmTransaction(connection, swapTx, [keypair]); + + const [afterUserXAccount, afterUserYAccount] = + await connection.getMultipleAccountsInfo([ + getAssociatedTokenAddressSync( + dlmm.tokenX.publicKey, + keypair.publicKey, + true, + dlmm.tokenX.owner + ), + getAssociatedTokenAddressSync( + dlmm.tokenY.publicKey, + keypair.publicKey, + true, + dlmm.tokenY.owner + ), + ]); + + const beforeUserX = unpackAccount( + dlmm.tokenX.publicKey, + beforeUserXAccount, + beforeUserXAccount.owner + ); + + const beforeUserY = unpackAccount( + dlmm.tokenY.publicKey, + beforeUserYAccount, + beforeUserYAccount.owner + ); + + const afterUserX = unpackAccount( + dlmm.tokenX.publicKey, + afterUserXAccount, + afterUserXAccount.owner + ); + + const afterUserY = unpackAccount( + dlmm.tokenY.publicKey, + afterUserYAccount, + afterUserYAccount.owner + ); + + const consumedYAmount = new BN( + (beforeUserY.amount - afterUserY.amount).toString() + ); + const receivedXAmount = new BN( + (afterUserX.amount - beforeUserX.amount).toString() + ); + + expect(consumedYAmount.toString()).toBe( + quoteResult.consumedInAmount.toString() + ); + expect(receivedXAmount.toString()).toBe(quoteResult.outAmount.toString()); + }); + }); + + describe("Claim fees and rewards", () => { + let userXAta: PublicKey, userYAta: PublicKey; + + beforeEach(async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + + // Generate some swap fees + const inAmount = new BN(100_000); + for (const [inToken, outToken] of [ + [dlmm.tokenX, dlmm.tokenY], + [dlmm.tokenY, dlmm.tokenX], + ]) { + const binArraysPubkey = await dlmm + .getBinArrayForSwap( + inToken.publicKey.equals(dlmm.tokenX.publicKey), + 3 + ) + .then((b) => b.map((b) => b.publicKey)); + + const swapTx = await dlmm.swap({ + inToken: inToken.publicKey, + outToken: outToken.publicKey, + inAmount: inAmount.mul(new BN(10 ** inToken.mint.decimals)), + minOutAmount: new BN(0), + user: keypair.publicKey, + lbPair: pairKey, + binArraysPubkey, + }); + + await sendAndConfirmTransaction(connection, swapTx, [keypair]); + await dlmm.refetchStates(); + } + + userXAta = getAssociatedTokenAddressSync( + dlmm.tokenX.publicKey, + keypair.publicKey, + true, + dlmm.tokenX.owner + ); + + userYAta = getAssociatedTokenAddressSync( + dlmm.tokenY.publicKey, + keypair.publicKey, + true, + dlmm.tokenY.owner + ); + }); + + const assertUserTokenBalanceWithDelta = ( + beforeAccount: AccountInfo>, + afterAccount: AccountInfo>, + expectedAmount: BN + ) => { + const before = unpackAccount( + PublicKey.default, + beforeAccount, + beforeAccount.owner + ); + + const after = unpackAccount( + PublicKey.default, + afterAccount, + afterAccount.owner + ); + + const delta = + before.amount > after.amount + ? before.amount - after.amount + : after.amount - before.amount; + + const deltaBn = new BN(delta.toString()); + expect(deltaBn.toString()).toBe(expectedAmount.toString()); + }; + + it("Claim all swap fees", async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + + const [beforeUserXAccount, beforeUserYAccount] = + await connection.getMultipleAccountsInfo([userXAta, userYAta]); + + const [widePosition, tightPosition] = await Promise.all([ + dlmm.getPosition(widePositionKeypair.publicKey), + dlmm.getPosition(tightPositionKeypair.publicKey), + ]); + + const totalClaimableFeeX = + widePosition.positionData.feeXExcludeTransferFee.add( + tightPosition.positionData.feeXExcludeTransferFee + ); + + const totalClaimableFeeY = + widePosition.positionData.feeYExcludeTransferFee.add( + tightPosition.positionData.feeYExcludeTransferFee + ); + + const claimFeeTxs = await dlmm.claimAllSwapFee({ + owner: keypair.publicKey, + positions: [widePosition, tightPosition], + }); + + await Promise.all( + claimFeeTxs.map((tx) => + sendAndConfirmTransaction(connection, tx, [keypair]) + ) + ); + + const [afterUserXAccount, afterUserYAccount] = + await connection.getMultipleAccountsInfo([userXAta, userYAta]); + + assertUserTokenBalanceWithDelta( + beforeUserXAccount, + afterUserXAccount, + totalClaimableFeeX + ); + + assertUserTokenBalanceWithDelta( + beforeUserYAccount, + afterUserYAccount, + totalClaimableFeeY + ); + }); + + it("Claim swap fee", async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + + for (const positionKey of [ + widePositionKeypair.publicKey, + tightPositionKeypair.publicKey, + ]) { + const position = await dlmm.getPosition(positionKey); + + const [beforeUserXAccount, beforeUserYAccount] = + await connection.getMultipleAccountsInfo([userXAta, userYAta]); + + const claimFeeTx = await dlmm.claimSwapFee({ + owner: keypair.publicKey, + position, + }); + + await sendAndConfirmTransaction(connection, claimFeeTx, [keypair]); + + const [afterUserXAccount, afterUserYAccount] = + await connection.getMultipleAccountsInfo([userXAta, userYAta]); + + assertUserTokenBalanceWithDelta( + beforeUserXAccount, + afterUserXAccount, + position.positionData.feeXExcludeTransferFee + ); + + assertUserTokenBalanceWithDelta( + beforeUserYAccount, + afterUserYAccount, + position.positionData.feeYExcludeTransferFee + ); + } + }); + + it("Claim swap fee chunk", async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + + for (const positionKey of [ + widePositionKeypair.publicKey, + tightPositionKeypair.publicKey, + ]) { + const position = await dlmm.getPosition(positionKey); + + const binRangeToShrink = + (position.positionData.upperBinId - + position.positionData.lowerBinId) * + 0.4; + + const minBinId = new BN( + Math.ceil(position.positionData.lowerBinId + binRangeToShrink) + ); + + const maxBinId = new BN( + Math.floor(position.positionData.upperBinId - binRangeToShrink) + ); + + const [beforeUserXAccount, beforeUserYAccount] = + await connection.getMultipleAccountsInfo([userXAta, userYAta]); + + const claimFeeTx = await dlmm.claimSwapFee({ + owner: keypair.publicKey, + position, + binRange: { + minBinId, + maxBinId, + }, + }); + + await sendAndConfirmTransaction(connection, claimFeeTx, [keypair]); + + const [afterUserXAccount, afterUserYAccount] = + await connection.getMultipleAccountsInfo([userXAta, userYAta]); + + const beforeUserX = unpackAccount( + dlmm.tokenX.publicKey, + beforeUserXAccount, + beforeUserXAccount.owner + ); + + const afterUserX = unpackAccount( + dlmm.tokenX.publicKey, + afterUserXAccount, + afterUserXAccount.owner + ); + + const beforeUserY = unpackAccount( + dlmm.tokenY.publicKey, + beforeUserYAccount, + beforeUserYAccount.owner + ); + + const afterUserY = unpackAccount( + dlmm.tokenY.publicKey, + afterUserYAccount, + afterUserYAccount.owner + ); + + const claimedAmountX = new BN( + (afterUserX.amount - beforeUserX.amount).toString() + ); + + const claimedAmountY = new BN( + (afterUserY.amount - beforeUserY.amount).toString() + ); + + console.log( + claimedAmountX.toString(), + position.positionData.feeXExcludeTransferFee.toString() + ); + + console.log( + claimedAmountY.toString(), + position.positionData.feeYExcludeTransferFee.toString() + ); + + expect( + claimedAmountX.lt(position.positionData.feeXExcludeTransferFee) + ).toBeTruthy(); + expect( + claimedAmountY.lt(position.positionData.feeYExcludeTransferFee) + ).toBeTruthy(); + } }); });