A practical reflection on multi-chain wallet integration

Finally, I have some free time to sort out the code. The Web3 project has access to multi-chain wallet connection functions, mainly involving Ethereum, Polygon, BSC and Solana. At first glance, it seemed like it was just a matter of “doing a few more sets of compatible logic”, but after I actually implemented it, I discovered that many things were actually not as simple as I thought.

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']
    }
}

Multi-chain is not simply “supporting multiple wallets”

The biggest feeling is: the chains are different, the wallet interaction methods are also different, and even the way of thinking of the SDK is different. The Ethereum ecosystem can use unified Web3.js to handle a lot of logic, but when it comes to Solana, you will find that it is a completely different system: Provider access, connection process, and PublicKey construction methods are all different, and even network delay and stability affect the experience.

// Initialize the corresponding connection according to the chain type
if (savedConnection.chain === 'solana') {
    //Create a connection using the public Solana devnet RPC endpoint
    this.solanaConnection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
} else if (['ethereum', 'polygon', 'bsc'].includes(savedConnection.chain)) {
    // EVM chain: detect various providers and create Web3 instances
    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;
    }
}

Wallet type also determines user experience

MetaMask has long been standard, but you can’t assume that users will use it. OKX Wallet, Phantom, and even a bunch of browser extensions are all competing for users. Moreover, the injection methods of many wallets are not uniform, which makes debugging very anti-human. For example, the mainstream SDK of the EVM chain is web3.js, and you can basically use one set of logic to take over wallets such as MetaMask and OKX Wallet. When it comes to Solana, you have to use @solana/web3.js alone, and the connection method, permission acquisition, and public key structure are completely different.

// EVM chain (Ethereum, Polygon, BSC) logic
if (chain === 'ethereum' || chain === 'polygon' || chain === 'bsc') {
    let provider = null; // Temporary variable stores the detected provider
    // Prioritize detection of common injections: 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) {
        // Set up and initialize the Web3 instance
        this.provider = provider;
        this.web3 = new Web3(provider);
        // Request user authorization and obtain account list
        const accounts = await provider.request({ method: 'eth_requestAccounts' });
        this.account = accounts[0]; // Get the first account
        return { success: true, account: this.account };
    } else {
        // No EVM compatible providers detected
        throw new Error('Ethereum provider not found. Please install MetaMask, OKX Wallet or another compatible wallet.');
    }

// Solana chain logic
} else if (chain === 'solana') {
    let solProvider = null; // Temporary variable to store Solana provider
    if (window.solana) {
        solProvider = window.solana;
    } else if (window.okxwallet && window.okxwallet.sol) {
        solProvider = window.okxwallet.sol;
    }
    if (solProvider) {
        // Create Solana connection (devnet)
        this.solanaConnection = new Connection('https://api.devnet.solana.com', 'confirmed');
        //Request connection and get publicKey
        const connection = await solProvider.connect();
        this.account = connection.publicKey.toString();
        return { success: true, account: this.account };
    } else {
        // No Solana wallet detected
        throw new Error('Solana provider not found. Please install Phantom wallet or OKX Wallet.');
    }
} else {
    // Unsupported blockchain type
    throw new Error(`Unsupported blockchain: ${chain}`);
}

Balance inquiry: one small step, one big pitfall

I thought it was just to adjust the interface, but the chain is different, the units are different, and the symbols are also different. Solana also has to use lamborts for unit conversion. We finally unified the format encapsulation in the service layer and returned balance and symbol to the front-end components. It seems very ordinary, but in fact there are many internal details.

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' };
  }
}

Wallet switching and chain switching logic

If you only use MetaMask, you may not realize that wallets and chains are two different things. The user may switch to the wrong network in MetaMask. At this time, if there is no active prompt or even automatic switching, subsequent calls will fail.

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],
      });
    }
  }
}

Summarize

If you are also working on a Web3 project, it is recommended not to “ALL IN” at the beginning. It is recommended to first evaluate the differences between the target chains, and then select the main support objects based on user profiles. Start with one chain, and clearly decouple the chain status, wallet, account, and provider first. Then it will be much smoother to expand to other chains. Not every project needs to “open up the entire chain”, but once it is done, it is necessary to avoid the “personality issues” of each chain as much as possible.

