@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,719 @@
|
|
|
1
|
+
import { simulatePoolSwap, quoteTimestamp, u64 } from "./quote-math";
|
|
2
|
+
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
|
|
3
|
+
import { PublicKey, Commitment, SYSVAR_CLOCK_PUBKEY } from "@solana/web3.js";
|
|
4
|
+
import BN from "bn.js";
|
|
5
|
+
import { CofferConfig } from "../config";
|
|
6
|
+
import { PoolInfo, PoolTokenInfo } from "../types/pool";
|
|
7
|
+
import { SdkResult, err, ok } from "../types/result";
|
|
8
|
+
import { AddLiquidityQuote, SwapQuote, SingleTokenDepositQuote } from "../types/tx";
|
|
9
|
+
import { CubicPoolEvent } from "../types/events";
|
|
10
|
+
import { RpcClient } from "./RpcClient";
|
|
11
|
+
import { decodePoolAccount, RawPoolAccount } from "../parsers/poolAccount";
|
|
12
|
+
import { decodeMintAccount } from "../parsers/mintAccount";
|
|
13
|
+
import { parseCubicPoolEvents } from "../parsers/events";
|
|
14
|
+
import { deriveAta, deriveBptMint, deriveHelperPda } from "../utils/pda";
|
|
15
|
+
import { resolveKnownToken } from "../config/tokens";
|
|
16
|
+
import { describeUnsupportedToken, unsupportedMintExtensions } from "../utils/extensions";
|
|
17
|
+
import {
|
|
18
|
+
calcBptOutGivenExactTokensIn,
|
|
19
|
+
calcTokensOutGivenBptIn,
|
|
20
|
+
} from "../math/cubicMath";
|
|
21
|
+
import { divDown, mulDown } from "../math/fixedPoint";
|
|
22
|
+
import { calculateInvariant } from "../math/weightedMath";
|
|
23
|
+
import { rescaleSelloffWindow } from "../math/maxSelloff";
|
|
24
|
+
import { applySlippage, priceImpactHbps } from "../math/slippage";
|
|
25
|
+
import { capDepositAmountsToLpRatio, computeAllocations } from "../math/singleToken";
|
|
26
|
+
import {
|
|
27
|
+
calcSegmentedSurgeFeeAmount,
|
|
28
|
+
calcSurgeFeePct,
|
|
29
|
+
computeSelloffWindow,
|
|
30
|
+
projectSelloffWindow,
|
|
31
|
+
SelloffWindowStatus,
|
|
32
|
+
} from "../math/maxSelloff";
|
|
33
|
+
import { PERCENT_SCALE, SWAP_FEE_PRECISION } from "../config";
|
|
34
|
+
import {
|
|
35
|
+
buildAddLiquidityTx,
|
|
36
|
+
buildRemoveLiquidityTx,
|
|
37
|
+
buildSingleTokenDepositTx,
|
|
38
|
+
buildSingleTokenDepositTxs,
|
|
39
|
+
buildSwapTx,
|
|
40
|
+
} from "./tx-builders";
|
|
41
|
+
import {
|
|
42
|
+
AddLiquidityParams,
|
|
43
|
+
BuiltTx,
|
|
44
|
+
RemoveLiquidityParams,
|
|
45
|
+
SingleTokenDepositParams,
|
|
46
|
+
SwapParams,
|
|
47
|
+
} from "../types/tx";
|
|
48
|
+
import { SingleTokenDepositClient } from "./SingleTokenDepositClient";
|
|
49
|
+
|
|
50
|
+
export interface CubicPoolClientParams {
|
|
51
|
+
config: CofferConfig;
|
|
52
|
+
poolAddress: PublicKey;
|
|
53
|
+
rpc?:
|
|
54
|
+
| RpcClient
|
|
55
|
+
| {
|
|
56
|
+
endpoint?: string;
|
|
57
|
+
endpoints?: string[];
|
|
58
|
+
fallbackEndpoints?: string[];
|
|
59
|
+
apiKey?: string;
|
|
60
|
+
commitment?: Commitment;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Per-pool client. Fetch the state with `sync()`, then call any of the
|
|
67
|
+
* quote / buildTx methods off the cached snapshot.
|
|
68
|
+
*/
|
|
69
|
+
export class CubicPoolClient {
|
|
70
|
+
readonly config: CofferConfig;
|
|
71
|
+
readonly poolAddress: PublicKey;
|
|
72
|
+
readonly rpc: RpcClient;
|
|
73
|
+
|
|
74
|
+
private cache: PoolInfo | undefined;
|
|
75
|
+
/** Raw decoded account from the last `sync()`; holds per-token window data. */
|
|
76
|
+
private rawAccount: RawPoolAccount | undefined;
|
|
77
|
+
|
|
78
|
+
constructor(params: CubicPoolClientParams) {
|
|
79
|
+
this.config = params.config;
|
|
80
|
+
this.poolAddress = params.poolAddress;
|
|
81
|
+
this.rpc =
|
|
82
|
+
params.rpc instanceof RpcClient
|
|
83
|
+
? params.rpc
|
|
84
|
+
: new RpcClient({
|
|
85
|
+
endpoint: params.rpc?.endpoint,
|
|
86
|
+
endpoints: params.rpc?.endpoints ?? (params.rpc?.endpoint ? undefined : params.config.defaults.rpcEndpoints),
|
|
87
|
+
fallbackEndpoints: params.rpc?.fallbackEndpoints,
|
|
88
|
+
apiKey: params.rpc?.apiKey,
|
|
89
|
+
commitment: params.rpc?.commitment ?? params.config.defaults.rpcCommitment,
|
|
90
|
+
timeoutMs: params.rpc?.timeoutMs ?? params.config.defaults.rpcTimeoutMs,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Last-fetched pool state. `undefined` before first `sync()`. */
|
|
95
|
+
getCached(): PoolInfo | undefined {
|
|
96
|
+
return this.cache;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Fetch and decode the pool account + BPT mint + per-token mint decimals.
|
|
101
|
+
* Safe to call repeatedly; subsequent calls replace the cache.
|
|
102
|
+
*/
|
|
103
|
+
async sync(): Promise<SdkResult<PoolInfo>> {
|
|
104
|
+
const poolInfo = await this.rpc.getAccountInfo(this.poolAddress);
|
|
105
|
+
if (!poolInfo.ok) return poolInfo;
|
|
106
|
+
if (poolInfo.data === null) {
|
|
107
|
+
return err("account_not_found", `Pool ${this.poolAddress.toBase58()} does not exist on-chain`);
|
|
108
|
+
}
|
|
109
|
+
if (!poolInfo.data.owner.equals(this.config.programs.cubicPool)) return err("parse_failure", "Pool account has an unexpected owner");
|
|
110
|
+
let raw;
|
|
111
|
+
try {
|
|
112
|
+
raw = decodePoolAccount(poolInfo.data.data);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return err("parse_failure", "Could not decode pool account", e);
|
|
115
|
+
}
|
|
116
|
+
const n = raw.tokenCount;
|
|
117
|
+
const mintAddrs = raw.tokenMints.slice(0, n);
|
|
118
|
+
const [bptMint, _bptBump] = deriveBptMint(this.config.programs.cubicPool, this.poolAddress);
|
|
119
|
+
|
|
120
|
+
const mintInfos = await this.rpc.getMultipleAccountsWithInfo([bptMint, ...mintAddrs, SYSVAR_CLOCK_PUBKEY]);
|
|
121
|
+
if (!mintInfos.ok) return mintInfos;
|
|
122
|
+
const [bptInfo, ...rest] = mintInfos.data;
|
|
123
|
+
const tokenMintInfos = rest.slice(0, n);
|
|
124
|
+
const clockInfo = rest[n];
|
|
125
|
+
const bptMintData = bptInfo?.data;
|
|
126
|
+
if (bptInfo && ![TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].some(p => p.equals(bptInfo.owner))) return err("parse_failure", "Invalid BPT token program");
|
|
127
|
+
if (!clockInfo || clockInfo.data.length < 40) return err("parse_failure", "Solana Clock account missing");
|
|
128
|
+
const chainTimestamp = Number(clockInfo.data.readBigInt64LE(32));
|
|
129
|
+
const createdAt = Number(raw.createdAt.toString());
|
|
130
|
+
if (!Number.isSafeInteger(chainTimestamp) || !Number.isSafeInteger(createdAt)) {
|
|
131
|
+
return err("parse_failure", "Pool/Clock timestamp cannot be represented exactly by PoolInfo; use the raw account decoder for full i64 values");
|
|
132
|
+
}
|
|
133
|
+
if (!bptMintData) {
|
|
134
|
+
return err("account_not_found", "BPT mint account missing");
|
|
135
|
+
}
|
|
136
|
+
let bptMintAcc;
|
|
137
|
+
try {
|
|
138
|
+
bptMintAcc = decodeMintAccount(bptMintData);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return err("parse_failure", "Could not decode BPT mint", e);
|
|
141
|
+
}
|
|
142
|
+
const tokens: PoolTokenInfo[] = [];
|
|
143
|
+
for (let i = 0; i < n; i++) {
|
|
144
|
+
const mintInfo = tokenMintInfos[i];
|
|
145
|
+
const mintRaw = mintInfo?.data;
|
|
146
|
+
if (mintInfo && !mintInfo.owner.equals(raw.tokenPrograms[i])) return err("parse_failure", `Mint token program mismatch at index ${i}`);
|
|
147
|
+
if (!mintRaw) {
|
|
148
|
+
return err("account_not_found", `Mint account missing for token index ${i}`);
|
|
149
|
+
}
|
|
150
|
+
let mintAcc;
|
|
151
|
+
try {
|
|
152
|
+
mintAcc = decodeMintAccount(mintRaw);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
return err("parse_failure", `Could not decode mint ${mintAddrs[i].toBase58()}`, e);
|
|
155
|
+
}
|
|
156
|
+
const actualBalance = raw.actualBalances[i];
|
|
157
|
+
const virtualBalance = raw.virtualBalances[i];
|
|
158
|
+
if (raw.normalizedWeights[i].gt(new BN(Number.MAX_SAFE_INTEGER))) {
|
|
159
|
+
return err("parse_failure", `Weight cannot be represented exactly at token index ${i}`);
|
|
160
|
+
}
|
|
161
|
+
const concentration =
|
|
162
|
+
virtualBalance.isZero() ? 0 : Number(actualBalance.toString()) / Number(virtualBalance.toString());
|
|
163
|
+
tokens.push({
|
|
164
|
+
index: i,
|
|
165
|
+
mint: raw.tokenMints[i],
|
|
166
|
+
tokenProgram: raw.tokenPrograms[i],
|
|
167
|
+
decimals: mintAcc.decimals,
|
|
168
|
+
weightBps: raw.normalizedWeights[i].toNumber(),
|
|
169
|
+
virtualBalance,
|
|
170
|
+
actualBalance,
|
|
171
|
+
protocolFeesOwed: raw.protocolFeesOwed[i],
|
|
172
|
+
vault: deriveAta(this.poolAddress, raw.tokenMints[i], raw.tokenPrograms[i]),
|
|
173
|
+
metadata:
|
|
174
|
+
this.config.tokens?.[raw.tokenMints[i].toBase58()] ??
|
|
175
|
+
resolveKnownToken(raw.tokenMints[i].toBase58()),
|
|
176
|
+
concentration,
|
|
177
|
+
isActive: raw.isActive[i],
|
|
178
|
+
maxSelloffPct: raw.maxSelloffPct[i],
|
|
179
|
+
maxSelloffPeriodLength: raw.maxSelloffPeriodLength[i],
|
|
180
|
+
variableFeeThresholdPct: raw.variableFeeThresholdPct[i],
|
|
181
|
+
variableFeeSlopeLowPct: raw.variableFeeSlopeLowPct[i],
|
|
182
|
+
variableFeeSlopeHighPct: raw.variableFeeSlopeHighPct[i],
|
|
183
|
+
previousSelloff: raw.previousSelloff[i],
|
|
184
|
+
currentSelloff: raw.currentSelloff[i],
|
|
185
|
+
windowStartTimestamp: raw.windowStartTimestamp[i],
|
|
186
|
+
selloffVbSnapshot: raw.selloffVbSnapshot[i],
|
|
187
|
+
variableFeeSlopeMidPct: raw.variableFeeSlopeMidPct[i],
|
|
188
|
+
variableFeeKinkPct: raw.variableFeeKinkPct[i],
|
|
189
|
+
extensions: mintAcc.extensions,
|
|
190
|
+
unsupportedExtensions: unsupportedMintExtensions(mintAcc.extensions, raw.bannedExtensions),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const info: PoolInfo = {
|
|
195
|
+
address: this.poolAddress,
|
|
196
|
+
config: raw.config,
|
|
197
|
+
bump: raw.bump,
|
|
198
|
+
poolId: raw.poolId,
|
|
199
|
+
tokenCount: n,
|
|
200
|
+
tokens,
|
|
201
|
+
unsupportedTokenIndices: tokens.filter((t) => (t.unsupportedExtensions?.length ?? 0) > 0).map((t) => t.index),
|
|
202
|
+
bptMint,
|
|
203
|
+
bptTokenProgram: bptInfo!.owner,
|
|
204
|
+
poolAdmin: raw.poolAdmin,
|
|
205
|
+
pendingPoolAdmin: raw.pendingPoolAdmin,
|
|
206
|
+
rangeManager: raw.rangeManager,
|
|
207
|
+
rangeManagerEnabled: raw.rangeManagerEnabled,
|
|
208
|
+
rangeManagerMaxVbChangePct: raw.rangeManagerMaxVbChangePct,
|
|
209
|
+
rangeManagerMaxWeightChangePct: raw.rangeManagerMaxWeightChangePct,
|
|
210
|
+
rangeManagerMinUpdateIntervalSecs: raw.rangeManagerMinUpdateIntervalSecs,
|
|
211
|
+
rangeManagerLastUpdated: raw.rangeManagerLastUpdated,
|
|
212
|
+
chainTimestamp,
|
|
213
|
+
bptTotalSupply: bptMintAcc.supply,
|
|
214
|
+
swapFeeRate: raw.swapFeeRate,
|
|
215
|
+
protocolFeeRate: raw.protocolFeeRate,
|
|
216
|
+
poolEnabled: raw.poolEnabled,
|
|
217
|
+
swapsEnabled: raw.swapsEnabled,
|
|
218
|
+
createdAt,
|
|
219
|
+
lookupTable: raw.lookupTable,
|
|
220
|
+
bannedExtensions: raw.bannedExtensions,
|
|
221
|
+
rangeManagerMaxLeverageBps: raw.rangeManagerMaxLeverageBps,
|
|
222
|
+
rangeManagerMinLeverageBps: raw.rangeManagerMinLeverageBps,
|
|
223
|
+
syncedAt: Date.now(),
|
|
224
|
+
};
|
|
225
|
+
this.cache = info;
|
|
226
|
+
this.rawAccount = raw;
|
|
227
|
+
return ok(info);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Derived helper PDA for this pool (used by single-token-deposit). */
|
|
231
|
+
helperPda(): PublicKey {
|
|
232
|
+
return deriveHelperPda(this.config.programs.singleTokenLiquidity, this.poolAddress)[0];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------- Quote helpers (pure, require sync()) ----------
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Quote a swap. Requires the cache to be populated (call `sync()` first).
|
|
239
|
+
* Returns exact on-chain math result + spot bound + price impact +
|
|
240
|
+
* slippage-derived minAmountOut.
|
|
241
|
+
*/
|
|
242
|
+
quoteSwap(
|
|
243
|
+
tokenInIndex: number,
|
|
244
|
+
tokenOutIndex: number,
|
|
245
|
+
amountIn: BN,
|
|
246
|
+
slippageHundredthsBps?: number,
|
|
247
|
+
nowSeconds?: number
|
|
248
|
+
): SdkResult<SwapQuote> {
|
|
249
|
+
const pool = this.requireCache();
|
|
250
|
+
if (!pool.ok) return pool;
|
|
251
|
+
const unsupported = this.requireSupported([tokenInIndex, tokenOutIndex], "swap");
|
|
252
|
+
if (!unsupported.ok) return unsupported;
|
|
253
|
+
const slip = slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
|
|
254
|
+
try {
|
|
255
|
+
const q = simulatePoolSwap(pool.data, tokenInIndex, tokenOutIndex, BigInt(amountIn.toString()), quoteTimestamp(pool.data, nowSeconds));
|
|
256
|
+
// v5 display contract: post-trade window fill and static+surge fee
|
|
257
|
+
// pct (span-average) — the same fields the pre-merge v5 SDK returned.
|
|
258
|
+
const inTok = pool.data.tokens[tokenInIndex];
|
|
259
|
+
let windowFillPct = 0;
|
|
260
|
+
let surgePct = 0;
|
|
261
|
+
if (q.window && q.window.maxSelloffCap > 0n) {
|
|
262
|
+
const scale = BigInt(PERCENT_SCALE);
|
|
263
|
+
const fillRaw = (q.window.effectiveSelloff * scale) / q.window.maxSelloffCap;
|
|
264
|
+
windowFillPct = Number(fillRaw > scale ? scale : fillRaw);
|
|
265
|
+
surgePct = calcSurgeFeePct(
|
|
266
|
+
q.window.effectiveSelloffBefore,
|
|
267
|
+
q.window.effectiveSelloff,
|
|
268
|
+
q.window.maxSelloffCap,
|
|
269
|
+
inTok.variableFeeThresholdPct ?? 0,
|
|
270
|
+
inTok.variableFeeSlopeLowPct ?? 0,
|
|
271
|
+
inTok.variableFeeSlopeMidPct ?? 0,
|
|
272
|
+
inTok.variableFeeSlopeHighPct ?? 0,
|
|
273
|
+
inTok.variableFeeKinkPct ?? 0
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const effectiveFeePct =
|
|
277
|
+
(pool.data.swapFeeRate * PERCENT_SCALE) / SWAP_FEE_PRECISION + surgePct;
|
|
278
|
+
return ok({
|
|
279
|
+
tokenInIndex,
|
|
280
|
+
tokenOutIndex,
|
|
281
|
+
amountIn,
|
|
282
|
+
amountOut: new BN(q.amountOut.toString()),
|
|
283
|
+
grossAmountOut: new BN(q.grossAmountOut.toString()),
|
|
284
|
+
surgeFeeAmount: new BN(q.surgeFeeAmount.toString()),
|
|
285
|
+
feeAmount: new BN(q.feeAmount.toString()),
|
|
286
|
+
protocolFeeAmount: new BN(q.protocolFeeAmount.toString()),
|
|
287
|
+
spotOut: new BN(q.spotOut.toString()),
|
|
288
|
+
// Impact excludes fees and the surge charge (industry standard):
|
|
289
|
+
// measured against the GROSS curve output.
|
|
290
|
+
priceImpactHbps: priceImpactHbps(q.spotOut, q.grossAmountOut),
|
|
291
|
+
minAmountOut: new BN(applySlippage(q.amountOut, slip).toString()),
|
|
292
|
+
effectiveFeePct,
|
|
293
|
+
windowFillPct,
|
|
294
|
+
});
|
|
295
|
+
} catch (e) {
|
|
296
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
297
|
+
// Keep the specific v5 error codes existing consumers rely on.
|
|
298
|
+
const code =
|
|
299
|
+
msg === "MaxSelloffExceeded" ? "selloff_window_full"
|
|
300
|
+
: msg === "TokenInactive" ? "token_swaps_disabled"
|
|
301
|
+
: msg === "PoolDisabled" ? "pool_disabled"
|
|
302
|
+
: msg === "SwapsDisabled" ? "swaps_disabled"
|
|
303
|
+
: "invalid_input";
|
|
304
|
+
const human =
|
|
305
|
+
code === "selloff_window_full"
|
|
306
|
+
? "Token max-selloff threshold exceeded for current window"
|
|
307
|
+
: `Swap quote failed: ${msg}`;
|
|
308
|
+
return err(code, human, e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Read-only status of a token's max-selloff window at `now`
|
|
314
|
+
* (default: current unix seconds). Requires a prior `sync()`.
|
|
315
|
+
*/
|
|
316
|
+
getSelloffWindowStatus(tokenIndex: number, now?: number): SdkResult<SelloffWindowStatus> {
|
|
317
|
+
const poolRes = this.requireCache();
|
|
318
|
+
if (!poolRes.ok) return poolRes;
|
|
319
|
+
const raw = this.rawAccount;
|
|
320
|
+
if (!raw) {
|
|
321
|
+
return err("invalid_input", "Call `sync()` first to populate window state");
|
|
322
|
+
}
|
|
323
|
+
const pool = poolRes.data;
|
|
324
|
+
if (tokenIndex < 0 || tokenIndex >= pool.tokens.length) {
|
|
325
|
+
return err("invalid_input", "Invalid tokenIndex");
|
|
326
|
+
}
|
|
327
|
+
const i = tokenIndex;
|
|
328
|
+
return ok(
|
|
329
|
+
computeSelloffWindow({
|
|
330
|
+
maxSelloffPct: raw.maxSelloffPct[i],
|
|
331
|
+
periodLength: raw.maxSelloffPeriodLength[i],
|
|
332
|
+
previousSelloff: raw.previousSelloff[i],
|
|
333
|
+
currentSelloff: raw.currentSelloff[i],
|
|
334
|
+
windowStartTimestamp: raw.windowStartTimestamp[i],
|
|
335
|
+
selloffVbSnapshot: raw.selloffVbSnapshot[i],
|
|
336
|
+
virtualBalance: pool.tokens[i].virtualBalance,
|
|
337
|
+
now: now ?? Math.floor(Date.now() / 1000),
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Quote a single-token deposit: split amountIn by W-based shares, quote
|
|
344
|
+
* each swap leg, then execute the helper cap and proportional-join math.
|
|
345
|
+
* `helperBalances` supplies existing helper ATA holdings (defaults to zero).
|
|
346
|
+
* Pass them when the helper contains donations or dust; all leftovers are refunded.
|
|
347
|
+
*/
|
|
348
|
+
quoteSingleTokenDeposit(
|
|
349
|
+
tokenInIndex: number,
|
|
350
|
+
amountIn: BN,
|
|
351
|
+
slippageHundredthsBps?: number,
|
|
352
|
+
nowSeconds?: number,
|
|
353
|
+
helperBalances?: BN[]
|
|
354
|
+
): SdkResult<SingleTokenDepositQuote> {
|
|
355
|
+
const poolRes = this.requireCache();
|
|
356
|
+
if (!poolRes.ok) return poolRes;
|
|
357
|
+
const pool = poolRes.data;
|
|
358
|
+
if (!Number.isInteger(tokenInIndex) || tokenInIndex < 0 || tokenInIndex >= pool.tokens.length) {
|
|
359
|
+
return err("invalid_input", "Invalid tokenInIndex");
|
|
360
|
+
}
|
|
361
|
+
if (!pool.poolEnabled) return err("pool_disabled", "Pool is disabled");
|
|
362
|
+
if (!pool.swapsEnabled) return err("invalid_input", "SwapsDisabled");
|
|
363
|
+
if (pool.bptTotalSupply.isZero()) return err("invalid_input", "PoolNotSeeded");
|
|
364
|
+
if (pool.tokens[tokenInIndex].actualBalance.isZero()) return err("invalid_input", "Input token is sidelined");
|
|
365
|
+
const unsupported = this.requireSupported(pool.tokens.filter(t => !t.actualBalance.isZero()).map(t => t.index), "deposit_single_token");
|
|
366
|
+
if (!unsupported.ok) return unsupported;
|
|
367
|
+
const slip = slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
|
|
368
|
+
try {
|
|
369
|
+
const input = u64(BigInt(amountIn.toString()), "amountIn");
|
|
370
|
+
if (input === 0n) return err("invalid_input", "amountIn must be positive");
|
|
371
|
+
const now = quoteTimestamp(pool, nowSeconds);
|
|
372
|
+
const actualBalances = pool.tokens.map(t => BigInt(t.actualBalance.toString()));
|
|
373
|
+
const virtualBalances = pool.tokens.map(t => BigInt(t.virtualBalance.toString()));
|
|
374
|
+
const alloc = computeAllocations({ actualBalances, virtualBalances, weightsBps: pool.tokens.map(t => t.weightBps), amountIn: input, tokenInIndex });
|
|
375
|
+
if (alloc.allocations.some((a, i) => actualBalances[i] > 0n && a === 0n)) throw new Error("AmountTooSmall");
|
|
376
|
+
const simulated: PoolInfo = { ...pool, tokens: pool.tokens.map(t => ({ ...t })) };
|
|
377
|
+
if (helperBalances && helperBalances.length !== pool.tokenCount) throw new Error("Invalid helper balance vector length");
|
|
378
|
+
const helper = pool.tokens.map((_,i) => u64(BigInt(helperBalances?.[i].toString() ?? "0"), "helper balance"));
|
|
379
|
+
const helperSupport = this.requireSupported(helper.flatMap((a,i) => a > 0n ? [i] : []), "helper refund");
|
|
380
|
+
if (!helperSupport.ok) return helperSupport;
|
|
381
|
+
u64(helper[tokenInIndex] + input, "helper input balance");
|
|
382
|
+
const expectedOuts = pool.tokens.map(() => new BN(0));
|
|
383
|
+
const minOuts = pool.tokens.map(() => new BN(0));
|
|
384
|
+
let remainingInput = input;
|
|
385
|
+
for (let i = 0; i < pool.tokenCount; i++) {
|
|
386
|
+
if (i === tokenInIndex || alloc.allocations[i] === 0n) continue;
|
|
387
|
+
const q = simulatePoolSwap(simulated, tokenInIndex, i, alloc.allocations[i], now, true);
|
|
388
|
+
remainingInput -= alloc.allocations[i]; helper[i] = u64(helper[i] + q.amountOut, "helper output balance");
|
|
389
|
+
expectedOuts[i] = new BN(q.amountOut.toString());
|
|
390
|
+
minOuts[i] = new BN(applySlippage(q.amountOut, slip).toString());
|
|
391
|
+
}
|
|
392
|
+
helper[tokenInIndex] = u64(helper[tokenInIndex] + remainingInput, "helper input balance");
|
|
393
|
+
const postActual = simulated.tokens.map(t => BigInt(t.actualBalance.toString()));
|
|
394
|
+
const capped = capDepositAmountsToLpRatio({ helperBalances: helper, actualBalances: postActual });
|
|
395
|
+
// The helper's integer ratio cap is followed by cubic-pool's fixed-point proportional join.
|
|
396
|
+
if (capped.depositAmounts.some((v, i) => (v === 0n) !== (postActual[i] === 0n))) throw new Error("TokenLivenessMismatch");
|
|
397
|
+
let ratio: bigint | null = null;
|
|
398
|
+
for (let i = 0; i < pool.tokenCount; i++) if (postActual[i] > 0n) {
|
|
399
|
+
const r = divDown(capped.depositAmounts[i], postActual[i]); ratio = ratio === null || r < ratio ? r : ratio;
|
|
400
|
+
}
|
|
401
|
+
if (ratio === null) throw new Error("Pool has no live tokens");
|
|
402
|
+
const deposited = postActual.map(a => mulDown(a, ratio!));
|
|
403
|
+
const estBpt = calcBptOutGivenExactTokensIn(postActual, capped.depositAmounts, BigInt(pool.bptTotalSupply.toString()));
|
|
404
|
+
if (estBpt === 0n || deposited.some((d, i) => postActual[i] > 0n && d === 0n)) throw new Error("DepositTooSmall");
|
|
405
|
+
u64(estBpt + BigInt(pool.bptTotalSupply.toString()), "BPT supply");
|
|
406
|
+
for (let i = 0; i < pool.tokenCount; i++) {
|
|
407
|
+
u64(postActual[i] + deposited[i], "reserve balance");
|
|
408
|
+
const vb = BigInt(simulated.tokens[i].virtualBalance.toString()); u64(vb + mulDown(vb, ratio), "virtual balance");
|
|
409
|
+
}
|
|
410
|
+
this.validateDepositWindows(simulated, ratio);
|
|
411
|
+
return ok({ tokenInIndex, amountIn, allocations: alloc.allocations.map(a => new BN(a.toString())),
|
|
412
|
+
expectedOuts, minOuts, depositedAmounts: deposited.map(a => new BN(a.toString())),
|
|
413
|
+
refundAmounts: helper.map((a,i) => new BN((a - deposited[i]).toString())),
|
|
414
|
+
estimatedBpt: new BN(estBpt.toString()),
|
|
415
|
+
sidelinedTokenIndices: pool.tokens.filter(t => t.actualBalance.isZero()).map(t => t.index),
|
|
416
|
+
});
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return err("invalid_input", `Single-token deposit quote failed: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Seed deposit: creator-only, exact amounts, BPT derived from virtual reserves. */
|
|
423
|
+
quoteSeedDeposit(user: PublicKey, tokenAmounts: BN[], slippageHundredthsBps?: number): SdkResult<AddLiquidityQuote> {
|
|
424
|
+
const state = this.requireCache();
|
|
425
|
+
if (!state.ok) return state;
|
|
426
|
+
const pool = state.data;
|
|
427
|
+
if (!pool.poolEnabled) return err("pool_disabled", "Pool is disabled");
|
|
428
|
+
if (!pool.bptTotalSupply.isZero()) return err("invalid_input", "Pool already seeded; use quoteAddLiquidity");
|
|
429
|
+
if (!pool.poolAdmin) return err("invalid_input", "Pool admin missing: sync the pool before quoting");
|
|
430
|
+
if (pool.poolAdmin.equals(PublicKey.default)) return err("invalid_input", "PoolAdminDisabled");
|
|
431
|
+
if (!pool.poolAdmin.equals(user)) return err("invalid_input", "SeedDepositNotPoolAdmin");
|
|
432
|
+
if (tokenAmounts.length !== pool.tokenCount || tokenAmounts.some(a => a.isNeg() || a.bitLength() > 64)) return err("invalid_input", "Expected one u64 amount per token");
|
|
433
|
+
if (tokenAmounts.every(a => a.isZero())) return err("invalid_input", "FirstDepositRequiresNonzero");
|
|
434
|
+
const supported = this.requireSupported(tokenAmounts.flatMap((a,i) => a.isZero() ? [] : [i]), "seed deposit");
|
|
435
|
+
if (!supported.ok) return supported;
|
|
436
|
+
try {
|
|
437
|
+
const virtual = pool.tokens.map(t => BigInt(t.virtualBalance.toString()));
|
|
438
|
+
const balances = virtual.every(v => v === 0n) ? tokenAmounts.map(a => BigInt(a.toString())) : virtual;
|
|
439
|
+
const bptOut = u64(calculateInvariant(balances, pool.tokens.map(t => t.weightBps), pool.tokens.map(t => t.decimals)), "BPT amount");
|
|
440
|
+
if (bptOut < 1000n) return err("invalid_input", "InitialLiquidityTooSmall");
|
|
441
|
+
return ok({ tokenAmounts, depositAmounts: tokenAmounts.map(a => a.clone()), refundAmounts: tokenAmounts.map(() => new BN(0)),
|
|
442
|
+
bptOut: new BN(bptOut.toString()), minimumBptAmount: new BN(applySlippage(bptOut, slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps).toString()),
|
|
443
|
+
limitingTokenIndex: -1 });
|
|
444
|
+
} catch (e) { return err("math_overflow", "Seed-deposit quote failed", e); }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Proportional-join quote. `tokenAmounts` is a CEILING (v5.1, audit
|
|
449
|
+
* M-4 / I-1): the program takes the largest strictly-proportional basket
|
|
450
|
+
* that fits inside it and leaves the rest in the wallet. This returns the
|
|
451
|
+
* basket it will actually pull, the BPT it will mint, and a
|
|
452
|
+
* slippage-derived `minimumBptAmount` to pass to `buildAddLiquidityTx`.
|
|
453
|
+
*
|
|
454
|
+
* Not applicable to the seed deposit (BPT supply == 0): the program
|
|
455
|
+
* mints invariant-based BPT there and takes the full amounts.
|
|
456
|
+
*/
|
|
457
|
+
quoteAddLiquidity(tokenAmounts: BN[], slippageHundredthsBps?: number): SdkResult<AddLiquidityQuote> {
|
|
458
|
+
const poolRes = this.requireCache();
|
|
459
|
+
if (!poolRes.ok) return poolRes;
|
|
460
|
+
const pool = poolRes.data;
|
|
461
|
+
if (tokenAmounts.length !== pool.tokenCount) {
|
|
462
|
+
return err("invalid_input", "tokenAmounts length must equal pool.tokenCount");
|
|
463
|
+
}
|
|
464
|
+
if (pool.bptTotalSupply.isZero()) {
|
|
465
|
+
return err("invalid_input", "Pool has zero BPT supply; use quoteSeedDeposit");
|
|
466
|
+
}
|
|
467
|
+
if (!pool.poolEnabled) return err("pool_disabled", "Pool is disabled");
|
|
468
|
+
if (tokenAmounts.some((a) => a.isNeg() || a.bitLength() > 64)) {
|
|
469
|
+
return err("invalid_input", "tokenAmounts must be u64 values");
|
|
470
|
+
}
|
|
471
|
+
if (pool.tokens.some((t, i) => t.actualBalance.isZero() !== tokenAmounts[i].isZero())) {
|
|
472
|
+
return err("invalid_input", "TokenLivenessMismatch: live tokens need positive amounts; zero-balance tokens need zero amounts");
|
|
473
|
+
}
|
|
474
|
+
const unsupported = this.requireSupported(pool.tokens.filter(t => !t.actualBalance.isZero()).map(t => t.index), "add_liquidity");
|
|
475
|
+
if (!unsupported.ok) return unsupported;
|
|
476
|
+
const slip = slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
|
|
477
|
+
try {
|
|
478
|
+
// Same inputs as cubic-pool/add_liquidity.rs: raw stored actual
|
|
479
|
+
// balances, ratio = min over tokens with actual > 0.
|
|
480
|
+
const bals = pool.tokens.map((t) => BigInt(t.actualBalance.toString()));
|
|
481
|
+
const amounts = tokenAmounts.map((a) => BigInt(a.toString()));
|
|
482
|
+
const supply = BigInt(pool.bptTotalSupply.toString());
|
|
483
|
+
let ratioMin: bigint | null = null;
|
|
484
|
+
let limiting = -1;
|
|
485
|
+
for (let i = 0; i < bals.length; i++) {
|
|
486
|
+
if (bals[i] === 0n) continue;
|
|
487
|
+
const r = divDown(amounts[i], bals[i]);
|
|
488
|
+
if (ratioMin === null || r < ratioMin) {
|
|
489
|
+
ratioMin = r;
|
|
490
|
+
limiting = i;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (ratioMin === null) return err("invalid_input", "Pool has no token with a non-zero balance");
|
|
494
|
+
const bptOut = calcBptOutGivenExactTokensIn(bals, amounts, supply);
|
|
495
|
+
if (bptOut === 0n || bptOut > ((1n << 64n) - 1n) || supply + bptOut > ((1n << 64n) - 1n)) return err("invalid_input", "DepositTooSmall or BPT supply overflow");
|
|
496
|
+
const deposit = bals.map((b) => (b === 0n ? 0n : mulDown(b, ratioMin as bigint)));
|
|
497
|
+
if (deposit.some((d, i) => bals[i] > 0n && d === 0n || bals[i] + d > ((1n << 64n) - 1n))) return err("invalid_input", "DepositTooSmall or reserve overflow");
|
|
498
|
+
if (pool.tokens.some(t => BigInt(t.virtualBalance.toString()) + mulDown(BigInt(t.virtualBalance.toString()), ratioMin!) > ((1n << 64n) - 1n))) return err("math_overflow", "Virtual balance overflow");
|
|
499
|
+
this.validateDepositWindows(pool, ratioMin);
|
|
500
|
+
const refund = amounts.map((a, i) => a - deposit[i]);
|
|
501
|
+
return ok({
|
|
502
|
+
tokenAmounts,
|
|
503
|
+
depositAmounts: deposit.map((d) => new BN(d.toString())),
|
|
504
|
+
refundAmounts: refund.map((r) => new BN(r.toString())),
|
|
505
|
+
bptOut: new BN(bptOut.toString()),
|
|
506
|
+
minimumBptAmount: new BN(applySlippage(bptOut, slip).toString()),
|
|
507
|
+
limitingTokenIndex: limiting,
|
|
508
|
+
});
|
|
509
|
+
} catch (e) {
|
|
510
|
+
return err("math_overflow", "Add-liquidity quote failed", e);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Proportional-withdraw quote for a given BPT amount. */
|
|
515
|
+
quoteRemove(bptIn: BN): SdkResult<{ tokenOuts: BN[]; effectiveBptIn: BN }> {
|
|
516
|
+
const poolRes = this.requireCache();
|
|
517
|
+
if (!poolRes.ok) return poolRes;
|
|
518
|
+
const pool = poolRes.data;
|
|
519
|
+
if (pool.bptTotalSupply.isZero()) {
|
|
520
|
+
return err("invalid_input", "Pool has zero BPT supply");
|
|
521
|
+
}
|
|
522
|
+
if (!pool.poolEnabled) return err("pool_disabled", "Pool is disabled");
|
|
523
|
+
try {
|
|
524
|
+
// Match cubic-pool/remove_liquidity.rs exactly: it computes
|
|
525
|
+
// token_amounts via divDown(bpt_amount, bpt_supply), then mulDown(actual, ratio)
|
|
526
|
+
// against the raw stored actual (no protocol-fee subtraction). Use
|
|
527
|
+
// the same input here so the SDK quote equals what the contract
|
|
528
|
+
// actually transfers.
|
|
529
|
+
const bals = pool.tokens.map((t) => BigInt(t.actualBalance.toString()));
|
|
530
|
+
const supply = BigInt(pool.bptTotalSupply.toString());
|
|
531
|
+
const requested = BigInt(bptIn.toString());
|
|
532
|
+
if (requested <= 0n || requested > supply || supply <= 1000n) return err("invalid_input", "InvalidBptAmount");
|
|
533
|
+
const effective = requested < supply - 1000n ? requested : supply - 1000n;
|
|
534
|
+
const outs = calcTokensOutGivenBptIn(bals, effective, supply);
|
|
535
|
+
const supported = this.requireSupported(outs.flatMap((amount,i) => amount > 0n ? [i] : []), "remove_liquidity");
|
|
536
|
+
if (!supported.ok) return supported;
|
|
537
|
+
return ok({ tokenOuts: outs.map((o) => new BN(o.toString())), effectiveBptIn: new BN(effective.toString()) });
|
|
538
|
+
} catch (e) {
|
|
539
|
+
return err("math_overflow", "Remove quote failed", e);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ---------- Transaction builders ----------
|
|
544
|
+
//
|
|
545
|
+
// For external SDK users, deposit/swap/remove are one-call operations via
|
|
546
|
+
// the CubicPoolClient. The `singleTokenDeposit` field is a proxy into the
|
|
547
|
+
// dedicated SingleTokenDepositClient as the user expects: importing this
|
|
548
|
+
// class is enough, no need to know the helper program exists.
|
|
549
|
+
|
|
550
|
+
/** Build a swap transaction. Requires `sync()` first. */
|
|
551
|
+
buildSwapTx(params: SwapParams): SdkResult<BuiltTx> {
|
|
552
|
+
const poolRes = this.requireCache();
|
|
553
|
+
if (!poolRes.ok) return poolRes;
|
|
554
|
+
const unsupported = this.requireSupported([params.tokenInIndex, params.tokenOutIndex], "swap");
|
|
555
|
+
if (!unsupported.ok) return unsupported;
|
|
556
|
+
const slip = params.slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
|
|
557
|
+
let minOut: BN;
|
|
558
|
+
if (params.minAmountOut) {
|
|
559
|
+
minOut = params.minAmountOut;
|
|
560
|
+
} else {
|
|
561
|
+
const q = this.quoteSwap(
|
|
562
|
+
params.tokenInIndex,
|
|
563
|
+
params.tokenOutIndex,
|
|
564
|
+
params.amountIn,
|
|
565
|
+
slip
|
|
566
|
+
);
|
|
567
|
+
if (!q.ok) {
|
|
568
|
+
return err(
|
|
569
|
+
q.error.code,
|
|
570
|
+
`Cannot build swap without minAmountOut: ${q.error.humanMessage}`,
|
|
571
|
+
q.error.cause
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
minOut = q.data.minAmountOut;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const tx = buildSwapTx(this.config, poolRes.data, { ...params, minAmountOut: minOut });
|
|
578
|
+
return ok(tx);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
return err("tx_build_failed", "Failed to build swap tx", e);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Build a proportional add-liquidity transaction. Requires `sync()` first. */
|
|
585
|
+
buildAddLiquidityTx(params: AddLiquidityParams): SdkResult<BuiltTx> {
|
|
586
|
+
const poolRes = this.requireCache();
|
|
587
|
+
if (!poolRes.ok) return poolRes;
|
|
588
|
+
if (params.tokenAmounts.length !== poolRes.data.tokenCount) {
|
|
589
|
+
return err("invalid_input", "tokenAmounts length must equal pool.tokenCount");
|
|
590
|
+
}
|
|
591
|
+
const unsupported = this.requireSupported(params.tokenAmounts.flatMap((a,i) => a.isZero() ? [] : [i]), "add_liquidity");
|
|
592
|
+
if (!unsupported.ok) return unsupported;
|
|
593
|
+
try {
|
|
594
|
+
return ok(buildAddLiquidityTx(this.config, poolRes.data, params));
|
|
595
|
+
} catch (e) {
|
|
596
|
+
return err("tx_build_failed", "Failed to build add_liquidity tx", e);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Build a proportional remove-liquidity transaction. Requires `sync()` first. */
|
|
601
|
+
buildRemoveLiquidityTx(params: RemoveLiquidityParams): SdkResult<BuiltTx> {
|
|
602
|
+
const poolRes = this.requireCache();
|
|
603
|
+
if (!poolRes.ok) return poolRes;
|
|
604
|
+
if (!params.minimumTokenAmounts) {
|
|
605
|
+
return err(
|
|
606
|
+
"invalid_input",
|
|
607
|
+
"minimumTokenAmounts is required (one per token). Derive per-token floors from quoteRemove(); pass explicit zeros only to intentionally disable slippage protection."
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (params.minimumTokenAmounts.length !== poolRes.data.tokenCount) {
|
|
611
|
+
return err("invalid_input", "minimumTokenAmounts length must equal pool.tokenCount");
|
|
612
|
+
}
|
|
613
|
+
const quote = this.quoteRemove(params.bptAmount);
|
|
614
|
+
if (!quote.ok) return quote;
|
|
615
|
+
try {
|
|
616
|
+
return ok(buildRemoveLiquidityTx(this.config, poolRes.data, params));
|
|
617
|
+
} catch (e) {
|
|
618
|
+
return err("tx_build_failed", "Failed to build remove_liquidity tx", e);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Build a single-token deposit transaction (swaps + add_liquidity via the
|
|
624
|
+
* helper program + BPT forward to user). Requires `sync()` first.
|
|
625
|
+
*/
|
|
626
|
+
buildSingleTokenDepositTx(params: SingleTokenDepositParams): SdkResult<BuiltTx> {
|
|
627
|
+
const poolRes = this.requireCache();
|
|
628
|
+
if (!poolRes.ok) return poolRes;
|
|
629
|
+
if (params.amountIn.lten(0)) return err("invalid_input", "amountIn must be > 0");
|
|
630
|
+
const unsupported = this.requireSupported(poolRes.data.tokens.filter(t => !t.actualBalance.isZero() || t.index === params.tokenInIndex).map(t => t.index), "deposit_single_token");
|
|
631
|
+
if (!unsupported.ok) return unsupported;
|
|
632
|
+
try {
|
|
633
|
+
return ok(buildSingleTokenDepositTx(this.config, poolRes.data, params));
|
|
634
|
+
} catch (e) {
|
|
635
|
+
return err("tx_build_failed", "Failed to build single-token deposit tx", e);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Single-token deposit split into `{ setup, deposit }` transactions.
|
|
641
|
+
*
|
|
642
|
+
* Required for pools beyond a handful of tokens: the ATA creates and the
|
|
643
|
+
* zap compete for the same 64-frame instruction-trace budget, and at
|
|
644
|
+
* N=10 the zap alone uses 52 of them. Send `setup` (idempotent, safe to
|
|
645
|
+
* repeat), wait for confirmation, then send `deposit` compiled against
|
|
646
|
+
* the pool's ALT via `compileBuiltTx`.
|
|
647
|
+
*/
|
|
648
|
+
buildSingleTokenDepositTxs(
|
|
649
|
+
params: SingleTokenDepositParams
|
|
650
|
+
): SdkResult<{ setup: BuiltTx | null; deposit: BuiltTx }> {
|
|
651
|
+
const poolRes = this.requireCache();
|
|
652
|
+
if (!poolRes.ok) return poolRes;
|
|
653
|
+
if (params.amountIn.lten(0)) return err("invalid_input", "amountIn must be > 0");
|
|
654
|
+
const unsupported = this.requireSupported(poolRes.data.tokens.filter(t => !t.actualBalance.isZero() || t.index === params.tokenInIndex).map(t => t.index), "deposit_single_token");
|
|
655
|
+
if (!unsupported.ok) return unsupported;
|
|
656
|
+
try {
|
|
657
|
+
return ok(buildSingleTokenDepositTxs(this.config, poolRes.data, params));
|
|
658
|
+
} catch (e) {
|
|
659
|
+
return err("tx_build_failed", "Failed to build single-token deposit txs", e);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Proxy into the dedicated SingleTokenDepositClient for consumers who
|
|
664
|
+
* want the quote/build methods exposed on a focused object. */
|
|
665
|
+
get singleTokenDeposit(): SingleTokenDepositClient {
|
|
666
|
+
if (!this._std) {
|
|
667
|
+
this._std = new SingleTokenDepositClient({
|
|
668
|
+
config: this.config,
|
|
669
|
+
poolAddress: this.poolAddress,
|
|
670
|
+
poolClient: this,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return this._std;
|
|
674
|
+
}
|
|
675
|
+
private _std?: SingleTokenDepositClient;
|
|
676
|
+
|
|
677
|
+
// ---------- Event helpers ----------
|
|
678
|
+
|
|
679
|
+
/** Parse logs from a confirmed transaction into typed events. */
|
|
680
|
+
parseEventsFromLogs(logs: string[]): CubicPoolEvent[] {
|
|
681
|
+
return parseCubicPoolEvents(logs);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ---------- Internals ----------
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Refuse when any of `indices` (or every token, for `"all"`) has an
|
|
688
|
+
* unsupported Token-2022 extension. The program would revert inside the
|
|
689
|
+
* token program on transfer, so failing here saves the user a fee.
|
|
690
|
+
*/
|
|
691
|
+
private requireSupported(indices: number[] | "all", op: string): SdkResult<void> {
|
|
692
|
+
const pool = this.cache;
|
|
693
|
+
if (!pool) return ok(undefined);
|
|
694
|
+
const idx = indices === "all" ? pool.tokens.map((t) => t.index) : indices;
|
|
695
|
+
const bad = idx.map((i) => pool.tokens[i]).filter((t) => t && (t.unsupportedExtensions?.length ?? 0) > 0);
|
|
696
|
+
if (bad.length === 0) return ok(undefined);
|
|
697
|
+
return err(
|
|
698
|
+
"unsupported_token_extension",
|
|
699
|
+
`${op} refused: ${bad.map(describeUnsupportedToken).join("; ")}. ` +
|
|
700
|
+
"The cubic-pool program cannot transfer such tokens; the transaction would revert on-chain."
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private validateDepositWindows(pool: PoolInfo, ratio: bigint): void {
|
|
705
|
+
for (const token of pool.tokens) {
|
|
706
|
+
if (token.maxSelloffPct === 0) continue;
|
|
707
|
+
if ([token.previousSelloff, token.currentSelloff, token.selloffVbSnapshot, token.windowStartTimestamp].some(v => v === undefined)) throw new Error("Selloff state missing: sync the pool before quoting");
|
|
708
|
+
rescaleSelloffWindow({ previousSelloff: BigInt(token.previousSelloff!.toString()), currentSelloff: BigInt(token.currentSelloff!.toString()),
|
|
709
|
+
selloffVbSnapshot: BigInt(token.selloffVbSnapshot!.toString()), windowStartTimestamp: BigInt(token.windowStartTimestamp!.toString()) }, ratio, true, token.maxSelloffPct);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private requireCache(): SdkResult<PoolInfo> {
|
|
714
|
+
if (!this.cache) {
|
|
715
|
+
return err("invalid_input", "Call `sync()` first to populate pool state");
|
|
716
|
+
}
|
|
717
|
+
return ok(this.cache);
|
|
718
|
+
}
|
|
719
|
+
}
|