Sign transactions from a user’s embedded Ethereum wallet without broadcasting them to the network. This allows you to review, store, or broadcast transactions at a later time.
Overview
The useSignTransaction hook from MoonKey’s React SDK provides an interface for signing transactions without immediately broadcasting them. This is useful for scenarios where you need to:
Batch multiple transactions for later submission
Implement custom transaction broadcasting logic
Store signed transactions for delayed execution
Create transactions that will be submitted by a relayer
Signing vs Sending : signTransaction only signs the transaction and returns the signature, while sendTransaction both signs and broadcasts the transaction to the network.
React SDK
Basic Usage
With UI Customization
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/ethereum' ;
function SignTransactionButton () {
const { user } = useMoonKey ();
const { signTransaction } = useSignTransaction ();
const handleSign = async () => {
if ( ! user ?. wallet ) {
alert ( 'No wallet found' );
return ;
}
try {
const result = await signTransaction ({
to: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' ,
value: '0x38d7ea4c68000' , // 0.001 ETH in wei (hex)
chainId: 1 // Mainnet
});
console . log ( 'Transaction signed:' , result . signedTransaction );
console . log ( 'Signature:' , result . signature );
} catch ( error ) {
console . error ( 'Signing failed:' , error );
}
};
return (
< button onClick = { handleSign } >
Sign Transaction
</ button >
);
}
Parameters
Transaction Object
The first parameter to signTransaction is the transaction object:
The recipient address for the transaction. Example: to : '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
The amount of ETH to send in wei, as a hexadecimal string. Omit for contract interactions with no ETH transfer. Example: value : '0x38d7ea4c68000' // 0.001 ETH
The encoded function call data for contract interactions, as a hexadecimal string. Example: data : '0x095ea7b3000000000000000000000000...'
The chain ID for the transaction. Example: chainId : 1 // Ethereum Mainnet
Optional gas limit as a hexadecimal string. MoonKey estimates gas automatically if not provided. Example: gasLimit : '0x5208' // 21000 in decimal
Optional maximum fee per gas for EIP-1559 transactions, as a hexadecimal string. Example: maxFeePerGas : '0x59682f00' // 1.5 gwei
Optional maximum priority fee per gas for EIP-1559 transactions, as a hexadecimal string. Example: maxPriorityFeePerGas : '0x3b9aca00' // 1 gwei
Optional nonce for the transaction. MoonKey determines this automatically if not provided. Example:
Options Object
The second parameter is an optional configuration object:
Configuration for the signing confirmation modal UI. Custom title for the confirmation modal.
Description text shown in the modal.
Text for the confirm button.
Text for the cancel button.
Whether to show the confirmation modal. Set to false to sign without confirmation.
Specific wallet to use for signing. If not provided, uses the user’s default wallet. Example:
Returns
The signTransaction method returns a Promise that resolves with:
The RLP-encoded signed transaction, ready to be broadcast. Example: "0x02f8730182012a8459682f00843b9aca0082520894d8da6bf26964af9d7eed9e03e53415d37aa9604587038d7ea4c6800080c080a0..."
The transaction signature. Example: "0xa0cd31b38c3e3a48230bee2d5c687a0b2a5efcb298c58cfc3b43449eefd17857cf..."
The encoding format of the signed transaction. Currently always 'rlp' for Ethereum.
Complete Examples
Contract Interaction Signing
'use client' ;
import { useMoonKey } from '@moon-key/react-auth' ;
import { useSignTransaction } from '@moon-key/react-auth/ethereum' ;
import { encodeFunctionData } from 'viem' ;
// ERC-20 ABI
const ERC20_ABI = [{
name: 'transfer' ,
type: 'function' ,
inputs: [
{ name: 'to' , type: 'address' },
{ name: 'amount' , type: 'uint256' }
],
outputs: [{ type: 'bool' }]
}];
export default function SignTokenTransfer () {
const { user } = useMoonKey ();
const { signTransaction } = useSignTransaction ();
const handleSignTransfer = async () => {
if ( ! user ?. wallet ) return ;
const tokenAddress = '0xTokenContractAddress...' ;
const recipientAddress = '0xRecipientAddress...' ;
const amount = BigInt ( '1000000000000000000' ); // 1 token (18 decimals)
try {
// Encode the transfer function call
const data = encodeFunctionData ({
abi: ERC20_ABI ,
functionName: 'transfer' ,
args: [ recipientAddress , amount ]
});
// Sign the transaction
const result = await signTransaction ({
to: tokenAddress ,
data ,
chainId: 1
});
console . log ( 'Token transfer signed:' , result . signedTransaction );
// Store or submit the signed transaction
await submitToBackend ( result . signedTransaction );
alert ( 'Token transfer signed successfully!' );
} catch ( error ) {
console . error ( 'Signing failed:' , error );
}
};
const submitToBackend = async ( signedTx : string ) => {
// Submit to your backend for processing
await fetch ( '/api/store-signed-transaction' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ signedTransaction: signedTx })
});
};
return (
< button onClick = { handleSignTransfer } >
Sign Token Transfer
</ button >
);
}
Broadcasting Signed Transactions
After signing a transaction, you can broadcast it using viem’s public client:
import { createPublicClient , http } from 'viem' ;
import { mainnet } from 'viem/chains' ;
// Create a public client
const publicClient = createPublicClient ({
chain: mainnet ,
transport: http ()
});
// Broadcast the signed transaction
const txHash = await publicClient . sendRawTransaction ({
serializedTransaction: signedTransaction as `0x ${ string } `
});
console . log ( 'Transaction broadcasted:' , txHash );
// Wait for confirmation
const receipt = await publicClient . waitForTransactionReceipt ({
hash: txHash
});
console . log ( 'Transaction confirmed:' , receipt . status );
UI Customization
Customize the signing confirmation modal:
import { useSignTransaction } from '@moon-key/react-auth/ethereum' ;
const { signTransaction } = useSignTransaction ();
await signTransaction ({
to: '0x...' ,
value: '0x38d7ea4c68000' ,
chainId: 1
}, {
uiConfig: {
title: 'Sign Transaction' ,
description: 'Review and sign this transaction' ,
confirmButtonText: 'Sign Transaction' ,
cancelButtonText: 'Cancel'
}
});
Hide Modal
For automated flows, you can sign without showing the modal:
await signTransaction ({
to: '0x...' ,
value: '0x38d7ea4c68000' ,
chainId: 1
}, {
uiConfig: {
showWalletUI: false
}
});
Hiding the modal removes the user’s ability to review transaction details before signing. Only use this for trusted operations or after obtaining explicit user consent.
Signing vs Sending
Understanding the difference between signing and sending transactions:
Feature signTransactionsendTransactionSigns transaction ✅ Yes ✅ Yes Broadcasts to network ❌ No ✅ Yes Returns Signed transaction Transaction hash Use case Deferred submission Immediate execution Gas required ❌ Not yet ✅ Yes Reversible ✅ Yes (not broadcasted) ❌ No (already broadcasted)
When to use signTransaction:
Batch operations : Sign multiple transactions and submit together
Relayer services : Submit through a third-party relayer for gasless transactions
Delayed execution : Sign now, broadcast later
Custom broadcasting : Implement your own transaction submission logic
Multi-sig workflows : Collect signatures from multiple parties
When to use sendTransaction:
Immediate execution : Transaction should execute right away
Simple workflows : Standard transaction flow without special handling
User-paid gas : User pays gas fees directly
Real-time updates : Need immediate confirmation
Error Handling
Handle common signing errors:
import { useMoonKey } from '@moon-key/react-auth' ;
import { useSignTransaction } from '@moon-key/react-auth/ethereum' ;
function SignWithErrorHandling () {
const { user } = useMoonKey ();
const { signTransaction } = useSignTransaction ();
const handleSign = async () => {
if ( ! user ?. wallet ) return ;
try {
const result = await signTransaction ({
to: '0x...' ,
value: '0x38d7ea4c68000' ,
chainId: 1
});
console . log ( 'Success:' , result . signedTransaction );
} catch ( error : any ) {
// Handle specific error types
if ( error . code === 'USER_REJECTED' ) {
console . log ( 'User cancelled signing' );
alert ( 'Signing cancelled' );
} else if ( error . code === 'INVALID_PARAMS' ) {
console . log ( 'Invalid transaction parameters' );
alert ( 'Invalid transaction data' );
} else if ( error . code === 'NETWORK_ERROR' ) {
console . log ( 'Network error during signing' );
alert ( 'Network error. Please try again' );
} else {
console . error ( 'Signing failed:' , error );
alert ( 'Failed to sign transaction: ' + error . message );
}
}
};
return < button onClick = { handleSign } > Sign Transaction </ button > ;
}
Best Practices
Store signed transactions securely
If storing signed transactions, use secure storage: // ❌ Don't store in localStorage for sensitive transactions
localStorage . setItem ( 'signedTx' , signedTransaction );
// ✅ Store securely on your backend
await fetch ( '/api/store-transaction' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ userToken } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
signedTransaction ,
userId: user . id ,
timestamp: Date . now ()
})
});
Set appropriate nonce values
When batching transactions, manage nonces carefully: import { createPublicClient , http } from 'viem' ;
import { mainnet } from 'viem/chains' ;
const publicClient = createPublicClient ({
chain: mainnet ,
transport: http ()
});
// Get current nonce
const currentNonce = await publicClient . getTransactionCount ({
address: user . wallet . address as `0x ${ string } `
});
// Sign multiple transactions with incremental nonces
const signedTxs = [];
for ( let i = 0 ; i < transactions . length ; i ++ ) {
const result = await signTransaction ({
... transactions [ i ],
nonce: currentNonce + i ,
chainId: 1
});
signedTxs . push ( result . signedTransaction );
}
Always validate transaction parameters before signing: import { isAddress } from 'viem' ;
const handleSign = async () => {
// Validate address
if ( ! isAddress ( recipient )) {
alert ( 'Invalid recipient address' );
return ;
}
// Validate amount
if ( BigInt ( value ) <= 0 n ) {
alert ( 'Amount must be greater than 0' );
return ;
}
// Validate chain ID
const supportedChains = [ 1 , 8453 , 137 ];
if ( ! supportedChains . includes ( chainId )) {
alert ( 'Unsupported chain' );
return ;
}
// Proceed with signing
await signTransaction ({ to: recipient , value , chainId });
};
Handle transaction expiration
Signed transactions can become stale. Implement expiration logic: interface StoredTransaction {
signedTransaction : string ;
createdAt : number ;
expiresAt : number ;
}
const storeSignedTransaction = ( signedTx : string ) => {
const now = Date . now ();
const expires = now + ( 5 * 60 * 1000 ); // 5 minutes
const stored : StoredTransaction = {
signedTransaction: signedTx ,
createdAt: now ,
expiresAt: expires
};
localStorage . setItem ( 'signedTx' , JSON . stringify ( stored ));
};
const getSignedTransaction = () : string | null => {
const stored = localStorage . getItem ( 'signedTx' );
if ( ! stored ) return null ;
const data : StoredTransaction = JSON . parse ( stored );
// Check if expired
if ( Date . now () > data . expiresAt ) {
localStorage . removeItem ( 'signedTx' );
return null ;
}
return data . signedTransaction ;
};
Provide clear user feedback
Keep users informed about signing status: const [ status , setStatus ] = useState < 'idle' | 'signing' | 'signed' | 'error' >( 'idle' );
const handleSign = async () => {
setStatus ( 'signing' );
try {
const result = await signTransaction ({ to: '0x...' , value: '0x38d7ea4c68000' , chainId: 1 });
setStatus ( 'signed' );
console . log ( 'Transaction signed:' , result . signedTransaction );
} catch ( error ) {
setStatus ( 'error' );
console . error ( 'Signing failed:' , error );
}
};
return (
< div >
< button onClick = { handleSign } disabled = { status === 'signing' } >
{ status === 'signing' && 'Signing...' }
{ status === 'signed' && 'Signed ✓' }
{ status === 'error' && 'Failed ✗' }
{ status === 'idle' && 'Sign Transaction' }
</ button >
{ status === 'signed' && < p > Transaction signed successfully. Ready to broadcast. </ p > }
</ div >
);
Troubleshooting
Signed transaction won't broadcast
Common causes when broadcasting fails:
Stale nonce : The nonce used when signing is outdated
Expired transaction : Too much time passed since signing
Insufficient gas : Gas parameters were underestimated
Invalid format : Signed transaction is malformed
try {
const txHash = await publicClient . sendRawTransaction ({
serializedTransaction: signedTransaction as `0x ${ string } `
});
} catch ( error : any ) {
if ( error . message . includes ( 'nonce' )) {
alert ( 'Transaction nonce is invalid. Please sign again.' );
} else if ( error . message . includes ( 'underpriced' )) {
alert ( 'Gas price is too low. Please sign with higher gas.' );
} else {
console . error ( 'Broadcast error:' , error );
}
}
Handle user cancellations gracefully: try {
const result = await signTransaction ({ to: '0x...' , value: '0x38d7ea4c68000' , chainId: 1 });
} catch ( error : any ) {
if ( error . code === 'USER_REJECTED' ) {
// Don't show error - this is expected behavior
console . log ( 'User cancelled signing' );
return ;
}
// Handle other errors
alert ( 'Signing failed: ' + error . message );
}
Ensure correct chain ID for the target network: const CHAIN_IDS = {
mainnet: 1 ,
base: 8453 ,
polygon: 137 ,
arbitrum: 42161 ,
optimism: 10
};
const handleSign = async ( network : keyof typeof CHAIN_IDS ) => {
const chainId = CHAIN_IDS [ network ];
await signTransaction ({
to: '0x...' ,
value: '0x38d7ea4c68000' ,
chainId
});
};
Next Steps