@ab-org/predicate-market-sdk 0.0.2 → 0.1.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.
@@ -8,7 +8,8 @@ import { DepositDetailsPanel } from "./components/DepositDetailsPanel.js";
8
8
  import { LoginRequiredOverlay } from "./components/LoginRequiredOverlay.js";
9
9
  import { useSession } from "./hooks/useSession.js";
10
10
  import { colors, fonts, radii } from "./theme.js";
11
- import { getChains, quote, } from "../modules/api.js";
11
+ import { getChains, quote, registerPlatform, } from "../modules/api.js";
12
+ import { getEnv } from "../utils/env";
12
13
  /** 校验是否为合法的充值地址(传入值):非空且长度满足常见链地址格式 */
13
14
  function isValidDepositAddress(v) {
14
15
  return typeof v === "string" && v.trim().length >= 20;
@@ -43,8 +44,6 @@ const DefaultCryptoIcons = () => {
43
44
  }, children: t.label }, i))) }));
44
45
  };
45
46
  /* ─── Helpers: derive options from getChains ─── */
46
- /** Deposit 时不可选 USD1(充值是转入 USD1,源代币为 USDT/USDC 等) */
47
- const DEPOSIT_EXCLUDED_TOKEN = "USD1";
48
47
  function chainsToTokenOptions(chains) {
49
48
  const bySymbol = new Map();
50
49
  for (const c of chains) {
@@ -54,7 +53,6 @@ function chainsToTokenOptions(chains) {
54
53
  }
55
54
  }
56
55
  return Array.from(bySymbol.entries())
57
- .filter(([id]) => id !== DEPOSIT_EXCLUDED_TOKEN)
58
56
  .map(([id, { symbol }]) => ({
59
57
  id,
60
58
  label: symbol,
@@ -88,6 +86,8 @@ function getTokenAddressForChain(chains, chainId, tokenSymbol) {
88
86
  const chain = chains.find((c) => c.chain_id === chainId);
89
87
  return chain?.tokens.find((t) => t.symbol === tokenSymbol)?.address;
90
88
  }
89
+ /* ─── Main Component ─────────────────────────── */
90
+ const FUNDING_TOKEN_SYMBOL = getEnv("FUNDING_TOKEN_SYMBOL");
91
91
  export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, chainOptions: chainOptionsProp, depositAddress, minimumDeposit, qrCenterIcon, cryptoIcons, depositAmount, onShowToast, txHash, explorerTxUrl, onTokenSelect, onChainSelect, onCopyAddress, onBuyCrypto, onSignIn, onBack, onClose, }) => {
92
92
  const session = useSession();
93
93
  const [view, setView] = useState("entry");
@@ -97,10 +97,12 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
97
97
  const [loadingChains, setLoadingChains] = useState(false);
98
98
  const [loadingQuote, setLoadingQuote] = useState(false);
99
99
  const [quoteRefreshKey, setQuoteRefreshKey] = useState(0);
100
+ const [internalDepositAddress, setInternalDepositAddress] = useState(undefined);
101
+ const lastEmittedAddressRef = useRef(undefined);
102
+ const lastEmittedChainRef = useRef(undefined);
100
103
  const tokenOptions = useMemo(() => {
101
- const excludeUsd1 = (opts) => opts.filter((o) => o.id !== DEPOSIT_EXCLUDED_TOKEN && o.label !== DEPOSIT_EXCLUDED_TOKEN);
102
104
  if (tokenOptionsProp?.length)
103
- return excludeUsd1(tokenOptionsProp);
105
+ return tokenOptionsProp;
104
106
  if (!apiChains?.length)
105
107
  return undefined;
106
108
  return chainsToTokenOptions(apiChains);
@@ -112,12 +114,6 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
112
114
  return undefined;
113
115
  return chainsToChainOptionsForToken(apiChains, token);
114
116
  }, [chainOptionsProp, apiChains, token]);
115
- // 当前选中的是 USD1 时自动切到第一个可选 token(deposit 不允许选 USD1)
116
- useEffect(() => {
117
- if (token !== DEPOSIT_EXCLUDED_TOKEN || !tokenOptions?.length || !onTokenSelect)
118
- return;
119
- onTokenSelect(tokenOptions[0].id);
120
- }, [token, tokenOptions, onTokenSelect]);
121
117
  // 仅有一个 chain 选项时默认选中
122
118
  useEffect(() => {
123
119
  if (chainOptions?.length !== 1 || !onChainSelect)
@@ -135,6 +131,25 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
135
131
  .then((res) => setApiChains(res?.chains ?? {}))
136
132
  .finally(() => setLoadingChains(false));
137
133
  }, [view]);
134
+ // Fetch depositAddress internally when user is signed in and a chain has been selected.
135
+ useEffect(() => {
136
+ if (view !== "transfer")
137
+ return;
138
+ if (!session?.address || !chain)
139
+ return;
140
+ registerPlatform({
141
+ platform_contract_address: session.address,
142
+ chain_id: chain,
143
+ })
144
+ .then((res) => {
145
+ if (res?.deposit_address) {
146
+ setInternalDepositAddress(res.deposit_address);
147
+ }
148
+ })
149
+ .catch(() => {
150
+ // silent failure; UI will keep placeholder if any
151
+ });
152
+ }, [view, chain, session?.address]);
138
153
  useEffect(() => {
139
154
  if (!apiChains?.length || !token || !chain) {
140
155
  setApiQuote(null);
@@ -166,12 +181,25 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
166
181
  rate: "1",
167
182
  chain_id: Number(chain) || 56,
168
183
  deposit_address: "0x" + "0".repeat(39) + "1",
169
- usd1_amount: depositAmount ?? "0",
184
+ dst_token_amount: depositAmount ?? "0",
170
185
  expires_at: new Date(Date.now() + 60000).toISOString(),
171
186
  });
172
187
  })
173
188
  .finally(() => setLoadingQuote(false));
174
189
  }, [apiChains, token, chain, depositAmount, quoteRefreshKey, onShowToast]);
