@coffer_so/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/dist/clients/AdminClient.d.ts +194 -0
  4. package/dist/clients/AdminClient.d.ts.map +1 -0
  5. package/dist/clients/AdminClient.js +482 -0
  6. package/dist/clients/AdminClient.js.map +1 -0
  7. package/dist/clients/CofferBackendClient.d.ts +877 -0
  8. package/dist/clients/CofferBackendClient.d.ts.map +1 -0
  9. package/dist/clients/CofferBackendClient.js +536 -0
  10. package/dist/clients/CofferBackendClient.js.map +1 -0
  11. package/dist/clients/CubeBackendClient.d.ts +877 -0
  12. package/dist/clients/CubeBackendClient.d.ts.map +1 -0
  13. package/dist/clients/CubeBackendClient.js +536 -0
  14. package/dist/clients/CubeBackendClient.js.map +1 -0
  15. package/dist/clients/CubicPoolClient.d.ts +120 -0
  16. package/dist/clients/CubicPoolClient.d.ts.map +1 -0
  17. package/dist/clients/CubicPoolClient.js +687 -0
  18. package/dist/clients/CubicPoolClient.js.map +1 -0
  19. package/dist/clients/PoolFactoryClient.d.ts +53 -0
  20. package/dist/clients/PoolFactoryClient.d.ts.map +1 -0
  21. package/dist/clients/PoolFactoryClient.js +91 -0
  22. package/dist/clients/PoolFactoryClient.js.map +1 -0
  23. package/dist/clients/RpcClient.d.ts +49 -0
  24. package/dist/clients/RpcClient.d.ts.map +1 -0
  25. package/dist/clients/RpcClient.js +135 -0
  26. package/dist/clients/RpcClient.js.map +1 -0
  27. package/dist/clients/SingleTokenDepositClient.d.ts +67 -0
  28. package/dist/clients/SingleTokenDepositClient.d.ts.map +1 -0
  29. package/dist/clients/SingleTokenDepositClient.js +105 -0
  30. package/dist/clients/SingleTokenDepositClient.js.map +1 -0
  31. package/dist/clients/contract-instructions.d.ts +24 -0
  32. package/dist/clients/contract-instructions.d.ts.map +1 -0
  33. package/dist/clients/contract-instructions.js +130 -0
  34. package/dist/clients/contract-instructions.js.map +1 -0
  35. package/dist/clients/index.d.ts +10 -0
  36. package/dist/clients/index.d.ts.map +1 -0
  37. package/dist/clients/index.js +26 -0
  38. package/dist/clients/index.js.map +1 -0
  39. package/dist/clients/quote-math.d.ts +15 -0
  40. package/dist/clients/quote-math.d.ts.map +1 -0
  41. package/dist/clients/quote-math.js +98 -0
  42. package/dist/clients/quote-math.js.map +1 -0
  43. package/dist/clients/tx-builders.d.ts +231 -0
  44. package/dist/clients/tx-builders.d.ts.map +1 -0
  45. package/dist/clients/tx-builders.js +861 -0
  46. package/dist/clients/tx-builders.js.map +1 -0
  47. package/dist/clients/versioned.d.ts +34 -0
  48. package/dist/clients/versioned.d.ts.map +1 -0
  49. package/dist/clients/versioned.js +55 -0
  50. package/dist/clients/versioned.js.map +1 -0
  51. package/dist/config/index.d.ts +65 -0
  52. package/dist/config/index.d.ts.map +1 -0
  53. package/dist/config/index.js +75 -0
  54. package/dist/config/index.js.map +1 -0
  55. package/dist/config/networks.d.ts +12 -0
  56. package/dist/config/networks.d.ts.map +1 -0
  57. package/dist/config/networks.js +51 -0
  58. package/dist/config/networks.js.map +1 -0
  59. package/dist/config/tokens.d.ts +19 -0
  60. package/dist/config/tokens.d.ts.map +1 -0
  61. package/dist/config/tokens.js +43 -0
  62. package/dist/config/tokens.js.map +1 -0
  63. package/dist/idl/cubic_pool.json +4203 -0
  64. package/dist/idl/index.d.ts +1769 -0
  65. package/dist/idl/index.d.ts.map +1 -0
  66. package/dist/idl/index.js +18 -0
  67. package/dist/idl/index.js.map +1 -0
  68. package/dist/idl/protocol_admin.json +2615 -0
  69. package/dist/idl/single_token_liquidity.json +1121 -0
  70. package/dist/index.d.ts +75 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +206 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/math/cubicMath.d.ts +40 -0
  75. package/dist/math/cubicMath.d.ts.map +1 -0
  76. package/dist/math/cubicMath.js +82 -0
  77. package/dist/math/cubicMath.js.map +1 -0
  78. package/dist/math/fixedPoint.d.ts +19 -0
  79. package/dist/math/fixedPoint.d.ts.map +1 -0
  80. package/dist/math/fixedPoint.js +86 -0
  81. package/dist/math/fixedPoint.js.map +1 -0
  82. package/dist/math/index.d.ts +9 -0
  83. package/dist/math/index.d.ts.map +1 -0
  84. package/dist/math/index.js +30 -0
  85. package/dist/math/index.js.map +1 -0
  86. package/dist/math/logExp.d.ts +6 -0
  87. package/dist/math/logExp.d.ts.map +1 -0
  88. package/dist/math/logExp.js +68 -0
  89. package/dist/math/logExp.js.map +1 -0
  90. package/dist/math/lpLoss.d.ts +44 -0
  91. package/dist/math/lpLoss.d.ts.map +1 -0
  92. package/dist/math/lpLoss.js +103 -0
  93. package/dist/math/lpLoss.js.map +1 -0
  94. package/dist/math/maxSelloff.d.ts +164 -0
  95. package/dist/math/maxSelloff.d.ts.map +1 -0
  96. package/dist/math/maxSelloff.js +397 -0
  97. package/dist/math/maxSelloff.js.map +1 -0
  98. package/dist/math/singleToken.d.ts +54 -0
  99. package/dist/math/singleToken.d.ts.map +1 -0
  100. package/dist/math/singleToken.js +188 -0
  101. package/dist/math/singleToken.js.map +1 -0
  102. package/dist/math/slippage.d.ts +13 -0
  103. package/dist/math/slippage.d.ts.map +1 -0
  104. package/dist/math/slippage.js +44 -0
  105. package/dist/math/slippage.js.map +1 -0
  106. package/dist/math/surgeFee.d.ts +24 -0
  107. package/dist/math/surgeFee.d.ts.map +1 -0
  108. package/dist/math/surgeFee.js +85 -0
  109. package/dist/math/surgeFee.js.map +1 -0
  110. package/dist/math/weightedMath.d.ts +20 -0
  111. package/dist/math/weightedMath.d.ts.map +1 -0
  112. package/dist/math/weightedMath.js +66 -0
  113. package/dist/math/weightedMath.js.map +1 -0
  114. package/dist/parsers/borsh.d.ts +28 -0
  115. package/dist/parsers/borsh.d.ts.map +1 -0
  116. package/dist/parsers/borsh.js +104 -0
  117. package/dist/parsers/borsh.js.map +1 -0
  118. package/dist/parsers/contracts.d.ts +26 -0
  119. package/dist/parsers/contracts.d.ts.map +1 -0
  120. package/dist/parsers/contracts.js +62 -0
  121. package/dist/parsers/contracts.js.map +1 -0
  122. package/dist/parsers/events.d.ts +3 -0
  123. package/dist/parsers/events.d.ts.map +1 -0
  124. package/dist/parsers/events.js +281 -0
  125. package/dist/parsers/events.js.map +1 -0
  126. package/dist/parsers/index.d.ts +6 -0
  127. package/dist/parsers/index.d.ts.map +1 -0
  128. package/dist/parsers/index.js +22 -0
  129. package/dist/parsers/index.js.map +1 -0
  130. package/dist/parsers/mintAccount.d.ts +24 -0
  131. package/dist/parsers/mintAccount.d.ts.map +1 -0
  132. package/dist/parsers/mintAccount.js +27 -0
  133. package/dist/parsers/mintAccount.js.map +1 -0
  134. package/dist/parsers/poolAccount.d.ts +141 -0
  135. package/dist/parsers/poolAccount.d.ts.map +1 -0
  136. package/dist/parsers/poolAccount.js +224 -0
  137. package/dist/parsers/poolAccount.js.map +1 -0
  138. package/dist/types/contracts.d.ts +1340 -0
  139. package/dist/types/contracts.d.ts.map +1 -0
  140. package/dist/types/contracts.js +3 -0
  141. package/dist/types/contracts.js.map +1 -0
  142. package/dist/types/events.d.ts +172 -0
  143. package/dist/types/events.d.ts.map +1 -0
  144. package/dist/types/events.js +3 -0
  145. package/dist/types/events.js.map +1 -0
  146. package/dist/types/index.d.ts +6 -0
  147. package/dist/types/index.d.ts.map +1 -0
  148. package/dist/types/index.js +22 -0
  149. package/dist/types/index.js.map +1 -0
  150. package/dist/types/pool.d.ts +149 -0
  151. package/dist/types/pool.d.ts.map +1 -0
  152. package/dist/types/pool.js +3 -0
  153. package/dist/types/pool.js.map +1 -0
  154. package/dist/types/result.d.ts +24 -0
  155. package/dist/types/result.d.ts.map +1 -0
  156. package/dist/types/result.js +11 -0
  157. package/dist/types/result.js.map +1 -0
  158. package/dist/types/tx.d.ts +261 -0
  159. package/dist/types/tx.d.ts.map +1 -0
  160. package/dist/types/tx.js +3 -0
  161. package/dist/types/tx.js.map +1 -0
  162. package/dist/utils/errors.d.ts +44 -0
  163. package/dist/utils/errors.d.ts.map +1 -0
  164. package/dist/utils/errors.js +192 -0
  165. package/dist/utils/errors.js.map +1 -0
  166. package/dist/utils/extensions.d.ts +98 -0
  167. package/dist/utils/extensions.d.ts.map +1 -0
  168. package/dist/utils/extensions.js +187 -0
  169. package/dist/utils/extensions.js.map +1 -0
  170. package/dist/utils/index.d.ts +5 -0
  171. package/dist/utils/index.d.ts.map +1 -0
  172. package/dist/utils/index.js +21 -0
  173. package/dist/utils/index.js.map +1 -0
  174. package/dist/utils/pda.d.ts +8 -0
  175. package/dist/utils/pda.d.ts.map +1 -0
  176. package/dist/utils/pda.js +27 -0
  177. package/dist/utils/pda.js.map +1 -0
  178. package/dist/utils/retry.d.ts +22 -0
  179. package/dist/utils/retry.d.ts.map +1 -0
  180. package/dist/utils/retry.js +62 -0
  181. package/dist/utils/retry.js.map +1 -0
  182. package/package.json +54 -0
  183. package/src/clients/AdminClient.ts +608 -0
  184. package/src/clients/CofferBackendClient.ts +1413 -0
  185. package/src/clients/CubicPoolClient.ts +719 -0
  186. package/src/clients/PoolFactoryClient.ts +110 -0
  187. package/src/clients/RpcClient.ts +195 -0
  188. package/src/clients/SingleTokenDepositClient.ts +143 -0
  189. package/src/clients/contract-instructions.ts +115 -0
  190. package/src/clients/index.ts +9 -0
  191. package/src/clients/quote-math.ts +81 -0
  192. package/src/clients/tx-builders.ts +1096 -0
  193. package/src/clients/versioned.ts +80 -0
  194. package/src/config/index.ts +111 -0
  195. package/src/config/networks.ts +71 -0
  196. package/src/config/tokens.ts +52 -0
  197. package/src/idl/cubic_pool.json +4203 -0
  198. package/src/idl/index.ts +13 -0
  199. package/src/idl/protocol_admin.json +2615 -0
  200. package/src/idl/single_token_liquidity.json +1121 -0
  201. package/src/index.ts +262 -0
  202. package/src/math/cubicMath.ts +92 -0
  203. package/src/math/fixedPoint.ts +71 -0
  204. package/src/math/index.ts +13 -0
  205. package/src/math/logExp.ts +61 -0
  206. package/src/math/lpLoss.ts +160 -0
  207. package/src/math/maxSelloff.ts +564 -0
  208. package/src/math/singleToken.ts +251 -0
  209. package/src/math/slippage.ts +38 -0
  210. package/src/math/surgeFee.ts +101 -0
  211. package/src/math/weightedMath.ts +64 -0
  212. package/src/parsers/borsh.ts +106 -0
  213. package/src/parsers/contracts.ts +64 -0
  214. package/src/parsers/events.ts +275 -0
  215. package/src/parsers/index.ts +5 -0
  216. package/src/parsers/mintAccount.ts +44 -0
  217. package/src/parsers/poolAccount.ts +372 -0
  218. package/src/types/contracts.ts +238 -0
  219. package/src/types/events.ts +198 -0
  220. package/src/types/index.ts +5 -0
  221. package/src/types/pool.ts +147 -0
  222. package/src/types/result.ts +51 -0
  223. package/src/types/tx.ts +278 -0
  224. package/src/utils/errors.ts +210 -0
  225. package/src/utils/extensions.ts +196 -0
  226. package/src/utils/index.ts +4 -0
  227. package/src/utils/pda.ts +58 -0
  228. package/src/utils/retry.ts +85 -0
