---
title: Offline Transactions
description: Learn how to create and sign transactions offline.
---

## Sign Transaction

To create an offline transaction, you have to sign the transaction and then
anyone can broadcast it on the network.

<CodeTabs storage="cookbook">

```typescript !! title="Legacy" file=packages/docs-examples/cookbook/transactions/offline-transactions/legacy.ts#region=sign

```

```py !! title="Python"
#!/usr/bin/env python3
"""
Solana Cookbook - Offline Transactions

To complete an offline transaction:
1. Create Transaction
2. Sign Transaction
3. Recover Transaction
4. Send Transaction
"""

import asyncio
from solana.rpc.async_api import AsyncClient
from solders.keypair import Keypair
from solders.system_program import transfer, TransferParams
from solders.transaction import VersionedTransaction
from solders.message import MessageV0
from solders.pubkey import Pubkey
from solders.hash import Hash
import nacl.signing
import nacl.encoding
import base58


async def main():
    # Create connection
    connection = AsyncClient("http://localhost:8899")

    # Create an example tx, alice transfer to bob and feePayer is `fee_payer`
    # alice and fee_payer are signers in this tx
    fee_payer = Keypair()

    # Airdrop to fee_payer
    airdrop_resp = await connection.request_airdrop(fee_payer.pubkey(), 1_000_000_000)
    await connection.confirm_transaction(airdrop_resp.value)

    alice = Keypair()
    # Airdrop to alice
    airdrop_resp = await connection.request_airdrop(alice.pubkey(), 1_000_000_000)
    await connection.confirm_transaction(airdrop_resp.value)

    bob = Keypair()

    # 1. Create Transaction
    transfer_instruction = transfer(
        TransferParams(
            from_pubkey=alice.pubkey(),
            to_pubkey=bob.pubkey(),
            lamports=int(0.1 * 1_000_000_000)  # 0.1 SOL
        )
    )

    # Get recent blockhash
    recent_blockhash_resp = await connection.get_latest_blockhash()
    recent_blockhash = recent_blockhash_resp.value.blockhash

    # Create transaction message
    message = MessageV0.try_compile(
        payer=fee_payer.pubkey(),
        instructions=[transfer_instruction],
        address_lookup_table_accounts=[],
        recent_blockhash=recent_blockhash
    )

    # Serialize the message that needs to be signed
    message_bytes = bytes(message)
    print(f"Message to sign (length: {len(message_bytes)} bytes)")

    # 2. Sign Transaction
    # Use nacl to sign the message
    fee_payer_signing_key = nacl.signing.SigningKey(bytes(fee_payer)[0:32])
    fee_payer_signature = fee_payer_signing_key.sign(message_bytes).signature

    alice_signing_key = nacl.signing.SigningKey(bytes(alice)[0:32])
    alice_signature = alice_signing_key.sign(message_bytes).signature

    print(f"Fee payer signature: {base58.b58encode(fee_payer_signature).decode()}")
    print(f"Alice signature: {base58.b58encode(alice_signature).decode()}")

    # 3. Recover Transaction

    # You can verify signatures before recovering the transaction
    fee_payer_verify_key = nacl.signing.VerifyKey(bytes(fee_payer.pubkey()))
    try:
        fee_payer_verify_key.verify(message_bytes, fee_payer_signature)
        print("✓ Fee payer signature verified")
    except nacl.exceptions.BadSignatureError:
        print("✗ Fee payer signature verification failed")

    alice_verify_key = nacl.signing.VerifyKey(bytes(alice.pubkey()))
    try:
        alice_verify_key.verify(message_bytes, alice_signature)
        print("✓ Alice signature verified")
    except nacl.exceptions.BadSignatureError:
        print("✗ Alice signature verification failed")

    # 3. Recover Transaction
    print("\n=== Recover and Send Transaction ===")
    recover_tx = VersionedTransaction(message, [fee_payer, alice])

    # 4. Send transaction
    try:
        txhash = await connection.send_raw_transaction(bytes(recover_tx))
        print(f"Transaction hash: {txhash}")

        # Confirm transaction
        await connection.confirm_transaction(txhash.value)
        print("✓ Transaction confirmed")
    except Exception as e:
        print(f"Transaction failed: {e}")

    # Serialize for transmission
    serialized_tx = bytes(recover_tx)
    print(f"Serialized transaction: {base58.b58encode(serialized_tx).decode()[:100]}...")

    print("\n=== Note ===")
    print("If this process takes too long, your recent blockhash will expire (after 150 blocks).")
    print("You can use 'durable nonce' to avoid blockhash expiration.")

    await connection.close()


if __name__ == "__main__":
    asyncio.run(main())
```

