For the complete documentation index, see llms.txt. This page is also available as Markdown.

Transactions with off-chain signatures

This guide demonstrates how to programmatically interact with the Transaction Service API to create, sign, and execute transactions within the Ledger Multisig environment.

While Ledger Multisig provides a seamless "magical" UI for these operations, developers can leverage the underlying infrastructure to build custom workflows, automated treasury operations, or complex smart contract interactions.

Security Note: Ledger Multisig adds an experience and security layer on top of Safe's infrastructure. The code below utilizes the standard protocol kits, ensuring full compatibility with your existing Safe setup while preparing you for the Ledger ecosystem.

Prerequisites

  • Node.js and npm (when using the TypeScript/JS Kit).

  • Python >= 3.9 (when using safe-eth-py).

  • A configured Safe (Ledger Multisig Account) with a threshold of 2 (requiring at least two signatures).

Steps

1

Install dependencies

To begin, install the necessary kits to interact with the protocol and the API.

yarn add @safe-global/api-kit @safe-global/protocol-kit @safe-global/types-kit
2

Imports

Import the necessary modules to handle the transaction logic and type definitions.

import SafeApiKit from '@safe-global/api-kit'
import Safe from '@safe-global/protocol-kit'
import {
  MetaTransactionData,
  OperationType
} from '@safe-global/types-kit'
3

Create a multisig transaction

Initialize the Protocol Kit with the credentials of the first signer (Owner A). In a production Ledger Multisig environment, this ensures the transaction is properly formatted before being proposed to the network.

// Initialize the Protocol Kit with Owner A
const protocolKitOwnerA = await Safe.init({
  provider: config.RPC_URL,
  signer: config.OWNER_A_PRIVATE_KEY,
  safeAddress: config.SAFE_ADDRESS
})

// Create the transaction payload
const safeTransactionData: MetaTransactionData = {
  to: config.TO,
  value: config.VALUE,
  data: '0x',
  operation: OperationType.Call
}

const safeTransaction = await protocolKitOwnerA.createTransaction({
  transactions: [safeTransactionData]
})
4

Generate Initial Signature

Before the transaction can be shared with other owners via the Transaction Service, it must be signed by the initiator.

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

Send the transaction to the service

Once signed, the transaction is sent to the Transaction Service. This makes the transaction visible in the Pending Transactions section of the Ledger Multisig interface, allowing other owners to review and sign it.

// Initialize the API Kit
// How to get an Api key => http://docs.safe.global/core-api/how-to-use-api-keys
const apiKit = new SafeApiKit({
  chainId: 11155111n
})

// Send the transaction to the Transaction Service with the signature from Owner A
await apiKit.proposeTransaction({
  safeAddress: config.SAFE_ADDRESS,
  safeTransactionData: safeTransaction.data,
  safeTxHash,
  senderAddress: config.OWNER_A_ADDRESS,
  senderSignature: signatureOwnerA.data
})

6

Gather Multi-Signature Approvals

For a threshold of 2, a second signer (Owner B) must retrieve the pending transaction and append their signature. In a real-world scenario, Owner B could also be a Ledger Stax or Flex user signing via the interface.

Get the pending transaction:

const signedTransaction = await apiKit.getTransaction(safeTxHash)

Add the missing signature:

// Initialize the Protocol Kit with Owner B
const protocolKitOwnerB = await Safe.init({
  provider: config.RPC_URL,
  signer: config.OWNER_B_PRIVATE_KEY,
  safeAddress: config.SAFE_ADDRESS
})

// Sign the transaction with Owner B
const signatureOwnerB = await protocolKitOwnerB.signHash(safeTxHash)

// Send the transaction to the Transaction Service with the signature from Owner B
await apiKit.confirmTransaction(
  safeTxHash,
  signatureOwnerB.data
)
7

Execute the Transaction

Once the threshold is met, the transaction is ready for execution. This broadcasts the fully signed transaction to the blockchain.

const transactionResponse =
  await protocolKitOwnerA.executeTransaction(signedTransaction)
8

Verify Execution

You can query the history to verify the transaction has been successfully indexed. This will reflect in the "Latest Transactions" view on your Ledger Multisig Dashboard.

const transactions = await apiKit.getMultisigTransactions(config.SAFE_ADDRESS)

if (transactions.results.length > 0) {
  console.log('Last executed transaction', transactions.results[0])
}

Last updated