Skip to content

Receipts & Logs

Transaction receipts and event logs are essential for tracking state changes and application events in Ethereum. Tevm's ReceiptsManager gives you powerful tools to work with them, enabling features like event listening, transaction status tracking, and log filtering.

Quick Start

import { createTevmNode } from 'tevm'
import { createImpersonatedTx } from 'tevm/tx'
import { runTx } from 'tevm/vm'
import { createAddress } from 'tevm/utils'
 
const node = createTevmNode() 
const receiptsManager = await node.getReceiptsManager() 
 
// Execute a transaction
const vm = await node.getVm() 
const tx = createImpersonatedTx({ 
  impersonatedAddress: createAddress('0x1234567890123456789012345678901234567890'), 
  to: createAddress('0x2345678901234567890123456789012345678901'), 
  value: 1000000000000000000n, // 1 ETH 
  gasLimit: 21000n, 
}) 
 
// Run the transaction
const result = await runTx(vm)({ tx }) 
const txHash = tx.hash() 
 
// Get the receipt
const receiptResult = await receiptsManager.getReceiptByTxHash(txHash) 
 
if (receiptResult) { 
  const [receipt, blockHash, txIndex, logIndex] = receiptResult 
  // Access receipt data
  console.log({ 
    status: 'status' in receipt ? receipt.status : undefined, 
    gasUsed: receipt.cumulativeBlockGasUsed, 
    logs: receipt.logs 
  }) 
} 
import { createTevmNode } from 'tevm'
import { createImpersonatedTx } from 'tevm/tx'
import { runTx } from 'tevm/vm'
import { createAddress, hexToBytes } from 'tevm/utils'
 
const node = createTevmNode() 
const receiptsManager = await node.getReceiptsManager() 
 
// Get blocks for filtering
const vm = await node.getVm() 
const fromBlock = await vm.blockchain.getBlockByTag('earliest') 
const toBlock = await vm.blockchain.getBlockByTag('latest') 
const contractAddress = createAddress('0x1234567890123456789012345678901234567890') 
 
// Filter by contract address
const addressLogs = await receiptsManager.getLogs( 
  fromBlock, 
  toBlock, 
  [contractAddress.toBytes()], // Filter by this address 
  undefined // No topic filter 
) 
 
// Filter by event topic (event signature hash)
const eventTopic = hexToBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef') // Transfer event 
const topicLogs = await receiptsManager.getLogs( 
  fromBlock, 
  toBlock, 
  undefined, // Any address 
  [eventTopic] // Filter by this topic 
) 
 
// Print results
console.log(`Found ${addressLogs.length} logs from the contract`)
console.log(`Found ${topicLogs.length} Transfer events`)

Receipt Types

🏺 Pre-Byzantium

Uses state root for transaction results

Post-Byzantium

Uses status codes (success/failure)

🫧 EIP-4844

Includes blob gas information

interface PreByzantiumReceipt {
  stateRoot: Uint8Array        // Merkle root after transaction
  cumulativeBlockGasUsed: bigint
  logs: Log[]                  // Event logs
  // No status field
}
 
interface PostByzantiumReceipt {
  status: number               // 1 for success, 0 for failure
  cumulativeBlockGasUsed: bigint
  logs: Log[]                  // Event logs
}
 
interface EIP4844Receipt extends PostByzantiumReceipt {
  blobGasUsed: bigint          // Gas used for blob data
  blobGasPrice: bigint         // Price paid for blob data
}
// Function to handle different receipt types
function processReceipt(receiptResult) {
  if (!receiptResult) return 'Receipt not found'
 
  const [receipt] = receiptResult
 
  if ('status' in receipt) {
    // Post-Byzantium receipt
    return `Transaction ${receipt.status === 1 ? 'succeeded' : 'failed'}`
  } else {
    // Pre-Byzantium receipt
    return `Transaction included with state root: 0x${Buffer.from(receipt.stateRoot).toString('hex')}`
  }
}

Working with Event Logs

Contract Deployment

First, deploy a contract that emits events:

