Skip to main content
A transaction receipt on Elysium shows whether the transaction succeeded and which events it emitted. It doesn’t list the calls that contracts made to each other, and it has no record of HYPE that a contract sends, because a HYPE transfer emits no event. debug_traceTransaction replays the transaction on your Elysium node and returns all of these calls as a tree. In this tutorial, you build a Node.js script that traces an Elysium Testnet transaction and prints its internal calls, the HYPE they moved, the calls that failed, and where the fee went.
Get your own node endpoint todayStart for free and get your app to production levels immediately. No credit card required.You can sign up with your GitHub, X, Google, or Microsoft account.
TLDR:
  • debug_traceTransaction with callTracer returns every internal call of an Elysium transaction, including the HYPE that contracts send to each other.
  • Elysium runs Nitro, so the top-level call also lists the balance changes outside EVM execution: the upfront gas payment, the refund, and the fees collected.
  • The script names each contract and function from the Elysium Testnet Blockscout and prints a readable call tree.
  • For a flat list of calls to store in a database, use flatCallTracer.

Prerequisites

  • A Chainstack account with an Elysium Testnet node. The debug_* methods need a paid plan. See Debug and trace APIs.
  • Node.js 20 or later.

Get an Elysium node

  1. Sign up with Chainstack.
  2. Deploy a node on Elysium Testnet.
  3. Copy the node’s HTTPS endpoint. See View node access and credentials.

The example transaction

This tutorial traces a token sale on Elysium Testnet, transaction 0x6034ffc6c2209586e0787f18b0f11805c18dd432618d42c377d0b56433899508. The sender called sell on the SignalCurveHypeRouter contract, attached no HYPE, and received HYPE for the tokens. Elysium’s Global Nodes serve traces back to genesis, so you can trace this transaction or any other Elysium Testnet transaction.

Step 1. Trace the transaction with cURL

Call debug_traceTransaction with callTracer. The onlyTopCall option returns only the top-level call:
cURL
JSON
In the top-level call:
  • value is 0x0 — the sender attached no HYPE.
  • output is the return value of sell, the HYPE the sender received: 0x423b244aa1db702 wei, or 0.298278008318179074 HYPE.
  • beforeEVMTransfers and afterEVMTransfers are Nitro fields that list the balance changes outside EVM execution. See Where the fee went.
For what the other fields mean, see Reading EVM traces: callTracer and prestateTracer. Without onlyTopCall, the same request returns all 87 calls of the transaction, nested. The script in the next steps turns them into a readable tree.

Step 2. Set up the project

Shell

Step 3. Write the script

Create trace-elysium-tx.mjs and replace YOUR_CHAINSTACK_ENDPOINT with your Elysium node’s HTTPS endpoint:
trace-elysium-tx.mjs
How the script reads the trace:
  • Contract names come from the Elysium Testnet Blockscout API at https://elysium.kinetiq.xyz/api/v2 — the token symbol first, then the implementation name for a proxy, then the contract name. Function names come from the verified ABIs of the contract and of a proxy’s implementations.
  • When no verified ABI covers a selector, the script looks it up in the Sourcify signature database and marks the name with *. A selector can match more than one signature, so a marked name is a likely match, not a verified one.
  • A proxy forwards a call to its implementation with a DELEGATECALL that carries the same input. The script folds that DELEGATECALL into the call that reached the proxy, so each proxied call takes one line.
  • The call tree hides a STATICCALL that succeeded, unless a call inside it failed. A STATICCALL reads state and can’t change it or move HYPE.
  • HYPE moves only in CALL, CREATE, CREATE2, and SELFDESTRUCT frames. A DELEGATECALL frame repeats its parent’s value without moving HYPE, so the script doesn’t count it.
  • The script labels each fee transfer by its recipient. It reads the infrastructure and network fee accounts from the ArbOwnerPublic precompile at 0x000000000000000000000000000000000000006b, and it recognizes the L1 pricer funds pool at 0xA4B00000000000000000000000000000000000F6, which collects L1 posting fees.

Step 4. Run the script

Shell
Output
The script works on any Elysium Testnet transaction hash.

Read the output

Where the HYPE came from

The call tree shows the sale step by step:
  1. sell moves the sender’s ELI tokens to the router with transferFrom.
  2. The router swaps the ELI for WHYPE, the wrapped HYPE token, through SignalProjectXV2Router and a Uniswap V2-style pair. The pair sends the WHYPE to the router.
  3. The router calls withdraw on WHYPE, and WHYPE sends 0.298278008318179074 HYPE to the router.
  4. The router sends the same 0.298278008318179074 HYPE to the sender.
The receipt of this transaction has 11 events, among them Transfer events for ELI and WHYPE and the router’s own CurveRouted event, which reports the amount received. No event records HYPE moving from WHYPE to the router or from the router to the sender. Those two transfers are in the trace only, and the script lists them under Internal HYPE transfers.

Calls that failed inside a successful transaction

Both failed calls use the selector 0xc45a0155, which is factory(). SignalProjectXAverageGuard.isPool checks whether an address is a liquidity pool by calling factory() on it with a low-level staticcall. The router has no factory function, so the call reverts, isPool returns false, and the token transfer continues. A failed call inside a successful transaction means that the calling contract handled the failure. When a whole transaction fails, start from the deepest call with an error, which is where the revert started. See Reverts.

Where the fee went

Nitro charges and distributes the transaction fee outside EVM execution and records each balance change in beforeEVMTransfers and afterEVMTransfers on the top-level call. The example transaction set a gas price of 0.12 gwei, but the receipt’s effectiveGasPrice is the block’s 0.1 gwei base fee, and every fee transfer uses the base fee: The refund and the two fee collections add up to the upfront payment. The two feeCollection recipients:
  • 0x252431E84D5E22435A0c833C2220770c52F59633 — Elysium’s infrastructure fee account, which is also its network fee account. getInfraFeeAccount() and getNetworkFeeAccount() on ArbOwnerPublic both return this address. The infrastructure fee account receives the execution gas at the minimum base fee. When the base fee exceeds the minimum, a second feeCollection entry pays the difference to the network fee account. In the example, the base fee equals the 0.1 gwei minimum, so the transaction has one execution fee entry.
  • 0xA4B00000000000000000000000000000000000F6 — Nitro’s L1 pricer funds pool. It collects the L1 posting fee, the cost of posting the transaction to HyperEVM, which the receipt reports as gasUsedForL1.

Store internal calls with flatCallTracer

To store internal calls in a database, flatCallTracer returns the same calls as a flat array in the Parity trace format, one entry per call:
cURL
The result has 87 entries. This entry is the transfer of 0.298278008318179074 HYPE from the router to the sender:
JSON
  • traceAddress — the call’s position in the tree. [24] is the call at index 24, counting from 0, among the calls that the top-level call made. The top-level call has traceAddress [].
  • subtraces — the number of calls that this call made.
  • action.value — the HYPE moved, in wei.
On Elysium, the first entry, the top-level call, also carries beforeEVMTransfers and afterEVMTransfers. To trace a whole block, pass the same tracer to debug_traceBlockByNumber or debug_traceBlockByHash.

See also

Last modified on October 8, 2026