WalletConnect
Published in

WalletConnect

WalletConnect Sign v2.0: Beginner’s Guide for JavaScript Developers

We’re excited to introduce the WalletConnect v2.0 Sign SDK! WalletConnect Sign is a remote protocol designed for secure peer-to-peer communication between dapps (web3 applications) and wallets.

WalletConnect Sign establishes a remote pairing between two devices using a relay server. The payloads from these relay servers are symmetrically encrypted with a shared private key between the peers.

The Sign SDK greatly enhances the user experience of dapps. The pairing can be initiated seamlessly by scanning a QR code displayed by a peer and approving the pairing request.

If you are interested in knowing more about the upcoming features and latest information on our protocols, check out our technical specifications.

Getting started with the JavaScript client

In today’s guide, we’re going to be building a React app that can:

  1. Initialize a session
  2. Send a transaction
  3. Close a session
  4. Listen to events

You can follow along step by step, commit by commit here if you get lost. You’re welcome!

Step 1: Creating the project

The first step is to create a React app. Open your terminal, create the app and install the dependencies.

Let’s look at what dependencies you will be installing:

@walletconnect/sign-client
The Sign client is the package you will be using to call the transactions.

@web3modal/core
Actions for the modal.

@web3modal/ui
This is the component that actually renders.

npx create-react-app sign-v2-demo
cd sign-v2-demo
npm install @walletconnect/sign-client @web3modal/core @web3modal/ui

Open the project in your favorite code editor.

Step 2: Get a Project ID

If you don’t have a Project ID yet, head over to the WalletConnect Cloud to create a project and copy its unique Project ID.

I’m going to store my projectId in an environment variable. To do that, I’m going to create a new file called .env and paste in the following code. Use the Project Id number from above. Don’t forget to add .env to .gitignore.

REACT_APP_PROJECT_ID=<YOUR_PROJECT_ID>

Step 3: Initialize the client

The next step is to initialize the SignClient. We’re going to create a new context file to keep most of the dapp’s logic. From the src folder, create a new folder called context . In the context folder, create a new file called ClientContext.js.

Check: Location for the new file: /src/context/ClientContext.js

Copy the following code into ClientContext.js.

// ClientContext.js

import {
createContext,
useState,
useMemo,
useCallback,
useEffect,
useContext,
} from "react";
import SignClient from "@walletconnect/sign-client";
import { ConfigCtrl } from "@web3modal/core";
import "@web3modal/ui";

export const ClientContext = createContext();

ConfigCtrl.setConfig({
projectId: process.env.REACT_APP_PROJECT_ID,
theme: "light",
});

export function ClientContextProvider({ children }) {
const [client, setClient] = useState();

const createClient = useCallback(async () => {
try {
const _client = await SignClient.init({
projectId: process.env.REACT_APP_PROJECT_ID,
metadata: {
name: "Example Dapp",
description: "Example Dapp",
url: "wwww.walletconnect.com",
icons: ["https://walletconnect.com/walletconnect-logo.png"],
},
});
setClient(_client);
} catch (err) {
throw err;
}
}, []);

useEffect(() => {
if (!client) {
createClient();
}
}, [client, createClient]);

const value = useMemo(
() => ({
client,
}),
[client]
);

return (
<ClientContext.Provider value={{ ...value }}>
{children}
<w3m-modal></w3m-modal>
</ClientContext.Provider>
);
}

export function useWalletConnectClient() {
const context = useContext(ClientContext);
if (context === undefined) {
throw new Error(
"useWalletConnectClient must be used within a ClientContextProvider"
);
}
return context;
}

Ok, there’s a lot happening. We’re going to go block by block. If you’re unfamiliar with the imported React hooks, take a look at their docs. SignClient is the SDK that you will be using to send the transaction. ConfigCtrl will be where you configure your Project Id, and lastly, ui will be the QR modal component.

// ClientContext.js

import {
createContext,
useState,
useMemo,
useCallback,
useEffect,
useContext,
} from "react";
import SignClient from "@walletconnect/sign-client";
import { ConfigCtrl} from "@web3modal/core";
import "@web3modal/ui";

Next, we’re going to config our modal. Set projectId to the environment variable if you set one up. For theme, there is a light and dark option.

// ClientContext.js

// ...

export const ClientContext = createContext();

ConfigCtrl.setConfig({
projectId: process.env.REACT_APP_PROJECT_ID,
theme: "light",
});

// ...

After Config.Ctrl, create the provider. Begin with creating a function you can export called ClientContextProvider. Pass {children} as an argument. Create a state variable called client. Follow that up with a useCallback hook and name it createClient. Inside, create a try/catch block. Initialize SignClient. Pass in an object with the Project Id and the metadata for the modal. Set client to _client. Finish with error handling and an empty dependency array.

