@cubee_ee/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/dist/clients/AdminClient.d.ts +61 -0
  4. package/dist/clients/AdminClient.d.ts.map +1 -0
  5. package/dist/clients/AdminClient.js +196 -0
  6. package/dist/clients/AdminClient.js.map +1 -0
  7. package/dist/clients/CubeBackendClient.d.ts +67 -0
  8. package/dist/clients/CubeBackendClient.d.ts.map +1 -0
  9. package/dist/clients/CubeBackendClient.js +126 -0
  10. package/dist/clients/CubeBackendClient.js.map +1 -0
  11. package/dist/clients/CubicPoolClient.d.ts +73 -0
  12. package/dist/clients/CubicPoolClient.d.ts.map +1 -0
  13. package/dist/clients/CubicPoolClient.js +425 -0
  14. package/dist/clients/CubicPoolClient.js.map +1 -0
  15. package/dist/clients/PoolFactoryClient.d.ts +45 -0
  16. package/dist/clients/PoolFactoryClient.d.ts.map +1 -0
  17. package/dist/clients/PoolFactoryClient.js +83 -0
  18. package/dist/clients/PoolFactoryClient.js.map +1 -0
  19. package/dist/clients/RpcClient.d.ts +28 -0
  20. package/dist/clients/RpcClient.d.ts.map +1 -0
  21. package/dist/clients/RpcClient.js +58 -0
  22. package/dist/clients/RpcClient.js.map +1 -0
  23. package/dist/clients/SingleTokenDepositClient.d.ts +45 -0
  24. package/dist/clients/SingleTokenDepositClient.d.ts.map +1 -0
  25. package/dist/clients/SingleTokenDepositClient.js +63 -0
  26. package/dist/clients/SingleTokenDepositClient.js.map +1 -0
  27. package/dist/clients/index.d.ts +8 -0
  28. package/dist/clients/index.d.ts.map +1 -0
  29. package/dist/clients/index.js +24 -0
  30. package/dist/clients/index.js.map +1 -0
  31. package/dist/clients/tx-builders.d.ts +32 -0
  32. package/dist/clients/tx-builders.d.ts.map +1 -0
  33. package/dist/clients/tx-builders.js +398 -0
  34. package/dist/clients/tx-builders.js.map +1 -0
  35. package/dist/config/index.d.ts +60 -0
  36. package/dist/config/index.d.ts.map +1 -0
  37. package/dist/config/index.js +59 -0
  38. package/dist/config/index.js.map +1 -0
  39. package/dist/config/networks.d.ts +10 -0
  40. package/dist/config/networks.d.ts.map +1 -0
  41. package/dist/config/networks.js +31 -0
  42. package/dist/config/networks.js.map +1 -0
  43. package/dist/config/tokens.d.ts +19 -0
  44. package/dist/config/tokens.d.ts.map +1 -0
  45. package/dist/config/tokens.js +43 -0
  46. package/dist/config/tokens.js.map +1 -0
  47. package/dist/idl/cubic_pool.json +2497 -0
  48. package/dist/idl/index.d.ts +975 -0
  49. package/dist/idl/index.d.ts.map +1 -0
  50. package/dist/idl/index.js +18 -0
  51. package/dist/idl/index.js.map +1 -0
  52. package/dist/idl/protocol_admin.json +1816 -0
  53. package/dist/idl/single_token_liquidity.json +745 -0
  54. package/dist/index.d.ts +48 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +136 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/math/cubicMath.d.ts +40 -0
  59. package/dist/math/cubicMath.d.ts.map +1 -0
  60. package/dist/math/cubicMath.js +75 -0
  61. package/dist/math/cubicMath.js.map +1 -0
  62. package/dist/math/fixedPoint.d.ts +14 -0
  63. package/dist/math/fixedPoint.d.ts.map +1 -0
  64. package/dist/math/fixedPoint.js +46 -0
  65. package/dist/math/fixedPoint.js.map +1 -0
  66. package/dist/math/index.d.ts +7 -0
  67. package/dist/math/index.d.ts.map +1 -0
  68. package/dist/math/index.js +23 -0
  69. package/dist/math/index.js.map +1 -0
  70. package/dist/math/logExp.d.ts +15 -0
  71. package/dist/math/logExp.d.ts.map +1 -0
  72. package/dist/math/logExp.js +91 -0
  73. package/dist/math/logExp.js.map +1 -0
  74. package/dist/math/singleToken.d.ts +53 -0
  75. package/dist/math/singleToken.d.ts.map +1 -0
  76. package/dist/math/singleToken.js +185 -0
  77. package/dist/math/singleToken.js.map +1 -0
  78. package/dist/math/slippage.d.ts +24 -0
  79. package/dist/math/slippage.d.ts.map +1 -0
  80. package/dist/math/slippage.js +54 -0
  81. package/dist/math/slippage.js.map +1 -0
  82. package/dist/math/weightedMath.d.ts +18 -0
  83. package/dist/math/weightedMath.d.ts.map +1 -0
  84. package/dist/math/weightedMath.js +45 -0
  85. package/dist/math/weightedMath.js.map +1 -0
  86. package/dist/parsers/borsh.d.ts +24 -0
  87. package/dist/parsers/borsh.d.ts.map +1 -0
  88. package/dist/parsers/borsh.js +80 -0
  89. package/dist/parsers/borsh.js.map +1 -0
  90. package/dist/parsers/events.d.ts +3 -0
  91. package/dist/parsers/events.d.ts.map +1 -0
  92. package/dist/parsers/events.js +172 -0
  93. package/dist/parsers/events.js.map +1 -0
  94. package/dist/parsers/index.d.ts +5 -0
  95. package/dist/parsers/index.d.ts.map +1 -0
  96. package/dist/parsers/index.js +21 -0
  97. package/dist/parsers/index.js.map +1 -0
  98. package/dist/parsers/mintAccount.d.ts +22 -0
  99. package/dist/parsers/mintAccount.d.ts.map +1 -0
  100. package/dist/parsers/mintAccount.js +22 -0
  101. package/dist/parsers/mintAccount.js.map +1 -0
  102. package/dist/parsers/poolAccount.d.ts +32 -0
  103. package/dist/parsers/poolAccount.d.ts.map +1 -0
  104. package/dist/parsers/poolAccount.js +88 -0
  105. package/dist/parsers/poolAccount.js.map +1 -0
  106. package/dist/types/events.d.ts +82 -0
  107. package/dist/types/events.d.ts.map +1 -0
  108. package/dist/types/events.js +3 -0
  109. package/dist/types/events.js.map +1 -0
  110. package/dist/types/index.d.ts +5 -0
  111. package/dist/types/index.d.ts.map +1 -0
  112. package/dist/types/index.js +21 -0
  113. package/dist/types/index.js.map +1 -0
  114. package/dist/types/pool.d.ts +66 -0
  115. package/dist/types/pool.d.ts.map +1 -0
  116. package/dist/types/pool.js +3 -0
  117. package/dist/types/pool.js.map +1 -0
  118. package/dist/types/result.d.ts +23 -0
  119. package/dist/types/result.d.ts.map +1 -0
  120. package/dist/types/result.js +11 -0
  121. package/dist/types/result.js.map +1 -0
  122. package/dist/types/tx.d.ts +80 -0
  123. package/dist/types/tx.d.ts.map +1 -0
  124. package/dist/types/tx.js +3 -0
  125. package/dist/types/tx.js.map +1 -0
  126. package/dist/utils/errors.d.ts +8 -0
  127. package/dist/utils/errors.d.ts.map +1 -0
  128. package/dist/utils/errors.js +83 -0
  129. package/dist/utils/errors.js.map +1 -0
  130. package/dist/utils/index.d.ts +4 -0
  131. package/dist/utils/index.d.ts.map +1 -0
  132. package/dist/utils/index.js +20 -0
  133. package/dist/utils/index.js.map +1 -0
  134. package/dist/utils/pda.d.ts +8 -0
  135. package/dist/utils/pda.d.ts.map +1 -0
  136. package/dist/utils/pda.js +27 -0
  137. package/dist/utils/pda.js.map +1 -0
  138. package/dist/utils/retry.d.ts +22 -0
  139. package/dist/utils/retry.d.ts.map +1 -0
  140. package/dist/utils/retry.js +62 -0
  141. package/dist/utils/retry.js.map +1 -0
  142. package/package.json +54 -0
  143. package/src/clients/AdminClient.ts +254 -0
  144. package/src/clients/CubeBackendClient.ts +193 -0
  145. package/src/clients/CubicPoolClient.ts +492 -0
  146. package/src/clients/PoolFactoryClient.ts +102 -0
  147. package/src/clients/RpcClient.ts +78 -0
  148. package/src/clients/SingleTokenDepositClient.ts +91 -0
  149. package/src/clients/index.ts +7 -0
  150. package/src/clients/tx-builders.ts +538 -0
  151. package/src/config/index.ts +82 -0
  152. package/src/config/networks.ts +49 -0
  153. package/src/config/tokens.ts +52 -0
  154. package/src/idl/cubic_pool.json +2497 -0
  155. package/src/idl/index.ts +13 -0
  156. package/src/idl/protocol_admin.json +1816 -0
  157. package/src/idl/single_token_liquidity.json +745 -0
  158. package/src/index.ts +154 -0
  159. package/src/math/cubicMath.ts +89 -0
  160. package/src/math/fixedPoint.ts +39 -0
  161. package/src/math/index.ts +6 -0
  162. package/src/math/logExp.ts +82 -0
  163. package/src/math/singleToken.ts +250 -0
  164. package/src/math/slippage.ts +49 -0
  165. package/src/math/weightedMath.ts +48 -0
  166. package/src/parsers/borsh.ts +80 -0
  167. package/src/parsers/events.ts +172 -0
  168. package/src/parsers/index.ts +4 -0
  169. package/src/parsers/mintAccount.ts +37 -0
  170. package/src/parsers/poolAccount.ts +113 -0
  171. package/src/types/events.ts +100 -0
  172. package/src/types/index.ts +4 -0
  173. package/src/types/pool.ts +64 -0
  174. package/src/types/result.ts +45 -0
  175. package/src/types/tx.ts +87 -0
  176. package/src/utils/errors.ts +78 -0
  177. package/src/utils/index.ts +3 -0
  178. package/src/utils/pda.ts +58 -0
  179. package/src/utils/retry.ts +85 -0
