一次多链钱包集成的实践反思
总算是空闲下来可以整理一下代码了,Web3项目接入多链钱包连接功能,主要涉及 Ethereum、Polygon、BSC 和 Solana。乍一听好像只是“多做几套兼容逻辑”的事,但真正落地后才发现,很多东西其实没想得那么简单。
this.networkConfigs = {
ethereum: {
chainId: '0x1', // 1
chainName: 'Ethereum Mainnet',
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-mainnet.public.blastapi.io'],
blockExplorerUrls: ['https://etherscan.io']
},
polygon: {
chainId: '0x89', // 137
chainName: 'Polygon Mainnet',
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
},
rpcUrls: ['https://polygon-rpc.com'],
blockExplorerUrls: ['https://polygonscan.com']
},
bsc: {
chainId: '0x38', // 56
chainName: 'BNB Smart Chain',
nativeCurrency: {
name: 'BNB',
symbol: 'BNB',
decimals: 18
},
rpcUrls: ['https://bsc-dataseed.binance.org'],
blockExplorerUrls: ['https://bscscan.com']
}
}
多链并不是简单的「支持多个钱包」
最大的感受是:链不一样,钱包交互方式也不一样,连 SDK 的思维方式都不一样。以太坊生态可以用统一的 Web3.js 处理很多逻辑,而到了 Solana,你会发现它完全是另一套系统:Provider 接入、连接流程、PublicKey 构建方式都不太一样,甚至连网络延迟和稳定性都影响体验。
// 根据链类型初始化对应连接
if (savedConnection.chain === 'solana') {
// 使用公共Solana devnet RPC端点创建连接
this.solanaConnection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
} else if (['ethereum', 'polygon', 'bsc'].includes(savedConnection.chain)) {
// EVM链:检测各种提供商并创建Web3实例
if (window.ethereum) {
this.web3 = new Web3(window.ethereum);
this.provider = window.ethereum;
} else if (window.okxwallet) {
this.web3 = new Web3(window.okxwallet);
this.provider = window.okxwallet;
} else if (window.okexchain) {
this.web3 = new Web3(window.okexchain);
this.provider = window.okexchain;
}
}
钱包类型也决定了用户体验
MetaMask 早就是标配,但你没法假设用户一定用它。OKX Wallet、Phantom、甚至还有一堆浏览器扩展都在抢用户。而且很多钱包的注入方式并不统一,调试起来非常反人类。
例如 EVM 链的主流 SDK 是web3.js
,你基本可以一套逻辑通吃 MetaMask、OKX Wallet 等钱包。而到了 Solana,你得单独用 @solana/web3.js
,连接方式、权限获取、公钥结构都完全不同。
// EVM链(Ethereum, Polygon, BSC)逻辑
if (chain === 'ethereum' || chain === 'polygon' || chain === 'bsc') {
let provider = null; // 临时变量存储检测到的提供商
// 优先检测常见注入:MetaMask(window.ethereum)
if (window.ethereum) {
provider = window.ethereum;
} else if (window.okxwallet) {
// OKX Wallet
provider = window.okxwallet;
} else if (window.okexchain) {
// OKExChain
provider = window.okexchain;
}
if (provider) {
// 设置并初始化Web3实例
this.provider = provider;
this.web3 = new Web3(provider);
// 请求用户授权并获取账户列表
const accounts = await provider.request({ method: 'eth_requestAccounts' });
this.account = accounts[0]; // 取第一个账户
return { success: true, account: this.account };
} else {
// 未检测到任何EVM兼容提供商
throw new Error('Ethereum provider not found. Please install MetaMask, OKX Wallet or another compatible wallet.');
}
// Solana链逻辑
} else if (chain === 'solana') {
let solProvider = null; // 临时变量存储Solana提供商
if (window.solana) {
solProvider = window.solana;
} else if (window.okxwallet && window.okxwallet.sol) {
solProvider = window.okxwallet.sol;
}
if (solProvider) {
// 创建Solana连接(devnet)
this.solanaConnection = new Connection('https://api.devnet.solana.com', 'confirmed');
// 请求连接并获取publicKey
const connection = await solProvider.connect();
this.account = connection.publicKey.toString();
return { success: true, account: this.account };
} else {
// 未检测到任何Solana钱包
throw new Error('Solana provider not found. Please install Phantom wallet or OKX Wallet.');
}
} else {
// 不支持的区块链类型
throw new Error(`Unsupported blockchain: ${chain}`);
}
余额查询:一小步,一大坑
原以为就是调个接口,但链不一样,单位不一样,符号也不一样。Solana 还得用 lamports 做单位换算。我们最后统一在服务层做格式封装,返回 balance 和 symbol 给前端组件用,看起来很平常,其实内部细节不少。
async getBalance() {
if (this.selectedChain === 'solana') {
const lamports = await this.solanaConnection.getBalance(new PublicKey(this.account));
return { balance: lamports / 1e9, symbol: 'SOL' };
} else {
const wei = await this.web3.eth.getBalance(this.account);
return { balance: this.web3.utils.fromWei(wei), symbol: 'ETH/MATIC/BNB' };
}
}
钱包切换和链切换逻辑
如果你只用 MetaMask,你可能没意识到钱包和链是两回事。用户在 MetaMask 中可能切换到了错误的网络,这时如果不主动提示甚至自动切换,会导致后续调用失败。
async switchNetwork(chainName) {
const networkConfig = this.networkConfigs[chainName];
try {
await this.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: networkConfig.chainId }],
});
} catch (e) {
if (e.code === 4902) {
await this.provider.request({
method: 'wallet_addEthereumChain',
params: [networkConfig],
});
}
}
}
总结
如果你也在做 Web3 项目,建议别一开始就“ALL IN”,建议先评估清楚目标链之间的差异,再看用户画像选择最主要的支持对象,从一个链做起,把链的状态、钱包、账户、Provider 的解耦先做清楚,再扩展别的链就会顺利很多。不是每个项目都需要“全链打通”,但一旦做了,就要尽可能规避每条链自己的“性格问题”。
多链不是趋势,它已经是默认门槛。只是,这道门,开得比想象中复杂。
相关代码
// 引入 web3 服务,用于管理钱包连接与状态
import web3Service from '@/services/web3Service.js';
export default {
name: 'WalletConnector',
data() {
return {
showDialog: false, // 是否显示连接钱包弹窗
showNetworks: false, // 是否显示网络下拉列表
showAccountMenu: false, // 是否显示账户操作菜单
selectedChain: 'solana', // 当前选中的区块链,默认 Solana
detectedWallets: [], // 本地检测到的钱包列表
isConnected: false, // 是否已连接钱包
connectedAccount: '', // 已连接的账户地址
connectedChain: '', // 已连接的链标识
errorMessage: '', // 错误信息
loading: false, // 异步操作加载状态
// 支持的网络列表及图标
networks: [
{ id: 'ethereum', name: 'Ethereum', icon: '/images/eth.png' },
{ id: 'solana', name: 'Solana', icon: '/images/solana.svg' },
{ id: 'polygon', name: 'Polygon', icon: '/images/Polygon.png' },
{ id: 'bsc', name: 'BNB Chain', icon: '/images/BNB Chain.png' }
],
// 支持的钱包及对应链配置
wallets: [
{ id: 'rainbow', name: 'Rainbow', icon: '/images/2.svg', chains: ['ethereum', 'polygon'] },
{ id: 'metamask', name: 'MetaMask', icon: '/images/3.svg', chains: ['ethereum', 'polygon', 'bsc'] },
{ id: 'walletconnect', name: 'WalletConnect', icon: '/images/5.svg', chains: ['ethereum', 'polygon', 'bsc'] },
{ id: 'phantom', name: 'Phantom', icon: '/images/8.svg', chains: ['solana'] },
{ id: 'okx', name: 'OKX Wallet', icon: '/images/1.png', chains: ['ethereum', 'polygon', 'bsc', 'solana'] }
]
}
},
computed: {
// 过滤出当前链下,未被检测出的推荐钱包
filteredWallets() {
const detectedIds = this.detectedWallets.map(w => w.id); // 已检测钱包 ID 列表
return this.wallets
.filter(wallet => wallet.chains.includes(this.selectedChain)) // 支持当前链
.filter(wallet => !detectedIds.includes(wallet.id)); // 排除已安装
}
},
mounted() {
this.detectInstalledWallets(); // 挂载后检测本地钱包
this.checkSavedConnection(); // 恢复本地保存的连接状态
document.addEventListener('click', this.handleClickOutside);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClickOutside);
},
methods: {
// 点击空白区域时关闭账户菜单
handleClickOutside(event) {
const isInside = event.target.closest('.connected-btn');
if (this.showAccountMenu && !isInside) {
this.showAccountMenu = false;
}
},
// 检查本地是否存在已保存的连接信息,若有则恢复
async checkSavedConnection() {
const saved = web3Service.getSavedConnection();
if (saved) {
this.isConnected = true;
this.connectedAccount = saved.account;
this.connectedChain = saved.chain;
// 通知父组件
this.$emit('wallet-connected', {
wallet: saved.wallet,
chain: saved.chain,
account: saved.account,
chainId: saved.chainId
});
}
},
// 检测本地已安装的钱包提供商
detectInstalledWallets() {
this.detectedWallets = [];
// MetaMask
if (window.ethereum && window.ethereum.isMetaMask) {
this.detectedWallets.push({ id: 'metamask', name: 'MetaMask', icon: '/images/3.svg' });
}
// Phantom
if (window.solana && window.solana.isPhantom) {
this.detectedWallets.push({ id: 'phantom', name: 'Phantom', icon: '/images/8.svg' });
}
// Coinbase Wallet
if (window.ethereum && window.ethereum.isCoinbaseWallet) {
this.detectedWallets.push({ id: 'coinbase', name: 'Coinbase Wallet', icon: '/images/4.svg' });
}
// OKX Wallet 增强检测
if (window.okxwallet || window.okexchain) {
this.detectedWallets.push({ id: 'okx', name: 'OKX Wallet', icon: '/images/1.png' });
}
},
// 关闭弹窗并重置网络下拉
closeDialog() {
this.showDialog = false;
this.showNetworks = false;
},
// 切换当前网络
async selectNetwork(networkId) {
this.selectedChain = networkId;
this.showNetworks = false;
// 如果已连接钱包且是EVM链,尝试切换网络
if (this.isConnected && ['ethereum', 'polygon', 'bsc'].includes(networkId)) {
try {
const result = await web3Service.switchNetwork(networkId);
if (result.success) {
this.connectedChain = networkId;
// 通知父组件网络已切换
this.$emit('network-changed', networkId);
}
} catch (error) {
console.error('Failed to switch network:', error);
alert(`Failed to switch network: ${error.message}`);
}
}
},
// 连接指定钱包到指定链
async connectWallet(walletId, chain) {
try {
this.loading = true; // 开启加载状态
this.errorMessage = ''; // 清空错误
// 钱包通用逻辑
const result = await web3Service.initialize(chain);
if (result.success) {
// 如果是EVM链,先尝试切换到正确的网络
if (['ethereum', 'polygon', 'bsc'].includes(chain)) {
try {
await web3Service.switchNetwork(chain);
} catch (error) {
console.error('Failed to switch network:', error);
throw new Error(`Failed to switch network: ${error.message}`);
}
}
web3Service.saveConnection(walletId, chain, result.account);
this.isConnected = true;
this.connectedAccount = result.account;
this.connectedChain = chain;
this.closeDialog();
this.$emit('wallet-connected', { wallet:walletId, chain, account:result.account });
} else {
throw new Error(result.error || 'Failed to connect wallet');
}
} catch (error) {
this.errorMessage = error.message || 'Failed to connect wallet';
alert(`Failed to connect wallet: ${this.errorMessage}`);
} finally {
this.loading = false; // 关闭加载状态
}
},
// 根据网络 ID 获取网络名称
getNetworkName(networkId) {
const net = this.networks.find(n => n.id === networkId);
return net ? net.name : 'Unknown';
},
// 地址简写显示,例如 0x12...Ab34
shortenAddress(address) {
if (!address) return '';
return address.substring(0,4) + '...' + address.substring(address.length-4);
},
// 将地址复制到剪贴板
copyAddress() {
if (this.connectedAccount) {
navigator.clipboard.writeText(this.connectedAccount)
.then(() => { alert('Address copied to clipboard'); })
.catch(err => { console.error('Address', err); });
}
this.showAccountMenu = false;
},
// 断开当前钱包连接
disconnect() {
web3Service.clearConnection(); // 清除本地连接信息
this.isConnected = false;
this.connectedAccount = '';
this.connectedChain = '';
this.showAccountMenu = false;
this.$emit('wallet-disconnected'); // 通知父组件
}
}
}
web3Service.js
import Web3 from 'web3';
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
class Web3Service {
constructor() {
// 初始化Web3Service实例属性
this.web3 = null; // EVM链(Ethereum/Polygon/BSC)Web3实例
this.solanaConnection = null; // Solana链Connection实例
this.selectedChain = null; // 当前选中的区块链标识
this.account = null; // 当前连接的钱包账号地址
this.provider = null; // 当前使用的钱包提供商(如window.ethereum)
// 网络配置
this.networkConfigs = {
ethereum: {
chainId: '0x1', // 1
chainName: 'Ethereum Mainnet',
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-mainnet.public.blastapi.io'],
blockExplorerUrls: ['https://etherscan.io']
},
polygon: {
chainId: '0x89', // 137
chainName: 'Polygon Mainnet',
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
},
rpcUrls: ['https://polygon-rpc.com'],
blockExplorerUrls: ['https://polygonscan.com']
},
bsc: {
chainId: '0x38', // 56
chainName: 'BNB Smart Chain',
nativeCurrency: {
name: 'BNB',
symbol: 'BNB',
decimals: 18
},
rpcUrls: ['https://bsc-dataseed.binance.org'],
blockExplorerUrls: ['https://bscscan.com']
}
};
// 尝试加载并恢复本地存储的连接信息
this.loadSavedConnection();
}
// 从localStorage加载并恢复保存的钱包连接信息
loadSavedConnection() {
try {
const savedConnection = this.getSavedConnection(); // 获取已保存的连接数据
if (savedConnection) {
// 恢复链类型和账号
this.selectedChain = savedConnection.chain;
this.account = savedConnection.account;
// 根据链类型初始化对应连接
if (savedConnection.chain === 'solana') {
// 使用公共Solana devnet RPC端点创建连接
this.solanaConnection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
} else if (['ethereum', 'polygon', 'bsc'].includes(savedConnection.chain)) {
// EVM链:检测各种提供商并创建Web3实例
if (window.ethereum) {
this.web3 = new Web3(window.ethereum);
this.provider = window.ethereum;
} else if (window.okxwallet) {
this.web3 = new Web3(window.okxwallet);
this.provider = window.okxwallet;
} else if (window.okexchain) {
this.web3 = new Web3(window.okexchain);
this.provider = window.okexchain;
}
}
}
} catch (error) {
console.error('Error loading saved connection:', error);
}
}
// 保存钱包连接信息到localStorage
saveConnection(wallet, chain, account, chainId) {
try {
// 构建要保存的连接数据对象
const connectionData = {
wallet, // 钱包类型(metamask/phantom/okx等)
chain, // 链类型(ethereum/solana等)
account, // 钱包地址
chainId, // 链ID
timestamp: Date.now() // 保存时间戳
};
// 序列化并保存到localStorage
localStorage.setItem('walletConnection', JSON.stringify(connectionData));
// 更新服务状态
this.selectedChain = chain;
this.account = account;
} catch (error) {
console.error('Error saving wallet connection:', error);
}
}
// 清除localStorage中的钱包连接信息及重置服务状态
clearConnection() {
try {
localStorage.removeItem('walletConnection'); // 删除存储记录
// 重置所有连接相关属性
this.selectedChain = null;
this.account = null;
this.web3 = null;
this.solanaConnection = null;
this.provider = null;
} catch (error) {
console.error('Error clearing wallet connection:', error);
}
}
// 根据指定链类型初始化对应的区块链连接
async initialize(chain) {
try {
this.selectedChain = chain; // 设置当前链类型
// EVM链(Ethereum, Polygon, BSC)逻辑
if (chain === 'ethereum' || chain === 'polygon' || chain === 'bsc') {
let provider = null; // 临时变量存储检测到的提供商
// 优先检测常见注入:MetaMask(window.ethereum)
if (window.ethereum) {
provider = window.ethereum;
} else if (window.okxwallet) {
// OKX Wallet
provider = window.okxwallet;
} else if (window.okexchain) {
// OKExChain
provider = window.okexchain;
}
if (provider) {
// 设置并初始化Web3实例
this.provider = provider;
this.web3 = new Web3(provider);
// 请求用户授权并获取账户列表
const accounts = await provider.request({ method: 'eth_requestAccounts' });
this.account = accounts[0]; // 取第一个账户
return { success: true, account: this.account };
} else {
// 未检测到任何EVM兼容提供商
throw new Error('Ethereum provider not found. Please install MetaMask, OKX Wallet or another compatible wallet.');
}
// Solana链逻辑
} else if (chain === 'solana') {
let solProvider = null; // 临时变量存储Solana提供商
if (window.solana) {
solProvider = window.solana;
} else if (window.okxwallet && window.okxwallet.sol) {
solProvider = window.okxwallet.sol;
}
if (solProvider) {
// 创建Solana连接(devnet)
this.solanaConnection = new Connection('https://api.devnet.solana.com', 'confirmed');
// 请求连接并获取publicKey
const connection = await solProvider.connect();
this.account = connection.publicKey.toString();
return { success: true, account: this.account };
} else {
// 未检测到任何Solana钱包
throw new Error('Solana provider not found. Please install Phantom wallet or OKX Wallet.');
}
} else {
// 不支持的区块链类型
throw new Error(`Unsupported blockchain: ${chain}`);
}
} catch (error) {
return { success: false, error: error.message };
}
}
// 获取原生代币余额(支持ETH/MATIC/BNB/SOL)
async getBalance() {
try {
// 验证账户是否已连接
if (!this.account) {
throw new Error('No connected account.');
}
// 定义链配置,包含符号和单位转换逻辑
const chainConfig = {
ethereum: { symbol: 'ETH', unit: 'ether' },
polygon: { symbol: 'MATIC', unit: 'ether' },
bsc: { symbol: 'BNB', unit: 'ether' },
solana: { symbol: 'SOL', unit: 'lamports' }
};
// 检查是否支持当前链
if (!chainConfig[this.selectedChain]) {
throw new Error(`Unsupported blockchain: ${this.selectedChain}`);
}
const { symbol, unit } = chainConfig[this.selectedChain];
// EVM 链余额获取
if (['ethereum', 'polygon', 'bsc'].includes(this.selectedChain)) {
if (!this.web3) {
throw new Error('Web3 not initialized.');
}
// 获取余额(wei 单位)
const balanceWei = await this.web3.eth.getBalance(this.account);
// 转换为 ether 单位,保留 6 位小数以提高可读性
const balance = Number(this.web3.utils.fromWei(balanceWei, unit)).toFixed(6);
return {
success: true,
balance: parseFloat(balance), // 返回数值类型,避免字符串
symbol
};
}
// Solana 链余额获取
if (this.selectedChain === 'solana') {
if (!this.solanaConnection) {
throw new Error('Solana connection not initialized.');
}
// 获取 Solana PublicKey
let publicKeyObj;
publicKeyObj = new PublicKey(this.account); // OKX 钱包
// 获取余额(lamports 单位)
const balanceLamports = await this.solanaConnection.getBalance(publicKeyObj);
console.log(balanceLamports)
// 转换为 SOL 单位,保留 6 位小数
const balance = (balanceLamports / 1e9).toFixed(6);
return {
success: true,
balance: parseFloat(balance), // 返回数值类型
symbol
};
}
} catch (error) {
console.error(`Error getting balance for ${this.selectedChain}:`, error);
return {
success: false,
error: error.message || 'Failed to fetch balance'
};
}
}
// 简易示例:执行代币购买(模拟)
async purchaseTokens(amount, contractAddress) {
// 参数校验
if (!this.account || !amount) {
throw new Error('Account or amount not specified');
}
// 在真实场景中,应创建合约实例、估算Gas并发送交易
console.log(`Purchasing tokens with ${amount} on ${this.selectedChain}`);
console.log(`Contract: ${contractAddress}`);
console.log(`From account: ${this.account}`);
// 模拟交易成功并返回假hash
return {
success: true,
hash: '0x' + Math.random().toString(16).substring(2, 42),
amount: amount
};
}
// 从localStorage读取已保存的连接信息
getSavedConnection() {
try {
const savedData = localStorage.getItem('walletConnection');
if (savedData) {
return JSON.parse(savedData); // 解析JSON并返回
}
return null;
} catch (error) {
console.error('Error retrieving saved connection:', error);
return null;
}
}
// 切换到指定的EVM网络
async switchNetwork(chainName) {
if (!this.provider) {
throw new Error('No provider available');
}
const networkConfig = this.networkConfigs[chainName];
if (!networkConfig) {
throw new Error(`Unsupported network: ${chainName}`);
}
try {
// 尝试切换到已存在的网络
await this.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: networkConfig.chainId }],
});
return { success: true };
} catch (switchError) {
// 如果网络不存在(error code 4902),则尝试添加网络
if (switchError.code === 4902) {
try {
await this.provider.request({
method: 'wallet_addEthereumChain',
params: [networkConfig],
});
return { success: true };
} catch (addError) {
throw new Error(`Failed to add network: ${addError.message}`);
}
} else {
throw new Error(`Failed to switch network: ${switchError.message}`);
}
}
}
}
export default new Web3Service();
最新评论