Docs

debug_traceCall - Celo RPC Method

Trace a call without creating a transaction on Celo. Simulate transactions, debug contract interactions, and analyze gas usage with full execution traces.

Traces a call on Celo without creating a transaction on-chain. This is a dry-run trace — it executes the call in the EVM at a specified block and returns detailed execution traces including opcodes, internal calls, and state changes, without any on-chain side effects.

Why Celo? Build on the mobile-first L2 powering 500K+ daily active users and $2B+ monthly stablecoin volume with phone number-based addressing, sub-cent fees, 150+ country adoption, Nightfall privacy layer, and Opera browser integration.

Archive Node Required

This method requires an archive node with debug APIs enabled when tracing against historical blocks. For "latest" or "pending" blocks, a full node with debug APIs may suffice. Dwellir provides archive node access for Celo — ensure your plan includes debug namespace support.

When to Use This Method

debug_traceCall is powerful for mobile payment developers, fintech builders, and teams targeting emerging markets:

  • Simulating Transactions Before Sending — Preview the full execution trace of a transaction before committing it on-chain, catching reverts and unexpected behavior before spending gas on Celo
  • Debugging Contract Interactions — Step through contract execution at the opcode level to understand complex interactions, delegate calls, and proxy patterns for mobile stablecoin payments (MiniPay 10M+ wallets), remittances, humanitarian aid, and local currency stablecoins (cUSD, cNGN, cEUR)
  • Gas Estimation With Trace Details — Go beyond eth_estimateGas by seeing exactly which opcodes and internal calls consume gas, enabling targeted optimization
  • Security Analysis — Analyze how a contract would execute a specific call, detecting reentrancy, unexpected state modifications, and access control issues

Code Examples

Common Use Cases

1. Pre-Flight Transaction Simulation

Test a transaction before sending it on Celo to catch reverts and estimate costs:

JavaScript
async function simulateTransaction(provider, txParams) {
  // Use callTracer to see the full call tree
  const trace = await provider.send('debug_traceCall', [
    {
      from: txParams.from,
      to: txParams.to,
      data: txParams.data,
      value: txParams.value || '0x0',
      gas: txParams.gasLimit || '0x1e8480' // 2M gas default
    },
    'latest',
    { tracer: 'callTracer' }
  ]);

  const gasUsed = parseInt(trace.gasUsed, 16);

  if (trace.error) {
    console.error('Transaction would revert!');
    console.error(`  Error: ${trace.error}`);
    console.error(`  Reason: ${trace.revertReason || 'unknown'}`);
    console.error(`  Gas wasted: ${gasUsed}`);
    return { success: false, error: trace.error, revertReason: trace.revertReason, gasUsed };
  }

  // Analyze internal calls for unexpected behavior
  const allCalls = flattenCalls(trace);
  const delegateCalls = allCalls.filter(c => c.type === 'DELEGATECALL');
  const creates = allCalls.filter(c => c.type === 'CREATE' || c.type === 'CREATE2');

  console.log('Simulation results:');
  console.log(`  Gas used: ${gasUsed}`);
  console.log(`  Internal calls: ${allCalls.length}`);
  console.log(`  Delegate calls: ${delegateCalls.length}`);
  console.log(`  Contract creations: ${creates.length}`);

  return { success: true, gasUsed, trace };
}

function flattenCalls(trace) {
  const calls = [trace];
  for (const sub of trace.calls || []) {
    calls.push(...flattenCalls(sub));
  }
  return calls;
}

2. Gas Optimization Analysis

Identify the most expensive opcodes in a contract call on Celo:

JavaScript
async function analyzeGasHotspots(provider, callObj) {
  // Use default opcode tracer for step-by-step gas analysis
  const trace = await provider.send('debug_traceCall', [
    callObj,
    'latest',
    { disableStorage: false, enableReturnData: true }
  ]);

  const opcodeGas = {};

  for (const log of trace.structLogs) {
    if (!opcodeGas[log.op]) {
      opcodeGas[log.op] = { count: 0, totalGas: 0 };
    }
    opcodeGas[log.op].count++;
    opcodeGas[log.op].totalGas += log.gasCost;
  }

  // Sort by total gas cost
  const sorted = Object.entries(opcodeGas)
    .map(([op, stats]) => ({ op, ...stats, avgGas: Math.round(stats.totalGas / stats.count) }))
    .sort((a, b) => b.totalGas - a.totalGas);

  console.log('Gas hotspots:');
  console.log('Op'.padEnd(15), 'Count'.padStart(8), 'Total Gas'.padStart(12), 'Avg Gas'.padStart(10));
  for (const entry of sorted.slice(0, 10)) {
    console.log(
      entry.op.padEnd(15),
      String(entry.count).padStart(8),
      String(entry.totalGas).padStart(12),
      String(entry.avgGas).padStart(10)
    );
  }

  // Identify SSTORE/SLOAD hotspots (most expensive storage operations)
  const storageOps = trace.structLogs.filter(
    log => log.op === 'SSTORE' || log.op === 'SLOAD'
  );
  console.log(`\nStorage operations: ${storageOps.length} (${storageOps.filter(s => s.op === 'SSTORE').length} writes)`);

  return { opcodeGas: sorted, totalSteps: trace.structLogs.length, totalGas: trace.gas };
}