Add a useEffect hook. Check if _client exists. If doesn’t, then call createClient . Add in client and createClient to the dependency array. Create a new useMemo hook, name it value. Inside, add client. These are the values we will be keeping state for. Finally, return ClientContextProvider passing in value and a prop. As a child element, pass in {children} and w3m-modal.

 // ClientContext.js 

export function ClientContextProvider({ children }) {
const [client, setClient] = useState();

const createClient = useCallback(async () => {
try {
const _client = await SignClient.init({
projectId: process.env.REACT_APP_PROJECT_ID,
metadata: {
name: "Example Dapp",
description: "Example Dapp",
url: "wwww.walletconnect.com",
icons: ["https://walletconnect.com/walletconnect-logo.png"],
},
});
setClient(_client);
} catch (err) {
throw err;
}
}, []);

useEffect(() => {
if (!client) {
createClient();
}
}, [client, createClient]);

const value = useMemo(
() => ({
client,
}),
[client]
);

return (
<ClientContext.Provider value={{ ...value }}>
{children}
<w3m-modal></w3m-modal>
</ClientContext.Provider>
);
}

After ClientContextProvider , create a new export function called useWalletConnectClient. It throws an error if you’re trying to use the logic in the provider in a component that isn’t wrapped by the provider.

// App.js

// ...

export function useWalletConnectClient() {
const context = useContext(ClientContext);
if (context === undefined) {
throw new Error(
"useWalletConnectClient must be used within a ClientContextProvider"
);
}
return context;
}

Let’s wrap the dapp. Go to the entry point of the dapp, in this case, index.js and wrap <App /> with the provider.

// index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { ClientContextProvider } from "./context/ClientContext";
import App from "./App";
import "../src/index.css";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<ClientContextProvider>
<App />
</ClientContextProvider>
</React.StrictMode>
);

Let’s give your front-end some love. Replace everything in App.js with the following code.

// App.js

import './App.css';

function App() {

return (
<div className="App">
<h1>Sign v2</h1>
<button>
Connect
</button>
</div>
);
}

export default App;

Now, spin up your server and let’s make sure there are no errors. You should see a button that doesn’t actually do anything.

npm run start

Step 4: Connecting the dapp - open modal

Next, we’re going to create a connect function. Go to ClientContext.js and update the following import to include ModalCtrl.

// ClientContext.js

import { ConfigCtrl, ModalCtrl } from "@web3modal/core";

Add two new state variables, session and accounts.

const [session, setSession] = useState([]);
const [accounts, setAccounts] = useState([]);

After the state variables, create a new function called connect. We start off by checking to see if the client exists. Once that check is done, we open up with a try/catch block. For this example, we will be connecting our dapp via the Goerli network and testing the eth_sendTransaction method. Inside the try block, create a new variable called requiredNamespaces. The proposal namespace contains the list of chains, methods and events that are required for the dapp. To learn more about namespaces, visit our docs. eip-155 is for EVM-based chains.

Next, we connect! Destruct uri and approval from the client. Set pairingTopic to pairing?.topic and pass requiredNamespaces.

Check to see if uri exists. If it does, open the modal by passing in the uri and standaloneChains. Wait for the approval, and set that to a new value called, session. Finish the function with error handling, and don’t forget to pass client into the dependency array.

  const connect = useCallback(
async (pairing) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}

try {
const requiredNamespaces = {
eip155: {
methods: [
"eth_sendTransaction",
],
chains: ["eip155:5"],
events: [],
},
};

const { uri, approval } = await client.connect({
pairingTopic: pairing?.topic,
requiredNamespaces,
});

if (uri) {
ModalCtrl.open({ uri, standaloneChains: ['eip155:5'] });
}

const session = await approval();
} catch (e) {
console.error(e);
} finally {
ModalCtrl.close();
}
},
[client]
);

Next, update value to include connect, accounts, and session.

  const value = useMemo(
() => ({
client, connect, accounts, session
}),
[ client, connect, accounts, session ]
);

Now that we have a way to pass state to other components, let’s go back to App.js and import useWalletConnectClient. Then inside App, destruct connect, client, accounts, and session from useWalletConnectClient.

// App.js
import "./App.css";
import { useWalletConnectClient } from "./context/ClientContext";

