Skip to main content

Sign Transaction

Sign Solana transactions without broadcasting them to the network. This is useful for advanced use cases like off-chain transaction batching, meta-transactions, or custom transaction submission workflows.

Overview

Signing transactions without sending allows you to:
  • Batch transactions: Collect multiple signed transactions before submitting
  • Off-chain processing: Sign now, broadcast later
  • Custom submission: Use custom RPC logic or transaction relayers
  • Transaction inspection: Review signed transaction data before broadcasting
Unlike sendTransaction which signs and broadcasts immediately, signTransaction only signs the transaction and returns it for you to broadcast separately.

Signing vs Sending

FeatureSign TransactionSend Transaction
Signs transaction
Broadcasts to network
ReturnsSigned transaction bytesTransaction signature
Use caseAdvanced workflowsStandard transfers

React SDK

  • Basic Usage
  • With UI Customization
  • With Callbacks
To sign a transaction, use the signTransaction method from the useSignTransaction hook:
import { useMoonKey } from '@moon-key/react-auth';
import { useSignTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  SystemProgram,
  PublicKey,
  Connection,
  LAMPORTS_PER_SOL
} from '@solana/web3.js';

function SignTransactionButton() {
  const { user } = useMoonKey();
  const { signTransaction } = useSignTransaction();

  const handleSign = async () => {
    if (!user?.wallet) {
      alert('No wallet found');
      return;
    }

    try {
      const connection = new Connection('https://api.devnet.solana.com');

      // Create transaction
      const transaction = new Transaction().add(
        SystemProgram.transfer({
          fromPubkey: new PublicKey(user.wallet.address),
          toPubkey: new PublicKey('RecipientAddressHere...'),
          lamports: 0.001 * LAMPORTS_PER_SOL
        })
      );

      // Get recent blockhash
      const { blockhash } = await connection.getLatestBlockhash();
      transaction.recentBlockhash = blockhash;
      transaction.feePayer = new PublicKey(user.wallet.address);

      // Sign transaction (does not broadcast)
      const { signedTransaction } = await signTransaction({
        transaction
      });

      console.log('Transaction signed:', signedTransaction);
      alert('Transaction signed successfully!');

      // You can now broadcast it later or store it
    } catch (error) {
      console.error('Failed to sign transaction:', error);
    }
  };

  return (
    <button onClick={handleSign}>
      Sign Transaction
    </button>
  );
}

Parameters

transaction
Transaction | VersionedTransaction
required
The Solana transaction to sign. Can be either a legacy Transaction or VersionedTransaction from @solana/web3.js.Example:
const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(fromAddress),
    toPubkey: new PublicKey(toAddress),
    lamports: amount
  })
);

Options Object

The second parameter is an optional configuration object:
uiConfig
object
Configuration for the signing confirmation modal.

Hook Callbacks

onSuccess
(result: { signedTransaction: Uint8Array }) => void
Callback executed after successful signing. Receives the signed transaction as Uint8Array.
onError
(error: Error) => void
Callback executed if signing fails or user cancels.

Returns

signedTransaction
Uint8Array
The signed transaction as a Uint8Array. This can be serialized and broadcast to the network.

Complete Examples

Sign and Broadcast Later

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSignTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  SystemProgram,
  PublicKey,
  Connection,
  LAMPORTS_PER_SOL
} from '@solana/web3.js';
import { useState } from 'react';

