Skip to main content

Sign messages on EVM and Solana

This guide shows you how to sign messages and typed data on both EVM networks and Solana from a single multichain session — no network switching required.

All signing methods route to the MetaMask wallet and require user approval.

Prerequisites

  • A multichain client initialized and connected as shown in the quickstart.

Initialize and connect

Set up the multichain client and connect to both ecosystems:

import { createMultichainClient, getInfuraRpcUrls } from '@metamask/connect-multichain'

const client = await createMultichainClient({
dapp: {
name: 'Multichain Demo',
url: window.location.href,
},
api: {
supportedNetworks: {
...getInfuraRpcUrls('YOUR_INFURA_API_KEY'),
},
},
})

await client.connect(
['eip155:1', 'eip155:137', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], // Ethereum, Polygon, Solana
[]
)

Sign an EVM message (personal_sign)

Use invokeMethod with personal_sign to sign a plaintext message. The message must be hex-encoded, and the params order is [message, account]:

const message = 'Hello MetaMask!'
const hexMessage =
'0x' +
Array.from(new TextEncoder().encode(message))
.map(b => b.toString(16).padStart(2, '0'))
.join('')

const signature = await client.invokeMethod({
scope: 'eip155:1', // Ethereum Mainnet
request: {
method: 'personal_sign',
params: [hexMessage, '0xYourAddress'],
},
})
console.log('Signature:', signature)

Target a different chain by changing the scope — for example, eip155:137 for Polygon.

Sign EVM typed data (eth_signTypedData_v4)

Use eth_signTypedData_v4 to sign EIP-712 structured data. The params order is [account, typedDataJSON] — the typed data must be passed as a JSON string, not an object:

const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Mail: [
{ name: 'from', type: 'string' },
{ name: 'to', type: 'string' },
{ name: 'contents', type: 'string' },
],
},
primaryType: 'Mail',
domain: {
name: 'My DApp',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
message: {
from: 'Alice',
to: 'Bob',
contents: 'Hello!',
},
}

const signature = await client.invokeMethod({
scope: 'eip155:1', // Ethereum Mainnet
request: {
method: 'eth_signTypedData_v4',
params: ['0xYourAddress', JSON.stringify(typedData)],
},
})
console.log('Typed data signature:', signature)
note

The EIP712Domain type must be declared in types even though primaryType is never EIP712Domain. Chain IDs in the typed data domain.chainId are integers (for example, 1), not hex strings.

Sign a Solana message (solana_signMessage)

Use invokeMethod with solana_signMessage to sign an arbitrary message on Solana. The message must be base64-encoded:

const message = btoa('Hello from Solana!')

const result = await client.invokeMethod({
scope: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana Mainnet
request: {
method: 'solana_signMessage',
params: {
message,
pubkey: 'YourSolanaPublicKeyBase58',
},
},
})
console.log('Signature:', result.signature)

Error handling

Error codeDescriptionAction
4001User rejected the requestShow a retry option. Do not treat as an application error.
-32002Request already pendingWait for the user to respond in MetaMask before retrying.
try {
const signature = await client.invokeMethod({
scope: 'eip155:1',
request: {
method: 'personal_sign',
params: [hexMessage, '0xYourAddress'],
},
})
} catch (err) {
if (err.code === 4001) {
console.log('User rejected the signature request')
return
}
if (err.code === -32002) {
console.log('A signing request is already pending')
return
}
throw err
}

Next steps