debug_traceCall - Pulsechain RPC Method
Trace a call without creating a transaction on Pulsechain. Simulate transactions, debug contract interactions, and analyze gas usage with full execution traces.
Traces a call on Pulsechain 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 Pulsechain? Build on the Ethereum fork L1 with lower gas fees and proof-of-stake consensus with full Ethereum state fork, proof-of-stake consensus, lower gas fees than Ethereum mainnet, and PulseX DEX ecosystem.
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 Pulsechain — ensure your plan includes debug namespace support.
When to Use This Method
debug_traceCall is powerful for DeFi developers, staking protocol builders, and teams migrating from Ethereum:
- 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 Pulsechain
- Debugging Contract Interactions — Step through contract execution at the opcode level to understand complex interactions, delegate calls, and proxy patterns for low-cost DeFi, staking protocols, and Ethereum-compatible dApps with reduced fees
- Gas Estimation With Trace Details — Go beyond
eth_estimateGasby 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 Pulsechain to catch reverts and estimate costs:
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 Pulsechain:
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 Pulsechain:
import requests
def security_trace_call(call_object, block='latest'):
response = requests.post('https://api-pulse-mainnet.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': '0xA1077a294dDE1B09bB078844df40758a5D0f9a27',
'data': '0x095ea7b3000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
})Error Handling
Common errors and solutions:
| Error Code | Description | Solution |
|---|---|---|
| -32000 | Execution reverted | The call would revert — check the revertReason in the trace result for details |
| -32000 | Missing trie node | Historical block state not available — use an archive node endpoint |
| -32601 | Method not found | Debug namespace is not enabled on the node |
| -32603 | Internal error | Call may be too complex — try increasing timeout or use disableStorage |
| -32005 | Rate limit exceeded | Reduce request frequency or upgrade your plan |
| -32602 | Invalid params | Verify the call object fields and block number format |
| -32000 | Insufficient funds | The from address lacks the balance for the value field — note this is simulated, not real funds |
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;
}
}Related Methods
eth_call— Execute a call without trace (returns only the result, not execution details)debug_traceTransaction— Trace an already-executed transaction by hasheth_estimateGas— Estimate gas for a call (without trace details)debug_traceBlockByNumber— Trace all transactions in a block by numberdebug_traceBlockByHash— Trace all transactions in a block by hash
debug_traceBlockByNumber
Trace all transactions in a block by number on Pulsechain. Analyze gas consumption, debug state transitions, and inspect historical block execution.
debug_traceTransaction
Trace a transaction execution on Pulsechain. Requires archive node for debugging low-cost DeFi, staking protocols, and Ethereum-compatible dApps with reduced fees.