190
+ // Emit resolved address when external prop is provided/changes
191
+ useEffect(() => {
192
+ if (!chain)
193
+ return;
194
+ if (!depositAddress)
195
+ return;
196
+ if (lastEmittedAddressRef.current === depositAddress &&
197
+ lastEmittedChainRef.current === chain) {
198
+ return;
199
+ }
200
+ lastEmittedAddressRef.current = depositAddress;
201
+ lastEmittedChainRef.current = chain;
202
+ }, [depositAddress, chain]);
175
203
  const handleQuoteExpired = useCallback(() => {
176
204
  setQuoteRefreshKey((k) => k + 1);
177
205
  }, []);
@@ -187,8 +215,8 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
187
215
  setView("entry");
188
216
  onBack?.();
189
217
  };
190
- const handleCopyAddress = useCallback(() => {
191
- onCopyAddress?.();
218
+ const handleCopyAddress = useCallback((address) => {
219
+ onCopyAddress?.(address);
192
220
  onShowToast?.("Address copied");
193
221
  setCopySuccessMessage("Address copied");
194
222
  }, [onCopyAddress, onShowToast]);
@@ -211,7 +239,7 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
211
239
  fontWeight: 500,
212
240
  boxShadow: "0 4px 12px rgba(0,0,0,.25)",
213
241
  zIndex: 10,
214
- }, children: copySuccessMessage })), view === "entry" ? (_jsx(EntryView, { cryptoIcons: cryptoIcons, onTransferCrypto: () => setView("transfer"), onBuyCrypto: onBuyCrypto })) : (_jsx(TransferView, { token: token, chain: chain, tokenOptions: tokenOptions, chainOptions: chainOptions, depositAddress: depositAddress, minimumDeposit: minimumDeposit, qrCenterIcon: qrCenterIcon, quote: apiQuote, quoteLoading: loadingQuote, txHash: txHash, chainIdForExplorer: chain, explorerTxUrl: explorerTxUrl, loadingChains: loadingChains, onTokenSelect: onTokenSelect, onChainSelect: onChainSelect, onCopyAddress: handleCopyAddress, onQuoteExpired: handleQuoteExpired, onRefreshQuote: () => {
242
+ }, children: copySuccessMessage })), view === "entry" ? (_jsx(EntryView, { cryptoIcons: cryptoIcons, onTransferCrypto: () => setView("transfer"), onBuyCrypto: onBuyCrypto })) : (_jsx(TransferView, { token: token, chain: chain, tokenOptions: tokenOptions, chainOptions: chainOptions, depositAddress: depositAddress ?? internalDepositAddress, minimumDeposit: minimumDeposit, qrCenterIcon: qrCenterIcon, quote: apiQuote, quoteLoading: loadingQuote, txHash: txHash, chainIdForExplorer: chain, explorerTxUrl: explorerTxUrl, loadingChains: loadingChains, onTokenSelect: onTokenSelect, onChainSelect: onChainSelect, onCopyAddress: handleCopyAddress, onQuoteExpired: handleQuoteExpired, onRefreshQuote: () => {
215
243
  if (!apiChains?.length || !token || !chain)
216
244
  return;
217
245
  const tokenAddress = getTokenAddressForChain(apiChains, chain, token);
@@ -227,7 +255,7 @@ export const DepositModal = ({ token, chain, tokenOptions: tokenOptionsProp, cha
227
255
  rate: "1",
228
256
  chain_id: Number(chain) || 56,
229
257
  deposit_address: "0x" + "0".repeat(39) + "1",
230
- usd1_amount: depositAmount ?? "0",
258
+ dst_token_amount: depositAmount ?? "0",
231
259
  expires_at: new Date(Date.now() + 60000).toISOString(),
232
260
  });
233
261
  })
@@ -302,7 +330,7 @@ const TransferView = ({ token, chain, tokenOptions, chainOptions, depositAddress
302
330
  display: "flex",
303
331
  flexDirection: "column",
304
332
  gap: 8,
305
- }, children: quoteLoading ? (_jsx("span", { style: { fontSize: 13, color: colors.textSecondary }, children: "Loading\u2026" })) : (_jsxs(_Fragment, { children: [_jsxs("span", { style: { fontSize: 13, color: colors.textSecondary }, children: ["1 ", quoteData.token_symbol, " = ", quoteData.rate, " USD1"] }), quoteData.expires_at && (_jsx(Countdown, { expiresAt: quoteData.expires_at, isExpired: quoteExpired, onExpired: onQuoteExpired })), quoteExpired && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: { fontSize: 13, color: "#f59e0b" }, children: "Quote expired, please refresh" }), onRefreshQuote && (_jsx("button", { type: "button", onClick: onRefreshQuote, style: {
333
+ }, children: quoteLoading ? (_jsx("span", { style: { fontSize: 13, color: colors.textSecondary }, children: "Loading\u2026" })) : (_jsxs(_Fragment, { children: [_jsxs("span", { style: { fontSize: 13, color: colors.textSecondary }, children: ["1 ", quoteData.token_symbol, " = ", quoteData.rate, " ", FUNDING_TOKEN_SYMBOL] }), quoteData.expires_at && (_jsx(Countdown, { expiresAt: quoteData.expires_at, isExpired: quoteExpired, onExpired: onQuoteExpired })), quoteExpired && (_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: { fontSize: 13, color: "#f59e0b" }, children: "Quote expired, please refresh" }), onRefreshQuote && (_jsx("button", { type: "button", onClick: onRefreshQuote, style: {
306
334
  padding: "4px 12px",
307
335
  fontSize: 12,
308
336
  borderRadius: radii.pill,
@@ -1,5 +1,6 @@
1
1
  import type { ChangeEventHandler } from "react";
2
2
  import { type SelectOption } from "./components/DropdownField.js";
3
+ import { type WithdrawOrderResponseData } from "../modules/api.js";
3
4
  export type WithdrawUiStatus = "idle" | "pending" | "success" | "manual_review";
4
5
  export interface WithdrawModalProps {
5
6
  address?: string;
@@ -18,6 +19,14 @@ export interface WithdrawModalProps {
18
19
  useMerchantApi?: boolean;
19
20
  /** 创建订单后传入,用于轮询提现订单状态 */
20
21
  orderId?: string;
22
+ /** 提现模式:direct 直接提现,cross_chain 跨链提现 */
23
+ withdrawMode?: "direct" | "cross_chain";
24
+ /**
25
+ * 直接提现模式(无 orderId)下的结果数据;
26
+ * 将用来构造一个与 getWithdrawOrder 返回值兼容的对象,从而复用“有 orderId”时的全部交互与展示。
27
+ * 最少建议提供:status、dst_token_amount、target_chain_id、target_address(必要时包含 dst_tx_hash / out_tx_hash、fee)。
28
+ */
29
+ withdrawDirectResult?: Partial<WithdrawOrderResponseData>;
21
30
  /** 成功页 Fee 展示:优先用订单接口返回的 fee,若后端未返回则用此值(如询价时的 fee),均无则显示 "—" */
22
31
  feeDisplay?: string;
23
32
  /** 广播后的 funding tx 所在链 id,与 txHash 一起用于展示「查看交易」链接 */
@@ -30,11 +39,12 @@ export interface WithdrawModalProps {
30
39
  onChainSelect?: (id: string) => void;
31
40
  onAmountChange?: ChangeEventHandler<HTMLInputElement>;
32
41
  onMaxClick?: () => void;
33
- /** 提交时传入当前表单值(收款地址、金额、所选 token、所选 chain),由调用方执行提现 */
42
+ /** 提交时传入当前表单值(收款地址、金额、所选 token、链上合约地址、所选 chain),由调用方执行提现 */
34
43
  onSubmit?: (payload: {
35
44
  toAddress: string;
36
45
  amount: string;
37
46
  token: string;
47
+ tokenAddress: string;
38
48
  chain: string;
39
49
  }) => void;
40
50
  /** 提现订单状态变为 completed 时调用,用于调用方刷新余额等 */
@@ -44,4 +54,4 @@ export interface WithdrawModalProps {
44
54
  onSignIn?: () => void;
45
55
  onClose?: () => void;
46
56
  }
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;
57
+ export declare const WithdrawModal: ({ address, token, tokenSymbol, chain, amount, balance, status, receiveAmount: receiveAmountProp, txHash, tokenOptions: tokenOptionsProp, chainOptions: chainOptionsProp, useMerchantApi, orderId, withdrawMode, withdrawDirectResult, feeDisplay, fundingChainId, onShowToast, onAddressChange, onTokenSelect, onChainSelect, onAmountChange, onMaxClick, onSubmit, onWithdrawCompleted, onStartAnotherWithdrawal, onSignIn, onClose, }: WithdrawModalProps) => import("react/jsx-runtime.js").JSX.Element;
@@ -8,11 +8,13 @@ import { LoginRequiredOverlay } from "./components/LoginRequiredOverlay.js";
8
8
  import { useSession } from "./hooks/useSession.js";
9
9
  import { colors, fonts, radii } from "./theme.js";
10
10
  import { getChains, quote, getWithdrawOrder, } from "../modules/api.js";
11
+ import { getEnv } from "../utils/env";
11
12
  import { getExplorerUrl } from "../utils/explorer.js";
12
13
  import { SuccessIcon } from "./components/Success";
13
14
  function CopyIcon() {
14
15
  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
  }
17
+ const FUNDING_TOKEN_SYMBOL = getEnv("FUNDING_TOKEN_SYMBOL");
16
18
  function chainsToTokenOptions(chains) {
17
19
  const bySymbol = new Map();
18
20
  for (const c of chains) {
@@ -36,7 +38,7 @@ function getTokenAddressForChain(chains, chainId, tokenSymbol) {
36
38
  const chain = chains.find((c) => c.chain_id === chainId);
37
39
  return chain?.tokens.find((t) => t.symbol === tokenSymbol)?.address;
38
40
  }
39
- /** 从 balance 字符串解析出数值部分(如 "123.46 USD1" → 123.46) */
41
+ /** 从 balance 字符串解析出数值部分(如 "123.46 xxx" → 123.46) */
40
42
  function parseBalanceNumber(balance) {
41
43
  const match = balance.trim().match(/^(\d*\.?\d*)/);
42
44
  if (!match)
@@ -44,7 +46,7 @@ function parseBalanceNumber(balance) {
44
46
  const n = Number(match[1]);
45
47
  return Number.isNaN(n) ? null : n;
46
48
  }
47
- /** 将 balance 字符串格式化为小数点后 2 位(如 "123.456789012 USD1" → "123.46 USD1") */
49
+ /** 将 balance 字符串格式化为小数点后 2 位(如 "123.456789012 xxx" → "123.46 xxx") */
48
50
  function formatBalanceTo2Decimals(balance) {
49
51
  const match = balance.trim().match(/^(\d*\.?\d*)(.*)$/);
50
52
  if (!match)
@@ -57,7 +59,6 @@ function formatBalanceTo2Decimals(balance) {
57
59
  const formatted = n.toFixed(2);
58
60
  return formatted + suffix;
59
61
  }
60
- const WEI_PER_ETHER = 1e18;
61
62
  /** Wei (string) to ether display string; keeps up to 6 decimal places, strips trailing zeros */
62
63
  function weiToEtherDisplay(wei) {
63
64
  const s = (wei || "0").trim().replace(/^0+/, "") || "0";
@@ -74,7 +75,7 @@ function weiToEtherDisplay(wei) {
74
75
  return fixed;
75
76
  }
76
77
  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
+ export const WithdrawModal = ({ address = "", token, tokenSymbol, chain, amount = "", balance, status = "idle", receiveAmount: receiveAmountProp, txHash, tokenOptions: tokenOptionsProp, chainOptions: chainOptionsProp, useMerchantApi = false, orderId, withdrawMode, withdrawDirectResult, feeDisplay, fundingChainId, onShowToast, onAddressChange, onTokenSelect, onChainSelect, onAmountChange, onMaxClick, onSubmit, onWithdrawCompleted, onStartAnotherWithdrawal, onSignIn, onClose, }) => {
78
79
  const session = useSession();
79
80
  const addressInputRef = useRef(null);
80
81
  const [addressInputFocused, setAddressInputFocused] = useState(false);
@@ -100,7 +101,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
100
101
  : undefined;
101
102
  if (!raw?.length)
102
103
  return undefined;
103
- return raw.filter((o) => o.id !== "USD1" && o.label !== "USD1");
104
+ return raw;
104
105
  }, [tokenOptionsProp, apiChains]);
105
106
  const chainOptions = useMemo(() => {
106
107
  if (chainOptionsProp?.length)
@@ -109,7 +110,19 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
109
110
  return undefined;
110
111
  return chainsToChainOptionsForToken(apiChains, token);
111
112
  }, [chainOptionsProp, apiChains, token]);
112
- const trackingWithdraw = useMemo(() => Boolean(orderId), [orderId]);
113
+ /** 当前选中的 token + chain getChains 数据中的合约地址(与询价 / quote 使用同一解析方式) */
114
+ const resolvedTokenAddress = useMemo(() => {
115
+ if (!apiChains?.length || !token || !chain)
116
+ return undefined;
117
+ return getTokenAddressForChain(apiChains, chain, token);
118
+ }, [apiChains, token, chain]);
119
+ // direct 模式的内部激活标记(便于“Start another withdrawal”时重置为表单态)
120
+ const [directActive, setDirectActive] = useState(false);
121
+ // 当外部把 withdrawMode 设为 direct 且提供 withdrawDirectResut 时,进入“追踪提现”态
122
+ useEffect(() => {
123
+ setDirectActive(withdrawMode === "direct" && Boolean(withdrawDirectResult));
124
+ }, [withdrawMode, withdrawDirectResult]);
125
+ const trackingWithdraw = useMemo(() => Boolean(orderId) || directActive, [orderId, directActive]);
113
126
  // Default focus on recipient address when form is visible; white border while focused, revert on blur
114
127
  useEffect(() => {
115
128
  if (trackingWithdraw)
@@ -126,10 +139,10 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
126
139
  return;
127
140
  setLoadingChains(true);
128
141
  getChains()
129
- .then((res) => setApiChains(res?.chains ?? {}))
142
+ .then((res) => setApiChains(res?.chains ?? []))
130
143
  .finally(() => setLoadingChains(false));
131
144
  }, [shouldLoadChains]);
132
- // 选中 token + chain 后即询价;未填 amount 时用 1 USD1 获取 rate
145
+ // 选中 token + chain 后即询价;未填 amount 时用 1 xxx 获取 rate
133
146
  useEffect(() => {
134
147
  if (!apiChains?.length || !token || !chain) {
135
148
  setApiQuote(null);
@@ -147,7 +160,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
147
160
  direction: "withdraw",
148
161
  chain_id: chain,
149
162
  token_address: tokenAddress,
150
- usd1_amount: amountWei,
163
+ dst_token_amount: amountWei,
151
164
  })
152
165
  .then((q) => setApiQuote(q ?? null))
153
166
  .catch(() => {
@@ -183,41 +196,73 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
183
196
  order_id: orderId ?? "",
184
197
  status: "pending",
185
198
  chain_id: "56",
186
- usd1_amount: "0",
199
+ dst_token_amount: "0",
187
200
  target_chain_id: chain ?? "1",
188
201
  target_address: address ?? "0x",
189
202
  created_at: new Date().toISOString(),
190
203
  updated_at: new Date().toISOString(),
191
204
  }), [orderId, chain, address]);
192
205
  useEffect(() => {
193
- if (!orderId) {
194
- setWithdrawOrder(null);
195
- return;
196
- }
197
- const t = setInterval(() => {
206
+ // 1) 有 orderId:按原逻辑轮询
207
+ if (orderId) {
208
+ const t = setInterval(() => {
209
+ getWithdrawOrder(orderId)
210
+ .then((order) => {
211
+ setWithdrawOrder(order);
212
+ if (order.status === "completed" && onShowToast) {
213
+ onShowToast("Withdrawal completed");
214
+ }
215
+ })
216
+ .catch(() => setWithdrawOrder(mockWithdrawOrderState));
217
+ }, POLL_INTERVAL_MS);
198
218
  getWithdrawOrder(orderId)
199
- .then((order) => {
200
- setWithdrawOrder(order);
201
- if (order.status === "completed" && onShowToast) {
202
- onShowToast("Withdrawal completed");
203
- }
204
- })
219
+ .then(setWithdrawOrder)
205
220
  .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);
221
+ return () => clearInterval(t);
222
+ }
223
+ // 2) 无 orderId 且 direct 模式:用传入的结果构造一个“伪订单”,以复用已提交/成功页
224
+ if (!orderId && directActive) {
225
+ const now = new Date().toISOString();
226
+ const inferredAmountWei = (() => {
227
+ if (withdrawDirectResult?.dst_token_amount)
228
+ return withdrawDirectResult.dst_token_amount;
229
+ const n = Number(amount || "0");
230
+ return String(n > 0 ? BigInt(Math.floor(n * 1e18)) : 0n);
231
+ })();
232
+ const synthetic = {
233
+ order_id: withdrawDirectResult?.order_id ?? `direct-${Date.now()}`,
234
+ status: withdrawDirectResult?.status ?? "pending",
235
+ chain_id: withdrawDirectResult?.chain_id ?? String(fundingChainId ?? "3131"),
236
+ dst_token_amount: inferredAmountWei,
237
+ fee: withdrawDirectResult?.fee ?? feeDisplay,
238
+ target_chain_id: withdrawDirectResult?.target_chain_id ?? (chain ?? ""),
239
+ target_address: withdrawDirectResult?.target_address ?? (address ?? ""),
240
+ funding_tx_hash: withdrawDirectResult?.funding_tx_hash ?? txHash,
241
+ dst_tx_hash: withdrawDirectResult?.dst_tx_hash,
242
+ out_tx_hash: withdrawDirectResult?.out_tx_hash,
243
+ created_at: withdrawDirectResult?.created_at ?? now,
244
+ updated_at: withdrawDirectResult?.updated_at ?? now,
245
+ one_time_address: withdrawDirectResult?.one_time_address,
246
+ };
247
+ setWithdrawOrder(synthetic);
248
+ return;
249
+ }
250
+ // 3) 默认:清空订单,显示表单
251
+ setWithdrawOrder(null);
252
+ }, [orderId, directActive, withdrawDirectResult, amount, feeDisplay, chain, address, txHash, fundingChainId, onShowToast, mockWithdrawOrderState]);
253
+ // 订单完成回调:支持 orderId 或 direct 模式
254
+ const completedKeyRef = useRef(null);
213
255
  useEffect(() => {
214
- if (withdrawOrder?.status === "completed" && orderId && completedOrderIdRef.current !== orderId) {
215
- completedOrderIdRef.current = orderId;
256
+ if (withdrawOrder?.status !== "completed")
257
+ return;
258
+ const key = orderId ?? withdrawOrder.order_id;
259
+ if (key && completedKeyRef.current !== key) {
260
+ completedKeyRef.current = key;
216
261
  onWithdrawCompleted?.();
217
262
  }
218
- if (!orderId)
219
- completedOrderIdRef.current = null;
220
- }, [withdrawOrder?.status, orderId, onWithdrawCompleted]);
263
+ if (!trackingWithdraw)
264
+ completedKeyRef.current = null;
265
+ }, [withdrawOrder?.status, orderId, trackingWithdraw, onWithdrawCompleted]);
221
266
  const receiveAmount = useMemo(() => {
222
267
  if (receiveAmountProp) {
223
268
  const n = Number(receiveAmountProp);
@@ -254,7 +299,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
254
299
  direction: "withdraw",
255
300
  chain_id: chain,
256
301
  token_address: tokenAddress,
257
- usd1_amount: amountWei,
302
+ dst_token_amount: amountWei,
258
303
  })
259
304
  .then((q) => setApiQuote(q ?? null))
260
305
  .catch(() => {
@@ -379,7 +424,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
379
424
  flexDirection: "column",
380
425
  gap: 4,
381
426
  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: {
427
+ }, 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.dst_token_amount), " ", tokenSymbol] })] }), _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
428
  padding: 0,
384
429
  border: "none",
385
430
  background: "transparent",
@@ -390,7 +435,12 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
390
435
  justifyContent: "center",
391
436
  width: 16,
392
437
  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: {
438
+ }, children: _jsx(CopyIcon, {}) })] })] }))] }), _jsx("div", { style: { paddingTop: 24, width: "100%", display: "flex", justifyContent: "center" }, children: _jsx("button", { type: "button", onClick: () => {
439
+ // 重置 direct 模式追踪与本地订单,恢复到表单态
440
+ setDirectActive(false);
441
+ setWithdrawOrder(null);
442
+ (onStartAnotherWithdrawal ?? onClose)?.();
443
+ }, style: {
394
444
  width: 274,
395
445
  padding: "12px 0",
396
446
  borderRadius: radii.full,
@@ -435,7 +485,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
435
485
  display: "flex",
436
486
  flexDirection: "column",
437
487
  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: {
488
+ }, 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 ", FUNDING_TOKEN_SYMBOL, " = ", 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
489
  padding: "4px 12px",
440
490
  fontSize: 12,
441
491
  borderRadius: radii.pill,
@@ -444,7 +494,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
444
494
  color: colors.textPrimary,
445
495
  cursor: "pointer",
446
496
  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: {
497
+ }, 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), " ", FUNDING_TOKEN_SYMBOL] }))] }), _jsxs("div", { style: {
448
498
  display: "flex",
449
499
  alignItems: "center",
450
500
  justifyContent: "space-between",
@@ -463,23 +513,18 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
463
513
  fontFamily: fonts.family,
464
514
  lineHeight: 1.4,
465
515
  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 && (() => {
516
+ } }), _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: _jsx("button", { type: "button", onClick: onMaxClick, style: {
517
+ background: colors.textPrimary,
518
+ border: "none",
519
+ borderRadius: radii.pill,
520
+ padding: "2px 8px",
521
+ fontSize: 12,
522
+ lineHeight: 1.4,
523
+ fontWeight: 400,
524
+ color: "#15181D",
525
+ fontFamily: fonts.family,
526
+ cursor: "pointer",
527
+ }, 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
528
  const fundingHash = txHash ?? withdrawOrder?.funding_tx_hash;
484
529
  const showFundingLink = fundingHash && fundingChainId;
485
530
  try {
@@ -500,6 +545,7 @@ export const WithdrawModal = ({ address = "", token, tokenSymbol = "USDT", chain
500
545
  toAddress: address,
501
546
  amount: amount ?? "",
502
547
  token: token ?? "",
548
+ tokenAddress: resolvedTokenAddress ?? apiQuote?.token_address ?? "",
503
549
  chain: chain ?? "",
504
550
  }) }))] })] }));
