Create Multichain Wallet with WalletKit

The web3 landscape is evolving rapidly, with new Layer 1 (L1) and Layer 2 (L2) blockchains emerging in the EVM ecosystem, while the altVM brings a surge of innovative chains. In this expanding multichain world, wallets must seamlessly support these networks to deliver the best UX.
In this guide, we’ll explore how to build a multichain wallet and integrate walletkit to support networks beyond EVM, including Solana, Polkadot, and more.
Multichain Connection Request
When connecting your wallet, you need to include approvedNamespaces in the dApp's approval request. This parameter specifies which chains, accounts, methods, and events you want the wallet to support.
The buildApprovedNamespaces method requires information about all desired namespaces, such as eip155 for EVM, solana, polkadot, or any other chains you wish to connect.
You can find the appropriate chainId for approvedNamespaces on this page.
const approvedNamespaces = buildApprovedNamespaces({
proposal: data.proposal?.params as ProposalTypes.Struct,
supportedNamespaces: {
eip155: {
chains: SUPPORTED_CHAINS,
methods: SUPPORTED_METHODS,
events: SUPPORTED_EVENTS,
accounts: [
`eip155:${sepolia.id}:${evmAddress}`,
`eip155:${baseSepolia.id}:${evmAddress}`,
],
},
solana: {
// Add chain with it's chainID.
chains: ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"],
methods: [
"solana_signTransaction",
"solana_signMessage",
"solana_signAndSendTransaction",
"solana_signAllTransactions",
],
events: SUPPORTED_EVENTS,
accounts: [
`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1:${solanaAddress}`,
],
},
polkadot: {
chains: ["polkadot:e143f23803ac50e8f6f8e62695d1ce9e"],
methods: ["polkadot_signTransaction", "polkadot_signMessage"],
events: SUPPORTED_EVENTS,
accounts: [
`polkadot:e143f23803ac50e8f6f8e62695d1ce9e:${polkadotAddress}`,
],
},
},
});
After creating approvedNamespaces, make sure to include it when approving the session. Learn more about this process here.
Multichain Requests
To handle multichain requests, you can extract the method that the dApp is calling from the request received via session_request. After identifying the method name, you can process it accordingly.
const { method } = request.params.request;
Let's handle signing methods from different namespaces: solana_signMessage and solana_signTransaction for Solana, polkadot_signMessage for Polkadot, and personal_sign and eth_sendTransaction for eip155 (EVM).
type SignPayload = {
method: string;
params: any;
};
export async function handleSignRequest({ method, params }: SignPayload) {
const chain = useChainStore.getState().chain;
try {
switch (method) {
case "personal_sign": {
const walletClient = getWalletClient();
const message = hexToString(params[0]);
const signature = await walletClient.signMessage({
message,
account: getWalletAccount(),
});
return { signature };
}
case "eth_sendTransaction": {
const walletClient = getWalletClient();
const transaction = params[0];
const gas = await estimateGas(transaction);
const txHash = await walletClient.sendTransaction({
to: transaction.to as `0x${string}`,
data: transaction.data as `0x${string}`,
value: transaction.value,
account: getWalletAccount(),
gas,
chain: chain === "sepolia" ? sepolia : baseSepolia,
});
return { signature: txHash }; // Keeping consistent return format
}
// Solana Methods
case "solana_signMessage": {
const keypair = getSolanaKeypair();
const signature = nacl.sign.detached(
bs58.decode(params.message),
keypair.secretKey
);
return { signature: bs58.encode(signature) };
}
case "solana_signTransaction": {
const connection = getSolanaConnection();
const keypair = getSolanaKeypair();
const tx = Transaction.from(Buffer.from(params.transaction, 'base64'));
tx.partialSign(keypair);
const signature = await connection.sendRawTransaction(tx.serialize());
return { signature };
}
// Polkadot Methods
case "polkadot_signMessage": {
const keypair = getPolkadotKeypair();
const signature = u8aToHex(keypair.sign(params.message));
return { signature };
}
case "polkadot_signTransaction": {
const keypair = getPolkadotKeypair();
const registry = new TypeRegistry();
const txPayload = registry.createType("ExtrinsicPayload", transaction, {
version: transaction.version,
});
const { signature } = txPayload.sign(keypair);
return { signature };
}
default:
throw new Error(`Unsupported method: ${method}`);
}
} catch (error) {
console.error(`Error handling ${method}:`, error);
throw error;
}
}
Now that we have a helper function to sign messages and return signatures, let's implement it in session_request.
useEffect(() => {
walletkit.on("session_request", onSessionRequest);
}, []);
const onSessionRequest = useCallback(
async (event: WalletKitTypes.SessionRequest) => {
const method = requestEvent?.params?.request?.method;
const params = requestEvent?.params?.request?.params;
const result = await handleSignRequest({
method,
params,
});
await walletKit.respondSessionRequest({
topic: data.requestEvent?.topic as string,
response: {
id: data.requestEvent?.id as number,
result: result,
jsonrpc: "2.0",
},
});
}
);
Switching a Chain
When creating a multichain wallet, you need to handle chain-switching events from the wallet. This ensures that when users switch chains in their wallet, the change is reflected in your dApp, assuming the dApp supports that chain. To achieve this, emit a chainChanged event with the chain details, allowing the dApp to detect and implement the chain switch.
await walletKit.emitSessionEvent({
topic: topic as string,
event: {
name: "chainChanged",
data: `${namespace}:{chainID}, // e.g. eip155:1
}
Congratulations! You’ve successfully learned how to build a multichain wallet using walletkit, enabling seamless interaction with Solana, Polkadot, and EVM-based chains.
For a complete implementation, check out this GitHub repository, where you’ll find the full source code to create a multichain wallet.
Keep building! Let’s improve onchain UX and drive better experiences for Web3 users