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 ethers @safe-global/api-kit @safe-global/protocol-kit @safe-global/safe-core-sdk-types
Imports
_10import { ethers } from 'ethers'_10import SafeApiKit, { AddMessageProps } from '@safe-global/api-kit'_10import Safe, { EthersAdapter, hashSafeMessage } from '@safe-global/protocol-kit'
Create a Safe message
_19const ethProvider = new ethers.JsonRpcProvider(config.RPC_URL)_19_19// Instantiate an EthAdapter with Owner A_19const ownerA = new ethers.Wallet(config.OWNER_A_PRIVATE_KEY, ethProvider)_19const ethAdapterOwnerA = new EthersAdapter({_19 ethers,_19 signerOrProvider: ownerA_19})_19_19// Initialize the Protocol Kit with Owner A_19const protocolKitOwnerA = await Safe.create({_19 ethAdapter: ethAdapterOwnerA,_19 safeAddress: config.SAFE_ADDRESS_19})_19_19const rawMessage: string = 'A Safe Message - ' + Date.now()_19_19// Create a Safe message_19const 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
_20// Instantiate an EthAdapter with Owner B_20const ownerB = new ethers.Wallet(config.OWNER_B_PRIVATE_KEY, ethProvider)_20const ethAdapterOwnerB = new EthersAdapter({_20 ethers,_20 signerOrProvider: ownerB_20})_20_20// Initialize the Protocol Kit with Owner B_20const protocolKitOwnerB = await Safe.create({_20 ethAdapter: ethAdapterOwnerB,_20 safeAddress: config.SAFE_ADDRESS_20})_20_20// Get the Safe message hash_20const safeMessageHash = await protocolKitOwnerB.getSafeMessageHash(_20 hashSafeMessage(rawMessage)_20)_20_20// Get the Safe message_20const safeServiceMessage = await apiKit.getMessage(safeMessageHash)
Add missing signatures
_10// Sign the message with Owner B_10const signedMessageOwnerB = await protocolKitOwnerB.signMessage(safeServiceMessage)_10_10const ownerBAddress = (await ethAdapterOwnerB.getSignerAddress()) || '0x'_10_10// Send the message to the Transaction Service with the signature from Owner B_10await apiKit.addMessageSignature(_10 safeMessageHash,_10 signedMessageOwnerB.getSignature(ownerBAddress)?.data || '0x'_10)