function App() {
const { connect, client, accounts, session } = useWalletConnectClient();

// ...

Create a new function called onConnect. This new function will check if client exists. If it does, then it will call the connect function we created earlier.

  const onConnect = () => {
try {
if (client) {
connect();
}
} catch (e) {
console.log(e);
}
};

Lastly, go ahead and add an onClick handler for your button.

{/* App.js */}

<button onClick={onConnect}>Connect</button>

Now if you try clicking on the connect button, you should be able to see the QR modal pop up.

You may have noticed clicking the button, but nothing might happen. Remember, you have a check to see if the client exists first, so to give the user a visual representation of that, let’s add a disabled prop to that button like this.

{/* App.js */}

<button onClick={onConnect} disabled={!client}>Connect</button>

Step 5: Connecting the dapp - render account number

We’re not done with the pairing. If you try clicking on anything in the modal, you’ll realize nothing is actually happening because we aren’t doing anything the with the actual session. Let’s change that. In ClientContext.js, after the state variables, create a new function and call it onSessionConnected.

What this function is doing is setting the session to whatever the current session is and then filtering down the account to set the accounts.

// ClientContext.js

// ...

const onSessionConnected = useCallback(async (_session) => {
const allNamespaceAccounts = Object.values(_session.namespaces)
.map((namespace) => namespace.accounts)
.flat();
setSession(_session);
setAccounts(allNamespaceAccounts);
}, []);

// ...

Now let’s call this new function. In connect , add this line.

// ClientContext.js

// ...

const session = await approval();
// call onSessionConnected
await onSessionConnected(session);

// ...

Lastly, don’t forget to add onSessionConnected to the dependency array in connect. This is what connect should look like now.

  const connect = useCallback(
async (pairing) => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}

try {
const requiredNamespaces = {
eip155: {
methods: [
"eth_sendTransaction",
],
chains: ["eip155:5"],
events: [],
},
};

const { uri, approval } = await client.connect({
pairingTopic: pairing?.topic,
requiredNamespaces,
});

if (uri) {
ModalCtrl.open({ uri, standaloneChains: ['eip155:5'] });
}

const session = await approval();
await onSessionConnected(session);
} catch (e) {
console.error(e);
} finally {
ModalCtrl.close();
}
},
[client, onSessionConnected]
);

Go back to App.js and let’s add the account info to the UI. After the Connect button add the following.

// .. App.js

{accounts.length ? <h2>{`${accounts[0]}`}</h2> : ""}

Next, let’s test this connection. In order for a wallet to connect to a dapp using Sign v2, the wallet also needs to have Sign v2 integrated. For this example, we are going to test our connection using a wallet that we made here at WalletConnect. Go to https://react-wallet.walletconnect.com/. Paste in the URI from the modal into the wallet and click Connect. You should see the account information.

You’re probably wondering what you just approved. Let’s break down the session proposal. The method listed is eth_sendTransaction which is what we are going to test. Events is left empty, because we haven’t subscribed to any events yet. You then select the account you want to connect.

This is wallet is for testing. Please don’t store real funds. A new wallet is generated anytime you open a wallet for the first time. You will see various chains with their own account number associated with it. Since we’re going to be sending a transaction, let’s send this new account number test funds. This transaction won’t work if the receiving and sending account have a 0 balance.

5. Send a transaction

If you haven’t already enabled testnets in your wallet, go into settings and enable testnets. Copy the Goerli chain account number. Now go get test funds from a faucet like this one.

Update your return statement in App.js to the following. Under the same conditional where we display the account info, we add a new button to send a transaction with an event handler called onSend.

// App.js

// ...

return (
<div className="App">
<h1>Sign v2</h1>
{accounts.length ? (
<>
<h2>{`${accounts[0]}`}</h2>
<button onClick={onSend}>send_Transaction</button>
</>
) : (
<button onClick={onConnect} disabled={!client}>
Connect
</button>
)}
</div>
);

Create a new state variable, and call it txnURL. You will also need to import useEffect from React.

// App.js

import { useState } from "react";

// ...

const [txnUrl, setTxnUrl] = useState();

// ...

Let’s create that new function. We begin with some error handling in case an account doesn’t exist, we will throw an error. Next, we create a transaction object. For the example, we are going to set everything to a static value.

result is the JSON-RPC request to the signer. Finally, return an object that includes the method being used, the address calling the function, whether it’s valid, and the result.

// App.js  

async function onSend() {
try {
if (client) {
const account = accounts[0];
if (account === undefined) throw new Error(`Account not found`);

const tx = {
from: "0xEc57410F1F15df337b54c66BD98F1702B407cB22",
to: "0xBDE1EAE59cE082505bB73fedBa56252b1b9C60Ce",
data: "0x",
gasPrice: "0x029104e28c",
gasLimit: "0x5208",
value: "0x00",
};

const result = await client.request({
topic: session.topic,
chainId: "eip155:5",
request: {
method: "eth_sendTransaction",
params: [tx],
},
});

setTxnUrl(result);

return {
method: "eth_sendTransaction",
address: "0xEc57410F1F15df337b54c66BD98F1702B407cB22",
valid: true,
result,
};
}
} catch (e) {
console.log(e);
}
}

