Skip to content

Latest commit

 

History

History
645 lines (554 loc) · 19.4 KB

File metadata and controls

645 lines (554 loc) · 19.4 KB
title Sign Data with MetaMask Connect EVM
sidebar_label Sign data
description Request cryptographic signatures from users using eth_signTypedData_v4 for structured EIP-712 data or personal_sign for simple messages.
keywords
eth_signTypedData_v4
personal_sign
EIP-712
message signing
cryptographic signature
sign data
metamask
ethereum
toc_max_heading_level 2

import Tabs from '@theme/Tabs' import TabItem from '@theme/TabItem'

Sign data

MetaMask Connect EVM supports two signing methods: eth_signTypedData_v4 for structured, human-readable EIP-712 signatures that can be verified onchain, and personal_sign for simple message signing commonly used in authentication flows like Sign-In with Ethereum (SIWE). Both methods work with viem, ethers.js, web3.js, and the raw Ethereum API.

Use the following RPC methods to request cryptographic signatures from users:

  • eth_signTypedData_v4 - Use this method to request the most human-readable signatures that are efficient to process onchain. We recommend this for most use cases.
  • personal_sign - Use this method for the easiest way to request human-readable signatures that don't need to be efficiently processed onchain.

:::caution eth_sign is deprecated. See MIP-3 for more information about this deprecation. :::

:::note MetaMask supports signing transactions using Trezor and Ledger hardware wallets. These wallets only support signing data using personal_sign. If you can't sign in to a dapp when using a Ledger or Trezor, the dapp might be requesting you to sign data using an unsupported method, in which case we recommend using your standard MetaMask account. :::

Prerequisites

Follow the JavaScript quickstart or Wagmi quickstart to install, initialize, and connect the EVM client.

Use eth_signTypedData_v4

eth_signTypedData_v4 provides the most human-readable signatures that are efficient to process onchain. It follows the EIP-712 specification to allow users to sign typed structured data that can be verified onchain. It renders the structured data in a useful way (for example, displaying known account names in place of addresses).

MetaMask confirmation dialog showing an eth_signTypedData_v4 structured data signing request

An eth_signTypedData_v4 payload uses a standard format of encoding structs, but has a different format for the top-level struct that is signed, which includes some metadata about the verifying contract to provide replay protection of these signatures between different contract instances.

We recommend using eth-sig-util to generate and validate signatures. Use eip712-codegen to generate most of the Solidity required to verify these signatures onchain. It currently doesn't generate the top-level struct verification code, so you must write that part manually. See this example implementation.

:::caution Since the top-level struct type's name and the domain.name are presented to the user prominently in the confirmation, consider your contract name, the top-level struct name, and the struct keys to be a user-facing security interface. Ensure your contract is as readable as possible to the user. :::

Example

The following is an example of using eth_signTypedData_v4 with MetaMask:

<Tabs defaultValue="viem" values={[ { label: "viem", value: "viem" }, { label: "web3.js", value: "web3" }, { label: "ethers.js", value: "ethers" }, { label: "Ethereum API", value: "eth_api" }, ]}>

import { createEVMClient } from '@metamask/connect-evm'

