From fc7dac3037c208fd1806ec80b8a43fbcc6648bdc Mon Sep 17 00:00:00 2001 From: Will <82029448+wjthieme@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:11:27 +0100 Subject: [PATCH] Reduced close position transaction builder to a single transaction (#128) * New approach * Small tweaks --- sdk/package.json | 4 +- sdk/src/impl/whirlpool-impl.ts | 23 +++- .../whirlpool-impl#closePosition.test.ts | 128 +++++++++++++++++- yarn.lock | 8 +- 4 files changed, 144 insertions(+), 19 deletions(-) diff --git a/sdk/package.json b/sdk/package.json index b0c17d220..8606539a4 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,13 +1,13 @@ { "name": "@orca-so/whirlpools-sdk", - "version": "0.11.8", + "version": "0.11.9", "description": "Typescript SDK to interact with Orca's Whirlpool program.", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { "@coral-xyz/anchor": "~0.27.0", - "@orca-so/common-sdk": "^0.3.3", + "@orca-so/common-sdk": "^0.3.6", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.75.0", "decimal.js": "^10.3.1", diff --git a/sdk/src/impl/whirlpool-impl.ts b/sdk/src/impl/whirlpool-impl.ts index 55374a856..6a1c9d0c2 100644 --- a/sdk/src/impl/whirlpool-impl.ts +++ b/sdk/src/impl/whirlpool-impl.ts @@ -1,6 +1,7 @@ import { Address, BN, translateAddress } from "@coral-xyz/anchor"; import { AddressUtil, + MEASUREMENT_BLOCKHASH, Percentage, TokenUtil, TransactionBuilder, @@ -40,6 +41,7 @@ import { import { Whirlpool } from "../whirlpool-client"; import { PositionImpl } from "./position-impl"; import { getRewardInfos, getTokenVaultAccountInfos } from "./util"; +import { checkMergedTransactionSizeIsValid } from "../utils/txn-utils"; export class WhirlpoolImpl implements Whirlpool { private data: WhirlpoolData; @@ -584,15 +586,24 @@ export class WhirlpoolImpl implements Whirlpool { txBuilder.addInstruction(positionIx); - const txBuilders: TransactionBuilder[] = []; - - if (!tokenAccountsTxBuilder.isEmpty()) { - txBuilders.push(tokenAccountsTxBuilder); + if (tokenAccountsTxBuilder.isEmpty()) { + return [txBuilder] } - txBuilders.push(txBuilder); + // This handles an edge case where the instructions are too + // large to fit in a single transaction and we need to split the + // instructions into two transactions. + const canFitInOneTransaction = await checkMergedTransactionSizeIsValid( + this.ctx, + [tokenAccountsTxBuilder, txBuilder], + MEASUREMENT_BLOCKHASH + ) + if (!canFitInOneTransaction) { + return [tokenAccountsTxBuilder, txBuilder] + } - return txBuilders; + tokenAccountsTxBuilder.addInstruction(txBuilder.compressIx(false)); + return [tokenAccountsTxBuilder] } private async refresh() { diff --git a/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts b/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts index ef9fb45d0..3493c0865 100644 --- a/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts +++ b/sdk/tests/sdk/whirlpools/whirlpool-impl#closePosition.test.ts @@ -144,11 +144,12 @@ describe("WhirlpoolImpl#closePosition()", () => { // TODO: Our createWSOLAccountInstructions ignores payer and requires destinationWallet to sign // We can remove this once we move to syncNative and wSOL becomes another ATA to handle. if (isWSOLTest) { - txs[1].addSigner(otherWallet) + txs[txs.length - 1].addSigner(otherWallet) } - await txs[0].buildAndExecute(); - await txs[1].buildAndExecute(); + for (const tx of txs) { + await tx.buildAndExecute(); + } // Verify liquidity and fees collected const liquidityCollectedQuote = decreaseLiquidityQuoteByLiquidity( @@ -441,8 +442,11 @@ describe("WhirlpoolImpl#closePosition()", () => { ctx.wallet.publicKey ); - await txs[0].buildAndExecute(); - await txs[1].addSigner(otherWallet).buildAndExecute(); + txs[txs.length - 1].addSigner(otherWallet) + + for (const tx of txs) { + await tx.buildAndExecute(); + } const positionDataAfter = await testCtx.whirlpoolCtx.fetcher.getPosition( position.getAddress(), @@ -544,8 +548,11 @@ describe("WhirlpoolImpl#closePosition()", () => { ctx.wallet.publicKey ); - await txs[0].buildAndExecute(); - await txs[1].addSigner(otherWallet).buildAndExecute(); + txs[txs.length - 1].addSigner(otherWallet) + + for (const tx of txs) { + await tx.buildAndExecute(); + } const positionDataAfter = await testCtx.whirlpoolCtx.fetcher.getPosition( position.getAddress(), @@ -555,4 +562,111 @@ describe("WhirlpoolImpl#closePosition()", () => { assert.equal(positionDataAfter, null); }); }); + + it("should only create 2 transactions if absolutely necessary", async () => { + const ctx = testCtx.whirlpoolCtx; + const fixture = await new WhirlpoolTestFixture(testCtx.whirlpoolCtx).init({ + tickSpacing, + positions: [ + { tickLowerIndex, tickUpperIndex, liquidityAmount }, // In range position + ], + rewards: [ + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + { + emissionsPerSecondX64: MathUtil.toX64(new Decimal(10)), + vaultAmount: new BN(vaultStartBalance), + }, + ], + tokenAIsNative: true, + }); + + const otherWallet = anchor.web3.Keypair.generate(); + const positionData = fixture.getInfos().positions[0]; + + const position = await testCtx.whirlpoolClient.getPosition(positionData.publicKey, IGNORE_CACHE); + + const walletPositionTokenAccount = getAssociatedTokenAddressSync( + positionData.mintKeypair.publicKey, + testCtx.whirlpoolCtx.wallet.publicKey, + ); + + const newOwnerPositionTokenAccount = await createAssociatedTokenAccount( + ctx.provider, + positionData.mintKeypair.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + await accrueFeesAndRewards(fixture); + await transferToken(testCtx.provider, walletPositionTokenAccount, newOwnerPositionTokenAccount, 1); + + const { poolInitInfo } = fixture.getInfos(); + + const pool = await testCtx.whirlpoolClient.getPool(poolInitInfo.whirlpoolPda.publicKey); + + const txsWith4Ata = await pool.closePosition( + position.getAddress(), + Percentage.fromFraction(10, 100), + otherWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + assert.equal(txsWith4Ata.length, 2); + + await createAssociatedTokenAccount( + ctx.provider, + position.getWhirlpoolData().rewardInfos[0].mint, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + const txsWith3Ata = await pool.closePosition( + position.getAddress(), + Percentage.fromFraction(10, 100), + otherWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + assert.equal(txsWith3Ata.length, 2); + + await createAssociatedTokenAccount( + ctx.provider, + position.getWhirlpoolData().rewardInfos[1].mint, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + const txsWith2Ata = await pool.closePosition( + position.getAddress(), + Percentage.fromFraction(10, 100), + otherWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + assert.equal(txsWith2Ata.length, 2); + + await createAssociatedTokenAccount( + ctx.provider, + position.getWhirlpoolData().rewardInfos[2].mint, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + + const txsWith1Ata = await pool.closePosition( + position.getAddress(), + Percentage.fromFraction(10, 100), + otherWallet.publicKey, + otherWallet.publicKey, + ctx.wallet.publicKey + ); + assert.equal(txsWith1Ata.length, 1); + await txsWith1Ata[0].addSigner(otherWallet).buildAndExecute(); + }); }); diff --git a/yarn.lock b/yarn.lock index c8054f9f9..66c5d4640 100644 --- a/yarn.lock +++ b/yarn.lock @@ -133,10 +133,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@orca-so/common-sdk@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.3.3.tgz#d208c9a3a77c91f057519dc590c4725b60de5b39" - integrity sha512-g4LSOkSjq9IwXgeVB5pV5bbB9DBX8MBJyE2dpv7KE/B2jyFe8yEqjCOk9aTwbM2ftVDLJRH0jBP4z0rKz6oFCQ== +"@orca-so/common-sdk@^0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@orca-so/common-sdk/-/common-sdk-0.3.6.tgz#18ce8176118bdf3dba276e0d5fb8cf9d79aacffa" + integrity sha512-Yf4AZqnJI881SmDn2tf/KL0KOcXVuwHsP1pF4O28UyoSgQG1hBHfhNExd6Bn0K3ocv/pIbi215U4zlydtPTJDw== dependencies: "@solana/spl-token" "^0.3.8" "@solana/web3.js" "^1.75.0"