We don’t want to forget to set the pairings. Go back to ClientContext.js and add a pairings state variable. Set pairings after onSessionConnected is called.

// ClientContext.js

// ...

const [pairings, setPairings] = useState([]);

// ...

await onSessionConnected(session);

// Add the pairing
setPairings(client.pairing.getAll({ active: true }));

// ...

Add a new state variable called txnUrl. Update the return statement to check if there is a txnURL to display the Etherscan transaction link on the page.

// App.js

// ...

const [txnUrl, setTxnUrl] = useState();

// ...

return (
<div className="App">
<h1>Sign v2</h1>
{accounts.length ? (
<>
<h2>{`${accounts[0]}`}</h2>
<button onClick={onSend}>send_Transaction</button>
{txnUrl && (
<h2>
Check it out{" "}
<a
href={`https://goerli.etherscan.io/tx/${txnUrl}`}
target="_blank"
rel="noreferrer"
>
here
</a>
</h2>
)}
</>
) : (
<button onClick={onConnect} disabled={!client}>
Connect
</button>
)}
</div>
);

Step 6. Disconnect the dapp

For the final step, we want to be able to disconnect from the dapp. Go to ClientContext.js, import getSdkError and create a new useCallback hook and name it disconnect. To learn more about the client API, visit our docs.

After you disconnect the dapp, we call a new function, reset. Pass in client and session in the dependence array.

// ClientContext.js

import { getSdkError } from "@walletconnect/utils";

// ...

const disconnect = useCallback(async () => {
if (typeof client === "undefined") {
throw new Error("WalletConnect is not initialized");
}
if (typeof session === "undefined") {
throw new Error("Session is not connected");
}
await client.disconnect({
topic: session.topic,
reason: getSdkError("USER_DISCONNECTED"),
});
reset();
}, [client, session]);

// ...

Above disconnect, create a new function called reset.

// ClientContext.js

const reset = () => {
setSession(undefined);
setAccounts([]);
};

Update value to include disconnect, like this.

// ClientContext.js

const value = useMemo(
() => ({
pairings,
accounts,
client,
session,
connect,
disconnect,
}),
[pairings, accounts, client, session, connect, disconnect]
);

Now go to App.js, and add disconnect to the list of values getting destructured.

// App.js

// ...

const { connect, client, accounts, session, disconnect } = useWalletConnectClient();

// ..

Let’s create a new function that will call disconnect. Call is onDisconnect.

// App.js

// ...

const onDisconnect = () => {
disconnect();
};

// ...

Add another button to disconnect the session.

<button onClick={onDisconnect}>Disconnect</button>

7. Listen to events

Up to this point, we have the minimum set up to send a transaction. However, as you build you dapp out, you’ll want to be able control the dapp’s UI depending on what the user does on their wallet. What if the user deletes a session? In this step we’ll go through creating an event listener for that scenario. To read more about event listeners, check out our docs.

In ClientContext.js create a new useContext hook and name it _subscribeToEvents. Inside, create an async function passing in _client. Start with error handling, and create a new event listener. The dapp will be listening for when the session is deleted by passing session_delete . We’ll log to see if this is working, and then call reset. Lastly, add onSessionConnected to the dependency array.

// ClientContext.js

const _subscribeToEvents = useCallback(
async(_client) => {
if (typeof _client === "undefined") {
throw new Error("WalletConnect is not initialized");
}

_client.on("session_delete", () => {
console.log("EVENT", "session_delete");
reset();
});

}, [onSessionConnected]
);

Inside the createClient hook, after you set the client, call and await _subscribeToEvents. Create a dependency array and add _subscribeToEvents.

// ClientContext.js

// ...

const createClient = useCallback(async () => {
try {
const _client = await SignClient.init({
projectId: process.env.REACT_APP_PROJECT_ID,
metadata: {
name: "Example Dapp",
description: "Example Dapp",
url: "wwww.walletconnect.com",
icons: ["https://walletconnect.com/walletconnect-logo.png"],
},
});
setClient(_client);
// call and await _subscribeToEvents
await _subscribeToEvents(_client);
} catch (err) {
throw err;
}
// add dependency array
}, [_subscribeToEvents]);

// ...

While connected, go to the Sessions in your wallet and try deleting the session. When you do, the dapp should now update it’s UI to show a disconnected user.

Where to go from here?

We encourage you to check out our GitHub and documentation if you are interested in learning more about our protocol.

If you want a more robust example, checkout our sample dapp. We have 9 chains, including Solana. For other JavaScript web examples, visit our docs.

Make sure to follow WalletConnect on Twitter to stay informed about our upcoming announcements and updates. If you have any questions, please feel free to shoot them out on our Discord server.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store