@@ -0,0 +1,1413 @@
1
+ import { SdkResult, err, ok } from "../types/result";
2
+ import { PoolSummary } from "../types/pool";
3
+ import { safeCall } from "../utils/retry";
4
+
5
+ export interface CofferBackendClientParams {
6
+ apiEndpoint: string;
7
+ apiKey?: string;
8
+ defaultHeaders?: Record<string, string>;
9
+ /**
10
+ * Called when tokens are refreshed automatically after a 401.
11
+ * The frontend should persist the new tokens (e.g. to localStorage).
12
+ */
13
+ onTokenRefreshed?: (tokens: AuthTokens) => void;
14
+ /**
15
+ * Called when both access and refresh tokens are expired/invalid.
16
+ * The frontend should trigger a full re-authentication (SIWS sign-in).
17
+ */
18
+ onAuthExpired?: () => void;
19
+ }
20
+
21
+ export type StatsKind =
22
+ | "tvl"
23
+ | "volume"
24
+ | "swap_count"
25
+ | "avg_swap"
26
+ | "median_swap"
27
+ | "fees_lp"
28
+ | "fees_protocol"
29
+ | "users_total"
30
+ | "dau"
31
+ | "mau"
32
+ | "deposits"
33
+ | "removals";
34
+
35
+ export type StatsWindow = "1d" | "7d" | "30d" | "all";
36
+
37
+ export interface StatsSeriesPoint {
38
+ t: number;
39
+ v: number;
40
+ }
41
+ export interface StatsSeries {
42
+ points: StatsSeriesPoint[];
43
+ }
44
+
45
+ export interface PriceMap {
46
+ [mint: string]: number;
47
+ }
48
+
49
+ // ── Swap route types ──
50
+
51
+ export interface SwapRouteEntry {
52
+ poolAddress: string;
53
+ poolName: string;
54
+ amountIn: string;
55
+ /** Raw expected output for this leg — NET of the v5 sell-off surge fee
56
+ * (what the user actually receives). */
57
+ expectedOut: string;
58
+ /** Raw minimum output for this leg after slippage — sign this into the
59
+ * swap instruction. */
60
+ minAmountOut: string;
61
+ percentage: number;
62
+ swapFee: number;
63
+ /** Estimated v5 sell-off surge fee for this leg (raw units of tokenOut).
64
+ * "0" in the common case — rises as the input token's max-selloff
65
+ * window fills. */
66
+ surgeFeeAmount: string;
67
+ /** Surge fee rate applied to this leg (%). 0 when the window is calm. */
68
+ surgeFeePct: number;
69
+ tokenProgramIn: string;
70
+ tokenProgramOut: string;
71
+ tokenInIndex: number;
72
+ tokenOutIndex: number;
73
+ vaultIn: string | null;
74
+ vaultOut: string | null;
75
+ }
76
+
77
+ export interface SwapRouteResponse {
78
+ routes: SwapRouteEntry[];
79
+ totalAmountIn: string;
80
+ totalExpectedOut: string;
81
+ /** Total raw minimum received after slippage (sum of per-leg
82
+ * minAmountOut). Always ≤ totalExpectedOut — display this as "Min
83
+ * received" instead of extrapolating from spot price. */
84
+ minReceived: string;
85
+ /** Slippage tolerance applied by the backend (basis points; 200 = 2%). */
86
+ slippageBps: number;
87
+ effectivePrice: number;
88
+ priceImpact: number;
89
+ spotPrice: number;
90
+ /** Weighted average fee across routes (%), INCLUDING the v5 surge fee
91
+ * when one is active. */
92
+ feePercent: number;
93
+ /** Total estimated v5 surge fee across legs (raw units of tokenOut).
94
+ * "0" in the common case. */
95
+ totalSurgeFee: string;
96
+ /** Estimated XP earned from this swap (based on LP fees generated) */
97
+ estimatedXp: number;
98
+ /** True when a personalized XP boost (e.g. referred user +3%) was
99
+ * applied to `estimatedXp` — requires the request to carry a JWT. */
100
+ xpBoostApplied: boolean;
101
+ }
102
+
103
+ // ── Leaderboard types ──
104
+
105
+ export interface LeaderboardEntry {
106
+ address: string;
107
+ points: number;
108
+ place: number;
109
+ }
110
+
111
+ export interface LeaderboardResponse {
112
+ total: number;
113
+ page: number;
114
+ limit: number;
115
+ data: LeaderboardEntry[];
116
+ }
117
+
118
+ export interface LeaderboardUserStats {
119
+ place: number;
120
+ address: string;
121
+ points: number;
122
+ lastAccrualSwapUsd: number;
123
+ lastAccrualLiqUsd: number;
124
+ lastAccrualAt: string | null;
125
+ }
126
+
127
+ export interface XpAccrualHistoryEntry {
128
+ accrualTime: string;
129
+ swapVolumeUsd: number;
130
+ swapXp: number;
131
+ lpValueUsd: number;
132
+ lpXp: number;
133
+ totalXp: number;
134
+ }
135
+
136
+ export interface XpAccrualHistoryResponse {
137
+ total: number;
138
+ page: number;
139
+ limit: number;
140
+ data: XpAccrualHistoryEntry[];
141
+ }
142
+
143
+ export interface EpochHistoryEntry {
144
+ epoch: number;
145
+ start: string;
146
+ end: string;
147
+ multiplier: number;
148
+ swapXpPerUsdLpFee: number;
149
+ lpXpPerUsd: number;
150
+ isCurrent: boolean;
151
+ }
152
+
153
+ export interface LeaderboardEpochResponse {
154
+ currentEpoch: number;
155
+ currentEpochStart: string;
156
+ nextEpochStart: string;
157
+ msUntilNextEpoch: number;
158
+ currentMultiplier: number;
159
+ baseRates: {
160
+ swapXpPerUsdLpFee: number;
161
+ lpXpPerUsd: number;
162
+ };
163
+ currentRates: {
164
+ swapXpPerUsdLpFee: number;
165
+ lpXpPerUsd: number;
166
+ };
167
+ epochs: EpochHistoryEntry[];
168
+ }
169
+
170
+ export interface LeaderboardStatsResponse {
171
+ /** Total number of users in the leaderboard */
172
+ totalUsers: number;
173
+ /** Sum of all XP points across all users */
174
+ totalXp: number;
175
+ }
176
+
177
+ // ── Campaign types ──
178
+
179
+ export interface CampaignStatusResponse {
180
+ /** Active campaign slug the backend is currently running. */
181
+ campaign: string;
182
+ participating: boolean;
183
+ /** ISO time of the ORIGINAL join; null when not participating. */
184
+ joinedAt: string | null;
185
+ }
186
+
187
+ export interface CampaignPrizeTier {
188
+ /** Inclusive place range this prize applies to. */
189
+ fromPlace: number;
190
+ toPlace: number;
191
+ usd: number;
192
+ }
193
+
194
+ export interface CampaignInfoResponse {
195
+ campaign: string;
196
+ startsAt: string;
197
+ endsAt: string;
198
+ /** Server-computed — drive the countdown from this, not client clocks. */
199
+ msUntilEnd: number;
200
+ ended: boolean;
201
+ prizePoolUsd: number;
202
+ /** Fixed for the whole campaign; paid out manually after the end. */
203
+ prizes: CampaignPrizeTier[];
204
+ rates: {
205
+ /** Same swap-XP rate as the leaderboard; LP XP is NOT accrued here. */
206
+ swapXpPerUsdLpFee: number;
207
+ };
208
+ }
209
+
210
+ export interface CampaignRankResponse {
211
+ campaign: string;
212
+ from: string;
213
+ /** Effective window end after server-side clamping to the campaign. */
214
+ to: string;
215
+ participating: boolean;
216
+ /**
217
+ * Place under the same ordering as the public top table; null when not
218
+ * participating or no swap XP earned inside the window.
219
+ */
220
+ place: number | null;
221
+ swapXp: number;
222
+ swapVolumeUsd: number;
223
+ /** Size of the ranked table — for "you are N of M" UI. */
224
+ totalRanked: number;
225
+ }
226
+
227
+ export interface CampaignTopEntry {
228
+ /** Continuous across pages. */
229
+ place: number;
230
+ address: string;
231
+ /** XP earned from swaps inside the window (LP XP not counted). */
232
+ swapXp: number;
233
+ swapVolumeUsd: number;
234
+ joinedAt: string;
235
+ }
236
+
237
+ export interface CampaignTopResponse {
238
+ campaign: string;
239
+ from: string;
240
+ /** Effective window end after server-side clamping to the campaign. */
241
+ to: string;
242
+ /** Participants with swap XP > 0 in the window. */
243
+ total: number;
244
+ page: number;
245
+ limit: number;
246
+ data: CampaignTopEntry[];
247
+ }
248
+
249
+ // ── Platform stats ──
250
+
251
+ export interface PlatformStatsResponse {
252
+ totalTvlUsd: number;
253
+ totalVirtualTvl: number;
254
+ totalVolume24h: number;
255
+ poolCount: number;
256
+ updatedAt: string | null;
257
+ }
258
+
259
+ // ── Pool admin types ──
260
+
261
+ export interface AdminPoolEntry {
262
+ poolAddress: string;
263
+ poolName: string;
264
+ /** Off-chain: mint shown first in the token list (null = default order). */
265
+ baseAssetMint: string | null;
266
+ /** Off-chain: short pool description. */
267
+ description: string | null;
268
+ tvlUsd: number;
269
+ apy: number;
270
+ volume24h: number;
271
+ swapFee: number;
272
+ poolEnabled: boolean;
273
+ swapsEnabled: boolean;
274
+ tokens: Array<{
275
+ mintAddress: string;
276
+ ticker: string;
277
+ imageUrl: string | null;
278
+ }>;
279
+ }
280
+
281
+ /** Off-chain, admin-editable pool settings. */
282
+ export interface UpdatePoolSettingsBody {
283
+ /** Mint of the base asset (one of the pool tokens); "" clears it. */
284
+ baseAssetMint?: string;
285
+ /** Short description; "" clears it. */
286
+ description?: string;
287
+ }
288
+
289
+ export interface AdminPoolsResponse {
290
+ pools: AdminPoolEntry[];
291
+ }
292
+
293
+ export interface IsAdminResponse {
294
+ poolAddress: string;
295
+ isAdmin: boolean;
296
+ }
297
+
298
+ export interface RenamePoolResponse {
299
+ poolAddress: string;
300
+ name: string;
301
+ updated: true;
302
+ }
303
+
304
+ // ── Referral types ──
305
+
306
+ export interface ReferralBindResponse {
307
+ referrer: string;
308
+ bound: true;
309
+ }
310
+
311
+ export interface ReferralRates {
312
+ l1Percent: number;
313
+ l2Percent: number;
314
+ }
315
+
316
+ export interface ReferralStats {
317
+ totalReferrals: number;
318
+ l1Count: number;
319
+ l2Count: number;
320
+ totalBonusPoints: number;
321
+ l1BonusPoints: number;
322
+ l2BonusPoints: number;
323
+ }
324
+
325
+ export interface ReferralStatusResponse {
326
+ referredBy: string | null;
327
+ referralCode: string;
328
+ customCodes: string[];
329
+ rates: ReferralRates;
330
+ stats: ReferralStats;
331
+ }
332
+
333
+ export interface ReferralEntry {
334
+ /** Wallet address of the referral */
335
+ address: string;
336
+ /** Total bonus XP this referral has earned for you */
337
+ earnedBonusXp: number;
338
+ /** When this user became your referral */
339
+ boundAt: string;
340
+ }
341
+
342
+ export interface ReferralListResponse {
343
+ total: number;
344
+ page: number;
345
+ limit: number;
346
+ data: ReferralEntry[];
347
+ }
348
+
349
+ // ── Portfolio types ──
350
+
351
+ export interface PortfolioValueChange {
352
+ abs: number;
353
+ pct: number;
354
+ }
355
+
356
+ export interface PortfolioSummaryResponse {
357
+ value: {
358
+ total: number;
359
+ inPositions: number;
360
+ inWallet: number;
361
+ change: {
362
+ d1: PortfolioValueChange;
363
+ d7: PortfolioValueChange;
364
+ };
365
+ };
366
+ totalPnl: {
367
+ net: number;
368
+ pct: number;
369
+ components: {
370
+ feesEarned: number;
371
+ ilCurrent: number;
372
+ ilRealized: number;
373
+ };
374
+ };
375
+ }
376
+
377
+ export interface PortfolioExposureToken {
378
+ symbol: string;
379
+ mint: string;
380
+ logo: string | null;
381
+ usd: number;
382
+ amount: number;
383
+ pct: number;
384
+ inLp: number;
385
+ inWallet: number;
386
+ pools: string[];
387
+ }
388
+
389
+ export interface PortfolioExposureResponse {
390
+ totalUsd: number;
391
+ tokens: PortfolioExposureToken[];
392
+ }
393
+
394
+ export interface PortfolioHistoryPoint {
395
+ t: number;
396
+ feesCumulative: number;
397
+ ilCumulative: number;
398
+ netPnl: number;
399
+ }
400
+
401
+ export interface PortfolioHistoryResponse {
402
+ range: string;
403
+ series: PortfolioHistoryPoint[];
404
+ }
405
+
406
+ export interface PortfolioPoolEntry {
407
+ poolAddress: string;
408
+ poolName: string;
409
+ tokens: Array<{ symbol: string; mint: string }>;
410
+ value: number;
411
+ feesEarned: number;
412
+ /** LIVE unrealized IL only (≤ 0); realized part is `ilRealized` */
413
+ il: number;
414
+ /** Result locked in on past withdrawals vs HODL ("Realized PnL" in UI) */
415
+ ilRealized: number;
416
+ netPnl: number;
417
+ /** POOL-wide APY, % — the same number as the pools page (legacy name). */
418
+ apr: number;
419
+ }
420
+
421
+ export interface PortfolioPoolsResponse {
422
+ pools: PortfolioPoolEntry[];
423
+ }
424
+
425
+ export interface PortfolioPoolHistoryPoint {
426
+ t: number;
427
+ feesCumulative: number;
428
+ ilCumulative: number;
429
+ }
430
+
431
+ export interface PortfolioPoolHistoryResponse {
432
+ poolAddress: string;
433
+ range: string;
434
+ series: PortfolioPoolHistoryPoint[];
435
+ }
436
+
437
+ // ── Portfolio v2 types (redesigned portfolio page) ──
438
+
439
+ export type PortfolioChartMetric = "networth" | "pnl" | "il" | "xp";
440
+ export type PortfolioChartRange = "24h" | "1w" | "1m" | "1y" | "all";
441
+
442
+ export interface PortfolioChartResponse {
443
+ metric: PortfolioChartMetric;
444
+ range: PortfolioChartRange;
445
+ /** Distance between series/line points, seconds. */
446
+ granularitySec: number;
447
+ /**
448
+ * Headline value: the last point. For `xp` — the lifetime XP total.
449
+ * For `il` — the NET result (profit + IL, the bottom line vs HODL).
450
+ */
451
+ current: number;
452
+ /**
453
+ * Change over the range (for XP: abs = XP earned in the range; for il:
454
+ * the net change). `pct` is null when the base is zero/near-zero
455
+ * (growth from nothing, or a signed pnl/net-il base under $1) — render
456
+ * it as "—".
457
+ */
458
+ change: { abs: number; pct: number | null };
459
+ /**
460
+ * True when the data is partially estimated: wallet history coverage
461
+ * hit its cap / the history API was unavailable (amounts frozen at the
462
+ * horizon), or LP events inside the range (il / pnl metrics).
463
+ */
464
+ approximate: boolean;
465
+ /**
466
+ * The single line, [unixSeconds, value] pairs ascending — for
467
+ * `networth`, `pnl` and `xp`. ABSENT for `il`, which returns two
468
+ * separate lines in `lines` instead.
469
+ *
470
+ * - networth: whole-portfolio USD value at each point.
471
+ * - pnl: cumulative profit/loss vs invested dollars (never-decreasing
472
+ * is NOT guaranteed — it moves with the market and with fixations).
473
+ * - xp: the total XP as of each point — cumulative, never decreasing.
474
+ */
475
+ series?: Array<[number, number]>;
476
+ /**
477
+ * `il` metric ONLY: two lines to draw on the same chart, both
478
+ * [unixSeconds, value] ascending on a shared grid, BOTH as POSITIVE
479
+ * magnitudes.
480
+ * - `profit`: cumulative LP fees earned (≥ 0, draw green).
481
+ * - `il`: impermanent loss as a positive number (≥ 0, draw red) — the
482
+ * red line already means "loss", so it carries no minus sign.
483
+ * The net LP result vs HODL is `profit − il`, and that is what
484
+ * `current` and `change` report. There is no `series` for this metric.
485
+ */
486
+ lines?: {
487
+ profit: Array<[number, number]>;
488
+ il: Array<[number, number]>;
489
+ };
490
+ }
491
+
492
+ export type PortfolioActivityType =
493
+ | "swap"
494
+ | "added"
495
+ | "removed"
496
+ | "zap"
497
+ | "sent"
498
+ | "received"
499
+ | "deployed";
500
+
501
+ export type PortfolioActivityFilter =
502
+ | "all"
503
+ | "liquidity"
504
+ | "zap"
505
+ | "swap"
506
+ | "transfer"
507
+ | "deployed";
508
+
509
+ export interface ActivityTokenInfo {
510
+ mint: string;
511
+ symbol: string | null;
512
+ logo: string | null;
513
+ }
514
+
515
+ export interface PortfolioActivityItem {
516
+ type: PortfolioActivityType;
517
+ signature: string;
518
+ /** Unix seconds. */
519
+ time: number;
520
+ /**
521
+ * Signed USD: + for added/zap/received, − for removed/sent, unsigned for
522
+ * swap; null when unpriceable and for deployed rows.
523
+ */
524
+ valueUsd: number | null;
525
+ pool: {
526
+ address: string;
527
+ name: string;
528
+ feePercent: number | null;
529
+ bptMint: string | null;
530
+ tokens: ActivityTokenInfo[];
531
+ /** Pool weights in % (for the deployed row's "80/20 weights" subtitle). */
532
+ weights?: number[];
533
+ } | null;
534
+ /** Only on swap rows. */
535
+ swap?: { tokenIn: ActivityTokenInfo; tokenOut: ActivityTokenInfo };
536
+ /** Only on sent/received rows (wallet-to-wallet LP token transfers). */
537
+ transfer?: { counterparty: string | null };
538
+ }
539
+
540
+ export interface PortfolioActivityResponse {
541
+ total: number;
542
+ page: number;
543
+ limit: number;
544
+ data: PortfolioActivityItem[];
545
+ }
546
+
547
+ export type SortOrder = "desc" | "asc";
548
+ export type PortfolioActivitySortField = "time" | "value";
549
+ export type PortfolioHoldingsSortField = "value" | "price" | "balance";
550
+
551
+ export interface PortfolioHoldingItem {
552
+ token: {
553
+ mint: string;
554
+ symbol: string | null;
555
+ /** Full token name (e.g. "USD Coin"); null when metadata has none. */
556
+ name: string | null;
557
+ logo: string | null;
558
+ };
559
+ priceUsd: number;
560
+ /** Total amount: wallet + decomposed LP positions. */
561
+ balance: number;
562
+ valueUsd: number;
563
+ /** Source breakdown for the tooltip; pct is of the token's balance. */
564
+ sources: {
565
+ wallet: { amount: number; pct: number };
566
+ pools: Array<{
567
+ poolAddress: string;
568
+ poolName: string;
569
+ amount: number;
570
+ pct: number;
571
+ }>;
572
+ };
573
+ }
574
+
575
+ export interface PortfolioHoldingsResponse {
576
+ total: number;
577
+ page: number;
578
+ limit: number;
579
+ data: PortfolioHoldingItem[];
580
+ }
581
+
582
+ export type PortfolioPositionsSortField =
583
+ | "value"
584
+ | "fees"
585
+ | "il"
586
+ | "pnl"
587
+ | "apy";
588
+
589
+ export interface PortfolioPositionItem {
590
+ pool: {
591
+ address: string;
592
+ name: string;
593
+ feePercent: number | null;
594
+ bptMint: string | null;
595
+ tokens: ActivityTokenInfo[];
596
+ };
597
+ /** Position value from the 30s-fresh wallet BPT balance. */
598
+ valueUsd: number;
599
+ /** Lifetime LP fees earned in this pool. */
600
+ feesEarned: number;
601
+ /** Live unrealized IL (≤ 0); the realized part is `ilRealized`. */
602
+ il: number;
603
+ ilRealized: number;
604
+ /** feesEarned + il + ilRealized. */
605
+ netPnl: number;
606
+ /** POOL-wide APY, % (24h-based) — the same number as the pools page. */
607
+ apy: number;
608
+ }
609
+
610
+ export interface PortfolioPositionsResponse {
611
+ /** Number of ACTIVE positions (the "N active positions" header). */
612
+ total: number;
613
+ page: number;
614
+ limit: number;
615
+ /** Σ value of ALL active positions (the header total), not the page. */
616
+ totalValueUsd: number;
617
+ data: PortfolioPositionItem[];
618
+ }
619
+
620
+ // ── Token pair-chart types (pool creation range picker) ──
621
+
622
+ export type PairChartRange = "1d" | "1w" | "1m" | "1y" | "all";
623
+
624
+ /**
625
+ * Price of token `a` expressed in units of token `b` over time — the
626
+ * one-line chart behind the range picker on the pool creation page.
627
+ */
628
+ export interface TokenPairChartResponse {
629
+ /** Numerator mint — the token being priced. */
630
+ a: string;
631
+ /** Denominator mint — the token `a` is priced IN. */
632
+ b: string;
633
+ range: PairChartRange;
634
+ /** Spacing between series points, seconds. */
635
+ granularitySec: number;
636
+ current: {
637
+ /**
638
+ * How many units of `b` one unit of `a` is worth right now — the
639
+ * big "1,284.52 SOL/USDT" headline number.
640
+ */
641
+ ratio: number;
642
+ /** Current USD price of `a` — the grey "$345.44" next to it. */
643
+ aUsd: number;
644
+ /** Current USD price of `b`. */
645
+ bUsd: number;
646
+ };
647
+ /**
648
+ * Change of the pair price over the range. `pct` is null when the
649
+ * range started from an unknown (zero) price — render "—".
650
+ */
651
+ change: { abs: number; pct: number | null };
652
+ /**
653
+ * [unixSeconds, priceAinB] ascending — ONE line. Values are rounded
654
+ * to 6 significant digits (pairs like BONK/SOL live around 1e-9).
655
+ */
656
+ series: Array<[number, number]>;
657
+ }
658
+
659
+ // ── Auth types ──
660
+
661
+ export interface NonceResponse {
662
+ nonce: string;
663
+ message: string;
664
+ }
665
+
666
+ /**
667
+ * Challenge for the transaction-based sign-in fallback (wallets whose
668
+ * signMessage path is broken — Ledger through mobile apps first of all).
669
+ */
670
+ export interface TxChallengeResponse {
671
+ nonce: string;
672
+ /** Base64-encoded UNSIGNED transaction to pass to wallet.signTransaction. */
673
+ transaction: string;
674
+ /** Human-readable memo embedded in the transaction (agreements + nonce). */
675
+ memo: string;
676
+ }
677
+
678
+ export interface AuthTokens {
679
+ accessToken: string;
680
+ refreshToken: string;
681
+ wallet: string;
682
+ expiresIn: string;
683
+ }
684
+
685
+ /**
686
+ * REST wrapper around the Coffer backend. Every method is a SdkResult; no
687
+ * exceptions escape. If a request fails, the result carries a
688
+ * human-readable error plus the original cause.
689
+ *
690
+ * Auto-refresh: when a request gets 401, the client automatically tries
691
+ * to refresh tokens via POST /api/auth/refresh. If successful, the
692
+ * original request is retried once with the new access token.
693
+ */
694
+ export class CofferBackendClient {
695
+ private readonly endpoint: string;
696
+ private readonly headers: Record<string, string>;
697
+ private refreshToken: string | null = null;
698
+ private refreshInFlight: Promise<boolean> | null = null;
699
+ private readonly onTokenRefreshed?: (tokens: AuthTokens) => void;
700
+ private readonly onAuthExpired?: () => void;
701
+
702
+ constructor(params: CofferBackendClientParams) {
703
+ this.endpoint = params.apiEndpoint.replace(/\/$/, "");
704
+ this.headers = {
705
+ "Content-Type": "application/json",
706
+ ...(params.apiKey ? { Authorization: `Bearer ${params.apiKey}` } : {}),
707
+ ...(params.defaultHeaders ?? {}),
708
+ };
709
+ this.onTokenRefreshed = params.onTokenRefreshed;
710
+ this.onAuthExpired = params.onAuthExpired;
711
+ }
712
+
713
+ listPools(): Promise<SdkResult<PoolSummary[]>> {
714
+ return this.get<PoolSummary[]>("/api/pools");
715
+ }
716
+
717
+ getPool(addr: string): Promise<SdkResult<PoolSummary>> {
718
+ return this.get<PoolSummary>(`/api/pools/${addr}`);
719
+ }
720
+
721
+ /**
722
+ * Raw pool-list response in the same envelope the backend returns
723
+ * (`{ data, hasMore, totalCount }`). Kept so the frontend's React
724
+ * Query hooks can map directly.
725
+ */
726
+ listPoolsRaw(
727
+ limit: number,
728
+ offset: number
729
+ ): Promise<SdkResult<{ data: unknown[]; hasMore: boolean; totalCount: number }>> {
730
+ const qs = new URLSearchParams({ limit: String(limit), offset: String(offset) });
731
+ return this.getEnvelope(`/api/pools?${qs.toString()}`);
732
+ }
733
+
734
+ getPoolRaw(addr: string): Promise<SdkResult<unknown>> {
735
+ return this.getDataField<unknown>(`/api/pools/${addr}`);
736
+ }
737
+
738
+ createPool<T>(body: unknown): Promise<SdkResult<T>> {
739
+ return this.postDataField<T>("/api/pools", body);
740
+ }
741
+
742
+ getPoolsByTokenPair<T>(tokenA: string, tokenB: string): Promise<SdkResult<T>> {
743
+ const qs = new URLSearchParams({ tokenA, tokenB });
744
+ return this.getDataField<T>(`/api/pools/by-pair?${qs.toString()}`);
745
+ }
746
+
747
+ getPlatformStats(): Promise<SdkResult<PlatformStatsResponse>> {
748
+ return this.getDataField<PlatformStatsResponse>("/api/pools/stats");
749
+ }
750
+
751
+ getPortfolio<T>(wallet: string): Promise<SdkResult<T>> {
752
+ return this.getDataField<T>(`/api/pools/portfolio?wallet=${encodeURIComponent(wallet)}`);
753
+ }
754
+
755
+ getAllTokens<T>(): Promise<SdkResult<T>> {
756
+ return this.getDataField<T>("/api/pools/tokens");
757
+ }
758
+
759
+ getTopTokens<T>(limit: number = 20): Promise<SdkResult<T>> {
760
+ return this.getDataField<T>(`/api/pools/top-tokens?limit=${limit}`);
761
+ }
762
+
763
+ getPoolTxStats<T>(addr: string): Promise<SdkResult<T>> {
764
+ return this.getDataField<T>(`/api/pools/${addr}/tx-stats`);
765
+ }
766
+
767
+ getTransactions<T>(
768
+ addr: string,
769
+ options?: {
770
+ limit?: number;
771
+ offset?: number;
772
+ type?: "swap" | "add_liquidity" | "remove_liquidity";
773
+ user?: string;
774
+ },
775
+ ): Promise<SdkResult<T>> {
776
+ const qs = new URLSearchParams({
777
+ limit: String(options?.limit ?? 20),
778
+ offset: String(options?.offset ?? 0),
779
+ });
780
+ if (options?.type) qs.set("type", options.type);
781
+ if (options?.user) qs.set("user", options.user);
782
+ return this.getDataField<T>(`/api/pools/${addr}/transactions?${qs.toString()}`);
783
+ }
784
+
785
+ getSwapRoute(
786
+ tokenIn: string,
787
+ tokenOut: string,
788
+ amountIn: string,
789
+ decimalsIn: number = 9,
790
+ slippageBps?: number,
791
+ /**
792
+ * Restrict the quote to this single pool: no split routing, all
793
+ * estimates (priceImpact against this pool's own spot, fees, XP) are
794
+ * computed as if swapping only through it.
795
+ */
796
+ pool?: string,
797
+ ): Promise<SdkResult<SwapRouteResponse>> {
798
+ const qs = new URLSearchParams({
799
+ tokenIn,
800
+ tokenOut,
801
+ amountIn,
802
+ decimalsIn: String(decimalsIn),
803
+ });
804
+ if (slippageBps !== undefined) {
805
+ qs.set('slippageBps', String(slippageBps));
806
+ }
807
+ if (pool !== undefined) {
808
+ qs.set('pool', pool);
809
+ }
810
+ return this.getDataField<SwapRouteResponse>(
811
+ `/api/pools/swap-route?${qs.toString()}`,
812
+ );
813
+ }
814
+
815
+ // ── Leaderboard ──
816
+
817
+ getLeaderboard(
818
+ page: number = 1,
819
+ limit: number = 20,
820
+ ): Promise<SdkResult<LeaderboardResponse>> {
821
+ const qs = new URLSearchParams({
822
+ page: String(page),
823
+ limit: String(limit),
824
+ });
825
+ return this.get<LeaderboardResponse>(`/api/leaderboard?${qs.toString()}`);
826
+ }
827
+
828
+ getLeaderboardUser(
829
+ address: string,
830
+ ): Promise<SdkResult<LeaderboardUserStats>> {
831
+ return this.getDataField<LeaderboardUserStats>(
832
+ `/api/leaderboard/user/${encodeURIComponent(address)}`,
833
+ );
834
+ }
835
+
836
+ getLeaderboardUserHistory(
837
+ address: string,
838
+ page: number = 1,
839
+ limit: number = 50,
840
+ ): Promise<SdkResult<XpAccrualHistoryResponse>> {
841
+ const qs = new URLSearchParams({
842
+ page: String(page),
843
+ limit: String(limit),
844
+ });
845
+ return this.get<XpAccrualHistoryResponse>(
846
+ `/api/leaderboard/user/${encodeURIComponent(address)}/history?${qs.toString()}`,
847
+ );
848
+ }
849
+
850
+ getLeaderboardEpoch(): Promise<SdkResult<LeaderboardEpochResponse>> {
851
+ return this.get<LeaderboardEpochResponse>("/api/leaderboard/epoch");
852
+ }
853
+
854
+ /** Get overall leaderboard stats: total users and total XP. */
855
+ getLeaderboardStats(): Promise<SdkResult<LeaderboardStatsResponse>> {
856
+ return this.get<LeaderboardStatsResponse>("/api/leaderboard/stats");
857
+ }
858
+
859
+ /**
860
+ * Join the active swap-XP campaign. Requires SIWS auth (`setTokens`);
861
+ * the wallet comes from the JWT. IDEMPOTENT — repeat calls are no-ops
862
+ * returning the same status with the original join time.
863
+ */
864
+ joinCampaign(): Promise<SdkResult<CampaignStatusResponse>> {
865
+ return this.post<CampaignStatusResponse>("/api/campaign/join", {});
866
+ }
867
+
868
+ /** Participation status of the authenticated wallet (requires SIWS auth). */
869
+ getCampaignStatus(): Promise<SdkResult<CampaignStatusResponse>> {
870
+ return this.get<CampaignStatusResponse>("/api/campaign/me");
871
+ }
872
+
873
+ /** Active campaign card: window, server-side countdown, prizes. Public. */
874
+ getCampaignInfo(): Promise<SdkResult<CampaignInfoResponse>> {
875
+ return this.get<CampaignInfoResponse>("/api/campaign/info");
876
+ }
877
+
878
+ /**
879
+ * The authenticated wallet's own place in the campaign standings
880
+ * (requires SIWS auth). Same window semantics and ordering as
881
+ * getCampaignTop, so the place always matches the public table.
882
+ */
883
+ getCampaignRank(
884
+ from?: string,
885
+ to?: string,
886
+ ): Promise<SdkResult<CampaignRankResponse>> {
887
+ const qs = new URLSearchParams();
888
+ if (from) qs.set("from", from);
889
+ if (to) qs.set("to", to);
890
+ return this.get<CampaignRankResponse>(
891
+ `/api/campaign/me/rank?${qs.toString()}`,
892
+ );
893
+ }
894
+
895
+ /**
896
+ * Campaign standings: participants only, ranked by swap XP inside
897
+ * [from, to). Both dates are optional and CLAMPED into the campaign
898
+ * window server-side (defaults: the whole campaign) — omit both for
899
+ * the live standings of the running campaign; they freeze by
900
+ * themselves once it ends. Public endpoint, paginated like the
901
+ * leaderboard.
902
+ */
903
+ getCampaignTop(
904
+ from?: string,
905
+ to?: string,
906
+ page: number = 1,
907
+ limit: number = 20,
908
+ ): Promise<SdkResult<CampaignTopResponse>> {
909
+ const qs = new URLSearchParams({
910
+ page: String(page),
911
+ limit: String(limit),
912
+ });
913
+ if (from) qs.set("from", from);
914
+ if (to) qs.set("to", to);
915
+ return this.get<CampaignTopResponse>(`/api/campaign/top?${qs.toString()}`);
916
+ }
917
+
918
+ getTokenPrices(mints: string[]): Promise<SdkResult<PriceMap>> {
919
+ const qs = new URLSearchParams({ mints: mints.join(",") });
920
+ return this.get<PriceMap>(`/api/prices?${qs.toString()}`);
921
+ }
922
+
923
+ getStats(
924
+ kind: StatsKind,
925
+ window: StatsWindow = "7d",
926
+ poolAddr?: string,
927
+ unit: "usd" | "token" = "usd"
928
+ ): Promise<SdkResult<StatsSeries>> {
929
+ const qs = new URLSearchParams({ window, unit });
930
+ if (poolAddr) qs.set("pool", poolAddr);
931
+ return this.get<StatsSeries>(`/api/stats/${kind}?${qs.toString()}`);
932
+ }
933
+
934
+ // ── Pool admin (auth required) ──
935
+
936
+ /**
937
+ * Get pools where the authenticated user is the on-chain admin.
938
+ * Requires authentication.
939
+ */
940
+ getAdminPools(): Promise<SdkResult<AdminPoolsResponse>> {
941
+ return this.get<AdminPoolsResponse>("/api/pools/admin/my-pools");
942
+ }
943
+
944
+ /**
945
+ * Check if the authenticated user is the on-chain admin of a specific pool.
946
+ * Requires authentication.
947
+ */
948
+ isPoolAdmin(poolAddress: string): Promise<SdkResult<IsAdminResponse>> {
949
+ return this.get<IsAdminResponse>(
950
+ `/api/pools/admin/is-admin/${encodeURIComponent(poolAddress)}`,
951
+ );
952
+ }
953
+
954
+ /**
955
+ * Rename a pool. Only the on-chain pool admin can do this.
956
+ * Requires authentication.
957
+ */
958
+ renamePool(poolAddress: string, name: string): Promise<SdkResult<RenamePoolResponse>> {
959
+ return this.put<RenamePoolResponse>(
960
+ `/api/pools/admin/${encodeURIComponent(poolAddress)}/name`,
961
+ { name },
962
+ );
963
+ }
964
+
965
+ /**
966
+ * Update off-chain pool settings (base asset, description). Only fields
967
+ * present are changed; empty string clears a field. Base asset must be
968
+ * one of the pool tokens. Pool admin only; requires authentication.
969
+ */
970
+ updatePoolSettings<T>(
971
+ poolAddress: string,
972
+ settings: UpdatePoolSettingsBody,
973
+ ): Promise<SdkResult<T>> {
974
+ return this.put<T>(
975
+ `/api/pools/admin/${encodeURIComponent(poolAddress)}/settings`,
976
+ settings,
977
+ );
978
+ }
979
+
980
+ // ── Referral ──
981
+
982
+ /**
983
+ * Bind the authenticated user as a referral of the given referrer.
984
+ * The code can be a wallet address or a custom referral code.
985
+ * Optionally pass UTM parameters from the referral link for analytics.
986
+ * Requires authentication (setTokens must be called first).
987
+ */
988
+ bindReferral(
989
+ code: string,
990
+ utm?: {
991
+ source?: string;
992
+ medium?: string;
993
+ campaign?: string;
994
+ content?: string;
995
+ term?: string;
996
+ },
997
+ ): Promise<SdkResult<ReferralBindResponse>> {
998
+ const body: Record<string, unknown> = { code };
999
+ if (utm) body.utm = utm;
1000
+ return this.post<ReferralBindResponse>("/api/referral/bind", body);
1001
+ }
1002
+
1003
+ /**
1004
+ * Get the authenticated user's referral status: referrer, referral code,
1005
+ * custom codes, bonus rates (L1/L2 %), and aggregated stats.
1006
+ * Requires authentication.
1007
+ */
1008
+ getReferralStatus(): Promise<SdkResult<ReferralStatusResponse>> {
1009
+ return this.get<ReferralStatusResponse>("/api/referral/my");
1010
+ }
1011
+
1012
+ /**
1013
+ * Get a paginated list of the authenticated user's direct referrals (L1).
1014
+ * Requires authentication.
1015
+ */
1016
+ getMyReferrals(
1017
+ page: number = 1,
1018
+ limit: number = 20,
1019
+ ): Promise<SdkResult<ReferralListResponse>> {
1020
+ const qs = new URLSearchParams({
1021
+ page: String(page),
1022
+ limit: String(limit),
1023
+ });
1024
+ return this.get<ReferralListResponse>(
1025
+ `/api/referral/my/referrals?${qs.toString()}`,
1026
+ );
1027
+ }
1028
+
1029
+ // ── Portfolio (auth required) ──
1030
+
1031
+ /**
1032
+ * Portfolio summary: value (LP + wallet) + total PnL (fees, IL current, IL realized).
1033
+ * Returns 404 if user has no LP positions.
1034
+ */
1035
+ getPortfolioSummary(): Promise<SdkResult<PortfolioSummaryResponse>> {
1036
+ return this.get<PortfolioSummaryResponse>("/api/portfolio/summary");
1037
+ }
1038
+
1039
+ /**
1040
+ * Token exposure: per-token breakdown of LP positions + wallet balances.
1041
+ */
1042
+ getPortfolioExposure(): Promise<SdkResult<PortfolioExposureResponse>> {
1043
+ return this.get<PortfolioExposureResponse>("/api/portfolio/exposure");
1044
+ }
1045
+
1046
+ /**
1047
+ * Portfolio history for charts (fees cumulative + IL cumulative).
1048
+ * Daily data points for the selected range.
1049
+ */
1050
+ getPortfolioHistory(
1051
+ range: "7d" | "30d" | "90d" | "all" = "30d",
1052
+ ): Promise<SdkResult<PortfolioHistoryResponse>> {
1053
+ return this.get<PortfolioHistoryResponse>(
1054
+ `/api/portfolio/history?range=${range}`,
1055
+ );
1056
+ }
1057
+
1058
+ /**
1059
+ * Per-pool portfolio metrics (value, fees, unrealized IL, realized
1060
+ * IL/PnL, net PnL, APR).
1061
+ */
1062
+ getPortfolioPools(): Promise<SdkResult<PortfolioPoolsResponse>> {
1063
+ return this.get<PortfolioPoolsResponse>("/api/portfolio/pools");
1064
+ }
1065
+
1066
+ /**
1067
+ * Per-pool history (lazy-loaded on row expand).
1068
+ * Fees cumulative + IL cumulative series for a single pool.
1069
+ */
1070
+ getPortfolioPoolHistory(
1071
+ poolAddress: string,
1072
+ range: "7d" | "30d" | "90d" | "all" = "30d",
1073
+ ): Promise<SdkResult<PortfolioPoolHistoryResponse>> {
1074
+ return this.get<PortfolioPoolHistoryResponse>(
1075
+ `/api/portfolio/pools/${encodeURIComponent(poolAddress)}/history?range=${range}`,
1076
+ );
1077
+ }
1078
+
1079
+ // ── Portfolio v2 (redesigned portfolio page, auth required) ──
1080
+
1081
+ /**
1082
+ * Chart data for the portfolio page chart card. Most metrics fill
1083
+ * `series` (one line); `il` fills `lines` (two lines) instead.
1084
+ *
1085
+ * Metrics: `networth` — whole portfolio value over time (wallet tokens +
1086
+ * LP positions, reconstructed from on-chain wallet history × historical
1087
+ * prices); `pnl` — CUMULATIVE profit/loss vs invested dollars:
1088
+ * unrealized (LP value minus entry-priced capital) + profit FIXED at
1089
+ * every exit (withdrawal = received USD − capital share, transfer-out =
1090
+ * amount × LP price at that moment − capital share) — continuous at
1091
+ * both top-ups and exits;
1092
+ * `il` — TWO lines in `lines`, both POSITIVE: `profit` (cumulative LP
1093
+ * fees earned) and `il` (impermanent loss magnitude, vs HODL); there is
1094
+ * no `series` for this metric, and `current`/`change` report the net
1095
+ * (profit − il);
1096
+ * `xp` — the total XP as of each point (cumulative, never decreasing).
1097
+ *
1098
+ * `all` range spans at least one year even for recent users, so it is
1099
+ * never shorter than `1y`.
1100
+ */
1101
+ getPortfolioChart(
1102
+ metric: PortfolioChartMetric,
1103
+ range: PortfolioChartRange,
1104
+ ): Promise<SdkResult<PortfolioChartResponse>> {
1105
+ const qs = new URLSearchParams({ metric, range });
1106
+ return this.get<PortfolioChartResponse>(
1107
+ `/api/portfolio/v2/chart?${qs.toString()}`,
1108
+ );
1109
+ }
1110
+
1111
+ /**
1112
+ * On-chain activity feed: swaps, liquidity added/removed, zaps
1113
+ * (single-token deposits), LP tokens sent/received (wallet transfers
1114
+ * with counterparties) and pools deployed by the user. Newest first,
1115
+ * page/limit pagination. The Recent Activity widget is `{ limit: 4 }`;
1116
+ * filters: `liquidity` = added + removed, `transfer` = sent + received.
1117
+ */
1118
+ getPortfolioActivity(options?: {
1119
+ page?: number;
1120
+ limit?: number;
1121
+ type?: PortfolioActivityFilter;
1122
+ /** Sort column; unpriced rows (deployed) always go last on "value". */
1123
+ sort?: PortfolioActivitySortField;
1124
+ order?: SortOrder;
1125
+ }): Promise<SdkResult<PortfolioActivityResponse>> {
1126
+ const qs = new URLSearchParams({
1127
+ page: String(options?.page ?? 1),
1128
+ limit: String(options?.limit ?? 20),
1129
+ type: options?.type ?? "all",
1130
+ sort: options?.sort ?? "time",
1131
+ order: options?.order ?? "desc",
1132
+ });
1133
+ return this.get<PortfolioActivityResponse>(
1134
+ `/api/portfolio/v2/activity?${qs.toString()}`,
1135
+ );
1136
+ }
1137
+
1138
+ /**
1139
+ * Token Holdings: every token the user holds, with LP positions
1140
+ * DECOMPOSED into their underlying tokens (per-pool share of actual
1141
+ * balances — what the user would receive on withdrawal). LP tokens
1142
+ * themselves are never listed. Each row carries a source breakdown
1143
+ * (wallet + per-pool amounts/percentages) for the Source tooltip.
1144
+ * The widget is `{ limit: 4 }`; sortable by value/price/balance.
1145
+ */
1146
+ getPortfolioHoldings(options?: {
1147
+ page?: number;
1148
+ limit?: number;
1149
+ sort?: PortfolioHoldingsSortField;
1150
+ order?: SortOrder;
1151
+ }): Promise<SdkResult<PortfolioHoldingsResponse>> {
1152
+ const qs = new URLSearchParams({
1153
+ page: String(options?.page ?? 1),
1154
+ limit: String(options?.limit ?? 20),
1155
+ sort: options?.sort ?? "value",
1156
+ order: options?.order ?? "desc",
1157
+ });
1158
+ return this.get<PortfolioHoldingsResponse>(
1159
+ `/api/portfolio/v2/holdings?${qs.toString()}`,
1160
+ );
1161
+ }
1162
+
1163
+ /**
1164
+ * Position Pools: the user's ACTIVE LP positions with per-pool money
1165
+ * metrics — value (from the fresh wallet BPT balance), fees earned,
1166
+ * unrealized + realized IL, net PnL and personal compounded APY.
1167
+ * A freshly received (transferred) position appears immediately with
1168
+ * its value and zeroed metrics until the background snapshot builds.
1169
+ */
1170
+ getPortfolioPositions(options?: {
1171
+ page?: number;
1172
+ limit?: number;
1173
+ sort?: PortfolioPositionsSortField;
1174
+ order?: SortOrder;
1175
+ }): Promise<SdkResult<PortfolioPositionsResponse>> {
1176
+ const qs = new URLSearchParams({
1177
+ page: String(options?.page ?? 1),
1178
+ limit: String(options?.limit ?? 20),
1179
+ sort: options?.sort ?? "value",
1180
+ order: options?.order ?? "desc",
1181
+ });
1182
+ return this.get<PortfolioPositionsResponse>(
1183
+ `/api/portfolio/v2/positions?${qs.toString()}`,
1184
+ );
1185
+ }
1186
+
1187
+ // ── Token pair-chart (pool creation range picker, public) ──
1188
+
1189
+ /**
1190
+ * Price of token `a` expressed in token `b` over time — one line for
1191
+ * the range-picker chart on the pool creation page. Works for ANY two
1192
+ * Solana mints; no auth required.
1193
+ *
1194
+ * Cheap to call in bursts: the backend caches price history PER TOKEN,
1195
+ * so charting every token of a pool against one base costs at most one
1196
+ * upstream fetch per unique mint — flipping the base token reuses all
1197
+ * of them. Rate-limited per IP (90/min) on top of the global limiter.
1198
+ *
1199
+ * `"all"` is capped at one year — the historical price horizon for
1200
+ * arbitrary mints.
1201
+ */
1202
+ getTokenPairChart(
1203
+ a: string,
1204
+ b: string,
1205
+ range: PairChartRange,
1206
+ ): Promise<SdkResult<TokenPairChartResponse>> {
1207
+ const qs = new URLSearchParams({ a, b, range });
1208
+ return this.get<TokenPairChartResponse>(
1209
+ `/api/tokens/pair-chart?${qs.toString()}`,
1210
+ );
1211
+ }
1212
+
1213
+ // ── Auth ──
1214
+
1215
+ /** Request a SIWS nonce + pre-built message for the given wallet. */
1216
+ getNonce(wallet: string): Promise<SdkResult<NonceResponse>> {
1217
+ return this.get<NonceResponse>(
1218
+ `/api/auth/nonce?wallet=${encodeURIComponent(wallet)}`,
1219
+ );
1220
+ }
1221
+
1222
+ /** Submit signed SIWS message to receive access + refresh tokens. */
1223
+ verifySignature(
1224
+ message: string,
1225
+ signature: string,
1226
+ ): Promise<SdkResult<AuthTokens>> {
1227
+ return this.post<AuthTokens>("/api/auth/verify", {
1228
+ message,
1229
+ signature,
1230
+ });
1231
+ }
1232
+
1233
+ /**
1234
+ * Request a sign-in challenge transaction (fallback for wallets whose
1235
+ * signMessage is broken: Ledger via mobile apps, etc.). The returned
1236
+ * transaction is a 0-lamport self-transfer plus a memo with the
1237
+ * agreements statement and a single-use nonce; it is verified offline
1238
+ * by the backend and never broadcast on-chain.
1239
+ */
1240
+ getTxChallenge(wallet: string): Promise<SdkResult<TxChallengeResponse>> {
1241
+ return this.get<TxChallengeResponse>(
1242
+ `/api/auth/tx-challenge?wallet=${encodeURIComponent(wallet)}`,
1243
+ );
1244
+ }
1245
+
1246
+ /**
1247
+ * Submit the signed challenge transaction (base64, from
1248
+ * wallet.signTransaction + serialize) to receive the same access +
1249
+ * refresh token pair as verifySignature().
1250
+ */
1251
+ verifyTransaction(
1252
+ signedTransactionBase64: string,
1253
+ ): Promise<SdkResult<AuthTokens>> {
1254
+ return this.post<AuthTokens>("/api/auth/verify-tx", {
1255
+ transaction: signedTransactionBase64,
1256
+ });
1257
+ }
1258
+
1259
+ /**
1260
+ * Set both tokens. Call this after verifySignature() and on app init
1261
+ * (restoring tokens from storage).
1262
+ */
1263
+ setTokens(accessToken: string, refreshToken: string): void {
1264
+ this.headers["Authorization"] = `Bearer ${accessToken}`;
1265
+ this.refreshToken = refreshToken;
1266
+ }
1267
+
1268
+ /** Clear both tokens (logout). */
1269
+ clearTokens(): void {
1270
+ delete this.headers["Authorization"];
1271
+ this.refreshToken = null;
1272
+ }
1273
+
1274
+ /** @deprecated Use setTokens() instead. */
1275
+ setAccessToken(token: string): void {
1276
+ this.headers["Authorization"] = `Bearer ${token}`;
1277
+ }
1278
+
1279
+ /** @deprecated Use clearTokens() instead. */
1280
+ clearAccessToken(): void {
1281
+ delete this.headers["Authorization"];
1282
+ this.refreshToken = null;
1283
+ }
1284
+
1285
+ /** Generic GET with retry. Callers that need it for other endpoints. */
1286
+ get<T>(path: string): Promise<SdkResult<T>> {
1287
+ return this.requestWithRefresh<T>("GET", path);
1288
+ }
1289
+
1290
+ post<T>(path: string, body: unknown): Promise<SdkResult<T>> {
1291
+ return this.requestWithRefresh<T>("POST", path, body);
1292
+ }
1293
+
1294
+ put<T>(path: string, body: unknown): Promise<SdkResult<T>> {
1295
+ return this.requestWithRefresh<T>("PUT", path, body);
1296
+ }
1297
+
1298
+ // ── Private: HTTP layer with auto-refresh ──
1299
+
1300
+ /**
1301
+ * Core request method with auto-refresh on 401.
1302
+ * If a request gets 401 and we have a refresh token:
1303
+ * 1. Call POST /api/auth/refresh (deduplicated if concurrent)
1304
+ * 2. On success: update tokens, notify via callback, retry original request
1305
+ * 3. On failure: notify via onAuthExpired callback, return original error
1306
+ */
1307
+ private async requestWithRefresh<T>(
1308
+ method: "GET" | "POST" | "PUT",
1309
+ path: string,
1310
+ body?: unknown,
1311
+ ): Promise<SdkResult<T>> {
1312
+ const result = await this.rawRequest<T>(method, path, body);
1313
+
1314
+ // Don't auto-refresh for auth endpoints themselves
1315
+ const isAuthPath = path.startsWith("/api/auth/");
1316
+ if (!isAuthPath && !result.ok && this.is401(result) && this.refreshToken) {
1317
+ const refreshed = await this.tryRefresh();
1318
+ if (refreshed) {
1319
+ return this.rawRequest<T>(method, path, body);
1320
+ }
1321
+ }
1322
+
1323
+ return result;
1324
+ }
1325
+
1326
+ private async rawRequest<T>(
1327
+ method: "GET" | "POST" | "PUT",
1328
+ path: string,
1329
+ body?: unknown
1330
+ ): Promise<SdkResult<T>> {
1331
+ const url = `${this.endpoint}${path}`;
1332
+ const fetchOpts: RequestInit = {
1333
+ method,
1334
+ headers: this.headers,
1335
+ body: body === undefined ? undefined : JSON.stringify(body),
1336
+ };
1337
+ const raw = await safeCall(async () => {
1338
+ const res = await fetch(url, fetchOpts);
1339
+ if (!res.ok) {
1340
+ const error = new Error(
1341
+ `${method} ${path} → HTTP ${res.status} ${res.statusText}`,
1342
+ );
1343
+ (error as any).status = res.status;
1344
+ throw error;
1345
+ }
1346
+ return (await res.json()) as T;
1347
+ });
1348
+ if (!raw.ok) return raw;
1349
+ return ok(raw.data);
1350
+ }
1351
+
1352
+ /**
1353
+ * Attempt to refresh tokens. Returns true if successful.
1354
+ * Deduplicates concurrent refresh attempts.
1355
+ */
1356
+ private async tryRefresh(): Promise<boolean> {
1357
+ // Deduplicate: if a refresh is already in flight, wait for it
1358
+ if (this.refreshInFlight) {
1359
+ return this.refreshInFlight;
1360
+ }
1361
+
1362
+ this.refreshInFlight = this.doRefresh();
1363
+ try {
1364
+ return await this.refreshInFlight;
1365
+ } finally {
1366
+ this.refreshInFlight = null;
1367
+ }
1368
+ }
1369
+
1370
+ private async doRefresh(): Promise<boolean> {
1371
+ const res = await this.rawRequest<AuthTokens>("POST", "/api/auth/refresh", {
1372
+ refreshToken: this.refreshToken,
1373
+ });
1374
+
1375
+ if (res.ok) {
1376
+ this.setTokens(res.data.accessToken, res.data.refreshToken);
1377
+ this.onTokenRefreshed?.(res.data);
1378
+ return true;
1379
+ }
1380
+
1381
+ // Refresh failed — both tokens are dead
1382
+ this.clearTokens();
1383
+ this.onAuthExpired?.();
1384
+ return false;
1385
+ }
1386
+
1387
+ private is401(result: SdkResult<unknown>): boolean {
1388
+ if (result.ok) return false;
1389
+ return result.error.humanMessage.includes("HTTP 401");
1390
+ }
1391
+
1392
+ /**
1393
+ * Fetch a response envelope of the form `{ data: T, ... }` and unwrap
1394
+ * the `.data` field. The existing Coffer backend wraps most endpoints
1395
+ * this way.
1396
+ */
1397
+ private async getDataField<T>(path: string): Promise<SdkResult<T>> {
1398
+ const res = await this.get<{ data: T }>(path);
1399
+ if (!res.ok) return res;
1400
+ return ok(res.data?.data);
1401
+ }
1402
+
1403
+ private async postDataField<T>(path: string, body: unknown): Promise<SdkResult<T>> {
1404
+ const res = await this.post<{ data: T }>(path, body);
1405
+ if (!res.ok) return res;
1406
+ return ok(res.data?.data);
1407
+ }
1408
+
1409
+ /** Fetch the full envelope (for endpoints that return meta alongside data). */
1410
+ private async getEnvelope<T>(path: string): Promise<SdkResult<T>> {
1411
+ return this.get<T>(path);
1412
+ }
1413
+ }