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'
}