Execute transactions
In this guide, you will learn how to create Safe transactions, sign them, collect the signatures from the different owners, and execute them.
See the Protocol Kit reference to find more details and configuration options.
Prerequisites
- Node.js and npm (opens in a new tab).
- An existing Safe with several signers.
Install dependencies
First, you need to install some dependencies.
_10pnpm add @safe-global/api-kit \_10 @safe-global/protocol-kit \_10 @safe-global/types-kit
Steps
Imports
Here are all the necessary imports for this guide.
_10import SafeApiKit from '@safe-global/api-kit'_10import Safe from '@safe-global/protocol-kit'_10import {_10 MetaTransactionData,_10 OperationType_10} from '@safe-global/types-types'
Setup
You need a Safe account setup with two or more signers and threshold two, so at least multiple signatures have to be collected when executing a transaction.
This example uses private keys, but any EIP-1193 compatible signers can be used.
_10const SAFE_ADDRESS = // ..._10_10const OWNER_1_ADDRESS = // ..._10const OWNER_1_PRIVATE_KEY = // ..._10_10const OWNER_2_PRIVATE_KEY = // ..._10_10const RPC_URL = 'https://eth-sepolia.public.blastapi.io'
This guide uses Sepolia, but you can use any chain from the Safe Transaction Service supported networks.
Initialize the Protocol Kit
To handle transactions and signatures, you need to create an instance of the Protocol Kit with the provider
, signer
and safeAddress
.
Optionally, you can track your Safe transactions on-chain by using the onchainAnalytics
property.
_10const protocolKitOwner1 = await Safe.init({_10 provider: RPC_URL,_10 signer: OWNER_1_PRIVATE_KEY,_10 safeAddress: SAFE_ADDRESS,_10 onchainAnalytics // Optional_10})
Create a transaction
Create a safeTransactionData
object with the properties of the transaction, add it to an array of transactions you want to execute, and pass it to the createTransaction
method.
_10const safeTransactionData: MetaTransactionData = {_10 to: '0x',_10 value: '1', // 1 wei_10 data: '0x',_10 operation: OperationType.Call_10}_10_10const safeTransaction = await protocolKitOwner1.createTransaction({_10 transactions: [safeTransactionData]_10})
For more details on what to include in a transaction, see the createTransaction
method in the reference.
Propose the transaction
Before a transaction can be executed, the signer who creates it needs to send it to the Safe Transaction Service so that it is accessible by the other owners, who can then give their approval and sign the transaction.
Firstly, you need to create an instance of the API Kit. In chains where the Safe Transaction Service is supported, it's enough to specify the chainId
property.
_10const apiKit = new SafeApiKit({_10 chainId: 11155111n_10})
You need to calculate the Safe transaction hash, sign the transaction hash, and call the proposeTransaction
method from the API Kit instance to propose a transaction.
For a full list and description of the properties see proposeTransaction
in the API Kit reference.
_13// Deterministic hash based on transaction parameters_13const safeTxHash = await protocolKitOwner1.getTransactionHash(safeTransaction)_13_13// Sign transaction to verify that the transaction is coming from owner 1_13const senderSignature = await protocolKitOwner1.signHash(safeTxHash)_13_13await apiKit.proposeTransaction({_13 safeAddress,_13 safeTransactionData: safeTransaction.data,_13 safeTxHash,_13 senderAddress: OWNER_1_ADDRESS,_13 senderSignature: senderSignature.data_13})
Retrieve the pending transactions
The other signers need to retrieve the pending transactions from the Safe Transaction Service. Depending on the situation, different methods in the API Kit are available.
Call the getPendingTransactions
method to retrieve all the pending transactions of a Safe account.
_10const pendingTransactions = (await apiKit.getPendingTransactions(safeAddress)).results
Confirm the transaction
Once a signer has the pending transaction, they need to sign it with the Protocol Kit and submit the signature to the service using the confirmTransaction
method.
_14const protocolKitOwner2 = await Safe.init({_14 provider: RPC_URL,_14 signer: OWNER_2_PRIVATE_KEY,_14 safeAddress: SAFE_ADDRESS_14 })_14_14 const safeTxHash = transaction.transactionHash_14 const signature = await protocolKitOwner2.signHash(safeTxHash)_14_14 // Confirm the Safe transaction_14 const signatureResponse = await apiKit.confirmTransaction(_14 safeTxHash,_14 signature.data_14)
Execute the transaction
The Safe transaction is now ready to be executed. This can be done using the Safe{Wallet} (opens in a new tab) web interface, the Protocol Kit, the Safe CLI or any other tool that's available.
In this guide, the first signer will get the transaction from the service by calling the getTransaction
method and execute it by passing the transaction with all the signatures to the executeTransaction
method.
_10const safeTransaction = await apiKit.getTransaction(safeTxHash)_10const executeTxResponse = await protocolKitOwner1.executeTransaction(safeTransaction)
Recap and further reading
After following this guide, you are able to create, sign, and execute Safe transactions with the Protocol Kit and share the signatures with the different signers using the API Kit.