@ab-org/predicate-market-sdk 0.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.
Files changed (86) hide show
  1. package/README.md +246 -0
  2. package/dist/auth/autoReconnect.d.ts +11 -0
  3. package/dist/auth/autoReconnect.js +36 -0
  4. package/dist/auth/bundledConfig.d.ts +2 -0
  5. package/dist/auth/bundledConfig.js +19 -0
  6. package/dist/auth/config.d.ts +29 -0
  7. package/dist/auth/config.js +53 -0
  8. package/dist/auth/google.d.ts +43 -0
  9. package/dist/auth/google.js +147 -0
  10. package/dist/auth/twitter.d.ts +7 -0
  11. package/dist/auth/twitter.js +94 -0
  12. package/dist/constants/chains.d.ts +22 -0
  13. package/dist/constants/chains.js +23 -0
  14. package/dist/index.d.ts +19 -0
  15. package/dist/index.js +19 -0
  16. package/dist/modules/api.d.ts +144 -0
  17. package/dist/modules/api.js +93 -0
  18. package/dist/modules/balanceQuery.d.ts +20 -0
  19. package/dist/modules/balanceQuery.js +58 -0
  20. package/dist/modules/deposit.d.ts +31 -0
  21. package/dist/modules/deposit.js +57 -0
  22. package/dist/modules/marketData.d.ts +8 -0
  23. package/dist/modules/marketData.js +113 -0
  24. package/dist/modules/withdraw.d.ts +31 -0
  25. package/dist/modules/withdraw.js +60 -0
  26. package/dist/modules/withdrawExecutor.d.ts +47 -0
  27. package/dist/modules/withdrawExecutor.js +208 -0
  28. package/dist/policyAdapter.d.ts +11 -0
  29. package/dist/policyAdapter.js +38 -0
  30. package/dist/types.d.ts +62 -0
  31. package/dist/types.js +1 -0
  32. package/dist/ui/DepositModal.d.ts +36 -0
  33. package/dist/ui/DepositModal.js +326 -0
  34. package/dist/ui/SignInModal.d.ts +22 -0
  35. package/dist/ui/SignInModal.js +74 -0
  36. package/dist/ui/SignInModal.sections.d.ts +33 -0
  37. package/dist/ui/SignInModal.sections.js +45 -0
  38. package/dist/ui/SignInModal.shared.d.ts +15 -0
  39. package/dist/ui/SignInModal.shared.js +87 -0
  40. package/dist/ui/WalletSelectionModal.d.ts +14 -0
  41. package/dist/ui/WalletSelectionModal.js +54 -0
  42. package/dist/ui/WithdrawModal.d.ts +47 -0
  43. package/dist/ui/WithdrawModal.js +528 -0
  44. package/dist/ui/components/CloseButton.d.ts +4 -0
  45. package/dist/ui/components/CloseButton.js +15 -0
  46. package/dist/ui/components/Countdown.d.ts +16 -0
  47. package/dist/ui/components/Countdown.js +42 -0
  48. package/dist/ui/components/DepositDetailsPanel.d.ts +8 -0
  49. package/dist/ui/components/DepositDetailsPanel.js +117 -0
  50. package/dist/ui/components/DropdownField.d.ts +19 -0
  51. package/dist/ui/components/DropdownField.js +81 -0
  52. package/dist/ui/components/Field.d.ts +10 -0
  53. package/dist/ui/components/Field.js +21 -0
  54. package/dist/ui/components/LoginRequiredOverlay.d.ts +6 -0
  55. package/dist/ui/components/LoginRequiredOverlay.js +31 -0
  56. package/dist/ui/components/ModalCard.d.ts +9 -0
  57. package/dist/ui/components/ModalCard.js +14 -0
  58. package/dist/ui/components/ModalFrame.d.ts +9 -0
  59. package/dist/ui/components/ModalFrame.js +18 -0
  60. package/dist/ui/components/PrimaryButton.d.ts +2 -0
  61. package/dist/ui/components/PrimaryButton.js +14 -0
  62. package/dist/ui/components/QRCodePanel.d.ts +4 -0
  63. package/dist/ui/components/QRCodePanel.js +43 -0
  64. package/dist/ui/components/Select.d.ts +12 -0
  65. package/dist/ui/components/Select.js +29 -0
  66. package/dist/ui/components/StepIndicator.d.ts +7 -0
  67. package/dist/ui/components/StepIndicator.js +35 -0
  68. package/dist/ui/components/Success.d.ts +1 -0
  69. package/dist/ui/components/Success.js +4 -0
  70. package/dist/ui/components/Toast.d.ts +8 -0
  71. package/dist/ui/components/Toast.js +51 -0
  72. package/dist/ui/hooks/useSession.d.ts +2 -0
  73. package/dist/ui/hooks/useSession.js +10 -0
  74. package/dist/ui/signInTypes.d.ts +25 -0
  75. package/dist/ui/signInTypes.js +1 -0
  76. package/dist/ui/theme.d.ts +31 -0
  77. package/dist/ui/theme.js +31 -0
  78. package/dist/ui/useSignInModalController.d.ts +25 -0
  79. package/dist/ui/useSignInModalController.js +173 -0
  80. package/dist/utils/env.d.ts +1 -0
  81. package/dist/utils/env.js +61 -0
  82. package/dist/utils/explorer.d.ts +3 -0
  83. package/dist/utils/explorer.js +47 -0
  84. package/dist/walletUtils.d.ts +3 -0
  85. package/dist/walletUtils.js +3 -0
  86. package/package.json +41 -0
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { colors, fonts, radii } from "./theme.js";
3
+ const placeholderIcon = (label) => (_jsx("div", { style: {
4
+ width: 40,
5
+ height: 40,
6
+ borderRadius: "12px",
7
+ background: colors.cardRaised,
8
+ display: "flex",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ color: colors.textSecondary,
12
+ fontWeight: 600,
13
+ }, children: label.slice(0, 2).toUpperCase() }));
14
+ export const WalletSelectionModal = ({ title = "Connect Wallet", options, onSelect, }) => {
15
+ const sections = [
16
+ {
17
+ id: "social",
18
+ label: "Social Wallets",
19
+ items: options.filter((item) => item.category === "social"),
20
+ },
21
+ {
22
+ id: "plugin",
23
+ label: "Plugin Wallets",
24
+ items: options.filter((item) => item.category === "plugin"),
25
+ },
26
+ ].filter((section) => section.items.length > 0);
27
+ return (_jsxs("div", { style: {
28
+ width: "min(720px, 90vw)",
29
+ background: colors.card,
30
+ borderRadius: radii.xl,
31
+ border: `1px solid ${colors.border}`,
32
+ padding: "40px",
33
+ display: "flex",
34
+ flexDirection: "column",
35
+ gap: 32,
36
+ color: colors.textPrimary,
37
+ fontFamily: fonts.family,
38
+ }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 26, fontWeight: 600 }, children: title }), _jsx("p", { style: { color: colors.textSecondary, marginTop: 8 }, children: "Choose social or plugin wallets" })] }), sections.map((section) => (_jsxs("section", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [_jsx("div", { style: { fontSize: 14, textTransform: "uppercase", letterSpacing: 1.5, color: colors.textSecondary }, children: section.label }), _jsx("div", { style: {
39
+ display: "grid",
40
+ gap: 16,
41
+ gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
42
+ }, children: section.items.map((option) => (_jsxs("button", { onClick: () => onSelect?.(option.id), style: {
43
+ borderRadius: radii.lg,
44
+ border: `1px solid ${colors.border}`,
45
+ background: colors.cardRaised,
46
+ padding: "20px",
47
+ display: "flex",
48
+ gap: 14,
49
+ alignItems: "center",
50
+ textAlign: "left",
51
+ cursor: "pointer",
52
+ color: colors.textPrimary,
53
+ }, children: [option.icon ?? placeholderIcon(option.label), _jsxs("div", { children: [_jsx("div", { style: { fontWeight: 600 }, children: option.label }), _jsx("div", { style: { color: colors.textSecondary, fontSize: 14 }, children: option.description })] })] }, option.id))) })] }, section.id)))] }));
54
+ };
@@ -0,0 +1,47 @@
1
+ import type { ChangeEventHandler } from "react";
2
+ import { type SelectOption } from "./components/DropdownField.js";
3
+ export type WithdrawUiStatus = "idle" | "pending" | "success" | "manual_review";
4
+ export interface WithdrawModalProps {
5
+ address?: string;
6
+ token?: string;
7
+ tokenSymbol?: string;
8
+ chain?: string;
9
+ amount?: string;
10
+ balance?: string;
11
+ status?: WithdrawUiStatus;
12
+ receiveAmount?: string;
13
+ txHash?: string;
14
+ eta?: string;
15
+ tokenOptions?: SelectOption[];
16
+ chainOptions?: SelectOption[];
17
+ /** 使用 Merchant API:getChains 拉取 token/chain,quote(withdraw) 询价 */
18
+ useMerchantApi?: boolean;
19
+ /** 创建订单后传入,用于轮询提现订单状态 */
20
+ orderId?: string;
21
+ /** 成功页 Fee 展示:优先用订单接口返回的 fee,若后端未返回则用此值(如询价时的 fee),均无则显示 "—" */
22
+ feeDisplay?: string;
23
+ /** 广播后的 funding tx 所在链 id,与 txHash 一起用于展示「查看交易」链接 */
24
+ fundingChainId?: string;
25
+ /** 构建链上交易浏览器链接(已弃用,使用 getExplorerUrl) */
26
+ explorerTxUrl?: (chainId: string, txHash: string) => string;
27
+ onShowToast?: (message: string) => void;
28
+ onAddressChange?: ChangeEventHandler<HTMLInputElement>;
29
+ onTokenSelect?: (id: string) => void;
30
+ onChainSelect?: (id: string) => void;
31
+ onAmountChange?: ChangeEventHandler<HTMLInputElement>;
32
+ onMaxClick?: () => void;
33
+ /** 提交时传入当前表单值(收款地址、金额、所选 token、所选 chain),由调用方执行提现 */
34
+ onSubmit?: (payload: {
35
+ toAddress: string;
36
+ amount: string;
37
+ token: string;
38
+ chain: string;
39
+ }) => void;
40
+ /** 提现订单状态变为 completed 时调用,用于调用方刷新余额等 */
41
+ onWithdrawCompleted?: () => void;
42
+ /** 点击「Start another withdrawal」时调用;应由调用方清空 orderId、重置 amount,恢复到待提交状态(不关弹层)。未传则退化为 onClose。 */
43
+ onStartAnotherWithdrawal?: () => void;
44
+ onSignIn?: () => void;
45
+ onClose?: () => void;
46
+ }
47
+ export declare const WithdrawModal: ({ address, token, tokenSymbol, chain, amount, balance, status, receiveAmount: receiveAmountProp, txHash, tokenOptions: tokenOptionsProp, chainOptions: chainOptionsProp, useMerchantApi, orderId, feeDisplay, fundingChainId, explorerTxUrl, onShowToast, onAddressChange, onTokenSelect, onChainSelect, onAmountChange, onMaxClick, onSubmit, onWithdrawCompleted, onStartAnotherWithdrawal, onSignIn, onClose, }: WithdrawModalProps) => import("react/jsx-runtime.js").JSX.Element;
@@ -0,0 +1,528 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback, useMemo, useRef } from "react";
3
+ import { ModalFrame } from "./components/ModalFrame.js";
4
+ import { StepIndicator } from "./components/StepIndicator.js";
5
+ import { DropdownField } from "./components/DropdownField.js";
6
+ import { Countdown } from "./components/Countdown.js";
7
+ import { LoginRequiredOverlay } from "./components/LoginRequiredOverlay.js";
8
+ import { useSession } from "./hooks/useSession.js";
9
+ import { colors, fonts, radii } from "./theme.js";
10
+ import { getChains, quote, getWithdrawOrder, } from "../modules/api.js";
11
+ import { getExplorerUrl } from "../utils/explorer.js";
12
+ import { SuccessIcon } from "./components/Success";
13
+ function CopyIcon() {
14
+ return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": true, children: [_jsx("rect", { x: "5", y: "5", width: "9", height: "9", rx: "1", stroke: "currentColor", strokeWidth: "1.2", fill: "none" }), _jsx("path", { d: "M3 11V3a1 1 0 0 1 1-1h8", stroke: "currentColor", strokeWidth: "1.2", fill: "none" })] }));
15
+ }
16
+ function chainsToTokenOptions(chains) {
17
+ const bySymbol = new Map();
18
+ for (const c of chains) {
19
+ for (const t of c.tokens) {
20
+ if (!bySymbol.has(t.symbol))
21
+ bySymbol.set(t.symbol, t.symbol);
22
+ }
23
+ }
24
+ return Array.from(bySymbol.entries()).map(([id, symbol]) => ({
25
+ id,
26
+ label: symbol,
27
+ subtitle: symbol,
28
+ }));
29
+ }
30
+ function chainsToChainOptionsForToken(chains, tokenSymbol) {
31
+ return chains
32
+ .filter((c) => c.tokens.some((t) => t.symbol === tokenSymbol))
33
+ .map((c) => ({ id: c.chain_id, label: c.network, subtitle: c.chain_id }));
34
+ }
35
+ function getTokenAddressForChain(chains, chainId, tokenSymbol) {
36
+ const chain = chains.find((c) => c.chain_id === chainId);
37
+ return chain?.tokens.find((t) => t.symbol === tokenSymbol)?.address;
38
+ }
39
+ /** 从 balance 字符串解析出数值部分(如 "123.46 USD1" → 123.46) */
40
+ function parseBalanceNumber(balance) {
41
+ const match = balance.trim().match(/^(\d*\.?\d*)/);
42
+ if (!match)
43
+ return null;
44
+ const n = Number(match[1]);
45
+ return Number.isNaN(n) ? null : n;
46
+ }
47
+ /** 将 balance 字符串格式化为小数点后 2 位(如 "123.456789012 USD1" → "123.46 USD1") */
48
+ function formatBalanceTo2Decimals(balance) {
49
+ const match = balance.trim().match(/^(\d*\.?\d*)(.*)$/);
50
+ if (!match)
51
+ return balance;
52
+ const numPart = match[1];
53
+ const suffix = match[2].trim() ? " " + match[2].trim() : "";
54
+ const n = Number(numPart);
55
+ if (Number.isNaN(n))
56
+ return balance;
57
+ const formatted = n.toFixed(2);
58
+ return formatted + suffix;
59
+ }
60
+ const WEI_PER_ETHER = 1e18;
61
+ /** Wei (string) to ether display string; keeps up to 6 decimal places, strips trailing zeros */
62
+ function weiToEtherDisplay(wei) {
63
+ const s = (wei || "0").trim().replace(/^0+/, "") || "0";
64
+ if (s === "0")
65
+ return "0";
66
+ const padded = s.padStart(19, "0");
67
+ const intPart = padded.slice(0, Math.max(0, padded.length - 18));
68
+ const decPart = padded.slice(-18).replace(/0+$/, "");
69
+ const combined = decPart ? `${intPart}.${decPart}` : intPart;
70
+ const num = Number(combined);
71
+ if (Number.isNaN(num))
72
+ return wei;
73
+ const fixed = num.toFixed(6).replace(/\.?0+$/, "");
74
+ return fixed;
75
+ }
76
+ const POLL_INTERVAL_MS = 4000;
77
+ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain, amount = "", balance, status = "idle", receiveAmount: receiveAmountProp, txHash, tokenOptions: tokenOptionsProp, chainOptions: chainOptionsProp, useMerchantApi = false, orderId, feeDisplay, fundingChainId, explorerTxUrl, onShowToast, onAddressChange, onTokenSelect, onChainSelect, onAmountChange, onMaxClick, onSubmit, onWithdrawCompleted, onStartAnotherWithdrawal, onSignIn, onClose, }) => {
78
+ const session = useSession();
79
+ const addressInputRef = useRef(null);
80
+ const [addressInputFocused, setAddressInputFocused] = useState(false);
81
+ const [amountInputFocused, setAmountInputFocused] = useState(false);
82
+ const [apiChains, setApiChains] = useState(null);
83
+ const [apiQuote, setApiQuote] = useState(null);
84
+ const [withdrawOrder, setWithdrawOrder] = useState(null);
85
+ const [loadingChains, setLoadingChains] = useState(false);
86
+ const [loadingQuote, setLoadingQuote] = useState(false);
87
+ // 默认收款地址为当前用户钱包地址
88
+ // useEffect(() => {
89
+ // if (session?.address && (address === "" || address === undefined) && onAddressChange) {
90
+ // onAddressChange({
91
+ // target: { value: session.address },
92
+ // } as ChangeEvent<HTMLInputElement>);
93
+ // }
94
+ // }, [session?.address, address, onAddressChange]);
95
+ const tokenOptions = useMemo(() => {
96
+ const raw = tokenOptionsProp?.length
97
+ ? tokenOptionsProp
98
+ : apiChains?.length
99
+ ? chainsToTokenOptions(apiChains)
100
+ : undefined;
101
+ if (!raw?.length)
102
+ return undefined;
103
+ return raw.filter((o) => o.id !== "USD1" && o.label !== "USD1");
104
+ }, [tokenOptionsProp, apiChains]);
105
+ const chainOptions = useMemo(() => {
106
+ if (chainOptionsProp?.length)
107
+ return chainOptionsProp;
108
+ if (!apiChains?.length || !token)
109
+ return undefined;
110
+ return chainsToChainOptionsForToken(apiChains, token);
111
+ }, [chainOptionsProp, apiChains, token]);
112
+ const trackingWithdraw = useMemo(() => Boolean(orderId), [orderId]);
113
+ // Default focus on recipient address when form is visible; white border while focused, revert on blur
114
+ useEffect(() => {
115
+ if (trackingWithdraw)
116
+ return;
117
+ const t = requestAnimationFrame(() => {
118
+ addressInputRef.current?.focus();
119
+ });
120
+ return () => cancelAnimationFrame(t);
121
+ }, [trackingWithdraw]);
122
+ // 选中 token+chain 后需要 chains 数据才能询价,因此无论 useMerchantApi 与否都拉取 getChains
123
+ const shouldLoadChains = useMerchantApi || Boolean(token && chain);
124
+ useEffect(() => {
125
+ if (!shouldLoadChains)
126
+ return;
127
+ setLoadingChains(true);
128
+ getChains()
129
+ .then((res) => setApiChains(res?.chains ?? {}))
130
+ .finally(() => setLoadingChains(false));
131
+ }, [shouldLoadChains]);
132
+ // 选中 token + chain 后即询价;未填 amount 时用 1 USD1 获取 rate
133
+ useEffect(() => {
134
+ if (!apiChains?.length || !token || !chain) {
135
+ setApiQuote(null);
136
+ return;
137
+ }
138
+ const tokenAddress = getTokenAddressForChain(apiChains, chain, token);
139
+ if (!tokenAddress) {
140
+ setApiQuote(null);
141
+ return;
142
+ }
143
+ const amountNum = amount && Number(amount) > 0 ? Number(amount) : 1;
144
+ const amountWei = String(BigInt(Math.floor(amountNum * 1e18)));
145
+ setLoadingQuote(true);
146
+ quote({
147
+ direction: "withdraw",
148
+ chain_id: chain,
149
+ token_address: tokenAddress,
150
+ usd1_amount: amountWei,
151
+ })
152
+ .then((q) => setApiQuote(q ?? null))
153
+ .catch(() => {
154
+ setApiQuote({
155
+ token_address: tokenAddress,
156
+ token_symbol: token,
157
+ token_decimals: 18,
158
+ rate: "1",
159
+ chain_id: Number(chain) || 56,
160
+ token_amount: amountWei,
161
+ expires_at: new Date(Date.now() + 60000).toISOString(),
162
+ });
163
+ })
164
+ .finally(() => setLoadingQuote(false));
165
+ }, [apiChains, token, chain, amount]);
166
+ useEffect(() => {
167
+ if (tokenOptions?.length !== 1 || !onTokenSelect)
168
+ return;
169
+ const onlyId = tokenOptions[0].id;
170
+ if (token === onlyId)
171
+ return;
172
+ onTokenSelect(onlyId);
173
+ }, [tokenOptions, token, onTokenSelect]);
174
+ useEffect(() => {
175
+ if (chainOptions?.length !== 1 || !onChainSelect)
176
+ return;
177
+ const onlyId = chainOptions[0].id;
178
+ if (chain === onlyId)
179
+ return;
180
+ onChainSelect(onlyId);
181
+ }, [chainOptions, chain, onChainSelect]);
182
+ const mockWithdrawOrderState = useMemo(() => ({
183
+ order_id: orderId ?? "",
184
+ status: "pending",
185
+ chain_id: "56",
186
+ usd1_amount: "0",
187
+ target_chain_id: chain ?? "1",
188
+ target_address: address ?? "0x",
189
+ created_at: new Date().toISOString(),
190
+ updated_at: new Date().toISOString(),
191
+ }), [orderId, chain, address]);
192
+ useEffect(() => {
193
+ if (!orderId) {
194
+ setWithdrawOrder(null);
195
+ return;
196
+ }
197
+ const t = setInterval(() => {
198
+ getWithdrawOrder(orderId)
199
+ .then((order) => {
200
+ setWithdrawOrder(order);
201
+ if (order.status === "completed" && onShowToast) {
202
+ onShowToast("Withdrawal completed");
203
+ }
204
+ })
205
+ .catch(() => setWithdrawOrder(mockWithdrawOrderState));
206
+ }, POLL_INTERVAL_MS);
207
+ getWithdrawOrder(orderId)
208
+ .then(setWithdrawOrder)
209
+ .catch(() => setWithdrawOrder(mockWithdrawOrderState));
210
+ return () => clearInterval(t);
211
+ }, [orderId, onShowToast, mockWithdrawOrderState]);
212
+ const completedOrderIdRef = useRef(null);
213
+ useEffect(() => {
214
+ if (withdrawOrder?.status === "completed" && orderId && completedOrderIdRef.current !== orderId) {
215
+ completedOrderIdRef.current = orderId;
216
+ onWithdrawCompleted?.();
217
+ }
218
+ if (!orderId)
219
+ completedOrderIdRef.current = null;
220
+ }, [withdrawOrder?.status, orderId, onWithdrawCompleted]);
221
+ const receiveAmount = useMemo(() => {
222
+ if (receiveAmountProp) {
223
+ const n = Number(receiveAmountProp);
224
+ if (!Number.isNaN(n))
225
+ return n.toFixed(2);
226
+ return receiveAmountProp;
227
+ }
228
+ if (!apiQuote?.token_amount || !apiQuote?.token_symbol)
229
+ return undefined;
230
+ const amount = Number(apiQuote.token_amount);
231
+ const formatted = Number.isNaN(amount) ? apiQuote.token_amount : amount.toFixed(2);
232
+ return `≈ ${formatted} (wei) ${apiQuote.token_symbol}`;
233
+ }, [receiveAmountProp, apiQuote]);
234
+ const quoteExpired = useMemo(() => {
235
+ if (!apiQuote?.expires_at)
236
+ return false;
237
+ try {
238
+ return new Date(apiQuote.expires_at).getTime() < Date.now();
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ }, [apiQuote?.expires_at]);
244
+ const handleRefreshQuote = useCallback(() => {
245
+ if (!apiChains?.length || !token || !chain)
246
+ return;
247
+ const tokenAddress = getTokenAddressForChain(apiChains, chain, token);
248
+ if (!tokenAddress)
249
+ return;
250
+ const amountNum = amount && Number(amount) > 0 ? Number(amount) : 1;
251
+ const amountWei = String(BigInt(Math.floor(amountNum * 1e18)));
252
+ setLoadingQuote(true);
253
+ quote({
254
+ direction: "withdraw",
255
+ chain_id: chain,
256
+ token_address: tokenAddress,
257
+ usd1_amount: amountWei,
258
+ })
259
+ .then((q) => setApiQuote(q ?? null))
260
+ .catch(() => {
261
+ setApiQuote({
262
+ token_address: tokenAddress,
263
+ token_symbol: token,
264
+ token_decimals: 18,
265
+ rate: "1",
266
+ chain_id: Number(chain) || 56,
267
+ token_amount: amountWei,
268
+ expires_at: new Date(Date.now() + 60000).toISOString(),
269
+ });
270
+ })
271
+ .finally(() => setLoadingQuote(false));
272
+ }, [apiChains, token, chain, amount]);
273
+ const handleAmountChange = useCallback((e) => {
274
+ const raw = e.target.value;
275
+ const maxNum = balance ? parseBalanceNumber(balance) : null;
276
+ if (maxNum != null && raw !== "" && raw !== ".") {
277
+ const num = Number(raw);
278
+ if (!Number.isNaN(num) && num > maxNum) {
279
+ onAmountChange?.({
280
+ ...e,
281
+ target: { ...e.target, value: String(maxNum) },
282
+ });
283
+ return;
284
+ }
285
+ }
286
+ onAmountChange?.(e);
287
+ }, [balance, onAmountChange]);
288
+ if (!session) {
289
+ return _jsx(LoginRequiredOverlay, { title: "Withdraw", onSignIn: onSignIn, onClose: onClose });
290
+ }
291
+ const activeStep = amount ? 3 : chain ? 2 : token ? 1 : address ? 0 : 0;
292
+ const canSubmit = address && token && chain && amount;
293
+ const isSubmitting = status !== "idle";
294
+ /** 已提交提现(有 orderId):隐藏表单;处理中显示等待,完成后显示成功说明,关闭弹层后由调用方清空 orderId 再开可继续提现 */
295
+ const orderInProgress = trackingWithdraw && (!withdrawOrder || withdrawOrder.status !== "completed");
296
+ const orderSucceeded = trackingWithdraw && withdrawOrder?.status === "completed";
297
+ return (_jsxs(ModalFrame, { onClose: onClose, contentStyle: {
298
+ justifyContent: "space-between",
299
+ padding: "24px",
300
+ }, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 32 }, children: [_jsx("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: _jsx("h2", { style: { margin: 0, fontSize: 24, fontWeight: 600, lineHeight: 1.4 }, children: "Withdraw" }) }), _jsxs("div", { style: { display: "flex", gap: 16 }, children: [_jsx("div", { style: {
301
+ opacity: trackingWithdraw ? 0 : 1,
302
+ maxWidth: trackingWithdraw ? 0 : 48,
303
+ overflow: "hidden",
304
+ transition: "opacity 0.3s ease, max-width 0.35s ease",
305
+ }, children: _jsx(StepIndicator, { steps: 4, stepActive: [!!address, !!token, !!chain, !!apiQuote], activeStep: activeStep }) }), _jsxs("div", { style: { flex: 1, position: "relative", minHeight: 400 }, children: [_jsxs("div", { style: {
306
+ position: "absolute",
307
+ inset: 0,
308
+ display: "flex",
309
+ flexDirection: "column",
310
+ alignItems: "center",
311
+ justifyContent: "center",
312
+ gap: 12,
313
+ padding: "0 16px",
314
+ opacity: trackingWithdraw ? 1 : 0,
315
+ maxHeight: trackingWithdraw ? 400 : 0,
316
+ overflow: "hidden",
317
+ pointerEvents: trackingWithdraw ? "auto" : "none",
318
+ transition: "opacity 0.3s ease, max-height 0.35s ease",
319
+ }, children: [orderInProgress && (_jsxs("div", { style: {
320
+ display: "flex",
321
+ flexDirection: "column",
322
+ alignItems: "center",
323
+ justifyContent: "center",
324
+ gap: 32,
325
+ }, children: [_jsx("span", { style: {
326
+ display: "inline-block",
327
+ width: 60,
328
+ height: 60,
329
+ border: `3px solid ${colors.border}`,
330
+ borderTopColor: colors.textPrimary,
331
+ borderRadius: "50%",
332
+ animation: "withdraw-modal-spin 0.8s linear infinite",
333
+ }, "aria-hidden": true }), _jsx("span", { style: {
334
+ fontSize: 20,
335
+ fontWeight: 600,
336
+ color: colors.textPrimary,
337
+ textAlign: "center",
338
+ fontFamily: fonts.family,
339
+ lineHeight: 1.4,
340
+ }, children: "Processing withdrawal..." })] })), orderSucceeded && (() => {
341
+ const destTxHash = withdrawOrder.dst_tx_hash ?? withdrawOrder.out_tx_hash;
342
+ const targetChainName = apiChains?.find((c) => c.chain_id === withdrawOrder.target_chain_id)?.network ?? withdrawOrder.target_chain_id ?? "—";
343
+ const explorerUrl = destTxHash && withdrawOrder.target_chain_id
344
+ ? getExplorerUrl(withdrawOrder.target_chain_id, { txId: destTxHash })
345
+ : null;
346
+ const copyHash = () => {
347
+ if (destTxHash && typeof navigator?.clipboard?.writeText === "function") {
348
+ navigator.clipboard.writeText(destTxHash);
349
+ onShowToast?.("Copied");
350
+ }
351
+ };
352
+ return (_jsxs("div", { style: {
353
+ display: "flex",
354
+ flexDirection: "column",
355
+ alignItems: "center",
356
+ gap: 20,
357
+ width: "100%",
358
+ maxWidth: 412,
359
+ }, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", gap: 24 }, children: [_jsx("span", { style: {
360
+ display: "flex",
361
+ alignItems: "center",
362
+ justifyContent: "center",
363
+ width: 60,
364
+ height: 60,
365
+ borderRadius: "50%",
366
+ color: "#22c55e",
367
+ fontSize: 40,
368
+ lineHeight: 1,
369
+ fontWeight: 700,
370
+ }, "aria-hidden": true, children: _jsx(SuccessIcon, {}) }), _jsx("span", { style: {
371
+ fontSize: 20,
372
+ fontWeight: 600,
373
+ color: colors.textPrimary,
374
+ textAlign: "center",
375
+ fontFamily: fonts.family,
376
+ lineHeight: 1.4,
377
+ }, children: "Withdrawal successful" })] }), _jsxs("div", { style: {
378
+ display: "flex",
379
+ flexDirection: "column",
380
+ gap: 4,
381
+ width: "100%",
382
+ }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", height: 36 }, children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textSecondary }, children: "Amount" }), _jsxs("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textPrimary }, children: [weiToEtherDisplay(withdrawOrder.usd1_amount), " ", tokenSymbol ?? "USD1"] })] }), _jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", height: 36 }, children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textSecondary }, children: "Fee" }), _jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textPrimary }, children: withdrawOrder.fee ?? feeDisplay ?? "—" })] }), _jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", height: 36 }, children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textSecondary }, children: "Network" }), _jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textPrimary }, children: targetChainName })] }), destTxHash && (_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", minHeight: 38 }, children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textSecondary }, children: "Transaction Hash" }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [explorerUrl ? (_jsxs("a", { href: explorerUrl, target: "_blank", rel: "noopener noreferrer", style: { fontSize: 14, color: colors.textPrimary, textDecoration: "underline" }, children: [destTxHash.slice(0, 10), "...", destTxHash.slice(-8)] })) : (_jsxs("span", { style: { fontSize: 14, color: colors.textPrimary }, children: [destTxHash.slice(0, 10), "...", destTxHash.slice(-8)] })), _jsx("button", { type: "button", onClick: copyHash, "aria-label": "Copy hash", style: {
383
+ padding: 0,
384
+ border: "none",
385
+ background: "transparent",
386
+ color: colors.textPrimary,
387
+ cursor: "pointer",
388
+ display: "flex",
389
+ alignItems: "center",
390
+ justifyContent: "center",
391
+ width: 16,
392
+ height: 16,
393
+ }, children: _jsx(CopyIcon, {}) })] })] }))] }), _jsx("div", { style: { paddingTop: 24, width: "100%", display: "flex", justifyContent: "center" }, children: _jsx("button", { type: "button", onClick: () => (onStartAnotherWithdrawal ?? onClose)?.(), style: {
394
+ width: 274,
395
+ padding: "12px 0",
396
+ borderRadius: radii.full,
397
+ border: `1px solid ${colors.buttonDisabledBorder}`,
398
+ background: colors.textPrimary,
399
+ color: "#121214",
400
+ fontSize: 16,
401
+ fontWeight: 500,
402
+ lineHeight: 1.4,
403
+ fontFamily: fonts.family,
404
+ cursor: "pointer",
405
+ textAlign: "center",
406
+ }, children: "Start another withdrawal" }) })] }));
407
+ })()] }), _jsx("div", { style: {
408
+ opacity: trackingWithdraw ? 0 : 1,
409
+ maxHeight: trackingWithdraw ? 0 : 1200,
410
+ overflow: "hidden",
411
+ pointerEvents: trackingWithdraw ? "none" : "auto",
412
+ transition: "opacity 0.3s ease, max-height 0.35s ease",
413
+ }, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 40 }, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, width: "100%" }, children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textPrimary }, children: "Recipient address" }), _jsx("div", { style: {
414
+ display: "flex",
415
+ alignItems: "center",
416
+ height: 44,
417
+ padding: "0 16px",
418
+ borderRadius: radii.input,
419
+ border: `1px solid ${addressInputFocused ? colors.borderFocused : colors.border}`,
420
+ boxSizing: "border-box",
421
+ }, children: _jsx("input", { ref: addressInputRef, value: address, onChange: onAddressChange, onFocus: () => setAddressInputFocused(true), onBlur: () => setAddressInputFocused(false), spellCheck: false, placeholder: "0x", style: {
422
+ flex: 1,
423
+ border: "none",
424
+ outline: "none",
425
+ background: "transparent",
426
+ color: colors.textPrimary,
427
+ fontSize: 14,
428
+ fontFamily: fonts.family,
429
+ lineHeight: 1.4,
430
+ padding: 0,
431
+ } }) })] }), _jsx(DropdownField, { label: "Select token", placeholder: loadingChains ? "Loading…" : "Token", value: token, options: tokenOptions, onSelect: onTokenSelect }), _jsx(DropdownField, { label: "Select chain", placeholder: loadingChains ? "Loading…" : "Chain", value: chain, options: chainOptions, onSelect: onChainSelect }), apiQuote && token && chain && (_jsx("div", { style: {
432
+ padding: "12px 16px",
433
+ borderRadius: radii.card,
434
+ border: `1px solid ${colors.border}`,
435
+ display: "flex",
436
+ flexDirection: "column",
437
+ gap: 8,
438
+ }, children: loadingQuote ? (_jsx("span", { style: { fontSize: 13, color: colors.textSecondary }, children: "Loading\u2026" })) : (_jsxs(_Fragment, { children: [_jsxs("span", { style: { fontSize: 13, color: colors.textSecondary }, children: ["Rate: 1 USD1 = ", apiQuote.rate, " ", apiQuote.token_symbol] }), apiQuote.expires_at && (_jsx(Countdown, { expiresAt: apiQuote.expires_at, isExpired: quoteExpired, onExpired: handleRefreshQuote })), quoteExpired && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: { fontSize: 13, color: "#f59e0b" }, children: "Quote expired, please refresh" }), _jsx("button", { type: "button", onClick: handleRefreshQuote, style: {
439
+ padding: "4px 12px",
440
+ fontSize: 12,
441
+ borderRadius: radii.pill,
442
+ border: `1px solid ${colors.border}`,
443
+ background: "transparent",
444
+ color: colors.textPrimary,
445
+ cursor: "pointer",
446
+ fontFamily: fonts.family,
447
+ }, children: "Refresh" })] }))] })) })), _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textPrimary }, children: "Amount" }), balance && (_jsxs("span", { style: { fontSize: 13, lineHeight: 1.2, color: colors.textSecondary }, children: ["Balance: \u00A0", formatBalanceTo2Decimals(balance)] }))] }), _jsxs("div", { style: {
448
+ display: "flex",
449
+ alignItems: "center",
450
+ justifyContent: "space-between",
451
+ height: 48,
452
+ padding: "0 16px",
453
+ borderRadius: radii.input,
454
+ border: `1px solid ${amountInputFocused ? colors.borderFocused : colors.border}`,
455
+ boxSizing: "border-box",
456
+ }, children: [_jsx("input", { value: amount, onChange: handleAmountChange, onFocus: () => setAmountInputFocused(true), onBlur: () => setAmountInputFocused(false), placeholder: "0.00", type: "text", inputMode: "decimal", style: {
457
+ flex: 1,
458
+ border: "none",
459
+ outline: "none",
460
+ background: "transparent",
461
+ color: amount ? colors.textPrimary : colors.textSecondary,
462
+ fontSize: 14,
463
+ fontFamily: fonts.family,
464
+ lineHeight: 1.4,
465
+ padding: 0,
466
+ } }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: {
467
+ fontSize: 14,
468
+ lineHeight: 1.4,
469
+ color: colors.textSecondary,
470
+ fontFamily: fonts.family,
471
+ }, children: "USD1" }), _jsx("button", { type: "button", onClick: onMaxClick, style: {
472
+ background: colors.textPrimary,
473
+ border: "none",
474
+ borderRadius: radii.pill,
475
+ padding: "2px 8px",
476
+ fontSize: 12,
477
+ lineHeight: 1.4,
478
+ fontWeight: 400,
479
+ color: "#15181D",
480
+ fontFamily: fonts.family,
481
+ cursor: "pointer",
482
+ }, children: "Max" })] })] })] }) })] }) })] })] })] }), _jsxs("div", { style: { padding: "20px 20px 0", display: "flex", flexDirection: "column", gap: 10, alignItems: "center" }, id: "WithdrawModalReceiveAmount", children: [!trackingWithdraw && amount && Number(amount) > 0 && (_jsxs("span", { style: { fontSize: 14, lineHeight: 1.4, color: colors.textPrimary }, children: ["You will receive : ", receiveAmount] })), orderInProgress && withdrawOrder && (() => {
483
+ const fundingHash = txHash ?? withdrawOrder?.funding_tx_hash;
484
+ const showFundingLink = fundingHash && fundingChainId;
485
+ try {
486
+ return (_jsx("div", { style: { display: "none", flexDirection: "column", gap: 6, alignItems: "center" }, children: showFundingLink && (_jsx("a", { href: getExplorerUrl(fundingChainId, { txId: fundingHash }), target: "_blank", rel: "noopener noreferrer", style: { fontSize: 13, color: colors.textPrimary, textDecoration: "underline" }, children: "Withdraw request transaction on explorer" })) }));
487
+ }
488
+ catch {
489
+ return (_jsx("span", { style: { fontSize: 13, color: colors.textPrimary }, children: "Withdraw in progress." }));
490
+ }
491
+ })(), orderInProgress && withdrawOrder && (_jsxs("span", { style: { fontSize: 13, color: colors.textSecondary, display: "none", alignItems: "center", gap: 6 }, children: ["Order status: ", withdrawOrder.status, _jsx("span", { style: {
492
+ display: "inline-block",
493
+ width: 10,
494
+ height: 10,
495
+ border: `2px solid ${colors.textSecondary}`,
496
+ borderTopColor: "transparent",
497
+ borderRadius: "50%",
498
+ animation: "withdraw-modal-spin 0.7s linear infinite",
499
+ }, "aria-hidden": true })] })), _jsx("style", { children: `@keyframes withdraw-modal-spin { to { transform: rotate(360deg); } }` }), orderSucceeded || trackingWithdraw ? null : (_jsx(SubmitButton, { disabled: !canSubmit || isSubmitting, status: status, onClick: () => onSubmit?.({
500
+ toAddress: address,
501
+ amount: amount ?? "",
502
+ token: token ?? "",
503
+ chain: chain ?? "",
504
+ }) }))] })] }));
505
+ };
506
+ const SubmitButton = ({ disabled, status, onClick }) => {
507
+ const labels = {
508
+ idle: disabled ? "Please fill in withdrawal information" : "Submit",
509
+ pending: "Processing...",
510
+ success: "Withdrawal successful",
511
+ manual_review: "Under manual review",
512
+ };
513
+ const isDisabled = disabled || status !== "idle";
514
+ return (_jsx("button", { type: "button", disabled: isDisabled, onClick: onClick, style: {
515
+ width: 364,
516
+ padding: "12px 0",
517
+ borderRadius: radii.full,
518
+ border: isDisabled ? `1px solid ${colors.buttonDisabledBorder}` : "none",
519
+ background: isDisabled ? colors.buttonDisabledBg : colors.textPrimary,
520
+ color: isDisabled ? colors.textSecondary : "#15181D",
521
+ fontSize: 16,
522
+ fontWeight: 500,
523
+ lineHeight: 1.4,
524
+ fontFamily: fonts.family,
525
+ cursor: isDisabled ? "not-allowed" : "pointer",
526
+ textAlign: "center",
527
+ }, children: labels[status] }));
528
+ };
@@ -0,0 +1,4 @@
1
+ export interface CloseButtonProps {
2
+ onClick?: () => void;
3
+ }
4
+ export declare const CloseButton: ({ onClick }: CloseButtonProps) => import("react/jsx-runtime.js").JSX.Element;