Checkpoint: v15: Bridge页面Web3.js集成 - 1) 新建useBridgeWeb3 hook(ethers.js v6),实现USDT余额查询和链上transfer()签名;2) Bridge页面Step 4区域新增USDT余额显示(连接钱包后自动获取,支持手动刷新);3) 新增"Send via Wallet"一键转账按钮,用户无需手动复制地址,直接通过钱包签名发起USDT转账;4) 转账成功后自动调用bridge.recordOrder记录到后端;5) 链不匹配时自动触发switchNetwork;6) 完整错误处理(用户取消/余额不足/网络错误)
This commit is contained in:
parent
84dd7d288f
commit
31a798a9ea
|
|
@ -0,0 +1,186 @@
|
||||||
|
// NAC XIC Presale — Bridge Web3 Hook
|
||||||
|
// Provides USDT balance query and on-chain USDT transfer via connected wallet
|
||||||
|
// Uses ethers.js v6 (already installed)
|
||||||
|
|
||||||
|
import { useState, useCallback, useEffect } from "react";
|
||||||
|
import { Contract, parseUnits, formatUnits, BrowserProvider, JsonRpcSigner } from "ethers";
|
||||||
|
|
||||||
|
// Minimal ERC-20 ABI for USDT operations
|
||||||
|
const ERC20_ABI = [
|
||||||
|
"function balanceOf(address owner) view returns (uint256)",
|
||||||
|
"function decimals() view returns (uint8)",
|
||||||
|
"function symbol() view returns (string)",
|
||||||
|
"function transfer(address to, uint256 amount) returns (bool)",
|
||||||
|
];
|
||||||
|
|
||||||
|
// USDT contract addresses per chain
|
||||||
|
const USDT_CONTRACTS: Record<number, string> = {
|
||||||
|
56: "0x55d398326f99059fF775485246999027B3197955", // BSC USDT (BEP-20, 18 decimals)
|
||||||
|
1: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // ETH USDT (ERC-20, 6 decimals)
|
||||||
|
137: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", // Polygon USDT (6 decimals)
|
||||||
|
42161: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // Arbitrum USDT (6 decimals)
|
||||||
|
43114: "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", // Avalanche USDT (6 decimals)
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BridgeWeb3State {
|
||||||
|
usdtBalance: string | null; // formatted balance e.g. "1234.56"
|
||||||
|
usdtBalanceLoading: boolean;
|
||||||
|
transferring: boolean;
|
||||||
|
transferError: string | null;
|
||||||
|
transferTxHash: string | null;
|
||||||
|
transferSuccess: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseBridgeWeb3Return extends BridgeWeb3State {
|
||||||
|
fetchUsdtBalance: () => Promise<void>;
|
||||||
|
sendUsdtTransfer: (params: {
|
||||||
|
toAddress: string;
|
||||||
|
usdtAmount: number;
|
||||||
|
chainId: number;
|
||||||
|
decimals: number;
|
||||||
|
}) => Promise<{ txHash: string } | null>;
|
||||||
|
resetTransferState: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBridgeWeb3(
|
||||||
|
provider: BrowserProvider | null,
|
||||||
|
signer: JsonRpcSigner | null,
|
||||||
|
address: string | null,
|
||||||
|
chainId: number | null
|
||||||
|
): UseBridgeWeb3Return {
|
||||||
|
const [state, setState] = useState<BridgeWeb3State>({
|
||||||
|
usdtBalance: null,
|
||||||
|
usdtBalanceLoading: false,
|
||||||
|
transferring: false,
|
||||||
|
transferError: null,
|
||||||
|
transferTxHash: null,
|
||||||
|
transferSuccess: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch USDT balance for current chain
|
||||||
|
const fetchUsdtBalance = useCallback(async () => {
|
||||||
|
if (!provider || !address || !chainId) {
|
||||||
|
setState(s => ({ ...s, usdtBalance: null }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const usdtAddr = USDT_CONTRACTS[chainId];
|
||||||
|
if (!usdtAddr) {
|
||||||
|
setState(s => ({ ...s, usdtBalance: null }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(s => ({ ...s, usdtBalanceLoading: true }));
|
||||||
|
try {
|
||||||
|
const contract = new Contract(usdtAddr, ERC20_ABI, provider);
|
||||||
|
const [rawBalance, decimals] = await Promise.all([
|
||||||
|
contract.balanceOf(address),
|
||||||
|
contract.decimals(),
|
||||||
|
]);
|
||||||
|
const formatted = formatUnits(rawBalance, decimals);
|
||||||
|
const display = parseFloat(formatted).toFixed(2);
|
||||||
|
setState(s => ({ ...s, usdtBalance: display, usdtBalanceLoading: false }));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[useBridgeWeb3] fetchUsdtBalance error:", err);
|
||||||
|
setState(s => ({ ...s, usdtBalance: null, usdtBalanceLoading: false }));
|
||||||
|
}
|
||||||
|
}, [provider, address, chainId]);
|
||||||
|
|
||||||
|
// Auto-fetch balance when wallet/chain changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (address && chainId && provider) {
|
||||||
|
fetchUsdtBalance();
|
||||||
|
} else {
|
||||||
|
setState(s => ({ ...s, usdtBalance: null }));
|
||||||
|
}
|
||||||
|
}, [address, chainId, provider, fetchUsdtBalance]);
|
||||||
|
|
||||||
|
// Send USDT transfer via wallet signature
|
||||||
|
const sendUsdtTransfer = useCallback(async ({
|
||||||
|
toAddress,
|
||||||
|
usdtAmount,
|
||||||
|
chainId: targetChainId,
|
||||||
|
decimals,
|
||||||
|
}: {
|
||||||
|
toAddress: string;
|
||||||
|
usdtAmount: number;
|
||||||
|
chainId: number;
|
||||||
|
decimals: number;
|
||||||
|
}): Promise<{ txHash: string } | null> => {
|
||||||
|
if (!signer || !address) {
|
||||||
|
setState(s => ({ ...s, transferError: "Wallet not connected" }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const usdtAddr = USDT_CONTRACTS[targetChainId];
|
||||||
|
if (!usdtAddr) {
|
||||||
|
setState(s => ({ ...s, transferError: `USDT not supported on chain ${targetChainId}` }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
transferring: true,
|
||||||
|
transferError: null,
|
||||||
|
transferTxHash: null,
|
||||||
|
transferSuccess: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contract = new Contract(usdtAddr, ERC20_ABI, signer);
|
||||||
|
const amountWei = parseUnits(usdtAmount.toString(), decimals);
|
||||||
|
|
||||||
|
// Send transfer transaction — wallet will prompt for signature
|
||||||
|
const tx = await contract.transfer(toAddress, amountWei);
|
||||||
|
const txHash: string = tx.hash;
|
||||||
|
|
||||||
|
setState(s => ({ ...s, transferTxHash: txHash }));
|
||||||
|
|
||||||
|
// Wait for 1 confirmation
|
||||||
|
await tx.wait(1);
|
||||||
|
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
transferring: false,
|
||||||
|
transferSuccess: true,
|
||||||
|
transferTxHash: txHash,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh balance after transfer
|
||||||
|
setTimeout(() => fetchUsdtBalance(), 2000);
|
||||||
|
|
||||||
|
return { txHash };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { code?: number; message?: string };
|
||||||
|
let msg: string;
|
||||||
|
if (
|
||||||
|
error?.code === 4001 ||
|
||||||
|
error?.message?.toLowerCase().includes("user rejected") ||
|
||||||
|
error?.message?.toLowerCase().includes("user denied") ||
|
||||||
|
error?.message?.toLowerCase().includes("cancelled")
|
||||||
|
) {
|
||||||
|
msg = "Transaction cancelled by user";
|
||||||
|
} else if (error?.message?.toLowerCase().includes("insufficient")) {
|
||||||
|
msg = "Insufficient USDT balance or gas fee";
|
||||||
|
} else {
|
||||||
|
msg = error?.message || "Transaction failed";
|
||||||
|
}
|
||||||
|
setState(s => ({ ...s, transferring: false, transferError: msg }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [signer, address, fetchUsdtBalance]);
|
||||||
|
|
||||||
|
const resetTransferState = useCallback(() => {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
transferring: false,
|
||||||
|
transferError: null,
|
||||||
|
transferTxHash: null,
|
||||||
|
transferSuccess: false,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchUsdtBalance,
|
||||||
|
sendUsdtTransfer,
|
||||||
|
resetTransferState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,12 @@ 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
|
Wallet, AlertCircle, CheckCircle2, Clock, XCircle, Zap
|
||||||
} 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";
|
||||||
|
|
||||||
// ─── Language ─────────────────────────────────────────────────────────────────
|
// ─── Language ─────────────────────────────────────────────────────────────────
|
||||||
type Lang = "zh" | "en";
|
type Lang = "zh" | "en";
|
||||||
|
|
@ -231,6 +232,14 @@ export default function Bridge() {
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
const [showWalletSelector, setShowWalletSelector] = useState(false);
|
const [showWalletSelector, setShowWalletSelector] = useState(false);
|
||||||
|
|
||||||
|
// Web3 bridge: USDT balance + on-chain transfer
|
||||||
|
const web3Bridge = useBridgeWeb3(
|
||||||
|
wallet.provider,
|
||||||
|
wallet.signer,
|
||||||
|
wallet.address,
|
||||||
|
wallet.chainId
|
||||||
|
);
|
||||||
|
|
||||||
// Auto-fill XIC receive address from connected wallet
|
// Auto-fill XIC receive address from connected wallet
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wallet.address && !xicReceiveAddress) {
|
if (wallet.address && !xicReceiveAddress) {
|
||||||
|
|
@ -240,6 +249,8 @@ export default function Bridge() {
|
||||||
|
|
||||||
// tRPC: register bridge intent
|
// tRPC: register bridge intent
|
||||||
const registerIntent = trpc.bridge.registerIntent.useMutation();
|
const registerIntent = trpc.bridge.registerIntent.useMutation();
|
||||||
|
// tRPC: record completed on-chain order
|
||||||
|
const recordOrder = trpc.bridge.recordOrder.useMutation();
|
||||||
|
|
||||||
// tRPC: query orders by wallet address
|
// tRPC: query orders by wallet address
|
||||||
const { data: orders, isLoading: ordersLoading, refetch: refetchOrders } = trpc.bridge.myOrders.useQuery(
|
const { data: orders, isLoading: ordersLoading, refetch: refetchOrders } = trpc.bridge.myOrders.useQuery(
|
||||||
|
|
@ -267,6 +278,71 @@ export default function Bridge() {
|
||||||
setTimeout(() => setCopiedHash(null), 2000);
|
setTimeout(() => setCopiedHash(null), 2000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle on-chain USDT transfer via wallet (Web3)
|
||||||
|
const handleSendViaWallet = 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 (!wallet.isConnected || !wallet.signer) {
|
||||||
|
toast.error(lang === "zh" ? "请先连接钱包" : "Please connect wallet first");
|
||||||
|
setShowWalletSelector(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check if on correct chain
|
||||||
|
if (wallet.chainId !== selectedChain.chainId) {
|
||||||
|
toast.error(lang === "zh"
|
||||||
|
? `请切换到 ${selectedChain.name} 网络`
|
||||||
|
: `Please switch to ${selectedChain.name} network`);
|
||||||
|
await wallet.switchNetwork(selectedChain.chainId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
web3Bridge.resetTransferState();
|
||||||
|
const result = await web3Bridge.sendUsdtTransfer({
|
||||||
|
toAddress: selectedChain.receivingAddress,
|
||||||
|
usdtAmount: amount,
|
||||||
|
chainId: selectedChain.chainId,
|
||||||
|
decimals: selectedChain.usdtDecimals,
|
||||||
|
});
|
||||||
|
if (result?.txHash) {
|
||||||
|
// Record the completed on-chain order
|
||||||
|
try {
|
||||||
|
await recordOrder.mutateAsync({
|
||||||
|
txHash: result.txHash,
|
||||||
|
walletAddress: wallet.address || xicReceiveAddress,
|
||||||
|
fromChainId: selectedChain.chainId,
|
||||||
|
fromToken: "USDT",
|
||||||
|
fromAmount: amount.toString(),
|
||||||
|
toChainId: 56, // XIC is on BSC
|
||||||
|
toToken: "XIC",
|
||||||
|
toAmount: (amount / XIC_PRICE).toFixed(0),
|
||||||
|
});
|
||||||
|
setRegistered(true);
|
||||||
|
toast.success(lang === "zh"
|
||||||
|
? `转账成功!TX: ${result.txHash.slice(0, 10)}...`
|
||||||
|
: `Transfer sent! TX: ${result.txHash.slice(0, 10)}...`);
|
||||||
|
setQueryAddress(xicReceiveAddress || wallet.address || "");
|
||||||
|
setHistoryAddress(xicReceiveAddress || wallet.address || "");
|
||||||
|
} catch {
|
||||||
|
setRegistered(true);
|
||||||
|
toast.success(lang === "zh"
|
||||||
|
? `转账已发送!TX: ${result.txHash.slice(0, 10)}...`
|
||||||
|
: `Transfer sent! TX: ${result.txHash.slice(0, 10)}...`);
|
||||||
|
}
|
||||||
|
} else if (web3Bridge.transferError) {
|
||||||
|
toast.error(web3Bridge.transferError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Validate and register intent
|
// Validate and register intent
|
||||||
const handleConfirmSend = async () => {
|
const handleConfirmSend = async () => {
|
||||||
const amount = Number(usdtAmount);
|
const amount = Number(usdtAmount);
|
||||||
|
|
@ -509,21 +585,47 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step 4: Receiving address */}
|
{/* Step 4: Receiving address + Web3 transfer */}
|
||||||
<div
|
<div
|
||||||
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)" }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
{/* Header row with chain badge and USDT balance */}
|
||||||
<span className="text-amber-400 text-sm font-semibold">{t.sendTo}</span>
|
<div className="flex items-center justify-between">
|
||||||
<span
|
<div className="flex items-center gap-2">
|
||||||
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
<span className="text-amber-400 text-sm font-semibold">{t.sendTo}</span>
|
||||||
style={{ background: `${selectedChain.color}22`, color: selectedChain.color }}
|
<span
|
||||||
>
|
className="text-xs px-2 py-0.5 rounded-full font-medium"
|
||||||
{selectedChain.icon} {selectedChain.name}
|
style={{ background: `${selectedChain.color}22`, color: selectedChain.color }}
|
||||||
</span>
|
>
|
||||||
|
{selectedChain.icon} {selectedChain.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* USDT Balance display */}
|
||||||
|
{wallet.isConnected && (
|
||||||
|
<div className="flex items-center gap-1 text-xs">
|
||||||
|
{web3Bridge.usdtBalanceLoading ? (
|
||||||
|
<Loader2 size={10} className="animate-spin text-white/40" />
|
||||||
|
) : web3Bridge.usdtBalance !== null ? (
|
||||||
|
<span className="text-white/50">
|
||||||
|
{lang === "zh" ? "余额" : "Bal"}:
|
||||||
|
<span className="text-amber-300 font-mono ml-1">{web3Bridge.usdtBalance} USDT</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
onClick={() => web3Bridge.fetchUsdtBalance()}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/* Receiving address with copy */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
|
className="flex items-center gap-2 p-3 rounded-lg cursor-pointer hover:bg-white/5 transition-colors"
|
||||||
style={{ background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)" }}
|
style={{ background: "rgba(0,0,0,0.3)", border: "1px solid rgba(255,255,255,0.1)" }}
|
||||||
|
|
@ -546,6 +648,66 @@ export default function Bridge() {
|
||||||
{copied ? t.copied : t.copyAddress}
|
{copied ? t.copied : t.copyAddress}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* One-click wallet transfer — shown when wallet connected and not yet registered */}
|
||||||
|
{wallet.isConnected && !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">{lang === "zh" ? "或一键转账" : "or send directly"}</span>
|
||||||
|
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSendViaWallet}
|
||||||
|
disabled={web3Bridge.transferring}
|
||||||
|
className="w-full py-3 rounded-xl font-bold text-sm transition-all flex items-center justify-center gap-2"
|
||||||
|
style={{
|
||||||
|
background: web3Bridge.transferring
|
||||||
|
? "rgba(0,212,255,0.1)"
|
||||||
|
: "linear-gradient(135deg, rgba(0,212,255,0.15) 0%, rgba(0,150,200,0.25) 100%)",
|
||||||
|
border: "1px solid rgba(0,212,255,0.35)",
|
||||||
|
color: web3Bridge.transferring ? "rgba(0,212,255,0.4)" : "#00d4ff",
|
||||||
|
cursor: web3Bridge.transferring ? "not-allowed" : "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{web3Bridge.transferring ? (
|
||||||
|
<>
|
||||||
|
<Loader2 size={15} className="animate-spin" />
|
||||||
|
{lang === "zh" ? "转账中...请在钱包确认" : "Sending... confirm in wallet"}
|
||||||
|
{web3Bridge.transferTxHash && (
|
||||||
|
<span className="text-xs opacity-50 font-mono">
|
||||||
|
{web3Bridge.transferTxHash.slice(0, 8)}...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Zap size={15} />
|
||||||
|
{lang === "zh" ? "一键钱包转账" : "Send via Wallet"}
|
||||||
|
{usdtAmount && Number(usdtAmount) > 0 && (
|
||||||
|
<span className="text-xs opacity-60">
|
||||||
|
{usdtAmount} USDT
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{web3Bridge.transferError && (
|
||||||
|
<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>{web3Bridge.transferError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-white/25 text-center">
|
||||||
|
{lang === "zh"
|
||||||
|
? "钱包将弹出签名确认,无需手动复制地址"
|
||||||
|
: "Wallet will prompt for signature — no manual copy needed"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Confirm button */}
|
{/* Confirm button */}
|
||||||
|
|
|
||||||
13
todo.md
13
todo.md
|
|
@ -185,3 +185,16 @@
|
||||||
- [x] 管理员后台添加Bridge订单管理页面(Bridge Intents + Bridge Orders表格,状态过滤,手动标记分发)
|
- [x] 管理员后台添加Bridge订单管理页面(Bridge Intents + Bridge Orders表格,状态过滤,手动标记分发)
|
||||||
- [ ] 构建部署到AI服务器并测试
|
- [ ] 构建部署到AI服务器并测试
|
||||||
- [ ] 同步到备份Git库
|
- [ ] 同步到备份Git库
|
||||||
|
|
||||||
|
## v15 Web3.js集成完成记录
|
||||||
|
|
||||||
|
- [x] ethers.js v6 已集成(已有依赖)
|
||||||
|
- [x] 创建 useBridgeWeb3 hook:USDT余额查询、链上转账签名(client/src/hooks/useBridgeWeb3.ts)
|
||||||
|
- [x] Bridge页面:连接钱包后在Step 4区域显示USDT余额(右上角余额+刷新按钮)
|
||||||
|
- [x] Bridge页面:新增"Send via Wallet"按钮,调用ethers.js发起真实USDT transfer()
|
||||||
|
- [x] Bridge页面:转账成功后自动调用bridge.recordOrder记录到后端数据库
|
||||||
|
- [x] 转账中显示加载动画和TX Hash前缀
|
||||||
|
- [x] 转账失败显示错误信息(用户取消/余额不足/其他错误)
|
||||||
|
- [x] 链不匹配时自动触发switchNetwork
|
||||||
|
- [ ] 构建部署到AI服务器并测试
|
||||||
|
- [ ] 同步到备份Git库
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue