@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.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/dist/clients/AdminClient.d.ts +194 -0
- package/dist/clients/AdminClient.d.ts.map +1 -0
- package/dist/clients/AdminClient.js +482 -0
- package/dist/clients/AdminClient.js.map +1 -0
- package/dist/clients/CofferBackendClient.d.ts +877 -0
- package/dist/clients/CofferBackendClient.d.ts.map +1 -0
- package/dist/clients/CofferBackendClient.js +536 -0
- package/dist/clients/CofferBackendClient.js.map +1 -0
- package/dist/clients/CubeBackendClient.d.ts +877 -0
- package/dist/clients/CubeBackendClient.d.ts.map +1 -0
- package/dist/clients/CubeBackendClient.js +536 -0
- package/dist/clients/CubeBackendClient.js.map +1 -0
- package/dist/clients/CubicPoolClient.d.ts +120 -0
- package/dist/clients/CubicPoolClient.d.ts.map +1 -0
- package/dist/clients/CubicPoolClient.js +687 -0
- package/dist/clients/CubicPoolClient.js.map +1 -0
- package/dist/clients/PoolFactoryClient.d.ts +53 -0
- package/dist/clients/PoolFactoryClient.d.ts.map +1 -0
- package/dist/clients/PoolFactoryClient.js +91 -0
- package/dist/clients/PoolFactoryClient.js.map +1 -0
- package/dist/clients/RpcClient.d.ts +49 -0
- package/dist/clients/RpcClient.d.ts.map +1 -0
- package/dist/clients/RpcClient.js +135 -0
- package/dist/clients/RpcClient.js.map +1 -0
- package/dist/clients/SingleTokenDepositClient.d.ts +67 -0
- package/dist/clients/SingleTokenDepositClient.d.ts.map +1 -0
- package/dist/clients/SingleTokenDepositClient.js +105 -0
- package/dist/clients/SingleTokenDepositClient.js.map +1 -0
- package/dist/clients/contract-instructions.d.ts +24 -0
- package/dist/clients/contract-instructions.d.ts.map +1 -0
- package/dist/clients/contract-instructions.js +130 -0
- package/dist/clients/contract-instructions.js.map +1 -0
- package/dist/clients/index.d.ts +10 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +26 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/quote-math.d.ts +15 -0
- package/dist/clients/quote-math.d.ts.map +1 -0
- package/dist/clients/quote-math.js +98 -0
- package/dist/clients/quote-math.js.map +1 -0
- package/dist/clients/tx-builders.d.ts +231 -0
- package/dist/clients/tx-builders.d.ts.map +1 -0
- package/dist/clients/tx-builders.js +861 -0
- package/dist/clients/tx-builders.js.map +1 -0
- package/dist/clients/versioned.d.ts +34 -0
- package/dist/clients/versioned.d.ts.map +1 -0
- package/dist/clients/versioned.js +55 -0
- package/dist/clients/versioned.js.map +1 -0
- package/dist/config/index.d.ts +65 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +75 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/networks.d.ts +12 -0
- package/dist/config/networks.d.ts.map +1 -0
- package/dist/config/networks.js +51 -0
- package/dist/config/networks.js.map +1 -0
- package/dist/config/tokens.d.ts +19 -0
- package/dist/config/tokens.d.ts.map +1 -0
- package/dist/config/tokens.js +43 -0
- package/dist/config/tokens.js.map +1 -0
- package/dist/idl/cubic_pool.json +4203 -0
- package/dist/idl/index.d.ts +1769 -0
- package/dist/idl/index.d.ts.map +1 -0
- package/dist/idl/index.js +18 -0
- package/dist/idl/index.js.map +1 -0
- package/dist/idl/protocol_admin.json +2615 -0
- package/dist/idl/single_token_liquidity.json +1121 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +206 -0
- package/dist/index.js.map +1 -0
- package/dist/math/cubicMath.d.ts +40 -0
- package/dist/math/cubicMath.d.ts.map +1 -0
- package/dist/math/cubicMath.js +82 -0
- package/dist/math/cubicMath.js.map +1 -0
- package/dist/math/fixedPoint.d.ts +19 -0
- package/dist/math/fixedPoint.d.ts.map +1 -0
- package/dist/math/fixedPoint.js +86 -0
- package/dist/math/fixedPoint.js.map +1 -0
- package/dist/math/index.d.ts +9 -0
- package/dist/math/index.d.ts.map +1 -0
- package/dist/math/index.js +30 -0
- package/dist/math/index.js.map +1 -0
- package/dist/math/logExp.d.ts +6 -0
- package/dist/math/logExp.d.ts.map +1 -0
- package/dist/math/logExp.js +68 -0
- package/dist/math/logExp.js.map +1 -0
- package/dist/math/lpLoss.d.ts +44 -0
- package/dist/math/lpLoss.d.ts.map +1 -0
- package/dist/math/lpLoss.js +103 -0
- package/dist/math/lpLoss.js.map +1 -0
- package/dist/math/maxSelloff.d.ts +164 -0
- package/dist/math/maxSelloff.d.ts.map +1 -0
- package/dist/math/maxSelloff.js +397 -0
- package/dist/math/maxSelloff.js.map +1 -0
- package/dist/math/singleToken.d.ts +54 -0
- package/dist/math/singleToken.d.ts.map +1 -0
- package/dist/math/singleToken.js +188 -0
- package/dist/math/singleToken.js.map +1 -0
- package/dist/math/slippage.d.ts +13 -0
- package/dist/math/slippage.d.ts.map +1 -0
- package/dist/math/slippage.js +44 -0
- package/dist/math/slippage.js.map +1 -0
- package/dist/math/surgeFee.d.ts +24 -0
- package/dist/math/surgeFee.d.ts.map +1 -0
- package/dist/math/surgeFee.js +85 -0
- package/dist/math/surgeFee.js.map +1 -0
- package/dist/math/weightedMath.d.ts +20 -0
- package/dist/math/weightedMath.d.ts.map +1 -0
- package/dist/math/weightedMath.js +66 -0
- package/dist/math/weightedMath.js.map +1 -0
- package/dist/parsers/borsh.d.ts +28 -0
- package/dist/parsers/borsh.d.ts.map +1 -0
- package/dist/parsers/borsh.js +104 -0
- package/dist/parsers/borsh.js.map +1 -0
- package/dist/parsers/contracts.d.ts +26 -0
- package/dist/parsers/contracts.d.ts.map +1 -0
- package/dist/parsers/contracts.js +62 -0
- package/dist/parsers/contracts.js.map +1 -0
- package/dist/parsers/events.d.ts +3 -0
- package/dist/parsers/events.d.ts.map +1 -0
- package/dist/parsers/events.js +281 -0
- package/dist/parsers/events.js.map +1 -0
- package/dist/parsers/index.d.ts +6 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +22 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/mintAccount.d.ts +24 -0
- package/dist/parsers/mintAccount.d.ts.map +1 -0
- package/dist/parsers/mintAccount.js +27 -0
- package/dist/parsers/mintAccount.js.map +1 -0
- package/dist/parsers/poolAccount.d.ts +141 -0
- package/dist/parsers/poolAccount.d.ts.map +1 -0
- package/dist/parsers/poolAccount.js +224 -0
- package/dist/parsers/poolAccount.js.map +1 -0
- package/dist/types/contracts.d.ts +1340 -0
- package/dist/types/contracts.d.ts.map +1 -0
- package/dist/types/contracts.js +3 -0
- package/dist/types/contracts.js.map +1 -0
- package/dist/types/events.d.ts +172 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +3 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +22 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/pool.d.ts +149 -0
- package/dist/types/pool.d.ts.map +1 -0
- package/dist/types/pool.js +3 -0
- package/dist/types/pool.js.map +1 -0
- package/dist/types/result.d.ts +24 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +11 -0
- package/dist/types/result.js.map +1 -0
- package/dist/types/tx.d.ts +261 -0
- package/dist/types/tx.d.ts.map +1 -0
- package/dist/types/tx.js +3 -0
- package/dist/types/tx.js.map +1 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +192 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/extensions.d.ts +98 -0
- package/dist/utils/extensions.d.ts.map +1 -0
- package/dist/utils/extensions.js +187 -0
- package/dist/utils/extensions.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/pda.d.ts +8 -0
- package/dist/utils/pda.d.ts.map +1 -0
- package/dist/utils/pda.js +27 -0
- package/dist/utils/pda.js.map +1 -0
- package/dist/utils/retry.d.ts +22 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +62 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +54 -0
- package/src/clients/AdminClient.ts +608 -0
- package/src/clients/CofferBackendClient.ts +1413 -0
- package/src/clients/CubicPoolClient.ts +719 -0
- package/src/clients/PoolFactoryClient.ts +110 -0
- package/src/clients/RpcClient.ts +195 -0
- package/src/clients/SingleTokenDepositClient.ts +143 -0
- package/src/clients/contract-instructions.ts +115 -0
- package/src/clients/index.ts +9 -0
- package/src/clients/quote-math.ts +81 -0
- package/src/clients/tx-builders.ts +1096 -0
- package/src/clients/versioned.ts +80 -0
- package/src/config/index.ts +111 -0
- package/src/config/networks.ts +71 -0
- package/src/config/tokens.ts +52 -0
- package/src/idl/cubic_pool.json +4203 -0
- package/src/idl/index.ts +13 -0
- package/src/idl/protocol_admin.json +2615 -0
- package/src/idl/single_token_liquidity.json +1121 -0
- package/src/index.ts +262 -0
- package/src/math/cubicMath.ts +92 -0
- package/src/math/fixedPoint.ts +71 -0
- package/src/math/index.ts +13 -0
- package/src/math/logExp.ts +61 -0
- package/src/math/lpLoss.ts +160 -0
- package/src/math/maxSelloff.ts +564 -0
- package/src/math/singleToken.ts +251 -0
- package/src/math/slippage.ts +38 -0
- package/src/math/surgeFee.ts +101 -0
- package/src/math/weightedMath.ts +64 -0
- package/src/parsers/borsh.ts +106 -0
- package/src/parsers/contracts.ts +64 -0
- package/src/parsers/events.ts +275 -0
- package/src/parsers/index.ts +5 -0
- package/src/parsers/mintAccount.ts +44 -0
- package/src/parsers/poolAccount.ts +372 -0
- package/src/types/contracts.ts +238 -0
- package/src/types/events.ts +198 -0
- package/src/types/index.ts +5 -0
- package/src/types/pool.ts +147 -0
- package/src/types/result.ts +51 -0
- package/src/types/tx.ts +278 -0
- package/src/utils/errors.ts +210 -0
- package/src/utils/extensions.ts +196 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/pda.ts +58 -0
- 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
|
+
}
|