Multi-chain is not a trend, it is already the default threshold. However, this door is more complicated to open than imagined.

//Introduce web3 service to manage wallet connection and status
import web3Service from '@/services/web3Service.js';
export default {
    name: 'WalletConnector',
    data() {
        return {
            showDialog: false, // Whether to display the wallet connection pop-up window
            showNetworks: false, // Whether to display the network drop-down list
            showAccountMenu: false, // Whether to display the account operation menu
            selectedChain: 'solana', // Currently selected blockchain, default is Solana
            detectedWallets: [], // List of locally detected wallets
            isConnected: false, // Whether the wallet is connected
            connectedAccount: '', // Connected account address
            connectedChain: '', // Connected chain identifier
            errorMessage: '', // error message
            loading: false, // Asynchronous operation loading status
            // Supported network list and icons
            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' }
            ],
            //Supported wallets and corresponding chain configurations
            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: {
        // Filter out the recommended wallets under the current chain that have not been detected
        filteredWallets() {
            const detectedIds = this.detectedWallets.map(w => w.id); // List of detected wallet IDs
            return this.wallets
                .filter(wallet => wallet.chains.includes(this.selectedChain)) // Support the current chain
                .filter(wallet => !detectedIds.includes(wallet.id)); // Exclude installed ones
        }
    },
    mounted() {
        this.detectInstalledWallets(); // Detect local wallet after mounting
        this.checkSavedConnection(); //Restore the locally saved connection status
        document.addEventListener('click', this.handleClickOutside);
    },
    beforeUnmount() {
        document.removeEventListener('click', this.handleClickOutside);
    },
    methods: {
        // Close the account menu when clicking on an empty area
        handleClickOutside(event) {
            const isInside = event.target.closest('.connected-btn');
            if (this.showAccountMenu && !isInside) {
                this.showAccountMenu = false;
            }
        },
        // Check whether there is saved connection information locally, and restore it if so
        async checkSavedConnection() {
            const saved = web3Service.getSavedConnection();
            if (saved) {
                this.isConnected = true;
                this.connectedAccount = saved.account;
                this.connectedChain = saved.chain;
                //Notify parent component
                this.$emit('wallet-connected', {
                    wallet: saved.wallet,
                    chain: saved.chain,
                    account: saved.account,
                    chainId: saved.chainId
                });
            }
        },
        // Detect locally installed wallet providers
        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 enhanced detection
            if (window.okxwallet || window.okexchain) {
                this.detectedWallets.push({ id: 'okx', name: 'OKX Wallet', icon: '/images/1.png' });
            }
        },
        // Close the pop-up window and reset the network drop-down
        closeDialog() {
            this.showDialog = false;
            this.showNetworks = false;
        },
        //Switch current network
        async selectNetwork(networkId) {
            this.selectedChain = networkId;
            this.showNetworks = false;

            // If the wallet is connected and it is an EVM chain, try switching networks
            if (this.isConnected && ['ethereum', 'polygon', 'bsc'].includes(networkId)) {
                try {
                    const result = await web3Service.switchNetwork(networkId);
                    if (result.success) {
                        this.connectedChain = networkId;
                        //Notify the parent component that the network has been switched
                        this.$emit('network-changed', networkId);
                    }
                } catch (error) {
                    console.error('Failed to switch network:', error);
                    alert(`Failed to switch network: ${error.message}`);
                }
            }
        },
        // Connect the specified wallet to the specified chain
        async connectWallet(walletId, chain) {
            try {
                this.loading = true; // Enable loading state
                this.errorMessage = ''; // Clear errors
                //Wallet general logic
                const result = await web3Service.initialize(chain);
                if (result.success) {
                    // If it is an EVM chain, first try to switch to the correct network
                    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; // Turn off loading state
            }
        },
        // Get the network name based on the network ID
        getNetworkName(networkId) {
            const net = this.networks.find(n => n.id === networkId);
            return net ? net.name : 'Unknown';
        },
        //Address abbreviation display, for example 0x12...Ab34
        shortenAddress(address) {
            if (!address) return '';
            return address.substring(0,4) + '...' + address.substring(address.length-4);
        },
        // Copy address to clipboard
        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 the current wallet connection
        disconnect() {
            web3Service.clearConnection(); // Clear local connection information
            this.isConnected = false;
            this.connectedAccount = '';
            this.connectedChain = '';
            this.showAccountMenu = false;
            this.$emit('wallet-disconnected'); // Notify the parent component
        }
    }
}

web3Service.js

import Web3 from 'web3';
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
class Web3Service {
    constructor() {
        //Initialize Web3Service instance properties
        this.web3 = null; // EVM chain (Ethereum/Polygon/BSC) Web3 instance
        this.solanaConnection = null; // Solana chain Connection instance
        this.selectedChain = null; // Currently selected blockchain ID
        this.account = null; // Currently connected wallet account address
        this.provider = null; // Currently used wallet provider (such as window.ethereum)

        // network configuration
        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']
            }
        };

        //Try to load and restore locally stored connection information
        this.loadSavedConnection();
    }

    // Load and restore saved wallet connection information from localStorage
    loadSavedConnection() {
        try {
            const savedConnection = this.getSavedConnection(); // Get saved connection data
            if (savedConnection) {
                //Restore chain type and account number
                this.selectedChain = savedConnection.chain;
                this.account = savedConnection.account;

                // Initialize the corresponding connection according to the chain type
                if (savedConnection.chain === 'solana') {
                    //Create a connection using the public Solana devnet RPC endpoint
                    this.solanaConnection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
                } else if (['ethereum', 'polygon', 'bsc'].includes(savedConnection.chain)) {
                    // EVM chain: detect various providers and create Web3 instances
                    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);
        }
    }

    // Save wallet connection information to localStorage
    saveConnection(wallet, chain, account, chainId) {
        try {
            //Construct the connection data object to be saved
            const connectionData = {
                wallet, // wallet type (metamask/phantom/okx, etc.)
                chain, // chain type (ethereum/solana, etc.)
                account, // wallet address
                chainId, // chain ID
                timestamp: Date.now() // Save timestamp
            };
            // Serialize and save to localStorage
            localStorage.setItem('walletConnection', JSON.stringify(connectionData));
            //Update service status
            this.selectedChain = chain;
            this.account = account;
        } catch (error) {
            console.error('Error saving wallet connection:', error);
        }
    }

    // Clear the wallet connection information in localStorage and reset the service status
    clearConnection() {
        try {
            localStorage.removeItem('walletConnection'); // Delete storage records
            //Reset all connection related properties
            this.selectedChain = null;
            this.account = null;
            this.web3 = null;
            this.solanaConnection = null;
            this.provider = null;
        } catch (error) {
            console.error('Error clearing wallet connection:', error);
        }
    }

    // Initialize the corresponding blockchain connection according to the specified chain type
    async initialize(chain) {
        try {
            this.selectedChain = chain; //Set the current chain type
            // EVM chain (Ethereum, Polygon, BSC) logic
            if (chain === 'ethereum' || chain === 'polygon' || chain === 'bsc') {
                let provider = null; // Temporary variable stores the detected provider
                // Prioritize detection of common injections: 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) {
                    // Set up and initialize the Web3 instance
                    this.provider = provider;
                    this.web3 = new Web3(provider);
                    // Request user authorization and obtain account list
                    const accounts = await provider.request({ method: 'eth_requestAccounts' });
                    this.account = accounts[0]; // Get the first account
                    return { success: true, account: this.account };
                } else {
                    // No EVM compatible providers detected
                    throw new Error('Ethereum provider not found. Please install MetaMask, OKX Wallet or another compatible wallet.');
                }

            // Solana chain logic
            } else if (chain === 'solana') {
                let solProvider = null; // Temporary variable to store Solana provider
                if (window.solana) {
                    solProvider = window.solana;
                } else if (window.okxwallet && window.okxwallet.sol) {
                    solProvider = window.okxwallet.sol;
                }
                if (solProvider) {
                    // Create Solana connection (devnet)
                    this.solanaConnection = new Connection('https://api.devnet.solana.com', 'confirmed');
                    //Request connection and get publicKey
                    const connection = await solProvider.connect();
                    this.account = connection.publicKey.toString();
                    return { success: true, account: this.account };
                } else {
                    // No Solana wallet detected
                    throw new Error('Solana provider not found. Please install Phantom wallet or OKX Wallet.');
                }
            } else {
                // Unsupported blockchain type
                throw new Error(`Unsupported blockchain: ${chain}`);
            }
        } catch (error) {
            return { success: false, error: error.message };
        }
    }

    // Get the native token balance (supports ETH/MATIC/BNB/SOL)
    async getBalance() {

        try {
            // Verify that the account is connected
            if (!this.account) {
                throw new Error('No connected account.');
            }

            // Define chain configuration, including symbol and unit conversion logic
            const chainConfig = {
                ethereum: { symbol: 'ETH', unit: 'ether' },
                polygon: { symbol: 'MATIC', unit: 'ether' },
                bsc: { symbol: 'BNB', unit: 'ether' },
                solana: { symbol: 'SOL', unit: 'lamports' }
            };

            // Check whether the current chain is supported
            if (!chainConfig[this.selectedChain]) {
                throw new Error(`Unsupported blockchain: ${this.selectedChain}`);
            }

            const { symbol, unit } = chainConfig[this.selectedChain];

            // Get EVM chain balance
            if (['ethereum', 'polygon', 'bsc'].includes(this.selectedChain)) {
                if (!this.web3) {
                    throw new Error('Web3 not initialized.');
                }

                // Get the balance (wei units)
                const balanceWei = await this.web3.eth.getBalance(this.account);
                // Convert to ether units, retaining 6 decimal places for better readability
                const balance = Number(this.web3.utils.fromWei(balanceWei, unit)).toFixed(6);

                return {
                    success: true,
                    balance: parseFloat(balance), // Return numeric type, avoid string
                    symbol
                };
            }

            // Get Solana chain balance
            if (this.selectedChain === 'solana') {
                if (!this.solanaConnection) {
                    throw new Error('Solana connection not initialized.');
                }

                // Get Solana PublicKey
                let publicKeyObj;
                publicKeyObj = new PublicKey(this.account); // OKX wallet

                // Get the balance (lamports units)
                const balanceLamports = await this.solanaConnection.getBalance(publicKeyObj);
                console.log(balanceLamports)

                // Convert to SOL units to 6 decimal places
                const balance = (balanceLamports / 1e9).toFixed(6);

                return {
                    success: true,
                    balance: parseFloat(balance), // Return numeric type
                    symbol
                };
            }
        } catch (error) {
            console.error(`Error getting balance for ${this.selectedChain}:`, error);
            return {
                success: false,
                error: error.message || 'Failed to fetch balance'
            };
        }
    }

    // Simple example: perform token purchase (simulation)
    async purchaseTokens(amount, contractAddress) {
        // Parameter verification
        if (!this.account || !amount) {
            throw new Error('Account or amount not specified');
        }
        // In a real scenario, the contract instance should be created, gas estimated and the transaction sent.
        console.log(`Purchasing tokens with ${amount} on ${this.selectedChain}`);
        console.log(`Contract: ${contractAddress}`);
        console.log(`From account: ${this.account}`);
        // The simulated transaction is successful and a fake hash is returned
        return {
            success: true,
            hash: '0x' + Math.random().toString(16).substring(2, 42),
            amount: amount
        };
    }

    //Read saved connection information from localStorage
    getSavedConnection() {
        try {
            const savedData = localStorage.getItem('walletConnection');
            if (savedData) {
                return JSON.parse(savedData); // Parse JSON and return
            }
            return null;
        } catch (error) {
            console.error('Error retrieving saved connection:', error);
            return null;
        }
    }

    //Switch to the specified EVM network
    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 {
            //Try to switch to an existing network
            await this.provider.request({
                method: 'wallet_switchEthereumChain',
                params: [{ chainId: networkConfig.chainId }],
            });
            return { success: true };
        } catch (switchError) {
            // If the network does not exist (error code 4902), try to add the network
            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();