diff --git a/artifacts/lb_clmm.so b/artifacts/lb_clmm.so index f32b170..56fa243 100644 Binary files a/artifacts/lb_clmm.so and b/artifacts/lb_clmm.so differ diff --git a/ts-client/src/dlmm/helpers/derive.ts b/ts-client/src/dlmm/helpers/derive.ts index 1ae3926..4668401 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/helpers/index.ts b/ts-client/src/dlmm/helpers/index.ts index 542a6ba..8740644 100644 --- a/ts-client/src/dlmm/helpers/index.ts +++ b/ts-client/src/dlmm/helpers/index.ts @@ -1,5 +1,6 @@ import { BN, EventParser } from "@coral-xyz/anchor"; import { + ASSOCIATED_TOKEN_PROGRAM_ID, NATIVE_MINT, TOKEN_PROGRAM_ID, TokenAccountNotFoundError, @@ -37,11 +38,7 @@ export function chunks(array: T[], size: number): T[][] { ); } -export function range( - min: number, - max: number, - mapfn: (i: number) => T -) { +export function range(min: number, max: number, mapfn: (i: number) => T) { const length = max - min + 1; return Array.from({ length }, (_, i) => mapfn(min + i)); } @@ -93,17 +90,21 @@ export const getOrCreateATAInstruction = async ( connection: Connection, tokenMint: PublicKey, owner: PublicKey, + programId?: PublicKey, payer: PublicKey = owner, allowOwnerOffCurve = true ): Promise => { + programId = programId ?? TOKEN_PROGRAM_ID; const toAccount = getAssociatedTokenAddressSync( tokenMint, owner, - allowOwnerOffCurve + allowOwnerOffCurve, + programId, + ASSOCIATED_TOKEN_PROGRAM_ID ); try { - await getAccount(connection, toAccount); + await getAccount(connection, toAccount, connection.commitment, programId); return { ataPubKey: toAccount, ix: undefined }; } catch (e) { @@ -115,7 +116,9 @@ export const getOrCreateATAInstruction = async ( payer, toAccount, owner, - tokenMint + tokenMint, + programId, + ASSOCIATED_TOKEN_PROGRAM_ID ); return { ataPubKey: toAccount, ix }; diff --git a/ts-client/src/dlmm/index.ts b/ts-client/src/dlmm/index.ts index 01af9d7..f11d688 100644 --- a/ts-client/src/dlmm/index.ts +++ b/ts-client/src/dlmm/index.ts @@ -152,6 +152,11 @@ type Opt = { programId?: PublicKey; }; +interface MintKeyWithOwner { + mint: PublicKey; + owner: PublicKey; +} + export class DLMM { constructor( public pubkey: PublicKey, @@ -240,24 +245,35 @@ export class DLMM { return lbPairKey; } - const presetParametersWthIndex = + const presetParametersWithIndex = await program.account.presetParameter2.all([ presetParameter2BinStepFilter(binStep), presetParameter2BaseFactorFilter(baseFactor), presetParameter2BaseFeePowerFactor(baseFeePowerFactor), ]); - if (presetParametersWthIndex.length > 0) { - const [lbPairKey] = deriveLbPairWithPresetParamWithIndexKey( - presetParametersWthIndex[0].publicKey, - tokenX, - tokenY, - program.programId + if (presetParametersWithIndex.length > 0) { + const possibleLbPairKeys = presetParametersWithIndex.map((account) => { + return deriveLbPairWithPresetParamWithIndexKey( + account.publicKey, + tokenX, + tokenY, + program.programId + )[0]; + }); + + const accounts = await chunkedGetMultipleAccountInfos( + program.provider.connection, + possibleLbPairKeys ); - const account = await program.account.lbPair.fetchNullable(lbPairKey); - if (account) { - return lbPairKey; + for (let i = 0; i < possibleLbPairKeys.length; i++) { + const pairKey = possibleLbPairKeys[i]; + const account = accounts[i]; + + if (account) { + return pairKey; + } } } @@ -1258,7 +1274,12 @@ export class DLMM { padding: Array(64).fill(0), }; - const userTokenX = getAssociatedTokenAddressSync(tokenX, creatorKey); + const userTokenX = getAssociatedTokenAddressSync( + tokenX, + creatorKey, + true, + tokenXAccount.owner + ); return program.methods .initializeCustomizablePermissionlessLbPair2(ixData) @@ -2358,12 +2379,14 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, this.tokenX.publicKey, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, - user + user, + this.tokenY.owner ), ]); createPayerTokenXIx && preInstructions.push(createPayerTokenXIx); @@ -2435,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, }; @@ -2557,12 +2580,14 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, this.tokenX.publicKey, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, - user + user, + this.tokenY.owner ), ]); createPayerTokenXIx && preInstructions.push(createPayerTokenXIx); @@ -2800,15 +2825,6 @@ export class DLMM { const strategyParameters: LiquidityParameterByStrategy["strategyParameters"] = toStrategyParameters(strategy) as ProgramStrategyParameter; - const positionAccount = - await this.program.provider.connection.getAccountInfo(positionPubKey); - - const position = wrapPosition( - this.program, - positionPubKey, - positionAccount - ); - const binArrayIndexes = getBinArrayIndexesCoverage( new BN(minBinId), new BN(maxBinId) @@ -2834,14 +2850,17 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, this.tokenX.publicKey, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, - user + user, + this.tokenY.owner ), ]); + createPayerTokenXIx && preInstructions.push(createPayerTokenXIx); createPayerTokenYIx && preInstructions.push(createPayerTokenYIx); @@ -3046,12 +3065,14 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, this.tokenX.publicKey, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, - user + user, + this.tokenY.owner ), ]); createPayerTokenXIx && preInstructions.push(createPayerTokenXIx); @@ -3282,25 +3303,29 @@ export class DLMM { this.program.provider.connection, this.tokenX.publicKey, owner, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, owner, - user + user, + this.tokenY.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenX.publicKey, walletToReceiveFee, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, this.tokenY.publicKey, walletToReceiveFee, - user + user, + this.tokenY.owner ), ]); @@ -3346,7 +3371,8 @@ export class DLMM { const { ataPubKey, ix: rewardAtaIx } = await getOrCreateATAInstruction( this.program.provider.connection, rewardInfo.mint, - user + user, + this.rewards[i].owner ); rewardAtaIx && preInstructions.push(rewardAtaIx); @@ -3494,12 +3520,12 @@ export class DLMM { */ public async decreasePositionLength({ - lengthToAdd: lengthToReduce, + lengthToReduce, position, side, feePayer, }: { - lengthToAdd: BN; + lengthToReduce: BN; position: PublicKey; side: ResizeSide; feePayer: PublicKey; @@ -3997,10 +4023,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( lastFilledActiveBinId.toNumber(), this.lbPair.binStep @@ -4012,6 +4034,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, @@ -4046,12 +4072,14 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, inToken, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, outToken, - user + user, + this.tokenY.owner ), ]); createInTokenAccountIx && preInstructions.push(createInTokenAccountIx); @@ -4157,12 +4185,14 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, inToken, - user + user, + this.tokenX.owner ), getOrCreateATAInstruction( this.program.provider.connection, outToken, - user + user, + this.tokenY.owner ), ]); createInTokenAccountIx && preInstructions.push(createInTokenAccountIx); @@ -4269,6 +4299,12 @@ export class DLMM { 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 }, @@ -4276,12 +4312,14 @@ export class DLMM { getOrCreateATAInstruction( this.program.provider.connection, inToken, - user + user, + inTokenProgram ), getOrCreateATAInstruction( this.program.provider.connection, outToken, - user + user, + outTokenProgram ), ]); createInTokenAccountIx && preInstructions.push(createInTokenAccountIx); @@ -4482,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); } /** @@ -4507,7 +4565,8 @@ export class DLMM { public async claimAllSwapFee({ owner, positions, - }: { + }: // todo: range + { owner: PublicKey; positions: LbPosition[]; }): Promise { @@ -4573,16 +4632,28 @@ export class DLMM { }): Promise { const preInstructions: TransactionInstruction[] = []; - const pairTokens = [this.tokenX.publicKey, this.tokenY.publicKey]; - const tokensInvolved = [...pairTokens]; + const pairTokens: MintKeyWithOwner[] = [ + { + mint: this.tokenX.publicKey, + owner: this.tokenX.owner, + }, + { + mint: this.tokenY.publicKey, + owner: this.tokenY.owner, + }, + ]; + const tokensInvolved: MintKeyWithOwner[] = [...pairTokens]; for (let i = 0; i < 2; i++) { const rewardMint = this.lbPair.rewardInfos[i].mint; if ( - !tokensInvolved.some((pubkey) => rewardMint.equals(pubkey)) && + !tokensInvolved.some(({ mint }) => rewardMint.equals(mint)) && !rewardMint.equals(PublicKey.default) ) { - tokensInvolved.push(this.lbPair.rewardInfos[i].mint); + tokensInvolved.push({ + mint: this.rewards[i].publicKey, + owner: this.rewards[i].owner, + }); } } @@ -4594,11 +4665,12 @@ export class DLMM { tokensInvolved.map((token) => { // Single position. Swap fee only belongs to owner, or the customized fee owner. - if (pairTokens.some((t) => t.equals(token))) { + if (pairTokens.some((t) => t.mint.equals(token.mint))) { return getOrCreateATAInstruction( this.program.provider.connection, - token, + token.mint, feeOwner, + token.owner, owner ); } @@ -4606,8 +4678,9 @@ export class DLMM { // Reward return getOrCreateATAInstruction( this.program.provider.connection, - token, - owner + token.mint, + owner, + token.owner ); }) ); @@ -4631,7 +4704,7 @@ export class DLMM { ); const postInstructions: TransactionInstruction[] = []; - if (tokensInvolved.some((pubkey) => pubkey.equals(NATIVE_MINT))) { + if (tokensInvolved.some(({ mint }) => mint.equals(NATIVE_MINT))) { const closeWrappedSOLIx = await unwrapSOLInstruction(owner); closeWrappedSOLIx && postInstructions.push(closeWrappedSOLIx); } @@ -4748,13 +4821,15 @@ export class DLMM { const operatorTokenX = getAssociatedTokenAddressSync( this.lbPair.tokenXMint, operator, - true + true, + this.tokenX.owner ); const ownerTokenX = getAssociatedTokenAddressSync( this.lbPair.tokenXMint, owner, - true + true, + this.tokenY.owner ); const initializePositionByOperatorTx = await this.program.methods @@ -4800,15 +4875,28 @@ export class DLMM { positions: LbPosition[]; }): Promise { const preInstructions: TransactionInstruction[] = []; - const pairsToken = [this.tokenX.publicKey, this.tokenY.publicKey]; + const pairsToken: MintKeyWithOwner[] = [ + { + mint: this.tokenX.publicKey, + owner: this.tokenX.owner, + }, + { + mint: this.tokenY.publicKey, + owner: this.tokenY.owner, + }, + ]; + const tokensInvolved = [...pairsToken]; for (let i = 0; i < 2; i++) { const rewardMint = this.lbPair.rewardInfos[i].mint; if ( - !tokensInvolved.some((pubkey) => rewardMint.equals(pubkey)) && + !tokensInvolved.some(({ mint }) => rewardMint.equals(mint)) && !rewardMint.equals(PublicKey.default) ) { - tokensInvolved.push(this.lbPair.rewardInfos[i].mint); + tokensInvolved.push({ + mint: this.rewards[i].publicKey, + owner: this.rewards[i].owner, + }); } } @@ -4834,12 +4922,13 @@ export class DLMM { tokensInvolved .map((token) => { // There's multiple positions, therefore swap fee ATA might includes account from owner, and customized fee owners - if (pairsToken.some((p) => p.equals(token))) { + if (pairsToken.some((p) => p.mint.equals(token.mint))) { return feeOwners.map((customOwner) => getOrCreateATAInstruction( this.program.provider.connection, - token, + token.mint, customOwner, + token.owner, owner ) ); @@ -4848,8 +4937,9 @@ export class DLMM { return [ getOrCreateATAInstruction( this.program.provider.connection, - token, - owner + token.mint, + owner, + token.owner ), ]; }) @@ -4899,7 +4989,7 @@ export class DLMM { ); const postInstructions: TransactionInstruction[] = []; - if (tokensInvolved.some((pubkey) => pubkey.equals(NATIVE_MINT))) { + if (tokensInvolved.some(({ mint }) => mint.equals(NATIVE_MINT))) { const closeWrappedSOLIx = await unwrapSOLInstruction(owner); closeWrappedSOLIx && postInstructions.push(closeWrappedSOLIx); } @@ -5447,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, @@ -5528,6 +5619,7 @@ export class DLMM { rewardTwoExcludeTransferFee, feeXExcludeTransferFee, feeYExcludeTransferFee, + owner: position.owner(), }; } @@ -5569,23 +5661,21 @@ export class DLMM { binArray.bins.forEach((bin, idx) => { const binId = lowerBinIdForBinArray.toNumber() + idx; if (binId >= lowerBinId && binId <= upperBinId) { - if (binId >= lowerBinId && binId <= upperBinId) { - const pricePerLamport = getPriceOfBinByBinId( - binId, - lbPair.binStep - ).toString(); - bins.push({ - binId, - xAmount: bin.amountX, - yAmount: bin.amountY, - supply: bin.liquiditySupply, - price: pricePerLamport, - version: binArray.version, - pricePerToken: new Decimal(pricePerLamport) - .mul(new Decimal(10 ** (baseTokenDecimal - quoteTokenDecimal))) - .toString(), - }); - } + const pricePerLamport = getPriceOfBinByBinId( + binId, + lbPair.binStep + ).toString(); + bins.push({ + binId, + xAmount: bin.amountX, + yAmount: bin.amountY, + supply: bin.liquiditySupply, + price: pricePerLamport, + version: binArray.version, + pricePerToken: new Decimal(pricePerLamport) + .mul(new Decimal(10 ** (baseTokenDecimal - quoteTokenDecimal))) + .toString(), + }); } }); } @@ -5829,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, }) @@ -5885,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/dlmm/types/index.ts b/ts-client/src/dlmm/types/index.ts index 0dae11f..c9d22ac 100644 --- a/ts-client/src/dlmm/types/index.ts +++ b/ts-client/src/dlmm/types/index.ts @@ -320,6 +320,7 @@ export interface PositionData { rewardTwoExcludeTransferFee: BN; totalXAmountExcludeTransferFee: BN; totalYAmountExcludeTransferFee: BN; + owner: PublicKey; } export interface SwapWithPriceImpactParams { diff --git a/ts-client/src/test/sdk.test.ts b/ts-client/src/test/sdk.test.ts index e61f598..4ecd145 100644 --- a/ts-client/src/test/sdk.test.ts +++ b/ts-client/src/test/sdk.test.ts @@ -890,13 +890,11 @@ describe("SDK test", () => { }); it("fetch all preset parameter", async () => { - const { presetParameter, presetParameter2 } = - await DLMM.getAllPresetParameters(connection, { - cluster: "localhost", - }); + const { presetParameter } = await DLMM.getAllPresetParameters(connection, { + cluster: "localhost", + }); - const length = presetParameter.length + presetParameter2.length; - expect(length).toBeGreaterThan(0); + expect(presetParameter.length).toBeGreaterThan(0); }); it("create LB pair with bitmap extension", async () => { diff --git a/ts-client/src/test/sdk_token2022.test.ts b/ts-client/src/test/sdk_token2022.test.ts index e69de29..10b3e7e 100644 --- a/ts-client/src/test/sdk_token2022.test.ts +++ b/ts-client/src/test/sdk_token2022.test.ts @@ -0,0 +1,1469 @@ +import { AnchorProvider, Program, Wallet } from "@coral-xyz/anchor"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + createInitializeMintInstruction, + createInitializeTransferFeeConfigInstruction, + createInitializeTransferHookInstruction, + createMint, + ExtensionType, + getAssociatedTokenAddressSync, + getMintLen, + getOrCreateAssociatedTokenAccount, + mintTo, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + unpackAccount, +} from "@solana/spl-token"; +import { + AccountInfo, + Cluster, + Connection, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + sendAndConfirmTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import BN from "bn.js"; +import fs from "fs"; +import { DLMM } from "../dlmm"; +import { LBCLMM_PROGRAM_IDS } from "../dlmm/constants"; +import { + deriveCustomizablePermissionlessLbPair, + deriveLbPairWithPresetParamWithIndexKey, + derivePermissionLbPair, + derivePresetParameterWithIndex, + deriveRewardVault, + deriveTokenBadge, + toAmountsBothSideByStrategy, +} from "../dlmm/helpers"; +import { IDL } from "../dlmm/idl"; +import { ActivationType, ResizeSide, StrategyType } from "../dlmm/types"; +import { createExtraAccountMetaListAndCounter } from "./external/helper"; +import { + createTransferHookCounterProgram, + TRANSFER_HOOK_COUNTER_PROGRAM_ID, +} from "./external/program"; +import Decimal from "decimal.js"; + +const keypairBuffer = fs.readFileSync( + "../keys/localnet/admin-bossj3JvwiNK7pvjr149DqdtJxf2gdygbcmEPTkb2F1.json", + "utf-8" +); +const connection = new Connection("http://127.0.0.1:8899", "confirmed"); +const keypair = Keypair.fromSecretKey( + new Uint8Array(JSON.parse(keypairBuffer)) +); + +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); + +let presetParameter2Key: PublicKey; +let pairKey: PublicKey; + +const provider = new AnchorProvider( + connection, + new Wallet(keypair), + AnchorProvider.defaultOptions() +); +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; +}; + +const opt: Opt = { + cluster: "localhost", +}; + +describe("SDK token2022 test", () => { + // Token setup + beforeAll(async () => { + const airdropSig = await connection.requestAirdrop( + keypair.publicKey, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(airdropSig, "confirmed"); + + await createMint( + connection, + keypair, + keypair.publicKey, + null, + usdcDecimal, + USDCKeypair, + { + commitment: "confirmed", + } + ); + + const userUsdcAccount = await getOrCreateAssociatedTokenAccount( + connection, + keypair, + USDC, + keypair.publicKey, + true, + "confirmed", + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const userUsdcAta = userUsdcAccount.address; + + await mintTo( + connection, + keypair, + USDC, + userUsdcAta, + keypair, + BigInt(1_000_000_000) * BigInt(10 ** usdcDecimal), + [], + { + commitment: "confirmed", + }, + TOKEN_PROGRAM_ID + ); + + const extensions = [ + ExtensionType.TransferFeeConfig, + ExtensionType.TransferHook, + ]; + + const mintLen = getMintLen(extensions); + const minLamports = await connection.getMinimumBalanceForRentExemption( + mintLen + ); + + const createBtcTx = new Transaction() + .add( + SystemProgram.createAccount({ + fromPubkey: keypair.publicKey, + newAccountPubkey: BTCKeypair.publicKey, + space: mintLen, + lamports: minLamports, + programId: TOKEN_2022_PROGRAM_ID, + }) + ) + .add( + createInitializeTransferFeeConfigInstruction( + BTC2022, + keypair.publicKey, + keypair.publicKey, + transferFeeBps, + maxFee, + TOKEN_2022_PROGRAM_ID + ) + ) + .add( + createInitializeTransferHookInstruction( + BTC2022, + keypair.publicKey, + TRANSFER_HOOK_COUNTER_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID + ) + ) + .add( + createInitializeMintInstruction( + BTC2022, + btcDecimal, + keypair.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction( + connection, + createBtcTx, + [keypair, BTCKeypair], + { commitment: "confirmed" } + ); + + const transferHookCounterProgram = createTransferHookCounterProgram( + new Wallet(keypair), + TRANSFER_HOOK_COUNTER_PROGRAM_ID, + connection + ); + + await createExtraAccountMetaListAndCounter( + transferHookCounterProgram, + BTC2022 + ); + + const userBtcAccount = await getOrCreateAssociatedTokenAccount( + connection, + keypair, + BTC2022, + keypair.publicKey, + true, + "confirmed", + { + commitment: "confirmed", + }, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + const userBtcAta = userBtcAccount.address; + + await mintTo( + connection, + keypair, + BTC2022, + userBtcAta, + keypair, + BigInt(1_000_000_000) * BigInt(10 ** btcDecimal), + [], + { + commitment: "confirmed", + }, + 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 + beforeAll(async () => { + const presetParameter2 = await program.account.presetParameter2.all(); + const idx = presetParameter2.length; + + [presetParameter2Key] = derivePresetParameterWithIndex( + new BN(idx), + program.programId + ); + + await program.methods + .initializePresetParameter2({ + index: idx, + binStep: 10, + baseFactor: 10_000, + filterPeriod: 30, + decayPeriod: 600, + reductionFactor: 5000, + variableFeeControl: 40000, + protocolShare: 0, + maxVolatilityAccumulator: 350000, + baseFeePowerFactor: 1, + }) + .accounts({ + presetParameter: presetParameter2Key, + admin: keypair.publicKey, + systemProgram: SystemProgram.programId, + }) + .rpc(); + + const [btcTokenBadge] = deriveTokenBadge(BTC2022, program.programId); + + await program.methods + .initializeTokenBadge() + .accounts({ + tokenBadge: btcTokenBadge, + admin: keypair.publicKey, + systemProgram: SystemProgram.programId, + 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 () => { + const { presetParameter2 } = await DLMM.getAllPresetParameters( + connection, + opt + ); + + expect(presetParameter2.length).toBeGreaterThan(0); + }); + + describe("Pair", () => { + it("createPermissionPair with token 2022", async () => { + const binStep = new BN(1); + const feeBps = new BN(100); + const protocolFeeBps = new BN(500); + const activeId = new BN(0); + + const createPermissionPairTx = await DLMM.createPermissionLbPair( + connection, + binStep, + BTC2022, + USDC, + activeId, + keypair.publicKey, + keypair.publicKey, + feeBps, + ActivationType.Timestamp, + protocolFeeBps, + opt + ); + + await sendAndConfirmTransaction(connection, createPermissionPairTx, [ + keypair, + ]); + + const [pairKey] = derivePermissionLbPair( + keypair.publicKey, + BTC2022, + USDC, + binStep, + program.programId + ); + + const dlmm = await DLMM.create(connection, pairKey, opt); + + const feeInfo = dlmm.getFeeInfo(); + expect(feeInfo.baseFeeRatePercentage.toNumber()).toBe(1); + expect(feeInfo.protocolFeePercentage.toNumber()).toBe(5); + }); + + it("createLbPair2 with token 2022", async () => { + const activeId = new BN(0); + + const createLbPair2Tx = await DLMM.createLbPair2( + connection, + keypair.publicKey, + BTC2022, + USDC, + presetParameter2Key, + activeId, + opt + ); + + await sendAndConfirmTransaction(connection, createLbPair2Tx, [keypair], { + commitment: "confirmed", + }); + + [pairKey] = deriveLbPairWithPresetParamWithIndexKey( + presetParameter2Key, + BTC2022, + USDC, + program.programId + ); + + const dlmm = await DLMM.create(connection, pairKey, opt); + + const feeInfo = dlmm.getFeeInfo(); + expect(feeInfo.baseFeeRatePercentage.toNumber()).toBe(1); + expect(dlmm.lbPair.binStep).toBe(10); + }); + + it("createCustomizablePermissionlessLbPair2 with token 2022", async () => { + const binStep = new BN(1); + const activeId = new BN(0); + const feeBps = new BN(150); + + const createCustomizablePermissionlessLbPair2Tx = + await DLMM.createCustomizablePermissionlessLbPair2( + connection, + binStep, + BTC2022, + USDC, + activeId, + feeBps, + ActivationType.Timestamp, + false, + keypair.publicKey, + null, + opt + ); + + await sendAndConfirmTransaction( + connection, + createCustomizablePermissionlessLbPair2Tx, + [keypair] + ); + + const [pairKey] = deriveCustomizablePermissionlessLbPair( + BTC2022, + USDC, + program.programId + ); + + const dlmm = await DLMM.create(connection, pairKey, opt); + + const feeInfo = dlmm.getFeeInfo(); + expect(feeInfo.baseFeeRatePercentage.toNumber()).toBe(1.5); + expect(feeInfo.protocolFeePercentage.toNumber()).toBe(20); + }); + + it("getPairPubkeyIfExists return pair permissionless pair pubkey", async () => { + const dlmm = await DLMM.create(connection, pairKey, opt); + + const pairPubkey = await DLMM.getPairPubkeyIfExists( + connection, + dlmm.lbPair.tokenXMint, + dlmm.lbPair.tokenYMint, + new BN(dlmm.lbPair.binStep), + new BN(dlmm.lbPair.parameters.baseFactor), + new BN(dlmm.lbPair.parameters.baseFeePowerFactor), + opt + ); + + expect(pairPubkey.toBase58()).toBe(pairKey.toBase58()); + }); + + it("createMultiple works", async () => { + const lbPairs = await DLMM.getLbPairs(connection, opt); + + const dlmms = await DLMM.createMultiple( + connection, + lbPairs.map((x) => x.publicKey), + opt + ); + + for (let i = 0; i < lbPairs.length; i++) { + const dlmm = dlmms[i]; + const lbPair = lbPairs[i]; + + expect(dlmm.pubkey.toBase58()).toBe(lbPair.publicKey.toBase58()); + expect(dlmm.tokenX.publicKey.toBase58()).toBe( + lbPair.account.tokenXMint.toBase58() + ); + expect(dlmm.tokenY.publicKey.toBase58()).toBe( + lbPair.account.tokenYMint.toBase58() + ); + } + }); + }); + + 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); + + const minBinId = -30; + const maxBinId = 30; + + const createPositionAndBinArraysTx = await dlmm.createEmptyPosition({ + positionPubKey: widePositionKeypair.publicKey, + minBinId, + maxBinId, + user: keypair.publicKey, + }); + + await sendAndConfirmTransaction( + connection, + createPositionAndBinArraysTx, + [keypair, widePositionKeypair] + ); + + const position = await dlmm.getPosition(widePositionKeypair.publicKey); + expect(position.publicKey.toBase58()).toBe( + widePositionKeypair.publicKey.toBase58() + ); + + const { positionData } = position; + expect(positionData.lowerBinId).toBe(minBinId); + expect(positionData.upperBinId).toBe(maxBinId); + + const binCount = maxBinId - minBinId + 1; + expect(positionData.positionBinData.length).toBe(binCount); + }); + + it("increasePositionLength", async () => { + const lengthToAdd = 30; + + const dlmm = await DLMM.create(connection, pairKey, opt); + + let before = await dlmm.getPosition(widePositionKeypair.publicKey); + + const increaseBuySideLengthTx = await dlmm.increasePositionLength({ + lengthToAdd: new BN(lengthToAdd), + position: widePositionKeypair.publicKey, + payer: keypair.publicKey, + side: ResizeSide.Lower, + }); + + await sendAndConfirmTransaction(connection, increaseBuySideLengthTx, [ + keypair, + ]); + + let after = await dlmm.getPosition(widePositionKeypair.publicKey); + let { positionData } = after; + expect(positionData.lowerBinId).toBe( + before.positionData.lowerBinId - lengthToAdd + ); + expect(positionData.upperBinId).toBe(before.positionData.upperBinId); + + let binCount = positionData.upperBinId - positionData.lowerBinId + 1; + expect(positionData.positionBinData.length).toBe(binCount); + + before = after; + + const increaseSellSideLengthTx = await dlmm.increasePositionLength({ + lengthToAdd: new BN(lengthToAdd), + position: widePositionKeypair.publicKey, + payer: keypair.publicKey, + side: ResizeSide.Upper, + }); + + await sendAndConfirmTransaction(connection, increaseSellSideLengthTx, [ + keypair, + ]); + + after = await dlmm.getPosition(widePositionKeypair.publicKey); + ({ positionData } = after); + expect(positionData.lowerBinId).toBe(before.positionData.lowerBinId); + expect(positionData.upperBinId).toBe( + before.positionData.upperBinId + lengthToAdd + ); + + binCount = positionData.upperBinId - positionData.lowerBinId + 1; + expect(positionData.positionBinData.length).toBe(binCount); + }); + + it("decreasePositionLength", async () => { + const lengthToReduce = 5; + + const dlmm = await DLMM.create(connection, pairKey, opt); + + let before = await dlmm.getPosition(widePositionKeypair.publicKey); + + const decreaseBuySideLengthTx = await dlmm.decreasePositionLength({ + lengthToReduce: new BN(lengthToReduce), + position: widePositionKeypair.publicKey, + feePayer: keypair.publicKey, + side: ResizeSide.Lower, + }); + + await sendAndConfirmTransaction(connection, decreaseBuySideLengthTx, [ + keypair, + ]); + + let after = await dlmm.getPosition(widePositionKeypair.publicKey); + let { positionData } = after; + expect(positionData.lowerBinId).toBe( + before.positionData.lowerBinId + lengthToReduce + ); + expect(positionData.upperBinId).toBe(before.positionData.upperBinId); + + let binCount = positionData.upperBinId - positionData.lowerBinId + 1; + expect(positionData.positionBinData.length).toBe(binCount); + + before = after; + + const decreaseSellSideLengthTx = await dlmm.decreasePositionLength({ + lengthToReduce: new BN(lengthToReduce), + position: widePositionKeypair.publicKey, + feePayer: keypair.publicKey, + side: ResizeSide.Upper, + }); + + await sendAndConfirmTransaction(connection, decreaseSellSideLengthTx, [ + keypair, + ]); + + after = await dlmm.getPosition(widePositionKeypair.publicKey); + ({ positionData } = after); + expect(positionData.lowerBinId).toBe(before.positionData.lowerBinId); + expect(positionData.upperBinId).toBe( + before.positionData.upperBinId - lengthToReduce + ); + + 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)); + + const dlmm = await DLMM.create(connection, pairKey, opt); + let position = await dlmm.getPosition(widePositionKeypair.publicKey); + + const activeBinInfo = await dlmm.getActiveBin(); + + const computedInBinAmount = toAmountsBothSideByStrategy( + dlmm.lbPair.activeId, + dlmm.lbPair.binStep, + position.positionData.lowerBinId, + position.positionData.upperBinId, + totalXAmount, + totalYAmount, + activeBinInfo.xAmount, + activeBinInfo.yAmount, + StrategyType.SpotImBalanced, + dlmm.tokenX.mint, + dlmm.tokenY.mint, + dlmm.clock + ); + + const addLiquidityTx = await dlmm.addLiquidityByStrategy({ + positionPubKey: widePositionKeypair.publicKey, + totalXAmount, + totalYAmount, + user: keypair.publicKey, + strategy: { + strategyType: StrategyType.SpotImBalanced, + minBinId: position.positionData.lowerBinId, + maxBinId: position.positionData.upperBinId, + }, + slippage: 0, + }); + + 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, 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(); + } + }); + }); + + describe("Position fetcher", () => { + const pairWithPositionKey: { + pair: PublicKey; + position: PublicKey; + user: PublicKey; + }[] = []; + + beforeAll(async () => { + const pairs = await DLMM.getLbPairs(connection, opt); + + for (const pair of pairs) { + const userKeypair = Keypair.generate(); + + const airdropSig = await connection.requestAirdrop( + userKeypair.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + await connection.confirmTransaction(airdropSig, "confirmed"); + + const positionKeypair = Keypair.generate(); + const dlmm = await DLMM.create(connection, pair.publicKey, opt); + + const minBinId = -30; + const maxBinId = 30; + + const createPositionAndBinArraysTx = await dlmm.createEmptyPosition({ + positionPubKey: positionKeypair.publicKey, + minBinId, + maxBinId, + user: userKeypair.publicKey, + }); + + await sendAndConfirmTransaction( + connection, + createPositionAndBinArraysTx, + [userKeypair, positionKeypair] + ); + + pairWithPositionKey.push({ + pair: pair.publicKey, + position: positionKeypair.publicKey, + user: userKeypair.publicKey, + }); + } + }); + + it("Load position by user and pair successfully", async () => { + for (const { pair, position, user } of pairWithPositionKey) { + const dlmm = await DLMM.create(connection, pair, opt); + const { userPositions } = await dlmm.getPositionsByUserAndLbPair(user); + + expect(userPositions.length).toBe(1); + expect( + userPositions.find((x) => x.publicKey.equals(position)) + ).toBeDefined(); + expect( + userPositions.filter((x) => x.positionData.owner.equals(user)).length + ).toBe(userPositions.length); + } + }); + + it("Load all positions by user successfully", async () => { + const pairKeyedPosition = await DLMM.getAllLbPairPositionsByUser( + connection, + keypair.publicKey, + opt + ); + + const positionContainers = Array.from(pairKeyedPosition.values()); + const positions = positionContainers.flatMap( + (x) => x.lbPairPositionsData + ); + + for (const position of positions) { + expect( + position.positionData.owner.equals(keypair.publicKey) + ).toBeTruthy(); + } + }); + }); +});