Messages with off-chain signatures
This guide shows how to interact with the Safe Transaction Service API to create and sign messages with a Safe account.
The different steps are implemented using Curl (opens in a new tab) requests, the Safe{Core} SDK (opens in a new tab) TypeScript library and the safe-eth-py (opens in a new tab) Python library.
Prerequisites
- Node.js and npm (opens in a new tab) when using the Safe{Core} SDK.
- Python (opens in a new tab) >= 3.9 when using
safe-eth-py
. - Have a Safe account configured with a threshold of 2, where two signatures are needed.
Steps
Install dependencies
_10yarn add @safe-global/api-kit @safe-global/protocol-kit @safe-global/types-kit
Imports
_10import SafeApiKit, { AddMessageProps } from '@safe-global/api-kit'_10import Safe, { hashSafeMessage } from '@safe-global/protocol-kit'
Create a Safe message
_11// Initialize the Protocol Kit with Owner A_11const protocolKitOwnerA = await Safe.init({_11 provider: config.RPC_URL,_11 signer: config.OWNER_A_PRIVATE_KEY,_11 safeAddress: config.SAFE_ADDRESS_11})_11_11const rawMessage: string = 'A Safe Message - ' + Date.now()_11_11// Create a Safe message_11const safeMessage = protocolKitOwnerA.createMessage(rawMessage)
Sign the message
_10// Sign the message with Owner A_10const signedMessageOwnerA = await protocolKitOwnerA.signMessage(safeMessage)
Send the message to the service
_12// Initialize the API Kit_12const apiKit = new SafeApiKit({_12 chainId: 11155111n_12})_12_12const messageProps: AddMessageProps = {_12 message: rawMessage,_12 signature: signedMessageOwnerA.encodedSignatures()_12}_12_12// Send the message to the Transaction Service with the signature from Owner A_12apiKit.addMessage(config.SAFE_ADDRESS, messageProps)
Collect the missing signatures
Get the pending message
_14// Initialize the Protocol Kit with Owner B_14const protocolKitOwnerB = await Safe.init({_14 provider: config.RPC_URL,_14 signer: config.OWNER_B_PRIVATE_KEY,_14 safeAddress: config.SAFE_ADDRESS_14})_14_14// Get the Safe message hash_14const safeMessageHash = await protocolKitOwnerB.getSafeMessageHash(_14 hashSafeMessage(rawMessage)_14)_14_14// Get the Safe message_14const safeServiceMessage = await apiKit.getMessage(safeMessageHash)
Add missing signatures
_11// Sign the message with Owner B_11const signedMessageOwnerB = await protocolKitOwnerB.signMessage(safeServiceMessage)_11_11// Get Owner B address_11const ownerBAddress = '0x...'_11_11// Send the message to the Transaction Service with the signature from Owner B_11await apiKit.addMessageSignature(_11 safeMessageHash,_11 signedMessageOwnerB.getSignature(ownerBAddress)?.data || '0x'_11)