@agg-build/hooks 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -393,7 +393,7 @@ Optional (only needed for wallet-based sign-in / deposits):
393
393
  ## Links
394
394
 
395
395
  - Documentation — <https://docs.agg.market/>
396
- - Demo app — <https://demo.agg.market/>
396
+ - Demo app — <https://agg.market/>
397
397
  - Vanilla SDK — [`@agg-build/sdk`](https://www.npmjs.com/package/@agg-build/sdk)
398
398
  - Trading UI — [`@agg-build/ui`](https://www.npmjs.com/package/@agg-build/ui)
399
399
  - Connect / sign-in UX — [`@agg-build/auth`](https://www.npmjs.com/package/@agg-build/auth)
@@ -0,0 +1,31 @@
1
+ import {
2
+ __async,
3
+ useAggClient
4
+ } from "./chunk-V6VNA7MX.mjs";
5
+
6
+ // src/use-ramp-quotes.ts
7
+ import { useMutation } from "@tanstack/react-query";
8
+ function useRampQuotes() {
9
+ const client = useAggClient();
10
+ return useMutation({
11
+ mutationFn: (params) => __async(null, null, function* () {
12
+ return client.getRampQuotes(params);
13
+ })
14
+ });
15
+ }
16
+
17
+ // src/use-ramp-session.ts
18
+ import { useMutation as useMutation2 } from "@tanstack/react-query";
19
+ function useRampSession() {
20
+ const client = useAggClient();
21
+ return useMutation2({
22
+ mutationFn: (params) => __async(null, null, function* () {
23
+ return client.createRampSession(params);
24
+ })
25
+ });
26
+ }
27
+
28
+ export {
29
+ useRampQuotes,
30
+ useRampSession
31
+ };
@@ -0,0 +1,389 @@
1
+ import {
2
+ __async,
3
+ invalidateBalanceQueries,
4
+ useAggBalanceState,
5
+ useAggClient,
6
+ useAggWebSocket,
7
+ useAggWebSocketConnectionState,
8
+ useDepositAddresses,
9
+ useManagedBalances,
10
+ useOnWithdrawalLifecycle,
11
+ useSyncBalances,
12
+ useWithdrawManaged
13
+ } from "./chunk-V6VNA7MX.mjs";
14
+
15
+ // src/withdraw/use-withdraw-flow.ts
16
+ import { useCallback, useEffect, useMemo, useState } from "react";
17
+ var EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
18
+ var SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
19
+ var SOLANA_CHAIN_ID = 792703809;
20
+ var WITHDRAWAL_SUPPORTED_CHAIN_IDS = /* @__PURE__ */ new Set([
21
+ 1,
22
+ // Ethereum
23
+ 137,
24
+ // Polygon
25
+ 42161,
26
+ // Arbitrum
27
+ 8453,
28
+ // Base
29
+ 56,
30
+ // BNB
31
+ SOLANA_CHAIN_ID
32
+ ]);
33
+ var isValidDestinationAddress = (address, chainId) => {
34
+ if (chainId === SOLANA_CHAIN_ID) return SOLANA_ADDRESS_REGEX.test(address);
35
+ return EVM_ADDRESS_REGEX.test(address);
36
+ };
37
+ var DEFAULT_WITHDRAW_SUMMARY = {
38
+ amountReceived: "",
39
+ network: "",
40
+ toWallet: "",
41
+ fees: "\u2014"
42
+ };
43
+ var formatRawTokenAmount = (rawAmount, decimals) => {
44
+ if (!rawAmount) return "0";
45
+ const raw = BigInt(rawAmount);
46
+ const divisor = BigInt(`1${"0".repeat(decimals)}`);
47
+ const whole = raw / divisor;
48
+ const remainder = raw % divisor;
49
+ if (remainder === BigInt(0)) return whole.toString();
50
+ return `${whole.toString()}.${remainder.toString().padStart(decimals, "0").replace(/0+$/, "")}`;
51
+ };
52
+ var formatRawTokenAmountForDisplay = (rawAmount, decimals, fractionDigits = 2) => {
53
+ const [wholePart, fractionalPart = ""] = formatRawTokenAmount(rawAmount, decimals).split(".");
54
+ const normalizedFraction = fractionalPart.padEnd(fractionDigits, "0").slice(0, fractionDigits);
55
+ return `${wholePart}.${normalizedFraction}`;
56
+ };
57
+ var parseTokenAmountToRaw = (amount, decimals) => {
58
+ const normalizedAmount = amount.trim();
59
+ if (!normalizedAmount || !/^\d*(?:\.\d*)?$/.test(normalizedAmount)) {
60
+ return null;
61
+ }
62
+ const [wholePart = "0", fractionalPart = ""] = normalizedAmount.split(".");
63
+ if (fractionalPart.length > decimals) return null;
64
+ const normalizedWhole = wholePart === "" ? "0" : wholePart;
65
+ const normalizedFraction = fractionalPart.padEnd(decimals, "0");
66
+ try {
67
+ return `${BigInt(normalizedWhole)}${normalizedFraction}`.replace(/^0+(?=\d)/, "");
68
+ } catch (e) {
69
+ return null;
70
+ }
71
+ };
72
+ var shortenAddress = (address) => address.length > 12 ? `${address.slice(0, 6)}...${address.slice(-4)}` : address;
73
+ function useWithdrawFlow(options) {
74
+ var _a, _b;
75
+ const { open, onOpenChange } = options;
76
+ const { totalBalance } = useAggBalanceState();
77
+ const { balances } = useManagedBalances({ enabled: open });
78
+ const { supportedChains } = useDepositAddresses({ enabled: open });
79
+ const withdrawMutation = useWithdrawManaged();
80
+ const syncBalances = useSyncBalances();
81
+ const ws = useAggWebSocket();
82
+ useEffect(() => {
83
+ if (!open) return;
84
+ syncBalances.mutate(void 0, {
85
+ onError: () => {
86
+ }
87
+ });
88
+ }, [open]);
89
+ const [withdrawDestination, setWithdrawDestination] = useState("");
90
+ const [withdrawAmount, setWithdrawAmount] = useState("");
91
+ const [withdrawToken, setWithdrawToken] = useState("USDC");
92
+ const [withdrawNetwork, setWithdrawNetwork] = useState("");
93
+ const [withdrawSummary, setWithdrawSummary] = useState(DEFAULT_WITHDRAW_SUMMARY);
94
+ const [withdrawalId, setWithdrawalId] = useState(null);
95
+ const networkOptions = useMemo(
96
+ () => (supportedChains != null ? supportedChains : []).filter((chain) => WITHDRAWAL_SUPPORTED_CHAIN_IDS.has(chain.chainId)).map((chain) => ({
97
+ value: String(chain.chainId),
98
+ label: chain.name
99
+ })),
100
+ [supportedChains]
101
+ );
102
+ const resolvedWithdrawNetwork = withdrawNetwork || ((_a = networkOptions[0]) == null ? void 0 : _a.value) || "";
103
+ useEffect(() => {
104
+ if (!networkOptions.length) return;
105
+ if (!withdrawNetwork || !networkOptions.some((option) => option.value === withdrawNetwork)) {
106
+ setWithdrawNetwork(networkOptions[0].value);
107
+ }
108
+ }, [networkOptions, withdrawNetwork]);
109
+ const tokenOptions = useMemo(() => {
110
+ var _a2, _b2, _c;
111
+ const selectedChainId = Number(resolvedWithdrawNetwork);
112
+ const supportedTokensForNetwork = (_b2 = (_a2 = supportedChains == null ? void 0 : supportedChains.find((chain) => chain.chainId === selectedChainId)) == null ? void 0 : _a2.tokens) != null ? _b2 : [];
113
+ const withdrawableSymbols = new Set(supportedTokensForNetwork.map((token) => token.symbol));
114
+ return ((_c = balances == null ? void 0 : balances.cash) != null ? _c : []).filter((cashEntry) => withdrawableSymbols.has(cashEntry.tokenSymbol)).map((cashEntry) => ({
115
+ value: cashEntry.tokenSymbol,
116
+ label: cashEntry.tokenSymbol
117
+ }));
118
+ }, [balances, resolvedWithdrawNetwork, supportedChains]);
119
+ const resolvedWithdrawToken = tokenOptions.some((option) => option.value === withdrawToken) ? withdrawToken : ((_b = tokenOptions[0]) == null ? void 0 : _b.value) || "";
120
+ useEffect(() => {
121
+ if (!tokenOptions.length) return;
122
+ if (!tokenOptions.some((option) => option.value === withdrawToken)) {
123
+ setWithdrawToken(tokenOptions[0].value);
124
+ }
125
+ }, [tokenOptions, withdrawToken]);
126
+ const selectedCashEntry = useMemo(
127
+ () => balances == null ? void 0 : balances.cash.find((cashEntry) => cashEntry.tokenSymbol === resolvedWithdrawToken),
128
+ [balances, resolvedWithdrawToken]
129
+ );
130
+ const selectedTokenDecimals = useMemo(() => {
131
+ var _a2, _b2, _c, _d;
132
+ const selectedChainId = Number(resolvedWithdrawNetwork);
133
+ return (_d = (_c = (_b2 = (_a2 = supportedChains == null ? void 0 : supportedChains.find((chain) => chain.chainId === selectedChainId)) == null ? void 0 : _a2.tokens.find((token) => token.symbol === resolvedWithdrawToken)) == null ? void 0 : _b2.decimals) != null ? _c : selectedCashEntry == null ? void 0 : selectedCashEntry.decimals) != null ? _d : 6;
134
+ }, [
135
+ resolvedWithdrawNetwork,
136
+ resolvedWithdrawToken,
137
+ selectedCashEntry == null ? void 0 : selectedCashEntry.decimals,
138
+ supportedChains
139
+ ]);
140
+ const exactBalance = useMemo(() => {
141
+ if (!selectedCashEntry) return "0";
142
+ return formatRawTokenAmount(selectedCashEntry.totalRaw, selectedCashEntry.decimals);
143
+ }, [selectedCashEntry]);
144
+ const balanceDisplay = useMemo(() => {
145
+ if (!selectedCashEntry) return `0.00 ${resolvedWithdrawToken || withdrawToken}`;
146
+ return `${formatRawTokenAmountForDisplay(
147
+ selectedCashEntry.totalRaw,
148
+ selectedCashEntry.decimals
149
+ )} ${resolvedWithdrawToken}`;
150
+ }, [resolvedWithdrawToken, selectedCashEntry, withdrawToken]);
151
+ const resetFlowState = useCallback(() => {
152
+ setWithdrawDestination("");
153
+ setWithdrawAmount("");
154
+ setWithdrawToken("USDC");
155
+ setWithdrawNetwork("");
156
+ setWithdrawSummary(DEFAULT_WITHDRAW_SUMMARY);
157
+ setWithdrawalId(null);
158
+ }, []);
159
+ const handleWithdrawOpenChange = useCallback(
160
+ (isOpen) => {
161
+ if (!isOpen) resetFlowState();
162
+ onOpenChange(isOpen);
163
+ },
164
+ [onOpenChange, resetFlowState]
165
+ );
166
+ useEffect(() => {
167
+ if (!open) resetFlowState();
168
+ }, [open, resetFlowState]);
169
+ const handleWithdrawProvider = useCallback(() => __async(null, null, function* () {
170
+ var _a2, _b2;
171
+ const destinationChainId = Number(resolvedWithdrawNetwork);
172
+ const trimmedDestination = withdrawDestination.trim();
173
+ const amountRaw = parseTokenAmountToRaw(withdrawAmount, selectedTokenDecimals);
174
+ if (!amountRaw || BigInt(amountRaw) <= BigInt(0)) {
175
+ throw new Error("Enter a withdrawal amount greater than zero.");
176
+ }
177
+ if (!Number.isFinite(destinationChainId) || destinationChainId <= 0) {
178
+ throw new Error("Select a destination network.");
179
+ }
180
+ if (!isValidDestinationAddress(trimmedDestination, destinationChainId)) {
181
+ const expected = destinationChainId === SOLANA_CHAIN_ID ? "a Solana wallet address (base58, 32\u201344 chars)" : "an EVM destination address (0x\u2026 40 hex chars)";
182
+ throw new Error(`Enter ${expected}.`);
183
+ }
184
+ if (selectedCashEntry) {
185
+ const scaleBy = (n, exp) => {
186
+ if (exp <= 0) return n;
187
+ return n * BigInt(`1${"0".repeat(exp)}`);
188
+ };
189
+ const balanceInDestFrame = (() => {
190
+ const native = BigInt(selectedCashEntry.totalRaw);
191
+ if (selectedCashEntry.decimals === selectedTokenDecimals) return native;
192
+ if (selectedCashEntry.decimals > selectedTokenDecimals) {
193
+ return native / scaleBy(BigInt(1), selectedCashEntry.decimals - selectedTokenDecimals);
194
+ }
195
+ return scaleBy(native, selectedTokenDecimals - selectedCashEntry.decimals);
196
+ })();
197
+ if (BigInt(amountRaw) > balanceInDestFrame) {
198
+ throw new Error("Withdrawal amount exceeds your available balance.");
199
+ }
200
+ }
201
+ ws == null ? void 0 : ws.connect();
202
+ yield new Promise((resolve, reject) => {
203
+ withdrawMutation.mutate(
204
+ {
205
+ amountRaw,
206
+ tokenSymbol: resolvedWithdrawToken,
207
+ destinationAddress: trimmedDestination,
208
+ destinationChainId
209
+ },
210
+ {
211
+ onSuccess: (data) => {
212
+ setWithdrawalId(data.withdrawalId);
213
+ resolve();
214
+ },
215
+ onError: reject
216
+ }
217
+ );
218
+ });
219
+ setWithdrawSummary({
220
+ amountReceived: `${formatRawTokenAmountForDisplay(amountRaw, selectedTokenDecimals)} ${resolvedWithdrawToken}`,
221
+ network: (_b2 = (_a2 = supportedChains == null ? void 0 : supportedChains.find((chain) => chain.chainId === destinationChainId)) == null ? void 0 : _a2.name) != null ? _b2 : resolvedWithdrawNetwork,
222
+ toWallet: shortenAddress(trimmedDestination),
223
+ fees: "\u2014"
224
+ });
225
+ }), [
226
+ resolvedWithdrawNetwork,
227
+ resolvedWithdrawToken,
228
+ selectedCashEntry,
229
+ selectedTokenDecimals,
230
+ supportedChains,
231
+ withdrawAmount,
232
+ withdrawDestination,
233
+ withdrawMutation,
234
+ ws
235
+ ]);
236
+ return {
237
+ open,
238
+ onOpenChange: handleWithdrawOpenChange,
239
+ withdrawFlow: {
240
+ balance: totalBalance,
241
+ balanceDisplay,
242
+ amount: withdrawAmount,
243
+ destinationWallet: withdrawDestination,
244
+ tokenOptions,
245
+ networkOptions,
246
+ selectedToken: resolvedWithdrawToken,
247
+ selectedNetwork: resolvedWithdrawNetwork,
248
+ purchaseSummary: withdrawSummary,
249
+ withdrawalId
250
+ },
251
+ onWithdrawDestinationChange: setWithdrawDestination,
252
+ onWithdrawAmountChange: setWithdrawAmount,
253
+ onWithdrawTokenChange: setWithdrawToken,
254
+ onWithdrawNetworkChange: setWithdrawNetwork,
255
+ onMaxClick: useCallback(() => {
256
+ setWithdrawAmount(exactBalance === "0" ? "0" : exactBalance);
257
+ }, [exactBalance]),
258
+ onSelectWithdrawProvider: useCallback(
259
+ (_providerId) => __async(null, null, function* () {
260
+ return handleWithdrawProvider();
261
+ }),
262
+ [handleWithdrawProvider]
263
+ ),
264
+ onDoneWithdraw: useCallback(() => handleWithdrawOpenChange(false), [handleWithdrawOpenChange])
265
+ };
266
+ }
267
+
268
+ // src/withdraw/use-withdrawal-lifecycle.ts
269
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState2 } from "react";
270
+ import { useQueryClient } from "@tanstack/react-query";
271
+ var INITIAL_STATE = {
272
+ pending: true,
273
+ status: null,
274
+ terminal: false,
275
+ lastLeg: null,
276
+ legs: [],
277
+ errorMessage: null,
278
+ timestamp: null
279
+ };
280
+ var restLegToWsLeg = (leg) => ({
281
+ sourceChainId: leg.sourceChainId,
282
+ destChainId: leg.destChainId,
283
+ type: leg.type,
284
+ // The wire-level WS leg status enum and the REST leg status enum are the
285
+ // same union (planned/submitted/confirmed/failed). Keep the cast narrow to
286
+ // avoid pulling the SDK enum apart for a one-line bridge.
287
+ status: leg.status,
288
+ amountRaw: leg.amountRaw,
289
+ txHash: leg.txHash,
290
+ bridgeOperationId: leg.bridgeOperationId
291
+ });
292
+ var mergeLegs = (prev, snapshot, delta) => {
293
+ if (snapshot) return snapshot;
294
+ if (!delta) return prev;
295
+ const idx = prev.findIndex(
296
+ (l) => l.sourceChainId === delta.sourceChainId && l.destChainId === delta.destChainId && l.type === delta.type
297
+ );
298
+ if (idx === -1) return [...prev, delta];
299
+ const next = prev.slice();
300
+ next[idx] = delta;
301
+ return next;
302
+ };
303
+ var restToLifecycleState = (response) => {
304
+ var _a;
305
+ return {
306
+ pending: false,
307
+ status: response.status,
308
+ terminal: response.status === "completed" || response.status === "partial" || response.status === "failed",
309
+ lastLeg: null,
310
+ legs: response.legs.map(restLegToWsLeg),
311
+ errorMessage: (_a = response.errorMessage) != null ? _a : null,
312
+ // No server timestamp on the REST response — we use 0 as "older than any
313
+ // WS timestamp" so a subsequent WS event always wins. Callers that read
314
+ // `timestamp` should treat 0/null interchangeably as "unset".
315
+ timestamp: 0
316
+ };
317
+ };
318
+ function useWithdrawalLifecycle(withdrawalId) {
319
+ const client = useAggClient();
320
+ const wsConnected = useAggWebSocketConnectionState();
321
+ const queryClient = useQueryClient();
322
+ const [state, setState] = useState2(INITIAL_STATE);
323
+ const stateRef = useRef(state);
324
+ stateRef.current = state;
325
+ const balanceRefetchedForRef = useRef(null);
326
+ useEffect2(() => {
327
+ setState(INITIAL_STATE);
328
+ }, [withdrawalId]);
329
+ useEffect2(() => {
330
+ if (!withdrawalId) return;
331
+ let cancelled = false;
332
+ (() => __async(null, null, function* () {
333
+ try {
334
+ const response = yield client.getWithdrawalStatus(withdrawalId);
335
+ if (cancelled) return;
336
+ if (response.withdrawalId !== withdrawalId) return;
337
+ const current = stateRef.current;
338
+ const wsAlreadyWon = current.timestamp != null && current.timestamp > 0;
339
+ if (wsAlreadyWon) return;
340
+ setState(restToLifecycleState(response));
341
+ } catch (e) {
342
+ }
343
+ }))();
344
+ return () => {
345
+ cancelled = true;
346
+ };
347
+ }, [client, withdrawalId, wsConnected]);
348
+ const handler = useMemo2(() => {
349
+ if (!withdrawalId) return null;
350
+ return (msg) => {
351
+ if (msg.withdrawalId !== withdrawalId) return;
352
+ setState((prev) => {
353
+ var _a, _b;
354
+ return {
355
+ pending: false,
356
+ status: msg.status,
357
+ terminal: msg.terminal,
358
+ lastLeg: (_a = msg.leg) != null ? _a : null,
359
+ // `legs[]` is the cumulative server-known truth. Snapshots
360
+ // (`pending` / terminal rollup) carry a full `legs[]` and replace
361
+ // it. Intermediate per-leg deltas carry only `leg` (no `legs[]`)
362
+ // — merge the delta into the existing array by
363
+ // (sourceChainId, destChainId, type) so the timeline UI doesn't
364
+ // collapse to empty between snapshots.
365
+ legs: mergeLegs(prev.legs, msg.legs, msg.leg),
366
+ errorMessage: (_b = msg.errorMessage) != null ? _b : null,
367
+ timestamp: msg.timestamp
368
+ };
369
+ });
370
+ };
371
+ }, [withdrawalId]);
372
+ useOnWithdrawalLifecycle(handler);
373
+ useEffect2(() => {
374
+ if (!withdrawalId) return;
375
+ if (!state.terminal) return;
376
+ if (balanceRefetchedForRef.current === withdrawalId) return;
377
+ balanceRefetchedForRef.current = withdrawalId;
378
+ invalidateBalanceQueries(queryClient);
379
+ client.syncManagedBalances().catch(() => {
380
+ });
381
+ }, [client, queryClient, state.terminal, withdrawalId]);
382
+ const reset = useCallback2(() => setState(INITIAL_STATE), []);
383
+ return { state, reset };
384
+ }
385
+
386
+ export {
387
+ useWithdrawFlow,
388
+ useWithdrawalLifecycle
389
+ };