EVM RPC: Receipt and Trace Playbook
Use this skill for EVM transaction debugging with JSON-RPC.
Quick Start
Set a hash and endpoint, then run receipt first and trace second.
evm_tx="0xYOUR_evm_tx"
EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
curl -s -X POST \
-H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$evm_tx\"],\"id\":1}" \
"$EVM_RPC" | jq
curl -s -X POST \
-H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"debug_traceTransaction\",\"params\":[\"$evm_tx\",{\"tracer\":\"callTracer\"}],\"id\":1}" \
"$EVM_RPC" | jq
Endpoint Map - Nibiru Examples
Chain-agnostic rule: methods determine endpoint family.
| Purpose | Example endpoint | Methods |
|---|---|---|
| EVM JSON-RPC (mainnet) | https://evm-rpc.nibiru.fi | eth_*, debug_*, net_*, web3_* |
| EVM JSON-RPC archive (mainnet) | https://evm-rpc.archive.nibiru.fi | historical reads, tracing |
| EVM JSON-RPC archive (testnet) | https://evm-rpc.archive.testnet-2.nibiru.fi | testnet tracing and historical reads |
| Comet RPC archive (mainnet) | https://rpc.archive.nibiru.fi | /block_results, /consensus_params |
| Comet RPC archive (testnet) | https://rpc.archive.testnet-2.nibiru.fi | /block_results, /consensus_params |
Rules:
- Run
eth_*anddebug_*only on EVM RPC endpoints. - Run
/block_resultsand/consensus_paramsonly on Comet RPC endpoints. - Prefer archive endpoints for old heights/hashes and tracing workflows.
Basic Queries (Health / Identity)
web3_clientVersion
Queries the traceability info like the client name, version, Git commit, Go version, runtime architecture, and build tags.
curl -X POST https://evm-rpc.nibiru.fi \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": [],
"id": 1
}'
The response looks like this: "Nibiru 2.6.0: Compiled at Git commit 3cf97e3468f8ce922c8760f839f8f336ca0c37f4 using Go go1.24.5, arch amd64, and build tags (netgo osusergo ledger static rocksdb pebbledb muslc)"
eth_chainId
Queries the EIP-155 replay-protection chain id for the current ethereum blockchain (Nibiru).
curl -X POST https://evm-rpc.nibiru.fi \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "eth_chainId",
"params": [],
"id": 1
}'
Transaction Triage Workflow
Use this sequence to avoid false conclusions:
eth_getTransactionByHash- Confirms the tx exists and gives gas fields, type, input, and block refs.
eth_getTransactionReceipt- Authoritative EVM execution result.
debug_traceTransaction(usuallycallTracer)- Explains internal call path and where execution diverged.
- Optional cross-layer checks on Comet RPC
block_results?height=...,consensus_params?height=....
Concrete Command Cookbook
1 - Get Transaction by Hash
evm_tx="0xYOUR_evm_tx"
EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
curl -s -X POST \
-H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$evm_tx\"],\"id\":1}" \
"$EVM_RPC" | jq
2 - Get Transaction Receipt - Authoritative Status
evm_tx="0xYOUR_evm_tx"
EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
curl -s -X POST \
-H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$evm_tx\"],\"id\":1}" \
"$EVM_RPC" | jq
Interpretation:
result == null: Not mined or rejected. Either the tx is still in the mempool, it was dropped, or it failed consensus/validation (e.g., bad nonce, invalid signature, insufficient funds for gas). It is not in any block.status == "0x1": Full Success. The transaction was executed successfully, and all state changes were applied.status == "0x0": Execution Failure (Revert). The transaction was successfully mined and included in a block (consuming gas), but the EVM execution was reverted. No state changes occurred except for the gas fee deduction from the sender.blockNumberpresent: Mined/included in a block.logspresent: Events emitted (decode byaddress,topics,data). Note that a reverted tx (0x0) will generally have an emptylogsarray, even if it attempted to emit them before reverting.
3 - Run Debug Tracer - Call Tree View
evm_tx="0xYOUR_evm_tx"
EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
curl -s -X POST \
-H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"debug_traceTransaction\",\"params\":[\"$evm_tx\",{\"tracer\":\"callTracer\"}],\"id\":1}" \
"$EVM_RPC" | jq
Interpretation:
- Inspect top-level
from,to,gas,gasUsed. - Walk
calls[]for internal call chain and outputs. - Use trace to explain behavior; use receipt status to decide success/failure.
4 - Cross-Layer Checks with Comet RPC
HEIGHT="5959008"
TM_RPC="https://rpc.archive.testnet-2.nibiru.fi"
curl -s "$TM_RPC/block_results?height=$HEIGHT" | jq
curl -s "$TM_RPC/consensus_params?height=$HEIGHT" | jq
Use when EVM and Cosmos observations appear inconsistent.
5 - Check Deployed Bytecode for an ABI Mismatch
Use this when a transaction reverts very early, the trace only shows a top-level
execution reverted, and the calldata targets a function that may not exist in
the deployed runtime bytecode. This catches frontend ABI or artifact drift, such
as calling a newly added function against an older deployed contract.
The method is:
- Compute the 4-byte selector from the canonical function signature.
- Fetch runtime bytecode with
eth_getCode. - Search the runtime bytecode for the selector without the
0xprefix. - Compare against known selectors that should exist on the same contract.
CONTRACT_ADDR="0xCONTRACT_ADDR"
FUNCTION_SIG="claimReferrerRewardsToEvm(uint256[])"
EVM_RPC="https://evm-rpc.archive.nibiru.fi"
BLOCK_TAG="latest" # or a hex block number like "0x273e1fa"
bun - <<'TS'
import { id } from "ethers"
const contractAddr = process.env.CONTRACT_ADDR
const functionSig = process.env.FUNCTION_SIG
const rpc = process.env.EVM_RPC
const blockTag = process.env.BLOCK_TAG ?? "latest"
if (!contractAddr || !functionSig || !rpc) {
throw new Error("set CONTRACT_ADDR, FUNCTION_SIG, and EVM_RPC")
}
const selector = id(functionSig).slice(0, 10)
const resp = await fetch(rpc, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getCode",
params: [contractAddr, blockTag],
id: 1,
}),
})
const json = await resp.json()
if (json.error) {
throw new Error(JSON.stringify(json.error))
}
const code = String(json.result ?? "0x").toLowerCase()
const selectorNoPrefix = selector.slice(2).toLowerCase()
console.log({
contractAddr,
blockTag,
functionSig,
selector,
byteLength: Math.max(0, (code.length - 2) / 2),
selectorPresent: code.includes(selectorNoPrefix),
})
TS
To compare multiple selectors, use the same runtime bytecode and check a list. This is useful when one new selector is missing but older selectors are present.
CONTRACT_ADDR="0xCONTRACT_ADDR"
EVM_RPC="https://evm-rpc.archive.nibiru.fi"
BLOCK_TAG="latest"
bun - <<'TS'
import { id } from "ethers"
const contractAddr = process.env.CONTRACT_ADDR
const rpc = process.env.EVM_RPC
const blockTag = process.env.BLOCK_TAG ?? "latest"
const signatures = [
"claimReferrerRewardsToEvm(uint256[])",
"executeSimpleFunctions(bytes)",
"getDenomOfCollateralIndex(uint256)",
]
if (!contractAddr || !rpc) {
throw new Error("set CONTRACT_ADDR and EVM_RPC")
}
const resp = await fetch(rpc, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "eth_getCode",
params: [contractAddr, blockTag],
id: 1,
}),
})
const json = await resp.json()
if (json.error) {
throw new Error(JSON.stringify(json.error))
}
const code = String(json.result ?? "0x").toLowerCase()
for (const sig of signatures) {
const selector = id(sig).slice(0, 10)
console.log({
sig,
selector,
present: code.includes(selector.slice(2).toLowerCase()),
})
}
TS
Interpretation:
selectorPresent: true: The runtime bytecode likely has a dispatcher entry for that function. Continue debugging arguments, require checks, internal calls, and traces.selectorPresent: falsewhile older selectors are present: The deployed contract likely does not implement the ABI function being called. Update the address, deploy the newer contract, or call an older available function.- Very low
gasUsedwithstatus: "0x0"and a missing selector often means the call hit the dispatcher fallback/revert before meaningful contract logic. - For proxies, minimal proxies, diamonds, or custom fallback dispatchers, the selector may be absent from the proxy runtime bytecode. Check the implementation address or facet bytecode instead.
Receipt and Trace Rules of Thumb
- Receipt status is the authoritative EVM truth for success vs. failure.
- Mined does not mean successful: A transaction can be in a block and consume gas (
0x0), but have zero effect on state. Always check thestatusfield. - Null receipt implies non-existence: If
eth_getTransactionReceiptreturnsnull, the transaction was never mined. This is usually due to a consensus/mempool error (invalid signature, wrong chain ID, bad nonce, or insufficient funds to cover the max gas cost). - Consensus failures don't have receipts: Errors like "insufficient funds" or "bad nonce" prevent a transaction from even entering a block, so no EVM status is ever generated.
- A non-empty
logsarray (at the receipt level) generally only exists for successful transactions (0x1). - Gas consumption: Even in failure (
0x0), thegasUsedis still charged to the sender up to the point of reversion. - Gas Comparison: Compare receipt
gasUsedwith thegas(limit) frometh_getTransactionByHashto see if the transaction ran out of gas. - Tracing Failures: If tracing fails, verify the provider supports the
debugnamespace and retry on an archive endpoint.
Endpoint Selection Guide
Choose endpoint by method and historical depth:
- Latest, simple reads (
eth_blockNumber, recent receipt):- Standard EVM RPC is usually enough.
- Historical receipts, old block state, trace/debug:
- Prefer archive EVM RPC.
- Tendermint/Comet block metadata and app-level event correlation:
- Use Comet RPC endpoints.
Common Pitfalls
- Calling Comet paths on EVM RPC host (or the reverse).
- Using the wrong network endpoint for a valid tx hash.
- Assuming
result == nullalways means pending; it may be wrong chain/provider. - Treating trace output alone as final status without reading receipt.
- Ignoring timeout/retry behavior for freshly broadcast transactions.
Reusable Triage Template
evm_tx="0xYOUR_evm_tx"
EVM_RPC="https://evm-rpc.archive.testnet-2.nibiru.fi"
TM_RPC="https://rpc.archive.testnet-2.nibiru.fi"
# 1) tx details
curl -s -X POST -H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionByHash\",\"params\":[\"$evm_tx\"],\"id\":1}" \
"$EVM_RPC" | jq
# 2) receipt status
curl -s -X POST -H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\",\"params\":[\"$evm_tx\"],\"id\":1}" \
"$EVM_RPC" | jq
# 3) call trace
curl -s -X POST -H "Content-Type: application/json" \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"debug_traceTransaction\",\"params\":[\"$evm_tx\",{\"tracer\":\"callTracer\"}],\"id\":1}" \
"$EVM_RPC" | jq
Additional Resources
- Zero-gas debug walkthrough:
/home/realu/ki/boku/epics/epic-evm/26-02-zero-gas/26-02-10-zero-gas-debug.md - Existing endpoint conventions:
/home/realu/ki/boku/nibi/cook/index.ts