export default function SignAndBroadcastLater() {
  const { user } = useMoonKey();
  const { signTransaction } = useSignTransaction();
  const [signedTx, setSignedTx] = useState<Uint8Array | null>(null);

  const handleSign = async () => {
    if (!user?.wallet) return;

    const connection = new Connection('https://api.devnet.solana.com');

    const transaction = new Transaction().add(
      SystemProgram.transfer({
        fromPubkey: new PublicKey(user.wallet.address),
        toPubkey: new PublicKey('RecipientAddressHere...'),
        lamports: 0.001 * LAMPORTS_PER_SOL
      })
    );

    const { blockhash } = await connection.getLatestBlockhash();
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = new PublicKey(user.wallet.address);

    try {
      const { signedTransaction } = await signTransaction({
        transaction
      }, {
        uiConfig: {
          title: 'Sign Transaction',
          description: 'Sign transaction to broadcast later',
          confirmButtonText: 'Sign'
        }
      });

      setSignedTx(signedTransaction);
      alert('Transaction signed! Ready to broadcast.');
    } catch (error) {
      console.error('Signing failed:', error);
    }
  };

  const handleBroadcast = async () => {
    if (!signedTx) return;

    try {
      const connection = new Connection('https://api.devnet.solana.com');

      // Deserialize and send the signed transaction
      const transaction = Transaction.from(signedTx);
      const signature = await connection.sendRawTransaction(signedTx);

      console.log('Transaction broadcast:', signature);
      
      // Wait for confirmation
      await connection.confirmTransaction(signature, 'confirmed');
      
      alert(`Transaction confirmed! Signature: ${signature}`);
      setSignedTx(null);
    } catch (error) {
      console.error('Broadcast failed:', error);
      alert('Failed to broadcast transaction');
    }
  };

  return (
    <div className="sign-and-broadcast">
      <h2>Sign Now, Broadcast Later</h2>

      <button onClick={handleSign} disabled={signedTx !== null}>
        Sign Transaction
      </button>

      {signedTx && (
        <div className="signed-tx-actions">
          <p>✅ Transaction signed and ready</p>
          <button onClick={handleBroadcast}>
            Broadcast Transaction
          </button>
        </div>
      )}
    </div>
  );
}

Batch Transaction Signing

'use client';
import { useMoonKey } from '@moon-key/react-auth';
import { useSignTransaction } from '@moon-key/react-auth/solana';
import {
  Transaction,
  SystemProgram,
  PublicKey,
  Connection,
  LAMPORTS_PER_SOL
} from '@solana/web3.js';
import { useState } from 'react';

export default function BatchTransactionSigning() {
  const { user } = useMoonKey();
  const { signTransaction } = useSignTransaction();
  const [signedTxs, setSignedTxs] = useState<Uint8Array[]>([]);

  const handleSignBatch = async () => {
    if (!user?.wallet) return;

    const connection = new Connection('https://api.devnet.solana.com');
    const { blockhash } = await connection.getLatestBlockhash();

    // Create multiple transactions
    const recipients = [
      'Recipient1AddressHere...',
      'Recipient2AddressHere...',
      'Recipient3AddressHere...'
    ];

    const signed: Uint8Array[] = [];

    try {
      for (const recipient of recipients) {
        const transaction = new Transaction().add(
          SystemProgram.transfer({
            fromPubkey: new PublicKey(user.wallet.address),
            toPubkey: new PublicKey(recipient),
            lamports: 0.001 * LAMPORTS_PER_SOL
          })
        );

        transaction.recentBlockhash = blockhash;
        transaction.feePayer = new PublicKey(user.wallet.address);

        const { signedTransaction } = await signTransaction({
          transaction
        }, {
          uiConfig: {
            title: `Sign Transaction ${signed.length + 1}/${recipients.length}`,
            description: `Signing batch transaction ${signed.length + 1} of ${recipients.length}`,
            confirmButtonText: 'Sign'
          }
        });

        signed.push(signedTransaction);
      }

      setSignedTxs(signed);
      alert(`All ${signed.length} transactions signed!`);
    } catch (error) {
      console.error('Batch signing failed:', error);
      alert('Failed to sign all transactions');
    }
  };

  const handleBroadcastBatch = async () => {
    if (signedTxs.length === 0) return;

    const connection = new Connection('https://api.devnet.solana.com');
    const signatures: string[] = [];

    try {
      for (const signedTx of signedTxs) {
        const signature = await connection.sendRawTransaction(signedTx);
        signatures.push(signature);
        console.log('Transaction sent:', signature);
      }

      alert(`All ${signatures.length} transactions broadcast!`);
      setSignedTxs([]);
    } catch (error) {
      console.error('Batch broadcast failed:', error);
    }
  };

  return (
    <div className="batch-signing">
      <h2>Batch Transaction Signing</h2>

      <button onClick={handleSignBatch} disabled={signedTxs.length > 0}>
        Sign Batch Transactions
      </button>

      {signedTxs.length > 0 && (
        <div className="batch-status">
          <p>{signedTxs.length} transactions signed</p>
          <button onClick={handleBroadcastBatch}>
            Broadcast All
          </button>
        </div>
      )}
    </div>
  );
}

Broadcasting Signed Transactions

After signing a transaction, you can broadcast it using Solana’s connection:
import { Connection } from '@solana/web3.js';

