Hello! 欢迎来到阿珏酱のBlog!

多链支持,真没你想的那么简单


avatar
阿珏 2025-07-05 9

一次多链钱包集成的实践反思

总算是空闲下来可以整理一下代码了,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();

暂无评论

发表评论
OωO表情

相关阅读

最新评论

恰饭