358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
// 跨链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...')
|