How to sign messages and verify signatures on Solana with React and JavaScript

In this tutorial, we are going to learn how to sign a message with the Solana wallet connected to your React app and how to verify that a signature was generated with the right wallet.

In this tutorial, we are going to learn how to sign a message with the Solana wallet connected to your React app and how to verify that a signature was generated with the right wallet.

Here is an example of how to do it:

import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import { useWallet } from '@solana/wallet-adapter-react';
import nacl from 'tweetnacl';
import base58 from 'bs58';
import { useState } from 'react';

const messageToSign = 'TEST MESSAGE';

export function Home() {
  const { publicKey, signMessage } = useWallet();

  const [signature, setSignature] = useState();

  async function sign() {
    try {
      const message = new TextEncoder().encode(messageToSign);
      const uint8arraySignature = await signMessage(message);

      setSignature(base58.encode(uint8arraySignature));
    } catch (e) {
      console.log('could not sign message');
    }
  }

  async function verify() {
    const message = new TextEncoder().encode(messageToSign);
    const uint8arraySignature = base58.decode(signature);
    const walletIsSigner = nacl.sign.detached.verify(message, uint8arraySignature, publicKey.toBuffer());

    if (walletIsSigner) {
      alert('The data was indeed signed with the connected wallet');
    } else {
      alert('The data was not signed with the connected wallet');
    }
  }

  return (
    <div>
      <WalletMultiButton />
      <button disabled={!publicKey} onClick={sign}>
        Sign message
      </button>
      <button disabled={!signature} onClick={verify}>
        Verify signature
      </button>
    </div>
  );
}

Let's dive in and talk about this code more in details.

But first, check out this tutorial if you want to learn how to connect a Solana wallet to your React app.

Sign data using the connected wallet

In the example above, I connect a wallet to the app using the @solana/wallet-adapter-react library and display a connect button.

Then, when a wallet is connected, we can use the useWallet hook to get the signMessage function.

That function takes in parameter the message to sign as a Uint8Array so we need to use TextEncoder to convert the string message we want the user to sign into a Uint8Array:

const messageToSign = "the data to sign"
const message = new TextEncoder().encode(messageToSign)

The signMessage function is asynchronous because we need the user to accept signing the message on their wallet.

So it's important to wrap the call to that function in a try ... catch because if the user doesn't accept, it will throw an error.

If there is no wallet connected, it will also throw an error so before calling that function, we need to make sure that a wallet is connected to our app. For that, we can just use the useWallet and get the publicKey which is the address of the connected wallet.

If that property is empty, that means no wallet is connected to our app. If it's not empty, then we can safely call signMessage because a wallet is connected.

try {
  const messageToSign = "the data to sign"
  const message = new TextEncoder().encode(messageToSign)
  const signature = await signMessage(message)
} catch (e) {
  console.log(e)
  console.log("could not sign message")
}

When we call signMessage, it will display a pop-up in the user's wallet asking them if they want to accept signing your data or not. On the Phantom wallet, it will look like this:

As you can see, Phantom displays the message that you asked to sign for transparency with the user. That's important for the user to trust the app.

If the user accepts signing the message, the signMessage function will return the signature that was generated. However, the returned value is a Uint8Array and not a string.

Converting the signature into a string

If we want to convert that value to a string, we need to use the bs58 library. That library allows us to encode and decode data using the base58 encoding which is what Solana uses for its signatures and transaction hashes (and other things).

To use that library, we can install it using Yarn or NPM:

npm install bs58

// OR

yarn add bs58

Then, we can turn a Uint8Array into a base58 encoded string using that library:

import base58 from 'bs58'

// ... rest of the app

try {
  const messageToSign = "the data to sign"
  const message = new TextEncoder().encode(messageToSign)
  const uint8arraySignature = await signMessage(message)
  
  const signature = base58.encode(uint8arraySignature)
  
} catch (e) {
  console.log(e)
  console.log("could not sign message")
}

// ... rest of the app

As a string, the signature will look like this:

4UxEq6zcJ9gCT9hqVZ9rCbcTCNKjGFMDzcroUfpVv45H9FmaWTmya4iUdooVDjptvfJ8HMkZjJe3dcAzjcWARhQ3

Verifying that the signature was generated with the right public key

Now, in your app or in the back-end, you can verify that the signature was generated with the right address.

You can use the code I'm going to show you in both the front-end in the React app or in your back-end.

For that, we need to install a new library called TweetNaCl which is a light crypto library for JavaScript that supports secret-key authenticated encryption, public-key authenticated encryption, hashing, and public-key signatures.

To install that library, we can use Yarn or NPM:

npm install tweetnacl

// OR

yarn add tweetnacl

Then we can verify a signature using the verify function:

import nacl from 'tweetnacl'

// ... rest of the app

const walletIsSigner = nacl.sign.detached.verify(
  message,
  uint8arraySignature,
  publicKey
);

The verify function takes 3 parameters:

  1. The message that was signed (as a Uint8Array)
  2. The signature to verify (as a Uint8Array)
  3. The public key that you expect to have generated the signature (as a Uint8Array)

If you have the signature as a string, you can convert it to a Uint8Array using the base58 library:

const uint8arraySignature = base58.decode(signature);

And to convert the message that was signed into a Uint8Array, you can use TextEncoder:

const messageSigned = "the data to sign"
const message = new TextEncoder().encode(messageSigned)

The public key needs to be a Uint8Array so if you have it as a string, you can convert it to a Uint8Array using the base58 library:

const publicKey = base58.decode("GX6nkQgcXy4xDyuSH9MKjG9nq5KN5ntE3ZUUHSqUrcC8")

Otherwise, if you're verifying the signature from the front-end, you can get the connected address as a Uint8Array using the useWallet hook and the toBuffer() method:

const { publicKey, signMessage } = useWallet()

const uint8ArrayPubKey = publicKey.toBuffer()

The verify function will return a boolean that will be true if  the public key you passed in the parameters is the public key of the wallet that generated the signature you passed for the message you passed in the parameters:

import nacl from 'tweetnacl'

const messageSigned = "the data to sign"
const message = new TextEncoder().encode(messageSigned)

const signature = "4UxEq6zcJ9gCT9hqVZ9rCbcTCNKjGFMDzcroUfpVv45H9FmaWTmya4iUdooVDjptvfJ8HMkZjJe3dcAzjcWARhQ3"
const uint8arraySignature = base58.decode(signature);

const publicKey = base58.decode("GX6nkQgcXy4xDyuSH9MKjG9nq5KN5ntE3ZUUHSqUrcC8")

const walletIsSigner = nacl.sign.detached.verify(
  message,
  uint8arraySignature,
  publicKey
);

And that's it 🎉

Thank you for reading this article