@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.
- package/README.md +246 -0
- package/dist/auth/autoReconnect.d.ts +11 -0
- package/dist/auth/autoReconnect.js +36 -0
- package/dist/auth/bundledConfig.d.ts +2 -0
- package/dist/auth/bundledConfig.js +19 -0
- package/dist/auth/config.d.ts +29 -0
- package/dist/auth/config.js +53 -0
- package/dist/auth/google.d.ts +43 -0
- package/dist/auth/google.js +147 -0
- package/dist/auth/twitter.d.ts +7 -0
- package/dist/auth/twitter.js +94 -0
- package/dist/constants/chains.d.ts +22 -0
- package/dist/constants/chains.js +23 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/modules/api.d.ts +144 -0
- package/dist/modules/api.js +93 -0
- package/dist/modules/balanceQuery.d.ts +20 -0
- package/dist/modules/balanceQuery.js +58 -0
- package/dist/modules/deposit.d.ts +31 -0
- package/dist/modules/deposit.js +57 -0
- package/dist/modules/marketData.d.ts +8 -0
- package/dist/modules/marketData.js +113 -0
- package/dist/modules/withdraw.d.ts +31 -0
- package/dist/modules/withdraw.js +60 -0
- package/dist/modules/withdrawExecutor.d.ts +47 -0
- package/dist/modules/withdrawExecutor.js +208 -0
- package/dist/policyAdapter.d.ts +11 -0
- package/dist/policyAdapter.js +38 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +1 -0
- package/dist/ui/DepositModal.d.ts +36 -0
- package/dist/ui/DepositModal.js +326 -0
- package/dist/ui/SignInModal.d.ts +22 -0
- package/dist/ui/SignInModal.js +74 -0
- package/dist/ui/SignInModal.sections.d.ts +33 -0
- package/dist/ui/SignInModal.sections.js +45 -0
- package/dist/ui/SignInModal.shared.d.ts +15 -0
- package/dist/ui/SignInModal.shared.js +87 -0
- package/dist/ui/WalletSelectionModal.d.ts +14 -0
- package/dist/ui/WalletSelectionModal.js +54 -0
- package/dist/ui/WithdrawModal.d.ts +47 -0
- package/dist/ui/WithdrawModal.js +528 -0
- package/dist/ui/components/CloseButton.d.ts +4 -0
- package/dist/ui/components/CloseButton.js +15 -0
- package/dist/ui/components/Countdown.d.ts +16 -0
- package/dist/ui/components/Countdown.js +42 -0
- package/dist/ui/components/DepositDetailsPanel.d.ts +8 -0
- package/dist/ui/components/DepositDetailsPanel.js +117 -0
- package/dist/ui/components/DropdownField.d.ts +19 -0
- package/dist/ui/components/DropdownField.js +81 -0
- package/dist/ui/components/Field.d.ts +10 -0
- package/dist/ui/components/Field.js +21 -0
- package/dist/ui/components/LoginRequiredOverlay.d.ts +6 -0
- package/dist/ui/components/LoginRequiredOverlay.js +31 -0
- package/dist/ui/components/ModalCard.d.ts +9 -0
- package/dist/ui/components/ModalCard.js +14 -0
- package/dist/ui/components/ModalFrame.d.ts +9 -0
- package/dist/ui/components/ModalFrame.js +18 -0
- package/dist/ui/components/PrimaryButton.d.ts +2 -0
- package/dist/ui/components/PrimaryButton.js +14 -0
- package/dist/ui/components/QRCodePanel.d.ts +4 -0
- package/dist/ui/components/QRCodePanel.js +43 -0
- package/dist/ui/components/Select.d.ts +12 -0
- package/dist/ui/components/Select.js +29 -0
- package/dist/ui/components/StepIndicator.d.ts +7 -0
- package/dist/ui/components/StepIndicator.js +35 -0
- package/dist/ui/components/Success.d.ts +1 -0
- package/dist/ui/components/Success.js +4 -0
- package/dist/ui/components/Toast.d.ts +8 -0
- package/dist/ui/components/Toast.js +51 -0
- package/dist/ui/hooks/useSession.d.ts +2 -0
- package/dist/ui/hooks/useSession.js +10 -0
- package/dist/ui/signInTypes.d.ts +25 -0
- package/dist/ui/signInTypes.js +1 -0
- package/dist/ui/theme.d.ts +31 -0
- package/dist/ui/theme.js +31 -0
- package/dist/ui/useSignInModalController.d.ts +25 -0
- package/dist/ui/useSignInModalController.js +173 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/env.js +61 -0
- package/dist/utils/explorer.d.ts +3 -0
- package/dist/utils/explorer.js +47 -0
- package/dist/walletUtils.d.ts +3 -0
- package/dist/walletUtils.js +3 -0
- package/package.json +41 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { sessionStore } from "@ab-org/sdk-core";
|
|
2
|
+
import { getEnv } from "../utils/env.js";
|
|
3
|
+
const DEFAULT_MERCHANT_BASE_URL = "https://merchant.tomo.services";
|
|
4
|
+
let cachedChains = null;
|
|
5
|
+
/** Fallback when chains API fails (network error, 4xx/5xx, parse error, missing base URL). */
|
|
6
|
+
const MOCK_CHAINS_FALLBACK = [
|
|
7
|
+
{
|
|
8
|
+
chain_id: "56",
|
|
9
|
+
network: "BSC",
|
|
10
|
+
tokens: [
|
|
11
|
+
{ symbol: "USD1", address: "0x", decimals: 18 },
|
|
12
|
+
{ symbol: "USDT", address: "0x", decimals: 6 },
|
|
13
|
+
{ symbol: "USDC", address: "0x", decimals: 6 },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
chain_id: "ETH",
|
|
18
|
+
network: "Ethereum",
|
|
19
|
+
tokens: [
|
|
20
|
+
{ symbol: "USDT", address: "0x", decimals: 6 },
|
|
21
|
+
{ symbol: "USDC", address: "0x", decimals: 6 },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
async function fetchChainsFromApi() {
|
|
26
|
+
if (cachedChains)
|
|
27
|
+
return cachedChains;
|
|
28
|
+
try {
|
|
29
|
+
const CHAINS_API_BASE = getEnv("MERCHANT_BASE_URL") || DEFAULT_MERCHANT_BASE_URL;
|
|
30
|
+
const res = await fetch(`${CHAINS_API_BASE}/chains`);
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
throw new Error(`Chains API error: ${res.status} ${res.statusText}`);
|
|
33
|
+
const json = (await res.json());
|
|
34
|
+
const chains = json.data?.chains ?? [];
|
|
35
|
+
if (chains.length > 0) {
|
|
36
|
+
cachedChains = chains;
|
|
37
|
+
return chains;
|
|
38
|
+
}
|
|
39
|
+
return MOCK_CHAINS_FALLBACK;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return MOCK_CHAINS_FALLBACK;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function chainToChainInfo(chain) {
|
|
46
|
+
return {
|
|
47
|
+
id: chain.chain_id,
|
|
48
|
+
name: chain.network,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function deriveTokensFromChains(chains) {
|
|
52
|
+
const bySymbol = new Map();
|
|
53
|
+
for (const chain of chains) {
|
|
54
|
+
for (const t of chain.tokens) {
|
|
55
|
+
if (!bySymbol.has(t.symbol)) {
|
|
56
|
+
bySymbol.set(t.symbol, {
|
|
57
|
+
symbol: t.symbol,
|
|
58
|
+
name: t.symbol,
|
|
59
|
+
decimals: t.decimals,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Array.from(bySymbol.values());
|
|
65
|
+
}
|
|
66
|
+
function computeMockQuote(request) {
|
|
67
|
+
const isStable = ["USDT", "USDC", "USD1"].includes(request.token);
|
|
68
|
+
const slippage = isStable ? "0.3" : "1.0";
|
|
69
|
+
const feeRate = request.direction === "withdraw" ? 0.001 : 0;
|
|
70
|
+
const amount = Number(request.amount) || 0;
|
|
71
|
+
const fee = (amount * feeRate).toFixed(2);
|
|
72
|
+
const estimatedAmount = (amount - Number(fee)).toFixed(6);
|
|
73
|
+
return {
|
|
74
|
+
quoteId: `quote-${Date.now()}`,
|
|
75
|
+
estimatedAmount: amount > 0 ? estimatedAmount : "0",
|
|
76
|
+
slippage,
|
|
77
|
+
fee,
|
|
78
|
+
feeToken: request.token,
|
|
79
|
+
exchangeRate: "1.0",
|
|
80
|
+
expiresAt: Date.now() + 30000,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Market data provider backed by merchant API.
|
|
85
|
+
* - getSupportedTokens / getSupportedChains: from GET https://merchant.tomo.services/chains
|
|
86
|
+
* - getQuote: mock (no quote API yet)
|
|
87
|
+
* - getDepositAddress: from session (no deposit-address API in this service)
|
|
88
|
+
*/
|
|
89
|
+
export function createMockMarketDataProvider() {
|
|
90
|
+
return {
|
|
91
|
+
async getSupportedTokens(_direction) {
|
|
92
|
+
const chains = await fetchChainsFromApi();
|
|
93
|
+
const tokens = deriveTokensFromChains(chains);
|
|
94
|
+
return tokens.length > 0 ? tokens : [];
|
|
95
|
+
},
|
|
96
|
+
async getSupportedChains(token, _direction) {
|
|
97
|
+
const chains = await fetchChainsFromApi();
|
|
98
|
+
const forToken = chains.filter((c) => c.tokens.some((t) => t.symbol === token));
|
|
99
|
+
const list = forToken.length > 0 ? forToken : chains;
|
|
100
|
+
return list.map(chainToChainInfo);
|
|
101
|
+
},
|
|
102
|
+
async getQuote(request) {
|
|
103
|
+
return computeMockQuote(request);
|
|
104
|
+
},
|
|
105
|
+
async getDepositAddress(_token, _chain) {
|
|
106
|
+
const session = sessionStore.getState().session;
|
|
107
|
+
return {
|
|
108
|
+
address: session?.address ?? "",
|
|
109
|
+
minimumDeposit: "15.00",
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChainInfo, CustodyAdapter, MarketDataProvider, ModalController, QuoteResult, TokenInfo } from "../types.js";
|
|
2
|
+
export interface WithdrawModalConfig {
|
|
3
|
+
defaultAmount?: string;
|
|
4
|
+
defaultToken?: string;
|
|
5
|
+
defaultChain?: string;
|
|
6
|
+
targetAddress?: string;
|
|
7
|
+
onStatusChange?: (status: WithdrawStatus) => void;
|
|
8
|
+
}
|
|
9
|
+
export type WithdrawStatus = {
|
|
10
|
+
phase: "idle";
|
|
11
|
+
} | {
|
|
12
|
+
phase: "requested";
|
|
13
|
+
requestId: string;
|
|
14
|
+
} | {
|
|
15
|
+
phase: "processing";
|
|
16
|
+
requestId: string;
|
|
17
|
+
} | {
|
|
18
|
+
phase: "settled";
|
|
19
|
+
requestId: string;
|
|
20
|
+
txHash?: string;
|
|
21
|
+
} | {
|
|
22
|
+
phase: "failed";
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
export interface WithdrawController extends ModalController<WithdrawModalConfig> {
|
|
26
|
+
readonly status: WithdrawStatus;
|
|
27
|
+
fetchTokens(): Promise<TokenInfo[]>;
|
|
28
|
+
fetchChains(token: string): Promise<ChainInfo[]>;
|
|
29
|
+
fetchQuote(token: string, chain: string, amount: string): Promise<QuoteResult>;
|
|
30
|
+
}
|
|
31
|
+
export declare const createWithdrawController: (custody: CustodyAdapter, marketData: MarketDataProvider) => WithdrawController;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sessionStore } from "@ab-org/sdk-core";
|
|
2
|
+
function requireSession() {
|
|
3
|
+
const session = sessionStore.getState().session;
|
|
4
|
+
if (!session)
|
|
5
|
+
throw new Error("Login required");
|
|
6
|
+
return session;
|
|
7
|
+
}
|
|
8
|
+
export const createWithdrawController = (custody, marketData) => {
|
|
9
|
+
let status = { phase: "idle" };
|
|
10
|
+
const update = (next, cb) => {
|
|
11
|
+
status = next;
|
|
12
|
+
cb?.(status);
|
|
13
|
+
};
|
|
14
|
+
const open = async (config) => {
|
|
15
|
+
const session = requireSession();
|
|
16
|
+
const token = config?.defaultToken ?? "USD1";
|
|
17
|
+
const chain = config?.defaultChain ??
|
|
18
|
+
session.chainContext?.settlementChain ??
|
|
19
|
+
session.chain ??
|
|
20
|
+
"AB_CORE";
|
|
21
|
+
const targetAddress = config?.targetAddress;
|
|
22
|
+
if (!targetAddress)
|
|
23
|
+
throw new Error("targetAddress required for withdraw");
|
|
24
|
+
try {
|
|
25
|
+
const { requestId } = await custody.requestWithdraw({
|
|
26
|
+
amount: config?.defaultAmount ?? "0",
|
|
27
|
+
token,
|
|
28
|
+
chain,
|
|
29
|
+
targetAddress,
|
|
30
|
+
});
|
|
31
|
+
update({ phase: "requested", requestId }, config?.onStatusChange);
|
|
32
|
+
update({ phase: "processing", requestId }, config?.onStatusChange);
|
|
33
|
+
const { status: finalStatus, txHash } = await custody.getWithdrawStatus(requestId);
|
|
34
|
+
update(finalStatus === "SETTLED"
|
|
35
|
+
? { phase: "settled", requestId, txHash }
|
|
36
|
+
: { phase: "failed", reason: finalStatus }, config?.onStatusChange);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
update({ phase: "failed", reason: error.message }, config?.onStatusChange);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
get status() {
|
|
44
|
+
return status;
|
|
45
|
+
},
|
|
46
|
+
open,
|
|
47
|
+
fetchTokens() {
|
|
48
|
+
requireSession();
|
|
49
|
+
return marketData.getSupportedTokens("withdraw");
|
|
50
|
+
},
|
|
51
|
+
fetchChains(token) {
|
|
52
|
+
requireSession();
|
|
53
|
+
return marketData.getSupportedChains(token, "withdraw");
|
|
54
|
+
},
|
|
55
|
+
fetchQuote(token, chain, amount) {
|
|
56
|
+
requireSession();
|
|
57
|
+
return marketData.getQuote({ token, chain, amount, direction: "withdraw" });
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface WithdrawRequest {
|
|
2
|
+
/** 用户收款地址(目标链) */
|
|
3
|
+
toAddress: string;
|
|
4
|
+
/** Human-readable amount, e.g. "100.5" */
|
|
5
|
+
amount: string;
|
|
6
|
+
/** 目标链代币 symbol,如 "USDT" */
|
|
7
|
+
token: string;
|
|
8
|
+
/** 目标链 chain_id */
|
|
9
|
+
chain: string;
|
|
10
|
+
}
|
|
11
|
+
export interface WithdrawResult {
|
|
12
|
+
/** BSC 上转入一次性地址的 funding tx hash */
|
|
13
|
+
txHash: string;
|
|
14
|
+
/** 提现订单 ID,用于轮询 getWithdrawOrder(orderId) */
|
|
15
|
+
orderId: string;
|
|
16
|
+
/** 广播 funding tx 的链 id,用于构建 explorer 链接 */
|
|
17
|
+
fundingChainId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A function that executes a withdraw operation.
|
|
21
|
+
* Callers can supply their own implementation to override the default behaviour.
|
|
22
|
+
*/
|
|
23
|
+
export type WithdrawExecutor = (request: WithdrawRequest) => Promise<WithdrawResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Convert a human-readable decimal string to the smallest unit (wei-equivalent).
|
|
26
|
+
*
|
|
27
|
+
* Examples with decimals=18:
|
|
28
|
+
* "1" → 1000000000000000000n
|
|
29
|
+
* "0.5" → 500000000000000000n
|
|
30
|
+
* "100.1" → 100100000000000000000n
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseUnits(value: string, decimals: number): bigint;
|
|
33
|
+
export interface BscUsd1WithdrawExecutorOptions {
|
|
34
|
+
/** 源代币 USD1 合约地址(提现场景下为 BSC 上的 USD1) */
|
|
35
|
+
tokenAddress?: string;
|
|
36
|
+
decimals?: number;
|
|
37
|
+
chainId?: number;
|
|
38
|
+
/** 系统配置的单笔限额(wei 字符串),若提供则校验 request.amount 不得超过此值 */
|
|
39
|
+
maxAmountWei?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Factory that returns a `WithdrawExecutor` implementing the flow in withdraw.md:
|
|
43
|
+
* 1) Create NATIVE_SWAP order → get one-time wallet address (OTW);
|
|
44
|
+
* 2) Sign ERC-20 transfer of USD1 on BSC to the OTW (not to user);
|
|
45
|
+
* 3) Broadcast tx and return { txHash, orderId } for the client to poll getWithdrawOrder(orderId).
|
|
46
|
+
*/
|
|
47
|
+
export declare function createBscUsd1WithdrawExecutor(options?: BscUsd1WithdrawExecutorOptions): WithdrawExecutor;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { sessionStore } from "@ab-org/sdk-core";
|
|
2
|
+
import { tryAutoReconnect } from "../auth/autoReconnect.js";
|
|
3
|
+
import { getChains, createOrder } from "./api.js";
|
|
4
|
+
import { fromHex, parseGwei, toHex } from "viem";
|
|
5
|
+
import { testBsc, BSC_USD1_ADDRESS } from "../constants/chains.js";
|
|
6
|
+
const MAX_WITHDRAW_GAS_LIMIT = 500000n;
|
|
7
|
+
const BSC_RPC_URL = testBsc.rpcUrls[0];
|
|
8
|
+
/* ─── Internal helpers ────────────────────────── */
|
|
9
|
+
const ERC20_TRANSFER_SELECTOR = "0xa9059cbb";
|
|
10
|
+
const USD1_DECIMALS = testBsc.nativeCurrencyDecimals;
|
|
11
|
+
function padHex256(value) {
|
|
12
|
+
return value.toString(16).padStart(64, "0");
|
|
13
|
+
}
|
|
14
|
+
function padAddress(address) {
|
|
15
|
+
return address.toLowerCase().replace("0x", "").padStart(64, "0");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert a human-readable decimal string to the smallest unit (wei-equivalent).
|
|
19
|
+
*
|
|
20
|
+
* Examples with decimals=18:
|
|
21
|
+
* "1" → 1000000000000000000n
|
|
22
|
+
* "0.5" → 500000000000000000n
|
|
23
|
+
* "100.1" → 100100000000000000000n
|
|
24
|
+
*/
|
|
25
|
+
export function parseUnits(value, decimals) {
|
|
26
|
+
if (!value || value === "0")
|
|
27
|
+
return 0n;
|
|
28
|
+
const [intPart = "0", fracPart = ""] = value.split(".");
|
|
29
|
+
const padded = fracPart.padEnd(decimals, "0").slice(0, decimals);
|
|
30
|
+
return BigInt(intPart) * 10n ** BigInt(decimals) + BigInt(padded);
|
|
31
|
+
}
|
|
32
|
+
function encodeTransferData(to, amountWei) {
|
|
33
|
+
return `${ERC20_TRANSFER_SELECTOR}${padAddress(to)}${padHex256(amountWei)}`;
|
|
34
|
+
}
|
|
35
|
+
function isUnsupportedMethodError(error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
return /unsupported rpc method|unsupported method|method not found|does not support/i.test(message);
|
|
38
|
+
}
|
|
39
|
+
function toHexQuantity(value) {
|
|
40
|
+
if (typeof value === "string") {
|
|
41
|
+
if (/^0x[0-9a-fA-F]+$/.test(value)) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (/^\d+$/.test(value)) {
|
|
45
|
+
return toHex(BigInt(value));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "number") {
|
|
49
|
+
return toHex(BigInt(value));
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === "bigint") {
|
|
52
|
+
return toHex(value);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Invalid EVM quantity: ${String(value)}`);
|
|
55
|
+
}
|
|
56
|
+
async function callRpc(rpcUrl, method, params) {
|
|
57
|
+
const response = await fetch(rpcUrl, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
jsonrpc: "2.0",
|
|
62
|
+
id: Date.now(),
|
|
63
|
+
method,
|
|
64
|
+
params,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
const json = (await response.json());
|
|
68
|
+
if (!response.ok || json.error) {
|
|
69
|
+
throw new Error(json.error?.message ?? `${method} failed`);
|
|
70
|
+
}
|
|
71
|
+
return json.result;
|
|
72
|
+
}
|
|
73
|
+
async function requestHexQuantity(provider, rpcUrl, method, params) {
|
|
74
|
+
try {
|
|
75
|
+
const result = await provider.request({ method, params });
|
|
76
|
+
return toHexQuantity(result);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!isUnsupportedMethodError(error)) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return toHexQuantity(await callRpc(rpcUrl, method, params));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function ensureBscChain(provider, chainId) {
|
|
86
|
+
const hex = `0x${chainId.toString(16)}`;
|
|
87
|
+
try {
|
|
88
|
+
await provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: hex }] });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Ignored – the wallet may already be on BSC or may not support switching.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function getDstTokenAddress(chains, chainId, tokenSymbol) {
|
|
95
|
+
const chain = chains.find((c) => c.chain_id === chainId);
|
|
96
|
+
return chain?.tokens.find((t) => t.symbol === tokenSymbol)?.address;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Factory that returns a `WithdrawExecutor` implementing the flow in withdraw.md:
|
|
100
|
+
* 1) Create NATIVE_SWAP order → get one-time wallet address (OTW);
|
|
101
|
+
* 2) Sign ERC-20 transfer of USD1 on BSC to the OTW (not to user);
|
|
102
|
+
* 3) Broadcast tx and return { txHash, orderId } for the client to poll getWithdrawOrder(orderId).
|
|
103
|
+
*/
|
|
104
|
+
export function createBscUsd1WithdrawExecutor(options) {
|
|
105
|
+
const tokenAddress = options?.tokenAddress ?? BSC_USD1_ADDRESS;
|
|
106
|
+
const decimals = options?.decimals ?? USD1_DECIMALS;
|
|
107
|
+
const maxAmountWei = options?.maxAmountWei;
|
|
108
|
+
const chainId = options?.chainId ?? Number(testBsc.chainId);
|
|
109
|
+
return async (request) => {
|
|
110
|
+
const amountWei = parseUnits(request.amount, decimals);
|
|
111
|
+
const amountWeiStr = amountWei.toString();
|
|
112
|
+
if (maxAmountWei != null && amountWei > BigInt(maxAmountWei)) {
|
|
113
|
+
throw new Error("Withdraw amount exceeds the single-transaction limit");
|
|
114
|
+
}
|
|
115
|
+
let session = sessionStore.getState().session;
|
|
116
|
+
if (!session)
|
|
117
|
+
throw new Error("Login required");
|
|
118
|
+
let { provider } = session;
|
|
119
|
+
try {
|
|
120
|
+
await provider.request({ method: "eth_chainId", params: [] });
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const msg = err.message ?? "";
|
|
124
|
+
if (msg.includes("restored from cache") || msg.includes("Reconnect your wallet")) {
|
|
125
|
+
const reconnected = await tryAutoReconnect();
|
|
126
|
+
if (reconnected) {
|
|
127
|
+
session = reconnected;
|
|
128
|
+
provider = reconnected.provider;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
sessionStore.clearSession();
|
|
132
|
+
throw new Error("Session expired. Please sign in again.");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const chainsRes = await getChains();
|
|
137
|
+
const chains = chainsRes?.chains ?? [];
|
|
138
|
+
const dstTokenAddress = getDstTokenAddress(chains, request.chain, request.token);
|
|
139
|
+
if (!dstTokenAddress) {
|
|
140
|
+
throw new Error(`Unsupported token ${request.token} on chain ${request.chain}`);
|
|
141
|
+
}
|
|
142
|
+
// 校验规则:源代币或目标代币中必须有一个是 USD1。提现场景下源代币(token_address)为 USD1;token_amount 已按 maxAmountWei 做单笔限额校验
|
|
143
|
+
const sourceTokenSymbol = "USD1";
|
|
144
|
+
const orderRes = await createOrder({
|
|
145
|
+
intent_id: `withdraw-${Date.now()}`,
|
|
146
|
+
order_type: "NATIVE_SWAP",
|
|
147
|
+
order_payload: {
|
|
148
|
+
chain_id: String(chainId),
|
|
149
|
+
token_address: tokenAddress,
|
|
150
|
+
token_amount: amountWeiStr,
|
|
151
|
+
dst_chain_id: request.chain,
|
|
152
|
+
dst_token_address: dstTokenAddress,
|
|
153
|
+
recipient: request.toAddress,
|
|
154
|
+
},
|
|
155
|
+
payment_pairs: [
|
|
156
|
+
{
|
|
157
|
+
token_symbol: sourceTokenSymbol,
|
|
158
|
+
token_amount: amountWeiStr,
|
|
159
|
+
token_address: tokenAddress,
|
|
160
|
+
user_address: session.address,
|
|
161
|
+
chain_id: String(chainId),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
const oneTimeAddress = orderRes.payment_sessions?.[0]?.one_time_wallet_address;
|
|
166
|
+
if (!oneTimeAddress) {
|
|
167
|
+
throw new Error("Order created but no one-time wallet address returned");
|
|
168
|
+
}
|
|
169
|
+
await ensureBscChain(provider, chainId);
|
|
170
|
+
const data = encodeTransferData(oneTimeAddress, amountWei);
|
|
171
|
+
const nonce = await requestHexQuantity(provider, BSC_RPC_URL, "eth_getTransactionCount", [session.address, "latest"]);
|
|
172
|
+
const tx = {
|
|
173
|
+
from: session.address,
|
|
174
|
+
to: tokenAddress,
|
|
175
|
+
value: "0x0",
|
|
176
|
+
nonce,
|
|
177
|
+
data,
|
|
178
|
+
chainId: toHex(chainId),
|
|
179
|
+
};
|
|
180
|
+
const estimatedGasHex = await requestHexQuantity(provider, BSC_RPC_URL, "eth_estimateGas", [tx]);
|
|
181
|
+
const estimatedGas = fromHex(estimatedGasHex, "bigint");
|
|
182
|
+
const gas = toHex(estimatedGas > MAX_WITHDRAW_GAS_LIMIT ? MAX_WITHDRAW_GAS_LIMIT : estimatedGas);
|
|
183
|
+
const transaction = {
|
|
184
|
+
...tx,
|
|
185
|
+
gas,
|
|
186
|
+
maxFeePerGas: toHex(parseGwei("5")),
|
|
187
|
+
maxPriorityFeePerGas: toHex(parseGwei("1")),
|
|
188
|
+
};
|
|
189
|
+
let txHash;
|
|
190
|
+
try {
|
|
191
|
+
txHash = await provider.request({
|
|
192
|
+
method: "eth_sendTransaction",
|
|
193
|
+
params: [transaction],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (!isUnsupportedMethodError(error)) {
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
const signedTx = await provider.request({
|
|
201
|
+
method: "eth_signTransaction",
|
|
202
|
+
params: [transaction],
|
|
203
|
+
});
|
|
204
|
+
txHash = await callRpc(BSC_RPC_URL, "eth_sendRawTransaction", [signedTx]);
|
|
205
|
+
}
|
|
206
|
+
return { txHash, orderId: orderRes.order_id, fundingChainId: String(chainId) };
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SessionCapabilityPolicy, type SupportedChain, type SupportedToken, type WalletCapability } from "@ab-org/sdk-core";
|
|
2
|
+
export interface PredicateMarketPolicyAdapterOptions {
|
|
3
|
+
appId?: string;
|
|
4
|
+
origin?: string;
|
|
5
|
+
expiresAt?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const createPredicateMarketPolicyAdapter: (options?: PredicateMarketPolicyAdapterOptions) => {
|
|
8
|
+
deposit(token: SupportedToken, chain: SupportedChain, maxAmount?: string): SessionCapabilityPolicy;
|
|
9
|
+
withdraw(token: SupportedToken, chain: SupportedChain, maxAmount?: string): SessionCapabilityPolicy;
|
|
10
|
+
trade(chain: SupportedChain, capabilities?: WalletCapability[]): SessionCapabilityPolicy;
|
|
11
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createSessionCapabilityPolicy, } from "@ab-org/sdk-core";
|
|
2
|
+
const createPolicy = (overrides, options) => createSessionCapabilityPolicy({
|
|
3
|
+
appId: options?.appId,
|
|
4
|
+
origin: options?.origin,
|
|
5
|
+
expiresAt: options?.expiresAt,
|
|
6
|
+
...overrides,
|
|
7
|
+
});
|
|
8
|
+
const withActionMetadata = (action, policy) => ({
|
|
9
|
+
...policy,
|
|
10
|
+
metadata: {
|
|
11
|
+
...(policy.metadata ?? {}),
|
|
12
|
+
action,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
export const createPredicateMarketPolicyAdapter = (options) => ({
|
|
16
|
+
deposit(token, chain, maxAmount) {
|
|
17
|
+
return withActionMetadata("deposit", createPolicy({
|
|
18
|
+
methods: ["eth_sendTransaction"],
|
|
19
|
+
chains: [chain],
|
|
20
|
+
tokens: [token],
|
|
21
|
+
maxAmount,
|
|
22
|
+
}, options));
|
|
23
|
+
},
|
|
24
|
+
withdraw(token, chain, maxAmount) {
|
|
25
|
+
return withActionMetadata("withdraw", createPolicy({
|
|
26
|
+
methods: ["eth_sendTransaction"],
|
|
27
|
+
chains: [chain],
|
|
28
|
+
tokens: [token],
|
|
29
|
+
maxAmount,
|
|
30
|
+
}, options));
|
|
31
|
+
},
|
|
32
|
+
trade(chain, capabilities = ["eth_sendTransaction"]) {
|
|
33
|
+
return withActionMetadata("trade", createPolicy({
|
|
34
|
+
methods: capabilities,
|
|
35
|
+
chains: [chain],
|
|
36
|
+
}, options));
|
|
37
|
+
},
|
|
38
|
+
});
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface CustodyAdapter {
|
|
2
|
+
getDepositStatus(depositId: string): Promise<{
|
|
3
|
+
status: string;
|
|
4
|
+
txHash?: string;
|
|
5
|
+
}>;
|
|
6
|
+
requestWithdraw(params: {
|
|
7
|
+
amount: string;
|
|
8
|
+
token: string;
|
|
9
|
+
chain: string;
|
|
10
|
+
targetAddress: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
requestId: string;
|
|
13
|
+
}>;
|
|
14
|
+
getWithdrawStatus(requestId: string): Promise<{
|
|
15
|
+
status: string;
|
|
16
|
+
txHash?: string;
|
|
17
|
+
etaSeconds?: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
export interface ModalController<TConfig> {
|
|
21
|
+
open(config?: TConfig): void;
|
|
22
|
+
}
|
|
23
|
+
export interface TokenInfo {
|
|
24
|
+
symbol: string;
|
|
25
|
+
name: string;
|
|
26
|
+
decimals: number;
|
|
27
|
+
iconUrl?: string;
|
|
28
|
+
minAmount?: string;
|
|
29
|
+
maxAmount?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ChainInfo {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
iconUrl?: string;
|
|
35
|
+
confirmations?: number;
|
|
36
|
+
estimatedTime?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface QuoteRequest {
|
|
39
|
+
token: string;
|
|
40
|
+
chain: string;
|
|
41
|
+
amount: string;
|
|
42
|
+
direction: "deposit" | "withdraw";
|
|
43
|
+
}
|
|
44
|
+
export interface QuoteResult {
|
|
45
|
+
quoteId: string;
|
|
46
|
+
estimatedAmount: string;
|
|
47
|
+
slippage: string;
|
|
48
|
+
fee: string;
|
|
49
|
+
feeToken: string;
|
|
50
|
+
exchangeRate: string;
|
|
51
|
+
expiresAt: number;
|
|
52
|
+
}
|
|
53
|
+
export interface DepositAddressResult {
|
|
54
|
+
address: string;
|
|
55
|
+
minimumDeposit?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface MarketDataProvider {
|
|
58
|
+
getSupportedTokens(direction: "deposit" | "withdraw"): Promise<TokenInfo[]>;
|
|
59
|
+
getSupportedChains(token: string, direction: "deposit" | "withdraw"): Promise<ChainInfo[]>;
|
|
60
|
+
getQuote(request: QuoteRequest): Promise<QuoteResult>;
|
|
61
|
+
getDepositAddress(token: string, chain: string): Promise<DepositAddressResult>;
|
|
62
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type SelectOption } from "./components/DropdownField.js";
|
|
3
|
+
export interface DepositModalProps {
|
|
4
|
+
/** Pre-selected token id. */
|
|
5
|
+
token?: string;
|
|
6
|
+
/** Pre-selected chain id. */
|
|
7
|
+
chain?: string;
|
|
8
|
+
/** Available tokens (when not provided, fetched from getChains). */
|
|
9
|
+
tokenOptions?: SelectOption[];
|
|
10
|
+
/** Available chains (when not provided, derived from getChains by token). */
|
|
11
|
+
chainOptions?: SelectOption[];
|
|
12
|
+
/** Deposit address,必须由调用方传入;在已选 token+chain 时若未传入合法值将抛错。 */
|
|
13
|
+
depositAddress?: string;
|
|
14
|
+
/** e.g. "0.01 USDT" */
|
|
15
|
+
minimumDeposit?: string;
|
|
16
|
+
/** Icon rendered at the center of the QR code. */
|
|
17
|
+
qrCenterIcon?: ReactNode;
|
|
18
|
+
/** Extra icons shown on the Transfer Crypto row. */
|
|
19
|
+
cryptoIcons?: ReactNode;
|
|
20
|
+
/** Deposit amount for quote (optional; when set, quote is fetched and shown). */
|
|
21
|
+
depositAmount?: string;
|
|
22
|
+
/** Called for toast messages (copy, quote expired, wait for balance, transfer confirmed). */
|
|
23
|
+
onShowToast?: (message: string) => void;
|
|
24
|
+
/** When set, show "Transfer confirmed" toast and explorer link. */
|
|
25
|
+
txHash?: string;
|
|
26
|
+
/** Build explorer URL for tx; e.g. (chainId, txHash) => `https://bscscan.com/tx/${txHash}` */
|
|
27
|
+
explorerTxUrl?: (chainId: string, txHash: string) => string;
|
|
28
|
+
onTokenSelect?: (id: string) => void;
|
|
29
|
+
onChainSelect?: (id: string) => void;
|
|
30
|
+
onCopyAddress?: () => void;
|
|
31
|
+
onBuyCrypto?: () => void;
|
|
32
|
+
onSignIn?: () => void;
|
|
33
|
+
onBack?: () => void;
|
|
34
|
+
onClose?: () => void;
|
|
35
|
+
}
|
|
36
|
+
export declare const DepositModal: ({ token, chain, tokenOptions: tokenOptionsProp, chainOptions: chainOptionsProp, depositAddress, minimumDeposit, qrCenterIcon, cryptoIcons, depositAmount, onShowToast, txHash, explorerTxUrl, onTokenSelect, onChainSelect, onCopyAddress, onBuyCrypto, onSignIn, onBack, onClose, }: DepositModalProps) => import("react/jsx-runtime.js").JSX.Element;
|