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
Feature Sign Transaction Send Transaction Signs transaction ✅ ✅ Broadcasts to network ❌ ✅ Returns Signed transaction bytes Transaction signature Use case Advanced workflows Standard 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:
Configuration for the signing confirmation modal. Custom title for the confirmation modal.
Description text shown in the modal.
Text for the confirm button.
Text for the cancel button.
Additional transaction information to display. Show transactionInfo properties
Description of the action being performed.
Estimated cost of the transaction.
Whether to show the confirmation modal. Set to false to sign without confirmation.
Hook Callbacks
onSuccess
(result: { signedTransaction: Uint8Array }) => void
Callback executed after successful signing. Receives the signed transaction as Uint8Array.
Callback executed if signing fails or user cancels.
Returns
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 );
Store signed transactions securely
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 });
Confirm after broadcasting
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 );
Signature verification failed
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
Cannot deserialize signed transaction
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