@coffer_so/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/dist/clients/AdminClient.d.ts +194 -0
  4. package/dist/clients/AdminClient.d.ts.map +1 -0
  5. package/dist/clients/AdminClient.js +482 -0
  6. package/dist/clients/AdminClient.js.map +1 -0
  7. package/dist/clients/CofferBackendClient.d.ts +877 -0
  8. package/dist/clients/CofferBackendClient.d.ts.map +1 -0
  9. package/dist/clients/CofferBackendClient.js +536 -0
  10. package/dist/clients/CofferBackendClient.js.map +1 -0
  11. package/dist/clients/CubeBackendClient.d.ts +877 -0
  12. package/dist/clients/CubeBackendClient.d.ts.map +1 -0
  13. package/dist/clients/CubeBackendClient.js +536 -0
  14. package/dist/clients/CubeBackendClient.js.map +1 -0
  15. package/dist/clients/CubicPoolClient.d.ts +120 -0
  16. package/dist/clients/CubicPoolClient.d.ts.map +1 -0
  17. package/dist/clients/CubicPoolClient.js +687 -0
  18. package/dist/clients/CubicPoolClient.js.map +1 -0
  19. package/dist/clients/PoolFactoryClient.d.ts +53 -0
  20. package/dist/clients/PoolFactoryClient.d.ts.map +1 -0
  21. package/dist/clients/PoolFactoryClient.js +91 -0
  22. package/dist/clients/PoolFactoryClient.js.map +1 -0
  23. package/dist/clients/RpcClient.d.ts +49 -0
  24. package/dist/clients/RpcClient.d.ts.map +1 -0
  25. package/dist/clients/RpcClient.js +135 -0
  26. package/dist/clients/RpcClient.js.map +1 -0
  27. package/dist/clients/SingleTokenDepositClient.d.ts +67 -0
  28. package/dist/clients/SingleTokenDepositClient.d.ts.map +1 -0
  29. package/dist/clients/SingleTokenDepositClient.js +105 -0
  30. package/dist/clients/SingleTokenDepositClient.js.map +1 -0
  31. package/dist/clients/contract-instructions.d.ts +24 -0
  32. package/dist/clients/contract-instructions.d.ts.map +1 -0
  33. package/dist/clients/contract-instructions.js +130 -0
  34. package/dist/clients/contract-instructions.js.map +1 -0
  35. package/dist/clients/index.d.ts +10 -0
  36. package/dist/clients/index.d.ts.map +1 -0
  37. package/dist/clients/index.js +26 -0
  38. package/dist/clients/index.js.map +1 -0
  39. package/dist/clients/quote-math.d.ts +15 -0
  40. package/dist/clients/quote-math.d.ts.map +1 -0
  41. package/dist/clients/quote-math.js +98 -0
  42. package/dist/clients/quote-math.js.map +1 -0
  43. package/dist/clients/tx-builders.d.ts +231 -0
  44. package/dist/clients/tx-builders.d.ts.map +1 -0
  45. package/dist/clients/tx-builders.js +861 -0
  46. package/dist/clients/tx-builders.js.map +1 -0
  47. package/dist/clients/versioned.d.ts +34 -0
  48. package/dist/clients/versioned.d.ts.map +1 -0
  49. package/dist/clients/versioned.js +55 -0
  50. package/dist/clients/versioned.js.map +1 -0
  51. package/dist/config/index.d.ts +65 -0
  52. package/dist/config/index.d.ts.map +1 -0
  53. package/dist/config/index.js +75 -0
  54. package/dist/config/index.js.map +1 -0
  55. package/dist/config/networks.d.ts +12 -0
  56. package/dist/config/networks.d.ts.map +1 -0
  57. package/dist/config/networks.js +51 -0
  58. package/dist/config/networks.js.map +1 -0
  59. package/dist/config/tokens.d.ts +19 -0
  60. package/dist/config/tokens.d.ts.map +1 -0
  61. package/dist/config/tokens.js +43 -0
  62. package/dist/config/tokens.js.map +1 -0
  63. package/dist/idl/cubic_pool.json +4203 -0
  64. package/dist/idl/index.d.ts +1769 -0
  65. package/dist/idl/index.d.ts.map +1 -0
  66. package/dist/idl/index.js +18 -0
  67. package/dist/idl/index.js.map +1 -0
  68. package/dist/idl/protocol_admin.json +2615 -0
  69. package/dist/idl/single_token_liquidity.json +1121 -0
  70. package/dist/index.d.ts +75 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +206 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/math/cubicMath.d.ts +40 -0
  75. package/dist/math/cubicMath.d.ts.map +1 -0
  76. package/dist/math/cubicMath.js +82 -0
  77. package/dist/math/cubicMath.js.map +1 -0
  78. package/dist/math/fixedPoint.d.ts +19 -0
  79. package/dist/math/fixedPoint.d.ts.map +1 -0
  80. package/dist/math/fixedPoint.js +86 -0
  81. package/dist/math/fixedPoint.js.map +1 -0
  82. package/dist/math/index.d.ts +9 -0
  83. package/dist/math/index.d.ts.map +1 -0
  84. package/dist/math/index.js +30 -0
  85. package/dist/math/index.js.map +1 -0
  86. package/dist/math/logExp.d.ts +6 -0
  87. package/dist/math/logExp.d.ts.map +1 -0
  88. package/dist/math/logExp.js +68 -0
  89. package/dist/math/logExp.js.map +1 -0
  90. package/dist/math/lpLoss.d.ts +44 -0
  91. package/dist/math/lpLoss.d.ts.map +1 -0
  92. package/dist/math/lpLoss.js +103 -0
  93. package/dist/math/lpLoss.js.map +1 -0
  94. package/dist/math/maxSelloff.d.ts +164 -0
  95. package/dist/math/maxSelloff.d.ts.map +1 -0
  96. package/dist/math/maxSelloff.js +397 -0
  97. package/dist/math/maxSelloff.js.map +1 -0
  98. package/dist/math/singleToken.d.ts +54 -0
  99. package/dist/math/singleToken.d.ts.map +1 -0
  100. package/dist/math/singleToken.js +188 -0
  101. package/dist/math/singleToken.js.map +1 -0
  102. package/dist/math/slippage.d.ts +13 -0
  103. package/dist/math/slippage.d.ts.map +1 -0
  104. package/dist/math/slippage.js +44 -0
  105. package/dist/math/slippage.js.map +1 -0
  106. package/dist/math/surgeFee.d.ts +24 -0
  107. package/dist/math/surgeFee.d.ts.map +1 -0
  108. package/dist/math/surgeFee.js +85 -0
  109. package/dist/math/surgeFee.js.map +1 -0
  110. package/dist/math/weightedMath.d.ts +20 -0
  111. package/dist/math/weightedMath.d.ts.map +1 -0
  112. package/dist/math/weightedMath.js +66 -0
  113. package/dist/math/weightedMath.js.map +1 -0
  114. package/dist/parsers/borsh.d.ts +28 -0
  115. package/dist/parsers/borsh.d.ts.map +1 -0
  116. package/dist/parsers/borsh.js +104 -0
  117. package/dist/parsers/borsh.js.map +1 -0
  118. package/dist/parsers/contracts.d.ts +26 -0
  119. package/dist/parsers/contracts.d.ts.map +1 -0
  120. package/dist/parsers/contracts.js +62 -0
  121. package/dist/parsers/contracts.js.map +1 -0
  122. package/dist/parsers/events.d.ts +3 -0
  123. package/dist/parsers/events.d.ts.map +1 -0
  124. package/dist/parsers/events.js +281 -0
  125. package/dist/parsers/events.js.map +1 -0
  126. package/dist/parsers/index.d.ts +6 -0
  127. package/dist/parsers/index.d.ts.map +1 -0
  128. package/dist/parsers/index.js +22 -0
  129. package/dist/parsers/index.js.map +1 -0
  130. package/dist/parsers/mintAccount.d.ts +24 -0
  131. package/dist/parsers/mintAccount.d.ts.map +1 -0
  132. package/dist/parsers/mintAccount.js +27 -0
  133. package/dist/parsers/mintAccount.js.map +1 -0
  134. package/dist/parsers/poolAccount.d.ts +141 -0
  135. package/dist/parsers/poolAccount.d.ts.map +1 -0
  136. package/dist/parsers/poolAccount.js +224 -0
  137. package/dist/parsers/poolAccount.js.map +1 -0
  138. package/dist/types/contracts.d.ts +1340 -0
  139. package/dist/types/contracts.d.ts.map +1 -0
  140. package/dist/types/contracts.js +3 -0
  141. package/dist/types/contracts.js.map +1 -0
  142. package/dist/types/events.d.ts +172 -0
  143. package/dist/types/events.d.ts.map +1 -0
  144. package/dist/types/events.js +3 -0
  145. package/dist/types/events.js.map +1 -0
  146. package/dist/types/index.d.ts +6 -0
  147. package/dist/types/index.d.ts.map +1 -0
  148. package/dist/types/index.js +22 -0
  149. package/dist/types/index.js.map +1 -0
  150. package/dist/types/pool.d.ts +149 -0
  151. package/dist/types/pool.d.ts.map +1 -0
  152. package/dist/types/pool.js +3 -0
  153. package/dist/types/pool.js.map +1 -0
  154. package/dist/types/result.d.ts +24 -0
  155. package/dist/types/result.d.ts.map +1 -0
  156. package/dist/types/result.js +11 -0
  157. package/dist/types/result.js.map +1 -0
  158. package/dist/types/tx.d.ts +261 -0
  159. package/dist/types/tx.d.ts.map +1 -0
  160. package/dist/types/tx.js +3 -0
  161. package/dist/types/tx.js.map +1 -0
  162. package/dist/utils/errors.d.ts +44 -0
  163. package/dist/utils/errors.d.ts.map +1 -0
  164. package/dist/utils/errors.js +192 -0
  165. package/dist/utils/errors.js.map +1 -0
  166. package/dist/utils/extensions.d.ts +98 -0
  167. package/dist/utils/extensions.d.ts.map +1 -0
  168. package/dist/utils/extensions.js +187 -0
  169. package/dist/utils/extensions.js.map +1 -0
  170. package/dist/utils/index.d.ts +5 -0
  171. package/dist/utils/index.d.ts.map +1 -0
  172. package/dist/utils/index.js +21 -0
  173. package/dist/utils/index.js.map +1 -0
  174. package/dist/utils/pda.d.ts +8 -0
  175. package/dist/utils/pda.d.ts.map +1 -0
  176. package/dist/utils/pda.js +27 -0
  177. package/dist/utils/pda.js.map +1 -0
  178. package/dist/utils/retry.d.ts +22 -0
  179. package/dist/utils/retry.d.ts.map +1 -0
  180. package/dist/utils/retry.js +62 -0
  181. package/dist/utils/retry.js.map +1 -0
  182. package/package.json +54 -0
  183. package/src/clients/AdminClient.ts +608 -0
  184. package/src/clients/CofferBackendClient.ts +1413 -0
  185. package/src/clients/CubicPoolClient.ts +719 -0
  186. package/src/clients/PoolFactoryClient.ts +110 -0
  187. package/src/clients/RpcClient.ts +195 -0
  188. package/src/clients/SingleTokenDepositClient.ts +143 -0
  189. package/src/clients/contract-instructions.ts +115 -0
  190. package/src/clients/index.ts +9 -0
  191. package/src/clients/quote-math.ts +81 -0
  192. package/src/clients/tx-builders.ts +1096 -0
  193. package/src/clients/versioned.ts +80 -0
  194. package/src/config/index.ts +111 -0
  195. package/src/config/networks.ts +71 -0
  196. package/src/config/tokens.ts +52 -0
  197. package/src/idl/cubic_pool.json +4203 -0
  198. package/src/idl/index.ts +13 -0
  199. package/src/idl/protocol_admin.json +2615 -0
  200. package/src/idl/single_token_liquidity.json +1121 -0
  201. package/src/index.ts +262 -0
  202. package/src/math/cubicMath.ts +92 -0
  203. package/src/math/fixedPoint.ts +71 -0
  204. package/src/math/index.ts +13 -0
  205. package/src/math/logExp.ts +61 -0
  206. package/src/math/lpLoss.ts +160 -0
  207. package/src/math/maxSelloff.ts +564 -0
  208. package/src/math/singleToken.ts +251 -0
  209. package/src/math/slippage.ts +38 -0
  210. package/src/math/surgeFee.ts +101 -0
  211. package/src/math/weightedMath.ts +64 -0
  212. package/src/parsers/borsh.ts +106 -0
  213. package/src/parsers/contracts.ts +64 -0
  214. package/src/parsers/events.ts +275 -0
  215. package/src/parsers/index.ts +5 -0
  216. package/src/parsers/mintAccount.ts +44 -0
  217. package/src/parsers/poolAccount.ts +372 -0
  218. package/src/types/contracts.ts +238 -0
  219. package/src/types/events.ts +198 -0
  220. package/src/types/index.ts +5 -0
  221. package/src/types/pool.ts +147 -0
  222. package/src/types/result.ts +51 -0
  223. package/src/types/tx.ts +278 -0
  224. package/src/utils/errors.ts +210 -0
  225. package/src/utils/extensions.ts +196 -0
  226. package/src/utils/index.ts +4 -0
  227. package/src/utils/pda.ts +58 -0
  228. package/src/utils/retry.ts +85 -0
