Transactions with off-chain signatures
This guide shows how to interact with the Safe Transaction Service API to create, sign, and execute transactions with the owners of 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 from '@safe-global/api-kit'_10import Safe, { EthersAdapter } from '@safe-global/protocol-kit'_10import {_10 MetaTransactionData,_10 OperationType_10} from '@safe-global/safe-core-sdk-types'
Create a Safe transaction
_26const ethProvider = new ethers.JsonRpcProvider(config.RPC_URL)_26_26// Instantiate an EthAdapter with Owner A_26const ownerA = new ethers.Wallet(config.OWNER_A_PRIVATE_KEY, ethProvider)_26const ethAdapterOwnerA = new EthersAdapter({_26 ethers,_26 signerOrProvider: ownerA_26})_26_26// Initialize the Protocol Kit with Owner A_26const protocolKitOwnerA = await Safe.create({_26 ethAdapter: ethAdapterOwnerA,_26 safeAddress: config.SAFE_ADDRESS_26})_26_26// Create a Safe transaction_26const safeTransactionData: MetaTransactionData = {_26 to: config.TO,_26 value: config.VALUE,_26 data: '0x',_26 operation: OperationType.Call_26}_26_26const safeTransaction = await protocolKitOwnerA.createTransaction({_26 transactions: [safeTransactionData]_26})
Sign the transaction
_10// Sign the transaction with Owner A_10const safeTxHash = await protocolKitOwnerA.getTransactionHash(safeTransaction)_10const signatureOwnerA = await protocolKitOwnerA.signHash(safeTxHash)
Send the transaction to the service
_15// Initialize the API Kit_15const apiKit = new SafeApiKit({_15 chainId: 11155111n_15})_15_15const senderAddress = await ownerA.getAddress()_15_15// Send the transaction to the Transaction Service with the signature from Owner A_15await apiKit.proposeTransaction({_15 safeAddress: config.SAFE_ADDRESS,_15 safeTransactionData: safeTransaction.data,_15 safeTxHash,_15 senderAddress,_15 senderSignature: signatureOwnerA.data_15})
Collect missing signatures
Get the pending transaction
_10const signedTransaction = await apiKit.getTransaction(safeTxHash)
Add missing signatures
_21// Instantiate an EthAdapter with Owner B_21const ownerB = new ethers.Wallet(config.OWNER_B_PRIVATE_KEY, ethProvider)_21const ethAdapterOwnerB = new EthersAdapter({_21 ethers,_21 signerOrProvider: ownerB_21})_21_21// Initialize the Protocol Kit with Owner B_21const protocolKitOwnerB = await Safe.create({_21 ethAdapter: ethAdapterOwnerB,_21 safeAddress: config.SAFE_ADDRESS_21})_21_21// Sign the transaction with Owner B_21const signatureOwnerB = await protocolKitOwnerB.signHash(safeTxHash)_21_21// Send the transaction to the Transaction Service with the signature from Owner B_21await apiKit.confirmTransaction(_21 safeTxHash,_21 signatureOwnerB.data_21)
Execute the transaction
_10const transactionResponse =_10 await protocolKitOwnerA.executeTransaction(signedTransaction)
Get the executed transaction
_10const transactions = await apiKit.getMultisigTransactions(config.SAFE_ADDRESS)_10_10if (transactions.results.length > 0) {_10 console.log('Last executed transaction', transactions.results[0])_10}