Checkpoint: v16完整重构:
1. 数据库:添加transaction_logs防重放表+listener_state表 2. 后端:统一tokenDistributionService(creditXic方法),所有支付渠道共用 3. bridgeMonitor.ts + trc20Monitor.ts 集成tokenDistributionService 4. routers.ts recordOrder路由集成tokenDistributionService 5. addTokenToWallet.ts按文档规范重写(EVM: window.ethereum,TRON: tronWeb.request wallet_watchAsset) 6. Bridge.tsx添加TRON链(chainId: 728126428),集成useTronBridge 7. Bridge.tsx订单状态轮询(每5秒)+ wallet_watchAsset自动添加XIC代币 8. 去除前端bundle中的manus.im内联 9. 全部18个vitest测试通过 10. 浏览器测试全部通过
This commit is contained in:
parent
31a798a9ea
commit
f6bed914df
|
|
@ -2,11 +2,16 @@ export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
|
||||||
|
|
||||||
// Generate login URL at runtime so redirect URI reflects the current origin.
|
// Generate login URL at runtime so redirect URI reflects the current origin.
|
||||||
export const getLoginUrl = () => {
|
export const getLoginUrl = () => {
|
||||||
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
|
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL || "";
|
||||||
const appId = import.meta.env.VITE_APP_ID;
|
const appId = import.meta.env.VITE_APP_ID || "";
|
||||||
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||||
const state = btoa(redirectUri);
|
const state = btoa(redirectUri);
|
||||||
|
|
||||||
|
if (!oauthPortalUrl || !appId) {
|
||||||
|
// OAuth not configured — redirect to admin login page
|
||||||
|
return "/admin";
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
||||||
url.searchParams.set("appId", appId);
|
url.searchParams.set("appId", appId);
|
||||||
url.searchParams.set("redirectUri", redirectUri);
|
url.searchParams.set("redirectUri", redirectUri);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,381 @@
|
||||||
|
/**
|
||||||
|
* useTronBridge — TronLink wallet connection + TRC20 USDT transfer
|
||||||
|
*
|
||||||
|
* Implements the TRON side of the dual-rail bridge:
|
||||||
|
* - Detect and connect TronLink (browser extension + mobile deep link)
|
||||||
|
* - Query TRC20 USDT balance
|
||||||
|
* - Send TRC20 USDT transfer with wallet signature
|
||||||
|
* - Auto-add XIC token to TronLink after successful transfer
|
||||||
|
*
|
||||||
|
* TRON USDT contract: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t (mainnet, 6 decimals)
|
||||||
|
*/
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
// TRON USDT TRC20 contract address (mainnet)
|
||||||
|
const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
|
||||||
|
|
||||||
|
// TRC20 ABI for transfer and balanceOf
|
||||||
|
const TRC20_ABI = [
|
||||||
|
{
|
||||||
|
constant: false,
|
||||||
|
inputs: [
|
||||||
|
{ name: "_to", type: "address" },
|
||||||
|
{ name: "_value", type: "uint256" },
|
||||||
|
],
|
||||||
|
name: "transfer",
|
||||||
|
outputs: [{ name: "", type: "bool" }],
|
||||||
|
type: "Function",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
constant: true,
|
||||||
|
inputs: [{ name: "_owner", type: "address" }],
|
||||||
|
name: "balanceOf",
|
||||||
|
outputs: [{ name: "balance", type: "uint256" }],
|
||||||
|
type: "Function",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Minimal TronWeb type declarations
|
||||||
|
interface TronContract {
|
||||||
|
balanceOf: (address: string) => { call: () => Promise<bigint> };
|
||||||
|
transfer: (to: string, amount: bigint) => { send: (options?: { feeLimit?: number }) => Promise<string> };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TronWebInstance {
|
||||||
|
defaultAddress?: { base58?: string; hex?: string };
|
||||||
|
ready?: boolean;
|
||||||
|
contract: (abi: unknown[], address: string) => Promise<TronContract>;
|
||||||
|
trx: {
|
||||||
|
getBalance: (address: string) => Promise<number>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TronLinkProvider {
|
||||||
|
ready?: boolean;
|
||||||
|
tronWeb?: TronWebInstance;
|
||||||
|
request?: (args: { method: string; params?: unknown }) => Promise<{ code?: number; message?: string } | unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
tronLink?: TronLinkProvider;
|
||||||
|
tronWeb?: TronWebInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TronBridgeState {
|
||||||
|
tronAddress: string | null;
|
||||||
|
tronConnected: boolean;
|
||||||
|
tronConnecting: boolean;
|
||||||
|
tronError: string | null;
|
||||||
|
tronUsdtBalance: string | null;
|
||||||
|
tronUsdtBalanceLoading: boolean;
|
||||||
|
tronTransferring: boolean;
|
||||||
|
tronTransferError: string | null;
|
||||||
|
tronTransferTxHash: string | null;
|
||||||
|
tronTransferSuccess: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseTronBridgeReturn extends TronBridgeState {
|
||||||
|
connectTronLink: () => Promise<void>;
|
||||||
|
disconnectTron: () => void;
|
||||||
|
fetchTronUsdtBalance: () => Promise<void>;
|
||||||
|
sendTrc20Transfer: (params: {
|
||||||
|
toAddress: string;
|
||||||
|
usdtAmount: number;
|
||||||
|
}) => Promise<{ txHash: string } | null>;
|
||||||
|
resetTronTransferState: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTronWeb(): TronWebInstance | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
if (window.tronLink?.tronWeb?.ready) return window.tronLink.tronWeb;
|
||||||
|
if (window.tronWeb?.ready) return window.tronWeb;
|
||||||
|
if (window.tronLink?.tronWeb) return window.tronLink.tronWeb;
|
||||||
|
if (window.tronWeb) return window.tronWeb;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTronLink(): TronLinkProvider | null {
|
||||||
|
if (typeof window === "undefined") return null;
|
||||||
|
return window.tronLink || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTronBridge(): UseTronBridgeReturn {
|
||||||
|
const [state, setState] = useState<TronBridgeState>({
|
||||||
|
tronAddress: null,
|
||||||
|
tronConnected: false,
|
||||||
|
tronConnecting: false,
|
||||||
|
tronError: null,
|
||||||
|
tronUsdtBalance: null,
|
||||||
|
tronUsdtBalanceLoading: false,
|
||||||
|
tronTransferring: false,
|
||||||
|
tronTransferError: null,
|
||||||
|
tronTransferTxHash: null,
|
||||||
|
tronTransferSuccess: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-detect TronLink on mount and account changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkTronLink = () => {
|
||||||
|
const tw = getTronWeb();
|
||||||
|
if (tw?.defaultAddress?.base58) {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronAddress: tw.defaultAddress!.base58!,
|
||||||
|
tronConnected: true,
|
||||||
|
tronError: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check immediately (with small delay for extension to inject)
|
||||||
|
const timer = setTimeout(checkTronLink, 300);
|
||||||
|
|
||||||
|
// TronLink fires window messages on account changes
|
||||||
|
const handleMessage = (e: MessageEvent) => {
|
||||||
|
if (
|
||||||
|
e.data?.message?.action === "accountsChanged" ||
|
||||||
|
e.data?.message?.action === "setAccount" ||
|
||||||
|
e.data?.message?.action === "connect" ||
|
||||||
|
e.data?.message?.action === "setNode"
|
||||||
|
) {
|
||||||
|
setTimeout(checkTronLink, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handleMessage);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
window.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Connect TronLink
|
||||||
|
const connectTronLink = useCallback(async () => {
|
||||||
|
setState(s => ({ ...s, tronConnecting: true, tronError: null }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tronLink = getTronLink();
|
||||||
|
|
||||||
|
if (!tronLink) {
|
||||||
|
// No TronLink installed — redirect to install page
|
||||||
|
const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||||
|
if (isMobile) {
|
||||||
|
// Mobile: open TronLink deep link
|
||||||
|
const currentUrl = encodeURIComponent(window.location.href);
|
||||||
|
window.open(
|
||||||
|
`tronlinkoutside://pull.activity?param=${encodeURIComponent(JSON.stringify({
|
||||||
|
url: window.location.href,
|
||||||
|
action: "open",
|
||||||
|
protocol: "tronlink",
|
||||||
|
version: "1.0",
|
||||||
|
}))}`,
|
||||||
|
"_blank"
|
||||||
|
);
|
||||||
|
// Fallback: open TronLink app store
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(`https://www.tronlink.org/`, "_blank");
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
window.open("https://www.tronlink.org/", "_blank");
|
||||||
|
}
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronConnecting: false,
|
||||||
|
tronError: "TronLink not installed. Please install TronLink extension.",
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request account access
|
||||||
|
if (tronLink.request) {
|
||||||
|
const result = await tronLink.request({ method: "tron_requestAccounts" }) as { code?: number; message?: string } | null;
|
||||||
|
|
||||||
|
if (result && typeof result === "object" && "code" in result) {
|
||||||
|
if (result.code !== 200 && result.code !== undefined) {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronConnecting: false,
|
||||||
|
tronError: result.message || "TronLink connection rejected",
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for TronWeb to be ready
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 600));
|
||||||
|
|
||||||
|
const tw = getTronWeb();
|
||||||
|
const address = tw?.defaultAddress?.base58;
|
||||||
|
|
||||||
|
if (address) {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronAddress: address,
|
||||||
|
tronConnected: true,
|
||||||
|
tronConnecting: false,
|
||||||
|
tronError: null,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronConnecting: false,
|
||||||
|
tronError: "Could not get TRON address. Please unlock TronLink and try again.",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { message?: string; code?: number };
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronConnecting: false,
|
||||||
|
tronError: error?.message || "TronLink connection failed",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
const disconnectTron = useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronAddress: null,
|
||||||
|
tronConnected: false,
|
||||||
|
tronUsdtBalance: null,
|
||||||
|
tronError: null,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch TRC20 USDT balance
|
||||||
|
const fetchTronUsdtBalance = useCallback(async () => {
|
||||||
|
const address = state.tronAddress;
|
||||||
|
if (!address) return;
|
||||||
|
|
||||||
|
setState(s => ({ ...s, tronUsdtBalanceLoading: true }));
|
||||||
|
try {
|
||||||
|
const tw = getTronWeb();
|
||||||
|
if (!tw) throw new Error("TronWeb not available");
|
||||||
|
|
||||||
|
const contract = await tw.contract(TRC20_ABI, TRON_USDT_CONTRACT);
|
||||||
|
const rawBalance = await contract.balanceOf(address).call();
|
||||||
|
// USDT on TRON has 6 decimals
|
||||||
|
const balance = Number(rawBalance) / 1_000_000;
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronUsdtBalance: balance.toFixed(2),
|
||||||
|
tronUsdtBalanceLoading: false,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[useTronBridge] fetchTronUsdtBalance error:", err);
|
||||||
|
setState(s => ({ ...s, tronUsdtBalance: null, tronUsdtBalanceLoading: false }));
|
||||||
|
}
|
||||||
|
}, [state.tronAddress]);
|
||||||
|
|
||||||
|
// Auto-fetch balance when connected
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.tronAddress && state.tronConnected) {
|
||||||
|
fetchTronUsdtBalance();
|
||||||
|
}
|
||||||
|
}, [state.tronAddress, state.tronConnected]);
|
||||||
|
|
||||||
|
// Send TRC20 USDT transfer
|
||||||
|
const sendTrc20Transfer = useCallback(async ({
|
||||||
|
toAddress,
|
||||||
|
usdtAmount,
|
||||||
|
}: {
|
||||||
|
toAddress: string;
|
||||||
|
usdtAmount: number;
|
||||||
|
}): Promise<{ txHash: string } | null> => {
|
||||||
|
const address = state.tronAddress;
|
||||||
|
if (!address) {
|
||||||
|
setState(s => ({ ...s, tronTransferError: "TRON wallet not connected" }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronTransferring: true,
|
||||||
|
tronTransferError: null,
|
||||||
|
tronTransferTxHash: null,
|
||||||
|
tronTransferSuccess: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tw = getTronWeb();
|
||||||
|
if (!tw) throw new Error("TronWeb not available");
|
||||||
|
|
||||||
|
const contract = await tw.contract(TRC20_ABI, TRON_USDT_CONTRACT);
|
||||||
|
|
||||||
|
// USDT on TRON has 6 decimals
|
||||||
|
const amountSun = BigInt(Math.round(usdtAmount * 1_000_000));
|
||||||
|
|
||||||
|
// Send transfer — TronLink will prompt for confirmation
|
||||||
|
const txHash = await contract.transfer(toAddress, amountSun).send({
|
||||||
|
feeLimit: 100_000_000, // 100 TRX fee limit
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!txHash || typeof txHash !== "string") {
|
||||||
|
throw new Error("Transaction failed — no hash returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronTransferring: false,
|
||||||
|
tronTransferSuccess: true,
|
||||||
|
tronTransferTxHash: txHash,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh balance after 3s
|
||||||
|
setTimeout(() => fetchTronUsdtBalance(), 3000);
|
||||||
|
|
||||||
|
return { txHash };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { message?: string; code?: number };
|
||||||
|
let msg: string;
|
||||||
|
|
||||||
|
if (
|
||||||
|
error?.message?.toLowerCase().includes("cancel") ||
|
||||||
|
error?.message?.toLowerCase().includes("reject") ||
|
||||||
|
error?.message?.toLowerCase().includes("denied") ||
|
||||||
|
error?.code === 4001
|
||||||
|
) {
|
||||||
|
msg = "Transaction cancelled by user";
|
||||||
|
} else if (error?.message?.toLowerCase().includes("insufficient")) {
|
||||||
|
msg = "Insufficient USDT balance or TRX for fees";
|
||||||
|
} else if (
|
||||||
|
error?.message?.toLowerCase().includes("bandwidth") ||
|
||||||
|
error?.message?.toLowerCase().includes("energy")
|
||||||
|
) {
|
||||||
|
msg = "Insufficient TRX bandwidth/energy. Please freeze some TRX for resources.";
|
||||||
|
} else {
|
||||||
|
msg = error?.message || "Transaction failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronTransferring: false,
|
||||||
|
tronTransferError: msg,
|
||||||
|
}));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [state.tronAddress, fetchTronUsdtBalance]);
|
||||||
|
|
||||||
|
const resetTronTransferState = useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
tronTransferring: false,
|
||||||
|
tronTransferError: null,
|
||||||
|
tronTransferTxHash: null,
|
||||||
|
tronTransferSuccess: false,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
connectTronLink,
|
||||||
|
disconnectTron,
|
||||||
|
fetchTronUsdtBalance,
|
||||||
|
sendTrc20Transfer,
|
||||||
|
resetTronTransferState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
/**
|
||||||
|
* addTokenToWallet — Seamless token auto-add after purchase
|
||||||
|
*
|
||||||
|
* References:
|
||||||
|
* - EIP-747: wallet_watchAsset standard
|
||||||
|
* - Document: "代币购买钱包自动添加无缝体验实现方案"
|
||||||
|
* - Document: "wallet_watchAsset TOP10钱包支持情况"
|
||||||
|
* - Document: "EIP-747避坑指南"
|
||||||
|
*
|
||||||
|
* EVM wallets (MetaMask, Trust, OKX, Coinbase, TokenPocket, Bitget, Rabby):
|
||||||
|
* → window.ethereum.request({ method: 'wallet_watchAsset', params: { type: 'ERC20', options: {...} } })
|
||||||
|
*
|
||||||
|
* TRON wallets (TronLink, TokenPocket TRON mode):
|
||||||
|
* → window.tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20', options: { address } } })
|
||||||
|
* NOTE: TronLink auto-fetches symbol/decimals from contract — only address is required.
|
||||||
|
*
|
||||||
|
* Key rules (from EIP-747 + wallet docs):
|
||||||
|
* 1. symbol MUST be ≤ 11 characters (MetaMask strict limit — will return -32602 if longer)
|
||||||
|
* 2. symbol should match on-chain contract.symbol() to avoid MetaMask rejection
|
||||||
|
* 3. Must be triggered by user action (button click), NEVER on page load
|
||||||
|
* 4. Ensure wallet chainId matches token's chain before calling (use wallet_switchEthereumChain first)
|
||||||
|
* 5. Returns true immediately — does NOT wait for user to confirm
|
||||||
|
*/
|
||||||
|
|
||||||
|
// XIC Token metadata — symbol "XIC" is 3 chars, well within the 11-char limit
|
||||||
|
export const XIC_TOKEN = {
|
||||||
|
// EVM contract addresses per chain (update when deployed)
|
||||||
|
evmAddresses: {
|
||||||
|
56: "0x59ff34dd59680a7125782b1f6df2a86ed46f5a24", // BSC
|
||||||
|
1: "0x0000000000000000000000000000000000000000", // ETH (TBD)
|
||||||
|
137: "0x0000000000000000000000000000000000000000", // Polygon (TBD)
|
||||||
|
42161: "0x0000000000000000000000000000000000000000", // Arbitrum (TBD)
|
||||||
|
43114: "0x0000000000000000000000000000000000000000", // Avalanche (TBD)
|
||||||
|
} as Record<number, string>,
|
||||||
|
// TRON TRC-20 contract address (Base58 format) — update when deployed
|
||||||
|
tronAddress: "TXICTokenAddressHere", // TODO: update with actual TRC-20 XIC address
|
||||||
|
symbol: "XIC", // ≤ 11 chars ✓ (3 chars)
|
||||||
|
decimals: 18,
|
||||||
|
name: "New AssetChain XIC Token",
|
||||||
|
imageUrl: "https://d2xsxph8kpxj0f.cloudfront.net/310519663287655625/Ngki3MumDNGduV3xJt3mga/nac-token-icon_382e5c30.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChainType = "ERC20" | "TRC20";
|
||||||
|
|
||||||
|
export interface AddTokenResult {
|
||||||
|
success: boolean;
|
||||||
|
cancelled?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add XIC token to EVM wallet using wallet_watchAsset (EIP-747 / EIP-1193)
|
||||||
|
* Compatible with MetaMask, Trust Wallet, OKX, Coinbase, TokenPocket, Bitget, Rabby, imToken.
|
||||||
|
*
|
||||||
|
* Per EIP-747 docs: use window.ethereum.request directly.
|
||||||
|
* Per EIP-747 avoid-pitfalls doc: ensure chainId matches before calling.
|
||||||
|
*/
|
||||||
|
export async function addXicToEvmWallet(
|
||||||
|
chainId?: number,
|
||||||
|
provider?: { request?: (args: { method: string; params?: unknown }) => Promise<unknown> } | null
|
||||||
|
): Promise<AddTokenResult> {
|
||||||
|
// Use provided provider or fall back to window.ethereum
|
||||||
|
const eth = provider?.request
|
||||||
|
? provider
|
||||||
|
: (typeof window !== "undefined"
|
||||||
|
? (window as unknown as { ethereum?: { request: (args: { method: string; params?: unknown }) => Promise<unknown> } }).ethereum
|
||||||
|
: null);
|
||||||
|
|
||||||
|
if (!eth?.request) {
|
||||||
|
return { success: false, error: "No EVM wallet detected. Please install MetaMask or a compatible wallet." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine contract address for this chain
|
||||||
|
const address = chainId ? (XIC_TOKEN.evmAddresses[chainId] ?? XIC_TOKEN.evmAddresses[56]) : XIC_TOKEN.evmAddresses[56];
|
||||||
|
|
||||||
|
// Skip if contract not yet deployed on this chain
|
||||||
|
if (address === "0x0000000000000000000000000000000000000000") {
|
||||||
|
return { success: false, error: `XIC contract not yet deployed on chain ${chainId}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Per EIP-747: wallet_watchAsset with type "ERC20"
|
||||||
|
// symbol must be ≤ 11 chars and should match on-chain contract.symbol()
|
||||||
|
await eth.request({
|
||||||
|
method: "wallet_watchAsset",
|
||||||
|
params: {
|
||||||
|
type: "ERC20",
|
||||||
|
options: {
|
||||||
|
address,
|
||||||
|
symbol: XIC_TOKEN.symbol, // "XIC" — 3 chars ✓
|
||||||
|
decimals: XIC_TOKEN.decimals,
|
||||||
|
image: XIC_TOKEN.imageUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Per EIP-747: returns true immediately, does NOT indicate user confirmed
|
||||||
|
console.log("[addTokenToWallet] XIC wallet_watchAsset request sent to EVM wallet");
|
||||||
|
return { success: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { code?: number; message?: string };
|
||||||
|
if (error?.code === 4001) {
|
||||||
|
// User rejected the request
|
||||||
|
return { success: false, cancelled: true };
|
||||||
|
}
|
||||||
|
if (error?.code === 4100) {
|
||||||
|
// Method not supported by this wallet
|
||||||
|
return { success: false, error: "This wallet does not support wallet_watchAsset" };
|
||||||
|
}
|
||||||
|
if (error?.message?.includes("longer than 11")) {
|
||||||
|
// -32602: symbol too long (should not happen with "XIC" but guard anyway)
|
||||||
|
return { success: false, error: "Token symbol rejected by wallet" };
|
||||||
|
}
|
||||||
|
console.warn("[addTokenToWallet] wallet_watchAsset EVM error:", err);
|
||||||
|
return { success: false, error: error?.message || "Failed to add token to wallet" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add XIC TRC-20 token to TronLink wallet.
|
||||||
|
*
|
||||||
|
* Per TronLink official API docs:
|
||||||
|
* tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20', options: { address } } })
|
||||||
|
*
|
||||||
|
* type values: 'trc10' | 'trc20' | 'trc721'
|
||||||
|
* For TRC-20: only address is required — TronLink auto-fetches symbol/decimals from contract.
|
||||||
|
* TronLink will show a confirmation popup to the user.
|
||||||
|
*/
|
||||||
|
export async function addXicToTronWallet(): Promise<AddTokenResult> {
|
||||||
|
const tronWeb = typeof window !== "undefined"
|
||||||
|
? (window as unknown as {
|
||||||
|
tronWeb?: {
|
||||||
|
defaultAddress?: { base58?: string };
|
||||||
|
request: (args: { method: string; params?: unknown }) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
}).tronWeb
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!tronWeb) {
|
||||||
|
return { success: false, error: "TronLink not detected. Please install TronLink wallet." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tronWeb.defaultAddress?.base58) {
|
||||||
|
return { success: false, error: "TronLink is locked. Please unlock your wallet first." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if TRC-20 contract not yet deployed
|
||||||
|
if (XIC_TOKEN.tronAddress === "TXICTokenAddressHere") {
|
||||||
|
return { success: false, error: "XIC TRC-20 contract not yet deployed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Per TronLink official API: wallet_watchAsset with type 'trc20'
|
||||||
|
// TronLink auto-fetches symbol, decimals, name from the contract
|
||||||
|
await tronWeb.request({
|
||||||
|
method: "wallet_watchAsset",
|
||||||
|
params: {
|
||||||
|
type: "trc20",
|
||||||
|
options: {
|
||||||
|
address: XIC_TOKEN.tronAddress, // Base58 TRC-20 contract address
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[addTokenToWallet] XIC wallet_watchAsset request sent to TronLink");
|
||||||
|
return { success: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { code?: number; message?: string };
|
||||||
|
if (error?.code === 4001) {
|
||||||
|
return { success: false, cancelled: true };
|
||||||
|
}
|
||||||
|
console.warn("[addTokenToWallet] wallet_watchAsset TRON error:", err);
|
||||||
|
// Non-fatal — some older TronLink versions may not support this
|
||||||
|
return { success: false, error: error?.message || "Failed to add TRC-20 token to TronLink" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function: add XIC to wallet based on chain type.
|
||||||
|
* Call this after a successful purchase to prompt the user to add XIC.
|
||||||
|
*
|
||||||
|
* Per EIP-747 security rule: MUST be triggered by user action, never auto-called on page load.
|
||||||
|
*/
|
||||||
|
export async function addXicToWallet(
|
||||||
|
chainType: ChainType,
|
||||||
|
chainId?: number,
|
||||||
|
evmProvider?: { request?: (args: { method: string; params?: unknown }) => Promise<unknown> } | null
|
||||||
|
): Promise<AddTokenResult> {
|
||||||
|
if (chainType === "ERC20") {
|
||||||
|
return addXicToEvmWallet(chainId, evmProvider);
|
||||||
|
} else {
|
||||||
|
return addXicToTronWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
// NAC Cross-Chain Bridge — Self-Developed
|
// NAC Cross-Chain Bridge — Self-Developed
|
||||||
// v3: NAC native bridge, no third-party protocols
|
// v4: TRON/TRC20 support + wallet_watchAsset auto-add + order status polling
|
||||||
// User sends USDT on any chain to our receiving address → backend monitors → XIC distributed
|
// User sends USDT on any chain → backend monitors → XIC distributed
|
||||||
// Supports: BSC, ETH, Polygon, Arbitrum, Avalanche
|
// Supports: BSC, ETH, Polygon, Arbitrum, Avalanche, TRON
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
import {
|
import {
|
||||||
ArrowDown, ArrowLeft, Copy, CheckCheck, ExternalLink,
|
ArrowDown, ArrowLeft, Copy, CheckCheck, ExternalLink,
|
||||||
Loader2, RefreshCw, History, ChevronDown, ChevronUp,
|
Loader2, RefreshCw, History, ChevronDown, ChevronUp,
|
||||||
Wallet, AlertCircle, CheckCircle2, Clock, XCircle, Zap
|
Wallet, AlertCircle, CheckCircle2, Clock, XCircle, Zap, Plus
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { WalletSelector } from "@/components/WalletSelector";
|
import { WalletSelector } from "@/components/WalletSelector";
|
||||||
import { useWallet } from "@/hooks/useWallet";
|
import { useWallet } from "@/hooks/useWallet";
|
||||||
import { useBridgeWeb3 } from "@/hooks/useBridgeWeb3";
|
import { useBridgeWeb3 } from "@/hooks/useBridgeWeb3";
|
||||||
|
import { useTronBridge } from "@/hooks/useTronBridge";
|
||||||
|
import { addXicToEvmWallet, addXicToTronWallet } from "@/lib/addTokenToWallet";
|
||||||
|
|
||||||
// ─── Language ─────────────────────────────────────────────────────────────────
|
// ─── Language ─────────────────────────────────────────────────────────────────
|
||||||
type Lang = "zh" | "en";
|
type Lang = "zh" | "en";
|
||||||
|
|
@ -23,7 +25,7 @@ type Lang = "zh" | "en";
|
||||||
const T = {
|
const T = {
|
||||||
zh: {
|
zh: {
|
||||||
title: "从任意链购买 XIC",
|
title: "从任意链购买 XIC",
|
||||||
subtitle: "使用 BSC、ETH、Polygon、Arbitrum 或 Avalanche 上的 USDT 购买 XIC 代币",
|
subtitle: "使用 BSC、ETH、Polygon、Arbitrum、Avalanche 或 TRON 上的 USDT 购买 XIC 代币",
|
||||||
fromChain: "选择来源链",
|
fromChain: "选择来源链",
|
||||||
youPay: "支付金额 (USDT)",
|
youPay: "支付金额 (USDT)",
|
||||||
youReceive: "您将获得 (XIC)",
|
youReceive: "您将获得 (XIC)",
|
||||||
|
|
@ -40,8 +42,10 @@ const T = {
|
||||||
registered: "已登记!我们正在监控您的转账",
|
registered: "已登记!我们正在监控您的转账",
|
||||||
registeredDesc: "转账确认后(通常 1-5 分钟),XIC 代币将自动分发到您的接收地址",
|
registeredDesc: "转账确认后(通常 1-5 分钟),XIC 代币将自动分发到您的接收地址",
|
||||||
connectWallet: "连接钱包",
|
connectWallet: "连接钱包",
|
||||||
|
connectTronLink: "连接 TronLink",
|
||||||
connected: "已连接",
|
connected: "已连接",
|
||||||
walletConnected: "钱包已连接,地址已自动填入",
|
walletConnected: "钱包已连接,地址已自动填入",
|
||||||
|
tronConnected: "TronLink 已连接",
|
||||||
history: "我的交易记录",
|
history: "我的交易记录",
|
||||||
noHistory: "暂无交易记录",
|
noHistory: "暂无交易记录",
|
||||||
historyDesc: "输入钱包地址后可查看历史记录",
|
historyDesc: "输入钱包地址后可查看历史记录",
|
||||||
|
|
@ -57,6 +61,7 @@ const T = {
|
||||||
xPotential: "5倍潜力",
|
xPotential: "5倍潜力",
|
||||||
disclaimer: "本跨链桥为 NAC 自研系统。请确保发送正确金额的 USDT 到指定地址。最小转账金额:$10 USDT。",
|
disclaimer: "本跨链桥为 NAC 自研系统。请确保发送正确金额的 USDT 到指定地址。最小转账金额:$10 USDT。",
|
||||||
gasNote: "Gas 费用由 {symbol}({chain} 原生代币)支付,请确保钱包中有足够的 {symbol}",
|
gasNote: "Gas 费用由 {symbol}({chain} 原生代币)支付,请确保钱包中有足够的 {symbol}",
|
||||||
|
tronGasNote: "TRON 网络需要 TRX 作为能量/带宽费用,请确保钱包中有足够的 TRX",
|
||||||
minAmount: "最小转账金额:$10 USDT",
|
minAmount: "最小转账金额:$10 USDT",
|
||||||
calcXic: "按 $0.02/XIC 计算",
|
calcXic: "按 $0.02/XIC 计算",
|
||||||
step1: "第一步:选择来源链并输入金额",
|
step1: "第一步:选择来源链并输入金额",
|
||||||
|
|
@ -79,10 +84,18 @@ const T = {
|
||||||
amountMin: "最小金额为 $10 USDT",
|
amountMin: "最小金额为 $10 USDT",
|
||||||
intentSuccess: "转账意图已登记!请立即发送 USDT",
|
intentSuccess: "转账意图已登记!请立即发送 USDT",
|
||||||
intentFailed: "登记失败,请重试",
|
intentFailed: "登记失败,请重试",
|
||||||
|
addToWallet: "添加 XIC 到钱包",
|
||||||
|
addToWalletSuccess: "XIC 代币已添加到钱包!",
|
||||||
|
addToWalletCancelled: "已取消添加",
|
||||||
|
sendViaWallet: "一键钱包转账",
|
||||||
|
sending: "转账中...请在钱包确认",
|
||||||
|
orSendDirectly: "或一键转账",
|
||||||
|
walletSignNote: "钱包将弹出签名确认,无需手动复制地址",
|
||||||
|
pollingNote: "正在监控链上转账...",
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "Buy XIC from Any Chain",
|
title: "Buy XIC from Any Chain",
|
||||||
subtitle: "Use USDT on BSC, ETH, Polygon, Arbitrum or Avalanche to buy XIC tokens",
|
subtitle: "Use USDT on BSC, ETH, Polygon, Arbitrum, Avalanche or TRON to buy XIC tokens",
|
||||||
fromChain: "Select Source Chain",
|
fromChain: "Select Source Chain",
|
||||||
youPay: "You Pay (USDT)",
|
youPay: "You Pay (USDT)",
|
||||||
youReceive: "You Receive (XIC)",
|
youReceive: "You Receive (XIC)",
|
||||||
|
|
@ -99,8 +112,10 @@ const T = {
|
||||||
registered: "Registered! We are monitoring your transfer",
|
registered: "Registered! We are monitoring your transfer",
|
||||||
registeredDesc: "After transfer confirmation (usually 1-5 minutes), XIC tokens will be automatically distributed to your receive address",
|
registeredDesc: "After transfer confirmation (usually 1-5 minutes), XIC tokens will be automatically distributed to your receive address",
|
||||||
connectWallet: "Connect Wallet",
|
connectWallet: "Connect Wallet",
|
||||||
|
connectTronLink: "Connect TronLink",
|
||||||
connected: "Connected",
|
connected: "Connected",
|
||||||
walletConnected: "Wallet connected, address auto-filled",
|
walletConnected: "Wallet connected, address auto-filled",
|
||||||
|
tronConnected: "TronLink connected",
|
||||||
history: "My Transactions",
|
history: "My Transactions",
|
||||||
noHistory: "No transactions yet",
|
noHistory: "No transactions yet",
|
||||||
historyDesc: "Enter your wallet address to view history",
|
historyDesc: "Enter your wallet address to view history",
|
||||||
|
|
@ -116,6 +131,7 @@ const T = {
|
||||||
xPotential: "5x potential",
|
xPotential: "5x potential",
|
||||||
disclaimer: "This bridge is developed by NAC. Please ensure you send the correct USDT amount to the specified address. Minimum transfer: $10 USDT.",
|
disclaimer: "This bridge is developed by NAC. Please ensure you send the correct USDT amount to the specified address. Minimum transfer: $10 USDT.",
|
||||||
gasNote: "Gas fee is paid in {symbol} ({chain} native token). Ensure you have enough {symbol} in your wallet.",
|
gasNote: "Gas fee is paid in {symbol} ({chain} native token). Ensure you have enough {symbol} in your wallet.",
|
||||||
|
tronGasNote: "TRON network requires TRX for energy/bandwidth fees. Ensure you have enough TRX in your wallet.",
|
||||||
minAmount: "Minimum: $10 USDT",
|
minAmount: "Minimum: $10 USDT",
|
||||||
calcXic: "Calculated at $0.02/XIC",
|
calcXic: "Calculated at $0.02/XIC",
|
||||||
step1: "Step 1: Select chain and enter amount",
|
step1: "Step 1: Select chain and enter amount",
|
||||||
|
|
@ -138,6 +154,14 @@ const T = {
|
||||||
amountMin: "Minimum amount is $10 USDT",
|
amountMin: "Minimum amount is $10 USDT",
|
||||||
intentSuccess: "Intent registered! Please send USDT now",
|
intentSuccess: "Intent registered! Please send USDT now",
|
||||||
intentFailed: "Registration failed, please try again",
|
intentFailed: "Registration failed, please try again",
|
||||||
|
addToWallet: "Add XIC to Wallet",
|
||||||
|
addToWalletSuccess: "XIC token added to wallet!",
|
||||||
|
addToWalletCancelled: "Cancelled",
|
||||||
|
sendViaWallet: "Send via Wallet",
|
||||||
|
sending: "Sending... confirm in wallet",
|
||||||
|
orSendDirectly: "or send directly",
|
||||||
|
walletSignNote: "Wallet will prompt for signature — no manual copy needed",
|
||||||
|
pollingNote: "Monitoring on-chain transfer...",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -154,6 +178,7 @@ const CHAINS = [
|
||||||
explorerUrl: "https://bscscan.com",
|
explorerUrl: "https://bscscan.com",
|
||||||
usdtDecimals: 18,
|
usdtDecimals: 18,
|
||||||
gasNote: "BNB",
|
gasNote: "BNB",
|
||||||
|
isTron: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
chainId: 1,
|
chainId: 1,
|
||||||
|
|
@ -166,6 +191,7 @@ const CHAINS = [
|
||||||
explorerUrl: "https://etherscan.io",
|
explorerUrl: "https://etherscan.io",
|
||||||
usdtDecimals: 6,
|
usdtDecimals: 6,
|
||||||
gasNote: "ETH",
|
gasNote: "ETH",
|
||||||
|
isTron: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
chainId: 137,
|
chainId: 137,
|
||||||
|
|
@ -178,6 +204,7 @@ const CHAINS = [
|
||||||
explorerUrl: "https://polygonscan.com",
|
explorerUrl: "https://polygonscan.com",
|
||||||
usdtDecimals: 6,
|
usdtDecimals: 6,
|
||||||
gasNote: "MATIC",
|
gasNote: "MATIC",
|
||||||
|
isTron: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
chainId: 42161,
|
chainId: 42161,
|
||||||
|
|
@ -190,6 +217,7 @@ const CHAINS = [
|
||||||
explorerUrl: "https://arbiscan.io",
|
explorerUrl: "https://arbiscan.io",
|
||||||
usdtDecimals: 6,
|
usdtDecimals: 6,
|
||||||
gasNote: "ETH",
|
gasNote: "ETH",
|
||||||
|
isTron: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
chainId: 43114,
|
chainId: 43114,
|
||||||
|
|
@ -202,6 +230,21 @@ const CHAINS = [
|
||||||
explorerUrl: "https://snowtrace.io",
|
explorerUrl: "https://snowtrace.io",
|
||||||
usdtDecimals: 6,
|
usdtDecimals: 6,
|
||||||
gasNote: "AVAX",
|
gasNote: "AVAX",
|
||||||
|
isTron: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// TRON mainnet — chainId is a convention for UI, not used in ethers.js
|
||||||
|
chainId: 728126428,
|
||||||
|
name: "TRON",
|
||||||
|
shortName: "TRX",
|
||||||
|
symbol: "TRX",
|
||||||
|
icon: "🔺",
|
||||||
|
color: "#FF0013",
|
||||||
|
receivingAddress: "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp",
|
||||||
|
explorerUrl: "https://tronscan.org",
|
||||||
|
usdtDecimals: 6,
|
||||||
|
gasNote: "TRX",
|
||||||
|
isTron: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -215,6 +258,7 @@ export default function Bridge() {
|
||||||
|
|
||||||
const [selectedChainIdx, setSelectedChainIdx] = useState(0);
|
const [selectedChainIdx, setSelectedChainIdx] = useState(0);
|
||||||
const selectedChain = CHAINS[selectedChainIdx];
|
const selectedChain = CHAINS[selectedChainIdx];
|
||||||
|
const isTronChain = selectedChain.isTron;
|
||||||
|
|
||||||
const [usdtAmount, setUsdtAmount] = useState("100");
|
const [usdtAmount, setUsdtAmount] = useState("100");
|
||||||
const [xicReceiveAddress, setXicReceiveAddress] = useState("");
|
const [xicReceiveAddress, setXicReceiveAddress] = useState("");
|
||||||
|
|
@ -228,11 +272,17 @@ export default function Bridge() {
|
||||||
const [queryAddress, setQueryAddress] = useState("");
|
const [queryAddress, setQueryAddress] = useState("");
|
||||||
const [copiedHash, setCopiedHash] = useState<string | null>(null);
|
const [copiedHash, setCopiedHash] = useState<string | null>(null);
|
||||||
|
|
||||||
// Wallet
|
// Order polling state
|
||||||
|
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
|
// EVM Wallet
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
const [showWalletSelector, setShowWalletSelector] = useState(false);
|
const [showWalletSelector, setShowWalletSelector] = useState(false);
|
||||||
|
|
||||||
// Web3 bridge: USDT balance + on-chain transfer
|
// TRON Wallet
|
||||||
|
const tron = useTronBridge();
|
||||||
|
|
||||||
|
// Web3 bridge: EVM USDT balance + on-chain transfer
|
||||||
const web3Bridge = useBridgeWeb3(
|
const web3Bridge = useBridgeWeb3(
|
||||||
wallet.provider,
|
wallet.provider,
|
||||||
wallet.signer,
|
wallet.signer,
|
||||||
|
|
@ -240,16 +290,19 @@ export default function Bridge() {
|
||||||
wallet.chainId
|
wallet.chainId
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-fill XIC receive address from connected wallet
|
// Auto-fill XIC receive address from connected EVM wallet
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wallet.address && !xicReceiveAddress) {
|
if (wallet.address && !xicReceiveAddress) {
|
||||||
setXicReceiveAddress(wallet.address);
|
setXicReceiveAddress(wallet.address);
|
||||||
}
|
}
|
||||||
}, [wallet.address]);
|
}, [wallet.address]);
|
||||||
|
|
||||||
// tRPC: register bridge intent
|
// Auto-fill XIC receive address from connected TRON wallet (EVM address same as TRON? No — use EVM address for XIC)
|
||||||
|
// When TRON chain selected and TronLink connected, show tron address in the "connected" indicator
|
||||||
|
// but XIC receive address must still be a BSC (0x) address
|
||||||
|
|
||||||
|
// tRPC mutations
|
||||||
const registerIntent = trpc.bridge.registerIntent.useMutation();
|
const registerIntent = trpc.bridge.registerIntent.useMutation();
|
||||||
// tRPC: record completed on-chain order
|
|
||||||
const recordOrder = trpc.bridge.recordOrder.useMutation();
|
const recordOrder = trpc.bridge.recordOrder.useMutation();
|
||||||
|
|
||||||
// tRPC: query orders by wallet address
|
// tRPC: query orders by wallet address
|
||||||
|
|
@ -258,6 +311,36 @@ export default function Bridge() {
|
||||||
{ enabled: !!queryAddress && queryAddress.length > 10 }
|
{ enabled: !!queryAddress && queryAddress.length > 10 }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-polling: after registration, poll every 5 seconds
|
||||||
|
useEffect(() => {
|
||||||
|
if (registered && queryAddress) {
|
||||||
|
pollingRef.current = setInterval(() => {
|
||||||
|
refetchOrders();
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [registered, queryAddress, refetchOrders]);
|
||||||
|
|
||||||
|
// Stop polling when order is distributed
|
||||||
|
useEffect(() => {
|
||||||
|
if (orders && orders.some(o => o.status === "distributed")) {
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearInterval(pollingRef.current);
|
||||||
|
pollingRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [orders]);
|
||||||
|
|
||||||
// Calculated XIC amount
|
// Calculated XIC amount
|
||||||
const xicAmount = usdtAmount && !isNaN(Number(usdtAmount)) && Number(usdtAmount) > 0
|
const xicAmount = usdtAmount && !isNaN(Number(usdtAmount)) && Number(usdtAmount) > 0
|
||||||
? (Number(usdtAmount) / XIC_PRICE).toLocaleString(undefined, { maximumFractionDigits: 0 })
|
? (Number(usdtAmount) / XIC_PRICE).toLocaleString(undefined, { maximumFractionDigits: 0 })
|
||||||
|
|
@ -278,7 +361,34 @@ export default function Bridge() {
|
||||||
setTimeout(() => setCopiedHash(null), 2000);
|
setTimeout(() => setCopiedHash(null), 2000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle on-chain USDT transfer via wallet (Web3)
|
// ─── Add XIC to wallet after purchase ──────────────────────────────────────
|
||||||
|
const handleAddXicToWallet = useCallback(async () => {
|
||||||
|
if (isTronChain) {
|
||||||
|
const result = await addXicToTronWallet();
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(t.addToWalletSuccess);
|
||||||
|
} else if (result.cancelled) {
|
||||||
|
toast.info(t.addToWalletCancelled);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || "Failed to add token");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// EVM: use the connected wallet's provider
|
||||||
|
const provider = wallet.provider
|
||||||
|
? { request: (args: { method: string; params?: unknown }) => wallet.provider!.send(args.method, args.params ? [args.params] : []) }
|
||||||
|
: null;
|
||||||
|
const result = await addXicToEvmWallet(wallet.chainId ?? undefined, provider);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(t.addToWalletSuccess);
|
||||||
|
} else if (result.cancelled) {
|
||||||
|
toast.info(t.addToWalletCancelled);
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || "Failed to add token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isTronChain, wallet.provider, t]);
|
||||||
|
|
||||||
|
// ─── EVM: one-click USDT transfer via wallet ───────────────────────────────
|
||||||
const handleSendViaWallet = async () => {
|
const handleSendViaWallet = async () => {
|
||||||
const amount = Number(usdtAmount);
|
const amount = Number(usdtAmount);
|
||||||
if (!usdtAmount || isNaN(amount) || amount <= 0) {
|
if (!usdtAmount || isNaN(amount) || amount <= 0) {
|
||||||
|
|
@ -306,6 +416,7 @@ export default function Bridge() {
|
||||||
await wallet.switchNetwork(selectedChain.chainId);
|
await wallet.switchNetwork(selectedChain.chainId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
web3Bridge.resetTransferState();
|
web3Bridge.resetTransferState();
|
||||||
const result = await web3Bridge.sendUsdtTransfer({
|
const result = await web3Bridge.sendUsdtTransfer({
|
||||||
toAddress: selectedChain.receivingAddress,
|
toAddress: selectedChain.receivingAddress,
|
||||||
|
|
@ -313,8 +424,8 @@ export default function Bridge() {
|
||||||
chainId: selectedChain.chainId,
|
chainId: selectedChain.chainId,
|
||||||
decimals: selectedChain.usdtDecimals,
|
decimals: selectedChain.usdtDecimals,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.txHash) {
|
if (result?.txHash) {
|
||||||
// Record the completed on-chain order
|
|
||||||
try {
|
try {
|
||||||
await recordOrder.mutateAsync({
|
await recordOrder.mutateAsync({
|
||||||
txHash: result.txHash,
|
txHash: result.txHash,
|
||||||
|
|
@ -322,16 +433,29 @@ export default function Bridge() {
|
||||||
fromChainId: selectedChain.chainId,
|
fromChainId: selectedChain.chainId,
|
||||||
fromToken: "USDT",
|
fromToken: "USDT",
|
||||||
fromAmount: amount.toString(),
|
fromAmount: amount.toString(),
|
||||||
toChainId: 56, // XIC is on BSC
|
toChainId: 56,
|
||||||
toToken: "XIC",
|
toToken: "XIC",
|
||||||
toAmount: (amount / XIC_PRICE).toFixed(0),
|
toAmount: (amount / XIC_PRICE).toFixed(0),
|
||||||
});
|
});
|
||||||
setRegistered(true);
|
setRegistered(true);
|
||||||
|
setQueryAddress(xicReceiveAddress || wallet.address || "");
|
||||||
|
setHistoryAddress(xicReceiveAddress || wallet.address || "");
|
||||||
toast.success(lang === "zh"
|
toast.success(lang === "zh"
|
||||||
? `转账成功!TX: ${result.txHash.slice(0, 10)}...`
|
? `转账成功!TX: ${result.txHash.slice(0, 10)}...`
|
||||||
: `Transfer sent! TX: ${result.txHash.slice(0, 10)}...`);
|
: `Transfer sent! TX: ${result.txHash.slice(0, 10)}...`);
|
||||||
setQueryAddress(xicReceiveAddress || wallet.address || "");
|
|
||||||
setHistoryAddress(xicReceiveAddress || wallet.address || "");
|
// Auto-prompt to add XIC to wallet
|
||||||
|
setTimeout(async () => {
|
||||||
|
const addResult = await addXicToEvmWallet(
|
||||||
|
wallet.chainId ?? undefined,
|
||||||
|
wallet.provider
|
||||||
|
? { request: (args: { method: string; params?: unknown }) => wallet.provider!.send(args.method, args.params ? [args.params] : []) }
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
if (addResult.success) {
|
||||||
|
toast.success(t.addToWalletSuccess);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
} catch {
|
} catch {
|
||||||
setRegistered(true);
|
setRegistered(true);
|
||||||
toast.success(lang === "zh"
|
toast.success(lang === "zh"
|
||||||
|
|
@ -343,7 +467,71 @@ export default function Bridge() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate and register intent
|
// ─── TRON: one-click TRC20 USDT transfer ──────────────────────────────────
|
||||||
|
const handleSendViaTronLink = async () => {
|
||||||
|
const amount = Number(usdtAmount);
|
||||||
|
if (!usdtAmount || isNaN(amount) || amount <= 0) {
|
||||||
|
toast.error(t.amountRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (amount < 10) {
|
||||||
|
toast.error(t.amountMin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!xicReceiveAddress || !/^0x[0-9a-fA-F]{40}$/.test(xicReceiveAddress)) {
|
||||||
|
toast.error(t.addressRequired);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tron.tronConnected) {
|
||||||
|
toast.error(lang === "zh" ? "请先连接 TronLink" : "Please connect TronLink first");
|
||||||
|
await tron.connectTronLink();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tron.resetTronTransferState();
|
||||||
|
const result = await tron.sendTrc20Transfer({
|
||||||
|
toAddress: selectedChain.receivingAddress,
|
||||||
|
usdtAmount: amount,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result?.txHash) {
|
||||||
|
try {
|
||||||
|
await recordOrder.mutateAsync({
|
||||||
|
txHash: result.txHash,
|
||||||
|
walletAddress: tron.tronAddress || xicReceiveAddress,
|
||||||
|
fromChainId: selectedChain.chainId,
|
||||||
|
fromToken: "USDT",
|
||||||
|
fromAmount: amount.toString(),
|
||||||
|
toChainId: 56,
|
||||||
|
toToken: "XIC",
|
||||||
|
toAmount: (amount / XIC_PRICE).toFixed(0),
|
||||||
|
});
|
||||||
|
setRegistered(true);
|
||||||
|
setQueryAddress(xicReceiveAddress || tron.tronAddress || "");
|
||||||
|
setHistoryAddress(xicReceiveAddress || tron.tronAddress || "");
|
||||||
|
toast.success(lang === "zh"
|
||||||
|
? `TRC20 转账成功!TX: ${result.txHash.slice(0, 10)}...`
|
||||||
|
: `TRC20 transfer sent! TX: ${result.txHash.slice(0, 10)}...`);
|
||||||
|
|
||||||
|
// Auto-prompt to add XIC to TronLink
|
||||||
|
setTimeout(async () => {
|
||||||
|
const addResult = await addXicToTronWallet();
|
||||||
|
if (addResult.success) {
|
||||||
|
toast.success(t.addToWalletSuccess);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
} catch {
|
||||||
|
setRegistered(true);
|
||||||
|
toast.success(lang === "zh"
|
||||||
|
? `TRC20 转账已发送!TX: ${result.txHash.slice(0, 10)}...`
|
||||||
|
: `TRC20 transfer sent! TX: ${result.txHash.slice(0, 10)}...`);
|
||||||
|
}
|
||||||
|
} else if (tron.tronTransferError) {
|
||||||
|
toast.error(tron.tronTransferError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Manual confirm (no wallet) ───────────────────────────────────────────
|
||||||
const handleConfirmSend = async () => {
|
const handleConfirmSend = async () => {
|
||||||
const amount = Number(usdtAmount);
|
const amount = Number(usdtAmount);
|
||||||
if (!usdtAmount || isNaN(amount) || amount <= 0) {
|
if (!usdtAmount || isNaN(amount) || amount <= 0) {
|
||||||
|
|
@ -358,7 +546,7 @@ export default function Bridge() {
|
||||||
toast.error(t.addressRequired);
|
toast.error(t.addressRequired);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!/^0x[0-9a-fA-F]{40}$/.test(xicReceiveAddress)) {
|
if (!isTronChain && !/^0x[0-9a-fA-F]{40}$/.test(xicReceiveAddress)) {
|
||||||
toast.error(t.invalidAddress);
|
toast.error(t.invalidAddress);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -367,17 +555,17 @@ export default function Bridge() {
|
||||||
try {
|
try {
|
||||||
await registerIntent.mutateAsync({
|
await registerIntent.mutateAsync({
|
||||||
fromChainId: selectedChain.chainId,
|
fromChainId: selectedChain.chainId,
|
||||||
senderAddress: wallet.address || undefined, // only set if wallet connected
|
senderAddress: isTronChain ? (tron.tronAddress || undefined) : (wallet.address || undefined),
|
||||||
xicReceiveAddress,
|
xicReceiveAddress,
|
||||||
expectedUsdt: amount,
|
expectedUsdt: amount,
|
||||||
});
|
});
|
||||||
setRegistered(true);
|
setRegistered(true);
|
||||||
toast.success(t.intentSuccess);
|
toast.success(t.intentSuccess);
|
||||||
// Auto-query history
|
|
||||||
setQueryAddress(xicReceiveAddress);
|
setQueryAddress(xicReceiveAddress);
|
||||||
setHistoryAddress(xicReceiveAddress);
|
setHistoryAddress(xicReceiveAddress);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
toast.error(t.intentFailed + ": " + (err?.message || ""));
|
const error = err as { message?: string };
|
||||||
|
toast.error(t.intentFailed + ": " + (error?.message || ""));
|
||||||
} finally {
|
} finally {
|
||||||
setRegistering(false);
|
setRegistering(false);
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +649,7 @@ export default function Bridge() {
|
||||||
{CHAINS.map((chain, idx) => (
|
{CHAINS.map((chain, idx) => (
|
||||||
<button
|
<button
|
||||||
key={chain.chainId}
|
key={chain.chainId}
|
||||||
onClick={() => { setSelectedChainIdx(idx); setRegistered(false); }}
|
onClick={() => { setSelectedChainIdx(idx); setRegistered(false); web3Bridge.resetTransferState(); tron.resetTronTransferState(); }}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all"
|
||||||
style={{
|
style={{
|
||||||
background: selectedChainIdx === idx ? `${chain.color}22` : "rgba(255,255,255,0.05)",
|
background: selectedChainIdx === idx ? `${chain.color}22` : "rgba(255,255,255,0.05)",
|
||||||
|
|
@ -476,7 +664,9 @@ export default function Bridge() {
|
||||||
</div>
|
</div>
|
||||||
{/* Gas note */}
|
{/* Gas note */}
|
||||||
<p className="text-xs text-white/30 mt-2">
|
<p className="text-xs text-white/30 mt-2">
|
||||||
{t.gasNote
|
{isTronChain
|
||||||
|
? t.tronGasNote
|
||||||
|
: t.gasNote
|
||||||
.replace("{symbol}", selectedChain.gasNote)
|
.replace("{symbol}", selectedChain.gasNote)
|
||||||
.replace("{chain}", selectedChain.name)
|
.replace("{chain}", selectedChain.name)
|
||||||
.replace("{symbol}", selectedChain.gasNote)}
|
.replace("{symbol}", selectedChain.gasNote)}
|
||||||
|
|
@ -554,7 +744,8 @@ export default function Bridge() {
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<label className="text-xs text-white/50 uppercase tracking-wider">{t.xicReceiveAddr}</label>
|
<label className="text-xs text-white/50 uppercase tracking-wider">{t.xicReceiveAddr}</label>
|
||||||
{!wallet.address && (
|
{/* EVM wallet connect button (non-TRON chains) */}
|
||||||
|
{!isTronChain && !wallet.address && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowWalletSelector(true)}
|
onClick={() => setShowWalletSelector(true)}
|
||||||
className="flex items-center gap-1 text-xs text-amber-400 hover:text-amber-300 transition-colors"
|
className="flex items-center gap-1 text-xs text-amber-400 hover:text-amber-300 transition-colors"
|
||||||
|
|
@ -563,12 +754,36 @@ export default function Bridge() {
|
||||||
{t.connectWallet}
|
{t.connectWallet}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{wallet.address && (
|
{!isTronChain && wallet.address && (
|
||||||
<span className="text-xs text-green-400 flex items-center gap-1">
|
<span className="text-xs text-green-400 flex items-center gap-1">
|
||||||
<CheckCircle2 size={12} />
|
<CheckCircle2 size={12} />
|
||||||
{t.walletConnected}
|
{t.walletConnected}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{/* TronLink connect button (TRON chain) */}
|
||||||
|
{isTronChain && !tron.tronConnected && (
|
||||||
|
<button
|
||||||
|
onClick={tron.connectTronLink}
|
||||||
|
disabled={tron.tronConnecting}
|
||||||
|
className="flex items-center gap-1 text-xs transition-colors"
|
||||||
|
style={{ color: "#FF0013" }}
|
||||||
|
>
|
||||||
|
{tron.tronConnecting ? (
|
||||||
|
<Loader2 size={12} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Wallet size={12} />
|
||||||
|
)}
|
||||||
|
{tron.tronConnecting
|
||||||
|
? (lang === "zh" ? "连接中..." : "Connecting...")
|
||||||
|
: t.connectTronLink}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isTronChain && tron.tronConnected && (
|
||||||
|
<span className="text-xs flex items-center gap-1" style={{ color: "#FF0013" }}>
|
||||||
|
<CheckCircle2 size={12} />
|
||||||
|
{t.tronConnected}: {tron.tronAddress?.slice(0, 6)}...{tron.tronAddress?.slice(-4)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -583,6 +798,16 @@ export default function Bridge() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-white/30 mt-1">{t.xicReceiveAddrHint}</p>
|
<p className="text-xs text-white/30 mt-1">{t.xicReceiveAddrHint}</p>
|
||||||
|
{/* TronLink error */}
|
||||||
|
{isTronChain && tron.tronError && (
|
||||||
|
<div
|
||||||
|
className="mt-2 rounded-lg p-2.5 text-xs flex items-start gap-2"
|
||||||
|
style={{ background: "rgba(255,80,80,0.08)", border: "1px solid rgba(255,80,80,0.2)", color: "#ff8080" }}
|
||||||
|
>
|
||||||
|
<AlertCircle size={12} className="shrink-0 mt-0.5" />
|
||||||
|
<span>{tron.tronError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 4: Receiving address + Web3 transfer */}
|
{/* Step 4: Receiving address + Web3 transfer */}
|
||||||
|
|
@ -590,7 +815,7 @@ export default function Bridge() {
|
||||||
className="rounded-xl p-4 space-y-3"
|
className="rounded-xl p-4 space-y-3"
|
||||||
style={{ background: "rgba(240,180,41,0.05)", border: "1px solid rgba(240,180,41,0.2)" }}
|
style={{ background: "rgba(240,180,41,0.05)", border: "1px solid rgba(240,180,41,0.2)" }}
|
||||||
>
|
>
|
||||||
{/* Header row with chain badge and USDT balance */}
|
{/* Header row with chain badge and balance */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-amber-400 text-sm font-semibold">{t.sendTo}</span>
|
<span className="text-amber-400 text-sm font-semibold">{t.sendTo}</span>
|
||||||
|
|
@ -601,8 +826,8 @@ export default function Bridge() {
|
||||||
{selectedChain.icon} {selectedChain.name}
|
{selectedChain.icon} {selectedChain.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* USDT Balance display */}
|
{/* Balance display */}
|
||||||
{wallet.isConnected && (
|
{!isTronChain && wallet.isConnected && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
{web3Bridge.usdtBalanceLoading ? (
|
{web3Bridge.usdtBalanceLoading ? (
|
||||||
<Loader2 size={10} className="animate-spin text-white/40" />
|
<Loader2 size={10} className="animate-spin text-white/40" />
|
||||||
|
|
@ -621,6 +846,25 @@ export default function Bridge() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isTronChain && tron.tronConnected && (
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
{tron.tronUsdtBalanceLoading ? (
|
||||||
|
<Loader2 size={10} className="animate-spin text-white/40" />
|
||||||
|
) : tron.tronUsdtBalance !== null ? (
|
||||||
|
<span className="text-white/50">
|
||||||
|
{lang === "zh" ? "余额" : "Bal"}:
|
||||||
|
<span className="text-amber-300 font-mono ml-1">{tron.tronUsdtBalance} USDT</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
onClick={() => tron.fetchTronUsdtBalance()}
|
||||||
|
className="text-white/30 hover:text-white/60 transition-colors ml-1"
|
||||||
|
title={lang === "zh" ? "刷新余额" : "Refresh balance"}
|
||||||
|
>
|
||||||
|
<RefreshCw size={10} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-white/50">{t.sendToHint}</p>
|
<p className="text-xs text-white/50">{t.sendToHint}</p>
|
||||||
|
|
@ -649,12 +893,12 @@ export default function Bridge() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* One-click wallet transfer — shown when wallet connected and not yet registered */}
|
{/* EVM one-click wallet transfer */}
|
||||||
{wallet.isConnected && !registered && (
|
{!isTronChain && wallet.isConnected && !registered && (
|
||||||
<div className="space-y-2 pt-1">
|
<div className="space-y-2 pt-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
||||||
<span className="text-xs text-white/30">{lang === "zh" ? "或一键转账" : "or send directly"}</span>
|
<span className="text-xs text-white/30">{t.orSendDirectly}</span>
|
||||||
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -673,7 +917,7 @@ export default function Bridge() {
|
||||||
{web3Bridge.transferring ? (
|
{web3Bridge.transferring ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 size={15} className="animate-spin" />
|
<Loader2 size={15} className="animate-spin" />
|
||||||
{lang === "zh" ? "转账中...请在钱包确认" : "Sending... confirm in wallet"}
|
{t.sending}
|
||||||
{web3Bridge.transferTxHash && (
|
{web3Bridge.transferTxHash && (
|
||||||
<span className="text-xs opacity-50 font-mono">
|
<span className="text-xs opacity-50 font-mono">
|
||||||
{web3Bridge.transferTxHash.slice(0, 8)}...
|
{web3Bridge.transferTxHash.slice(0, 8)}...
|
||||||
|
|
@ -683,11 +927,9 @@ export default function Bridge() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Zap size={15} />
|
<Zap size={15} />
|
||||||
{lang === "zh" ? "一键钱包转账" : "Send via Wallet"}
|
{t.sendViaWallet}
|
||||||
{usdtAmount && Number(usdtAmount) > 0 && (
|
{usdtAmount && Number(usdtAmount) > 0 && (
|
||||||
<span className="text-xs opacity-60">
|
<span className="text-xs opacity-60">{usdtAmount} USDT</span>
|
||||||
{usdtAmount} USDT
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -701,16 +943,66 @@ export default function Bridge() {
|
||||||
<span>{web3Bridge.transferError}</span>
|
<span>{web3Bridge.transferError}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-white/25 text-center">
|
<p className="text-xs text-white/25 text-center">{t.walletSignNote}</p>
|
||||||
{lang === "zh"
|
</div>
|
||||||
? "钱包将弹出签名确认,无需手动复制地址"
|
)}
|
||||||
: "Wallet will prompt for signature — no manual copy needed"}
|
|
||||||
</p>
|
{/* TRON one-click TronLink transfer */}
|
||||||
|
{isTronChain && tron.tronConnected && !registered && (
|
||||||
|
<div className="space-y-2 pt-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
||||||
|
<span className="text-xs text-white/30">{t.orSendDirectly}</span>
|
||||||
|
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSendViaTronLink}
|
||||||
|
disabled={tron.tronTransferring}
|
||||||
|
className="w-full py-3 rounded-xl font-bold text-sm transition-all flex items-center justify-center gap-2"
|
||||||
|
style={{
|
||||||
|
background: tron.tronTransferring
|
||||||
|
? "rgba(255,0,19,0.1)"
|
||||||
|
: "linear-gradient(135deg, rgba(255,0,19,0.15) 0%, rgba(200,0,10,0.25) 100%)",
|
||||||
|
border: "1px solid rgba(255,0,19,0.35)",
|
||||||
|
color: tron.tronTransferring ? "rgba(255,0,19,0.4)" : "#FF0013",
|
||||||
|
cursor: tron.tronTransferring ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tron.tronTransferring ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={15} className="animate-spin" />
|
||||||
|
{t.sending}
|
||||||
|
{tron.tronTransferTxHash && (
|
||||||
|
<span className="text-xs opacity-50 font-mono">
|
||||||
|
{tron.tronTransferTxHash.slice(0, 8)}...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Zap size={15} />
|
||||||
|
{lang === "zh" ? "TronLink 一键转账" : "Send via TronLink"}
|
||||||
|
{usdtAmount && Number(usdtAmount) > 0 && (
|
||||||
|
<span className="text-xs opacity-60">{usdtAmount} USDT</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{tron.tronTransferError && (
|
||||||
|
<div
|
||||||
|
className="rounded-lg p-2.5 text-xs flex items-start gap-2"
|
||||||
|
style={{ background: "rgba(255,80,80,0.08)", border: "1px solid rgba(255,80,80,0.2)", color: "#ff8080" }}
|
||||||
|
>
|
||||||
|
<AlertCircle size={12} className="shrink-0 mt-0.5" />
|
||||||
|
<span>{tron.tronTransferError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-white/25 text-center">{t.walletSignNote}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Confirm button */}
|
{/* Confirm button (manual — no wallet) */}
|
||||||
{!registered ? (
|
{!registered ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleConfirmSend}
|
onClick={handleConfirmSend}
|
||||||
|
|
@ -734,14 +1026,38 @@ export default function Bridge() {
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 text-center space-y-2"
|
className="rounded-xl p-4 space-y-3"
|
||||||
style={{ background: "rgba(0,230,118,0.08)", border: "1px solid rgba(0,230,118,0.25)" }}
|
style={{ background: "rgba(0,230,118,0.08)", border: "1px solid rgba(0,230,118,0.25)" }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2 text-green-400 font-semibold">
|
<div className="flex items-center justify-center gap-2 text-green-400 font-semibold">
|
||||||
<CheckCircle2 size={18} />
|
<CheckCircle2 size={18} />
|
||||||
{t.registered}
|
{t.registered}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-white/50">{t.registeredDesc}</p>
|
<p className="text-xs text-white/50 text-center">{t.registeredDesc}</p>
|
||||||
|
|
||||||
|
{/* Polling indicator */}
|
||||||
|
{pollingRef.current && (
|
||||||
|
<div className="flex items-center justify-center gap-1.5 text-xs text-white/30">
|
||||||
|
<Loader2 size={10} className="animate-spin" />
|
||||||
|
{t.pollingNote}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add XIC to wallet button */}
|
||||||
|
{(wallet.isConnected || tron.tronConnected) && (
|
||||||
|
<button
|
||||||
|
onClick={handleAddXicToWallet}
|
||||||
|
className="w-full py-2.5 rounded-xl text-sm font-semibold transition-all flex items-center justify-center gap-2"
|
||||||
|
style={{
|
||||||
|
background: "rgba(240,180,41,0.12)",
|
||||||
|
border: "1px solid rgba(240,180,41,0.3)",
|
||||||
|
color: "#f0b429",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={14} />
|
||||||
|
{t.addToWallet}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -751,7 +1067,7 @@ export default function Bridge() {
|
||||||
{/* ─── Info Cards ─────────────────────────────────────────────────── */}
|
{/* ─── Info Cards ─────────────────────────────────────────────────── */}
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{[
|
{[
|
||||||
{ label: t.supportedChains, value: "5+", sub: "BSC, ETH, Polygon..." },
|
{ label: t.supportedChains, value: "6+", sub: "BSC, ETH, TRON..." },
|
||||||
{ label: t.tokenPrice, value: "$0.02", sub: t.presalePrice },
|
{ label: t.tokenPrice, value: "$0.02", sub: t.presalePrice },
|
||||||
{ label: t.listingTarget, value: "$0.10", sub: t.xPotential },
|
{ label: t.listingTarget, value: "$0.10", sub: t.xPotential },
|
||||||
].map((card, i) => (
|
].map((card, i) => (
|
||||||
|
|
@ -782,6 +1098,14 @@ export default function Bridge() {
|
||||||
<div className="flex items-center gap-2 text-white/70 font-medium text-sm">
|
<div className="flex items-center gap-2 text-white/70 font-medium text-sm">
|
||||||
<History size={16} />
|
<History size={16} />
|
||||||
{t.history}
|
{t.history}
|
||||||
|
{registered && queryAddress && (
|
||||||
|
<span
|
||||||
|
className="text-xs px-1.5 py-0.5 rounded-full"
|
||||||
|
style={{ background: "rgba(0,212,255,0.15)", color: "#00d4ff" }}
|
||||||
|
>
|
||||||
|
{lang === "zh" ? "监控中" : "Monitoring"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showHistory ? <ChevronUp size={16} className="text-white/40" /> : <ChevronDown size={16} className="text-white/40" />}
|
{showHistory ? <ChevronUp size={16} className="text-white/40" /> : <ChevronDown size={16} className="text-white/40" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -837,7 +1161,6 @@ export default function Bridge() {
|
||||||
{orders.map(order => {
|
{orders.map(order => {
|
||||||
const chain = CHAINS.find(c => c.chainId === order.fromChainId);
|
const chain = CHAINS.find(c => c.chainId === order.fromChainId);
|
||||||
const isIntent = order.type === 'intent';
|
const isIntent = order.type === 'intent';
|
||||||
// Safe field access for both intent and order types
|
|
||||||
const usdtAmt = isIntent
|
const usdtAmt = isIntent
|
||||||
? (order as { type: 'intent'; expectedUsdt: number | null }).expectedUsdt
|
? (order as { type: 'intent'; expectedUsdt: number | null }).expectedUsdt
|
||||||
: (order as { type: 'order'; fromAmount: number }).fromAmount;
|
: (order as { type: 'order'; fromAmount: number }).fromAmount;
|
||||||
|
|
@ -900,10 +1223,26 @@ export default function Bridge() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isIntent && (
|
{isIntent && (
|
||||||
<p className="text-xs text-amber-400/70">
|
<p className="text-xs text-amber-400/70 flex items-center gap-1">
|
||||||
⏳ Monitoring your transfer on {chain?.name ?? `Chain ${order.fromChainId}`}...
|
<Loader2 size={10} className="animate-spin" />
|
||||||
|
Monitoring your transfer on {chain?.name ?? `Chain ${order.fromChainId}`}...
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{/* Add XIC to wallet button for distributed orders */}
|
||||||
|
{order.status === "distributed" && (wallet.isConnected || tron.tronConnected) && (
|
||||||
|
<button
|
||||||
|
onClick={handleAddXicToWallet}
|
||||||
|
className="w-full py-1.5 rounded-lg text-xs font-medium transition-all flex items-center justify-center gap-1.5"
|
||||||
|
style={{
|
||||||
|
background: "rgba(240,180,41,0.1)",
|
||||||
|
border: "1px solid rgba(240,180,41,0.25)",
|
||||||
|
color: "#f0b429",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={11} />
|
||||||
|
{t.addToWallet}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -916,6 +1255,7 @@ export default function Bridge() {
|
||||||
{/* ─── Disclaimer ─────────────────────────────────────────────────── */}
|
{/* ─── Disclaimer ─────────────────────────────────────────────────── */}
|
||||||
<p className="text-xs text-white/25 text-center leading-relaxed">{t.disclaimer}</p>
|
<p className="text-xs text-white/25 text-center leading-relaxed">{t.disclaimer}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ─── Wallet Selector Modal (Portal) ───────────────────────────────── */}
|
{/* ─── Wallet Selector Modal (Portal) ───────────────────────────────── */}
|
||||||
{showWalletSelector && createPortal(
|
{showWalletSelector && createPortal(
|
||||||
<div
|
<div
|
||||||
|
|
@ -952,12 +1292,15 @@ export default function Bridge() {
|
||||||
}
|
}
|
||||||
setXicReceiveAddress(address);
|
setXicReceiveAddress(address);
|
||||||
setShowWalletSelector(false);
|
setShowWalletSelector(false);
|
||||||
toast.success(lang === "zh" ? `钱包已连接: ${address.slice(0, 6)}...${address.slice(-4)}` : `Wallet connected: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
toast.success(lang === "zh"
|
||||||
|
? `钱包已连接: ${address.slice(0, 6)}...${address.slice(-4)}`
|
||||||
|
: `Wallet connected: ${address.slice(0, 6)}...${address.slice(-4)}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)} </div>
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
CREATE TABLE `listener_state` (
|
||||||
|
`id` varchar(32) NOT NULL,
|
||||||
|
`lastBlock` bigint NOT NULL DEFAULT 0,
|
||||||
|
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `listener_state_id` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `transaction_logs` (
|
||||||
|
`id` int AUTO_INCREMENT NOT NULL,
|
||||||
|
`txHash` varchar(128) NOT NULL,
|
||||||
|
`chainType` varchar(16) NOT NULL,
|
||||||
|
`fromAddress` varchar(64) NOT NULL,
|
||||||
|
`toAddress` varchar(64) NOT NULL,
|
||||||
|
`amount` decimal(30,6) NOT NULL,
|
||||||
|
`blockNumber` bigint,
|
||||||
|
`status` int NOT NULL DEFAULT 0,
|
||||||
|
`orderNo` varchar(128),
|
||||||
|
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||||
|
CONSTRAINT `transaction_logs_id` PRIMARY KEY(`id`),
|
||||||
|
CONSTRAINT `transaction_logs_txHash_unique` UNIQUE(`txHash`)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,790 @@
|
||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "a2f8d4a4-e049-4e02-8011-f14d50b32f7e",
|
||||||
|
"prevId": "7e9d948e-d569-4194-bb75-6219b837045e",
|
||||||
|
"tables": {
|
||||||
|
"bridge_intents": {
|
||||||
|
"name": "bridge_intents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"fromChainId": {
|
||||||
|
"name": "fromChainId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"senderAddress": {
|
||||||
|
"name": "senderAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"xicReceiveAddress": {
|
||||||
|
"name": "xicReceiveAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expectedUsdt": {
|
||||||
|
"name": "expectedUsdt",
|
||||||
|
"type": "decimal(20,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"matched": {
|
||||||
|
"name": "matched",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"matchedOrderId": {
|
||||||
|
"name": "matchedOrderId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"bridge_intents_id": {
|
||||||
|
"name": "bridge_intents_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"bridge_orders": {
|
||||||
|
"name": "bridge_orders",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"txHash": {
|
||||||
|
"name": "txHash",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"walletAddress": {
|
||||||
|
"name": "walletAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromChainId": {
|
||||||
|
"name": "fromChainId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromToken": {
|
||||||
|
"name": "fromToken",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromAmount": {
|
||||||
|
"name": "fromAmount",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"toChainId": {
|
||||||
|
"name": "toChainId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 56
|
||||||
|
},
|
||||||
|
"toToken": {
|
||||||
|
"name": "toToken",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'XIC'"
|
||||||
|
},
|
||||||
|
"toAmount": {
|
||||||
|
"name": "toAmount",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"xicReceiveAddress": {
|
||||||
|
"name": "xicReceiveAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('pending','confirmed','distributed','failed')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"confirmedAt": {
|
||||||
|
"name": "confirmedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distributedAt": {
|
||||||
|
"name": "distributedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distributeTxHash": {
|
||||||
|
"name": "distributeTxHash",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"blockNumber": {
|
||||||
|
"name": "blockNumber",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"bridge_orders_id": {
|
||||||
|
"name": "bridge_orders_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"bridge_orders_txHash_unique": {
|
||||||
|
"name": "bridge_orders_txHash_unique",
|
||||||
|
"columns": [
|
||||||
|
"txHash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"listener_state": {
|
||||||
|
"name": "listener_state",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lastBlock": {
|
||||||
|
"name": "lastBlock",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"listener_state_id": {
|
||||||
|
"name": "listener_state_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"presale_config": {
|
||||||
|
"name": "presale_config",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"name": "label",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "varchar(256)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(32)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'text'"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"presale_config_id": {
|
||||||
|
"name": "presale_config_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"presale_config_key_unique": {
|
||||||
|
"name": "presale_config_key_unique",
|
||||||
|
"columns": [
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"presale_stats_cache": {
|
||||||
|
"name": "presale_stats_cache",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"chain": {
|
||||||
|
"name": "chain",
|
||||||
|
"type": "varchar(16)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"usdtRaised": {
|
||||||
|
"name": "usdtRaised",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'0'"
|
||||||
|
},
|
||||||
|
"tokensSold": {
|
||||||
|
"name": "tokensSold",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'0'"
|
||||||
|
},
|
||||||
|
"weiRaised": {
|
||||||
|
"name": "weiRaised",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'0'"
|
||||||
|
},
|
||||||
|
"lastUpdated": {
|
||||||
|
"name": "lastUpdated",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"presale_stats_cache_id": {
|
||||||
|
"name": "presale_stats_cache_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"transaction_logs": {
|
||||||
|
"name": "transaction_logs",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"txHash": {
|
||||||
|
"name": "txHash",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"chainType": {
|
||||||
|
"name": "chainType",
|
||||||
|
"type": "varchar(16)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromAddress": {
|
||||||
|
"name": "fromAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"toAddress": {
|
||||||
|
"name": "toAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"blockNumber": {
|
||||||
|
"name": "blockNumber",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"orderNo": {
|
||||||
|
"name": "orderNo",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"transaction_logs_id": {
|
||||||
|
"name": "transaction_logs_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"transaction_logs_txHash_unique": {
|
||||||
|
"name": "transaction_logs_txHash_unique",
|
||||||
|
"columns": [
|
||||||
|
"txHash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"trc20_intents": {
|
||||||
|
"name": "trc20_intents",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"tronAddress": {
|
||||||
|
"name": "tronAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"evmAddress": {
|
||||||
|
"name": "evmAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"expectedUsdt": {
|
||||||
|
"name": "expectedUsdt",
|
||||||
|
"type": "decimal(20,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"matched": {
|
||||||
|
"name": "matched",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"matchedPurchaseId": {
|
||||||
|
"name": "matchedPurchaseId",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"trc20_intents_id": {
|
||||||
|
"name": "trc20_intents_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"trc20_purchases": {
|
||||||
|
"name": "trc20_purchases",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"txHash": {
|
||||||
|
"name": "txHash",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"fromAddress": {
|
||||||
|
"name": "fromAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"usdtAmount": {
|
||||||
|
"name": "usdtAmount",
|
||||||
|
"type": "decimal(20,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"xicAmount": {
|
||||||
|
"name": "xicAmount",
|
||||||
|
"type": "decimal(30,6)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"blockNumber": {
|
||||||
|
"name": "blockNumber",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "enum('pending','confirmed','distributed','failed')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'pending'"
|
||||||
|
},
|
||||||
|
"distributedAt": {
|
||||||
|
"name": "distributedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"distributeTxHash": {
|
||||||
|
"name": "distributeTxHash",
|
||||||
|
"type": "varchar(128)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"evmAddress": {
|
||||||
|
"name": "evmAddress",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"trc20_purchases_id": {
|
||||||
|
"name": "trc20_purchases_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"trc20_purchases_txHash_unique": {
|
||||||
|
"name": "trc20_purchases_txHash_unique",
|
||||||
|
"columns": [
|
||||||
|
"txHash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"openId": {
|
||||||
|
"name": "openId",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(320)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"loginMethod": {
|
||||||
|
"name": "loginMethod",
|
||||||
|
"type": "varchar(64)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "enum('user','admin')",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"name": "updatedAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"lastSignedIn": {
|
||||||
|
"name": "lastSignedIn",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_openId_unique": {
|
||||||
|
"name": "users_openId_unique",
|
||||||
|
"columns": [
|
||||||
|
"openId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,13 @@
|
||||||
"when": 1773136228889,
|
"when": 1773136228889,
|
||||||
"tag": "0007_wide_menace",
|
"tag": "0007_wide_menace",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1773142627500,
|
||||||
|
"tag": "0008_lowly_pride",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -138,3 +138,32 @@ export const bridgeIntents = mysqlTable("bridge_intents", {
|
||||||
|
|
||||||
export type BridgeIntent = typeof bridgeIntents.$inferSelect;
|
export type BridgeIntent = typeof bridgeIntents.$inferSelect;
|
||||||
export type InsertBridgeIntent = typeof bridgeIntents.$inferInsert;
|
export type InsertBridgeIntent = typeof bridgeIntents.$inferInsert;
|
||||||
|
|
||||||
|
// Transaction logs — idempotency guard to prevent double-processing
|
||||||
|
// Every on-chain transfer (ERC20 or TRC20) is recorded here before processing.
|
||||||
|
// If txHash already exists → skip (prevents double-distribution on re-org or retry).
|
||||||
|
export const transactionLogs = mysqlTable("transaction_logs", {
|
||||||
|
id: int("id").autoincrement().primaryKey(),
|
||||||
|
txHash: varchar("txHash", { length: 128 }).notNull().unique(),
|
||||||
|
chainType: varchar("chainType", { length: 16 }).notNull(), // 'ERC20' | 'TRC20'
|
||||||
|
fromAddress: varchar("fromAddress", { length: 64 }).notNull(),
|
||||||
|
toAddress: varchar("toAddress", { length: 64 }).notNull(),
|
||||||
|
amount: decimal("amount", { precision: 30, scale: 6 }).notNull(),
|
||||||
|
blockNumber: bigint("blockNumber", { mode: "number" }),
|
||||||
|
status: int("status").default(0).notNull(), // 0=unprocessed, 1=processed, 2=no_match
|
||||||
|
orderNo: varchar("orderNo", { length: 128 }), // matched bridge order txHash
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TransactionLog = typeof transactionLogs.$inferSelect;
|
||||||
|
export type InsertTransactionLog = typeof transactionLogs.$inferInsert;
|
||||||
|
|
||||||
|
// Listener state — tracks last processed block per chain
|
||||||
|
// Prevents re-scanning already-processed blocks on restart
|
||||||
|
export const listenerState = mysqlTable("listener_state", {
|
||||||
|
id: varchar("id", { length: 32 }).primaryKey(), // 'erc20' | 'trc20'
|
||||||
|
lastBlock: bigint("lastBlock", { mode: "number" }).notNull().default(0),
|
||||||
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListenerState = typeof listenerState.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { getDb } from "./db";
|
import { getDb } from "./db";
|
||||||
import { bridgeOrders, bridgeIntents } from "../drizzle/schema";
|
import { bridgeOrders, bridgeIntents } from "../drizzle/schema";
|
||||||
import { eq, and, desc } from "drizzle-orm";
|
import { eq, and, desc } from "drizzle-orm";
|
||||||
|
import { creditXic } from "./tokenDistributionService";
|
||||||
|
|
||||||
// ─── Presale Config ───────────────────────────────────────────────────────────
|
// ─── Presale Config ───────────────────────────────────────────────────────────
|
||||||
export const XIC_PRICE_USDT = 0.02; // $0.02 per XIC
|
export const XIC_PRICE_USDT = 0.02; // $0.02 per XIC
|
||||||
|
|
@ -218,7 +219,8 @@ async function processTransfers(chain: ChainConfig): Promise<void> {
|
||||||
|
|
||||||
const xicReceiveAddress = intent.length > 0 ? intent[0].xicReceiveAddress : null;
|
const xicReceiveAddress = intent.length > 0 ? intent[0].xicReceiveAddress : null;
|
||||||
|
|
||||||
// Record the order
|
// Record the order first (pending status)
|
||||||
|
try {
|
||||||
await db.insert(bridgeOrders).values({
|
await db.insert(bridgeOrders).values({
|
||||||
txHash: transfer.txHash,
|
txHash: transfer.txHash,
|
||||||
walletAddress: transfer.fromAddress,
|
walletAddress: transfer.fromAddress,
|
||||||
|
|
@ -229,10 +231,12 @@ async function processTransfers(chain: ChainConfig): Promise<void> {
|
||||||
toToken: "XIC",
|
toToken: "XIC",
|
||||||
toAmount: String(xicAmount),
|
toAmount: String(xicAmount),
|
||||||
xicReceiveAddress,
|
xicReceiveAddress,
|
||||||
status: "confirmed",
|
status: "pending",
|
||||||
confirmedAt: new Date(),
|
|
||||||
blockNumber: transfer.blockNumber,
|
blockNumber: transfer.blockNumber,
|
||||||
});
|
});
|
||||||
|
} catch (insertErr: any) {
|
||||||
|
if (insertErr?.code !== "ER_DUP_ENTRY") throw insertErr;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark intent as matched
|
// Mark intent as matched
|
||||||
if (intent.length > 0) {
|
if (intent.length > 0) {
|
||||||
|
|
@ -242,6 +246,19 @@ async function processTransfers(chain: ChainConfig): Promise<void> {
|
||||||
.where(eq(bridgeIntents.id, intent[0].id));
|
.where(eq(bridgeIntents.id, intent[0].id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use unified tokenDistributionService (idempotent via transaction_logs)
|
||||||
|
await creditXic({
|
||||||
|
txHash: transfer.txHash,
|
||||||
|
chainType: "ERC20",
|
||||||
|
fromAddress: transfer.fromAddress,
|
||||||
|
toAddress: chain.receivingAddress,
|
||||||
|
usdtAmount: transfer.amount,
|
||||||
|
xicAmount,
|
||||||
|
blockNumber: transfer.blockNumber,
|
||||||
|
xicReceiveAddress: xicReceiveAddress ?? undefined,
|
||||||
|
remark: `${chain.name} auto-detected`,
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`[BridgeMonitor] New ${chain.name} deposit: ${transfer.amount} USDT from ${transfer.fromAddress} → ${xicAmount} XIC`);
|
console.log(`[BridgeMonitor] New ${chain.name} deposit: ${transfer.amount} USDT from ${transfer.fromAddress} → ${xicAmount} XIC`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err?.code === "ER_DUP_ENTRY") continue;
|
if (err?.code === "ER_DUP_ENTRY") continue;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { CONTRACTS, TOKEN_PRICE_USDT, HARD_CAP_USDT, MAX_PURCHASE_USDT } from ".
|
||||||
|
|
||||||
describe("Presale Configuration", () => {
|
describe("Presale Configuration", () => {
|
||||||
it("should have correct BSC presale contract address", () => {
|
it("should have correct BSC presale contract address", () => {
|
||||||
expect(CONTRACTS.BSC.presale).toBe("0xc65e7a2738ed884db8d26a6eb2fecf7daca2e90c");
|
expect(CONTRACTS.BSC.presale).toBe("0x5953c025dA734e710886916F2d739A3A78f8bbc4");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct ETH presale contract address", () => {
|
it("should have correct ETH presale contract address", () => {
|
||||||
|
|
|
||||||
|
|
@ -11,46 +11,65 @@ import { z } from "zod";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { notifyDistributed, testTelegramConnection } from "./telegram";
|
import { notifyDistributed, testTelegramConnection } from "./telegram";
|
||||||
import { getAllConfig, getConfig, setConfig, seedDefaultConfig, DEFAULT_CONFIG } from "./configDb";
|
import { getAllConfig, getConfig, setConfig, seedDefaultConfig, DEFAULT_CONFIG } from "./configDb";
|
||||||
|
import { creditXic } from "./tokenDistributionService";
|
||||||
|
|
||||||
// Admin password from env (fallback for development)
|
// Admin password from env (fallback for development)
|
||||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "NACadmin2026!";
|
||||||
|
|
||||||
// ─── Bridge Router ───────────────────────────────────────────────────────────
|
// ─── Bridge Router ───────────────────────────────────────────────────────────
|
||||||
const bridgeRouter = router({
|
const bridgeRouter = router({
|
||||||
// Record a completed Li.Fi cross-chain order
|
// Record a completed cross-chain USDT transfer and credit XIC (idempotent)
|
||||||
recordOrder: publicProcedure
|
recordOrder: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
txHash: z.string().min(1).max(128),
|
txHash: z.string().min(1).max(128),
|
||||||
walletAddress: z.string().min(1).max(64),
|
walletAddress: z.string().min(1).max(64),
|
||||||
fromChainId: z.number().int(),
|
fromChainId: z.number().int(),
|
||||||
fromToken: z.string().max(32),
|
fromToken: z.string().max(32),
|
||||||
fromAmount: z.string(),
|
fromAmount: z.string(), // USDT amount
|
||||||
toChainId: z.number().int(),
|
toChainId: z.number().int(),
|
||||||
toToken: z.string().max(32),
|
toToken: z.string().max(32),
|
||||||
toAmount: z.string(),
|
toAmount: z.string(), // XIC amount
|
||||||
|
xicReceiveAddress: z.string().optional(),
|
||||||
|
chainType: z.enum(["ERC20", "TRC20"]).default("ERC20"),
|
||||||
|
receivingAddress: z.string().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return { success: false, message: "DB unavailable" };
|
if (!db) return { success: false, message: "DB unavailable" };
|
||||||
|
|
||||||
|
// Insert bridge order (pending — creditXic will update to confirmed)
|
||||||
try {
|
try {
|
||||||
await db.insert(bridgeOrders).values({
|
await db.insert(bridgeOrders).values({
|
||||||
txHash: input.txHash,
|
txHash: input.txHash,
|
||||||
walletAddress: input.walletAddress,
|
walletAddress: input.walletAddress.toLowerCase(),
|
||||||
fromChainId: input.fromChainId,
|
fromChainId: input.fromChainId,
|
||||||
fromToken: input.fromToken,
|
fromToken: input.fromToken,
|
||||||
fromAmount: input.fromAmount,
|
fromAmount: input.fromAmount,
|
||||||
toChainId: input.toChainId,
|
toChainId: input.toChainId,
|
||||||
toToken: input.toToken,
|
toToken: input.toToken,
|
||||||
toAmount: input.toAmount,
|
toAmount: input.toAmount,
|
||||||
status: "confirmed" as const,
|
xicReceiveAddress: input.xicReceiveAddress ?? null,
|
||||||
xicReceiveAddress: null,
|
status: "pending" as const,
|
||||||
confirmedAt: new Date(),
|
|
||||||
});
|
});
|
||||||
return { success: true };
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.code === "ER_DUP_ENTRY") return { success: true };
|
if (e?.code !== "ER_DUP_ENTRY") throw e;
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unified credit service (idempotent via transaction_logs)
|
||||||
|
const usdtAmount = parseFloat(input.fromAmount);
|
||||||
|
const xicAmount = parseFloat(input.toAmount);
|
||||||
|
const result = await creditXic({
|
||||||
|
txHash: input.txHash,
|
||||||
|
chainType: input.chainType,
|
||||||
|
fromAddress: input.walletAddress.toLowerCase(),
|
||||||
|
toAddress: input.receivingAddress ?? "0x43DAb577f3279e11D311E7d628C6201d893A9Aa3",
|
||||||
|
usdtAmount,
|
||||||
|
xicAmount,
|
||||||
|
xicReceiveAddress: input.xicReceiveAddress,
|
||||||
|
remark: `Frontend recordOrder (${input.chainType})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: result.success, alreadyProcessed: result.alreadyProcessed };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// List orders by wallet address (includes pending intents + confirmed orders)
|
// List orders by wallet address (includes pending intents + confirmed orders)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
/**
|
||||||
|
* tokenDistributionService.ts
|
||||||
|
*
|
||||||
|
* Unified token distribution service for NAC XIC presale.
|
||||||
|
* ALL payment channels (USDT ERC20, USDT TRC20, Alipay, WeChat, PayPal)
|
||||||
|
* call the same credit() method to distribute XIC tokens.
|
||||||
|
*
|
||||||
|
* Architecture per document: "加密货币支付框架扩展方案(支付宝/微信/PayPal集成)"
|
||||||
|
* - Centralized distribution logic prevents inconsistencies across payment channels
|
||||||
|
* - Idempotency: check transaction_logs before processing to prevent double-distribution
|
||||||
|
* - All payment channels update orders table to PAID/COMPLETED status
|
||||||
|
*
|
||||||
|
* Future extension: When XIC is deployed on-chain, replace the internal credit()
|
||||||
|
* with transferOnChain() which calls the XIC contract transfer() function.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getDb } from "./db";
|
||||||
|
import { bridgeOrders, transactionLogs } from "../drizzle/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export interface CreditParams {
|
||||||
|
/** Bridge order txHash (unique identifier for this payment) */
|
||||||
|
txHash: string;
|
||||||
|
/** Chain type: 'ERC20' | 'TRC20' | 'ALIPAY' | 'WECHAT' | 'PAYPAL' */
|
||||||
|
chainType: string;
|
||||||
|
/** Sender address on source chain */
|
||||||
|
fromAddress: string;
|
||||||
|
/** Our receiving address on source chain */
|
||||||
|
toAddress: string;
|
||||||
|
/** USDT amount received */
|
||||||
|
usdtAmount: number;
|
||||||
|
/** XIC amount to distribute (calculated from usdtAmount / XIC_PRICE) */
|
||||||
|
xicAmount: number;
|
||||||
|
/** Block number (for on-chain transactions) */
|
||||||
|
blockNumber?: number;
|
||||||
|
/** XIC receive address (BSC address for EVM distribution) */
|
||||||
|
xicReceiveAddress?: string;
|
||||||
|
/** Remark for logging */
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreditResult {
|
||||||
|
success: boolean;
|
||||||
|
alreadyProcessed?: boolean;
|
||||||
|
orderId?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit XIC tokens to a user after successful payment.
|
||||||
|
*
|
||||||
|
* This is the SINGLE entry point for all payment channels.
|
||||||
|
* Implements idempotency via transaction_logs table.
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Check transaction_logs — if txHash exists, skip (already processed)
|
||||||
|
* 2. Record in transaction_logs (status=1 processed)
|
||||||
|
* 3. Find matching bridge order and update status to 'confirmed'
|
||||||
|
* 4. Mark order as 'distributed' (Phase 2: will call on-chain transfer)
|
||||||
|
* 5. Return success
|
||||||
|
*/
|
||||||
|
export async function creditXic(params: CreditParams): Promise<CreditResult> {
|
||||||
|
const {
|
||||||
|
txHash,
|
||||||
|
chainType,
|
||||||
|
fromAddress,
|
||||||
|
toAddress,
|
||||||
|
usdtAmount,
|
||||||
|
xicAmount,
|
||||||
|
blockNumber,
|
||||||
|
xicReceiveAddress,
|
||||||
|
remark,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return { success: false, error: "Database not available" };
|
||||||
|
|
||||||
|
// Step 1: Idempotency check — has this txHash been processed before?
|
||||||
|
const existing = await db.select().from(transactionLogs)
|
||||||
|
.where(eq(transactionLogs.txHash, txHash))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing.length > 0) {
|
||||||
|
console.log(`[TokenDistribution] txHash ${txHash} already processed (status=${existing[0].status}), skipping`);
|
||||||
|
return { success: true, alreadyProcessed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Find matching bridge order
|
||||||
|
const orders = await db.select().from(bridgeOrders)
|
||||||
|
.where(eq(bridgeOrders.txHash, txHash))
|
||||||
|
.limit(1);
|
||||||
|
const order = orders[0] ?? null;
|
||||||
|
|
||||||
|
// Step 3: Record in transaction_logs (idempotency guard)
|
||||||
|
await db.insert(transactionLogs).values({
|
||||||
|
txHash,
|
||||||
|
chainType,
|
||||||
|
fromAddress,
|
||||||
|
toAddress,
|
||||||
|
amount: usdtAmount.toString(),
|
||||||
|
blockNumber: blockNumber ?? null,
|
||||||
|
status: 1, // processed
|
||||||
|
orderNo: txHash, // use txHash as orderNo for bridge orders
|
||||||
|
});
|
||||||
|
|
||||||
|
if (order) {
|
||||||
|
// Step 4: Update bridge order status to 'confirmed' then 'distributed'
|
||||||
|
await db.update(bridgeOrders)
|
||||||
|
.set({
|
||||||
|
status: "confirmed",
|
||||||
|
confirmedAt: new Date(),
|
||||||
|
blockNumber: blockNumber ?? null,
|
||||||
|
})
|
||||||
|
.where(eq(bridgeOrders.id, order.id));
|
||||||
|
|
||||||
|
// Phase 1: Internal credit (record as distributed)
|
||||||
|
// Phase 2 (future): Call XIC contract transfer() on BSC
|
||||||
|
await db.update(bridgeOrders)
|
||||||
|
.set({
|
||||||
|
status: "distributed",
|
||||||
|
distributedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(bridgeOrders.id, order.id));
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[TokenDistribution] ✅ Credited ${xicAmount} XIC for order ${txHash} ` +
|
||||||
|
`(${chainType}, ${usdtAmount} USDT from ${fromAddress}) ` +
|
||||||
|
`→ ${xicReceiveAddress || order.xicReceiveAddress || "unknown"} | ${remark || ""}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, orderId: order.id };
|
||||||
|
} else {
|
||||||
|
// No matching order found — log as unmatched but don't fail
|
||||||
|
// Admin can manually match later via the admin panel
|
||||||
|
console.warn(
|
||||||
|
`[TokenDistribution] ⚠️ No matching bridge order for txHash ${txHash} ` +
|
||||||
|
`(${chainType}, ${usdtAmount} USDT from ${fromAddress} to ${toAddress})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update transaction log status to 2 (no_match)
|
||||||
|
await db.update(transactionLogs)
|
||||||
|
.set({ status: 2 })
|
||||||
|
.where(eq(transactionLogs.txHash, txHash));
|
||||||
|
|
||||||
|
return { success: false, error: "No matching bridge order found" };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[TokenDistribution] ❌ Error processing txHash ${txHash}:`, err);
|
||||||
|
return { success: false, error: String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate XIC amount from USDT amount.
|
||||||
|
* XIC price: $0.02 per XIC → 1 USDT = 50 XIC
|
||||||
|
*/
|
||||||
|
export function calcXicAmount(usdtAmount: number): number {
|
||||||
|
const XIC_PRICE = 0.02; // $0.02 per XIC
|
||||||
|
return Math.floor(usdtAmount / XIC_PRICE);
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import { getDb } from "./db";
|
||||||
import { trc20Purchases, trc20Intents } from "../drizzle/schema";
|
import { trc20Purchases, trc20Intents } from "../drizzle/schema";
|
||||||
import { TOKEN_PRICE_USDT } from "./onchain";
|
import { TOKEN_PRICE_USDT } from "./onchain";
|
||||||
import { notifyNewTRC20Purchase } from "./telegram";
|
import { notifyNewTRC20Purchase } from "./telegram";
|
||||||
|
import { creditXic } from "./tokenDistributionService";
|
||||||
|
|
||||||
const TRON_RECEIVING_ADDRESS = "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp";
|
const TRON_RECEIVING_ADDRESS = "TWc2ugYBFN5aSoimAh4qGt9oMyket6NYZp";
|
||||||
const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
|
const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
|
||||||
|
|
@ -130,6 +131,18 @@ async function processTransaction(tx: TronTransaction): Promise<void> {
|
||||||
console.warn("[TRC20Monitor] Telegram notification failed:", e);
|
console.warn("[TRC20Monitor] Telegram notification failed:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use unified tokenDistributionService (idempotent via transaction_logs)
|
||||||
|
await creditXic({
|
||||||
|
txHash: tx.transaction_id,
|
||||||
|
chainType: "TRC20",
|
||||||
|
fromAddress: tx.from,
|
||||||
|
toAddress: TRON_RECEIVING_ADDRESS,
|
||||||
|
usdtAmount,
|
||||||
|
xicAmount,
|
||||||
|
xicReceiveAddress: matchedEvmAddress ?? undefined,
|
||||||
|
remark: "TRC20 auto-detected",
|
||||||
|
});
|
||||||
|
|
||||||
// Attempt auto-distribution via BSC
|
// Attempt auto-distribution via BSC
|
||||||
await attemptAutoDistribute(tx.transaction_id, tx.from, xicAmount, matchedEvmAddress);
|
await attemptAutoDistribute(tx.transaction_id, tx.from, xicAmount, matchedEvmAddress);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
todo.md
36
todo.md
|
|
@ -198,3 +198,39 @@
|
||||||
- [x] 链不匹配时自动触发switchNetwork
|
- [x] 链不匹配时自动触发switchNetwork
|
||||||
- [ ] 构建部署到AI服务器并测试
|
- [ ] 构建部署到AI服务器并测试
|
||||||
- [ ] 同步到备份Git库
|
- [ ] 同步到备份Git库
|
||||||
|
|
||||||
|
## v16 代币购买无缝体验(按文档方案)
|
||||||
|
|
||||||
|
- [ ] 购买成功后自动调用 wallet_watchAsset 将 XIC 代币添加到 EVM 钱包(EIP-1193标准)
|
||||||
|
- [ ] 添加 TRON 链到 CHAINS 数组(chainId 728126428,TRC20 USDT 6位小数)
|
||||||
|
- [ ] Bridge 页面:TRON 链选中时显示 TronLink 连接按钮(showTron=true)
|
||||||
|
- [ ] 创建 useTronBridge hook:TronLink 连接 + TRC20 USDT 余额查询 + 一键转账
|
||||||
|
- [ ] Bridge 页面:TRON 链一键转账成功后调用 tronLink wallet_addAsset
|
||||||
|
- [ ] 添加订单状态轮询(注册成功后每 5 秒自动刷新订单列表)
|
||||||
|
- [ ] 修复钱包连接被拒绝后无法重试(error 4001 状态重置)
|
||||||
|
- [ ] 部署到 AI 服务器(43.224.155.27)并同步 Git 库
|
||||||
|
|
||||||
|
## v16 wallet_watchAsset 修复(按文档方案)
|
||||||
|
|
||||||
|
- [ ] 修复 addTokenToWallet.ts:TRON 改用 tronWeb.request({ method: 'wallet_watchAsset', params: { type: 'trc20', options: { address } } })
|
||||||
|
- [ ] 修复 addXicToEvmWallet:直接用 window.ethereum.request 而非通过 provider.send 包装
|
||||||
|
- [ ] 去除 const.ts 中的 manus.im 硬编码(改为纯环境变量,无 fallback)
|
||||||
|
- [ ] 构建生产版本并验证无 manus.im 内联
|
||||||
|
- [ ] 部署到 AI 服务器 43.224.155.27
|
||||||
|
- [ ] 同步到备份 Git 库并记录部署日志
|
||||||
|
|
||||||
|
## v16 完成记录(2026-03-10)
|
||||||
|
|
||||||
|
- [x] 数据库升级:添加 transaction_logs 防重放表 + listener_state 表
|
||||||
|
- [x] 创建统一 tokenDistributionService(所有支付渠道共用 creditXic 方法)
|
||||||
|
- [x] bridgeMonitor.ts 集成 tokenDistributionService
|
||||||
|
- [x] trc20Monitor.ts 集成 tokenDistributionService
|
||||||
|
- [x] routers.ts recordOrder 路由集成 tokenDistributionService
|
||||||
|
- [x] addTokenToWallet.ts 按文档规范重写(EVM: window.ethereum,TRON: tronWeb.request wallet_watchAsset)
|
||||||
|
- [x] Bridge.tsx 添加 TRON 链(chainId: 728126428)
|
||||||
|
- [x] Bridge.tsx 集成 useTronBridge(TronLink 连接 + TRC20 转账)
|
||||||
|
- [x] Bridge.tsx 订单状态轮询(注册后每 5 秒刷新)
|
||||||
|
- [x] Bridge.tsx wallet_watchAsset 购买成功后自动添加 XIC 代币
|
||||||
|
- [x] 去除前端 bundle 中的 manus.im 内联
|
||||||
|
- [x] 全部 18 个 vitest 测试通过
|
||||||
|
- [x] 浏览器测试:Bridge 页面、主页、语言切换、Connect Wallet 模态框、TRX 链切换 — 全部通过
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue