Try it Live
Run Transaction examples in the interactive playground
Transaction Usage Patterns
Common patterns and best practices for working with Ethereum transactions.Building Transactions
Simple ETH Transfer
import * as Transaction from 'tevm/Transaction'
async function createTransfer(
from: AddressType,
to: AddressType,
value: bigint,
nonce: bigint
): Promise<Transaction.EIP1559> {
// Get current base fee
const block = await provider.getBlock('latest')
const baseFee = block.baseFeePerGas
return {
type: Transaction.Type.EIP1559,
chainId: 1n,
nonce,
maxPriorityFeePerGas: 1000000000n, // 1 gwei tip
maxFeePerGas: baseFee * 2n + 1000000000n,
gasLimit: 21000n,
to,
value,
data: new Uint8Array(),
accessList: [],
yParity: 0,
r: Bytes32(),
s: Bytes32(),
}
}
Contract Deployment
async function createDeployment(
deployer: AddressType,
bytecode: Uint8Array,
nonce: bigint
): Promise<Transaction.EIP1559> {
const block = await provider.getBlock('latest')
const baseFee = block.baseFeePerGas
// Estimate gas
const estimatedGas = await provider.estimateGas({
from: deployer,
data: bytecode,
})
return {
type: Transaction.Type.EIP1559,
chainId: 1n,
nonce,
maxPriorityFeePerGas: 2000000000n,
maxFeePerGas: baseFee * 2n + 2000000000n,
gasLimit: estimatedGas * 120n / 100n, // +20% buffer
to: null, // Contract creation
value: 0n,
data: bytecode,
accessList: [],
yParity: 0,
r: Bytes32(),
s: Bytes32(),
}
}
Contract Call with Access List
async function createContractCall(
from: AddressType,
to: AddressType,
data: Uint8Array,
nonce: bigint
): Promise<Transaction.EIP1559> {
// Generate access list
const accessListResult = await provider.send('eth_createAccessList', [{
from,
to,
data,
}])
const block = await provider.getBlock('latest')
const baseFee = block.baseFeePerGas
return {
type: Transaction.Type.EIP1559,
chainId: 1n,
nonce,
maxPriorityFeePerGas: 2000000000n,
maxFeePerGas: baseFee * 2n + 2000000000n,
gasLimit: BigInt(accessListResult.gasUsed) * 120n / 100n,
to,
value: 0n,
data,
accessList: accessListResult.accessList,
yParity: 0,
r: Bytes32(),
s: Bytes32(),
}
}
Signing Transactions
With Private Key
import { getSigningHash } from 'tevm/Transaction'
import { secp256k1 } from '@noble/curves/secp256k1'
function signTransaction(
tx: Transaction.Any,
privateKey: Uint8Array
): Transaction.Any {
// Get hash to sign
const signingHash = getSigningHash(tx)
// Sign with secp256k1
const signature = secp256k1.sign(signingHash, privateKey)
// Add signature to transaction
if (Transaction.isLegacy(tx)) {
const chainId = Transaction.getChainId(tx) || 0n
const v = chainId > 0n
? chainId * 2n + 35n + BigInt(signature.recovery)
: 27n + BigInt(signature.recovery)
return {
...tx,
v,
r: signature.r.toBytes('be', 32),
s: signature.s.toBytes('be', 32),
}
} else {
return {
...tx,
yParity: signature.recovery,
r: signature.r.toBytes('be', 32),
s: signature.s.toBytes('be', 32),
}
}
}
With Hardware Wallet
async function signWithHardwareWallet(
tx: Transaction.Any,
wallet: HardwareWallet
): Promise<Transaction.Any> {
// Serialize unsigned transaction
const signingHash = getSigningHash(tx)
// Send to hardware wallet
const signature = await wallet.signHash(signingHash)
// Add signature
if (Transaction.isLegacy(tx)) {
return {
...tx,
v: signature.v,
r: signature.r,
s: signature.s,
}
} else {
return {
...tx,
yParity: signature.yParity,
r: signature.r,
s: signature.s,
}
}
}
Transaction Submission
Submit and Wait
import { serialize, hash } from 'tevm/Transaction'
async function submitTransaction(
tx: Transaction.Any
): Promise<TransactionReceipt> {
// Serialize
const serialized = serialize(tx)
const txHash = hash(tx)
// Submit to network
await provider.send('eth_sendRawTransaction', [
Hex(serialized)
])
// Wait for confirmation
let receipt = null
while (!receipt) {
receipt = await provider.getTransactionReceipt(Hex(txHash))
if (!receipt) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
return receipt
}
Submit with Retry
async function submitWithRetry(
tx: Transaction.Any,
maxRetries = 3
): Promise<string> {
const serialized = serialize(tx)
const txHash = Hex(hash(tx))
for (let i = 0; i < maxRetries; i++) {
try {
await provider.send('eth_sendRawTransaction', [
Hex(serialized)
])
return txHash
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
}
}
throw new TransactionError('Failed to submit transaction', {
code: 'MAX_RETRIES_EXCEEDED',
context: { maxRetries }
})
}
Replace by Fee (RBF)
async function replaceTransaction(
original: Transaction.EIP1559,
newPriorityFee: bigint
): Promise<Transaction.EIP1559> {
// Keep same nonce to replace
// Increase fee by at least 10%
const minFeeIncrease = original.maxPriorityFeePerGas * 110n / 100n
const replacement: Transaction.EIP1559 = {
...original,
maxPriorityFeePerGas: newPriorityFee > minFeeIncrease
? newPriorityFee
: minFeeIncrease,
maxFeePerGas: original.maxFeePerGas * 110n / 100n,
}
return replacement
}
Validation
Complete Transaction Validation
import {
isSigned,
verifySignature,
getSender,
getChainId,
getGasPrice
} from 'tevm/Transaction'
interface ValidationResult {
valid: boolean
errors: string[]
}
async function validateTransaction(
tx: Transaction.Any,
expectedChainId: bigint,
baseFee?: bigint
): Promise<ValidationResult> {
const errors: string[] = []
// Check signature
if (!isSigned(tx)) {
errors.push('Transaction not signed')
return { valid: false, errors }
}
if (!verifySignature(tx)) {
errors.push('Invalid signature')
}
// Check chain ID
const txChainId = getChainId(tx)
if (txChainId && txChainId !== expectedChainId) {
errors.push(`Wrong chain: expected ${expectedChainId}, got ${txChainId}`)
}
// Check gas price
try {
const gasPrice = getGasPrice(tx, baseFee)
if (gasPrice === 0n) {
errors.push('Zero gas price')
}
} catch (e) {
errors.push('Cannot calculate gas price: ' + e.message)
}
// Check gas limit
if (tx.gasLimit < 21000n) {
errors.push('Gas limit too low (minimum 21000)')
}
// Check nonce
const sender = getSender(tx)
const currentNonce = await provider.getTransactionCount(sender)
if (tx.nonce < currentNonce) {
errors.push(`Nonce too low: ${tx.nonce} < ${currentNonce}`)
}
// Check balance
const balance = await provider.getBalance(sender)
const maxCost = tx.gasLimit * getGasPrice(tx, baseFee) + tx.value
if (balance < maxCost) {
errors.push('Insufficient balance')
}
return {
valid: errors.length === 0,
errors
}
}
Transaction Pool
Simple Pool Implementation
import { hash, getSender, getGasPrice } from 'tevm/Transaction'
class TransactionPool {
private pending = new Map<string, Transaction.Any>()
private byNonce = new Map<string, Map<bigint, Transaction.Any>>()
add(tx: Transaction.Any, baseFee: bigint): void {
const txHash = Hex(hash(tx))
const sender = Address.toHex(getSender(tx))
// Store by hash
this.pending.set(txHash, tx)
// Store by sender + nonce
if (!this.byNonce.has(sender)) {
this.byNonce.set(sender, new Map())
}
this.byNonce.get(sender)!.set(tx.nonce, tx)
}
remove(txHash: string): void {
const tx = this.pending.get(txHash)
if (!tx) return
this.pending.delete(txHash)
const sender = Address.toHex(getSender(tx))
this.byNonce.get(sender)?.delete(tx.nonce)
}
getByNonce(sender: AddressType, nonce: bigint): Transaction.Any | undefined {
return this.byNonce.get(Address.toHex(sender))?.get(nonce)
}
getAll(): Transaction.Any[] {
return Array(this.pending.values())
}
getPending(sender: AddressType): Transaction.Any[] {
const senderNonces = this.byNonce.get(Address.toHex(sender))
if (!senderNonces) return []
return Array(senderNonces.values())
.sort((a, b) => Number(a.nonce - b.nonce))
}
}
Gas Estimation
Dynamic Fee Estimation
async function estimateFees(): Promise<{
slow: bigint
standard: bigint
fast: bigint
}> {
const block = await provider.getBlock('latest')
const baseFee = block.baseFeePerGas
return {
slow: baseFee + 1000000000n, // +1 gwei
standard: baseFee + 2000000000n, // +2 gwei
fast: baseFee + 5000000000n, // +5 gwei
}
}
Fee History Analysis
async function estimateFromHistory(): Promise<bigint> {
const feeHistory = await provider.send('eth_feeHistory', [
'0x14', // 20 blocks
'latest',
[25, 50, 75] // 25th, 50th, 75th percentile
])
// Use median of 50th percentile
const rewards = feeHistory.reward.map(r => BigInt(r[1]))
rewards.sort((a, b) => Number(a - b))
return rewards[Math.floor(rewards.length / 2)]
}
Error Handling
Comprehensive Error Handling
async function safeSubmitTransaction(
tx: Transaction.Any
): Promise<{ success: boolean; txHash?: string; error?: string }> {
try {
// Validate
assertSigned(tx)
// Serialize
const serialized = serialize(tx)
const txHash = Hex(hash(tx))
// Submit
await provider.send('eth_sendRawTransaction', [
Hex(serialized)
])
return { success: true, txHash }
} catch (error) {
if (error.message.includes('nonce too low')) {
return { success: false, error: 'Nonce already used' }
}
if (error.message.includes('insufficient funds')) {
return { success: false, error: 'Insufficient balance' }
}
if (error.message.includes('gas price too low')) {
return { success: false, error: 'Gas price too low' }
}
if (error.message.includes('already known')) {
return { success: false, error: 'Transaction already in pool' }
}
return { success: false, error: error.message }
}
}
Best Practices
- Always validate before signing
- Use appropriate transaction type (EIP-1559 for modern networks)
- Include access lists when beneficial
- Set reasonable gas limits (estimate + 10-20% buffer)
- Cache expensive operations (getSender, hash)
- Handle errors gracefully
- Verify signatures before accepting transactions
- Check chain ID for replay protection
- Monitor gas prices and adjust dynamically
- Use proper nonce management to avoid conflicts

