Block 22VIDEO
Creating and signing a Raw Ethereum Transaction
Build a raw Ethereum transaction, RLP-encode it and sign it with a private key in Node.js, then the same steps with viem for an EIP-1559 transaction on Sepolia.
Today we’re looking into a more technical way of creating and signing a transaction.
We’re doing this with the most basic tools an engineer can possess: a terminal window and some Google-Fu.
So without further talking, take a look at the video below and please follow along with me if you are a technical guy or gal. If you are not, watch it anyways because it’s going to give you a better understanding of the technology behind which you are probably investing in. Remember that what I’m showing you happens in almost every wallet.
Library used: https://github.com/ethereumjs/ethereumjs-tx
Source code:
Warning: the private key in this code is a throwaway demo key. It has been public since 2019, so anyone can spend from its address. Never send funds to it, and never put a real private key in source code.
const ethTx = require('ethereumjs-tx').Transaction;
const txData = {
nonce: '0x0',
gasPrice: '0x09184e72a000',
gasLimit: '0x30000',
to: '0x583fBf10DDd81cFf2Ed208299E465e8EbDC7DAA3',
value: '0x01',
data: 'any data value',
v: '0x1c',
r: 0,
s: 0
};
tx = new ethTx(txData);
console.log('RLP-Encoded Tx: 0x' + tx.serialize().toString('hex'));
txHash = tx.hash();
console.log('Tx Hash: 0x' + txHash.toString('hex'));
//signing of transaction
const privateKey = Buffer.from('492b5eac1c7aae1957e60a3d3ce6daa7799c6941a480d12d38264af0d55883aa', 'hex');
tx.sign(privateKey)
serializedTx = tx.serialize()
rawTx = 'Signed Raw Transaction: 0x' + serializedTx.toString('hex')
console.log(rawTx)
The result of the execution of the above code is:
RLP-Encoded Tx: 0xf4808609184e72a0008303000094583fbf10ddd81cff2ed208299e465e8ebdc7daa3018e616e7920646174612076616c75651c8080
Tx Hash: 0xcf94ceac52eeccc4958d770b021a9dbd0ce5db6be3b745f738f0c0fa005d6ea6
Signed Raw Transaction: 0xf874808609184e72a0008303000094583fbf10ddd81cff2ed208299e465e8ebdc7daa3018e616e7920646174612076616c756526a01099362a773bd32b7bcdc2943cfa0aa27b4da4a182819e7337b2be7ad5814499a031a6c5054b4c1147e35f9d7756ccfa3b954629ecfa056c943c4781e68ecc9ede
Again I recorded this in a couple of shots without too much re-doing and editing, so I might have missed something. If so please let me know in the comments and ask questions if you have any.
Last but not least, I wanted to give big big thanks to the authors of the book “Mastering Ethereum” for providing code and the idea for me to do this.
Update, 2026
The ethereumjs-tx package used above is deprecated (its successor is @ethereumjs/tx), and most JavaScript projects now use viem or ethers.js. The idea hasn’t changed: fill in the fields, sign them with the private key, and get back a serialized transaction that any node will accept.
Two other things changed since the video:
- Legacy vs EIP-1559 transactions. The transaction above is a legacy transaction: a single
gasPrice, and the chain ID folded intov(EIP-155). Since the London upgrade in 2021 (EIP-1559), the usual transaction is type 2. It carrieschainIdas its own field, replacesgasPricewithmaxFeePerGasandmaxPriorityFeePerGas, has an optionalaccessList, and its signature usesyParity(0 or 1) instead ofv. Legacy transactions are still valid. - Testnets. Ropsten is gone. Use Sepolia, chain ID 11155111.
Here is the same signing step with viem, as an EIP-1559 transaction on Sepolia. Save it as sign.mjs and run it with node sign.mjs after npm install viem:
import { keccak256, parseEther, parseGwei } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
// The same public demo key as above. Never send real funds to it.
const account = privateKeyToAccount(
'0x492b5eac1c7aae1957e60a3d3ce6daa7799c6941a480d12d38264af0d55883aa'
);
const signedTx = await account.signTransaction({
type: 'eip1559',
chainId: 11155111, // Sepolia
nonce: 0,
to: '0x583fbf10ddd81cff2ed208299e465e8ebdc7daa3',
value: parseEther('0.001'),
gas: 21000n, // the gas limit, in units of gas
maxFeePerGas: parseGwei('20'),
maxPriorityFeePerGas: parseGwei('1'),
});
console.log('Signed raw transaction:', signedTx); // starts with 0x02, the type byte
console.log('Tx hash:', keccak256(signedTx));
Signing happens locally: nothing is sent anywhere. To broadcast the transaction, pass the signed hex to a node’s eth_sendRawTransaction method.
The transaction hash is the keccak256 hash of the signed, serialized transaction, which is what the last line prints. That also means the “Tx Hash” in my 2019 script, computed before signing, is not the hash the network would show for that transaction.