> For the complete documentation index, see [llms.txt](https://help.multisig.ledger.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.multisig.ledger.com/guides/transactions-with-off-chain-signatures.md).

# 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](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm#using-a-node-version-manager-to-install-nodejs-and-npm) (when using the TypeScript/JS Kit).
* [Python](https://www.python.org/downloads/) >= 3.9 (when using `safe-eth-py`).
* A configured Safe (Ledger Multisig Account) with a threshold of 2 (requiring at least two signatures).

### Steps

{% stepper %}
{% step %}

### Install dependencies

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

{% tabs %}
{% tab title="TypeScript" %}

```bash
yarn add @safe-global/api-kit @safe-global/protocol-kit @safe-global/types-kit
```

{% endtab %}

{% tab title="Python" %}

```bash
pip install safe-eth-py web3 hexbytes
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### Imports

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

{% tabs %}
{% tab title="TypeScript" %}

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

```

{% endtab %}

{% tab title="Python" %}

```python
from safe_eth.eth import EthereumClient, EthereumNetwork
from safe_eth.safe.api.transaction_service_api import TransactionServiceApi
from safe_eth.safe import Safe
from hexbytes import HexBytes

```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### 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.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// 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]
})
```

{% endtab %}

{% tab title="Python" %}

```python
ethereum_client = EthereumClient(config.get("RPC_URL"))

# Instantiate a Safe
safe = Safe(config.get("SAFE_ADDRESS"), ethereum_client)

# Create the transaction payload
safe_tx = safe.build_multisig_tx(
    config.get("TO"),
    config.get("VALUE"),
    HexBytes(""))
```

{% endtab %}

{% tab title="Curl" %}

```bash
curl -X 'POST' \
'https://multisig.ledger.com/tx-service/sep/api/v1/safes/0xc62C5cbB964ffffffffff82f78A4d30713174b2E/multisig-transactions/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
    "safe": "0xc62C5cbB964459F3C984682f78A4d3ffffffffff",
    "to": "0x795D6C88B4Ea3CCffffffffffCa8a11Bc0496228",
    "value": 2000000000000000,
    "data": null,
    "operation": 0,
    "gasToken": "0x0000000000000000000000000000000000000000",
    "safeTxGas": 0,
    "baseGas": 0,
    "gasPrice": 0,
    "refundReceiver": "0x0000000000000000000000000000000000000000",
    "nonce": 15,
    "contractTransactionHash": "0x56b2931d1053b6afffffffffff3ba29b5c2baafdf1a588850da72a62674941b6",
    "sender": "0xAA86E576c084aCFa56fc4D0E17967ffffffffff8",
    "signature": "0x6a2b57023af16241511619ea95f7cd03d00aa6b79d1ca80e21a0b89cd2c38ffffffffff9b738ffbd680c4d717b9b0c9eae568f3edebc40a0c004700bffffffffff"
}'
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### Generate Initial Signature

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

{% tabs %}
{% tab title="TypeScript" %}

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

{% endtab %}

{% tab title="Python" %}

```python
# Sign the transaction with Owner A
safe_tx.sign(config.get("OWNER_A_PRIVATE_KEY"))
```

{% endtab %}

{% tab title="Curl" %}
We skip this step because the transaction we created in the Transaction Service using Curl already has the signature of the transaction creator. Check the [Create a Safe transaction](https://docs.safe.global/core-api/transaction-service-guides/transactions#create-a-safe-transaction) step.
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Send the transaction to the service <a href="#send-the-transaction-to-the-service" id="send-the-transaction-to-the-service"></a>

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.

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// 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
})
```

{% endtab %}

{% tab title="Python" %}

```python
# Instantiate the Transaction Service API
transaction_service_api = TransactionServiceApi(
    network=EthereumNetwork.SEPOLIA,
    ethereum_client=ethereum_client,
)

# Send the transaction to the Transaction Service with the signature from Owner A
transaction_service_api.post_transaction(safe_tx)
```

{% endtab %}

{% tab title="Curl" %}
We skip this step because the transaction we created using Curl is already in the Transaction Service. Check the [Create a Safe transaction](https://docs.safe.global/core-api/transaction-service-guides/transactions#create-a-safe-transaction) step.
{% endtab %}
{% endtabs %}

{% endstep %}

{% step %}

#### 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:**

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const signedTransaction = await apiKit.getTransaction(safeTxHash)
```

{% endtab %}

{% tab title="Python" %}

```python
(safe_tx_from_tx_service, _) = transaction_service_api.get_safe_transaction(
    safe_tx_hash)
```

{% endtab %}

{% tab title="Curl" %}

```bash
curl -X 'GET' \
'https://multisig.ledger.com/tx-service/sep/api/v1/multisig-transactions/0xe4ceea4ffffffffff0c9ce0af82780ffffffffffd3096836fff2528cb90d156/' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY'
```

{% endtab %}
{% endtabs %}

**Add the missing signature:**

{% tabs %}
{% tab title="TypeScript" %}

```typescript
// 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
)
```

{% endtab %}

{% tab title="Python" %}

```python
# Sign the transaction with Owner B
owner_b_signature = safe_tx_from_tx_service.sign(
    config.get("OWNER_B_PRIVATE_KEY"))

# Send the transaction to the Transaction Service with the signature from Owner B
transaction_service_api.post_signatures(
    safe_tx_from_tx_service.safe_tx_hash,
    owner_b_signature)
```

{% endtab %}

{% tab title="Curl" %}

```bash
curl -X 'POST' \
'https://multisig.ledger.com/tx-service/sep/api/v1/multisig-transactions/0x56b2931ffffffffff303bbaadf3ba29b5c2baafdf1a5ffffffffff62674941b6/confirmations/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
    "signature": "0x8c41aaa029db2942a1574c3ec9442b80764e953f21f994edb641d33ffffffffffa55ffffffffffefcaea5b3360bd4c789803fe289b2ad467fc9b3bedb479dad20"
}'
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Execute the Transaction

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

{% tabs %}
{% tab title="TypeScript" %}

```typescript
const transactionResponse =
  await protocolKitOwnerA.executeTransaction(signedTransaction)
```

{% endtab %}

{% tab title="Python" %}

```python
result = safe_tx_from_tx_service.execute(config.get("OWNER_A_PRIVATE_KEY"))
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### 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.

{% tabs %}
{% tab title="TypeScript" %}

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

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

{% endtab %}

{% tab title="Python" %}

```python
transactions = transaction_service_api.get_transactions(
    config.get("SAFE_ADDRESS"))

last_executed_tx = next(
    (x for x in transactions if x.get('isExecuted')),
    None)
```

{% endtab %}

{% tab title="Curl" %}

```shellscript
curl -X 'GET' \
'https://multisig.ledger.com/tx-service/sep/api/v1/safes/0xc62C5cbB96ffffffffffff2f78A4d3071317ffff/multisig-transactions/?executed=true&limit=1' \
-H 'accept: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY'
```

{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}