</CodeTabs>

## Partial Sign Transaction

When a transaction requires multiple signatures, you can partially sign it. The
other signers can then sign and broadcast it on the network.

Some examples of when this is useful:

- Send an SPL token in return for payment
- Sign a transaction so that you can later verify its authenticity
- Call custom programs in a transaction that require your signature

In this example Bob sends Alice an SPL token in return for her payment:

```typescript title="Legacy"
import {
  createTransferCheckedInstruction,
  getAssociatedTokenAddress,
  getMint,
  getOrCreateAssociatedTokenAccount
} from "@solana/spl-token";
import {
  clusterApiUrl,
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction
} from "@solana/web3.js";
import base58 from "bs58";

/* The transaction:
 * - sends 0.01 SOL from Alice to Bob
 * - sends 1 token from Bob to Alice
 * - is partially signed by Bob, so Alice can approve + send it
 */

(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const alicePublicKey = new PublicKey(
    "5YNmS1R9nNSCDzb5a7mMJ1dwK9uHeAAF4CmPEwKgVWr8"
  );
  const bobKeypair = Keypair.fromSecretKey(
    base58.decode(
      "4NMwxzmYj2uvHuq8xoqhY8RXg63KSVJM1DXkpbmkUY7YQWuoyQgFnnzn6yo3CMnqZasnNPNuAT2TLwQsCaKkUddp"
    )
  );
  const tokenAddress = new PublicKey(
    "Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"
  );
  const bobTokenAddress = await getAssociatedTokenAddress(
    tokenAddress,
    bobKeypair.publicKey
  );

  // Alice may not have a token account, so Bob creates one if not
  const aliceTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    bobKeypair, // Bob pays the fee to create it
    tokenAddress, // which token the account is for
    alicePublicKey // who the token account is for
  );

  // Get the details about the token mint
  const tokenMint = await getMint(connection, tokenAddress);

  // Get a recent blockhash to include in the transaction
  const { blockhash } = await connection.getLatestBlockhash("finalized");

  const transaction = new Transaction({
    recentBlockhash: blockhash,
    // Alice pays the transaction fee
    feePayer: alicePublicKey
  });

  // Transfer 0.01 SOL from Alice -> Bob
  transaction.add(
    SystemProgram.transfer({
      fromPubkey: alicePublicKey,
      toPubkey: bobKeypair.publicKey,
      lamports: 0.01 * LAMPORTS_PER_SOL
    })
  );

  // Transfer 1 token from Bob -> Alice
  transaction.add(
    createTransferCheckedInstruction(
      bobTokenAddress, // source
      tokenAddress, // mint
      aliceTokenAccount.address, // destination
      bobKeypair.publicKey, // owner of source account
      1 * 10 ** tokenMint.decimals, // amount to transfer
      tokenMint.decimals // decimals of token
    )
  );

  // Partial sign as Bob
  transaction.partialSign(bobKeypair);

  // Serialize the transaction and convert to base64 to return it
  const serializedTransaction = transaction.serialize({
    // We will need Alice to deserialize and sign the transaction
    requireAllSignatures: false
  });
  const transactionBase64 = serializedTransaction.toString("base64");
  return transactionBase64;

  // The caller of this can convert it back to a transaction object:
  const recoveredTransaction = Transaction.from(
    Buffer.from(transactionBase64, "base64")
  );
})();
```

## Durable Nonce

`recentBlockhash` is an important value for a transaction. Your transaction
will  
be rejected if you use an expired blockhash (older than 150 blocks). Instead of
a recent blockhash, you can use a durable nonce, which never expires. To use a
durable nonce, your transaction must:

1. use a `nonce` stored in `nonce account` as a recent blockhash
2. put `nonce advance` operation in the first instruction

### Create Nonce Account

