xtraderClient/sdk/approve.ts
2026-03-09 15:08:09 +08:00

358 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 跨链USDT授权核心方法 - 修复版无需外部ethers库
// 适用于支持EIP-1193标准的钱包如MetaMask
interface ChainConfig {
chainId: string;
name: string;
usdtAddress: string;
rpcUrl: string;
explorer: string;
}
// 链配置
const chains: Record<string, ChainConfig> = {
eth: {
chainId: '0x1', // Ethereum Mainnet
name: 'Ethereum',
usdtAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
rpcUrl: 'https://mainnet.infura.io/v3/',
explorer: 'https://etherscan.io'
},
bnb: {
chainId: '0x38', // Binance Smart Chain Mainnet
name: 'Binance Smart Chain',
usdtAddress: '0x55d398326f99059fF775485246999027B3197955',
rpcUrl: 'https://bsc-dataseed.binance.org/',
explorer: 'https://bscscan.com'
},
polygon: {
chainId: '0x89', // Polygon Mainnet
name: 'Polygon',
usdtAddress: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
rpcUrl: 'https://polygon-rpc.com/',
explorer: 'https://polygonscan.com'
}
};
// USDT ABI
const USDT_ABI = [
"function approve(address spender, uint256 value) public returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function balanceOf(address owner) view returns (uint256)"
];
// 将数字转换为十六进制字符串
function numberToHex(num: number | string): string {
return '0x' + BigInt(num).toString(16);
}
// 将十进制数字转换为带指定小数位的整数形式
function parseUnits(value: string, decimals: number): string {
const [integerPart = '0', decimalPart = ''] = value.split('.');
let result = integerPart.replace(/^0+/, '') || '0'; // 移除前导零
if (decimalPart.length > decimals) {
throw new Error(`小数位数超过限制: ${decimals}`);
}
const paddedDecimal = decimalPart.padEnd(decimals, '0');
result += paddedDecimal;
return result;
}
// 验证钱包连接
function validateWallet(): boolean {
if (typeof window === 'undefined' || typeof (window as any).ethereum === 'undefined') {
throw new Error('请在支持以太坊的钱包浏览器中运行此代码');
}
return true;
}
// 获取当前钱包账户
async function getAccount(): Promise<string> {
try {
const accounts = await (window as any).ethereum.request({
method: 'eth_requestAccounts'
});
return accounts[0];
} catch (error) {
console.error('用户拒绝授权:', error);
throw error;
}
}
// 获取当前网络ID
async function getCurrentNetworkId(): Promise<string> {
try {
const chainId = await (window as any).ethereum.request({
method: 'eth_chainId'
});
return chainId;
} catch (error) {
console.error('获取网络ID失败:', error);
throw error;
}
}
// 切换网络
async function switchNetwork(chainId: string): Promise<void> {
const chainName = Object.values(chains).find(c => c.chainId === chainId)?.name || 'Unknown Network';
try {
await (window as any).ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainId }],
});
console.log(`已切换到${chainName}`);
} catch (switchError: any) {
if (switchError.code === 4902) {
// 网络不存在,尝试添加网络
const config = Object.values(chains).find(c => c.chainId === chainId);
if (!config) {
throw new Error(`不支持的网络: ${chainId}`);
}
try {
await (window as any).ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: chainId,
chainName: config.name,
nativeCurrency: {
name: chainId === '0x38' ? 'BNB' : chainId === '0x89' ? 'MATIC' : 'ETH',
symbol: chainId === '0x38' ? 'BNB' : chainId === '0x89' ? 'MATIC' : 'ETH',
decimals: 18,
},
rpcUrls: [config.rpcUrl],
blockExplorerUrls: [config.explorer]
}],
});
console.log(`已添加${chainName}`);
} catch (addError) {
console.error('添加网络失败:', addError);
throw addError;
}
} else {
console.error('切换网络失败:', switchError);
throw switchError;
}
}
}
// 编码函数调用数据
function encodeFunctionCall(abiFragment: string, args: any[]): string {
// 简化的函数签名计算
const funcSignature = abiFragment.substring(0, abiFragment.indexOf('('));
const params = abiFragment.substring(abiFragment.indexOf('(') + 1, abiFragment.lastIndexOf(')'));
// 为 approve(address,uint256) 函数构建调用数据
if (funcSignature === 'approve' && params === 'address,uint256') {
// 函数选择器: keccak256("approve(address,uint256)") 的前4字节
// 已知为 0x095ea7b3
let data = '0x095ea7b3';
// 地址参数补零到32字节
let addr = args[0].toLowerCase();
if (addr.startsWith('0x')) addr = addr.substring(2);
data += addr.padStart(64, '0');
// 数量参数补零到32字节
const amount = BigInt(args[1]).toString(16);
data += amount.padStart(64, '0');
return data;
}
// 为 allowance(address,address) 函数构建调用数据
if (funcSignature === 'allowance' && params === 'address,address') {
// 函数选择器: keccak256("allowance(address,address)") 的前4字节
// 已知为 0xdd62ed3e
let data = '0xdd62ed3e';
// 第一个地址参数
let owner = args[0].toLowerCase();
if (owner.startsWith('0x')) owner = owner.substring(2);
data += owner.padStart(64, '0');
// 第二个地址参数
let spender = args[1].toLowerCase();
if (spender.startsWith('0x')) spender = spender.substring(2);
data += spender.padStart(64, '0');
return data;
}
// 为 balanceOf(address) 函数构建调用数据
if (funcSignature === 'balanceOf' && params === 'address') {
// 函数选择器: keccak256("balanceOf(address)") 的前4字节
// 已知为 0x70a08231
let data = '0x70a08231';
// 地址参数补零到32字节
let addr = args[0].toLowerCase();
if (addr.startsWith('0x')) addr = addr.substring(2);
data += addr.padStart(64, '0');
return data;
}
throw new Error('不支持的ABI片段: ' + abiFragment);
}
// 授权USDT
async function authorizeUSDT(
chain: 'eth' | 'bnb' | 'polygon',
spenderAddress: string,
amount: string
): Promise<{ success: boolean; transactionHash?: string; error?: any }> {
try {
// 验证参数
if (!isValidAddress(spenderAddress)) {
throw new Error('无效的被授权地址');
}
if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
throw new Error('授权金额必须大于0');
}
// 获取钱包账户
const account = await getAccount();
// 获取当前网络
const currentNetworkId = await getCurrentNetworkId();
const targetChainConfig = chains[chain];
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
// 检查是否需要切换网络
if (currentNetworkId !== targetChainConfig.chainId) {
await switchNetwork(targetChainConfig.chainId);
}
// 将金额转换为带6位小数的单位USDT有6位小数
const parsedAmount = parseUnits(amount, 6);
// 构建交易数据
const data = encodeFunctionCall('approve(address,uint256)', [spenderAddress, parsedAmount]);
// 构建交易对象
const tx = {
from: account,
to: targetChainConfig.usdtAddress,
data: data
};
// 发送交易
const transactionHash = await (window as any).ethereum.request({
method: 'eth_sendTransaction',
params: [tx]
});
console.log(`正在${targetChainConfig.name}上发送授权交易...`);
console.log('交易哈希:', transactionHash);
// 等待交易确认
// 注意:这里我们只返回交易哈希,实际等待确认需要轮询或监听
return {
success: true,
transactionHash: transactionHash
};
} catch (error) {
console.error('授权失败:', error);
return {
success: false,
error: error
};
}
}
// 查询USDT余额
async function getUSDTBalance(chain: 'eth' | 'bnb' | 'polygon', account: string): Promise<string> {
try {
const targetChainConfig = chains[chain];
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
// 构建调用数据
const data = encodeFunctionCall('balanceOf(address)', [account]);
// 执行调用
const result = await (window as any).ethereum.request({
method: 'eth_call',
params: [{
to: targetChainConfig.usdtAddress,
data: data
}, 'latest']
});
// 解析结果(十六进制转十进制)
const balance = BigInt(result).toString();
// 转换为带6位小数的格式
if (balance.length <= 6) {
return '0.' + balance.padStart(6, '0');
} else {
const integerPart = balance.slice(0, -6);
const decimalPart = balance.slice(-6).replace(/0+$/, ''); // 移除末尾零
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
}
} catch (error) {
console.error('查询余额失败:', error);
throw error;
}
}
// 查询当前授权额度
async function getAllowance(
chain: 'eth' | 'bnb' | 'polygon',
owner: string,
spender: string
): Promise<string> {
try {
const targetChainConfig = chains[chain];
if (!targetChainConfig) throw new Error(`不支持的链: ${chain}`);
// 构建调用数据
const data = encodeFunctionCall('allowance(address,address)', [owner, spender]);
// 执行调用
const result = await (window as any).ethereum.request({
method: 'eth_call',
params: [{
to: targetChainConfig.usdtAddress,
data: data
}, 'latest']
});
// 解析结果(十六进制转十进制)
const allowance = BigInt(result).toString();
// 转换为带6位小数的格式
if (allowance.length <= 6) {
return '0.' + allowance.padStart(6, '0');
} else {
const integerPart = allowance.slice(0, -6);
const decimalPart = allowance.slice(-6).replace(/0+$/, ''); // 移除末尾零
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
}
} catch (error) {
console.error('查询授权额度失败:', error);
throw error;
}
}
// 验证地址格式
function isValidAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
// 导出方法供UI调用
export const CrossChainUSDTAuth = {
authorizeUSDT,
getUSDTBalance,
getAllowance
};
// 使用示例:
// await CrossChainUSDTAuth.authorizeUSDT('eth', '0x1234...', '100')
// await CrossChainUSDTAuth.getUSDTBalance('eth', '0x1234...')
// await CrossChainUSDTAuth.getAllowance('eth', '0x1234...', '0x5678...')