API
Guides
Transactions

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

  1. Node.js and npm (opens in a new tab) when using the Safe{Core} SDK.
  2. Python (opens in a new tab) >= 3.9 when using safe-eth-py.
  3. Have a Safe account configured with a threshold of 2, where two signatures are needed.

Steps

Install dependencies


_10
yarn add ethers @safe-global/api-kit @safe-global/protocol-kit @safe-global/safe-core-sdk-types

Imports


_10
import { ethers } from 'ethers'
_10
import SafeApiKit from '@safe-global/api-kit'
_10
import Safe, { EthersAdapter } from '@safe-global/protocol-kit'
_10
import {
_10
MetaTransactionData,
_10
OperationType
_10
} from '@safe-global/safe-core-sdk-types'

Create a Safe transaction


_26
const ethProvider = new ethers.JsonRpcProvider(config.RPC_URL)
_26
_26
// Instantiate an EthAdapter with Owner A
_26
const ownerA = new ethers.Wallet(config.OWNER_A_PRIVATE_KEY, ethProvider)
_26
const ethAdapterOwnerA = new EthersAdapter({
_26
ethers,
_26
signerOrProvider: ownerA
_26
})
_26
_26
// Initialize the Protocol Kit with Owner A
_26
const protocolKitOwnerA = await Safe.create({
_26
ethAdapter: ethAdapterOwnerA,
_26
safeAddress: config.SAFE_ADDRESS
_26
})
_26
_26
// Create a Safe transaction
_26
const safeTransactionData: MetaTransactionData = {
_26
to: config.TO,
_26
value: config.VALUE,
_26
data: '0x',
_26
operation: OperationType.Call
_26
}
_26
_26
const safeTransaction = await protocolKitOwnerA.createTransaction({
_26
transactions: [safeTransactionData]
_26
})

Sign the transaction


_10
// Sign the transaction with Owner A
_10
const safeTxHash = await protocolKitOwnerA.getTransactionHash(safeTransaction)
_10
const signatureOwnerA = await protocolKitOwnerA.signHash(safeTxHash)

Send the transaction to the service


_15
// Initialize the API Kit
_15
const apiKit = new SafeApiKit({
_15
chainId: 11155111n
_15
})
_15
_15
const senderAddress = await ownerA.getAddress()
_15
_15
// Send the transaction to the Transaction Service with the signature from Owner A
_15
await 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


_10
const signedTransaction = await apiKit.getTransaction(safeTxHash)

Add missing signatures


_21
// Instantiate an EthAdapter with Owner B
_21
const ownerB = new ethers.Wallet(config.OWNER_B_PRIVATE_KEY, ethProvider)
_21
const ethAdapterOwnerB = new EthersAdapter({
_21
ethers,
_21
signerOrProvider: ownerB
_21
})
_21
_21
// Initialize the Protocol Kit with Owner B
_21
const protocolKitOwnerB = await Safe.create({
_21
ethAdapter: ethAdapterOwnerB,
_21
safeAddress: config.SAFE_ADDRESS
_21
})
_21
_21
// Sign the transaction with Owner B
_21
const signatureOwnerB = await protocolKitOwnerB.signHash(safeTxHash)
_21
_21
// Send the transaction to the Transaction Service with the signature from Owner B
_21
await apiKit.confirmTransaction(
_21
safeTxHash,
_21
signatureOwnerB.data
_21
)

Execute the transaction


_10
const transactionResponse =
_10
await protocolKitOwnerA.executeTransaction(signedTransaction)

Get the executed transaction


_10
const transactions = await apiKit.getMultisigTransactions(config.SAFE_ADDRESS)
_10
_10
if (transactions.results.length > 0) {
_10
console.log('Last executed transaction', transactions.results[0])
_10
}

Was this page helpful?