505
551
  };
@@ -3,6 +3,6 @@ export interface DepositDetailsPanelProps {
3
3
  address: string;
4
4
  tokenIcon?: ReactNode;
5
5
  minimumDeposit?: string;
6
- onCopyAddress?: () => void;
6
+ onCopyAddress?: (address: string) => void;
7
7
  }
8
8
  export declare const DepositDetailsPanel: ({ address, tokenIcon, minimumDeposit, onCopyAddress, }: DepositDetailsPanelProps) => import("react/jsx-runtime.js").JSX.Element;
@@ -56,9 +56,32 @@ const StyledQR = ({ data }) => {
56
56
  const CopyIcon = () => (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", children: [_jsx("rect", { x: "4.5", y: "4.5", width: "7", height: "7", rx: "1.5", stroke: colors.textPrimary, strokeWidth: "1.2" }), _jsx("path", { d: "M9.5 4.5V3.5C9.5 2.67 8.83 2 8 2H3.5C2.67 2 2 2.67 2 3.5V8C2 8.83 2.67 9.5 3.5 9.5H4.5", stroke: colors.textPrimary, strokeWidth: "1.2" })] }));
57
57
  /* ─── Main ──────────────────────────────────── */
58
58
  export const DepositDetailsPanel = ({ address, tokenIcon, minimumDeposit, onCopyAddress, }) => {
59
- const handleCopy = () => {
60
- navigator.clipboard?.writeText(address);
61
- onCopyAddress?.();
59
+ const canCopy = typeof address === "string" && address.trim().length > 0;
60
+ const handleCopy = async () => {
61
+ if (!canCopy)
62
+ return;
63
+ try {
64
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
65
+ await navigator.clipboard.writeText(address);
66
+ }
67
+ else {
68
+ const el = document.createElement("textarea");
69
+ el.value = address;
70
+ el.setAttribute("readonly", "");
71
+ el.style.position = "absolute";
72
+ el.style.left = "-9999px";
73
+ document.body.appendChild(el);
74
+ el.select();
75
+ try {
76
+ document.execCommand("copy");
77
+ }
78
+ catch { }
79
+ document.body.removeChild(el);
80
+ }
81
+ }
82
+ finally {
83
+ onCopyAddress?.(address);
84
+ }
62
85
  };
63
86
  return (_jsxs("div", { style: {
64
87
  display: "flex",
@@ -95,7 +118,7 @@ export const DepositDetailsPanel = ({ address, tokenIcon, minimumDeposit, onCopy
95
118
  display: "flex",
96
119
  justifyContent: "space-between",
97
120
  fontSize: 13,
98
- }, children: [_jsx("span", { style: { color: colors.textSecondary }, children: "Minimum deposit" }), _jsx("span", { style: { color: colors.textPrimary, fontWeight: 500 }, children: minimumDeposit })] })), _jsxs("button", { type: "button", onClick: handleCopy, style: {
121
+ }, children: [_jsx("span", { style: { color: colors.textSecondary }, children: "Minimum deposit" }), _jsx("span", { style: { color: colors.textPrimary, fontWeight: 500 }, children: minimumDeposit })] })), _jsxs("button", { type: "button", onClick: handleCopy, disabled: !canCopy, style: {
99
122
  display: "flex",
100
123
  alignItems: "center",
101
124
  gap: 6,
@@ -106,10 +129,13 @@ export const DepositDetailsPanel = ({ address, tokenIcon, minimumDeposit, onCopy
106
129
  color: colors.textPrimary,
107
130
  fontSize: 14,
108
131
  fontWeight: 500,
109
- cursor: "pointer",
132
+ cursor: canCopy ? "pointer" : "not-allowed",
133
+ opacity: canCopy ? 1 : 0.6,
110
134
  fontFamily: fonts.family,
111
135
  transition: "background .15s",
112
136
  }, onMouseEnter: (e) => {
137
+ if (!canCopy)
138
+ return;
113
139
  e.currentTarget.style.background = "rgba(255,255,255,0.06)";
114
140
  }, onMouseLeave: (e) => {
115
141
  e.currentTarget.style.background = "transparent";
@@ -16,6 +16,9 @@ export interface SignInUiConfig {
16
16
  socialProviders?: SocialProvider[];
17
17
  wallets?: WalletItem[];
18
18
  initialVisibleCount?: number;
19
+ /**
20
+ * @deprecated Ignored by `SignInModal` / `useSignInModalController`: social login creates the session without `window.confirm`.
21
+ */
19
22
  sessionConfirmation?: {
20
23
  enabled?: boolean;
21
24
  title?: string;