@@ -0,0 +1,193 @@
1
+ import { SdkResult, err, ok } from "../types/result";
2
+ import { PoolSummary } from "../types/pool";
3
+ import { safeCall } from "../utils/retry";
4
+
5
+ export interface CubeBackendClientParams {
6
+ apiEndpoint: string;
7
+ apiKey?: string;
8
+ defaultHeaders?: Record<string, string>;
9
+ }
10
+
11
+ export type StatsKind =
12
+ | "tvl"
13
+ | "volume"
14
+ | "swap_count"
15
+ | "avg_swap"
16
+ | "median_swap"
17
+ | "fees_lp"
18
+ | "fees_protocol"
19
+ | "users_total"
20
+ | "dau"
21
+ | "mau"
22
+ | "deposits"
23
+ | "removals";
24
+
25
+ export type StatsWindow = "1d" | "7d" | "30d" | "all";
26
+
27
+ export interface StatsSeriesPoint {
28
+ t: number;
29
+ v: number;
30
+ }
31
+ export interface StatsSeries {
32
+ points: StatsSeriesPoint[];
33
+ }
34
+
35
+ export interface PriceMap {
36
+ [mint: string]: number;
37
+ }
38
+
39
+ /**
40
+ * REST wrapper around the Cube backend. Every method is a SdkResult; no
41
+ * exceptions escape. If a request fails, the result carries a
42
+ * human-readable error plus the original cause.
43
+ */
44
+ export class CubeBackendClient {
45
+ private readonly endpoint: string;
46
+ private readonly headers: Record<string, string>;
47
+
48
+ constructor(params: CubeBackendClientParams) {
49
+ this.endpoint = params.apiEndpoint.replace(/\/$/, "");
50
+ this.headers = {
51
+ "Content-Type": "application/json",
52
+ ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
53
+ ...(params.defaultHeaders ?? {}),
54
+ };
55
+ }
56
+
57
+ listPools(): Promise<SdkResult<PoolSummary[]>> {
58
+ return this.get<PoolSummary[]>("/api/pools");
59
+ }
60
+
61
+ getPool(addr: string): Promise<SdkResult<PoolSummary>> {
62
+ return this.get<PoolSummary>(`/api/pools/${addr}`);
63
+ }
64
+
65
+ /**
66
+ * Raw pool-list response in the same envelope the backend returns
67
+ * (`{ data, hasMore, totalCount }`). Kept so the frontend's React
68
+ * Query hooks can map directly.
69
+ */
70
+ listPoolsRaw(
71
+ limit: number,
72
+ offset: number
73
+ ): Promise<SdkResult<{ data: unknown[]; hasMore: boolean; totalCount: number }>> {
74
+ const qs = new URLSearchParams({ limit: String(limit), offset: String(offset) });
75
+ return this.getEnvelope(`/api/pools?${qs.toString()}`);
76
+ }
77
+
78
+ getPoolRaw(addr: string): Promise<SdkResult<unknown>> {
79
+ return this.getDataField<unknown>(`/api/pools/${addr}`);
80
+ }
81
+
82
+ createPool<T>(body: unknown): Promise<SdkResult<T>> {
83
+ return this.postDataField<T>("/api/pools", body);
84
+ }
85
+
86
+ getPoolsByTokenPair<T>(tokenA: string, tokenB: string): Promise<SdkResult<T>> {
87
+ const qs = new URLSearchParams({ tokenA, tokenB });
88
+ return this.getDataField<T>(`/api/pools/by-pair?${qs.toString()}`);
89
+ }
90
+
91
+ getPlatformStats<T>(): Promise<SdkResult<T>> {
92
+ return this.getDataField<T>("/api/pools/stats");
93
+ }
94
+
95
+ getPortfolio<T>(owner: string): Promise<SdkResult<T>> {
96
+ return this.getDataField<T>(`/api/pools/portfolio?owner=${encodeURIComponent(owner)}`);
97
+ }
98
+
99
+ getAllTokens<T>(): Promise<SdkResult<T>> {
100
+ return this.getDataField<T>("/api/pools/tokens");
101
+ }
102
+
103
+ getTopTokens<T>(limit: number = 20): Promise<SdkResult<T>> {
104
+ return this.getDataField<T>(`/api/pools/top-tokens?limit=${limit}`);
105
+ }
106
+
107
+ getPoolTxStats<T>(addr: string): Promise<SdkResult<T>> {
108
+ return this.getDataField<T>(`/api/pools/${addr}/tx-stats`);
109
+ }
110
+
111
+ getTransactions<T>(addr: string, limit: number = 20, offset: number = 0): Promise<SdkResult<T>> {
112
+ const qs = new URLSearchParams({ limit: String(limit), offset: String(offset) });
113
+ return this.getDataField<T>(`/api/pools/${addr}/transactions?${qs.toString()}`);
114
+ }
115
+
116
+ getSwapRoute<T>(
117
+ tokenIn: string,
118
+ tokenOut: string,
119
+ amountIn: string
120
+ ): Promise<SdkResult<T>> {
121
+ const qs = new URLSearchParams({ tokenIn, tokenOut, amountIn });
122
+ return this.getDataField<T>(`/api/swap/route?${qs.toString()}`);
123
+ }
124
+
125
+ getTokenPrices(mints: string[]): Promise<SdkResult<PriceMap>> {
126
+ const qs = new URLSearchParams({ mints: mints.join(",") });
127
+ return this.get<PriceMap>(`/api/prices?${qs.toString()}`);
128
+ }
129
+
130
+ getStats(
131
+ kind: StatsKind,
132
+ window: StatsWindow = "7d",
133
+ poolAddr?: string,
134
+ unit: "usd" | "token" = "usd"
135
+ ): Promise<SdkResult<StatsSeries>> {
136
+ const qs = new URLSearchParams({ window, unit });
137
+ if (poolAddr) qs.set("pool", poolAddr);
138
+ return this.get<StatsSeries>(`/api/stats/${kind}?${qs.toString()}`);
139
+ }
140
+
141
+ /** Generic GET with retry. Callers that need it for other endpoints. */
142
+ get<T>(path: string): Promise<SdkResult<T>> {
143
+ return this.request<T>("GET", path);
144
+ }
145
+
146
+ post<T>(path: string, body: unknown): Promise<SdkResult<T>> {
147
+ return this.request<T>("POST", path, body);
148
+ }
149
+
150
+ private async request<T>(
151
+ method: "GET" | "POST",
152
+ path: string,
153
+ body?: unknown
154
+ ): Promise<SdkResult<T>> {
155
+ const url = `${this.endpoint}${path}`;
156
+ const fetchOpts: RequestInit = {
157
+ method,
158
+ headers: this.headers,
159
+ body: body === undefined ? undefined : JSON.stringify(body),
160
+ };
161
+ const raw = await safeCall(async () => {
162
+ const res = await fetch(url, fetchOpts);
163
+ if (!res.ok) {
164
+ throw new Error(`${method} ${path} → HTTP ${res.status} ${res.statusText}`);
165
+ }
166
+ return (await res.json()) as T;
167
+ });
168
+ if (!raw.ok) return raw;
169
+ return ok(raw.data);
170
+ }
171
+
172
+ /**
173
+ * Fetch a response envelope of the form `{ data: T, ... }` and unwrap
174
+ * the `.data` field. The existing Cube backend wraps most endpoints
175
+ * this way.
176
+ */
177
+ private async getDataField<T>(path: string): Promise<SdkResult<T>> {
178
+ const res = await this.get<{ data: T }>(path);
179
+ if (!res.ok) return res;
180
+ return ok(res.data?.data);
181
+ }
182
+
183
+ private async postDataField<T>(path: string, body: unknown): Promise<SdkResult<T>> {
184
+ const res = await this.post<{ data: T }>(path, body);
185
+ if (!res.ok) return res;
186
+ return ok(res.data?.data);
187
+ }
188
+
189
+ /** Fetch the full envelope (for endpoints that return meta alongside data). */
190
+ private async getEnvelope<T>(path: string): Promise<SdkResult<T>> {
191
+ return this.get<T>(path);
192
+ }
193
+ }
@@ -0,0 +1,492 @@
1
+ import { PublicKey, Commitment } from "@solana/web3.js";
2
+ import BN from "bn.js";
3
+ import { CubeConfig } from "../config";
4
+ import { PoolInfo, PoolTokenInfo } from "../types/pool";
5
+ import { SdkResult, err, ok } from "../types/result";
6
+ import { SwapQuote, SingleTokenDepositQuote } from "../types/tx";
7
+ import { CubicPoolEvent } from "../types/events";
8
+ import { RpcClient } from "./RpcClient";
9
+ import { decodePoolAccount } from "../parsers/poolAccount";
10
+ import { decodeMintAccount } from "../parsers/mintAccount";
11
+ import { parseCubicPoolEvents } from "../parsers/events";
12
+ import { deriveAta, deriveBptMint, deriveHelperPda } from "../utils/pda";
13
+ import { resolveKnownToken } from "../config/tokens";
14
+ import {
15
+ calcBptOutGivenExactTokensIn,
16
+ calcOutGivenIn,
17
+ calcSpotOut,
18
+ calcTokensOutGivenBptIn,
19
+ } from "../math/cubicMath";
20
+ import { applySlippage, applySwapFee, priceImpactHbps } from "../math/slippage";
21
+ import { capDepositAmountsToLpRatio, computeAllocations } from "../math/singleToken";
22
+ import {
23
+ buildAddLiquidityTx,
24
+ buildRemoveLiquidityTx,
25
+ buildSingleTokenDepositTx,
26
+ buildSwapTx,
27
+ } from "./tx-builders";
28
+ import {
29
+ AddLiquidityParams,
30
+ BuiltTx,
31
+ RemoveLiquidityParams,
32
+ SingleTokenDepositParams,
33
+ SwapParams,
34
+ } from "../types/tx";
35
+ import { SingleTokenDepositClient } from "./SingleTokenDepositClient";
36
+
37
+ export interface CubicPoolClientParams {
38
+ config: CubeConfig;
39
+ poolAddress: PublicKey;
40
+ rpc:
41
+ | RpcClient
42
+ | {
43
+ endpoint: string;
44
+ apiKey?: string;
45
+ commitment?: Commitment;
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Per-pool client. Fetch the state with `sync()`, then call any of the
51
+ * quote / buildTx methods off the cached snapshot.
52
+ */
53
+ export class CubicPoolClient {
54
+ readonly config: CubeConfig;
55
+ readonly poolAddress: PublicKey;
56
+ readonly rpc: RpcClient;
57
+
58
+ private cache: PoolInfo | undefined;
59
+
60
+ constructor(params: CubicPoolClientParams) {
61
+ this.config = params.config;
62
+ this.poolAddress = params.poolAddress;
63
+ this.rpc =
64
+ params.rpc instanceof RpcClient
65
+ ? params.rpc
66
+ : new RpcClient({
67
+ endpoint: params.rpc.endpoint,
68
+ apiKey: params.rpc.apiKey,
69
+ commitment: params.rpc.commitment ?? params.config.defaults.rpcCommitment,
70
+ });
71
+ }
72
+
73
+ /** Last-fetched pool state. `undefined` before first `sync()`. */
74
+ getCached(): PoolInfo | undefined {
75
+ return this.cache;
76
+ }
77
+
78
+ /**
79
+ * Fetch and decode the pool account + BPT mint + per-token mint decimals.
80
+ * Safe to call repeatedly; subsequent calls replace the cache.
81
+ */
82
+ async sync(): Promise<SdkResult<PoolInfo>> {
83
+ const poolInfo = await this.rpc.getAccountInfo(this.poolAddress);
84
+ if (!poolInfo.ok) return poolInfo;
85
+ if (poolInfo.data === null) {
86
+ return err("account_not_found", `Pool ${this.poolAddress.toBase58()} does not exist on-chain`);
87
+ }
88
+ let raw;
89
+ try {
90
+ raw = decodePoolAccount(poolInfo.data.data);
91
+ } catch (e) {
92
+ return err("parse_failure", "Could not decode pool account", e);
93
+ }
94
+ const n = raw.tokenCount;
95
+ const mintAddrs = raw.tokenMints.slice(0, n);
96
+ const [bptMint, _bptBump] = deriveBptMint(this.config.programs.cubicPool, this.poolAddress);
97
+
98
+ const mintInfos = await this.rpc.getMultipleAccountsInfo([bptMint, ...mintAddrs]);
99
+ if (!mintInfos.ok) return mintInfos;
100
+ const [bptMintData, ...tokenMintDatas] = mintInfos.data;
101
+ if (!bptMintData) {
102
+ return err("account_not_found", "BPT mint account missing");
103
+ }
104
+ let bptMintAcc;
105
+ try {
106
+ bptMintAcc = decodeMintAccount(bptMintData);
107
+ } catch (e) {
108
+ return err("parse_failure", "Could not decode BPT mint", e);
109
+ }
110
+ const tokens: PoolTokenInfo[] = [];
111
+ for (let i = 0; i < n; i++) {
112
+ const mintRaw = tokenMintDatas[i];
113
+ if (!mintRaw) {
114
+ return err("account_not_found", `Mint account missing for token index ${i}`);
115
+ }
116
+ let mintAcc;
117
+ try {
118
+ mintAcc = decodeMintAccount(mintRaw);
119
+ } catch (e) {
120
+ return err("parse_failure", `Could not decode mint ${mintAddrs[i].toBase58()}`, e);
121
+ }
122
+ const actualBalance = raw.actualBalances[i];
123
+ const virtualBalance = raw.virtualBalances[i];
124
+ const concentration =
125
+ virtualBalance.isZero() ? 0 : actualBalance.mul(new BN(1_000_000)).div(virtualBalance).toNumber() / 1_000_000;
126
+ tokens.push({
127
+ index: i,
128
+ mint: raw.tokenMints[i],
129
+ tokenProgram: raw.tokenPrograms[i],
130
+ decimals: mintAcc.decimals,
131
+ weightBps: raw.normalizedWeights[i].toNumber(),
132
+ virtualBalance,
133
+ actualBalance,
134
+ protocolFeesOwed: raw.protocolFeesOwed[i],
135
+ vault: deriveAta(this.poolAddress, raw.tokenMints[i], raw.tokenPrograms[i]),
136
+ metadata:
137
+ this.config.tokens?.[raw.tokenMints[i].toBase58()] ??
138
+ resolveKnownToken(raw.tokenMints[i].toBase58()),
139
+ concentration,
140
+ });
141
+ }
142
+
143
+ const info: PoolInfo = {
144
+ address: this.poolAddress,
145
+ config: raw.config,
146
+ bump: raw.bump,
147
+ poolId: raw.poolId,
148
+ tokenCount: n,
149
+ tokens,
150
+ bptMint,
151
+ bptTotalSupply: bptMintAcc.supply,
152
+ swapFeeRate: raw.swapFeeRate,
153
+ protocolFeeRate: raw.protocolFeeRate,
154
+ poolEnabled: raw.poolEnabled,
155
+ swapsEnabled: raw.swapsEnabled,
156
+ createdAt: raw.createdAt.toNumber(),
157
+ syncedAt: Date.now(),
158
+ };
159
+ this.cache = info;
160
+ return ok(info);
161
+ }
162
+
163
+ /** Derived helper PDA for this pool (used by single-token-deposit). */
164
+ helperPda(): PublicKey {
165
+ return deriveHelperPda(this.config.programs.singleTokenLiquidity, this.poolAddress)[0];
166
+ }
167
+
168
+ // ---------- Quote helpers (pure, require sync()) ----------
169
+
170
+ /**
171
+ * Quote a swap. Requires the cache to be populated (call `sync()` first).
172
+ * Returns exact on-chain math result + spot bound + price impact +
173
+ * slippage-derived minAmountOut.
174
+ */
175
+ quoteSwap(
176
+ tokenInIndex: number,
177
+ tokenOutIndex: number,
178
+ amountIn: BN,
179
+ slippageHundredthsBps?: number
180
+ ): SdkResult<SwapQuote> {
181
+ const pool = this.requireCache();
182
+ if (!pool.ok) return pool;
183
+ const { tokens, swapFeeRate, protocolFeeRate } = pool.data;
184
+ if (
185
+ tokenInIndex < 0 ||
186
+ tokenInIndex >= tokens.length ||
187
+ tokenOutIndex < 0 ||
188
+ tokenOutIndex >= tokens.length ||
189
+ tokenInIndex === tokenOutIndex
190
+ ) {
191
+ return err("invalid_input", "Invalid tokenInIndex / tokenOutIndex");
192
+ }
193
+ const inTok = tokens[tokenInIndex];
194
+ const outTok = tokens[tokenOutIndex];
195
+ const slip = slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
196
+
197
+ try {
198
+ const amountBI = BigInt(amountIn.toString());
199
+ const amountAfterFee = applySwapFee(amountBI, swapFeeRate);
200
+ const feeAmount = amountBI - amountAfterFee;
201
+ const protocolFeeAmount =
202
+ (feeAmount * BigInt(protocolFeeRate)) / BigInt(10_000);
203
+
204
+ // Mirror cubic-pool's swap.rs: raw virtual balances drive the math,
205
+ // lp_actual_out caps the output amount. Using lp_virtual here would
206
+ // diverge from the on-chain quote on pools with pending protocol fees.
207
+ const virtIn = BigInt(inTok.virtualBalance.toString());
208
+ const virtOut = BigInt(outTok.virtualBalance.toString());
209
+ const actualOut = BigInt(outTok.actualBalance.toString());
210
+ const pfoOut = BigInt(outTok.protocolFeesOwed.toString());
211
+ const lpActualOut = actualOut > pfoOut ? actualOut - pfoOut : 0n;
212
+
213
+ const amountOut = calcOutGivenIn({
214
+ virtualBalanceIn: virtIn,
215
+ weightInBps: BigInt(inTok.weightBps),
216
+ virtualBalanceOut: virtOut,
217
+ weightOutBps: BigInt(outTok.weightBps),
218
+ amountIn: amountAfterFee,
219
+ actualBalanceOut: lpActualOut,
220
+ });
221
+
222
+ const spotOut = calcSpotOut({
223
+ virtualBalanceIn: virtIn,
224
+ weightInBps: BigInt(inTok.weightBps),
225
+ virtualBalanceOut: virtOut,
226
+ weightOutBps: BigInt(outTok.weightBps),
227
+ amountIn: amountAfterFee,
228
+ });
229
+
230
+ const minAmountOut = applySlippage(amountOut, slip);
231
+ const impact = priceImpactHbps(spotOut, amountOut);
232
+ return ok({
233
+ tokenInIndex,
234
+ tokenOutIndex,
235
+ amountIn,
236
+ amountOut: new BN(amountOut.toString()),
237
+ spotOut: new BN(spotOut.toString()),
238
+ priceImpactHbps: impact,
239
+ feeAmount: new BN(feeAmount.toString()),
240
+ protocolFeeAmount: new BN(protocolFeeAmount.toString()),
241
+ minAmountOut: new BN(minAmountOut.toString()),
242
+ });
243
+ } catch (e) {
244
+ return err("math_overflow", "Swap quote math overflow", e);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Quote a single-token deposit: split amountIn by W-based shares, quote
250
+ * each swap leg, estimate resulting BPT.
251
+ */
252
+ quoteSingleTokenDeposit(
253
+ tokenInIndex: number,
254
+ amountIn: BN,
255
+ slippageHundredthsBps?: number
256
+ ): SdkResult<SingleTokenDepositQuote> {
257
+ const poolRes = this.requireCache();
258
+ if (!poolRes.ok) return poolRes;
259
+ const pool = poolRes.data;
260
+ if (tokenInIndex < 0 || tokenInIndex >= pool.tokens.length) {
261
+ return err("invalid_input", "Invalid tokenInIndex");
262
+ }
263
+ const inTok = pool.tokens[tokenInIndex];
264
+ if (inTok.actualBalance.isZero()) {
265
+ return err("invalid_input", "Input token is sidelined (actualBalance=0); pick a live token");
266
+ }
267
+ const slip = slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
268
+
269
+ try {
270
+ const actualBalances = pool.tokens.map((t) => BigInt(t.actualBalance.toString()));
271
+ const virtualBalances = pool.tokens.map((t) => BigInt(t.virtualBalance.toString()));
272
+ const protocolFeesOwed = pool.tokens.map((t) => BigInt(t.protocolFeesOwed.toString()));
273
+ const weightsBps = pool.tokens.map((t) => t.weightBps);
274
+ const amountInBI = BigInt(amountIn.toString());
275
+ // Mirror the helper contract: weight by LP-accessible balances
276
+ // (actual - protocolFeesOwed). This avoids the heavy 2-token optimizer
277
+ // which used to live here but exceeded the program's BPF CU budget.
278
+ const lpAccessibleBalances = actualBalances.map((actual, i) =>
279
+ actual > protocolFeesOwed[i] ? actual - protocolFeesOwed[i] : 0n
280
+ );
281
+ const alloc = computeAllocations({
282
+ actualBalances: lpAccessibleBalances,
283
+ virtualBalances,
284
+ weightsBps,
285
+ amountIn: amountInBI,
286
+ tokenInIndex,
287
+ });
288
+
289
+ const expectedOuts: BN[] = [];
290
+ const minOuts: BN[] = [];
291
+ const sidelined: number[] = [];
292
+ const simActual = [...actualBalances];
293
+ const simVirtual = [...virtualBalances];
294
+ const simProtocolFees = [...protocolFeesOwed];
295
+ const depositAmounts = pool.tokens.map(() => 0n);
296
+ let remainingInput = amountInBI;
297
+ for (let i = 0; i < pool.tokens.length; i++) {
298
+ if (actualBalances[i] === 0n) {
299
+ sidelined.push(i);
300
+ expectedOuts.push(new BN(0));
301
+ minOuts.push(new BN(0));
302
+ continue;
303
+ }
304
+ if (i === tokenInIndex || alloc.allocations[i] === 0n) {
305
+ expectedOuts.push(new BN(0));
306
+ minOuts.push(new BN(0));
307
+ continue;
308
+ }
309
+ const swapAmount = alloc.allocations[i];
310
+ remainingInput -= swapAmount;
311
+ const amountAfterFee = applySwapFee(swapAmount, pool.swapFeeRate);
312
+ const feeAmount = swapAmount - amountAfterFee;
313
+ const protocolFeeAmount =
314
+ (feeAmount * BigInt(pool.protocolFeeRate)) / 10_000n;
315
+ // Match cubic-pool/swap.rs exactly: it uses RAW virtual balances and
316
+ // takes lp_actual_out only as the output cap. Earlier the SDK used
317
+ // LP-virtuals here (and the helper did too), causing slippage drift
318
+ // on pools with pending protocol fees.
319
+ const lpActualOut =
320
+ simActual[i] > simProtocolFees[i] ? simActual[i] - simProtocolFees[i] : 0n;
321
+ const out = calcOutGivenIn({
322
+ virtualBalanceIn: simVirtual[tokenInIndex],
323
+ weightInBps: BigInt(weightsBps[tokenInIndex]),
324
+ virtualBalanceOut: simVirtual[i],
325
+ weightOutBps: BigInt(weightsBps[i]),
326
+ amountIn: amountAfterFee,
327
+ actualBalanceOut: lpActualOut,
328
+ });
329
+ const min = applySlippage(out > 0n ? out - 1n : 0n, slip);
330
+ expectedOuts.push(new BN(out.toString()));
331
+ minOuts.push(new BN(min.toString()));
332
+
333
+ simActual[tokenInIndex] += swapAmount;
334
+ simVirtual[tokenInIndex] += amountAfterFee;
335
+ simProtocolFees[tokenInIndex] += protocolFeeAmount;
336
+ simActual[i] -= out;
337
+ simVirtual[i] -= out;
338
+ depositAmounts[i] = out;
339
+ }
340
+ depositAmounts[tokenInIndex] = remainingInput;
341
+
342
+ const capped = capDepositAmountsToLpRatio({
343
+ helperBalances: depositAmounts,
344
+ actualBalances: simActual,
345
+ protocolFeesOwed: simProtocolFees,
346
+ });
347
+ const estBpt = calcBptOutGivenExactTokensIn(
348
+ capped.lpBalancesForAdd,
349
+ capped.depositAmounts,
350
+ BigInt(pool.bptTotalSupply.toString())
351
+ );
352
+
353
+ return ok({
354
+ tokenInIndex,
355
+ amountIn,
356
+ allocations: alloc.allocations.map((b) => new BN(b.toString())),
357
+ expectedOuts,
358
+ minOuts,
359
+ depositedAmounts: capped.depositAmounts.map((b) => new BN(b.toString())),
360
+ refundAmounts: capped.refundAmounts.map((b) => new BN(b.toString())),
361
+ estimatedBpt: new BN(estBpt.toString()),
362
+ sidelinedTokenIndices: sidelined,
363
+ });
364
+ } catch (e) {
365
+ return err("math_overflow", "Single-token deposit quote failed", e);
366
+ }
367
+ }
368
+
369
+ /** Proportional-withdraw quote for a given BPT amount. */
370
+ quoteRemove(bptIn: BN): SdkResult<{ tokenOuts: BN[] }> {
371
+ const poolRes = this.requireCache();
372
+ if (!poolRes.ok) return poolRes;
373
+ const pool = poolRes.data;
374
+ if (pool.bptTotalSupply.isZero()) {
375
+ return err("invalid_input", "Pool has zero BPT supply");
376
+ }
377
+ try {
378
+ // Match cubic-pool/remove_liquidity.rs exactly: it computes
379
+ // token_amounts as `actual_balances[i] * bpt_amount / bpt_supply`
380
+ // against the raw stored actual (no protocol-fee subtraction). Use
381
+ // the same input here so the SDK quote equals what the contract
382
+ // actually transfers.
383
+ const bals = pool.tokens.map((t) => BigInt(t.actualBalance.toString()));
384
+ const outs = calcTokensOutGivenBptIn(bals, BigInt(bptIn.toString()), BigInt(pool.bptTotalSupply.toString()));
385
+ return ok({ tokenOuts: outs.map((o) => new BN(o.toString())) });
386
+ } catch (e) {
387
+ return err("math_overflow", "Remove quote failed", e);
388
+ }
389
+ }
390
+
391
+ // ---------- Transaction builders ----------
392
+ //
393
+ // For external SDK users, deposit/swap/remove are one-call operations via
394
+ // the CubicPoolClient. The `singleTokenDeposit` field is a proxy into the
395
+ // dedicated SingleTokenDepositClient as the user expects: importing this
396
+ // class is enough, no need to know the helper program exists.
397
+
398
+ /** Build a swap transaction. Requires `sync()` first. */
399
+ buildSwapTx(params: SwapParams): SdkResult<BuiltTx> {
400
+ const poolRes = this.requireCache();
401
+ if (!poolRes.ok) return poolRes;
402
+ const slip = params.slippageHundredthsBps ?? this.config.defaults.slippageHundredthsBps;
403
+ const minOut = params.minAmountOut
404
+ ? params.minAmountOut
405
+ : (() => {
406
+ const q = this.quoteSwap(params.tokenInIndex, params.tokenOutIndex, params.amountIn, slip);
407
+ return q.ok ? q.data.minAmountOut : new BN(0);
408
+ })();
409
+ try {
410
+ const tx = buildSwapTx(this.config, poolRes.data, { ...params, minAmountOut: minOut });
411
+ return ok(tx);
412
+ } catch (e) {
413
+ return err("tx_build_failed", "Failed to build swap tx", e);
414
+ }
415
+ }
416
+
417
+ /** Build a proportional add-liquidity transaction. Requires `sync()` first. */
418
+ buildAddLiquidityTx(params: AddLiquidityParams): SdkResult<BuiltTx> {
419
+ const poolRes = this.requireCache();
420
+ if (!poolRes.ok) return poolRes;
421
+ if (params.tokenAmounts.length !== poolRes.data.tokenCount) {
422
+ return err("invalid_input", "tokenAmounts length must equal pool.tokenCount");
423
+ }
424
+ try {
425
+ return ok(buildAddLiquidityTx(this.config, poolRes.data, params));
426
+ } catch (e) {
427
+ return err("tx_build_failed", "Failed to build add_liquidity tx", e);
428
+ }
429
+ }
430
+
431
+ /** Build a proportional remove-liquidity transaction. Requires `sync()` first. */
432
+ buildRemoveLiquidityTx(params: RemoveLiquidityParams): SdkResult<BuiltTx> {
433
+ const poolRes = this.requireCache();
434
+ if (!poolRes.ok) return poolRes;
435
+ if (
436
+ params.minimumTokenAmounts &&
437
+ params.minimumTokenAmounts.length !== poolRes.data.tokenCount
438
+ ) {
439
+ return err("invalid_input", "minimumTokenAmounts length must equal pool.tokenCount");
440
+ }
441
+ try {
442
+ return ok(buildRemoveLiquidityTx(this.config, poolRes.data, params));
443
+ } catch (e) {
444
+ return err("tx_build_failed", "Failed to build remove_liquidity tx", e);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Build a single-token deposit transaction (swaps + add_liquidity via the
450
+ * helper program + BPT forward to user). Requires `sync()` first.
451
+ */
452
+ buildSingleTokenDepositTx(params: SingleTokenDepositParams): SdkResult<BuiltTx> {
453
+ const poolRes = this.requireCache();
454
+ if (!poolRes.ok) return poolRes;
455
+ if (params.amountIn.lten(0)) return err("invalid_input", "amountIn must be > 0");
456
+ try {
457
+ return ok(buildSingleTokenDepositTx(this.config, poolRes.data, params));
458
+ } catch (e) {
459
+ return err("tx_build_failed", "Failed to build single-token deposit tx", e);
460
+ }
461
+ }
462
+
463
+ /** Proxy into the dedicated SingleTokenDepositClient for consumers who
464
+ * want the quote/build methods exposed on a focused object. */
465
+ get singleTokenDeposit(): SingleTokenDepositClient {
466
+ if (!this._std) {
467
+ this._std = new SingleTokenDepositClient({
468
+ config: this.config,
469
+ poolAddress: this.poolAddress,
470
+ poolClient: this,
471
+ });
472
+ }
473
+ return this._std;
474
+ }
475
+ private _std?: SingleTokenDepositClient;
476
+
477
+ // ---------- Event helpers ----------
478
+
479
+ /** Parse logs from a confirmed transaction into typed events. */
480
+ parseEventsFromLogs(logs: string[]): CubicPoolEvent[] {
481
+ return parseCubicPoolEvents(logs);
482
+ }
483
+
484
+ // ---------- Internals ----------
485
+
486
+ private requireCache(): SdkResult<PoolInfo> {
487
+ if (!this.cache) {
488
+ return err("invalid_input", "Call `sync()` first to populate pool state");
489
+ }
490
+ return ok(this.cache);
491
+ }
492
+ }