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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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)
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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

