Once the sprint finally slowed down, I went back through the code for a multi-chain wallet integration. The project needed wallet connections on Ethereum, Polygon, BSC, and Solana. On paper it sounded like adding a few compatibility branches. In practice, each chain brought its own wallet behavior, SDK assumptions, and failure modes.
The first thing I had to pin down was network metadata. Chain IDs, native currency symbols, RPC endpoints, and block explorers all need to be handled consistently before the wallet layer can behave predictably.
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']
}
}
The bigger lesson is that multi-chain is not the same as supporting multiple wallets. The interaction model changes with the chain, and sometimes the SDK's whole mental model changes too. EVM chains can share a lot of logic through Web3.js. Solana is a different system: provider access, connection flow, PublicKey construction, and even network latency and stability all shape the experience.
// Initialize the corresponding connection based on chain type
if (savedConnection.chain === 'solana') {
// Create connection using 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 chains: detect providers and create Web3 instance
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 choice also decides a lot about user experience. MetaMask is the default assumption, but you cannot assume every user has it. OKX Wallet, Phantom, and a pile of browser extensions are all competing for the same users. Many wallets inject themselves differently, and debugging that is painful.
On EVM chains, Web3.js can cover a lot of ground across MetaMask, OKX Wallet, and similar providers. On Solana, you need @solana/web3.js separately, and connection setup, permission requests, and public key structure are all different.
// EVM chain (Ethereum, Polygon, BSC) logic
if (chain === 'ethereum' || chain === 'polygon' || chain === 'bsc') {
let provider = null; // Temporary variable for detected provider
// Prefer 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 and initialize Web3 instance
this.provider = provider;
this.web3 = new Web3(provider);
// Request user authorization and get accounts
const accounts = await provider.request({ method: 'eth_requestAccounts' });
this.account = accounts[0]; // Take the first account
return { success: true, account: this.account };
} else {
// No EVM-compatible provider 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 for 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 checks look like a small step until units and symbols get involved. Solana uses lamports, EVM chains use wei, and the display layer should not have to care. We wrapped formatting in the service layer and returned balance plus symbol to the frontend component. It looks ordinary from the outside, but there are plenty of details underneath.
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 are separate problems. A user can switch MetaMask to the wrong network, and if you do not prompt or switch them back, later 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],
});
}
}
}
If you are building a Web3 project, do not go ALL IN on every chain from day one. First understand how the target chains differ, then look at your user base and pick the main chain to support. Start with one chain, separate chain state, wallet, account, and provider cleanly, and expansion will be much smoother. Not every project needs every chain connected. But once you do it, expect to deal with each chain's own personality.
Multi-chain is not a trend anymore. It is the default threshold. The door just opens with more complexity than it looks.
Supporting code
// Import the web3 service that manages wallet connection and state
import web3Service from '@/services/web3Service.js';
export default {
name: 'WalletConnector',
data() {
return {
showDialog: false, // Whether to show the connect wallet dialog
showNetworks: false, // Whether to show the network dropdown
showAccountMenu: false, // Whether to show the account menu
selectedChain: 'solana', // Currently selected blockchain, default Solana
detectedWallets: [], // Locally detected wallets
isConnected: false, // Whether a wallet is connected
connectedAccount: '', // Connected account address
connectedChain: '', // Connected chain identifier
errorMessage: '', // Error message
loading: false, // Loading state for async operations
// Supported networks 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 their chain configuration
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 recommended wallets for the current chain that have not been detected
filteredWallets() {
const detectedIds = this.detectedWallets.map(w => w.id); // Detected wallet IDs
return this.wallets
.filter(wallet => wallet.chains.includes(this.selectedChain)) // Supports the current chain
.filter(wallet => !detectedIds.includes(wallet.id)); // Exclude installed wallets
}
},
mounted() {
this.detectInstalledWallets(); // Detect local wallets after mount
this.checkSavedConnection(); // Restore saved connection state
document.addEventListener('click', this.handleClickOutside);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClickOutside);
},
methods: {
// Close the account menu when clicking outside
handleClickOutside(event) {
const isInside = event.target.closest('.connected-btn');
if (this.showAccountMenu && !isInside) {
this.showAccountMenu = false;
}
},
// Check for saved connection info and restore it if present
async checkSavedConnection() {
const saved = web3Service.getSavedConnection();
if (saved) {
this.isConnected = true;
this.connectedAccount = saved.account;
this.connectedChain = saved.chain;
// Notify the 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' });
}
// Enhanced OKX Wallet detection
if (window.okxwallet || window.okexchain) {
this.detectedWallets.push({ id: 'okx', name: 'OKX Wallet', icon: '/images/1.png' });
}
},
// Close the dialog and reset the network dropdown
closeDialog() {
this.showDialog = false;
this.showNetworks = false;
},
// Switch the current network
async selectNetwork(networkId) {
this.selectedChain = networkId;
this.showNetworks = false;
// If a wallet is connected and the chain is EVM, 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 changed
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; // Turn on loading state
this.errorMessage = ''; // Clear errors
// Common wallet logic
const result = await web3Service.initialize(chain);
if (result.success) {
// If it is an EVM chain, try switching to the correct network first
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 network name by network ID
getNetworkName(networkId) {
const net = this.networks.find(n => n.id === networkId);
return net ? net.name : 'Unknown';
},
// Shorten address display, e.g. 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
disconnect() {
web3Service.clearConnection(); // Clear local connection info
this.isConnected = false;
this.connectedAccount = '';
this.connectedChain = '';
this.showAccountMenu = false;
this.$emit('wallet-disconnected'); // Notify parent component
}
}
}
import Web3 from 'web3';
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
class Web3Service {
constructor() {
// Initialize Web3Service instance properties
this.web3 = null; // Web3 instance for EVM chains (Ethereum/Polygon/BSC)
this.solanaConnection = null; // Connection instance for Solana
this.selectedChain = null; // Currently selected blockchain identifier
this.account = null; // Currently connected wallet account address
this.provider = null; // Current wallet provider (e.g. 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 saved connection info
this.loadSavedConnection();
}
// Load and restore saved wallet connection from localStorage
loadSavedConnection() {
try {
const savedConnection = this.getSavedConnection(); // Get saved connection data
if (savedConnection) {
// Restore chain type and account
this.selectedChain = savedConnection.chain;
this.account = savedConnection.account;
// Initialize the corresponding connection by chain type
if (savedConnection.chain === 'solana') {
// Create connection using 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 chains: detect providers and create Web3 instance
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 info to localStorage
saveConnection(wallet, chain, account, chainId) {
try {
// Build the connection data object to save
const connectionData = {
wallet, // Wallet type (metamask/phantom/okx, etc.)
chain, // Chain type (ethereum/solana, etc.)
account, // Wallet address
chainId, // Chain ID
timestamp: Date.now() // Saved timestamp
};
// Serialize and save to localStorage
localStorage.setItem('walletConnection', JSON.stringify(connectionData));
// Update service state
this.selectedChain = chain;
this.account = account;
} catch (error) {
console.error('Error saving wallet connection:', error);
}
}
// Clear wallet connection info from localStorage and reset service state
clearConnection() {
try {
localStorage.removeItem('walletConnection'); // Remove stored record
// 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 blockchain connection for the specified chain type
async initialize(chain) {
try {
this.selectedChain = chain; // Set current chain type
// EVM chain (Ethereum, Polygon, BSC) logic
if (chain === 'ethereum' || chain === 'polygon' || chain === 'bsc') {
let provider = null; // Temporary variable for detected provider
// Prefer 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 and initialize Web3 instance
this.provider = provider;
this.web3 = new Web3(provider);
// Request user authorization and get accounts
const accounts = await provider.request({ method: 'eth_requestAccounts' });
this.account = accounts[0]; // Take the first account
return { success: true, account: this.account };
} else {
// No EVM-compatible provider 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 for 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 native token balance (supports ETH/MATIC/BNB/SOL)
async getBalance() {
try {
// Verify account is connected
if (!this.account) {
throw new Error('No connected account.');
}
// Define chain config including symbol and unit conversion
const chainConfig = {
ethereum: { symbol: 'ETH', unit: 'ether' },
polygon: { symbol: 'MATIC', unit: 'ether' },
bsc: { symbol: 'BNB', unit: 'ether' },
solana: { symbol: 'SOL', unit: 'lamports' }
};
// Check whether 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 balance (wei unit)
const balanceWei = await this.web3.eth.getBalance(this.account);
// Convert to ether unit and keep 6 decimals for readability
const balance = Number(this.web3.utils.fromWei(balanceWei, unit)).toFixed(6);
return {
success: true,
balance: parseFloat(balance), // Return number 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 balance (lamports unit)
const balanceLamports = await this.solanaConnection.getBalance(publicKeyObj);
console.log(balanceLamports)
// Convert to SOL unit and keep 6 decimals
const balance = (balanceLamports / 1e9).toFixed(6);
return {
success: true,
balance: parseFloat(balance), // Return number 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: execute token purchase (simulated)
async purchaseTokens(amount, contractAddress) {
// Parameter validation
if (!this.account || !amount) {
throw new Error('Account or amount not specified');
}
// In a real scenario, create contract instance, estimate gas, and send transaction
console.log(`Purchasing tokens with ${amount} on ${this.selectedChain}`);
console.log(`Contract: ${contractAddress}`);
console.log(`From account: ${this.account}`);
// Simulate successful transaction and return fake hash
return {
success: true,
hash: '0x' + Math.random().toString(16).substring(2, 42),
amount: amount
};
}
// Read saved connection info 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 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 switching to an existing network
await this.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: networkConfig.chainId }],
});
return { success: true };
} catch (switchError) {
// If network does not exist (error code 4902), try adding it
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();