const evmClient = await createEVMClient({
  dapp: {
    name: 'MetaMask Connect EVM Example',
    url: window.location.href,
    iconUrl: 'https://mydapp.com/icon.png', // Optional
  },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
      '0xaa36a7': 'https://sepolia.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

async function signTypedDataV4() {
  // Get current account
  const accounts = await provider.request({
    method: 'eth_requestAccounts',
  })

  const message = {
    domain: {
      // This defines the network, in this case, Mainnet.
      chainId: 1,
      // Give a user-friendly name to the specific contract you're signing for.
      name: 'Ether Mail',
      // Add a verifying contract to make sure you're establishing contracts with the proper entity.
      verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
      // This identifies the latest version.
      version: '1',
    },

    // This defines the message you're proposing the user to sign, is dapp-specific, and contains
    // anything you want. There are no required fields. Be as explicit as possible when building out
    // the message schema.
    message: {
      contents: 'Hello, Bob!',
      attachedMoneyInEth: 4.2,
      from: {
        name: 'Cow',
        wallets: [
          '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
          '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
        ],
      },
      to: [
        {
          name: 'Bob',
          wallets: [
            '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
            '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
            '0xB0B0b0b0b0b0B000000000000000000000000000',
          ],
        },
      ],
    },
    // This refers to the keys of the following types object.
    primaryType: 'Mail',
    types: {
      // This refers to the domain the contract is hosted on.
      EIP712Domain: [
        { name: 'name', type: 'string' },
        { name: 'version', type: 'string' },
        { name: 'chainId', type: 'uint256' },
        { name: 'verifyingContract', type: 'address' },
      ],
      // Not an EIP712Domain definition.
      Group: [
        { name: 'name', type: 'string' },
        { name: 'members', type: 'Person[]' },
      ],
      // Refer to primaryType.
      Mail: [
        { name: 'from', type: 'Person' },
        { name: 'to', type: 'Person[]' },
        { name: 'contents', type: 'string' },
      ],
      // Not an EIP712Domain definition.
      Person: [
        { name: 'name', type: 'string' },
        { name: 'wallets', type: 'address[]' },
      ],
    },
  }

  const signature = await provider.request({
    method: 'eth_signTypedData_v4',
    params: [accounts[0], JSON.stringify(message)],
  })

  return signature
}
import { createEVMClient } from '@metamask/connect-evm'
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'

const evmClient = await createEVMClient({
  dapp: { name: 'My Dapp', url: window.location.href },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

const walletClient = createWalletClient({ chain: mainnet, transport: custom(provider) })

const address = await walletClient.getAddresses()

const signature = await walletClient.signTypedData({
  account: address[0],
  domain: {
    // This defines the network, in this case, Mainnet.
    chainId: 1,
    // Give a user-friendly name to the specific contract you're signing for.
    name: 'Ether Mail',
    // Add a verifying contract to make sure you're establishing contracts with the proper entity.
    verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
    // This identifies the latest version.
    version: '1',
  },

  // This defines the message you're proposing the user to sign, is dapp-specific, and contains
  // anything you want. There are no required fields. Be as explicit as possible when building out
  // the message schema.
  message: {
    contents: 'Hello, Bob!',
    attachedMoneyInEth: 4.2,
    from: {
      name: 'Cow',
      wallets: [
        '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
        '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
      ],
    },
    to: [
      {
        name: 'Bob',
        wallets: [
          '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
          '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
          '0xB0B0b0b0b0b0B000000000000000000000000000',
        ],
      },
    ],
  },
  // This refers to the keys of the following types object.
  primaryType: 'Mail',
  types: {
    // This refers to the domain the contract is hosted on.
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    // Not an EIP712Domain definition.
    Group: [
      { name: 'name', type: 'string' },
      { name: 'members', type: 'Person[]' },
    ],
    // Refer to primaryType.
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person[]' },
      { name: 'contents', type: 'string' },
    ],
    // Not an EIP712Domain definition.
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallets', type: 'address[]' },
    ],
  },
})
import { createEVMClient } from '@metamask/connect-evm'
import { ethers } from 'ethers'
import { BrowserProvider, parseUnits } from 'ethers'

const evmClient = await createEVMClient({
  dapp: { name: 'My Dapp', url: window.location.href },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

const ethersProvider = new ethers.BrowserProvider(provider)
const signer = await ethersProvider.getSigner()

const fromAddress = await signer.getAddress()

const message = {
  domain: {
    // This defines the network, in this case, Mainnet.
    chainId: 1,
    // Give a user-friendly name to the specific contract you're signing for.
    name: 'Ether Mail',
    // Add a verifying contract to make sure you're establishing contracts with the proper entity.
    verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
    // This identifies the latest version.
    version: '1',
  },

  // This defines the message you're proposing the user to sign, is dapp-specific, and contains
  // anything you want. There are no required fields. Be as explicit as possible when building out
  // the message schema.
  message: {
    contents: 'Hello, Bob!',
    attachedMoneyInEth: 4.2,
    from: {
      name: 'Cow',
      wallets: [
        '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
        '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
      ],
    },
    to: [
      {
        name: 'Bob',
        wallets: [
          '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
          '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
          '0xB0B0b0b0b0b0B000000000000000000000000000',
        ],
      },
    ],
  },
  // This refers to the keys of the following types object.
  primaryType: 'Mail',
  types: {
    // This refers to the domain the contract is hosted on.
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    // Not an EIP712Domain definition.
    Group: [
      { name: 'name', type: 'string' },
      { name: 'members', type: 'Person[]' },
    ],
    // Refer to primaryType.
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person[]' },
      { name: 'contents', type: 'string' },
    ],
    // Not an EIP712Domain definition.
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallets', type: 'address[]' },
    ],
  },
}

const params = [fromAddress, JSON.stringify(message)]
const method = 'eth_signTypedData_v4'

const signature = await signer.provider.send(method, params)
import { createEVMClient } from '@metamask/connect-evm'
import { Web3 } from 'web3'

const evmClient = await createEVMClient({
  dapp: { name: 'My Dapp', url: window.location.href },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

const web3 = new Web3(provider)

// Get user's Ethereum public address
const fromAddress = (await web3.eth.getAccounts())[0]

const message = {
  domain: {
    // This defines the network, in this case, Mainnet.
    chainId: 1,
    // Give a user-friendly name to the specific contract you're signing for.
    name: 'Ether Mail',
    // Add a verifying contract to make sure you're establishing contracts with the proper entity.
    verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
    // This identifies the latest version.
    version: '1',
  },

  // This defines the message you're proposing the user to sign, is dapp-specific, and contains
  // anything you want. There are no required fields. Be as explicit as possible when building out
  // the message schema.
  message: {
    contents: 'Hello, Bob!',
    attachedMoneyInEth: 4.2,
    from: {
      name: 'Cow',
      wallets: [
        '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
        '0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF',
      ],
    },
    to: [
      {
        name: 'Bob',
        wallets: [
          '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
          '0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57',
          '0xB0B0b0b0b0b0B000000000000000000000000000',
        ],
      },
    ],
  },
  // This refers to the keys of the following types object.
  primaryType: 'Mail',
  types: {
    // This refers to the domain the contract is hosted on.
    EIP712Domain: [
      { name: 'name', type: 'string' },
      { name: 'version', type: 'string' },
      { name: 'chainId', type: 'uint256' },
      { name: 'verifyingContract', type: 'address' },
    ],
    // Not an EIP712Domain definition.
    Group: [
      { name: 'name', type: 'string' },
      { name: 'members', type: 'Person[]' },
    ],
    // Refer to primaryType.
    Mail: [
      { name: 'from', type: 'Person' },
      { name: 'to', type: 'Person[]' },
      { name: 'contents', type: 'string' },
    ],
    // Not an EIP712Domain definition.
    Person: [
      { name: 'name', type: 'string' },
      { name: 'wallets', type: 'address[]' },
    ],
  },
}

const params = [fromAddress, JSON.stringify(message)]
const method = 'eth_signTypedData_v4'

const signature = await provider.request({ method, params })

Use personal_sign

personal_sign is the easiest way to request human-readable signatures that don't need to be efficiently processed onchain. It's often used for signature challenges that are authenticated on a web server, such as Sign-In with Ethereum.

MetaMask personal_sign message signing confirmation dialog

:::caution important

  • Don't use this method to display binary data, because the user wouldn't be able to understand what they're agreeing to.
  • If using this method for a signature challenge, think about what would prevent a phisher from reusing the same challenge and impersonating your site. Add text referring to your domain, or the current time, so the user can verify if this challenge is legitimate.

:::

Example

The following is an example of using personal_sign with MetaMask:

<Tabs defaultValue="viem" values={[ { label: "viem", value: "viem" }, { label: "web3.js", value: "web3" }, { label: "ethers.js", value: "ethers" }, { label: "Ethereum API", value: "eth_api" }, ]}>

import { createEVMClient } from '@metamask/connect-evm'

const evmClient = await createEVMClient({
  dapp: {
    name: 'MetaMask Connect EVM Example',
    url: window.location.href,
    iconUrl: 'https://mydapp.com/icon.png', // Optional
  },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
      '0xaa36a7': 'https://sepolia.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

async function personalSign() {
  // Get current account
  const accounts = await provider.request({
    method: 'eth_requestAccounts',
  })

  const exampleMessage = 'Example `personal_sign` message.'
  const message = `0x${Buffer.from(exampleMessage, 'utf8').toString('hex')}`

  const signature = await provider.request({
    method: 'personal_sign',
    params: [message, accounts[0]],
  })

  return signature
}
import { createEVMClient } from '@metamask/connect-evm'
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'

const evmClient = await createEVMClient({
  dapp: { name: 'My Dapp', url: window.location.href },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

const walletClient = createWalletClient({ chain: mainnet, transport: custom(provider) })

const address = await walletClient.getAddresses()

const originalMessage = 'Example `personal_sign` message.'

const signature = await walletClient.signMessage({
  account: address[0],
  message: originalMessage,
})
import { createEVMClient } from '@metamask/connect-evm'
import { ethers } from 'ethers'

const evmClient = await createEVMClient({
  dapp: { name: 'My Dapp', url: window.location.href },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

const ethersProvider = new ethers.BrowserProvider(provider)
const signer = await ethersProvider.getSigner()

const exampleMessage = 'Example `personal_sign` message.'

const signature = await signer.signMessage(exampleMessage)
import { createEVMClient } from '@metamask/connect-evm'
import { Web3 } from 'web3'

const evmClient = await createEVMClient({
  dapp: { name: 'My Dapp', url: window.location.href },
  api: {
    supportedNetworks: {
      '0x1': 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
    },
  },
})
const provider = evmClient.getProvider()

const web3 = new Web3(provider)

// Get user's Ethereum public address
const fromAddress = (await web3.eth.getAccounts())[0]

const exampleMessage = 'Example `personal_sign` message.'

const signature = await web3.eth.sign(fromAddress, exampleMessage)

personal_sign prepends the message with \x19Ethereum Signed Message:\n<length of message> before hashing and signing it.

:::tip Connect and sign You can also connect to MetaMask and personal_sign in a single step, using MetaMask Connect EVM's connectAndSign method. :::

Next steps