In the previous articles, we explored how to create and unlock Ethereum wallets. Now, we move forward into one of the most critical aspects of blockchain development: transaction handling. This guide dives deep into offline transaction signing for both ETH transfers and ERC-20 token transfers, offering developers a secure, reliable method to manage transactions without exposing private keys to online environments.
Whether you're building a wallet application, a decentralized exchange, or any Web3 service, understanding how to properly sign and broadcast transactions is essential. We'll walk through the full process—from constructing raw transactions and generating signatures to broadcasting them via JSON-RPC.
Understanding Ethereum Offline Transaction Signing
Offline signing allows users to generate and sign transactions on a device disconnected from the internet, significantly reducing the risk of private key exposure. Once signed, the transaction payload (in hexadecimal format) can be safely broadcasted to the Ethereum network using a node or third-party service.
The core workflow consists of three steps:
- Construct the raw transaction with all required parameters.
- Sign it locally using your wallet's credentials.
- Broadcast the signed transaction via
eth_sendRawTransaction.
This approach is widely used in hardware wallets, cold storage systems, and high-security applications.
👉 Discover secure ways to manage digital assets with advanced tools
Signing Native ETH Transfers
To transfer Ether (ETH), you must construct a RawTransaction containing:
nonce: The sender’s transaction count.gasPrice: Price per unit of gas (in Wei).gasLimit: Maximum gas allowed for the transaction.to: Recipient address.value: Amount to send (in Wei).chainId: Identifies the network (e.g., 1 for mainnet).
Here’s a simplified Java implementation using Web3j:
public String signedEthTransactionData(
String to,
BigInteger nonce,
BigInteger gasPrice,
BigInteger gasLimit,
BigDecimal amount,
HLWallet wallet,
String password) throws Exception {
BigDecimal amountInWei = Convert.toWei(amount.toString(), Convert.Unit.ETHER);
RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
nonce, gasPrice, gasLimit, to, amountInWei.toBigInteger());
return signData(rawTransaction, wallet, password);
}
private String signData(RawTransaction rawTransaction, HLWallet wallet, String password) throws Exception {
Credentials credentials = Credentials.create(LWallet.decrypt(password, wallet.walletFile));
byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, ChainId.MAINNET, credentials);
return Numeric.toHexString(signMessage);
}What Is Nonce and Why Does It Matter?
A nonce is a sequential number tied to an Ethereum account's outgoing transactions. It prevents replay attacks and ensures execution order.
Key rules:
- Each transaction from an account must have a unique, incrementing nonce starting at 0.
- A lower-than-expected nonce will be rejected.
- A higher-than-current nonce will queue the transaction until intervening nonces are filled.
- Restarting a node may clear queued transactions.
You can retrieve the current nonce using the JSON-RPC method: eth_getTransactionCount.Handling ERC-20 Token Transfers
Unlike ETH transfers, ERC-20 token movements occur through smart contracts. Instead of sending value directly, you call the transfer function on the token contract.
Manual ABI Encoding Method
An ERC-20 transfer requires encoding data that includes:
- Method ID:
0xa9059cbb— derived fromkeccak256("transfer(address,uint256)"). - Recipient address: Padded to 64 hex characters.
- Amount: Also padded, in the smallest denomination (e.g., wei for tokens with 18 decimals).
Example code:
public String signedContractTransactionData(
String contractAddress,
String to,
BigInteger nonce,
BigInteger gasPrice,
BigInteger gasLimit,
BigDecimal amount,
BigDecimal decimal,
HLWallet wallet,
String password) throws Exception {
BigDecimal realValue = amount.multiply(decimal);
String data = Params.Abi.transfer +
Numeric.toHexStringNoPrefixZeroPadded(Numeric.toBigInt(to), 64) +
Numeric.toHexStringNoPrefixZeroPadded(realValue.toBigInteger(), 64);
RawTransaction rawTransaction = RawTransaction.createTransaction(
nonce, gasPrice, gasLimit, contractAddress, data);
return signData(rawTransaction, wallet, password);
}🔍 The reason
0xa9059cbbis used as the method ID:String methodSig = "transfer(address,uint256)"; byte[] hash = Hash.sha3(methodSig.getBytes()); String methodId = Numeric.toHexString(hash, 0, 4, true); // Result: 0xa9059cbb
This technique follows the Ethereum Contract ABI specification, which defines how functions and parameters are encoded.
👉 Learn how to securely interact with smart contracts using modern platforms
Recommended Approach: Use Web3j’s Built-in Function Encoder
Rather than manually formatting data, use Web3j’s FunctionEncoder to simplify the process and reduce errors.
public String signContractTransaction(
String contractAddress,
String to,
BigInteger nonce,
BigInteger gasPrice,
BigInteger gasLimit,
BigDecimal amount,
BigDecimal decimal,
HLWallet wallet,
String password) throws IOException, CipherException {
BigDecimal realValue = amount.multiply(decimal);
Function function = new Function(
"transfer",
Arrays.asList(new Address(to), new Uint256(realValue.toBigInteger())),
Collections.emptyList()
);
String data = FunctionEncoder.encode(function);
RawTransaction rawTransaction = RawTransaction.createTransaction(
nonce, gasPrice, gasLimit, contractAddress, data);
return signData(rawTransaction, wallet, password);
}This approach abstracts away low-level encoding details and supports complex types like arrays and structs.
Broadcasting the Signed Transaction
Once signed, send the transaction using the JSON-RPC endpoint eth_sendRawTransaction. The input is the signed transaction in hexadecimal format.
Example request:
{
"jsonrpc": "2.0",
"method": "eth_sendRawTransaction",
"params": ["0xf8ab..."],
"id": 1
}On success, the node returns the transaction hash (txHash), which can be used to track status on explorers like Etherscan.
You can verify:
- ETH transfer: Check "Input Data" field — should be empty or
0x. - ERC-20 transfer: Input Data shows Method ID
0xa9059cbb, followed by encoded recipient and amount.
Frequently Asked Questions (FAQ)
Q: After calling sendRawTransaction, why can’t I find the transaction on Etherscan?
A: The transaction hash (txHash) only confirms submission — not confirmation. Due to network congestion or low gas fees, it may take time to be mined. If dropped from mempools, it won't appear. Monitor its status via polling or event listeners.
Q: Why do I get an “invalid sender” error when sending a raw transaction?
A: This usually indicates a chain ID mismatch during signing. Always specify the correct chainId (e.g., 1 for Ethereum Mainnet, 5 for Goerli). Using an incorrect or missing chain ID results in an invalid signature.
byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials);Ensure your signing environment matches the target network.
Q: Can I reuse a signed transaction?
A: No. Once broadcasted — whether successful or failed — a transaction with a specific nonce cannot be reused. However, you can replace it with a new transaction using the same nonce but higher gas price (gas bumping).
Q: How do I handle decimal precision for ERC-20 tokens?
A: Tokens define their own decimals (e.g., USDT uses 6, most use 18). Convert human-readable amounts using:
BigDecimal amountInSmallestUnit = userAmount.multiply(BigDecimal.TEN.pow(tokenDecimals));Always fetch the token’s decimals() value from its contract before calculating.
Q: Is offline signing safe against all attack vectors?
A: While offline signing protects private keys, ensure:
- Accurate
gasPriceandgasLimitto avoid front-running or failure. - Correct
noncetracking to prevent stuck transactions. - Secure transmission of signed payloads.
For maximum security, combine offline signing with hardware wallets or MPC-based solutions.
Core Keywords for SEO
- Ethereum offline transaction signing
- ETH transfer with Web3j
- ERC-20 token transfer coding
- RawTransaction in Java
- Secure blockchain transactions
- Ethereum JSON-RPC methods
- Nonce in Ethereum transactions
- Smart contract interaction
These keywords reflect developer search intent around secure transaction implementation and are naturally integrated throughout this guide.
👉 Explore next-gen tools that simplify blockchain interactions securely
By mastering offline transaction signing, developers enhance both security and user trust in decentralized applications. Whether handling native ETH or ERC-20 tokens, precise construction and proper encoding are non-negotiable for reliability.
With clear patterns for signing logic and robust error handling practices, you’re well-equipped to build resilient Web3 infrastructure.