Ethereum Offline Transaction Signing: A Developer’s Guide to ETH and ERC-20 Transfers

·

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:

  1. Construct the raw transaction with all required parameters.
  2. Sign it locally using your wallet's credentials.
  3. 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:

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:

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:

  1. Method ID: 0xa9059cbb — derived from keccak256("transfer(address,uint256)").
  2. Recipient address: Padded to 64 hex characters.
  3. 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 0xa9059cbb is 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:


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:

For maximum security, combine offline signing with hardware wallets or MPC-based solutions.


Core Keywords for SEO

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.