@@ -0,0 +1,1096 @@
1
+ import {
2
+ AccountMeta,
3
+ AddressLookupTableProgram,
4
+ ComputeBudgetProgram,
5
+ PublicKey,
6
+ SystemProgram,
7
+ TransactionInstruction,
8
+ } from "@solana/web3.js";
9
+ import {
10
+ ASSOCIATED_TOKEN_PROGRAM_ID,
11
+ TOKEN_PROGRAM_ID,
12
+ TOKEN_2022_PROGRAM_ID,
13
+ createAssociatedTokenAccountIdempotentInstruction,
14
+ } from "@solana/spl-token";
15
+ import BN from "bn.js";
16
+ import { CofferConfig, MINIMUM_INITIAL_BPT } from "../config";
17
+ import { CUBIC_POOL_IDL, PROTOCOL_ADMIN_IDL, SINGLE_TOKEN_LIQUIDITY_IDL } from "../idl";
18
+ import { PoolInfo } from "../types/pool";
19
+ import {
20
+ AddLiquidityParams,
21
+ BuiltTx,
22
+ DeployPoolParams,
23
+ RangeManagerUpdateParams,
24
+ RemoveLiquidityParams,
25
+ SetRangeManagerConfigParams,
26
+ SetRangeManagerParams,
27
+ SingleTokenDepositParams,
28
+ SwapParams,
29
+ SelloffParams,
30
+ TokenChange,
31
+ } from "../types/tx";
32
+ import { deriveAta, deriveBptMint, deriveHelperPda } from "../utils/pda";
33
+ import { buildContractInstruction } from "./contract-instructions";
34
+ import { calcTokensOutGivenBptIn, calcBptOutGivenExactTokensIn } from "../math/cubicMath";
35
+ import { divDown, mulDown } from "../math/fixedPoint";
36
+ import { assertTokensSupported } from "../utils/extensions";
37
+
38
+ /**
39
+ * Low-level transaction builders. Emit raw `TransactionInstruction`s suitable
40
+ * for combination with others (e.g. versioned transactions with ALTs) in a
41
+ * single tx. Higher-level convenience lives on `CubicPoolClient`.
42
+ */
43
+
44
+ /**
45
+ * Anchor discriminator: sha256("global:<ix_name>")[0..8].
46
+ *
47
+ * Read straight out of the shipped IDL rather than hand-maintained, so a
48
+ * regenerated IDL can never silently disagree with the encoder. This table
49
+ * is a tripwire, not the source of truth: if a refreshed IDL changes bytes
50
+ * for an instruction we already ship, that is either a renamed instruction
51
+ * or a bad IDL copy, and we want a loud failure at import time instead of a
52
+ * transaction the program rejects with `InstructionFallbackNotFound`.
53
+ *
54
+ * NOTE: declared before `CUBIC_POOL_DISC` on purpose. Those maps call
55
+ * `computeDiscriminator` at module-evaluation time, and a `const` declared
56
+ * after them would still be in its temporal dead zone when they run.
57
+ */
58
+ const PINNED: Record<string, string> = {
59
+ swap: "f8c69e91e17587c8",
60
+ add_liquidity: "b59d59438fb63448",
61
+ remove_liquidity: "5055d14818ceb16c",
62
+ initialize_cubic_pool: "d79474cf79686f83",
63
+ initialize_pool_alt: "fb874202f4490c90",
64
+ deposit_single_token: "a688a62fc7c056a9",
65
+ initialize_config: "d07f1501c2bec446",
66
+ set_range_manager: "6766ad7008022584",
67
+ set_range_manager_config: "67d300c8a4bdf5cd",
68
+ range_manager_update: "f5b72345c80aac2c",
69
+ pool_initialize_config: "d06eadbc279990b9",
70
+ };
71
+
72
+ function computeDiscriminator(
73
+ ixName: string,
74
+ idl: "cubicPool" | "stld" | "protocolAdmin" = "cubicPool",
75
+ ): Buffer {
76
+ const source =
77
+ idl === "stld"
78
+ ? SINGLE_TOKEN_LIQUIDITY_IDL
79
+ : idl === "protocolAdmin"
80
+ ? PROTOCOL_ADMIN_IDL
81
+ : CUBIC_POOL_IDL;
82
+ const ix = (source.instructions as Array<{ name: string; discriminator: number[] }>).find(
83
+ (candidate) => candidate.name === ixName,
84
+ );
85
+ if (!ix) {
86
+ throw new Error(`tx-builders: no "${ixName}" instruction in the ${idl} IDL`);
87
+ }
88
+ const bytes = Buffer.from(ix.discriminator);
89
+ const pinned = PINNED[ixName];
90
+ if (pinned && bytes.toString("hex") !== pinned) {
91
+ throw new Error(
92
+ `tx-builders: IDL discriminator for "${ixName}" is ${bytes.toString("hex")}, ` +
93
+ `pinned value is ${pinned}. The IDL and this encoder disagree — ` +
94
+ `do not ship until you know which one is right.`,
95
+ );
96
+ }
97
+ return bytes;
98
+ }
99
+
100
+ const CUBIC_POOL_DISC = {
101
+ swap: computeDiscriminator("swap"),
102
+ addLiquidity: computeDiscriminator("add_liquidity"),
103
+ removeLiquidity: computeDiscriminator("remove_liquidity"),
104
+ initializeCubicPool: computeDiscriminator("initialize_cubic_pool"),
105
+ initializePoolAlt: computeDiscriminator("initialize_pool_alt"),
106
+ setRangeManager: computeDiscriminator("set_range_manager"),
107
+ setRangeManagerConfig: computeDiscriminator("set_range_manager_config"),
108
+ rangeManagerUpdate: computeDiscriminator("range_manager_update"),
109
+ };
110
+
111
+ const PROTOCOL_ADMIN_DISC = {
112
+ poolInitializeConfig: computeDiscriminator("pool_initialize_config", "protocolAdmin"),
113
+ };
114
+
115
+ const STLD_DISC = {
116
+ depositSingleToken: computeDiscriminator("deposit_single_token", "stld"),
117
+ };
118
+
119
+ function bptProgram(pool: PoolInfo): PublicKey {
120
+ const program = pool.bptTokenProgram ?? TOKEN_PROGRAM_ID;
121
+ if (!program.equals(TOKEN_PROGRAM_ID) && !program.equals(TOKEN_2022_PROGRAM_ID)) {
122
+ throw new Error("BPT mint must be owned by SPL Token or Token-2022");
123
+ }
124
+ return program;
125
+ }
126
+
127
+ function requirePositiveMinimumBpt(minimumBptAmount: BN | undefined, ixName: string): BN {
128
+ if (!minimumBptAmount || minimumBptAmount.lte(new BN(0))) {
129
+ throw new Error(`${ixName}: minimumBptAmount must be positive`);
130
+ }
131
+ return minimumBptAmount;
132
+ }
133
+
134
+ /**
135
+ * Require an explicit per-token slippage floor for remove_liquidity.
136
+ *
137
+ * Previously this defaulted to all-zero when omitted, which silently shipped
138
+ * a remove tx with NO slippage protection (sandwich/MEV exposure). Callers
139
+ * must now pass `minimumTokenAmounts` (derive them from a fresh `quoteRemove()`
140
+ * minus tolerance). To intentionally disable protection, pass explicit zeros.
141
+ */
142
+ function requireExplicitMinimums(
143
+ minimumTokenAmounts: BN[] | undefined,
144
+ tokenCount: number,
145
+ ixName: string
146
+ ): BN[] {
147
+ if (!minimumTokenAmounts) {
148
+ throw new Error(
149
+ `${ixName}: minimumTokenAmounts must be provided (one per token). ` +
150
+ `Derive per-token floors from a fresh quoteRemove(); pass explicit zeros ` +
151
+ `only to intentionally disable slippage protection.`
152
+ );
153
+ }
154
+ if (minimumTokenAmounts.length !== tokenCount) {
155
+ throw new Error(
156
+ `${ixName}: minimumTokenAmounts length (${minimumTokenAmounts.length}) must equal token count (${tokenCount})`
157
+ );
158
+ }
159
+ return minimumTokenAmounts;
160
+ }
161
+
162
+ // ============================================================
163
+ // Swap
164
+ // ============================================================
165
+
166
+ export function buildSwapIx(
167
+ cfg: CofferConfig,
168
+ pool: PoolInfo,
169
+ params: SwapParams & { minAmountOut: BN }
170
+ ): TransactionInstruction {
171
+ assertTokensSupported(pool, [params.tokenInIndex, params.tokenOutIndex], "swap");
172
+ const inTok = pool.tokens[params.tokenInIndex];
173
+ const outTok = pool.tokens[params.tokenOutIndex];
174
+
175
+ const userTokenIn = deriveAta(params.user, inTok.mint, inTok.tokenProgram);
176
+ const userTokenOut = deriveAta(params.user, outTok.mint, outTok.tokenProgram);
177
+
178
+ const data = Buffer.concat([
179
+ CUBIC_POOL_DISC.swap,
180
+ encodeU64(params.amountIn),
181
+ encodeU64(params.minAmountOut),
182
+ encodeU8(params.tokenInIndex),
183
+ encodeU8(params.tokenOutIndex),
184
+ ]);
185
+
186
+ const keys: AccountMeta[] = [
187
+ { pubkey: pool.address, isSigner: false, isWritable: true },
188
+ { pubkey: inTok.mint, isSigner: false, isWritable: false },
189
+ { pubkey: outTok.mint, isSigner: false, isWritable: false },
190
+ { pubkey: userTokenIn, isSigner: false, isWritable: true },
191
+ { pubkey: userTokenOut, isSigner: false, isWritable: true },
192
+ { pubkey: inTok.vault, isSigner: false, isWritable: true },
193
+ { pubkey: outTok.vault, isSigner: false, isWritable: true },
194
+ { pubkey: params.user, isSigner: true, isWritable: true },
195
+ { pubkey: inTok.tokenProgram, isSigner: false, isWritable: false },
196
+ { pubkey: outTok.tokenProgram, isSigner: false, isWritable: false },
197
+ ];
198
+
199
+ return new TransactionInstruction({
200
+ programId: cfg.programs.cubicPool,
201
+ keys,
202
+ data,
203
+ });
204
+ }
205
+
206
+ export function buildSwapTx(
207
+ cfg: CofferConfig,
208
+ pool: PoolInfo,
209
+ params: SwapParams & { minAmountOut: BN }
210
+ ): BuiltTx {
211
+ return {
212
+ instructions: [
213
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
214
+ buildSwapIx(cfg, pool, params),
215
+ ],
216
+ suggestedCuLimit: 400_000,
217
+ };
218
+ }
219
+
220
+ // ============================================================
221
+ // Add liquidity (proportional)
222
+ // ============================================================
223
+
224
+ /**
225
+ * `add_liquidity` — proportional join.
226
+ *
227
+ * ⚠ **`params.tokenAmounts` is a CEILING, not an exact amount** (v5.1,
228
+ * audit M-4 / I-1). The program takes the largest strictly-proportional
229
+ * basket that fits inside the vector you pass and leaves the remainder in
230
+ * the wallet; it does NOT pull the full amounts. Any UI copy of the form
231
+ * "you will deposit exactly X" is wrong — quote the proportional basket
232
+ * instead (see `CubicPoolClient.quoteAddLiquidity`).
233
+ *
234
+ * The wire format is unchanged, so this is a behaviour note, not an
235
+ * encoding change: the same bytes now mean "at most this much".
236
+ */
237
+ export function buildAddLiquidityIx(
238
+ cfg: CofferConfig,
239
+ pool: PoolInfo,
240
+ params: AddLiquidityParams
241
+ ): TransactionInstruction {
242
+ assertTokensSupported(pool, params.tokenAmounts.flatMap((amount, i) => amount.gt(new BN(0)) ? [i] : []), "add_liquidity");
243
+ const userBpt = deriveAta(params.user, pool.bptMint, bptProgram(pool));
244
+ const minBpt = requirePositiveMinimumBpt(params.minimumBptAmount, "add_liquidity");
245
+ if (params.tokenAmounts.length !== pool.tokenCount) {
246
+ // The program rejects this with `InvalidArrayLength` (6064); catching it
247
+ // here costs a round trip less.
248
+ throw new Error(
249
+ `add_liquidity: tokenAmounts length (${params.tokenAmounts.length}) ` +
250
+ `must equal the pool's token count (${pool.tokenCount})`
251
+ );
252
+ }
253
+
254
+ // Refuse joins whose integer-rounded basket has a zero live leg. Such a
255
+ // basket can mint positive BPT in this deployment despite moving no tokens.
256
+ if (pool.bptTotalSupply.gt(new BN(0))) {
257
+ const balances = pool.tokens.map(token => BigInt(token.actualBalance.toString()));
258
+ const amounts = params.tokenAmounts.map(amount => BigInt(amount.toString()));
259
+ let ratio: bigint | null = null;
260
+ balances.forEach((balance, i) => {
261
+ if ((balance > 0n) !== (amounts[i] > 0n)) throw new Error("add_liquidity: token liveness mismatch");
262
+ if (balance === 0n) return;
263
+ const candidate = divDown(amounts[i], balance);
264
+ ratio = ratio === null || candidate < ratio ? candidate : ratio;
265
+ });
266
+ if (ratio === null || calcBptOutGivenExactTokensIn(balances, amounts, BigInt(pool.bptTotalSupply.toString())) === 0n ||
267
+ balances.some(balance => balance > 0n && mulDown(balance, ratio!) === 0n)) {
268
+ throw new Error("add_liquidity: deposit too small after proportional rounding");
269
+ }
270
+ }
271
+
272
+ const data = Buffer.concat([
273
+ CUBIC_POOL_DISC.addLiquidity,
274
+ encodeVecU64(params.tokenAmounts),
275
+ encodeU64(minBpt),
276
+ ]);
277
+
278
+ // remaining_accounts layout:
279
+ // [user_token_i, vault_i] × N, mint_i × N, token_program_i × N
280
+ const remaining: AccountMeta[] = [];
281
+ for (let i = 0; i < pool.tokenCount; i++) {
282
+ const t = pool.tokens[i];
283
+ const userAta = deriveAta(params.user, t.mint, t.tokenProgram);
284
+ remaining.push({ pubkey: userAta, isSigner: false, isWritable: true });
285
+ remaining.push({ pubkey: t.vault, isSigner: false, isWritable: true });
286
+ }
287
+ for (let i = 0; i < pool.tokenCount; i++) {
288
+ remaining.push({ pubkey: pool.tokens[i].mint, isSigner: false, isWritable: false });
289
+ }
290
+ for (let i = 0; i < pool.tokenCount; i++) {
291
+ remaining.push({ pubkey: pool.tokens[i].tokenProgram, isSigner: false, isWritable: false });
292
+ }
293
+
294
+ const keys: AccountMeta[] = [
295
+ { pubkey: pool.address, isSigner: false, isWritable: true },
296
+ { pubkey: pool.bptMint, isSigner: false, isWritable: true },
297
+ { pubkey: userBpt, isSigner: false, isWritable: true },
298
+ { pubkey: params.user, isSigner: true, isWritable: true },
299
+ { pubkey: bptProgram(pool), isSigner: false, isWritable: false },
300
+ ...remaining,
301
+ ];
302
+
303
+ return new TransactionInstruction({
304
+ programId: cfg.programs.cubicPool,
305
+ keys,
306
+ data,
307
+ });
308
+ }
309
+
310
+ export function buildAddLiquidityTx(
311
+ cfg: CofferConfig,
312
+ pool: PoolInfo,
313
+ params: AddLiquidityParams
314
+ ): BuiltTx {
315
+ const userBpt = deriveAta(params.user, pool.bptMint, bptProgram(pool));
316
+ return {
317
+ instructions: [
318
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 }),
319
+ createAssociatedTokenAccountIdempotentInstruction(
320
+ params.user,
321
+ userBpt,
322
+ params.user,
323
+ pool.bptMint,
324
+ bptProgram(pool)
325
+ ),
326
+ buildAddLiquidityIx(cfg, pool, params),
327
+ ],
328
+ suggestedCuLimit: 600_000,
329
+ };
330
+ }
331
+
332
+ // ============================================================
333
+ // Remove liquidity
334
+ // ============================================================
335
+
336
+ export function buildRemoveLiquidityIx(
337
+ cfg: CofferConfig,
338
+ pool: PoolInfo,
339
+ params: RemoveLiquidityParams
340
+ ): TransactionInstruction {
341
+ const supply = pool.bptTotalSupply;
342
+ const maxBurn = BN.max(supply.sub(new BN(MINIMUM_INITIAL_BPT.toString())), new BN(0));
343
+ const effectiveBurn = BN.min(params.bptAmount, maxBurn);
344
+ const transferred = supply.gt(new BN(0))
345
+ ? calcTokensOutGivenBptIn(pool.tokens.map(token => BigInt(token.actualBalance.toString())), BigInt(effectiveBurn.toString()), BigInt(supply.toString()))
346
+ .flatMap((amount, i) => amount > 0n ? [i] : [])
347
+ : [];
348
+ assertTokensSupported(pool, transferred, "remove_liquidity");
349
+ const userBpt = deriveAta(params.user, pool.bptMint, bptProgram(pool));
350
+ const mins = requireExplicitMinimums(params.minimumTokenAmounts, pool.tokenCount, "remove_liquidity");
351
+
352
+ const data = Buffer.concat([
353
+ CUBIC_POOL_DISC.removeLiquidity,
354
+ encodeU64(params.bptAmount),
355
+ encodeVecU64(mins),
356
+ ]);
357
+
358
+ // remove_liquidity remaining_accounts format:
359
+ // [vault_i, user_token_i] × N, mint_i × N, token_program_i × N
360
+ const remaining: AccountMeta[] = [];
361
+ for (let i = 0; i < pool.tokenCount; i++) {
362
+ const t = pool.tokens[i];
363
+ const userAta = deriveAta(params.user, t.mint, t.tokenProgram);
364
+ remaining.push({ pubkey: t.vault, isSigner: false, isWritable: true });
365
+ remaining.push({ pubkey: userAta, isSigner: false, isWritable: true });
366
+ }
367
+ for (let i = 0; i < pool.tokenCount; i++) {
368
+ remaining.push({ pubkey: pool.tokens[i].mint, isSigner: false, isWritable: false });
369
+ }
370
+ for (let i = 0; i < pool.tokenCount; i++) {
371
+ remaining.push({ pubkey: pool.tokens[i].tokenProgram, isSigner: false, isWritable: false });
372
+ }
373
+
374
+ const keys: AccountMeta[] = [
375
+ { pubkey: pool.address, isSigner: false, isWritable: true },
376
+ { pubkey: pool.bptMint, isSigner: false, isWritable: true },
377
+ { pubkey: userBpt, isSigner: false, isWritable: true },
378
+ { pubkey: params.user, isSigner: true, isWritable: true },
379
+ { pubkey: bptProgram(pool), isSigner: false, isWritable: false },
380
+ ...remaining,
381
+ ];
382
+
383
+ return new TransactionInstruction({
384
+ programId: cfg.programs.cubicPool,
385
+ keys,
386
+ data,
387
+ });
388
+ }
389
+
390
+ export function buildRemoveLiquidityTx(
391
+ cfg: CofferConfig,
392
+ pool: PoolInfo,
393
+ params: RemoveLiquidityParams
394
+ ): BuiltTx {
395
+ const ixs: TransactionInstruction[] = [
396
+ ComputeBudgetProgram.setComputeUnitLimit({ units: cfg.defaults.cuLimit }),
397
+ ];
398
+
399
+ // The contract transfers every pool token to the user's ATA. Include
400
+ // idempotent creates so a proportional burn works even when the user never
401
+ // held one of the receive tokens before.
402
+ for (const t of pool.tokens) {
403
+ const userAta = deriveAta(params.user, t.mint, t.tokenProgram);
404
+ ixs.push(
405
+ createAssociatedTokenAccountIdempotentInstruction(
406
+ params.user,
407
+ userAta,
408
+ params.user,
409
+ t.mint,
410
+ t.tokenProgram
411
+ )
412
+ );
413
+ }
414
+
415
+ ixs.push(buildRemoveLiquidityIx(cfg, pool, params));
416
+
417
+ return {
418
+ instructions: ixs,
419
+ suggestedCuLimit: cfg.defaults.cuLimit,
420
+ };
421
+ }
422
+
423
+ // ============================================================
424
+ // Single-token deposit (helper program)
425
+ // ============================================================
426
+
427
+ /**
428
+ * Largest pool the zap will accept, mirroring `STLD_MAX_TOKENS` in
429
+ * `single-token-liquidity/src/constants.rs` (which is defined as
430
+ * cubic-pool's `MAX_TOKENS`).
431
+ *
432
+ * Raised from 5 to 10 in v5.1 (audit SF-3): the old ceiling existed because
433
+ * the per-leg allocations exhausted the 32 KiB bump heap. Above this the
434
+ * program rejects with `PoolTooLargeForZap` (stld 6032).
435
+ */
436
+ export const STLD_MAX_TOKENS = 10;
437
+
438
+ /**
439
+ * Compute units to request for the zap.
440
+ *
441
+ * Measured on the v5.1 build at the worst case N=10: 1,073,206 CU for
442
+ * classic SPL and 913,986 CU for Token-2022. 1.4M is the per-transaction
443
+ * maximum and leaves headroom for the surge-fee path.
444
+ */
445
+ export const STLD_DEPOSIT_CU_LIMIT = 1_400_000;
446
+
447
+ export function buildSingleTokenDepositIx(
448
+ cfg: CofferConfig,
449
+ pool: PoolInfo,
450
+ params: SingleTokenDepositParams
451
+ ): TransactionInstruction {
452
+ // v5 / new contract: deposit_single_token args are
453
+ // (amount_in: u64, token_in_index: u8, minimum_bpt_amount: u64).
454
+ // The old per-call `slippage_hundredths_bps: u32` was removed — the single
455
+ // `minimum_bpt_amount` floor now bounds the whole zap (internal swaps +
456
+ // surge fees + join); per-leg swaps use min_out = 0.
457
+ if (pool.tokenCount > STLD_MAX_TOKENS) {
458
+ throw new Error(
459
+ `deposit_single_token: pool has ${pool.tokenCount} tokens, the zap supports at most ` +
460
+ `${STLD_MAX_TOKENS} (PoolTooLargeForZap). Use add_liquidity instead.`
461
+ );
462
+ }
463
+ assertTokensSupported(pool, "all", "deposit_single_token");
464
+ const minBpt = requirePositiveMinimumBpt(params.minimumBptAmount, "deposit_single_token");
465
+ const [helper] = deriveHelperPda(cfg.programs.singleTokenLiquidity, pool.address);
466
+ const helperBpt = deriveAta(helper, pool.bptMint, bptProgram(pool));
467
+ const userBpt = deriveAta(params.user, pool.bptMint, bptProgram(pool));
468
+
469
+ const data = Buffer.concat([
470
+ STLD_DISC.depositSingleToken,
471
+ encodeU64(params.amountIn),
472
+ encodeU8(params.tokenInIndex),
473
+ encodeU64(minBpt),
474
+ ]);
475
+
476
+ // stld remaining_accounts: [mint_i, user_ata_i, helper_ata_i, vault_i, tp_i] × N
477
+ const remaining: AccountMeta[] = [];
478
+ for (let i = 0; i < pool.tokenCount; i++) {
479
+ const t = pool.tokens[i];
480
+ const userAta = deriveAta(params.user, t.mint, t.tokenProgram);
481
+ const helperAta = deriveAta(helper, t.mint, t.tokenProgram);
482
+ remaining.push({ pubkey: t.mint, isSigner: false, isWritable: false });
483
+ remaining.push({ pubkey: userAta, isSigner: false, isWritable: true });
484
+ remaining.push({ pubkey: helperAta, isSigner: false, isWritable: true });
485
+ remaining.push({ pubkey: t.vault, isSigner: false, isWritable: true });
486
+ remaining.push({ pubkey: t.tokenProgram, isSigner: false, isWritable: false });
487
+ }
488
+
489
+ const keys: AccountMeta[] = [
490
+ { pubkey: pool.address, isSigner: false, isWritable: true },
491
+ { pubkey: helper, isSigner: false, isWritable: false },
492
+ { pubkey: pool.bptMint, isSigner: false, isWritable: true },
493
+ { pubkey: helperBpt, isSigner: false, isWritable: true },
494
+ { pubkey: userBpt, isSigner: false, isWritable: true },
495
+ { pubkey: params.user, isSigner: true, isWritable: true },
496
+ { pubkey: cfg.programs.cubicPool, isSigner: false, isWritable: false },
497
+ { pubkey: bptProgram(pool), isSigner: false, isWritable: false },
498
+ ...remaining,
499
+ ];
500
+
501
+ return new TransactionInstruction({
502
+ programId: cfg.programs.singleTokenLiquidity,
503
+ keys,
504
+ data,
505
+ });
506
+ }
507
+
508
+ /**
509
+ * The idempotent ATA creates a zap depends on: the user's and the helper's
510
+ * ATA for every pool token, plus both BPT ATAs. `2N + 2` instructions.
511
+ *
512
+ * The helper validates every user ATA up front because it may refund dust
513
+ * in any pool token, so these cannot be trimmed to just the input token.
514
+ */
515
+ export function buildSingleTokenDepositAtaIxs(
516
+ cfg: CofferConfig,
517
+ pool: PoolInfo,
518
+ user: PublicKey
519
+ ): TransactionInstruction[] {
520
+ const [helper] = deriveHelperPda(cfg.programs.singleTokenLiquidity, pool.address);
521
+ const helperBpt = deriveAta(helper, pool.bptMint, bptProgram(pool));
522
+ const userBpt = deriveAta(user, pool.bptMint, bptProgram(pool));
523
+
524
+ const ixs: TransactionInstruction[] = [];
525
+ // User + helper ATAs (helper has an off-curve owner). Idempotent — safe to
526
+ // include even when the accounts already exist.
527
+ for (const t of pool.tokens) {
528
+ ixs.push(
529
+ createAssociatedTokenAccountIdempotentInstruction(
530
+ user,
531
+ deriveAta(user, t.mint, t.tokenProgram),
532
+ user,
533
+ t.mint,
534
+ t.tokenProgram
535
+ )
536
+ );
537
+ ixs.push(
538
+ createAssociatedTokenAccountIdempotentInstruction(
539
+ user,
540
+ deriveAta(helper, t.mint, t.tokenProgram),
541
+ helper,
542
+ t.mint,
543
+ t.tokenProgram
544
+ )
545
+ );
546
+ }
547
+ ixs.push(
548
+ createAssociatedTokenAccountIdempotentInstruction(
549
+ user,
550
+ helperBpt,
551
+ helper,
552
+ pool.bptMint,
553
+ bptProgram(pool)
554
+ )
555
+ );
556
+ ixs.push(
557
+ createAssociatedTokenAccountIdempotentInstruction(
558
+ user,
559
+ userBpt,
560
+ user,
561
+ pool.bptMint,
562
+ bptProgram(pool)
563
+ )
564
+ );
565
+ return ixs;
566
+ }
567
+
568
+ /**
569
+ * Full single-token deposit in ONE transaction: idempotent ATA setup
570
+ * followed by the deposit itself.
571
+ *
572
+ * ⚠ **This only fits for small pools.** Two independent runtime ceilings
573
+ * bite as `N` grows, and neither is a byte-size problem an ALT can fix:
574
+ *
575
+ * - `MAX_INSTRUCTION_TRACE_LENGTH = 64` counts CPI frames across the whole
576
+ * transaction. One idempotent ATA create costs 5, and the v5.1 zap alone
577
+ * uses 52/64 at N=10. The `2N + 2` setup instructions cost `10N + 10`
578
+ * frames, so the combined transaction blows the limit well before N=10.
579
+ * - Raw size at N=10 is 1796 bytes (437 with the pool's ALT), over the
580
+ * 1232-byte legacy ceiling.
581
+ *
582
+ * For anything but a small pool use {@link buildSingleTokenDepositTxs},
583
+ * which splits setup and deposit into two transactions, and compile the
584
+ * deposit leg through the pool's ALT via `compileBuiltTx`.
585
+ */
586
+ export function buildSingleTokenDepositTx(
587
+ cfg: CofferConfig,
588
+ pool: PoolInfo,
589
+ params: SingleTokenDepositParams
590
+ ): BuiltTx {
591
+ const cuLimit = cfg.defaults.cuLimit ?? STLD_DEPOSIT_CU_LIMIT;
592
+ return {
593
+ instructions: [
594
+ ComputeBudgetProgram.setComputeUnitLimit({ units: cuLimit }),
595
+ ...buildSingleTokenDepositAtaIxs(cfg, pool, params.user),
596
+ buildSingleTokenDepositIx(cfg, pool, params),
597
+ ],
598
+ suggestedCuLimit: cuLimit,
599
+ };
600
+ }
601
+
602
+ /**
603
+ * Single-token deposit split into the two transactions large pools need.
604
+ *
605
+ * Send `setup` first and let it confirm, then send `deposit`. They are
606
+ * separated because the ATA creates and the zap compete for the same
607
+ * 64-frame instruction-trace budget (see {@link buildSingleTokenDepositTx}).
608
+ *
609
+ * `setup` is `null` when there is nothing to create — the caller can check
610
+ * ATA existence itself and skip the leg. Otherwise it is always safe to
611
+ * send: every instruction in it is idempotent.
612
+ *
613
+ * The `deposit` leg should be compiled as a v0 transaction against the
614
+ * pool's Address Lookup Table (`compileBuiltTx(conn, user, deposit, pool)`).
615
+ * At N=10 it is 1796 bytes raw and 437 with the ALT — the ALT is mandatory,
616
+ * not an optimisation. Note that an ALT cannot be referenced in the same
617
+ * slot it was extended, so a freshly-created table needs a slot to settle.
618
+ */
619
+ export function buildSingleTokenDepositTxs(
620
+ cfg: CofferConfig,
621
+ pool: PoolInfo,
622
+ params: SingleTokenDepositParams
623
+ ): { setup: BuiltTx | null; deposit: BuiltTx } {
624
+ const ataIxs = buildSingleTokenDepositAtaIxs(cfg, pool, params.user);
625
+ const cuLimit = cfg.defaults.cuLimit ?? STLD_DEPOSIT_CU_LIMIT;
626
+ // ~30k CU per idempotent create, with headroom.
627
+ const setupCu = Math.min(1_400_000, 50_000 * ataIxs.length);
628
+
629
+ return {
630
+ setup:
631
+ ataIxs.length === 0
632
+ ? null
633
+ : {
634
+ instructions: [
635
+ ComputeBudgetProgram.setComputeUnitLimit({ units: setupCu }),
636
+ ...ataIxs,
637
+ ],
638
+ suggestedCuLimit: setupCu,
639
+ },
640
+ deposit: {
641
+ instructions: [
642
+ ComputeBudgetProgram.setComputeUnitLimit({ units: cuLimit }),
643
+ buildSingleTokenDepositIx(cfg, pool, params),
644
+ ],
645
+ suggestedCuLimit: cuLimit,
646
+ },
647
+ };
648
+ }
649
+
650
+ // ============================================================
651
+ // Deploy new pool (PoolFactory)
652
+ // ============================================================
653
+
654
+ /**
655
+ * Raw `cubic_pool::initialize_config`.
656
+ *
657
+ * ⚠ **This instruction is no longer directly callable from a wallet.** In
658
+ * v5.1 (audit H-2) the `protocol_admin: Pubkey` ARGUMENT was removed and
659
+ * the Treasury PDA account became a `Signer`. The config's admin is now
660
+ * whoever signs, which by construction can only be protocol-admin's
661
+ * Treasury PDA via `invoke_signed` — no keypair exists for it.
662
+ *
663
+ * Sending this instruction from a wallet fails signature verification. The
664
+ * only working path is `AdminClient.poolInitializeConfigIx()`, which calls
665
+ * `protocol_admin::pool_initialize_config` and CPIs in here. This builder
666
+ * is kept so the wire format stays documented and testable, and so the
667
+ * treasury meta carries `isSigner: true` — Anchor derives CPI account
668
+ * metas from the field type, and emitting it as a non-signer would strand
669
+ * the outer `invoke_signed` signature.
670
+ */
671
+ export function buildInitializeConfigIx(
672
+ cfg: CofferConfig,
673
+ params: { config: PublicKey; payer: PublicKey; defaultProtocolFeeRate: number }
674
+ ): TransactionInstruction {
675
+ const [treasuryPda] = PublicKey.findProgramAddressSync(
676
+ [Buffer.from("treasury")],
677
+ cfg.programs.protocolAdmin
678
+ );
679
+ // Args are `(default_protocol_fee_rate: u16)` and nothing else. The
680
+ // leading 32-byte `protocol_admin` pubkey this used to encode is gone —
681
+ // leaving it in makes the program read the fee rate out of the middle of
682
+ // a pubkey.
683
+ const data = Buffer.concat([
684
+ computeDiscriminator("initialize_config"),
685
+ encodeU16(params.defaultProtocolFeeRate),
686
+ ]);
687
+ const keys: AccountMeta[] = [
688
+ { pubkey: params.config, isSigner: true, isWritable: true },
689
+ { pubkey: treasuryPda, isSigner: true, isWritable: false },
690
+ { pubkey: params.payer, isSigner: true, isWritable: true },
691
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
692
+ ];
693
+ return new TransactionInstruction({
694
+ programId: cfg.programs.cubicPool,
695
+ keys,
696
+ data,
697
+ });
698
+ }
699
+
700
+ /**
701
+ * `protocol_admin::pool_initialize_config` — the ONLY way to create a
702
+ * `CubicPoolConfig` that a wallet can actually sign.
703
+ *
704
+ * `cubic_pool::initialize_config` takes the Treasury PDA as a `Signer`
705
+ * (audit H-01: an unauthenticated config let anyone open a pool that charged
706
+ * a swap fee and paid the protocol nothing). A PDA cannot sign a transaction
707
+ * — only the program that owns its seeds can, via `invoke_signed`. So the
708
+ * direct instruction is unreachable from a wallet by design, and every caller
709
+ * has to come through this wrapper, where the Treasury `admin` is the human
710
+ * signer and protocol-admin performs the CPI.
711
+ *
712
+ * Signers: `admin` (must equal `Treasury.admin`) and `config` (a freshly
713
+ * generated keypair — the account is `init`, not a PDA).
714
+ */
715
+ export function buildPoolInitializeConfigIx(
716
+ cfg: CofferConfig,
717
+ params: { config: PublicKey; admin: PublicKey; defaultProtocolFeeRate: number }
718
+ ): TransactionInstruction {
719
+ const [treasuryPda] = PublicKey.findProgramAddressSync(
720
+ [Buffer.from("treasury")],
721
+ cfg.programs.protocolAdmin
722
+ );
723
+ const data = Buffer.concat([
724
+ PROTOCOL_ADMIN_DISC.poolInitializeConfig,
725
+ encodeU16(params.defaultProtocolFeeRate),
726
+ ]);
727
+ const keys: AccountMeta[] = [
728
+ { pubkey: treasuryPda, isSigner: false, isWritable: false },
729
+ { pubkey: params.admin, isSigner: true, isWritable: true },
730
+ { pubkey: params.config, isSigner: true, isWritable: true },
731
+ { pubkey: cfg.programs.cubicPool, isSigner: false, isWritable: false },
732
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
733
+ ];
734
+ return new TransactionInstruction({
735
+ programId: cfg.programs.protocolAdmin,
736
+ keys,
737
+ data,
738
+ });
739
+ }
740
+
741
+ export function buildInitializeCubicPoolIx(
742
+ cfg: CofferConfig,
743
+ params: DeployPoolParams
744
+ ): TransactionInstruction {
745
+ const tokenProgram = params.bptTokenProgram ?? TOKEN_PROGRAM_ID;
746
+ const [pool] = PublicKey.findProgramAddressSync(
747
+ [Buffer.from("cubic_pool"), params.configKey.toBuffer(), params.poolId.toArrayLike(Buffer, "le", 8)],
748
+ cfg.programs.cubicPool
749
+ );
750
+ const [bptMint] = deriveBptMint(cfg.programs.cubicPool, pool);
751
+
752
+ const data = Buffer.concat([
753
+ CUBIC_POOL_DISC.initializeCubicPool,
754
+ encodeVecU64(params.weightsBps.map((w) => new BN(w))),
755
+ encodeVecU64(params.virtualBalances),
756
+ encodeU32(params.swapFeeRate),
757
+ encodeU64(params.poolId),
758
+ // Option<u64> banned_extensions_override: 0x00 = None (inherit config
759
+ // default), 0x01 + u64 LE = Some(bitmap) chosen by the creator.
760
+ encodeOptionU64(params.bannedExtensions),
761
+ ]);
762
+
763
+ // There is no `tokens` ix arg — the pool's token set is defined SOLELY by
764
+ // `params.tokens` via remaining_accounts (one mint per token, in the same
765
+ // order as `weightsBps`/`virtualBalances`).
766
+ const remaining: AccountMeta[] = params.tokens.map((m) => ({
767
+ pubkey: m,
768
+ isSigner: false,
769
+ isWritable: false,
770
+ }));
771
+
772
+ const keys: AccountMeta[] = [
773
+ { pubkey: params.configKey, isSigner: false, isWritable: false },
774
+ { pubkey: pool, isSigner: false, isWritable: true },
775
+ { pubkey: bptMint, isSigner: false, isWritable: true },
776
+ { pubkey: params.payer, isSigner: true, isWritable: true },
777
+ { pubkey: tokenProgram, isSigner: false, isWritable: false },
778
+ { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
779
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
780
+ ...remaining,
781
+ ];
782
+
783
+ return new TransactionInstruction({
784
+ programId: cfg.programs.cubicPool,
785
+ keys,
786
+ data,
787
+ });
788
+ }
789
+
790
+ export function buildDeployPoolTx(cfg: CofferConfig, params: DeployPoolParams): BuiltTx {
791
+ return {
792
+ instructions: [
793
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
794
+ buildInitializeCubicPoolIx(cfg, params),
795
+ ],
796
+ suggestedCuLimit: 400_000,
797
+ };
798
+ }
799
+
800
+ // ============================================================
801
+ // initialize_pool_alt — per-pool Address Lookup Table
802
+ // ============================================================
803
+
804
+ export interface InitializePoolAltParams {
805
+ /** Pool PDA. */
806
+ pool: PublicKey;
807
+ /** `CubicPoolConfig` account this pool is pinned to. Read by the
808
+ * program to gate the alternative-authority path (`config.protocol_admin`). */
809
+ config: PublicKey;
810
+ /** Authority. Must equal either `pool.pool_admin` (per-pool owner
811
+ * path) or `config.protocol_admin` (Treasury PDA via the
812
+ * protocol-admin CPI wrapper). The ALT address is derived from
813
+ * `[authority, recent_slot]`. */
814
+ authority: PublicKey;
815
+ /** Rent payer. Decoupled from `authority` so the wrapper path can
816
+ * pay from a regular wallet while Treasury PDA acts as authority.
817
+ * In the pool-admin path pass the same key for both. */
818
+ payer: PublicKey;
819
+ /**
820
+ * Recent slot. Used by the upstream ALT program to derive the table
821
+ * address as `[authority, recent_slot]`. Must be a slot the runtime
822
+ * still has in slot-hashes — caller should pass
823
+ * `await connection.getSlot('finalized')`.
824
+ */
825
+ recentSlot: BN;
826
+ }
827
+
828
+ /**
829
+ * Re-derives the Address Lookup Table address from `(authority, recent_slot)`
830
+ * the same way the upstream ALT program does.
831
+ *
832
+ * `@solana/web3.js` v1 doesn't expose `deriveLookupTableAddress` as a static,
833
+ * so we inline the seed derivation.
834
+ */
835
+ export function deriveAltAddress(authority: PublicKey, recentSlot: BN): PublicKey {
836
+ const slotBuf = recentSlot.toArrayLike(Buffer, "le", 8);
837
+ const [addr] = PublicKey.findProgramAddressSync(
838
+ [authority.toBuffer(), slotBuf],
839
+ AddressLookupTableProgram.programId,
840
+ );
841
+ return addr;
842
+ }
843
+
844
+ /**
845
+ * Build the on-chain `initialize_pool_alt` instruction. Performs three
846
+ * upstream CPIs inside the program: create + extend + freeze. After
847
+ * this ix lands, the ALT is immutable and the pool's `lookup_table`
848
+ * field points at it.
849
+ *
850
+ * Caller must NOT use the ALT in the same slot (warmup) — wait at
851
+ * least one slot before sending any v0 tx that references it.
852
+ */
853
+ export function buildInitializePoolAltIx(
854
+ cfg: CofferConfig,
855
+ params: InitializePoolAltParams,
856
+ ): TransactionInstruction {
857
+ const altAddr = deriveAltAddress(params.authority, params.recentSlot);
858
+
859
+ const data = Buffer.concat([
860
+ CUBIC_POOL_DISC.initializePoolAlt,
861
+ encodeU64(params.recentSlot),
862
+ ]);
863
+
864
+ // Account order MUST match cubic-pool's InitializePoolAlt context:
865
+ // pool, config, authority, payer, lookup_table, system_program, alt_program.
866
+ const keys: AccountMeta[] = [
867
+ { pubkey: params.pool, isSigner: false, isWritable: true },
868
+ { pubkey: params.config, isSigner: false, isWritable: false },
869
+ { pubkey: params.authority, isSigner: true, isWritable: false },
870
+ { pubkey: params.payer, isSigner: true, isWritable: true },
871
+ { pubkey: altAddr, isSigner: false, isWritable: true },
872
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
873
+ { pubkey: AddressLookupTableProgram.programId, isSigner: false, isWritable: false },
874
+ ];
875
+
876
+ return new TransactionInstruction({
877
+ programId: cfg.programs.cubicPool,
878
+ keys,
879
+ data,
880
+ });
881
+ }
882
+
883
+ export function buildInitializePoolAltTx(
884
+ cfg: CofferConfig,
885
+ params: InitializePoolAltParams,
886
+ ): BuiltTx & { lookupTable: PublicKey } {
887
+ return {
888
+ instructions: [
889
+ ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
890
+ buildInitializePoolAltIx(cfg, params),
891
+ ],
892
+ suggestedCuLimit: 200_000,
893
+ lookupTable: deriveAltAddress(params.authority, params.recentSlot),
894
+ };
895
+ }
896
+
897
+ // ============================================================
898
+ // Range manager
899
+ // ============================================================
900
+
901
+ /**
902
+ * `set_range_manager` — appoint or disable a pool's range manager.
903
+ *
904
+ * ⚠ v5.1 (audit L-1) PREPENDED a `config` account before `pool`. The
905
+ * account list is `[config, pool, authority]`. A client still sending
906
+ * `[pool, authority]` has the program read the pool account as the config
907
+ * and fail on the discriminator, or worse pass a lookalike.
908
+ *
909
+ * Pool admin can appoint or change the manager. The contract also accepts a
910
+ * config protocol-admin signer for disable-only updates that retain the current
911
+ * manager key; the deployed protocol-admin program has no CPI wrapper for it. Combine
912
+ * `newManager = PublicKey.default` with `enabled = false` for the fully
913
+ * disabled state.
914
+ */
915
+ export function buildSetRangeManagerIx(
916
+ cfg: CofferConfig,
917
+ pool: PublicKey,
918
+ params: SetRangeManagerParams,
919
+ ): TransactionInstruction {
920
+ const data = Buffer.concat([
921
+ CUBIC_POOL_DISC.setRangeManager,
922
+ params.newManager.toBuffer(),
923
+ encodeBool(params.enabled),
924
+ ]);
925
+ const keys: AccountMeta[] = [
926
+ { pubkey: params.config, isSigner: false, isWritable: false },
927
+ { pubkey: pool, isSigner: false, isWritable: true },
928
+ { pubkey: params.authority, isSigner: true, isWritable: false },
929
+ ];
930
+ return new TransactionInstruction({ programId: cfg.programs.cubicPool, keys, data });
931
+ }
932
+
933
+ /**
934
+ * `set_range_manager_config` — the range manager's movement envelope.
935
+ *
936
+ * v5.1 appended `max_leverage_bps` and `min_leverage_bps` to the argument
937
+ * list. Both are required; pass `0` to leave a bound disabled.
938
+ */
939
+ export function buildSetRangeManagerConfigIx(
940
+ cfg: CofferConfig,
941
+ pool: PublicKey,
942
+ params: SetRangeManagerConfigParams,
943
+ ): TransactionInstruction {
944
+ const data = Buffer.concat([
945
+ CUBIC_POOL_DISC.setRangeManagerConfig,
946
+ encodeU16(params.maxVbChangePct),
947
+ encodeU16(params.maxWeightChangePct),
948
+ encodeU32(params.minUpdateIntervalSecs),
949
+ encodeU32(params.maxLeverageBps),
950
+ encodeU32(params.minLeverageBps),
951
+ ]);
952
+ const keys: AccountMeta[] = [
953
+ { pubkey: pool, isSigner: false, isWritable: true },
954
+ { pubkey: params.authority, isSigner: true, isWritable: false },
955
+ ];
956
+ return new TransactionInstruction({ programId: cfg.programs.cubicPool, keys, data });
957
+ }
958
+
959
+ /**
960
+ * `range_manager_update` — sparse compare-and-swap over virtual balances
961
+ * and/or weights.
962
+ *
963
+ * Every {@link TokenChange} carries an `expectedCurrent` guard (v5.1, audit
964
+ * M-6). The program reverts the whole instruction with
965
+ * `RangeManagerStaleValue` (6061) if any entry no longer matches the stored
966
+ * value, so a bot MUST derive `expectedCurrent` and `newValue` from the
967
+ * same pool read and retry from a fresh read on failure — never re-send the
968
+ * same payload.
969
+ *
970
+ * The instruction also honours the pool kill switch now: it reverts while
971
+ * `pool_enabled` is false.
972
+ */
973
+ export function buildRangeManagerUpdateIx(
974
+ cfg: CofferConfig,
975
+ pool: PublicKey,
976
+ params: RangeManagerUpdateParams,
977
+ ): TransactionInstruction {
978
+ const vbChanges = params.vbChanges ?? [];
979
+ const weightChanges = params.weightChanges ?? [];
980
+ if (vbChanges.length === 0 && weightChanges.length === 0) {
981
+ // `RangeManagerEmptyUpdate` (6044) — cheaper to catch here.
982
+ throw new Error("range_manager_update: must change at least one virtual balance or weight");
983
+ }
984
+ const data = Buffer.concat([
985
+ CUBIC_POOL_DISC.rangeManagerUpdate,
986
+ encodeVecTokenChange(vbChanges),
987
+ encodeVecTokenChange(weightChanges),
988
+ ]);
989
+ const keys: AccountMeta[] = [
990
+ { pubkey: pool, isSigner: false, isWritable: true },
991
+ { pubkey: params.authority, isSigner: true, isWritable: false },
992
+ ];
993
+ return new TransactionInstruction({ programId: cfg.programs.cubicPool, keys, data });
994
+ }
995
+
996
+ /** Set the pool-local swap fee (hundredths of a basis point). */
997
+ export function buildSetSwapFeeRateIx(cfg: CofferConfig, pool: PublicKey, authority: PublicKey, swapFeeRate: number): TransactionInstruction {
998
+ return buildContractInstruction(cfg, "cubicPool", "set_swap_fee_rate", { swap_fee_rate: swapFeeRate }, { pool, authority });
999
+ }
1000
+
1001
+ /** Replace every token's sell-off policy, including both kink parameters. */
1002
+ export function buildSetMaxSelloffIx(cfg: CofferConfig, pool: PublicKey, authority: PublicKey, params: SelloffParams[]): TransactionInstruction {
1003
+ return buildContractInstruction(cfg, "cubicPool", "set_max_selloff", { params: params.map(p => ({
1004
+ max_selloff_pct: p.maxSelloffPct, period_length: p.periodLength,
1005
+ fee_threshold_pct: p.feeThresholdPct, fee_slope_low_pct: p.feeSlopeLowPct,
1006
+ fee_slope_high_pct: p.feeSlopeHighPct, fee_slope_mid_pct: p.feeSlopeMidPct,
1007
+ fee_kink_pct: p.feeKinkPct,
1008
+ })) }, { pool, authority });
1009
+ }
1010
+
1011
+ export function buildInitiatePoolAdminTransferIx(cfg: CofferConfig, pool: PublicKey, authority: PublicKey, newAdmin: PublicKey): TransactionInstruction {
1012
+ return buildContractInstruction(cfg, "cubicPool", "initiate_pool_admin_transfer", { new_admin: newAdmin }, { pool, authority });
1013
+ }
1014
+
1015
+ export function buildAcceptPoolAdminTransferIx(cfg: CofferConfig, pool: PublicKey, newAdmin: PublicKey): TransactionInstruction {
1016
+ return buildContractInstruction(cfg, "cubicPool", "accept_pool_admin_transfer", {}, { pool, new_admin: newAdmin });
1017
+ }
1018
+
1019
+ export function buildCancelPoolAdminTransferIx(cfg: CofferConfig, pool: PublicKey, authority: PublicKey): TransactionInstruction {
1020
+ return buildContractInstruction(cfg, "cubicPool", "cancel_pool_admin_transfer", {}, { pool, authority });
1021
+ }
1022
+
1023
+ /** Permanently renounce the pool-local admin role. */
1024
+ export function buildDisablePoolAdminIx(cfg: CofferConfig, pool: PublicKey, authority: PublicKey): TransactionInstruction {
1025
+ return buildContractInstruction(cfg, "cubicPool", "disable_pool_admin", {}, { pool, authority });
1026
+ }
1027
+
1028
+ export function buildGetPoolInfoIx(cfg: CofferConfig, pool: PublicKey): TransactionInstruction {
1029
+ return buildContractInstruction(cfg, "cubicPool", "get_pool_info", {}, { pool });
1030
+ }
1031
+
1032
+ // ============================================================
1033
+ // Borsh encoding helpers (subset used above)
1034
+ // ============================================================
1035
+
1036
+ function encodeU8(v: number): Buffer {
1037
+ const b = Buffer.alloc(1);
1038
+ requireUnsignedNumber(v, 8);
1039
+ b.writeUInt8(v, 0);
1040
+ return b;
1041
+ }
1042
+ function encodeU16(v: number): Buffer {
1043
+ const b = Buffer.alloc(2);
1044
+ requireUnsignedNumber(v, 16);
1045
+ b.writeUInt16LE(v, 0);
1046
+ return b;
1047
+ }
1048
+ function encodeU32(v: number): Buffer {
1049
+ const b = Buffer.alloc(4);
1050
+ requireUnsignedNumber(v, 32);
1051
+ b.writeUInt32LE(v, 0);
1052
+ return b;
1053
+ }
1054
+ function requireUnsignedNumber(value: number, bits: number): void {
1055
+ if (!Number.isSafeInteger(value) || value < 0 || value >= 2 ** bits) {
1056
+ throw new Error(`Expected an unsigned ${bits}-bit integer`);
1057
+ }
1058
+ }
1059
+ function encodeU64(v: BN): Buffer {
1060
+ if (!BN.isBN(v) || v.isNeg() || v.bitLength() > 64) throw new Error("Expected an unsigned 64-bit BN");
1061
+ return v.toArrayLike(Buffer, "le", 8);
1062
+ }
1063
+ /** Borsh `Option<u64>`: 1 tag byte (0=None, 1=Some) + u64 LE when Some. */
1064
+ function encodeOptionU64(v?: BN | number | null): Buffer {
1065
+ if (v === undefined || v === null) return Buffer.from([0]);
1066
+ if (typeof v === "number" && (!Number.isSafeInteger(v) || v < 0)) throw new Error("Bitmap must be a non-negative safe integer or BN");
1067
+ const bn = BN.isBN(v) ? v : new BN(v);
1068
+ return Buffer.concat([Buffer.from([1]), encodeU64(bn)]);
1069
+ }
1070
+ function encodeVecU64(vs: BN[]): Buffer {
1071
+ const len = Buffer.alloc(4);
1072
+ len.writeUInt32LE(vs.length, 0);
1073
+ return Buffer.concat([len, ...vs.map(encodeU64)]);
1074
+ }
1075
+ function encodeBool(v: boolean): Buffer {
1076
+ if (typeof v !== "boolean") throw new Error("Expected boolean");
1077
+ return Buffer.from([v ? 1 : 0]);
1078
+ }
1079
+ /**
1080
+ * Borsh `TokenChange { index: u8, expected_current: u64, new_value: u64 }`.
1081
+ *
1082
+ * Field order verified against the `TokenChange` type in
1083
+ * `src/idl/cubic_pool.json`. `expected_current` sits IN THE MIDDLE — it was
1084
+ * inserted between `index` and `new_value`, not appended. Encoding it last
1085
+ * produces a 17-byte record the program happily parses as
1086
+ * `(index, new_value=expected, expected=new)`, which either reverts as
1087
+ * stale or writes the wrong value.
1088
+ */
1089
+ function encodeTokenChange(c: TokenChange): Buffer {
1090
+ return Buffer.concat([encodeU8(c.index), encodeU64(c.expectedCurrent), encodeU64(c.newValue)]);
1091
+ }
1092
+ function encodeVecTokenChange(cs: TokenChange[]): Buffer {
1093
+ const len = Buffer.alloc(4);
1094
+ len.writeUInt32LE(cs.length, 0);
1095
+ return Buffer.concat([len, ...cs.map(encodeTokenChange)]);
1096
+ }