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.
Related codes
//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();