// Deploy a contract that emits events
const deployTx = createImpersonatedTx({
  impersonatedAddress: createAddress('0x1234567890123456789012345678901234567890'),
  data: CONTRACT_BYTECODE, // Your contract bytecode
  gasLimit: 1000000n,
})
 
const vm = await node.getVm()
const deployResult = await runTx(vm)({ tx: deployTx })
 
// Check deployment success
const contractAddress = deployResult.createdAddress
if (!contractAddress) {
  throw new Error('Contract deployment failed')
}

Emit Events

Next, interact with the contract to emit events:

// Call a function that emits events
const interactTx = createImpersonatedTx({
  impersonatedAddress: createAddress('0x1234567890123456789012345678901234567890'),
  to: contractAddress,
  // Function selector + parameters (e.g., transfer function on an ERC-20)
  data: '0xa9059cbb000000000000000000000000123456789012345678901234567890123456789000000000000000000000000000000000000000000000008ac7230489e80000',
  gasLimit: 100000n,
})
 
await runTx(vm)({ tx: interactTx })

Query Logs

Then, query the logs using various filters:

// Get the block range for filtering
const fromBlock = await vm.blockchain.getBlockByTag('earliest')
const toBlock = await vm.blockchain.getBlockByTag('latest')
 
// 1. Get all logs from the contract
const contractLogs = await receiptsManager.getLogs(
  fromBlock,
  toBlock,
  [contractAddress.toBytes()],
  undefined
)
 
// 2. Get logs for a specific event (e.g., Transfer event)
const transferEventSignature = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
const transferLogs = await receiptsManager.getLogs(
  fromBlock,
  toBlock,
  [contractAddress.toBytes()],
  [hexToBytes(transferEventSignature)]
)
 
// 3. Get logs with specific parameters (e.g., transfers to a specific address)
const receiverAddress = '0x1234567890123456789012345678901234567890'
const paddedAddress = '0x000000000000000000000000' + receiverAddress.slice(2)
const topicForReceiver = hexToBytes(paddedAddress)
 
const transfersToReceiver = await receiptsManager.getLogs(
  fromBlock,
  toBlock,
  [contractAddress.toBytes()],
  [hexToBytes(transferEventSignature), undefined, topicForReceiver]
)

Process Log Data

Finally, decode and process the log data:

// Process Transfer event logs
for (const log of transferLogs) {
  // A Transfer event typically has this structure:
  // topic[0]: event signature
  // topic[1]: from address (padded to 32 bytes)
  // topic[2]: to address (padded to 32 bytes)
  // data: amount (as a 32-byte value)
 
  const from = '0x' + Buffer.from(log.topics[1]).toString('hex').slice(24)
  const to = '0x' + Buffer.from(log.topics[2]).toString('hex').slice(24)
 
  // Convert the data to a BigInt (amount)
  const amount = BigInt('0x' + Buffer.from(log.data).toString('hex'))
 
  console.log(`Transfer: ${from} → ${to}: ${amount} tokens`)
 
  // You can also access other metadata
  console.log(`Block: ${log.blockNumber}, TxIndex: ${log.txIndex}, LogIndex: ${log.logIndex}`)
}

Advanced Features

// Get logs with complex filters:
// 1. From a specific contract
// 2. With Transfer event signature
// 3. From a specific sender
// 4. To a specific receiver
 
const fromAddress = '0x1234567890123456789012345678901234567890'
const toAddress = '0x2345678901234567890123456789012345678901'
 
// Event topics with wildcards (undefined means "any value")
const topics = [
  hexToBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'), // Transfer event
  hexToBytes('0x000000000000000000000000' + fromAddress.slice(2)),                   // From address (padded)
  hexToBytes('0x000000000000000000000000' + toAddress.slice(2))                      // To address (padded)
]
 
const filteredLogs = await receiptsManager.getLogs(
  fromBlock,
  toBlock,
  [contractAddress.toBytes()], // Contract address
  topics                        // Topic filters
)
// Track events across multiple contracts (e.g., a token and a marketplace)
const tokenAddress = createAddress('0x1234567890123456789012345678901234567890')
const marketplaceAddress = createAddress('0x2345678901234567890123456789012345678901')
 
