diff --git a/legacy-sdk/cli/README.md b/legacy-sdk/cli/README.md index f5ae22377..20f2de74d 100644 --- a/legacy-sdk/cli/README.md +++ b/legacy-sdk/cli/README.md @@ -53,14 +53,29 @@ Token-2022 tokens are acceptable 👍 - `yarn start collectRewards`: collect rewards from a position - `yarn start closePosition`: close an empty position +## PositionBundle +- `yarn start initializePositionBundle`: create a new position bundle +- `yarn start syncPositionBundleState`: update the state of a position bundle based on an input CSV file + ## Swap - `yarn start pushPrice`: adjust pool price (possible if pool liquidity is zero or very small) ## WSOL and ATA creation TODO: WSOL handling & create ATA if needed (workaround exists, please see the following) +## ALT +### Shared ALT +Shared ALT contains well-known program ID and WhirlpoolsConfig address and mint addresses. + +Solana: `7Vyx1y8vG9e9Q1MedmXpopRC6ZhVaZzGcvYh5Z3Cs75i` +Eclipse: `Fsq7DQa13Lx9FvR5QheHigaccRkjiNqfnHQouXyFsg4z` + +### Custom ALT +- `yarn start initializeAltForWhirlpool`: create ALT containing whirlpool address, mint address, vault address, some TickArray addresses and ATAs +- `yarn start initializeAltForBundledPositions`: create ALT containing all (256) bundled position addresses + ### workaround for WSOL -`whirlpool-mgmt-tools` works well with ATA, so using WSOL on ATA is workaround. +CLI works well with ATA, so using WSOL on ATA is workaround. - wrap 1 SOL: `spl-token wrap 1` (ATA for WSOL will be initialized with 1 SOL) - unwrap: `spl-token unwrap` (ATA for WSOL will be closed) diff --git a/legacy-sdk/cli/package.json b/legacy-sdk/cli/package.json index c02a678db..ad127cea8 100644 --- a/legacy-sdk/cli/package.json +++ b/legacy-sdk/cli/package.json @@ -9,8 +9,8 @@ }, "dependencies": { "@coral-xyz/anchor": "0.29.0", - "@orca-so/common-sdk": "*", - "@orca-so/whirlpools-sdk": "*", + "@orca-so/common-sdk": "0.6.4", + "@orca-so/whirlpools-sdk": "0.13.12", "@solana/spl-token": "0.4.1", "@solana/web3.js": "^1.90.0", "@types/bn.js": "^5.1.0", diff --git a/legacy-sdk/cli/sample/position_bundle_state/close.csv b/legacy-sdk/cli/sample/position_bundle_state/close.csv new file mode 100644 index 000000000..635c645cb --- /dev/null +++ b/legacy-sdk/cli/sample/position_bundle_state/close.csv @@ -0,0 +1,257 @@ +bundle index,state,lower tick index,upper tick index,liquidity +0,closed,,, +1,closed,,, +2,closed,,, +3,closed,,, +4,closed,,, +5,closed,,, +6,closed,,, +7,closed,,, +8,closed,,, +9,closed,,, +10,closed,,, +11,closed,,, +12,closed,,, +13,closed,,, +14,closed,,, +15,closed,,, +16,closed,,, +17,closed,,, +18,closed,,, +19,closed,,, +20,closed,,, +21,closed,,, +22,closed,,, +23,closed,,, +24,closed,,, +25,closed,,, +26,closed,,, +27,closed,,, +28,closed,,, +29,closed,,, +30,closed,,, +31,closed,,, +32,closed,,, +33,closed,,, +34,closed,,, +35,closed,,, +36,closed,,, +37,closed,,, +38,closed,,, +39,closed,,, +40,closed,,, +41,closed,,, +42,closed,,, +43,closed,,, +44,closed,,, +45,closed,,, +46,closed,,, +47,closed,,, +48,closed,,, +49,closed,,, +50,closed,,, +51,closed,,, +52,closed,,, +53,closed,,, +54,closed,,, +55,closed,,, +56,closed,,, +57,closed,,, +58,closed,,, +59,closed,,, +60,closed,,, +61,closed,,, +62,closed,,, +63,closed,,, +64,closed,,, +65,closed,,, +66,closed,,, +67,closed,,, +68,closed,,, +69,closed,,, +70,closed,,, +71,closed,,, +72,closed,,, +73,closed,,, +74,closed,,, +75,closed,,, +76,closed,,, +77,closed,,, +78,closed,,, +79,closed,,, +80,closed,,, +81,closed,,, +82,closed,,, +83,closed,,, +84,closed,,, +85,closed,,, +86,closed,,, +87,closed,,, +88,closed,,, +89,closed,,, +90,closed,,, +91,closed,,, +92,closed,,, +93,closed,,, +94,closed,,, +95,closed,,, +96,closed,,, +97,closed,,, +98,closed,,, +99,closed,,, +100,closed,,, +101,closed,,, +102,closed,,, +103,closed,,, +104,closed,,, +105,closed,,, +106,closed,,, +107,closed,,, +108,closed,,, +109,closed,,, +110,closed,,, +111,closed,,, +112,closed,,, +113,closed,,, +114,closed,,, +115,closed,,, +116,closed,,, +117,closed,,, +118,closed,,, +119,closed,,, +120,closed,,, +121,closed,,, +122,closed,,, +123,closed,,, +124,closed,,, +125,closed,,, +126,closed,,, +127,closed,,, +128,closed,,, +129,closed,,, +130,closed,,, +131,closed,,, +132,closed,,, +133,closed,,, +134,closed,,, +135,closed,,, +136,closed,,, +137,closed,,, +138,closed,,, +139,closed,,, +140,closed,,, +141,closed,,, +142,closed,,, +143,closed,,, +144,closed,,, +145,closed,,, +146,closed,,, +147,closed,,, +148,closed,,, +149,closed,,, +150,closed,,, +151,closed,,, +152,closed,,, +153,closed,,, +154,closed,,, +155,closed,,, +156,closed,,, +157,closed,,, +158,closed,,, +159,closed,,, +160,closed,,, +161,closed,,, +162,closed,,, +163,closed,,, +164,closed,,, +165,closed,,, +166,closed,,, +167,closed,,, +168,closed,,, +169,closed,,, +170,closed,,, +171,closed,,, +172,closed,,, +173,closed,,, +174,closed,,, +175,closed,,, +176,closed,,, +177,closed,,, +178,closed,,, +179,closed,,, +180,closed,,, +181,closed,,, +182,closed,,, +183,closed,,, +184,closed,,, +185,closed,,, +186,closed,,, +187,closed,,, +188,closed,,, +189,closed,,, +190,closed,,, +191,closed,,, +192,closed,,, +193,closed,,, +194,closed,,, +195,closed,,, +196,closed,,, +197,closed,,, +198,closed,,, +199,closed,,, +200,closed,,, +201,closed,,, +202,closed,,, +203,closed,,, +204,closed,,, +205,closed,,, +206,closed,,, +207,closed,,, +208,closed,,, +209,closed,,, +210,closed,,, +211,closed,,, +212,closed,,, +213,closed,,, +214,closed,,, +215,closed,,, +216,closed,,, +217,closed,,, +218,closed,,, +219,closed,,, +220,closed,,, +221,closed,,, +222,closed,,, +223,closed,,, +224,closed,,, +225,closed,,, +226,closed,,, +227,closed,,, +228,closed,,, +229,closed,,, +230,closed,,, +231,closed,,, +232,closed,,, +233,closed,,, +234,closed,,, +235,closed,,, +236,closed,,, +237,closed,,, +238,closed,,, +239,closed,,, +240,closed,,, +241,closed,,, +242,closed,,, +243,closed,,, +244,closed,,, +245,closed,,, +246,closed,,, +247,closed,,, +248,closed,,, +249,closed,,, +250,closed,,, +251,closed,,, +252,closed,,, +253,closed,,, +254,closed,,, +255,closed,,, \ No newline at end of file diff --git a/legacy-sdk/cli/sample/position_bundle_state/open.csv b/legacy-sdk/cli/sample/position_bundle_state/open.csv new file mode 100644 index 000000000..18e1ad007 --- /dev/null +++ b/legacy-sdk/cli/sample/position_bundle_state/open.csv @@ -0,0 +1,257 @@ +bundle index,state,lower tick index,upper tick index,liquidity +0,open,-69120,-68992,20982 +1,open,-68992,-68864,744581 +2,open,-68864,-68736,4280947 +3,open,-68736,-68608,13347375 +4,open,-68608,-68480,31064042 +5,open,-68480,-68352,60993079 +6,open,-68352,-68224,107181618 +7,open,-68224,-68096,174208368 +8,open,-68096,-67968,267233953 +9,open,-67968,-67840,392055320 +10,open,-67840,-67712,555164408 +11,open,-67712,-67584,763811555 +12,open,-67584,-67456,1026073880 +13,open,-67456,-67328,1350928922 +14,open,-67328,-67200,1748334161 +15,open,-67200,-67072,2229312547 +16,open,-67072,-66944,2806044726 +17,open,-66944,-66816,3491968161 +18,open,-66816,-66688,4301884021 +19,open,-66688,-66560,5252071932 +20,open,-66560,-66432,6360413486 +21,open,-66432,-66304,7646525049 +22,open,-66304,-66176,9131900359 +23,open,-66176,-66048,10840063798 +24,open,-66048,-65920,12796734965 +25,open,-65920,-65792,15030005397 +26,open,-65792,-65664,17570528210 +27,open,-65664,-65536,20451721579 +28,open,-65536,-65408,23709987093 +29,open,-65408,-65280,27384943799 +30,open,-65280,-65152,31519679313 +31,open,-65152,-65024,36161018803 +32,open,-65024,-64896,41359813374 +33,open,-64896,-64768,47171249035 +34,open,-64768,-64640,53655177557 +35,open,-64640,-64512,60876470963 +36,open,-64512,-64384,68905400905 +37,open,-64384,-64256,77818044917 +38,open,-64256,-64128,87696721153 +39,open,-64128,-64000,98630453560 +40,open,-64000,-63872,110715469613 +41,open,-63872,-63744,124055732651 +42,open,-63744,-63616,138763511217 +43,open,-63616,-63488,154959987819 +44,open,-63488,-63360,172775909770 +45,open,-63360,-63232,192352284828 +46,open,-63232,-63104,213841124676 +47,open,-63104,-62976,237406239386 +48,open,-62976,-62848,263224086101 +49,open,-62848,-62720,291484675806 +50,open,-62720,-62592,322392541605 +51,open,-62592,-62464,356167772903 +52,open,-62464,-62336,393047119560 +53,open,-62336,-62208,433285170749 +54,open,-62208,-62080,477155613307 +55,open,-62080,-61952,524952574760 +56,open,-61952,-61824,576992056654 +57,open,-61824,-61696,633613463839 +58,open,-61696,-61568,695181236260 +59,open,-61568,-61440,762086589477 +60,open,-61440,-61312,834749371404 +61,open,-61312,-61184,913620042445 +62,open,-61184,-61056,999181787116 +63,open,-61056,-60928,1091952765675 +64,open,-60928,-60800,1192488514750 +65,open,-60800,-60672,1301384506402 +66,open,-60672,-60544,1419278876104 +67,open,-60544,-60416,1546855330194 +68,open,-60416,-60288,1684846244414 +69,open,-60288,-60160,1834035965822 +70,open,-60160,-60032,1995264330925 +71,open,-60032,-59904,2169430414104 +72,open,-59904,-59776,2357496520637 +73,open,-59776,-59648,2560492440375 +74,open,-59648,-59520,2779519978216 +75,open,-59520,-59392,3015757779275 +76,open,-59392,-59264,3270466467438 +77,open,-59264,-59136,3544994117026 +78,open,-59136,-59008,3840782078784 +79,open,-59008,-58880,4159371182832 +80,open,-58880,-58752,4502408341978 +81,open,-58752,-58624,4871653581234 +82,open,-58624,-58496,5268987519924 +83,open,-58496,-58368,5696419335504 +84,open,-58368,-58240,6156095238897 +85,open,-58240,-58112,6650307494009 +86,open,-58112,-57984,7181504015368 +87,open,-57984,-57856,7752298580570 +88,open,-57856,-57728,8365481695640 +89,open,-57728,-57600,9024032154893 +90,open,-57600,-57472,9731129338396 +91,open,-57472,-57344,10490166293550 +92,open,-57344,-57216,11304763649591 +93,open,-57216,-57088,12178784417514 +94,open,-57088,-56960,13116349730425 +95,open,-56960,-56832,14121855583296 +96,open,-56832,-56704,15199990634380 +97,open,-56704,-56576,16355755134693 +98,open,-56576,-56448,17594481055629 +99,open,-56448,-56320,18921853489586 +100,open,-56320,-56192,20343933402721 +101,open,-56192,-56064,21867181823982 +102,open,-56064,-55936,23498485559738 +103,open,-55936,-55808,25245184528629 +104,open,-55808,-55680,27115100817563 +105,open,-55680,-55552,29116569565170 +106,open,-55552,-55424,31258471786556 +107,open,-55424,-55296,33550269259130 +108,open,-55296,-55168,36002041597593 +109,open,-55168,-55040,38624525653229 +110,open,-55040,-54912,41429157381586 +111,open,-54912,-54784,44428116330693 +112,open,-54784,-54656,47634372912294 +113,open,-54656,-54528,51061738627385 +114,open,-54528,-54400,54724919428698 +115,open,-54400,-54272,58639572413563 +116,open,-54272,-54144,62822366052219 +117,open,-54144,-54016,67291044169864 +118,open,-54016,-53888,72064493912922 +119,open,-53888,-53760,77162817945488 +120,open,-53760,-53632,82607411135440 +121,open,-53632,-53504,88421042006865 +122,open,-53504,-53376,94627939251257 +123,open,-53376,-53248,101253883608438 +124,open,-53248,-53120,108326305446800 +125,open,-53120,-52992,115874388392524 +126,closed,,, +127,closed,,, +128,closed,,, +129,closed,,, +130,closed,,, +131,closed,,, +132,closed,,, +133,closed,,, +134,closed,,, +135,closed,,, +136,closed,,, +137,closed,,, +138,closed,,, +139,closed,,, +140,closed,,, +141,closed,,, +142,closed,,, +143,closed,,, +144,closed,,, +145,closed,,, +146,closed,,, +147,closed,,, +148,closed,,, +149,closed,,, +150,closed,,, +151,closed,,, +152,closed,,, +153,closed,,, +154,closed,,, +155,closed,,, +156,closed,,, +157,closed,,, +158,closed,,, +159,closed,,, +160,closed,,, +161,closed,,, +162,closed,,, +163,closed,,, +164,closed,,, +165,closed,,, +166,closed,,, +167,closed,,, +168,closed,,, +169,closed,,, +170,closed,,, +171,closed,,, +172,closed,,, +173,closed,,, +174,closed,,, +175,closed,,, +176,closed,,, +177,closed,,, +178,closed,,, +179,closed,,, +180,closed,,, +181,closed,,, +182,closed,,, +183,closed,,, +184,closed,,, +185,closed,,, +186,closed,,, +187,closed,,, +188,closed,,, +189,closed,,, +190,closed,,, +191,closed,,, +192,closed,,, +193,closed,,, +194,closed,,, +195,closed,,, +196,closed,,, +197,closed,,, +198,closed,,, +199,closed,,, +200,closed,,, +201,closed,,, +202,closed,,, +203,closed,,, +204,closed,,, +205,closed,,, +206,closed,,, +207,closed,,, +208,closed,,, +209,closed,,, +210,closed,,, +211,closed,,, +212,closed,,, +213,closed,,, +214,closed,,, +215,closed,,, +216,closed,,, +217,closed,,, +218,closed,,, +219,closed,,, +220,closed,,, +221,closed,,, +222,closed,,, +223,closed,,, +224,closed,,, +225,closed,,, +226,closed,,, +227,closed,,, +228,closed,,, +229,closed,,, +230,closed,,, +231,closed,,, +232,closed,,, +233,closed,,, +234,closed,,, +235,closed,,, +236,closed,,, +237,closed,,, +238,closed,,, +239,closed,,, +240,closed,,, +241,closed,,, +242,closed,,, +243,closed,,, +244,closed,,, +245,closed,,, +246,closed,,, +247,closed,,, +248,closed,,, +249,closed,,, +250,closed,,, +251,closed,,, +252,closed,,, +253,closed,,, +254,closed,,, +255,closed,,, \ No newline at end of file diff --git a/legacy-sdk/cli/src/commands/alt/initialize_alt_for_bundled_positions.ts b/legacy-sdk/cli/src/commands/alt/initialize_alt_for_bundled_positions.ts new file mode 100644 index 000000000..baee22255 --- /dev/null +++ b/legacy-sdk/cli/src/commands/alt/initialize_alt_for_bundled_positions.ts @@ -0,0 +1,136 @@ +import { AddressLookupTableProgram, PublicKey } from "@solana/web3.js"; +import { + IGNORE_CACHE, + PDAUtil, + POSITION_BUNDLE_SIZE, + toTx, +} from "@orca-so/whirlpools-sdk"; +import { TransactionBuilder } from "@orca-so/common-sdk"; +import { sendTransaction } from "../../utils/transaction_sender"; +import { ctx } from "../../utils/provider"; +import { promptText } from "../../utils/prompt"; + +console.info("initialize ALT for bundled positions..."); + +// prompt +const positionBundlePubkeyStr = await promptText("positionBundlePubkey"); +const positionBundlePubkey = new PublicKey(positionBundlePubkeyStr); + +const positionBundle = await ctx.fetcher.getPositionBundle( + positionBundlePubkey, + IGNORE_CACHE, +); +if (!positionBundle) { + throw new Error("positionBundle not found"); +} + +const bundledPositionAddresses: PublicKey[] = []; +for (let bundleIndex = 0; bundleIndex < POSITION_BUNDLE_SIZE; bundleIndex++) { + bundledPositionAddresses.push( + PDAUtil.getBundledPosition( + ctx.program.programId, + positionBundle.positionBundleMint, + bundleIndex, + ).publicKey, + ); +} + +const txs: TransactionBuilder[] = []; +let altAddressPubkey: PublicKey; +let altAddressPubkeyStr = await promptText( + "altAddressPubkey", + "create new ALT", +); +let altEntries = 0; +if (altAddressPubkeyStr === "create new ALT") { + const [createLookupTableIx, alt] = + AddressLookupTableProgram.createLookupTable({ + authority: ctx.wallet.publicKey, + payer: ctx.wallet.publicKey, + recentSlot: await ctx.connection.getSlot({ commitment: "confirmed" }), + }); + altAddressPubkey = alt; + txs.push( + toTx(ctx, { + instructions: [createLookupTableIx], + cleanupInstructions: [], + signers: [], + }), + ); +} else { + altAddressPubkey = new PublicKey(altAddressPubkeyStr); + const res = await ctx.connection.getAddressLookupTable(altAddressPubkey); + if (!res || !res.value) { + throw new Error("altAddress not found"); + } + altEntries = res.value.state.addresses.length; +} + +console.info("ALT address:", altAddressPubkey.toBase58()); + +for ( + let bundleIndexStart = altEntries; + bundleIndexStart < POSITION_BUNDLE_SIZE; + bundleIndexStart += 16 +) { + const extendLookupTableIx = AddressLookupTableProgram.extendLookupTable({ + lookupTable: altAddressPubkey, + authority: ctx.wallet.publicKey, + payer: ctx.wallet.publicKey, + addresses: bundledPositionAddresses.slice( + bundleIndexStart, + bundleIndexStart + 16, + ), + }); + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder.addInstruction({ + instructions: [extendLookupTableIx], + cleanupInstructions: [], + signers: [], + }); + txs.push(builder); +} + +if (txs.length === 0) { + console.info("ALT is full"); + process.exit(0); +} + +const defaultPriorityFeeInLamports = 10_000; // 0.00001 SOL +for (const tx of txs) { + const landed = await sendTransaction(tx, defaultPriorityFeeInLamports); + if (!landed) { + throw new Error("transaction failed"); + } +} + +/* + +SAMPLE EXECUTION LOG + +connection endpoint http://localhost:8899 +wallet r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6 +initialize ALT for bundled positions... +✔ positionBundlePubkey … qHbk42b2ub8K6Rw6p7t1aUoJpwGZ6xpzDC75CQ4QgPD +✔ altAddressPubkey … create new ALT +ALT address: CjBg5mt3n43aGAKVZqNA1rLBoH16sbwcgkAncLrgoXxw +estimatedComputeUnits: 1400000 +process transaction... +transaction is still valid, 151 blocks left (at most) +sending... +confirming... +✅successfully landed +signature 4sHGkrE2XxcNahCZbqG2V3yRAawNYa6VXJH7LfVPe5LuFnwt3YmAeUxXgEdP7pTLPUbizbdGSEaEUGfPcxuKd97C +... +... +... +estimatedComputeUnits: 100900 +process transaction... +transaction is still valid, 151 blocks left (at most) +sending... +confirming... +✅successfully landed +signature 4Y1V9tSLcCEGNueL7Rr8YZrXWJCjWLTuWAk17DRw6v8kLo1QWDT914F26kETEUWEnxuPfsVcFjV82ZwhvnnjUV2i + +*/ diff --git a/legacy-sdk/cli/src/commands/alt/initialize_alt_for_whirlpool.ts b/legacy-sdk/cli/src/commands/alt/initialize_alt_for_whirlpool.ts new file mode 100644 index 000000000..20f010a6f --- /dev/null +++ b/legacy-sdk/cli/src/commands/alt/initialize_alt_for_whirlpool.ts @@ -0,0 +1,188 @@ +import { AddressLookupTableProgram, PublicKey } from "@solana/web3.js"; +import { + IGNORE_CACHE, + MAX_TICK_INDEX, + MIN_TICK_INDEX, + PDAUtil, + PoolUtil, + TICK_ARRAY_SIZE, + TickUtil, +} from "@orca-so/whirlpools-sdk"; +import { TransactionBuilder } from "@orca-so/common-sdk"; +import type { MintWithTokenProgram } from "@orca-so/common-sdk"; +import { sendTransaction } from "../../utils/transaction_sender"; +import { ctx } from "../../utils/provider"; +import { promptText } from "../../utils/prompt"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; + +console.info("initialize ALT for whirlpool..."); + +// prompt +const whirlpoolPubkeyStr = await promptText("whirlpoolPubkey"); +const whirlpoolPubkey = new PublicKey(whirlpoolPubkeyStr); + +const whirlpool = await ctx.fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE); +if (!whirlpool) { + throw new Error("whirlpool not found"); +} +const mintA = (await ctx.fetcher.getMintInfo( + whirlpool.tokenMintA, +)) as MintWithTokenProgram; +const mintB = (await ctx.fetcher.getMintInfo( + whirlpool.tokenMintB, +)) as MintWithTokenProgram; + +// 8 keys +const addresses: PublicKey[] = [ + whirlpoolPubkey, + whirlpool.whirlpoolsConfig, + whirlpool.tokenMintA, + whirlpool.tokenMintB, + whirlpool.tokenVaultA, + whirlpool.tokenVaultB, + // This ALT is just for ctx.wallet + getAssociatedTokenAddressSync( + whirlpool.tokenMintA, + ctx.wallet.publicKey, + true, + mintA.tokenProgram, + ), + getAssociatedTokenAddressSync( + whirlpool.tokenMintB, + ctx.wallet.publicKey, + true, + mintB.tokenProgram, + ), +]; +// max 9 keys +for (const rewardInfo of whirlpool.rewardInfos.filter((rewardInfo) => + PoolUtil.isRewardInitialized(rewardInfo), +)) { + const mint = (await ctx.fetcher.getMintInfo( + rewardInfo.mint, + )) as MintWithTokenProgram; + + addresses.push(rewardInfo.mint); + addresses.push(rewardInfo.vault); + // This ALT is just for ctx.wallet + addresses.push( + getAssociatedTokenAddressSync( + rewardInfo.mint, + ctx.wallet.publicKey, + true, + mint.tokenProgram, + ), + ); +} +// 1 key +addresses.push( + PDAUtil.getOracle(ctx.program.programId, whirlpoolPubkey).publicKey, +); + +// at most 11 TickArrays (previous 5 + current + next 5) +const minStartTickIndex = TickUtil.getStartTickIndex( + MIN_TICK_INDEX, + whirlpool.tickSpacing, +); +const maxStartTickIndex = TickUtil.getStartTickIndex( + MAX_TICK_INDEX, + whirlpool.tickSpacing, +); +const currentStartTickIndex = TickUtil.getStartTickIndex( + whirlpool.tickCurrentIndex, + whirlpool.tickSpacing, +); +const ticksInArray = whirlpool.tickSpacing * TICK_ARRAY_SIZE; + +const firstStartTickIndex = Math.max( + minStartTickIndex, + currentStartTickIndex - 5 * ticksInArray, +); +const lastStartTickIndex = Math.min( + maxStartTickIndex, + currentStartTickIndex + 5 * ticksInArray, +); +for ( + let startTickIndex = firstStartTickIndex; + startTickIndex <= lastStartTickIndex; + startTickIndex += ticksInArray +) { + addresses.push( + PDAUtil.getTickArray(ctx.program.programId, whirlpoolPubkey, startTickIndex) + .publicKey, + ); +} + +// at most 29 entries (8 + 9 + 1 + 11) +// single transaction (createLookupTable + extendLookupTable can cover up to 30 entries based on local test) + +const [createLookupTableIx, alt] = AddressLookupTableProgram.createLookupTable({ + authority: ctx.wallet.publicKey, + payer: ctx.wallet.publicKey, + recentSlot: await ctx.connection.getSlot({ commitment: "confirmed" }), +}); + +const extendLookupTableIx = AddressLookupTableProgram.extendLookupTable({ + lookupTable: alt, + authority: ctx.wallet.publicKey, + payer: ctx.wallet.publicKey, + addresses, +}); + +const builder = new TransactionBuilder(ctx.connection, ctx.wallet); +builder.addInstruction({ + instructions: [createLookupTableIx, extendLookupTableIx], + cleanupInstructions: [], + signers: [], +}); + +const landed = await sendTransaction(builder); +if (landed) { + console.info(`ALT initialized: ${alt.toBase58()}`); + console.info("Entries:"); + addresses.forEach((address, index) => { + console.info(`\t${index}: ${address.toBase58()}`); + }); +} + +/* + +SAMPLE EXECUTION LOG + +connection endpoint http://localhost:8899 +wallet r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6 +initialize ALT for whirlpool... +✔ whirlpoolPubkey … 95XaJMqCLiWtUwF9DtSvDpDbPYhEHoVyCeeNwmUD7cwr +estimatedComputeUnits: 1400000 +✔ priorityFeeInSOL … 0 +Priority fee: 0 SOL +process transaction... +transaction is still valid, 151 blocks left (at most) +sending... +confirming... +✅successfully landed +signature 3Xxo22EJ7BmMob7J2q2QJCtnJrcgvG8BAiya1wf3Ka66RKCxsz2w6ss8ywbK2XwWzcJHbAmpDVMuiD3iVjZFy1T9 +ALT initialized: 5Fk3kjiAyz1TEQUpvHThibfssgQqkMkddjBMbPL2TgiZ +Entries: + 0: 95XaJMqCLiWtUwF9DtSvDpDbPYhEHoVyCeeNwmUD7cwr + 1: 2LecshUwdy9xi7meFgHtFJQNSKk4KdTrcpvaB56dP2NQ + 2: orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE + 3: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v + 4: 8tgCn1xei522Di8AjYtfkFEjhn6MmUHW2YcQr8mDYfL2 + 5: 2PieLBFZd3GjwMdeMcQwkr5fcxk4xNPYfMEHNAZxGjey + 6: 7B8yNHX62NLvRswD86ttbGcV5TYxUsDNxEg2ZRMZLPRt + 7: FbQdXCQgGQYj3xcGeryVVFjKCTsAuu53vmCRtmjQEqM5 + 8: 5S8p3uciu7AaEREiibf4Z4hEB3B61mSf671taY497mvv + 9: 3ScwtoeaBQBzxncQZeiZSBCdTwd8jTfQsy52JrsRdpp2 + 10: 7bfNn3E5jgFXs4sbXiXVWBo2JJ5vWKhFhExR6TcamsQy + 11: 4ronWzGzbV2wEWHKvBQBxdHNPcUqor6vE4KNg6B4wbXT + 12: Eq4tmgWRG4jpAxwiktmh4sbBr8q1rvUDDFMXb4121F2i + 13: GwtBgneHFYQnLGFwVmQdDWB1qw5jYj3HbqAaErzYW8PZ + 14: HhQN6yDwC6Bv7DMn6fbREWTjJR7hKMycPr9nmo8tK3pL + 15: Hbjh64DET4ER9vSiCNwVJmiwR2NaedVSM36bh2urZeLS + 16: DGYoQK2gEtvanRt6ci6mz42PGne6eWjmHYBu9D1YZUrX + 17: BhiUaDDpsguydMWeoQy4k9qBtyh8rBLDssFLw9e4LYXq + 18: HMnbcY8bGLdcH2vmBMDgi4XCYnQKXqPhmWMPP1vYvo5z + 19: AryTbgjgHbt9o59DtcLS2RnhsDpudRVaCJzDge9Hieo8 + +*/ diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state.ts new file mode 100644 index 000000000..867d51cb6 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state.ts @@ -0,0 +1,404 @@ +import type { AddressLookupTableAccount } from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; +import { + IGNORE_CACHE, + PoolUtil, + POSITION_BUNDLE_SIZE, +} from "@orca-so/whirlpools-sdk"; +import type { WhirlpoolContext, WhirlpoolData } from "@orca-so/whirlpools-sdk"; +import { DecimalUtil, Percentage } from "@orca-so/common-sdk"; +import { ctx } from "../../utils/provider"; +import { promptConfirm, promptText } from "../../utils/prompt"; +import BN from "bn.js"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import type Decimal from "decimal.js"; +import { + checkATAInitialization, + checkTickArrayInitialization, + checkPositionBundleStateDifference, + generateQuotesToSync, + buildTransactions, + sendTransactions, + calculateBalanceDifference, + readPositionBundleStateCsv, +} from "./sync_position_bundle_state_impl"; + +console.info("sync PositionBundle state..."); + +// prompt +const positionBundlePubkeyStr = await promptText("positionBundlePubkey"); +const positionBundlePubkey = new PublicKey(positionBundlePubkeyStr); +const whirlpoolPubkeyStr = await promptText("whirlpoolPubkey"); +const whirlpoolPubkey = new PublicKey(whirlpoolPubkeyStr); + +const positionBundleTargetStateCsvPath = await promptText( + "positionBundleTargetStateCsvPath", +); + +const commaSeparatedAltPubkeyStrs = await promptText( + "commaSeparatedAltPubkeys", + "no ALTs", +); +const noAlts = commaSeparatedAltPubkeyStrs === "no ALTs"; +const altPubkeyStrs = noAlts + ? [] + : commaSeparatedAltPubkeyStrs + .split(",") + .map((str) => str.trim()) + .filter((str) => str.length > 0); +const altPubkeys = altPubkeyStrs.map((str) => new PublicKey(str)); + +console.info("check positionBundle..."); +const positionBundle = await ctx.fetcher.getPositionBundle( + positionBundlePubkey, + IGNORE_CACHE, +); +if (!positionBundle) { + throw new Error("positionBundle not found"); +} + +console.info("check whirlpool..."); +const whirlpool = await ctx.fetcher.getPool(whirlpoolPubkey, IGNORE_CACHE); +if (!whirlpool) { + throw new Error("whirlpool not found"); +} + +const alts: AddressLookupTableAccount[] = []; +if (altPubkeys.length > 0) { + console.info("check ALTs..."); + for (const altPubkey of altPubkeys) { + const res = await ctx.connection.getAddressLookupTable(altPubkey); + if (!res || !res.value) { + throw new Error(`altAddress not found: ${altPubkey.toBase58()}`); + } else { + console.info( + ` loaded ALT ${altPubkey.toBase58()}, ${res.value.state.addresses.length} entries`, + ); + } + alts.push(res.value); + } +} + +// read position bundle target state +console.info("read position bundle target state..."); +const positionBundleTargetState = readPositionBundleStateCsv( + positionBundleTargetStateCsvPath, + whirlpool.tickSpacing, +); + +// ensure that all required TickArrays are initialized +console.info("check if required TickArrays are initialized..."); +await checkTickArrayInitialization( + ctx, + whirlpoolPubkey, + positionBundleTargetState, +); + +// ensure that all required ATA are initialized +console.info("check if required ATAs are initialized..."); +await checkATAInitialization(ctx, whirlpool); + +const { toDecimalAmountA, toDecimalAmountB, toDecimalAmountReward } = + await getToDecimalAmountFunctions(ctx, whirlpool); + +let firstIteration = true; +while (true) { + console.info("check position bundle state difference..."); + const difference = await checkPositionBundleStateDifference( + ctx, + positionBundlePubkey, + whirlpoolPubkey, + positionBundleTargetState, + ); + + if (difference.noDifference.length === POSITION_BUNDLE_SIZE) { + console.info("synced"); + break; + } + + if (!firstIteration) { + console.warn( + "There are still differences between the current state and the target state (some transaction may have failed)", + ); + } + + // TODO: prompt for slippage + const slippage = Percentage.fromFraction(1, 100); // 1% + const quotes = await generateQuotesToSync( + ctx, + whirlpoolPubkey, + positionBundleTargetState, + difference, + slippage, + ); + const balanceDifference = calculateBalanceDifference(quotes); + + const { tokenABalance, tokenBBalance } = await getWalletATABalance( + ctx, + whirlpool, + ); + + console.info("building transactions..."); + const transactions = await buildTransactions( + ctx, + alts, + positionBundlePubkey, + whirlpoolPubkey, + difference, + positionBundleTargetState, + quotes, + ); + + console.info( + [ + "\n📝 ACTION SUMMARY\n", + "\n", + `Pool: ${whirlpoolPubkey.toBase58()}\n`, + `PositionBundle: ${positionBundlePubkey.toBase58()}\n`, + `Target state: ${positionBundleTargetStateCsvPath}\n`, + "\n", + "Position state changes:\n", + "\n", + ` close position: ${difference.shouldBeClosed.length.toString().padStart(3, " ")} position(s)\n`, + ` open position: ${difference.shouldBeOpened.length.toString().padStart(3, " ")} position(s)\n`, + ` withdraw liquidity: ${difference.shouldBeDecreased.length.toString().padStart(3, " ")} position(s)\n`, + ` deposit liquidity: ${difference.shouldBeIncreased.length.toString().padStart(3, " ")} position(s)\n`, + "\n", + "Balance changes:\n", + "\n", + ` slippage: ${slippage.toDecimal().mul(100).toString()} %\n`, + "\n", + ` tokenA withdrawn (est): ${toDecimalAmountA(balanceDifference.tokenAWithdrawnEst)}\n`, + ` tokenB withdrawn (est): ${toDecimalAmountB(balanceDifference.tokenBWithdrawnEst)}\n`, + ` tokenA withdrawn (min): ${toDecimalAmountA(balanceDifference.tokenAWithdrawnMin)}\n`, + ` tokenB withdrawn (min): ${toDecimalAmountB(balanceDifference.tokenBWithdrawnMin)}\n`, + ` tokenA collected: ${toDecimalAmountA(balanceDifference.tokenACollected)}\n`, + ` tokenB collected: ${toDecimalAmountB(balanceDifference.tokenBCollected)}\n`, + ` rewards collected: ${balanceDifference.rewardsCollected.map((reward, i) => (reward ? toDecimalAmountReward(reward, i).toString() : "no reward")).join(", ")}\n`, + ` tokenA deposited (est): ${toDecimalAmountA(balanceDifference.tokenADepositedEst)}\n`, + ` tokenB deposited (est): ${toDecimalAmountB(balanceDifference.tokenBDepositedEst)}\n`, + ` tokenA deposited (max): ${toDecimalAmountA(balanceDifference.tokenADepositedMax)}\n`, + ` tokenB deposited (max): ${toDecimalAmountB(balanceDifference.tokenBDepositedMax)}\n`, + "\n", + ` tokenA balance delta (est): ${toDecimalAmountA(balanceDifference.tokenABalanceDeltaEst)}\n`, + ` tokenB balance delta (est): ${toDecimalAmountB(balanceDifference.tokenBBalanceDeltaEst)}\n`, + "\n", + " * negative balance delta means deposited more than withdrawn\n", + "\n", + "Wallet balances:\n", + "\n", + ` tokenA: ${toDecimalAmountA(tokenABalance)}\n`, + ` tokenB: ${toDecimalAmountB(tokenBBalance)}\n`, + "\n", + "Transactions:\n", + "\n", + ` withdraw: ${transactions.withdrawTransactions.length} transaction(s)\n`, + ` deposit: ${transactions.depositTransactions.length} transaction(s)\n`, + ].join(""), + ); + + if ( + balanceDifference.tokenABalanceDeltaEst.isNeg() && + balanceDifference.tokenABalanceDeltaEst.abs().gt(tokenABalance) + ) { + console.warn( + "WARNING: tokenA balance delta exceeds the wallet balance, some deposits may fail\n", + ); + } + if ( + balanceDifference.tokenBBalanceDeltaEst.isNeg() && + balanceDifference.tokenBBalanceDeltaEst.abs().gt(tokenBBalance) + ) { + console.warn( + "WARNING: tokenB balance delta exceeds the wallet balance, some deposits may fail\n", + ); + } + + // prompt for confirmation + const confirmed = await promptConfirm("proceed?"); + if (!confirmed) { + console.info("canceled"); + break; + } + + // TODO: prompt for priority fee + const defaultPriorityFeeInLamports = 10_000; // 0.00001 SOL + await sendTransactions( + ctx, + alts, + transactions.withdrawTransactions, + defaultPriorityFeeInLamports, + ); + await sendTransactions( + ctx, + alts, + transactions.depositTransactions, + defaultPriorityFeeInLamports, + ); + + firstIteration = false; +} + +async function getToDecimalAmountFunctions( + ctx: WhirlpoolContext, + whirlpool: WhirlpoolData, +): Promise<{ + toDecimalAmountA: (amount: BN) => Decimal; + toDecimalAmountB: (amount: BN) => Decimal; + toDecimalAmountReward: (amount: BN, rewardIndex: number) => Decimal; +}> { + const mintStrings = new Set(); + mintStrings.add(whirlpool.tokenMintA.toBase58()); + mintStrings.add(whirlpool.tokenMintB.toBase58()); + whirlpool.rewardInfos.forEach((rewardInfo) => { + if (PoolUtil.isRewardInitialized(rewardInfo)) { + mintStrings.add(rewardInfo.mint.toBase58()); + } + }); + + const mintAddresses = Array.from(mintStrings).map( + (mintStr) => new PublicKey(mintStr), + ); + const mints = await ctx.fetcher.getMintInfos(mintAddresses, IGNORE_CACHE); + + const decimalsA = mints.get(whirlpool.tokenMintA.toBase58())!.decimals; + const decimalsB = mints.get(whirlpool.tokenMintB.toBase58())!.decimals; + const decimalsRewards = whirlpool.rewardInfos.map((rewardInfo) => { + if (PoolUtil.isRewardInitialized(rewardInfo)) { + return mints.get(rewardInfo.mint.toBase58())!.decimals; + } else { + return 0; + } + }); + + const toDecimalAmountA = (amount: BN) => + DecimalUtil.fromBN(amount, decimalsA); + const toDecimalAmountB = (amount: BN) => + DecimalUtil.fromBN(amount, decimalsB); + const toDecimalAmountReward = (amount: BN, rewardIndex: number) => + DecimalUtil.fromBN(amount, decimalsRewards[rewardIndex]); + + return { toDecimalAmountA, toDecimalAmountB, toDecimalAmountReward }; +} + +async function getWalletATABalance( + ctx: WhirlpoolContext, + whirlpool: WhirlpoolData, +): Promise<{ + tokenABalance: BN; + tokenBBalance: BN; +}> { + const mintAddresses = [whirlpool.tokenMintA, whirlpool.tokenMintB]; + const mints = await ctx.fetcher.getMintInfos(mintAddresses); + + const ataAddresses = mintAddresses.map((mint) => + getAssociatedTokenAddressSync( + mint, + ctx.wallet.publicKey, + true, // allow PDA for safety + mints.get(mint.toBase58())!.tokenProgram, // may be Token-2022 token + ), + ); + + const atas = await ctx.fetcher.getTokenInfos(ataAddresses, IGNORE_CACHE); + + return { + tokenABalance: new BN( + atas.get(ataAddresses[0].toBase58())!.amount.toString(), + ), + tokenBBalance: new BN( + atas.get(ataAddresses[1].toBase58())!.amount.toString(), + ), + }; +} + +/* + +SAMPLE EXECUTION LOG + +connection endpoint http://localhost:8899 +wallet r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6 +sync PositionBundle state... +✔ positionBundlePubkey … qHbk42b2ub8K6Rw6p7t1aUoJpwGZ6xpzDC75CQ4QgPD +✔ whirlpoolPubkey … 95XaJMqCLiWtUwF9DtSvDpDbPYhEHoVyCeeNwmUD7cwr +✔ positionBundleTargetStateCsvPath … sample/position_bundle_state/open.csv +✔ commaSeparatedAltPubkeys … 7Vyx1y8vG9e9Q1MedmXpopRC6ZhVaZzGcvYh5Z3Cs75i, AnXmyHSfuAaWkCxaUuTW39SN5H5ztH8bBxm647uESgTd, FjTZwDecYM3G66VKFuAaLgw3rY1QitziKdM5Ng4EpoKd +check positionBundle... +check whirlpool... +check ALTs... + loaded ALT 7Vyx1y8vG9e9Q1MedmXpopRC6ZhVaZzGcvYh5Z3Cs75i, 254 entries + loaded ALT AnXmyHSfuAaWkCxaUuTW39SN5H5ztH8bBxm647uESgTd, 256 entries + loaded ALT FjTZwDecYM3G66VKFuAaLgw3rY1QitziKdM5Ng4EpoKd, 20 entries +read position bundle target state... +check if required TickArrays are initialized... +check if required ATAs are initialized... +check position bundle state difference... +building transactions... + +📝 ACTION SUMMARY + +Pool: 95XaJMqCLiWtUwF9DtSvDpDbPYhEHoVyCeeNwmUD7cwr +PositionBundle: qHbk42b2ub8K6Rw6p7t1aUoJpwGZ6xpzDC75CQ4QgPD +Target state: sample/position_bundle_target_state_open.csv + +Position state changes: + + close position: 0 position(s) + open position: 126 position(s) + withdraw liquidity: 0 position(s) + deposit liquidity: 0 position(s) + +Balance changes: + + slippage: 1 % + + tokenA withdrawn (est): 0 + tokenB withdrawn (est): 0 + tokenA withdrawn (min): 0 + tokenB withdrawn (min): 0 + tokenA collected: 0 + tokenB collected: 0 + rewards collected: no reward, no reward, no reward + tokenA deposited (est): 0 + tokenB deposited (est): 711326.515908 + tokenA deposited (max): 0 + tokenB deposited (max): 718439.781001 + + tokenA balance delta (est): 0 + tokenB balance delta (est): -711326.515908 + + * negative balance delta means deposited more than withdrawn + +Wallet balances: + + tokenA: 1000000000 + tokenB: 999999999.999622 + +Transactions: + + withdraw: 0 transaction(s) + deposit: 14 transaction(s) + +✔ proceed? › Yes + +estimatedComputeUnits: 1400000 +process transaction... +transaction is still valid, 151 blocks left (at most) +sending... +confirming... +✅successfully landed +signature 3K2nYSwpSCcHNvNtUCQZ9UCbpeR64BL7GGf2xdW1RMLim1wU53Ju9rfEYv1qLbhmm1vMYRvtV3g2CYxnoz4MQWTT +... +... +... +estimatedComputeUnits: 1400000 +process transaction... +transaction is still valid, 151 blocks left (at most) +sending... +confirming... +✅successfully landed +signature 2U1SLJSQnH434DgTTiAQw52YrSMBKUELJ73knfvNMV2KbMofSTk94w4DKY1fEuMe8rD2bTPT8V6C1qavBYMB2JCc +check position bundle state difference... +synced + +*/ diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/csv.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/csv.ts new file mode 100644 index 000000000..81e3d4352 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/csv.ts @@ -0,0 +1,102 @@ +import { readFileSync } from "fs"; +import BN from "bn.js"; +import { + MAX_TICK_INDEX, + MIN_TICK_INDEX, + POSITION_BUNDLE_SIZE, +} from "@orca-so/whirlpools-sdk"; + +export type PositionBundleOpenState = { + state: "open"; + lowerTickIndex: number; + upperTickIndex: number; + liquidity: BN; +}; +export type PositionBundleClosedState = { state: "closed" }; +export type PositionBundleStateItem = + | PositionBundleOpenState + | PositionBundleClosedState; + +export function readPositionBundleStateCsv( + positionBundleStateCsvPath: string, + tickSpacing: number, +): PositionBundleStateItem[] { + // read entire CSV file + const csv = readFileSync(positionBundleStateCsvPath, "utf8"); + + // parse CSV (trim is needed for safety (remove CR code)) + const lines = csv.split("\n"); + const header = lines[0].trim(); + const data = lines.slice(1).map((line) => line.trim().split(",")); + + // check header + const EXPECTED_HEADER = + "bundle index,state,lower tick index,upper tick index,liquidity"; + if (header !== EXPECTED_HEADER) { + console.debug(`${header}<`); + console.debug(`${EXPECTED_HEADER}<`); + throw new Error(`unexpected header: ${header}`); + } + + // check data + if (data.length !== POSITION_BUNDLE_SIZE) { + throw new Error( + `unexpected data length: ${data.length} (must be ${POSITION_BUNDLE_SIZE})`, + ); + } + + // parse data + return data.map((entry, expectedBundleIndex) => { + // sanity checks... + + if (entry.length !== 5) { + throw new Error( + `unexpected entry length: ${entry.length}, line: ${entry}`, + ); + } + + const bundleIndex = parseInt(entry[0]); + if (bundleIndex !== expectedBundleIndex) { + throw new Error( + `unexpected bundle index: ${bundleIndex}, expected: ${expectedBundleIndex}`, + ); + } + + const state = entry[1]; + if (state === "closed") { + return { state: "closed" }; + } + if (state !== "open") { + throw new Error(`unexpected state: ${state}`); + } + + const lowerTickIndex = parseInt(entry[2]); + const upperTickIndex = parseInt(entry[3]); + const liquidity = new BN(entry[4]); + if (isNaN(lowerTickIndex) || isNaN(upperTickIndex)) { + throw new Error( + `invalid tick indexes (not number): ${entry[2]}, ${entry[3]}`, + ); + } + if (lowerTickIndex >= upperTickIndex) { + throw new Error( + `invalid tick indexes (lower >= upper): ${entry[2]}, ${entry[3]}`, + ); + } + if (lowerTickIndex < MIN_TICK_INDEX || upperTickIndex > MAX_TICK_INDEX) { + throw new Error( + `invalid tick indexes (out of range): ${entry[2]}, ${entry[3]}`, + ); + } + if ( + lowerTickIndex % tickSpacing !== 0 || + upperTickIndex % tickSpacing !== 0 + ) { + throw new Error( + `invalid tick indexes (not initializable): ${entry[2]}, ${entry[3]}`, + ); + } + + return { state: "open", lowerTickIndex, upperTickIndex, liquidity }; + }); +} diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/index.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/index.ts new file mode 100644 index 000000000..c80a77967 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/index.ts @@ -0,0 +1,5 @@ +export * from "./quote"; +export * from "./csv"; +export * from "./state_difference"; +export * from "./pre_check"; +export * from "./transaction"; diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/pre_check.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/pre_check.ts new file mode 100644 index 000000000..ef20464b9 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/pre_check.ts @@ -0,0 +1,88 @@ +import { PublicKey } from "@solana/web3.js"; +import { + IGNORE_CACHE, + TickArrayUtil, + TickUtil, + PDAUtil, + PoolUtil, +} from "@orca-so/whirlpools-sdk"; +import type { WhirlpoolContext, WhirlpoolData } from "@orca-so/whirlpools-sdk"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import type { PositionBundleStateItem } from "./csv"; + +export async function checkTickArrayInitialization( + ctx: WhirlpoolContext, + whirlpoolPubkey: PublicKey, + positionBundleTargetState: PositionBundleStateItem[], +) { + const whirlpool = (await ctx.fetcher.getPool( + whirlpoolPubkey, + )) as WhirlpoolData; // tickSpacing is immutable + const tickSpacing = whirlpool.tickSpacing; + + const tickArrayStartIndexes = new Set(); + for (const targetState of positionBundleTargetState) { + if (targetState.state === "open") { + tickArrayStartIndexes.add( + TickUtil.getStartTickIndex(targetState.lowerTickIndex, tickSpacing), + ); + tickArrayStartIndexes.add( + TickUtil.getStartTickIndex(targetState.upperTickIndex, tickSpacing), + ); + } + } + + const tickArrayAddresses = Array.from(tickArrayStartIndexes).map( + (startIndex) => + PDAUtil.getTickArray(ctx.program.programId, whirlpoolPubkey, startIndex) + .publicKey, + ); + + const uninitialized = await TickArrayUtil.getUninitializedArraysString( + tickArrayAddresses, + ctx.fetcher, + IGNORE_CACHE, + ); + if (uninitialized) { + throw new Error(`uninitialized TickArrays: ${uninitialized}`); + } +} + +export async function checkATAInitialization( + ctx: WhirlpoolContext, + whirlpool: WhirlpoolData, +) { + const mintStrings = new Set(); + mintStrings.add(whirlpool.tokenMintA.toBase58()); + mintStrings.add(whirlpool.tokenMintB.toBase58()); + whirlpool.rewardInfos.forEach((rewardInfo) => { + if (PoolUtil.isRewardInitialized(rewardInfo)) { + mintStrings.add(rewardInfo.mint.toBase58()); + } + }); + + const mintAddresses = Array.from(mintStrings).map( + (mintStr) => new PublicKey(mintStr), + ); + const mints = await ctx.fetcher.getMintInfos(mintAddresses, IGNORE_CACHE); + + const ataAddresses = mintAddresses.map((mint) => + getAssociatedTokenAddressSync( + mint, + ctx.wallet.publicKey, + true, // allow PDA for safety + mints.get(mint.toBase58())!.tokenProgram, // may be Token-2022 token + ), + ); + + const atas = await ctx.fetcher.getTokenInfos(ataAddresses, IGNORE_CACHE); + const uninitialized = mintAddresses.filter( + (_, i) => !atas.get(ataAddresses[i].toBase58()), + ); + + if (uninitialized.length > 0) { + throw new Error( + `uninitialized ATAs for mint: ${uninitialized.map((mint) => mint.toBase58()).join(", ")}`, + ); + } +} diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/quote.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/quote.ts new file mode 100644 index 000000000..59dc0bb52 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/quote.ts @@ -0,0 +1,399 @@ +import type { PublicKey } from "@solana/web3.js"; +import { + collectFeesQuote, + collectRewardsQuote, + decreaseLiquidityQuoteByLiquidityWithParams, + IGNORE_CACHE, + increaseLiquidityQuoteByLiquidityWithParams, + NO_TOKEN_EXTENSION_CONTEXT, + PDAUtil, + PREFER_CACHE, + TickArrayUtil, + TickUtil, + TokenExtensionUtil, +} from "@orca-so/whirlpools-sdk"; +import type { + CollectFeesQuote, + CollectRewardsQuote, + DecreaseLiquidityQuote, + IncreaseLiquidityQuote, + IncreaseLiquidityQuoteByLiquidityParam, + PositionData, + TickArrayData, + WhirlpoolContext, + WhirlpoolData, +} from "@orca-so/whirlpools-sdk"; +import { Percentage } from "@orca-so/common-sdk"; +import BN from "bn.js"; +import { adjustForSlippage } from "@orca-so/whirlpools-sdk/dist/utils/position-util"; +import type { PositionBundleOpenState, PositionBundleStateItem } from "./csv"; +import type { PositionBundleStateDifference } from "./state_difference"; + +export type QuotesForDecrease = { + bundleIndex: number; + decrease: DecreaseLiquidityQuote; +}; +export type QuotesForClose = { + bundleIndex: number; + decrease: DecreaseLiquidityQuote | undefined; + collectFees: CollectFeesQuote; + collectRewards: CollectRewardsQuote; +}; +export type QuotesForOpen = { + bundleIndex: number; + increase: IncreaseLiquidityQuote | undefined; +}; +export type QuotesForIncrease = { + bundleIndex: number; + increase: IncreaseLiquidityQuote; +}; +export type QuotesToSync = { + quotesForDecrease: QuotesForDecrease[]; + quotesForClose: QuotesForClose[]; + quotesForOpen: QuotesForOpen[]; + quotesForIncrease: QuotesForIncrease[]; +}; + +export async function generateQuotesToSync( + ctx: WhirlpoolContext, + whirlpoolPubkey: PublicKey, + positionBundleTargetState: PositionBundleStateItem[], + difference: PositionBundleStateDifference, + slippageTolerance: Percentage, +): Promise { + const { + bundledPositions, + shouldBeDecreased, + shouldBeClosed, + shouldBeOpened, + shouldBeIncreased, + } = difference; + + const whirlpool = (await ctx.fetcher.getPool( + whirlpoolPubkey, + IGNORE_CACHE, + )) as WhirlpoolData; + const tickSpacing = whirlpool.tickSpacing; + + const tokenExtensionCtx = await TokenExtensionUtil.buildTokenExtensionContext( + ctx.fetcher, + whirlpool, + IGNORE_CACHE, + ); + + // make TickArray cache for closing positions to calculate collectable fees and rewards + const tickArrayStartIndexes = new Set(); + for (const closingBundleIndex of shouldBeClosed) { + const closingPosition = bundledPositions[ + closingBundleIndex + ] as PositionData; + tickArrayStartIndexes.add( + TickUtil.getStartTickIndex(closingPosition.tickLowerIndex, tickSpacing), + ); + tickArrayStartIndexes.add( + TickUtil.getStartTickIndex(closingPosition.tickUpperIndex, tickSpacing), + ); + } + const tickArrayAddresses = Array.from(tickArrayStartIndexes).map( + (startIndex) => + PDAUtil.getTickArray(ctx.program.programId, whirlpoolPubkey, startIndex) + .publicKey, + ); + await ctx.fetcher.getTickArrays(tickArrayAddresses, IGNORE_CACHE); + + // decrease liquidity quotes + const quotesForDecrease = shouldBeDecreased.map((bundleIndex) => { + const position = bundledPositions[bundleIndex] as PositionData; + const targetState = positionBundleTargetState[ + bundleIndex + ] as PositionBundleOpenState; + const liquidityDelta = position.liquidity.sub(targetState.liquidity); + const decrease = decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: liquidityDelta, + sqrtPrice: whirlpool.sqrtPrice, + tickCurrentIndex: whirlpool.tickCurrentIndex, + tickLowerIndex: position.tickLowerIndex, + tickUpperIndex: position.tickUpperIndex, + tokenExtensionCtx, + slippageTolerance, + }); + + return { bundleIndex, decrease }; + }); + + // close position quotes + const quotesForClose = await Promise.all( + shouldBeClosed.map(async (bundleIndex) => { + const position = bundledPositions[bundleIndex] as PositionData; + + const decrease = position.liquidity.isZero() + ? undefined + : decreaseLiquidityQuoteByLiquidityWithParams({ + liquidity: position.liquidity, + sqrtPrice: whirlpool.sqrtPrice, + tickCurrentIndex: whirlpool.tickCurrentIndex, + tickLowerIndex: position.tickLowerIndex, + tickUpperIndex: position.tickUpperIndex, + tokenExtensionCtx, + slippageTolerance, + }); + + const lowerTickArrayPubkey = PDAUtil.getTickArrayFromTickIndex( + position.tickLowerIndex, + tickSpacing, + whirlpoolPubkey, + ctx.program.programId, + ).publicKey; + const upperTickArrayPubkey = PDAUtil.getTickArrayFromTickIndex( + position.tickUpperIndex, + tickSpacing, + whirlpoolPubkey, + ctx.program.programId, + ).publicKey; + + // async, but no RPC calls (already cached) + const [lowerTickArray, upperTickArray] = (await ctx.fetcher.getTickArrays( + [lowerTickArrayPubkey, upperTickArrayPubkey], + PREFER_CACHE, + )) as [TickArrayData, TickArrayData]; + const tickLower = TickArrayUtil.getTickFromArray( + lowerTickArray, + position.tickLowerIndex, + tickSpacing, + ); + const tickUpper = TickArrayUtil.getTickFromArray( + upperTickArray, + position.tickUpperIndex, + tickSpacing, + ); + + const collectFees = collectFeesQuote({ + position, + whirlpool, + tickLower, + tickUpper, + tokenExtensionCtx, + }); + const collectRewards = collectRewardsQuote({ + position, + whirlpool, + tickLower, + tickUpper, + tokenExtensionCtx, + }); + + return { bundleIndex, decrease, collectFees, collectRewards }; + }), + ); + + // open position quotes + const quotesForOpen = shouldBeOpened.map((bundleIndex) => { + const targetState = positionBundleTargetState[ + bundleIndex + ] as PositionBundleOpenState; + const increase = targetState.liquidity.isZero() + ? undefined + : increaseLiquidityQuoteByLiquidityWithParamsUsingTokenAmountSlippage({ + liquidity: targetState.liquidity, + sqrtPrice: whirlpool.sqrtPrice, + tickCurrentIndex: whirlpool.tickCurrentIndex, + tickLowerIndex: targetState.lowerTickIndex, + tickUpperIndex: targetState.upperTickIndex, + tokenExtensionCtx, + slippageTolerance, + }); + + return { bundleIndex, increase }; + }); + + // increase liquidity quotes + const quotesForIncrease = shouldBeIncreased.map((bundleIndex) => { + const position = bundledPositions[bundleIndex] as PositionData; + const targetState = positionBundleTargetState[ + bundleIndex + ] as PositionBundleOpenState; + const liquidityDelta = targetState.liquidity.sub(position.liquidity); + const increase = + increaseLiquidityQuoteByLiquidityWithParamsUsingTokenAmountSlippage({ + liquidity: liquidityDelta, + sqrtPrice: whirlpool.sqrtPrice, + tickCurrentIndex: whirlpool.tickCurrentIndex, + tickLowerIndex: position.tickLowerIndex, + tickUpperIndex: position.tickUpperIndex, + tokenExtensionCtx, + slippageTolerance, + }); + + return { bundleIndex, increase }; + }); + + return { + quotesForDecrease, + quotesForClose, + quotesForOpen, + quotesForIncrease, + }; +} + +function increaseLiquidityQuoteByLiquidityWithParamsUsingTokenAmountSlippage( + params: IncreaseLiquidityQuoteByLiquidityParam, +): IncreaseLiquidityQuote { + const increase = increaseLiquidityQuoteByLiquidityWithParams({ + ...params, + slippageTolerance: Percentage.fromFraction(0, 100), // not use price slippage + tokenExtensionCtx: NO_TOKEN_EXTENSION_CONTEXT, // no transfer fee calculation + }); + const tokenEstA = increase.tokenEstA; + const tokenEstB = increase.tokenEstB; + const tokenMaxA = adjustForSlippage( + tokenEstA, + params.slippageTolerance, + true, + ); + const tokenMaxB = adjustForSlippage( + tokenEstB, + params.slippageTolerance, + true, + ); + + const tokenEstAIncluded = + TokenExtensionUtil.calculateTransferFeeIncludedAmount( + tokenEstA, + params.tokenExtensionCtx.tokenMintWithProgramA, + params.tokenExtensionCtx.currentEpoch, + ); + const tokenEstBIncluded = + TokenExtensionUtil.calculateTransferFeeIncludedAmount( + tokenEstB, + params.tokenExtensionCtx.tokenMintWithProgramB, + params.tokenExtensionCtx.currentEpoch, + ); + const tokenMaxAIncluded = + TokenExtensionUtil.calculateTransferFeeIncludedAmount( + tokenMaxA, + params.tokenExtensionCtx.tokenMintWithProgramA, + params.tokenExtensionCtx.currentEpoch, + ); + const tokenMaxBIncluded = + TokenExtensionUtil.calculateTransferFeeIncludedAmount( + tokenMaxB, + params.tokenExtensionCtx.tokenMintWithProgramB, + params.tokenExtensionCtx.currentEpoch, + ); + + return { + liquidityAmount: increase.liquidityAmount, + tokenEstA: tokenEstAIncluded.amount, + tokenEstB: tokenEstBIncluded.amount, + tokenMaxA: tokenMaxAIncluded.amount, + tokenMaxB: tokenMaxBIncluded.amount, + transferFee: { + deductingFromTokenEstA: tokenEstAIncluded.fee, + deductingFromTokenEstB: tokenEstBIncluded.fee, + deductingFromTokenMaxA: tokenMaxAIncluded.fee, + deductingFromTokenMaxB: tokenMaxBIncluded.fee, + }, + }; +} + +export type BalanceDifference = { + tokenAWithdrawnEst: BN; + tokenBWithdrawnEst: BN; + tokenAWithdrawnMin: BN; + tokenBWithdrawnMin: BN; + tokenACollected: BN; + tokenBCollected: BN; + rewardsCollected: [BN | undefined, BN | undefined, BN | undefined]; + tokenADepositedEst: BN; + tokenBDepositedEst: BN; + tokenADepositedMax: BN; + tokenBDepositedMax: BN; + // withdrawn - deposited = negative means deposited more than withdrawn + tokenABalanceDeltaEst: BN; // no consideration of fees and rewards + tokenBBalanceDeltaEst: BN; // no consideration of fees and rewards +}; + +export function calculateBalanceDifference( + quotes: QuotesToSync, +): BalanceDifference { + const { + quotesForDecrease, + quotesForClose, + quotesForOpen, + quotesForIncrease, + } = quotes; + + let tokenAWithdrawnEst = new BN(0); + let tokenBWithdrawnEst = new BN(0); + let tokenAWithdrawnMin = new BN(0); + let tokenBWithdrawnMin = new BN(0); + let tokenACollected = new BN(0); + let tokenBCollected = new BN(0); + let rewardsCollected: [BN | undefined, BN | undefined, BN | undefined] = [ + undefined, + undefined, + undefined, + ]; + let tokenADepositedEst = new BN(0); + let tokenBDepositedEst = new BN(0); + let tokenADepositedMax = new BN(0); + let tokenBDepositedMax = new BN(0); + + for (const { decrease } of quotesForDecrease) { + tokenAWithdrawnEst = tokenAWithdrawnEst.add(decrease.tokenEstA); + tokenBWithdrawnEst = tokenBWithdrawnEst.add(decrease.tokenEstB); + tokenAWithdrawnMin = tokenAWithdrawnMin.add(decrease.tokenMinA); + tokenBWithdrawnMin = tokenBWithdrawnMin.add(decrease.tokenMinB); + } + + for (const { decrease, collectFees, collectRewards } of quotesForClose) { + if (decrease) { + tokenAWithdrawnEst = tokenAWithdrawnEst.add(decrease.tokenEstA); + tokenBWithdrawnEst = tokenBWithdrawnEst.add(decrease.tokenEstB); + tokenAWithdrawnMin = tokenAWithdrawnMin.add(decrease.tokenMinA); + tokenBWithdrawnMin = tokenBWithdrawnMin.add(decrease.tokenMinB); + } + tokenACollected = tokenACollected.add(collectFees.feeOwedA); + tokenBCollected = tokenBCollected.add(collectFees.feeOwedB); + for (let i = 0; i < rewardsCollected.length; i++) { + rewardsCollected[i] = collectRewards.rewardOwed[i]?.add( + rewardsCollected[i] ?? new BN(0), + ); + } + } + + for (const { increase } of quotesForOpen) { + if (increase) { + tokenADepositedEst = tokenADepositedEst.add(increase.tokenEstA); + tokenBDepositedEst = tokenBDepositedEst.add(increase.tokenEstB); + tokenADepositedMax = tokenADepositedMax.add(increase.tokenMaxA); + tokenBDepositedMax = tokenBDepositedMax.add(increase.tokenMaxB); + } + } + + for (const { increase } of quotesForIncrease) { + tokenADepositedEst = tokenADepositedEst.add(increase.tokenEstA); + tokenBDepositedEst = tokenBDepositedEst.add(increase.tokenEstB); + tokenADepositedMax = tokenADepositedMax.add(increase.tokenMaxA); + tokenBDepositedMax = tokenBDepositedMax.add(increase.tokenMaxB); + } + + const tokenABalanceDeltaEst = tokenAWithdrawnEst.sub(tokenADepositedEst); + const tokenBBalanceDeltaEst = tokenBWithdrawnEst.sub(tokenBDepositedEst); + + return { + tokenAWithdrawnEst, + tokenBWithdrawnEst, + tokenAWithdrawnMin, + tokenBWithdrawnMin, + tokenACollected, + tokenBCollected, + rewardsCollected, + tokenADepositedEst, + tokenBDepositedEst, + tokenADepositedMax, + tokenBDepositedMax, + tokenABalanceDeltaEst, + tokenBBalanceDeltaEst, + }; +} diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/state_difference.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/state_difference.ts new file mode 100644 index 000000000..05e32fb50 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/state_difference.ts @@ -0,0 +1,129 @@ +import type { PublicKey } from "@solana/web3.js"; +import { + IGNORE_CACHE, + PDAUtil, + POSITION_BUNDLE_SIZE, + PositionBundleUtil, +} from "@orca-so/whirlpools-sdk"; +import type { + PositionBundleData, + PositionData, + WhirlpoolContext, +} from "@orca-so/whirlpools-sdk"; +import type { PositionBundleStateItem } from "./csv"; + +export type PositionBundleStateDifference = { + positionBundle: PositionBundleData; + bundledPositions: (PositionData | undefined)[]; + noDifference: number[]; + shouldBeDecreased: number[]; + shouldBeClosed: number[]; + shouldBeOpened: number[]; + shouldBeIncreased: number[]; +}; + +export async function checkPositionBundleStateDifference( + ctx: WhirlpoolContext, + positionBundlePubkey: PublicKey, + whirlpoolPubkey: PublicKey, + positionBundleTargetState: PositionBundleStateItem[], +): Promise { + // fetch all bundled positions + const positionBundle = (await ctx.fetcher.getPositionBundle( + positionBundlePubkey, + IGNORE_CACHE, + )) as PositionBundleData; + const bundledPositions = await fetchBundledPositions(ctx, positionBundle); + + // ensure that all bundled positions belong to the provided whirlpool + if ( + bundledPositions.some( + (position) => position && !position.whirlpool.equals(whirlpoolPubkey), + ) + ) { + throw new Error( + `not all bundled positions belong to the whirlpool(${whirlpoolPubkey.toBase58()})`, + ); + } + + // check differences between current state and target state + const noDifference: number[] = []; + const shouldBeDecreased: number[] = []; + const shouldBeClosed: number[] = []; + const shouldBeOpened: number[] = []; + const shouldBeIncreased: number[] = []; + for (let bundleIndex = 0; bundleIndex < POSITION_BUNDLE_SIZE; bundleIndex++) { + const targetState = positionBundleTargetState[bundleIndex]; + const currentPosition = bundledPositions[bundleIndex]; + + if (targetState.state === "closed") { + if (currentPosition) { + shouldBeClosed.push(bundleIndex); + } else { + // nop + noDifference.push(bundleIndex); + } + } else { + if (!currentPosition) { + shouldBeOpened.push(bundleIndex); + } else { + if ( + currentPosition.tickLowerIndex !== targetState.lowerTickIndex || + currentPosition.tickUpperIndex !== targetState.upperTickIndex + ) { + // close and reopen + shouldBeClosed.push(bundleIndex); + shouldBeOpened.push(bundleIndex); + } else if (currentPosition.liquidity.lt(targetState.liquidity)) { + shouldBeIncreased.push(bundleIndex); + } else if (currentPosition.liquidity.gt(targetState.liquidity)) { + shouldBeDecreased.push(bundleIndex); + } else { + // nop + noDifference.push(bundleIndex); + } + } + } + } + + return { + positionBundle, + bundledPositions, + noDifference, + shouldBeDecreased, + shouldBeClosed, + shouldBeOpened, + shouldBeIncreased, + }; +} + +async function fetchBundledPositions( + ctx: WhirlpoolContext, + positionBundle: PositionBundleData, +): Promise<(PositionData | undefined)[]> { + const openBundleIndexes = + PositionBundleUtil.getOccupiedBundleIndexes(positionBundle); + const bundledPositions: (PositionData | undefined)[] = new Array( + POSITION_BUNDLE_SIZE, + ).fill(undefined); + + const addresses = openBundleIndexes.map( + (index) => + PDAUtil.getBundledPosition( + ctx.program.programId, + positionBundle.positionBundleMint, + index, + ).publicKey, + ); + const positions = await ctx.fetcher.getPositions(addresses, IGNORE_CACHE); + + addresses.forEach((address, i) => { + const position = positions.get(address.toBase58()); + if (!position) { + throw new Error("bundled position not found"); + } + bundledPositions[openBundleIndexes[i]] = position; + }); + + return bundledPositions; +} diff --git a/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/transaction.ts b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/transaction.ts new file mode 100644 index 000000000..3d912e922 --- /dev/null +++ b/legacy-sdk/cli/src/commands/bundle/sync_position_bundle_state_impl/transaction.ts @@ -0,0 +1,284 @@ +import type { AddressLookupTableAccount, PublicKey } from "@solana/web3.js"; +import { + PDAUtil, + PoolUtil, + PREFER_CACHE, + toTx, + WhirlpoolIx, +} from "@orca-so/whirlpools-sdk"; +import type { WhirlpoolContext, WhirlpoolData } from "@orca-so/whirlpools-sdk"; +import { TransactionBuilder } from "@orca-so/common-sdk"; +import type { MintWithTokenProgram } from "@orca-so/common-sdk"; +import { sendTransaction } from "../../../utils/transaction_sender"; +import { + getAssociatedTokenAddressSync, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import { mergeTransactionBuilders } from "../../../utils/merge_transaction"; +import type { PositionBundleOpenState, PositionBundleStateItem } from "./csv"; +import type { PositionBundleStateDifference } from "./state_difference"; +import type { QuotesToSync } from "./quote"; + +export async function sendTransactions( + ctx: WhirlpoolContext, + alts: AddressLookupTableAccount[], + transactions: TransactionBuilder[], + defaultPriorityFeeInLamports: number, +) { + for (const tx of transactions) { + const landed = await sendTransaction( + tx, + defaultPriorityFeeInLamports, + alts, + ); + if (!landed) { + throw new Error("transaction failed"); + } + } +} + +export async function buildTransactions( + ctx: WhirlpoolContext, + alts: AddressLookupTableAccount[], + positionBundlePubkey: PublicKey, + whirlpoolPubkey: PublicKey, + difference: PositionBundleStateDifference, + positionBundleTargetState: PositionBundleStateItem[], + quotes: QuotesToSync, +): Promise<{ + withdrawTransactions: TransactionBuilder[]; + depositTransactions: TransactionBuilder[]; +}> { + const { + quotesForDecrease, + quotesForClose, + quotesForOpen, + quotesForIncrease, + } = quotes; + + const whirlpool = (await ctx.fetcher.getPool( + whirlpoolPubkey, + PREFER_CACHE, + )) as WhirlpoolData; + const mintA = (await ctx.fetcher.getMintInfo( + whirlpool.tokenMintA, + PREFER_CACHE, + )) as MintWithTokenProgram; + const mintB = (await ctx.fetcher.getMintInfo( + whirlpool.tokenMintB, + PREFER_CACHE, + )) as MintWithTokenProgram; + + const ataA = getAssociatedTokenAddressSync( + whirlpool.tokenMintA, + ctx.wallet.publicKey, + true, + mintA.tokenProgram, + ); + const ataB = getAssociatedTokenAddressSync( + whirlpool.tokenMintB, + ctx.wallet.publicKey, + true, + mintB.tokenProgram, + ); + const ataPositionBundle = getAssociatedTokenAddressSync( + difference.positionBundle.positionBundleMint, + ctx.wallet.publicKey, + true, + TOKEN_PROGRAM_ID, + ); + + const baseParams = { + positionAuthority: ctx.wallet.publicKey, + positionTokenAccount: ataPositionBundle, + tokenMintA: whirlpool.tokenMintA, + tokenMintB: whirlpool.tokenMintB, + tokenOwnerAccountA: ataA, + tokenOwnerAccountB: ataB, + tokenProgramA: mintA.tokenProgram, + tokenProgramB: mintB.tokenProgram, + tokenVaultA: whirlpool.tokenVaultA, + tokenVaultB: whirlpool.tokenVaultB, + whirlpool: whirlpoolPubkey, + }; + + const rewardParams = await Promise.all( + whirlpool.rewardInfos + .filter((rewardInfo) => PoolUtil.isRewardInitialized(rewardInfo)) + .map(async (rewardInfo) => { + const mint = (await ctx.fetcher.getMintInfo( + rewardInfo.mint, + )) as MintWithTokenProgram; + const ata = getAssociatedTokenAddressSync( + rewardInfo.mint, + ctx.wallet.publicKey, + true, + mint.tokenProgram, + ); + return { + mint, + rewardInfo, + ata, + }; + }), + ); + + const getBundledPositionPDA = (bundleIndex: number) => { + return PDAUtil.getBundledPosition( + ctx.program.programId, + difference.positionBundle.positionBundleMint, + bundleIndex, + ); + }; + const getBundledPositionPubkey = (bundleIndex: number) => { + return getBundledPositionPDA(bundleIndex).publicKey; + }; + const getTickArrayPubkey = (tickIndex: number) => { + return PDAUtil.getTickArrayFromTickIndex( + tickIndex, + whirlpool.tickSpacing, + whirlpoolPubkey, + ctx.program.programId, + ).publicKey; + }; + + const withdrawTransactions: TransactionBuilder[] = []; + for (const { bundleIndex, decrease } of quotesForDecrease) { + withdrawTransactions.push( + toTx( + ctx, + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...baseParams, + ...decrease, + position: getBundledPositionPubkey(bundleIndex), + tickArrayLower: getTickArrayPubkey( + difference.bundledPositions[bundleIndex]!.tickLowerIndex, + ), + tickArrayUpper: getTickArrayPubkey( + difference.bundledPositions[bundleIndex]!.tickUpperIndex, + ), + }), + ), + ); + } + for (const { bundleIndex, decrease } of quotesForClose) { + const tx = new TransactionBuilder(ctx.connection, ctx.wallet); + if (decrease) { + tx.addInstruction( + WhirlpoolIx.decreaseLiquidityV2Ix(ctx.program, { + ...baseParams, + ...decrease, + position: getBundledPositionPubkey(bundleIndex), + tickArrayLower: getTickArrayPubkey( + difference.bundledPositions[bundleIndex]!.tickLowerIndex, + ), + tickArrayUpper: getTickArrayPubkey( + difference.bundledPositions[bundleIndex]!.tickUpperIndex, + ), + }), + ); + } + tx.addInstruction( + WhirlpoolIx.collectFeesV2Ix(ctx.program, { + ...baseParams, + position: getBundledPositionPubkey(bundleIndex), + }), + ); + + rewardParams.forEach(({ mint, rewardInfo, ata }, rewardIndex) => { + tx.addInstruction( + WhirlpoolIx.collectRewardV2Ix(ctx.program, { + ...baseParams, + position: getBundledPositionPubkey(bundleIndex), + rewardIndex, + rewardMint: mint.address, + rewardOwnerAccount: ata, + rewardTokenProgram: mint.tokenProgram, + rewardVault: rewardInfo.vault, + }), + ); + }); + + tx.addInstruction( + WhirlpoolIx.closeBundledPositionIx(ctx.program, { + bundleIndex, + bundledPosition: getBundledPositionPubkey(bundleIndex), + positionBundle: positionBundlePubkey, + positionBundleAuthority: ctx.wallet.publicKey, + positionBundleTokenAccount: ataPositionBundle, + receiver: ctx.wallet.publicKey, + }), + ); + + withdrawTransactions.push(tx); + } + + const depositTransactions: TransactionBuilder[] = []; + for (const { bundleIndex, increase } of quotesForOpen) { + const tx = new TransactionBuilder(ctx.connection, ctx.wallet); + const targetState = positionBundleTargetState[ + bundleIndex + ] as PositionBundleOpenState; + + tx.addInstruction( + WhirlpoolIx.openBundledPositionIx(ctx.program, { + positionBundle: positionBundlePubkey, + bundleIndex, + bundledPositionPda: getBundledPositionPDA(bundleIndex), + positionBundleAuthority: ctx.wallet.publicKey, + funder: ctx.wallet.publicKey, + positionBundleTokenAccount: ataPositionBundle, + tickLowerIndex: targetState.lowerTickIndex, + tickUpperIndex: targetState.upperTickIndex, + whirlpool: whirlpoolPubkey, + }), + ); + + if (increase) { + tx.addInstruction( + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + ...baseParams, + ...increase, + position: getBundledPositionPubkey(bundleIndex), + tickArrayLower: getTickArrayPubkey(targetState.lowerTickIndex), + tickArrayUpper: getTickArrayPubkey(targetState.upperTickIndex), + }), + ); + } + + depositTransactions.push(tx); + } + for (const { bundleIndex, increase } of quotesForIncrease) { + const targetState = positionBundleTargetState[ + bundleIndex + ] as PositionBundleOpenState; + depositTransactions.push( + toTx( + ctx, + WhirlpoolIx.increaseLiquidityV2Ix(ctx.program, { + ...baseParams, + ...increase, + position: getBundledPositionPubkey(bundleIndex), + tickArrayLower: getTickArrayPubkey(targetState.lowerTickIndex), + tickArrayUpper: getTickArrayPubkey(targetState.upperTickIndex), + }), + ), + ); + } + + const mergedWithdrawTransactions = mergeTransactionBuilders( + ctx, + withdrawTransactions, + alts, + ); + const mergedDepositTransactions = mergeTransactionBuilders( + ctx, + depositTransactions, + alts, + ); + + return { + withdrawTransactions: mergedWithdrawTransactions, + depositTransactions: mergedDepositTransactions, + }; +} diff --git a/legacy-sdk/cli/src/commands/initialize_position_bundle.ts b/legacy-sdk/cli/src/commands/initialize_position_bundle.ts new file mode 100644 index 000000000..f6fe5bfb3 --- /dev/null +++ b/legacy-sdk/cli/src/commands/initialize_position_bundle.ts @@ -0,0 +1,57 @@ +import { Keypair } from "@solana/web3.js"; +import { PDAUtil, WhirlpoolIx } from "@orca-so/whirlpools-sdk"; +import { TransactionBuilder } from "@orca-so/common-sdk"; +import { sendTransaction } from "../utils/transaction_sender"; +import { ctx } from "../utils/provider"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; + +console.info("initialize PositionBundle..."); + +const positionBundleMintKeypair = Keypair.generate(); + +const pda = PDAUtil.getPositionBundle( + ctx.program.programId, + positionBundleMintKeypair.publicKey, +); + +// PositionBundle is Token program based (not Token-2022 program based) +const positionBundleTokenAccount = getAssociatedTokenAddressSync( + positionBundleMintKeypair.publicKey, + ctx.wallet.publicKey, +); + +const builder = new TransactionBuilder(ctx.connection, ctx.wallet); +builder.addInstruction( + WhirlpoolIx.initializePositionBundleIx(ctx.program, { + owner: ctx.wallet.publicKey, + positionBundleMintKeypair, + positionBundlePda: pda, + positionBundleTokenAccount, + funder: ctx.wallet.publicKey, + }), +); + +const landed = await sendTransaction(builder); +if (landed) { + console.info("positionBundle address:", pda.publicKey.toBase58()); +} + +/* + +SAMPLE EXECUTION LOG + +connection endpoint http://localhost:8899 +wallet r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6 +initialize PositionBundle... +estimatedComputeUnits: 170954 +✔ priorityFeeInSOL … 0 +Priority fee: 0 SOL +process transaction... +transaction is still valid, 151 blocks left (at most) +sending... +confirming... +successfully landed +signature 55Qs1vGXeQ8EXokLLdPbD3iYEjPPM2ryvStU3cQm4Qw4sVhboRxzFZwdjPqZR4rGcRQygJut9puYWS1V8i94zsUH +positionBundle address: 3XmaBcpvHdNTv6u35M13w55SpJKVhxSahTkzMiVRVsqC + +*/ diff --git a/legacy-sdk/cli/src/commands/initialize_tick_array.ts b/legacy-sdk/cli/src/commands/initialize_tick_array.ts index 3512ca56a..e7d47e98d 100644 --- a/legacy-sdk/cli/src/commands/initialize_tick_array.ts +++ b/legacy-sdk/cli/src/commands/initialize_tick_array.ts @@ -12,7 +12,7 @@ import { TransactionBuilder } from "@orca-so/common-sdk"; import type Decimal from "decimal.js"; import { sendTransaction } from "../utils/transaction_sender"; import { ctx } from "../utils/provider"; -import { promptText } from "../utils/prompt"; +import { promptConfirm, promptText } from "../utils/prompt"; console.info("initialize TickArray..."); @@ -125,47 +125,54 @@ tickArrayInfos.push({ isFullRange: true, }); -const checkInitialized = await ctx.fetcher.getTickArrays( - tickArrayInfos.map((info) => info.pda.publicKey), - IGNORE_CACHE, -); -checkInitialized.forEach((ta, i) => (tickArrayInfos[i].isInitialized = !!ta)); - -console.info("neighring tickarrays & fullrange tickarrays..."); -tickArrayInfos.forEach((ta) => - console.info( - ta.isCurrent ? ">>" : " ", - ta.pda.publicKey.toBase58().padEnd(45, " "), - ta.isInitialized ? " initialized" : "NOT INITIALIZED", - "start", - ta.startTickIndex.toString().padStart(10, " "), - "covered range", - ta.startPrice.toSignificantDigits(6), - "-", - ta.endPrice.toSignificantDigits(6), - ta.isFullRange ? "(FULL)" : "", - ), -); - -const tickArrayPubkeyStr = await promptText("tickArrayPubkey"); -const tickArrayPubkey = new PublicKey(tickArrayPubkeyStr); -const which = tickArrayInfos.filter((ta) => - ta.pda.publicKey.equals(tickArrayPubkey), -)[0]; - -const builder = new TransactionBuilder(ctx.connection, ctx.wallet); -builder.addInstruction( - WhirlpoolIx.initTickArrayIx(ctx.program, { - funder: ctx.wallet.publicKey, - whirlpool: whirlpoolPubkey, - startTick: which.startTickIndex, - tickArrayPda: which.pda, - }), -); - -const landed = await sendTransaction(builder); -if (landed) { - console.info("initialized tickArray address:", tickArrayPubkey.toBase58()); +while (true) { + const checkInitialized = await ctx.fetcher.getTickArrays( + tickArrayInfos.map((info) => info.pda.publicKey), + IGNORE_CACHE, + ); + checkInitialized.forEach((ta, i) => (tickArrayInfos[i].isInitialized = !!ta)); + + console.info("neighring tickarrays & fullrange tickarrays..."); + tickArrayInfos.forEach((ta) => + console.info( + ta.isCurrent ? ">>" : " ", + ta.pda.publicKey.toBase58().padEnd(45, " "), + ta.isInitialized ? " initialized" : "NOT INITIALIZED", + "start", + ta.startTickIndex.toString().padStart(10, " "), + "covered range", + ta.startPrice.toSignificantDigits(6), + "-", + ta.endPrice.toSignificantDigits(6), + ta.isFullRange ? "(FULL)" : "", + ), + ); + + const tickArrayPubkeyStr = await promptText("tickArrayPubkey"); + const tickArrayPubkey = new PublicKey(tickArrayPubkeyStr); + const which = tickArrayInfos.filter((ta) => + ta.pda.publicKey.equals(tickArrayPubkey), + )[0]; + + const builder = new TransactionBuilder(ctx.connection, ctx.wallet); + builder.addInstruction( + WhirlpoolIx.initTickArrayIx(ctx.program, { + funder: ctx.wallet.publicKey, + whirlpool: whirlpoolPubkey, + startTick: which.startTickIndex, + tickArrayPda: which.pda, + }), + ); + + const landed = await sendTransaction(builder); + if (landed) { + console.info("initialized tickArray address:", tickArrayPubkey.toBase58()); + } + + const more = await promptConfirm("initialize more tickArray ?"); + if (!more) { + break; + } } /* diff --git a/legacy-sdk/cli/src/commands/initialize_whirlpool.ts b/legacy-sdk/cli/src/commands/initialize_whirlpool.ts index 6d63b9b66..131874ee8 100644 --- a/legacy-sdk/cli/src/commands/initialize_whirlpool.ts +++ b/legacy-sdk/cli/src/commands/initialize_whirlpool.ts @@ -102,6 +102,8 @@ console.info( "setting...", "\n\twhirlpoolsConfig", whirlpoolsConfigPubkey.toBase58(), + "\n\twhirlpool", + pda.publicKey.toBase58(), "\n\ttokenMintA", tokenMintAPubkey.toBase58(), `(${tokenProgramA})`, diff --git a/legacy-sdk/cli/src/commands/push_price.ts b/legacy-sdk/cli/src/commands/poolops/push_price.ts similarity index 98% rename from legacy-sdk/cli/src/commands/push_price.ts rename to legacy-sdk/cli/src/commands/poolops/push_price.ts index d89d3108a..76060a473 100644 --- a/legacy-sdk/cli/src/commands/push_price.ts +++ b/legacy-sdk/cli/src/commands/poolops/push_price.ts @@ -22,10 +22,10 @@ import { } from "@orca-so/common-sdk"; import BN from "bn.js"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { sendTransaction } from "../utils/transaction_sender"; +import { sendTransaction } from "../../utils/transaction_sender"; import Decimal from "decimal.js"; -import { ctx } from "../utils/provider"; -import { promptConfirm, promptText } from "../utils/prompt"; +import { ctx } from "../../utils/provider"; +import { promptConfirm, promptText } from "../../utils/prompt"; const SIGNIFICANT_DIGITS = 9; diff --git a/legacy-sdk/cli/src/commands/todo/initializeTickArrayRange b/legacy-sdk/cli/src/commands/todo/initializeTickArrayRange deleted file mode 100644 index 5996a0930..000000000 --- a/legacy-sdk/cli/src/commands/todo/initializeTickArrayRange +++ /dev/null @@ -1,379 +0,0 @@ -import type { PDA} from "@orca-so/common-sdk"; -import { TransactionBuilder } from "@orca-so/common-sdk"; -import type { - AccountFetcher, - WhirlpoolData} from "@orca-so/whirlpools-sdk"; -import { - MAX_TICK_INDEX, - MIN_TICK_INDEX, - ORCA_WHIRLPOOL_PROGRAM_ID, - PriceMath, - TickArrayUtil, - TickUtil, - TICK_ARRAY_SIZE, - WhirlpoolContext, - WhirlpoolIx, -} from "@orca-so/whirlpools-sdk"; -import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet"; -import type { MintInfo } from "@solana/spl-token"; -import type { Connection, PublicKey } from "@solana/web3.js"; -import Decimal from "decimal.js"; -import prompts from "prompts"; -import { getConnectionFromEnv } from "../connection"; -import { getOwnerKeypair } from "../loadAccounts"; -import { promptForNetwork, promptForWhirlpool } from "./whirlpool-script-utils"; -const SOL_9_DEC = new Decimal(10).pow(9); -const TICK_ARRAYS_TO_FETCH_PER_TX = 5; - -async function run() { - const owner = await getOwnerKeypair(); - const network = await promptForNetwork(); - const connection = await getConnectionFromEnv(network); - console.log(`Connected to RPC - ${connection.rpcEndpoint}`); - - const ctx = WhirlpoolContext.from( - connection, - new NodeWallet(owner), - ORCA_WHIRLPOOL_PROGRAM_ID, - ); - const fetcher = ctx.fetcher; - - // Derive Whirlpool address - const { key: whirlpoolAddr, tokenKeyInverted } = await promptForWhirlpool(); - if (tokenKeyInverted) { - console.log(`NOTE: tokenMintA & B order had been inverted.`); - } - console.log(`Fetching Whirlpool at address - ${whirlpoolAddr.toBase58()}`); - console.log(`...`); - - const acct = await fetchWhirlpoolAccounts(fetcher, whirlpoolAddr); - - const { lowerTick, upperTick } = await promptForTickRange(acct); - - console.log( - `Script will initialize arrays range [${lowerTick} -> ${upperTick}] (start-indices). Current tick is at ${ - acct.data.tickCurrentIndex - } / price - $${PriceMath.sqrtPriceX64ToPrice( - acct.data.sqrtPrice, - acct.tokenA.decimals, - acct.tokenB.decimals, - ).toDecimalPlaces(5)}`, - ); - console.log(`Fetching tick-array data...`); - const tickArrayInfo = await getTickArrays( - fetcher, - connection, - whirlpoolAddr, - acct, - lowerTick, - upperTick, - ); - - console.log(``); - console.log( - `There are ${tickArrayInfo.numTickArraysInRange} arrays in range, with ${tickArrayInfo.numTickArraysToInit} arrays that needs to be initialized.`, - ); - - if (tickArrayInfo.numTickArraysToInit > 0) { - await checkWalletBalance( - connection, - owner.publicKey, - tickArrayInfo.costToInit, - ); - await executeInitialization(ctx, whirlpoolAddr, tickArrayInfo.pdasToInit); - } - - console.log("Complete."); -} - -async function executeInitialization( - ctx: WhirlpoolContext, - addr: PublicKey, - arraysToInit: { startIndex: number; pda: PDA }[], -) { - const response = await prompts([ - { - type: "select", - name: "execute", - message: "Execute?", - choices: [ - { title: "yes", value: "yes" }, - { title: "no", value: "no" }, - ], - }, - ]); - - if (response.execute === "no") { - return; - } - - let start = 0; - let end = TICK_ARRAYS_TO_FETCH_PER_TX; - - do { - const chunk = arraysToInit.slice(start, end); - - const startIndicies = chunk.map((val) => val.startIndex); - console.log( - `Executing initializations for array w/ start indices [${startIndicies}]...`, - ); - const txBuilder = new TransactionBuilder(ctx.connection, ctx.wallet); - for (const tickArray of chunk) { - txBuilder.addInstruction( - WhirlpoolIx.initTickArrayIx(ctx.program, { - startTick: tickArray.startIndex, - tickArrayPda: tickArray.pda, - whirlpool: addr, - funder: ctx.wallet.publicKey, - }), - ); - } - - const txId = await txBuilder.buildAndExecute(); - console.log(`Tx executed at ${txId}`); - - start = end; - end = end + TICK_ARRAYS_TO_FETCH_PER_TX; - } while (start < arraysToInit.length); -} - -async function checkWalletBalance( - connection: Connection, - ownerKey: PublicKey, - costToInit: Decimal, -) { - const walletBalance = new Decimal(await connection.getBalance(ownerKey)).div( - SOL_9_DEC, - ); - console.log( - `Wallet balance (${ownerKey.toBase58()}) - ${walletBalance} SOL. Est. cost - ${costToInit} SOL`, - ); - if (walletBalance.lessThan(costToInit)) { - throw new Error("Wallet has insufficent SOL to complete this operation."); - } -} - -async function getTickArrays( - fetcher: AccountFetcher, - connection: Connection, - whirlpool: PublicKey, - acct: WhirlpoolAccounts, - lowerTick: number, - upperTick: number, -) { - const lowerStartTick = TickUtil.getStartTickIndex( - lowerTick, - acct.data.tickSpacing, - ); - const upperStartTick = TickUtil.getStartTickIndex( - upperTick, - acct.data.tickSpacing, - ); - const numTickArraysInRange = - Math.ceil( - (upperStartTick - lowerStartTick) / - acct.data.tickSpacing / - TICK_ARRAY_SIZE, - ) + 1; - const arrayStartIndicies = [...Array(numTickArraysInRange).keys()].map( - (index) => lowerStartTick + index * acct.data.tickSpacing * TICK_ARRAY_SIZE, - ); - - const initArrayKeys = await TickArrayUtil.getUninitializedArraysPDAs( - arrayStartIndicies, - ORCA_WHIRLPOOL_PROGRAM_ID, - whirlpool, - acct.data.tickSpacing, - fetcher, - true, - ); - - // TickArray = Tick.LEN(113) * Array_SIZE (88) + 36 + 8 = 9988 - const rentExemptPerAcct = - await connection.getMinimumBalanceForRentExemption(9988); - - const costToInit = new Decimal(initArrayKeys.length * rentExemptPerAcct).div( - SOL_9_DEC, - ); - - return { - numTickArraysInRange, - numTickArraysToInit: initArrayKeys.length, - pdasToInit: initArrayKeys, - costToInit, - }; -} - -type WhirlpoolAccounts = { - tokenMintA: PublicKey; - tokenMintB: PublicKey; - tokenA: MintInfo; - tokenB: MintInfo; - data: WhirlpoolData; -}; -async function fetchWhirlpoolAccounts( - fetcher: AccountFetcher, - whirlpoolAddr: PublicKey, -): Promise { - const pool = await fetcher.getPool(whirlpoolAddr, true); - if (!pool) { - throw new Error( - `Unable to fetch Whirlpool at addr - ${whirlpoolAddr.toBase58()}`, - ); - } - const { tokenMintA, tokenMintB } = pool; - const [tokenA, tokenB] = await fetcher.listMintInfos( - [tokenMintA, tokenMintB], - true, - ); - - if (!tokenA) { - throw new Error(`Unable to fetch token - ${tokenMintA.toBase58()}`); - } - - if (!tokenB) { - throw new Error(`Unable to fetch token - ${tokenMintB.toBase58()}`); - } - - return { - tokenMintA, - tokenMintB, - tokenA, - tokenB, - data: pool, - }; -} - -type PriceRangeResponse = { - lowerTick: number; - upperTick: number; -}; -async function promptForTickRange( - acct: WhirlpoolAccounts, -): Promise { - const provideTypeResponse = await prompts([ - { - type: "select", - name: "provideType", - message: "How would you like to provide the price range?", - choices: [ - { title: "Full Range", value: "fullRange" }, - { title: "By Price", value: "price" }, - { title: "By tick", value: "tick" }, - { title: "Current Price", value: "currentPrice" }, - ], - }, - ]); - - let lowerTick = 0, - upperTick = 0; - switch (provideTypeResponse.provideType) { - case "fullRange": { - lowerTick = MIN_TICK_INDEX; - upperTick = MAX_TICK_INDEX; - break; - } - case "price": { - const priceResponse = await prompts([ - { - type: "number", - name: "lowerPrice", - message: `Lower Price for ${acct.tokenMintB.toBase58()}/${acct.tokenMintB.toBase58()}`, - }, - { - type: "number", - name: "upperPrice", - message: `Upper Price for ${acct.tokenMintB.toBase58()}/${acct.tokenMintB.toBase58()}`, - }, - ]); - lowerTick = PriceMath.priceToTickIndex( - new Decimal(priceResponse.lowerPrice), - acct.tokenA.decimals, - acct.tokenB.decimals, - ); - upperTick = PriceMath.priceToTickIndex( - new Decimal(priceResponse.upperPrice), - acct.tokenA.decimals, - acct.tokenB.decimals, - ); - break; - } - case "tick": { - const tickResponse = await prompts([ - { - type: "text", - name: "lowerTick", - message: `Lower Tick for ${acct.tokenMintB.toBase58()}/${acct.tokenMintB.toBase58()}`, - }, - { - type: "text", - name: "upperTick", - message: `Upper Tick for ${acct.tokenMintB.toBase58()}/${acct.tokenMintB.toBase58()}`, - }, - ]); - - lowerTick = new Decimal(tickResponse.lowerTick) - .toDecimalPlaces(0) - .toNumber(); - upperTick = new Decimal(tickResponse.upperTick) - .toDecimalPlaces(0) - .toNumber(); - break; - } - case "currentPrice": { - const currPriceResponse = await prompts([ - { - type: "number", - name: "expandBy", - message: `Current price is ${PriceMath.sqrtPriceX64ToPrice( - acct.data.sqrtPrice, - acct.tokenA.decimals, - acct.tokenB.decimals, - ).toDecimalPlaces(9)} / tick - ${ - acct.data.tickCurrentIndex - }. How many tick arrays on each direction would you like to initialize?`, - }, - ]); - const currTick = TickUtil.getInitializableTickIndex( - acct.data.tickCurrentIndex, - acct.data.tickSpacing, - ); - const expandByTick = - currPriceResponse.expandBy * acct.data.tickSpacing * TICK_ARRAY_SIZE; - lowerTick = currTick - expandByTick; - upperTick = currTick + expandByTick; - break; - } - } - - if (lowerTick < MIN_TICK_INDEX || lowerTick > MAX_TICK_INDEX) { - throw new Error( - `Lower tick - ${lowerTick} is lower than MIN allowed [(${MIN_TICK_INDEX}, ${MAX_TICK_INDEX}]`, - ); - } - - if (upperTick < MIN_TICK_INDEX || upperTick > MAX_TICK_INDEX) { - throw new Error( - `Upper tick - ${lowerTick} is not within bounds [${MIN_TICK_INDEX}, ${MAX_TICK_INDEX}]`, - ); - } - - if (lowerTick >= upperTick) { - throw new Error( - `Upper tick ${upperTick} must be higher than lower tick - ${lowerTick}`, - ); - } - - return { - lowerTick: TickUtil.getInitializableTickIndex( - lowerTick, - acct.data.tickSpacing, - ), - upperTick: TickUtil.getInitializableTickIndex( - upperTick, - acct.data.tickSpacing, - ), - }; -} - -run(); diff --git a/legacy-sdk/cli/src/commands/todo/initializeWhirlpoolReward b/legacy-sdk/cli/src/commands/todo/initializeWhirlpoolReward deleted file mode 100644 index 023416659..000000000 --- a/legacy-sdk/cli/src/commands/todo/initializeWhirlpoolReward +++ /dev/null @@ -1,38 +0,0 @@ -import { Provider } from "@project-serum/anchor"; -import { OrcaNetwork, OrcaWhirlpoolClient } from "@orca-so/whirlpool-sdk"; - -const SOLANA_NETWORK_URL = "https://api.devnet.solana.com"; - -async function run() { - // @ts-expect-error this script doesn't work with latest anchor version - const provider = Provider.local(SOLANA_NETWORK_URL); - - const rewardAuthority = "81dVYq6RgX6Jt1TEDWpLkYUMWesNq3GMSYLKaKsopUqi"; - const poolAddress = "75dykYVKVj15kHEYiK4p9XEy8XpkrnfWMR8q3pbiC9Uo"; - const rewardMint = "orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L"; - - const client = new OrcaWhirlpoolClient({ - network: OrcaNetwork.DEVNET, - }); - - const { tx, rewardVault } = client.admin.getInitRewardTx({ - provider, - rewardAuthority, - poolAddress, - rewardMint, - rewardIndex: 0, - }); - - const txId = await tx.buildAndExecute(); - - console.log("txId", txId); - console.log("rewardVault", rewardVault.toBase58()); -} - -run() - .then(() => { - console.log("Success"); - }) - .catch((e) => { - console.error(e); - }); diff --git a/legacy-sdk/cli/src/index.ts b/legacy-sdk/cli/src/index.ts index 9258e83d4..0b546becb 100644 --- a/legacy-sdk/cli/src/index.ts +++ b/legacy-sdk/cli/src/index.ts @@ -10,6 +10,17 @@ const commands = readdirSync("./src/commands") value: () => import(`./commands/${file}.ts`), })); +for (const aux of ["poolops", "alt", "bundle"]) { + const auxCommands = readdirSync(`./src/commands/${aux}`) + .filter((file) => file.endsWith(".ts")) + .map((file) => file.replace(".ts", "")) + .map((file) => ({ + title: file, + value: () => import(`./commands/${aux}/${file}.ts`), + })); + commands.push(...auxCommands); +} + const arg = toSnakeCase(process.argv[2]); const maybeCommand = commands.find((c) => c.title === arg); diff --git a/legacy-sdk/cli/src/utils/merge_transaction.ts b/legacy-sdk/cli/src/utils/merge_transaction.ts new file mode 100644 index 000000000..3e314cc84 --- /dev/null +++ b/legacy-sdk/cli/src/utils/merge_transaction.ts @@ -0,0 +1,79 @@ +import { ComputeBudgetProgram } from "@solana/web3.js"; +import type { AddressLookupTableAccount } from "@solana/web3.js"; +import type { WhirlpoolContext } from "@orca-so/whirlpools-sdk"; +import { MEASUREMENT_BLOCKHASH, TransactionBuilder } from "@orca-so/common-sdk"; + +export function mergeTransactionBuilders( + ctx: WhirlpoolContext, + txs: TransactionBuilder[], + alts: AddressLookupTableAccount[], +): TransactionBuilder[] { + const merged: TransactionBuilder[] = []; + let tx: TransactionBuilder | undefined = undefined; + let cursor = 0; + while (cursor < txs.length) { + if (!tx) { + tx = new TransactionBuilder(ctx.connection, ctx.wallet); + // reserve space for ComputeBudgetProgram + tx.addInstruction({ + instructions: [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 0 }), // dummy ix + ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 0 }), // dummy ix + ], + cleanupInstructions: [], + signers: [], + }); + } + + const mergeable = checkMergedTransactionSizeIsValid( + ctx, + [tx, txs[cursor]], + alts, + ); + if (mergeable) { + tx.addInstruction(txs[cursor].compressIx(true)); + cursor++; + } else { + merged.push(tx); + tx = undefined; + } + } + + if (tx) { + merged.push(tx); + } + + // remove dummy ComputeBudgetProgram ixs + return merged.map((tx) => { + const newTx = new TransactionBuilder(ctx.connection, ctx.wallet); + const ix = tx.compressIx(true); + ix.instructions = ix.instructions.slice(2); // remove dummy ComputeBudgetProgram ixs + newTx.addInstruction(ix); + return newTx; + }); +} + +function checkMergedTransactionSizeIsValid( + ctx: WhirlpoolContext, + builders: TransactionBuilder[], + alts: AddressLookupTableAccount[], +): boolean { + const merged = new TransactionBuilder( + ctx.connection, + ctx.wallet, + ctx.txBuilderOpts, + ); + builders.forEach((builder) => + merged.addInstruction(builder.compressIx(true)), + ); + try { + merged.txnSize({ + latestBlockhash: MEASUREMENT_BLOCKHASH, + maxSupportedTransactionVersion: 0, + lookupTableAccounts: alts, + }); + return true; + } catch { + return false; + } +} diff --git a/legacy-sdk/cli/src/utils/transaction_sender.ts b/legacy-sdk/cli/src/utils/transaction_sender.ts index 03b6b8cff..b1ce983f9 100644 --- a/legacy-sdk/cli/src/utils/transaction_sender.ts +++ b/legacy-sdk/cli/src/utils/transaction_sender.ts @@ -1,4 +1,8 @@ -import type { Keypair, VersionedTransaction } from "@solana/web3.js"; +import type { + AddressLookupTableAccount, + Keypair, + VersionedTransaction, +} from "@solana/web3.js"; import { ComputeBudgetProgram, LAMPORTS_PER_SOL } from "@solana/web3.js"; import { DecimalUtil, @@ -11,6 +15,8 @@ import { promptConfirm, promptText } from "./prompt"; export async function sendTransaction( builder: TransactionBuilder, + defaultPriorityFeeInLamports?: number, + alts?: AddressLookupTableAccount[], ): Promise { const instructions = builder.compressIx(true); // HACK: to clone TransactionBuilder @@ -25,35 +31,43 @@ export async function sendTransaction( ); console.info("estimatedComputeUnits:", estimatedComputeUnits); + let useDefaultPriorityFeeInLamports = + defaultPriorityFeeInLamports !== undefined; + let landed = false; let success = false; while (true) { let priorityFeeInLamports = 0; - while (true) { - const priorityFeeInSOL = await promptText("priorityFeeInSOL"); - priorityFeeInLamports = DecimalUtil.toBN( - new Decimal(priorityFeeInSOL), - 9, - ).toNumber(); - if (priorityFeeInLamports > LAMPORTS_PER_SOL) { - console.info("> 1 SOL is obviously too much for priority fee"); - continue; - } - if (priorityFeeInLamports > 5_000_000) { + if (useDefaultPriorityFeeInLamports) { + priorityFeeInLamports = defaultPriorityFeeInLamports!; + useDefaultPriorityFeeInLamports = false; + } else { + while (true) { + const priorityFeeInSOL = await promptText("priorityFeeInSOL"); + priorityFeeInLamports = DecimalUtil.toBN( + new Decimal(priorityFeeInSOL), + 9, + ).toNumber(); + if (priorityFeeInLamports > LAMPORTS_PER_SOL) { + console.info("> 1 SOL is obviously too much for priority fee"); + continue; + } + if (priorityFeeInLamports > 5_000_000) { + console.info( + `Is it okay to use ${priorityFeeInLamports / LAMPORTS_PER_SOL} SOL for priority fee ? (if it is OK, enter OK)`, + ); + const ok = await promptConfirm("OK"); + if (!ok) continue; + } + console.info( - `Is it okay to use ${priorityFeeInLamports / LAMPORTS_PER_SOL} SOL for priority fee ? (if it is OK, enter OK)`, + "Priority fee:", + priorityFeeInLamports / LAMPORTS_PER_SOL, + "SOL", ); - const ok = await promptConfirm("OK"); - if (!ok) continue; + break; } - - console.info( - "Priority fee:", - priorityFeeInLamports / LAMPORTS_PER_SOL, - "SOL", - ); - break; } const builderWithPriorityFee = new TransactionBuilder( @@ -83,7 +97,7 @@ export async function sendTransaction( let withDifferentPriorityFee = false; while (true) { console.info("process transaction..."); - const result = await send(builderWithPriorityFee); + const result = await send(builderWithPriorityFee, alts); landed = result.landed; success = result.success; if (landed) break; @@ -109,12 +123,16 @@ export async function sendTransaction( async function send( builder: TransactionBuilder, + alts: AddressLookupTableAccount[] = [], ): Promise<{ landed: boolean; success: boolean }> { const connection = builder.connection; const wallet = builder.wallet; // manual build - const built = await builder.build({ maxSupportedTransactionVersion: 0 }); + const built = await builder.build({ + maxSupportedTransactionVersion: 0, + lookupTableAccounts: alts, + }); const blockhash = await connection.getLatestBlockhashAndContext("confirmed"); const blockHeight = await connection.getBlockHeight({ diff --git a/yarn.lock b/yarn.lock index 2652ebd7a..29999f1db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4176,8 +4176,8 @@ __metadata: resolution: "@orca-so/whirlpools-sdk-cli@workspace:legacy-sdk/cli" dependencies: "@coral-xyz/anchor": "npm:0.29.0" - "@orca-so/common-sdk": "npm:*" - "@orca-so/whirlpools-sdk": "npm:*" + "@orca-so/common-sdk": "npm:0.6.4" + "@orca-so/whirlpools-sdk": "npm:0.13.12" "@solana/spl-token": "npm:0.4.1" "@solana/web3.js": "npm:^1.90.0" "@types/bn.js": "npm:^5.1.0" @@ -4223,6 +4223,21 @@ __metadata: languageName: unknown linkType: soft +"@orca-so/whirlpools-sdk@npm:0.13.12": + version: 0.13.12 + resolution: "@orca-so/whirlpools-sdk@npm:0.13.12" + dependencies: + tiny-invariant: "npm:^1.3.1" + peerDependencies: + "@coral-xyz/anchor": ~0.29.0 + "@orca-so/common-sdk": 0.6.4 + "@solana/spl-token": ^0.4.8 + "@solana/web3.js": ^1.90.0 + decimal.js: ^10.4.3 + checksum: 10c0/150a6ffe890f9c5894e6c5df16ee7074ee679d064152931923835a45905b52b9cf823fc9de924775ed28199f9adb7d09e4d8eedecbc7c8fb6a31c74bf1b884e0 + languageName: node + linkType: hard + "@orca-so/whirlpools@npm:*, @orca-so/whirlpools@workspace:ts-sdk/whirlpool": version: 0.0.0-use.local resolution: "@orca-so/whirlpools@workspace:ts-sdk/whirlpool"