How to build a wallet with WalletKit: Complete guide

Have you ever scanned a QR code to connect your wallet?

If you have, you've experienced the power of WalletKit. WalletKit is a comprehensive toolkit designed to enable seamless wallet building and connectivity. By integrating WalletKit, your wallet can connect to thousands of apps supported by the WalletConnect Network.

Now that you understand what WalletKit is, let's dive in and build a web-based wallet that can connect and sign messages using WalletKit.

Here's a quick demo of what we'll be building

Installation

To get started with WalletKit, first install the required libraries using the following command:

pnpm add @reown/walletkit @walletconnect/utils @walletconnect/core

Initialisation

After installing the required libraries, let's initialise the WalletKit instance. If you're using Next JS or another React-based framework, initialise this instance in your root file.

Let's create a provider that we'll use in the root file (layout.tsx for Next JS).

'use client';

import { Core } from '@walletconnect/core'
import { WalletKit, IWalletKit } from '@reown/walletkit'

export let walletKit: IWalletKit | null = null;

export const WalletKitInitialise = () => {
	
	const initialiseWalletKit = () => {
		try {
		
			// Make sure you have setup ProjectID in .env
	    const core = new Core({
	      projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
	    });

	    // Initialize the walletkit
	    walletKit = await WalletKit.init({
	      core,
	      metadata: {
		    name: 'Cool Wallet', // replace with your own name
		    description: 'Wallet to demo power of WalletKit', // replace with your desc
		    url: 'https://reown.com/walletkit', // replace with your url
		    icons: [] // add you wallet's icon URL
		  });
  };

	useEffect(() => {
		if (!walletKit) {
			initialiseWalletKit();
		}
	}, []);
	
	return <></>
}

Now let's implement this provider in your layout file.

...
<body>
	{children}
	<WalletKitInitialise />
</body>
...

Session Proposals

To connect your wallet with any app, you need to create a session. This requires a URI from the WalletConnect QR modal. Once you have the URI, calling walletKit.pair({ uri }) will trigger a session_proposal event.

The walletKit instance emits various events that handle different wallet actions. When connecting your wallet to a dApp, walletKit.pair() emits a session_proposal event that you can listen to and approve to establish the connection.

Before approving a session proposal, you'll need to create approvedNamespaces using buildApprovedNamespaces. This defines which chains, methods, events, and accounts you want to use in the session.

Let's create supportedNamespaces to use it in approvedNamespaces

const supportedNamspaces = {
	 // You can add multiple namespaces like cosmos, near, solana, etc
   eip155: {
      chains: ["eip155:1", "eip155:137"],
      methods: ["eth_sendTransaction", "personal_sign"],
      events: ["accountsChanged", "chainChanged"],
      // Replace wallet address with your address
      accounts: [
	      `eip155:1:0x3039e4a4a540F35ae03A09f3D5A122c49566f919`, 
	      `eip155:137:0x3039e4a4a540F35ae03A09f3D5A122c49566f919`
      ],
    },
   },
 });       

Listen to the session_proposal event and call walletKit.approveSession to establish a session between your wallet and the app.

useEffect(() => {
	 walletkit.on("session_proposal", onSessionProposal);
}, []);

const onSessionProposal = useCallback(
	async (proposal: WalletKitTypes.SessionProposal) => {
		try {
			const approvedNamespaces = buildApprovedNamespaces({
				proposal,
				supportedNamespaces
			});
			
			await walletKit.approveSession({
        id: proposal?.id as number,
        namespaces: approvedNamespaces,
      });
		} 
		catch (error) {
			console.error(error);
    }
	},
	[]
);

You can access all active wallet sessions by calling walletKit.getActiveSessions().

Rejecting a Proposal

To reject a request, call walletKit.rejectSession with an appropriate reason, such as "user rejected".

await walletKit.rejectSession({
  id: proposal.id, // You need to store proposal Id when getting a request
  reason: getSdkError("USER_REJECTED") 
})

Session Requests

Now, once a session is created, you can send session requests to perform actions like personal_sign, eth_sendTransaction, and more. Let's explore how to handle a personal_sign request and return a valid signature from our wallet.

The session_request event triggers when our wallet receives an action request from the app. The event contains topic and request objects that help us process and respond to the request.

Let's listen to the session_request event to handle personal_sign and return valid signature.

useEffect(() => {
	 walletkit.on("session_request", onSessionRequest);
}, []);

const onSessionRequest = useCallback(
	async (event: WalletKitTypes.SessionRequest) => {
		const { topic, params, id } = event;
	  const { request } = params;
	  // Get the message to sign
	  const requestParamsMessage = request.params[0];
	  
	  // Convert the message to sign
	  const message = hextToString(requestParamsMessage);
	  
	  // Sign the message
	  // You can use the `signMessage` method from your walletClient instance
	  const signature = await walletClient.signMessage({
		  message,
		  account: Account // get this from privateKey 
		});
		
		// once you have signed, return the signature 
		await walletKit.respondSessionRequest({
	    topic: data.requestEvent?.topic as string,
      response: {
	      id,
	      result: signature,
	      jsonrpc: "2.0",
      },
    });
   }
 );

To handle the eth_sendTransaction event, follow the same pattern used for personal_sign. You'll receive all necessary transaction data from params. Once you have this data, use your walletClient to execute the transaction.

Rejecting a Request

If you want to reject the session_request, you can respond with an error message using the same respondSessionRequest method.

const handleRejectRequest = async (data: WalletKitTypes.SessionRequest) => {
	// Response object 
	const response = {
		id: data?.id as number,
		jsonrpc: "2.0",
		error: {
			code: 5000,
			message: "User rejected",
		},
		
	// Respond with error response
	await walletKit.respondSessionRequest({
		topic: data?.topic as string,
		response,
	});
};

Congratulations!

You've learned how to integrate WalletKit into your wallet, enabling essential features like wallet connections, message signing, and transaction handling. Beyond these core functions, WalletKit offers Verify API, Notifications, and One Click Auth, with Chain Abstraction coming soon.

For a complete implementation example, you can refer to this sample wallet repository built with WalletKit.

Related articles