@agg-build/hooks 1.0.0 → 1.0.1

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)
@@ -320,6 +320,8 @@ var enUsLabels = {
320
320
  successDescription: "Your USDC has been successfully added to your balance.",
321
321
  pendingTitle: (provider) => `Complete your payment on ${provider}`,
322
322
  pendingDescription: "Once your transaction is finished, your balance may take a few minutes to update. The deposit will appear in your activity once it's successful.",
323
+ pendingWalletAddressHelp: "Some providers may ask for a wallet address during checkout, use this one to receive your deposit:",
324
+ pendingCopyAddress: "Copy deposit address",
323
325
  viewActivity: "View Activity",
324
326
  chooseAnotherProvider: "Choose another provider",
325
327
  summary: {
@@ -356,9 +358,55 @@ var enUsLabels = {
356
358
  done: "Done",
357
359
  balancePrefix: "Balance:",
358
360
  methods: {
361
+ walletTitle: "Withdraw to wallet",
362
+ walletDescription: "Withdraw funds instantly to your crypto wallet",
359
363
  cardTitle: "Withdraw with card",
360
364
  cardDescription: "Withdraw funds to your card"
361
365
  },
366
+ walletFlow: {
367
+ title: "Withdraw to wallet",
368
+ recipientAddressLabel: "Recipient address",
369
+ amountLabel: "Amount",
370
+ max: "Max",
371
+ tokenLabel: "Receive token",
372
+ networkLabel: "Receive network",
373
+ confirm: "Confirm withdrawal",
374
+ successTitle: "Withdrawal submitted",
375
+ successDescription: (tokenSymbol) => `Your ${tokenSymbol} withdrawal is being processed and will arrive shortly.`,
376
+ // Terminal-state copy. The success step swaps `successTitle` /
377
+ // `successDescription` for these once the lifecycle has reached a
378
+ // terminal status — otherwise a finished withdrawal would keep showing
379
+ // "submitted / processing" forever and force the user to hard-refresh.
380
+ successTitleCompleted: "Withdrawal complete",
381
+ successDescriptionCompleted: (tokenSymbol) => `Your ${tokenSymbol} withdrawal has been delivered.`,
382
+ successTitlePartial: "Withdrawal partially completed",
383
+ successDescriptionPartial: (tokenSymbol) => `Some legs of your ${tokenSymbol} withdrawal completed; see details below.`,
384
+ successTitleFailed: "Withdrawal failed",
385
+ successDescriptionFailed: (tokenSymbol) => `Your ${tokenSymbol} withdrawal could not be completed.`,
386
+ summary: {
387
+ // The response is `pricingStatus: "unquoted"` — we don't know net
388
+ // output until on-chain settlement. Calling this "Amount received"
389
+ // would imply receipt before the lifecycle has confirmed. Keep it
390
+ // honest as the submitted amount until the multi-stable quote
391
+ // layer (PR-E) populates `expected.outputRaw`.
392
+ amountReceived: "Amount",
393
+ network: "Network",
394
+ toWallet: "To wallet",
395
+ fees: "Fees"
396
+ },
397
+ lifecycle: {
398
+ pending: "Submitted \u2014 preparing your withdrawal\u2026",
399
+ bridging: "Bridging funds across chains\u2026",
400
+ transferring: "Transferring to your wallet\u2026",
401
+ completed: "Withdrawal complete.",
402
+ partial: "Withdrawal partially completed \u2014 see details below.",
403
+ failed: "Withdrawal failed.",
404
+ steps: {
405
+ bridge: (sourceChainName, destChainName) => `Bridging from ${sourceChainName} to ${destChainName}`,
406
+ transfer: (destChainName) => `Transferring on ${destChainName}`
407
+ }
408
+ }
409
+ },
362
410
  cardFlow: {
363
411
  title: "Sell crypto",
364
412
  amountLabel: "Amount",
@@ -382,7 +430,10 @@ var enUsLabels = {
382
430
  }
383
431
  },
384
432
  summary: {
385
- amountReceived: "Amount received",
433
+ // Lifecycle-honest: until quoting is live (PR-E) the response is
434
+ // `pricingStatus: "unquoted"` so this is the submitted amount, not
435
+ // a guaranteed net output.
436
+ amountReceived: "Amount",
386
437
  network: "Network"
387
438
  }
388
439
  },
@@ -399,12 +450,16 @@ var enUsLabels = {
399
450
  userProfile: {
400
451
  activity: {
401
452
  depositType: "Deposit",
402
- withdrawalType: "Withdraw",
453
+ withdrawalType: "Withdrawal",
403
454
  depositTitles: {
404
455
  connectedWallet: "Deposit from connected wallet",
405
456
  externalWallet: "Deposit from external wallet",
406
457
  card: "Deposit with card"
407
458
  },
459
+ // Activity-row title for any withdrawal regardless of lifecycle
460
+ // state (pending / completed / failed) — render the asset rather
461
+ // than implying success. The ActivityRow renders a separate status
462
+ // chip when the row is failed.
408
463
  withdrawalTitle: (tokenSymbol) => `Withdraw ${tokenSymbol}`
409
464
  },
410
465
  positions: {
@@ -1690,6 +1745,20 @@ function useOnRedeemEvent(callback) {
1690
1745
  return listeners.addRedeemEventListener(handler);
1691
1746
  }, [listeners, hasCallback]);
1692
1747
  }
1748
+ function useOnWithdrawalLifecycle(callback) {
1749
+ const listeners = useContext4(AggWsUserEventContext);
1750
+ const callbackRef = useRef2(callback);
1751
+ callbackRef.current = callback;
1752
+ const hasCallback = callback !== null;
1753
+ useEffect(() => {
1754
+ if (!listeners || !callbackRef.current) return;
1755
+ const handler = (msg) => {
1756
+ var _a;
1757
+ (_a = callbackRef.current) == null ? void 0 : _a.call(callbackRef, msg);
1758
+ };
1759
+ return listeners.addWithdrawalLifecycleListener(handler);
1760
+ }, [listeners, hasCallback]);
1761
+ }
1693
1762
  function AggWebSocketProvider({ children }) {
1694
1763
  const client = useAggClient();
1695
1764
  const { enableWebsocketsLogs } = useAggUiConfig();
@@ -1710,6 +1779,9 @@ function AggWebSocketProvider({ children }) {
1710
1779
  const balanceUpdateListenersRef = useRef2(/* @__PURE__ */ new Set());
1711
1780
  const orderEventListenersRef = useRef2(/* @__PURE__ */ new Set());
1712
1781
  const redeemEventListenersRef = useRef2(/* @__PURE__ */ new Set());
1782
+ const withdrawalLifecycleListenersRef = useRef2(
1783
+ /* @__PURE__ */ new Set()
1784
+ );
1713
1785
  const userEventListeners = useMemo2(
1714
1786
  () => ({
1715
1787
  addOrderSubmittedListener: (cb) => {
@@ -1735,6 +1807,12 @@ function AggWebSocketProvider({ children }) {
1735
1807
  return () => {
1736
1808
  redeemEventListenersRef.current.delete(cb);
1737
1809
  };
1810
+ },
1811
+ addWithdrawalLifecycleListener: (cb) => {
1812
+ withdrawalLifecycleListenersRef.current.add(cb);
1813
+ return () => {
1814
+ withdrawalLifecycleListenersRef.current.delete(cb);
1815
+ };
1738
1816
  }
1739
1817
  }),
1740
1818
  []
@@ -1909,6 +1987,14 @@ function AggWebSocketProvider({ children }) {
1909
1987
  listener(msg);
1910
1988
  }
1911
1989
  },
1990
+ onWithdrawalLifecycle: (msg) => {
1991
+ if (isClientAuthenticated) {
1992
+ invalidateBalanceCaches();
1993
+ }
1994
+ for (const listener of withdrawalLifecycleListenersRef.current) {
1995
+ listener(msg);
1996
+ }
1997
+ },
1912
1998
  onError: (msg) => {
1913
1999
  const outcomeId = resolveSnapshotUnavailableOutcomeId(msg.message);
1914
2000
  if (!outcomeId) return;
@@ -2989,33 +3075,11 @@ var AggProvider = ({ client, config, children }) => {
2989
3075
  return /* @__PURE__ */ jsx8(AggClientProvider, { client, children: /* @__PURE__ */ jsx8(AggUiProvider, { config, children: /* @__PURE__ */ jsx8(EventListStateProvider, { children: /* @__PURE__ */ jsx8(AggWebSocketProvider, { children: /* @__PURE__ */ jsx8(AggAuthProvider, { children: /* @__PURE__ */ jsx8(AggBalanceProvider, { children }) }) }) }) }) });
2990
3076
  };
2991
3077
 
2992
- // src/use-ramp-quotes.ts
2993
- import { useMutation } from "@tanstack/react-query";
2994
- function useRampQuotes() {
2995
- const client = useAggClient();
2996
- return useMutation({
2997
- mutationFn: (params) => __async(null, null, function* () {
2998
- return client.getRampQuotes(params);
2999
- })
3000
- });
3001
- }
3002
-
3003
- // src/use-ramp-session.ts
3004
- import { useMutation as useMutation2 } from "@tanstack/react-query";
3005
- function useRampSession() {
3006
- const client = useAggClient();
3007
- return useMutation2({
3008
- mutationFn: (params) => __async(null, null, function* () {
3009
- return client.createRampSession(params);
3010
- })
3011
- });
3012
- }
3013
-
3014
3078
  // src/execution/use-quote-managed.ts
3015
- import { useMutation as useMutation3 } from "@tanstack/react-query";
3079
+ import { useMutation } from "@tanstack/react-query";
3016
3080
  function useQuoteManaged(options) {
3017
3081
  const client = useAggClient();
3018
- return useMutation3({
3082
+ return useMutation({
3019
3083
  mutationFn: (params) => client.quoteManaged(params),
3020
3084
  onSuccess: options == null ? void 0 : options.onSuccess,
3021
3085
  onError: options == null ? void 0 : options.onError
@@ -3023,11 +3087,11 @@ function useQuoteManaged(options) {
3023
3087
  }
3024
3088
 
3025
3089
  // src/execution/use-execute-managed.ts
3026
- import { useMutation as useMutation4, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
3090
+ import { useMutation as useMutation2, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
3027
3091
  function useExecuteManaged(options) {
3028
3092
  const client = useAggClient();
3029
3093
  const queryClient = useQueryClient3();
3030
- return useMutation4({
3094
+ return useMutation2({
3031
3095
  mutationFn: (params) => client.executeManaged(params),
3032
3096
  onSuccess: (data) => {
3033
3097
  var _a;
@@ -3041,11 +3105,11 @@ function useExecuteManaged(options) {
3041
3105
  }
3042
3106
 
3043
3107
  // src/execution/use-withdraw-managed.ts
3044
- import { useMutation as useMutation5, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
3108
+ import { useMutation as useMutation3, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
3045
3109
  function useWithdrawManaged(options) {
3046
3110
  const client = useAggClient();
3047
3111
  const queryClient = useQueryClient4();
3048
- return useMutation5({
3112
+ return useMutation3({
3049
3113
  mutationFn: (params) => client.withdrawManaged(params),
3050
3114
  onSuccess: (data) => {
3051
3115
  var _a;
@@ -3153,11 +3217,11 @@ function useDepositAddresses(options) {
3153
3217
  }
3154
3218
 
3155
3219
  // src/execution/use-sync-balances.ts
3156
- import { useMutation as useMutation6, useQueryClient as useQueryClient5 } from "@tanstack/react-query";
3220
+ import { useMutation as useMutation4, useQueryClient as useQueryClient5 } from "@tanstack/react-query";
3157
3221
  function useSyncBalances(options) {
3158
3222
  const client = useAggClient();
3159
3223
  const queryClient = useQueryClient5();
3160
- return useMutation6({
3224
+ return useMutation4({
3161
3225
  mutationFn: () => client.syncManagedBalances(),
3162
3226
  onSuccess: () => {
3163
3227
  var _a;
@@ -3465,11 +3529,11 @@ var computeClosedPositionTotals = (group) => {
3465
3529
  };
3466
3530
 
3467
3531
  // src/execution/use-redeem.ts
3468
- import { useMutation as useMutation7, useQueryClient as useQueryClient6 } from "@tanstack/react-query";
3532
+ import { useMutation as useMutation5, useQueryClient as useQueryClient6 } from "@tanstack/react-query";
3469
3533
  var useRedeem = () => {
3470
3534
  const client = useAggClient();
3471
3535
  const queryClient = useQueryClient6();
3472
- return useMutation7({
3536
+ return useMutation5({
3473
3537
  mutationFn: (body) => client.redeem(body),
3474
3538
  onSuccess: () => {
3475
3539
  queryClient.invalidateQueries({ queryKey: executionKeys.positionsPrefix() });
@@ -3571,6 +3635,7 @@ export {
3571
3635
  useAggWebSocketConnectionState,
3572
3636
  useOnOrderSubmitted,
3573
3637
  useOnBalanceUpdate,
3638
+ useOnWithdrawalLifecycle,
3574
3639
  DEFAULT_AGG_ROOT_CLASS_NAME,
3575
3640
  CHART_TIME_RANGES,
3576
3641
  resolveTradingStateKind,
@@ -3583,8 +3648,6 @@ export {
3583
3648
  useEventTradingContext,
3584
3649
  AggUiProvider,
3585
3650
  AggProvider,
3586
- useRampQuotes,
3587
- useRampSession,
3588
3651
  useQuoteManaged,
3589
3652
  useExecuteManaged,
3590
3653
  useWithdrawManaged,
@@ -0,0 +1,31 @@
1
+ import {
2
+ __async,
3
+ useAggClient
4
+ } from "./chunk-BWXNOWAS.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-BWXNOWAS.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
+ };