3. Security Analysis of Contract Interactions

Detect potentially dangerous patterns when calling a contract on Celo:

Python
import requests

def security_trace_call(call_object, block='latest'):
    response = requests.post('https://api-celo-mainnet-archive.n.dwellir.com/YOUR_API_KEY', json={
        'jsonrpc': '2.0',
        'method': 'debug_traceCall',
        'params': [call_object, block, {'tracer': 'callTracer'}],
        'id': 1
    })
    trace = response.json()['result']

    warnings = []
    all_calls = flatten_calls(trace)

    for call in all_calls:
        # Detect unexpected delegate calls
        if call['type'] == 'DELEGATECALL':
            warnings.append(f'DELEGATECALL to {call["to"]} — could modify caller storage')

        # Detect value transfers to unexpected addresses
        value = int(call.get('value', '0x0'), 16)
        if value > 0 and call['to'] != call_object.get('to', '').lower():
            warnings.append(
                f'Value transfer of {value} wei to unexpected address {call["to"]}'
            )

        # Detect selfdestruct (CALL with no input to EOA after value)
        if call.get('error'):
            warnings.append(f'Internal revert at {call["to"]}: {call["error"]}')

    if trace.get('error'):
        print(f'TOP-LEVEL REVERT: {trace["error"]}')
        if trace.get('revertReason'):
            print(f'  Reason: {trace["revertReason"]}')
    else:
        gas_used = int(trace['gasUsed'], 16)
        print(f'Call succeeded: {gas_used} gas used')

    if warnings:
        print(f'\nSecurity warnings ({len(warnings)}):')
        for w in warnings:
            print(f'  - {w}')
    else:
        print('No security warnings detected')

    return {'success': not trace.get('error'), 'warnings': warnings}

def flatten_calls(trace):
    calls = [trace]
    for sub in trace.get('calls', []):
        calls.extend(flatten_calls(sub))
    return calls

# Example: analyze a token approval
security_trace_call({
    'from': '0x1234567890abcdef1234567890abcdef12345678',
    'to': '0x471EcE3750Da237f93B8E339c536989b8978a438',
    'data': '0x095ea7b3000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
})

Error Handling

Common errors and solutions:

Error CodeDescriptionSolution
-32000Execution revertedThe call would revert — check the revertReason in the trace result for details
-32000Missing trie nodeHistorical block state not available — use an archive node endpoint
-32601Method not foundDebug namespace is not enabled on the node
-32603Internal errorCall may be too complex — try increasing timeout or use disableStorage
-32005Rate limit exceededReduce request frequency or upgrade your plan
-32602Invalid paramsVerify the call object fields and block number format
-32000Insufficient fundsThe from address lacks the balance for the value field — note this is simulated, not real funds
JavaScript
async function safeTraceCall(provider, callObj, blockNumber = 'latest', options = {}) {
  const defaultOptions = {
    tracer: 'callTracer',
    timeout: '30s',
    ...options
  };

  try {
    return await provider.send('debug_traceCall', [callObj, blockNumber, defaultOptions]);
  } catch (error) {
    if (error.code === -32601) {
      throw new Error('debug_traceCall not available — ensure archive node with debug API');
    }
    if (error.code === -32000 && error.message?.includes('trie node')) {
      throw new Error(`State unavailable for block ${blockNumber} — use an archive endpoint`);
    }
    if (error.message?.includes('timeout')) {
      console.warn('Trace timed out — retrying with reduced detail');
      return await provider.send('debug_traceCall', [
        callObj,
        blockNumber,
        { tracer: 'callTracer', tracerConfig: { onlyTopCall: true } }
      ]);
    }
    throw error;
  }
}