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 @safe-global/api-kit @safe-global/protocol-kit @safe-global/types-kit
Imports
_10import SafeApiKit from '@safe-global/api-kit'_10import Safe from '@safe-global/protocol-kit'_10import {_10 MetaTransactionData,_10 OperationType_10} from '@safe-global/types-kit'
Create a Safe transaction
_18// Initialize the Protocol Kit with Owner A_18const protocolKitOwnerA = await Safe.init({_18 provider: config.RPC_URL,_18 signer: config.OWNER_A_PRIVATE_KEY,_18 safeAddress: config.SAFE_ADDRESS_18})_18_18// Create a Safe transaction_18const safeTransactionData: MetaTransactionData = {_18 to: config.TO,_18 value: config.VALUE,_18 data: '0x',_18 operation: OperationType.Call_18}_18_18const safeTransaction = await protocolKitOwnerA.createTransaction({_18 transactions: [safeTransactionData]_18})
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
_13// Initialize the API Kit_13const apiKit = new SafeApiKit({_13 chainId: 11155111n_13})_13_13// Send the transaction to the Transaction Service with the signature from Owner A_13await apiKit.proposeTransaction({_13 safeAddress: config.SAFE_ADDRESS,_13 safeTransactionData: safeTransaction.data,_13 safeTxHash,_13 senderAddress: config.OWNER_A_ADDRESS,_13 senderSignature: signatureOwnerA.data_13})
Collect missing signatures
Get the pending transaction
_10const signedTransaction = await apiKit.getTransaction(safeTxHash)
Add missing signatures
_15// Initialize the Protocol Kit with Owner B_15const protocolKitOwnerB = await Safe.init({_15 provider: config.RPC_URL,_15 signer: config.OWNER_B_PRIVATE_KEY,_15 safeAddress: config.SAFE_ADDRESS_15})_15_15// Sign the transaction with Owner B_15const signatureOwnerB = await protocolKitOwnerB.signHash(safeTxHash)_15_15// Send the transaction to the Transaction Service with the signature from Owner B_15await apiKit.confirmTransaction(_15 safeTxHash,_15 signatureOwnerB.data_15)
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}