```typescript title="Legacy"
import {
  clusterApiUrl,
  Connection,
  Keypair,
  Transaction,
  NONCE_ACCOUNT_LENGTH,
  SystemProgram,
  LAMPORTS_PER_SOL
} from "@solana/web3.js";

(async () => {
  // Setup our connection and wallet
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const feePayer = Keypair.generate();

  // Fund our wallet with 1 SOL
  const airdropSignature = await connection.requestAirdrop(
    feePayer.publicKey,
    LAMPORTS_PER_SOL
  );
  await connection.confirmTransaction(airdropSignature);

  // you can use any keypair as nonce account authority,
  // this uses the default Solana keypair file (id.json) as the nonce account authority
  const nonceAccountAuth = await getKeypairFromFile();

  let nonceAccount = Keypair.generate();
  console.log(`nonce account: ${nonceAccount.publicKey.toBase58()}`);

  let tx = new Transaction().add(
    // create nonce account
    SystemProgram.createAccount({
      fromPubkey: feePayer.publicKey,
      newAccountPubkey: nonceAccount.publicKey,
      lamports:
        await connection.getMinimumBalanceForRentExemption(
          NONCE_ACCOUNT_LENGTH
        ),
      space: NONCE_ACCOUNT_LENGTH,
      programId: SystemProgram.programId
    }),
    // init nonce account
    SystemProgram.nonceInitialize({
      noncePubkey: nonceAccount.publicKey, // nonce account pubkey
      authorizedPubkey: nonceAccountAuth.publicKey // nonce account authority (for advance and close)
    })
  );

  console.log(
    `txhash: ${await sendAndConfirmTransaction(connection, tx, [feePayer, nonceAccount])}`
  );
})();
```

### Get Nonce Account

```typescript title="get-nonce-account.ts"
import {
  clusterApiUrl,
  Connection,
  PublicKey,
  Keypair,
  NonceAccount
} from "@solana/web3.js";

(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  const nonceAccountPubkey = new PublicKey(
    "7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4"
  );

  let accountInfo = await connection.getAccountInfo(nonceAccountPubkey);
  let nonceAccount = NonceAccount.fromAccountData(accountInfo.data);
  console.log(`nonce: ${nonceAccount.nonce}`);
  console.log(`authority: ${nonceAccount.authorizedPubkey.toBase58()}`);
  console.log(`fee calculator: ${JSON.stringify(nonceAccount.feeCalculator)}`);
})();
```

### Use Nonce Account

```typescript title="Legacy"
import {
  clusterApiUrl,
  Connection,
  PublicKey,
  Keypair,
  Transaction,
  SystemProgram,
  NonceAccount,
  LAMPORTS_PER_SOL
} from "@solana/web3.js";
import * as bs58 from "bs58";
import { getKeypairFromFile } from "@solana-developers/helpers";

(async () => {
  // Setup our connection and wallet
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const feePayer = Keypair.generate();

  // Fund our wallet with 1 SOL
  const airdropSignature = await connection.requestAirdrop(
    feePayer.publicKey,
    LAMPORTS_PER_SOL
  );
  await connection.confirmTransaction(airdropSignature);

  // you can use any keypair as nonce account authority,
  // but nonceAccountAuth must be the same as the one used in nonce account creation
  // load default solana keypair for nonce account authority
  const nonceAccountAuth = await getKeypairFromFile();

  const nonceAccountPubkey = new PublicKey(
    "7H18z3v3rZEoKiwY3kh8DLn9eFT6nFCQ2m4kiC7RZ3a4"
  );
  let nonceAccountInfo = await connection.getAccountInfo(nonceAccountPubkey);
  let nonceAccount = NonceAccount.fromAccountData(nonceAccountInfo.data);

  let tx = new Transaction().add(
    // nonce advance must be the first instruction
    SystemProgram.nonceAdvance({
      noncePubkey: nonceAccountPubkey,
      authorizedPubkey: nonceAccountAuth.publicKey
    }),
    // after that, you do what you really want to do, here we append a transfer instruction as an example.
    SystemProgram.transfer({
      fromPubkey: feePayer.publicKey,
      toPubkey: nonceAccountAuth.publicKey,
      lamports: 1
    })
  );
  // assign `nonce` as recentBlockhash
  tx.recentBlockhash = nonceAccount.nonce;
  tx.feePayer = feePayer.publicKey;
  tx.sign(
    feePayer,
    nonceAccountAuth
  ); /* fee payer + nonce account authority + ... */

  console.log(`txhash: ${await connection.sendRawTransaction(tx.serialize())}`);
})();
```