// Get Transfer events from either contract
const transferEvent = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
 
const transfers = await receiptsManager.getLogs(
  fromBlock,
  toBlock,
  [tokenAddress.toBytes(), marketplaceAddress.toBytes()], // Multiple addresses
  [hexToBytes(transferEvent)]
)
 
console.log('Events by contract:')
const tokenEvents = transfers.filter(log =>
  Buffer.from(log.address).toString('hex') ===
  Buffer.from(tokenAddress.toBytes()).toString('hex')
)
const marketplaceEvents = transfers.filter(log =>
  Buffer.from(log.address).toString('hex') ===
  Buffer.from(marketplaceAddress.toBytes()).toString('hex')
)
// The ReceiptsManager maintains indexes for:
// 1. Transaction hash → receipt
// 2. Block hash → receipts
// 3. Address → logs
// 4. Topics → logs
 
// You can directly get a receipt by transaction hash
const receipt = await receiptsManager.getReceiptByTxHash(txHash)
 
// Or get all receipts in a block
const block = await vm.blockchain.getLatestBlock()
const blockHash = block.hash()
const receipts = await receiptsManager.getBlockReceipts(blockHash)
 
// Performance considerations:
// - Receipt storage grows with blockchain size
// - ReceiptsManager implements limits to prevent excessive resource usage
 
// Built-in limits
const GET_LOGS_LIMIT = 10000          // Maximum number of logs returned
const GET_LOGS_LIMIT_MEGABYTES = 150  // Maximum response size
const GET_LOGS_BLOCK_RANGE_LIMIT = 2500 // Maximum block range for queries
 
// For large-scale applications, implement pagination or additional filtering

Best Practices

🔍 Efficient Queries

Use specific filters and limit block ranges for better performance

⚠️ Handle Null Results

Always check for null/undefined results when working with receipts

🛡️ Type Safety

Check receipt types before accessing properties

📄 Pagination

Implement pagination for large log queries

Efficient Log Queries

// Instead of querying the entire chain
// Focus on specific block ranges when possible
const latestBlock = await vm.blockchain.getLatestBlock()
const blockNumber = latestBlock.header.number
 
// Get logs from the last 100 blocks
const fromBlock = await vm.blockchain.getBlock(blockNumber - 100n > 0n ? blockNumber - 100n : 0n)
 
// Use specific filters (address + topics)
const logs = await receiptsManager.getLogs(
  fromBlock, latestBlock, [contractAddress.toBytes()], [eventTopic]
)

Proper Error Handling

// Handle missing receipts gracefully
async function safeGetReceipt(txHash) {
  try {
    const receiptResult = await receiptsManager.getReceiptByTxHash(txHash)
 
    if (receiptResult === null) {
      console.log('Receipt not found - transaction may be pending or not exist')
      return null
    }
 
    const [receipt] = receiptResult
    return receipt
  } catch (error) {
    console.error('Error retrieving receipt:', error.message)
    // Handle specific error types if needed
    return null
  }
}

Working with Receipt Types

// Safely work with different receipt types
function getTransactionStatus(receipt) {
  if (!receipt) return 'Unknown'
 
  if ('status' in receipt) {
    // Post-Byzantium receipt
    return receipt.status === 1 ? 'Success' : 'Failed'
  } else if ('stateRoot' in receipt) {
    // Pre-Byzantium receipt - check if it's the empty root
    const emptyRoot = '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421'
    const actualRoot = '0x' + Buffer.from(receipt.stateRoot).toString('hex')
    return actualRoot === emptyRoot ? 'Likely Failed' : 'Likely Success'
  }
 
  return 'Unknown Format'
}

Related Resources

JSON-RPC Support
Log and receipt APIs via JSON-RPC
VM & Submodules
Low-level access to the EVM
Transaction Pool
Managing pending transactions
Solidity Events
Learn about events in Solidity