// Broadcast signed transaction
const connection = new Connection('https://api.devnet.solana.com');
const signature = await connection.sendRawTransaction(signedTransaction);

console.log('Transaction signature:', signature);

// Wait for confirmation
const confirmation = await connection.confirmTransaction(signature, 'confirmed');

if (confirmation.value.err) {
  console.error('Transaction failed');
} else {
  console.log('Transaction confirmed!');
}

With Custom Commitment

// Use different commitment levels
await connection.confirmTransaction(signature, 'processed'); // Fast, less secure
await connection.confirmTransaction(signature, 'confirmed'); // Balanced
await connection.confirmTransaction(signature, 'finalized'); // Slow, most secure

Transaction Serialization

Serialize to Base64

import { Transaction } from '@solana/web3.js';

// Convert signed transaction to base64
const transaction = Transaction.from(signedTransaction);
const base64Tx = Buffer.from(signedTransaction).toString('base64');

console.log('Base64 transaction:', base64Tx);

Deserialize from Base64

// Convert base64 back to transaction
const txBuffer = Buffer.from(base64Tx, 'base64');
const transaction = Transaction.from(txBuffer);

UI Customization

Customize the signing modal:
await signTransaction({
  transaction
}, {
  uiConfig: {
    title: 'Sign Transaction',
    description: 'Review and sign this transaction',
    confirmButtonText: 'Sign',
    cancelButtonText: 'Cancel',
    transactionInfo: {
      action: 'Transfer SOL',
      cost: '0.001 SOL'
    }
  }
});

Hide Modal

await signTransaction({
  transaction
}, {
  uiConfig: {
    showWalletUI: false
  }
});
Hiding the modal removes the user’s ability to review the transaction before signing. Only use this for trusted operations or after obtaining explicit user consent.

Best Practices

Always fetch a recent blockhash before signing:
// ✅ Good - Fresh blockhash
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;

// ❌ Bad - Reusing old blockhash
// Blockhashes expire after ~60 seconds
Always specify the fee payer:
transaction.feePayer = new PublicKey(user.wallet.address);
If storing signed transactions, use secure storage:
// Encrypt before storing
const encrypted = encryptData(signedTransaction);
localStorage.setItem('signedTx', encrypted);

// Decrypt when broadcasting
const decrypted = decryptData(localStorage.getItem('signedTx'));
await connection.sendRawTransaction(decrypted);
Signed transactions can expire if blockhash is old:
try {
  const signature = await connection.sendRawTransaction(signedTransaction);
} catch (error: any) {
  if (error.message?.includes('blockhash')) {
    alert('Transaction expired. Please sign again.');
    // Re-sign with fresh blockhash
  }
}
Check transaction validity before signing:
// Validate recipient address
if (!isValidSolanaAddress(recipient)) {
  alert('Invalid recipient address');
  return;
}

// Check balance
const balance = await connection.getBalance(
  new PublicKey(user.wallet.address)
);

if (balance < transferAmount) {
  alert('Insufficient balance');
  return;
}

// Now sign
await signTransaction({ transaction });
Always wait for confirmation after broadcasting:
const signature = await connection.sendRawTransaction(signedTransaction);

// Wait for confirmation with timeout
const confirmation = await connection.confirmTransaction(
  signature,
  'confirmed'
);

if (confirmation.value.err) {
  throw new Error('Transaction failed');
}

Troubleshooting

Signed transactions expire if blockhash is too old:
// Ensure fresh blockhash when signing
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;

// Check if still valid before broadcasting
const currentBlockHeight = await connection.getBlockHeight();

if (currentBlockHeight > lastValidBlockHeight) {
  alert('Transaction expired, please sign again');
  return;
}

await connection.sendRawTransaction(signedTransaction);
Ensure transaction is properly constructed:
// ✅ Correct - Set all required fields
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(user.wallet.address);

// All signers must be specified
transaction.sign(); // If additional signers needed
Use proper serialization/deserialization:
import { Transaction } from '@solana/web3.js';

// Serialize
const serialized = signedTransaction;

// Deserialize
const transaction = Transaction.from(serialized);
Handle cancellations gracefully:
const { signTransaction } = useSignTransaction({
  onError: (error: any) => {
    if (error.code === 'USER_REJECTED') {
      console.log('User cancelled signing');
      return;
    }
    console.error('Signing error:', error);
  }
});

Next Steps