debug_traceTransaction
Returns a detailed execution trace of a transaction, showing all internal calls, state changes, and gas consumption. Essential for debugging complex smart contract interactions.
Premium Method
This method requires enhanced API access. Please contact support for access to debug methods.
When to Use This Method​
debug_traceTransaction
is crucial for:
- Smart Contract Debugging - Analyze transaction execution step-by-step
- Gas Optimization - Identify gas-intensive operations
- Security Analysis - Trace fund flows and state changes
- Failure Investigation - Understand why transactions reverted
Parameters​
-
Transaction Hash -
DATA
, 32 Bytes- Hash of the transaction to trace
-
Tracer Configuration -
Object
tracer
- Type of tracer ("callTracer", "prestateTracer", or custom)tracerConfig
- Additional configuration optionsonlyTopCall
- Return only top-level calltimeout
- Execution timeout (e.g., "5s")disableStorage
- Disable storage capturedisableMemory
- Disable memory capturedisableStack
- Disable stack capture
{
"jsonrpc": "2.0",
"method": "debug_traceTransaction",
"params": [
"0x12345...",
{
"tracer": "callTracer",
"tracerConfig": {
"onlyTopCall": false
}
}
],
"id": 1
}
Returns​
Detailed trace object containing:
type
- Call type (CALL, CREATE, DELEGATECALL, etc.)from
- Sender addressto
- Recipient addressvalue
- Wei transferredgas
- Gas providedgasUsed
- Gas consumedinput
- Input dataoutput
- Return dataerror
- Error message if failedcalls
- Array of sub-callsrevertReason
- Decoded revert reason
Implementation Examples​
- JavaScript
- Python
import { JsonRpcProvider } from 'ethers';
const provider = new JsonRpcProvider('https://api-iotex-mainnet.n.dwellir.com/YOUR_API_KEY');
// Advanced transaction tracer
class TransactionDebugger {
constructor(provider) {
this.provider = provider;
}
async traceTransaction(txHash, options = {}) {
const defaultOptions = {
tracer: 'callTracer',
tracerConfig: {
onlyTopCall: false
}
};
const tracerConfig = { ...defaultOptions, ...options };
try {
const trace = await this.provider.send('debug_traceTransaction', [
txHash,
tracerConfig
]);
return this.parseTrace(trace);
} catch (error) {
throw new Error(`Failed to trace transaction: ${error.message}`);
}
}
parseTrace(trace) {
const result = {
mainCall: {
type: trace.type,
from: trace.from,
to: trace.to,
value: trace.value || '0x0',
gas: parseInt(trace.gas, 16),
gasUsed: parseInt(trace.gasUsed, 16),
success: !trace.error,
error: trace.error,
input: trace.input,
output: trace.output
},
subCalls: [],
totalGasUsed: parseInt(trace.gasUsed, 16),
callDepth: 0
};
if (trace.calls && trace.calls.length > 0) {
result.subCalls = this.flattenCalls(trace.calls);
result.callDepth = this.getMaxDepth(trace.calls);
}
return result;
}
flattenCalls(calls, depth = 1, result = []) {
for (const call of calls) {
result.push({
depth: depth,
type: call.type,
from: call.from,
to: call.to,
value: call.value || '0x0',
gas: parseInt(call.gas, 16),
gasUsed: parseInt(call.gasUsed, 16),
success: !call.error,
error: call.error,
input: call.input,
output: call.output
});
if (call.calls && call.calls.length > 0) {
this.flattenCalls(call.calls, depth + 1, result);
}
}
return result;
}
getMaxDepth(calls, currentDepth = 1) {
let maxDepth = currentDepth;
for (const call of calls) {
if (call.calls && call.calls.length > 0) {
const childDepth = this.getMaxDepth(call.calls, currentDepth + 1);
maxDepth = Math.max(maxDepth, childDepth);
}
}
return maxDepth;
}
async analyzeGasUsage(txHash) {
const trace = await this.traceTransaction(txHash);
const gasAnalysis = {
totalGas: trace.totalGasUsed,
mainCallGas: trace.mainCall.gasUsed,
subCallsGas: 0,
gasPerCall: [],
mostExpensiveCalls: []
};
// Analyze sub-calls
for (const call of trace.subCalls) {
gasAnalysis.subCallsGas += call.gasUsed;
gasAnalysis.gasPerCall.push({
to: call.to,
type: call.type,
gas: call.gasUsed,
percentage: ((call.gasUsed / trace.totalGasUsed) * 100).toFixed(2)
});
}
// Sort by gas usage
gasAnalysis.mostExpensiveCalls = gasAnalysis.gasPerCall
.sort((a, b) => b.gas - a.gas)
.slice(0, 5);
return gasAnalysis;
}
async traceWithPrestate(txHash) {
const trace = await this.provider.send('debug_traceTransaction', [
txHash,
{
tracer: 'prestateTracer',
tracerConfig: {
diffMode: true
}
}
]);
return this.analyzePrestateChanges(trace);
}
analyzePrestateChanges(trace) {
const changes = {
accountsModified: [],
storageChanges: [],
balanceChanges: []
};
// Analyze pre-state
if (trace.pre) {
for (const [address, state] of Object.entries(trace.pre)) {
changes.accountsModified.push(address);
if (state.balance) {
changes.balanceChanges.push({
address: address,
preBal: state.balance,
postBal: trace.post?.[address]?.balance || state.balance
});
}
if (state.storage) {
for (const [slot, value] of Object.entries(state.storage)) {
changes.storageChanges.push({
address: address,
slot: slot,
preValue: value,
postValue: trace.post?.[address]?.storage?.[slot] || value
});
}
}
}
}
return changes;
}
async traceContractCreation(txHash) {
const trace = await this.traceTransaction(txHash, {
tracer: 'callTracer',
tracerConfig: {
withLog: true
}
});
if (trace.mainCall.type !== 'CREATE' && trace.mainCall.type !== 'CREATE2') {
throw new Error('Transaction is not a contract creation');
}
return {
deployedAddress: trace.mainCall.to,
deploymentCost: trace.mainCall.gasUsed,
bytecode: trace.mainCall.input,
constructorReturn: trace.mainCall.output,
success: trace.mainCall.success,
subContracts: trace.subCalls
.filter(call => call.type === 'CREATE' || call.type === 'CREATE2')
.map(call => ({
address: call.to,
gasUsed: call.gasUsed
}))
};
}
async findRevertReason(txHash) {
const trace = await this.traceTransaction(txHash);
// Check main call
if (trace.mainCall.error) {
const reason = this.decodeRevertReason(trace.mainCall.output);
return {
level: 'main',
error: trace.mainCall.error,
decodedReason: reason,
callData: trace.mainCall.input
};
}
// Check sub-calls
for (const call of trace.subCalls) {
if (call.error) {
const reason = this.decodeRevertReason(call.output);
return {
level: `depth_${call.depth}`,
to: call.to,
error: call.error,
decodedReason: reason,
callData: call.input
};
}
}
return null;
}
decodeRevertReason(output) {
if (!output || output === '0x') return 'No revert message';
// Check for standard revert string (0x08c379a0)
if (output.startsWith('0x08c379a0')) {
try {
const reason = ethers.AbiCoder.defaultAbiCoder().decode(
['string'],
'0x' + output.slice(10)
)[0];
return reason;
} catch {
return 'Unable to decode revert reason';
}
}
// Check for panic code (0x4e487b71)
if (output.startsWith('0x4e487b71')) {
const panicCodes = {
'0x00': 'Generic compiler panic',
'0x01': 'Assert failed',
'0x11': 'Arithmetic overflow/underflow',
'0x12': 'Division by zero',
'0x21': 'Invalid enum value',
'0x22': 'Storage byte array error',
'0x31': 'Pop empty array',
'0x32': 'Array out of bounds',
'0x41': 'Too much memory allocated',
'0x51': 'Internal function call error'
};
const code = '0x' + output.slice(10, 12);
return panicCodes[code] || `Panic code: ${code}`;
}
return 'Custom error or unknown revert';
}
}
// Usage examples
const debugger = new TransactionDebugger(provider);
// Basic trace
const trace = await debugger.traceTransaction('0xTxHash');
console.log('Main call gas:', trace.mainCall.gasUsed);
console.log('Total sub-calls:', trace.subCalls.length);
// Gas analysis
const gasAnalysis = await debugger.analyzeGasUsage('0xTxHash');
console.log('Most expensive calls:', gasAnalysis.mostExpensiveCalls);
// Find revert reason
const revertInfo = await debugger.findRevertReason('0xFailedTxHash');
if (revertInfo) {
console.log('Revert reason:', revertInfo.decodedReason);
}
// Trace contract deployment
const deployment = await debugger.traceContractCreation('0xDeployTxHash');
console.log('Deployed at:', deployment.deployedAddress);
from web3 import Web3
import json
from typing import Dict, List, Any, Optional
from eth_utils import to_hex, from_wei
w3 = Web3(Web3.HTTPProvider('https://api-iotex-mainnet.n.dwellir.com/YOUR_API_KEY'))
class TransactionTracer:
"""Advanced transaction tracing and debugging on IoTeX L2"""
def __init__(self, w3_instance):
self.w3 = w3_instance
def trace_transaction(
self,
tx_hash: str,
tracer: str = 'callTracer',
config: Optional[Dict] = None
) -> Dict[str, Any]:
"""Trace transaction execution"""
tracer_config = {
'tracer': tracer,
'tracerConfig': config or {'onlyTopCall': False}
}
try:
trace = self.w3.provider.make_request(
'debug_traceTransaction',
[tx_hash, tracer_config]
)
return self._parse_trace(trace['result'])
except Exception as e:
return {'error': str(e)}
def _parse_trace(self, trace: Dict) -> Dict[str, Any]:
"""Parse and structure trace data"""
result = {
'main_call': {
'type': trace.get('type'),
'from': trace.get('from'),
'to': trace.get('to'),
'value': trace.get('value', '0x0'),
'gas': int(trace.get('gas', '0x0'), 16),
'gas_used': int(trace.get('gasUsed', '0x0'), 16),
'success': 'error' not in trace,
'error': trace.get('error'),
'input': trace.get('input'),
'output': trace.get('output')
},
'sub_calls': [],
'statistics': {}
}
# Process sub-calls
if 'calls' in trace:
result['sub_calls'] = self._flatten_calls(trace['calls'])
result['statistics'] = self._calculate_statistics(result)
return result
def _flatten_calls(
self,
calls: List[Dict],
depth: int = 1
) -> List[Dict[str, Any]]:
"""Flatten nested call structure"""
flattened = []
for call in calls:
flat_call = {
'depth': depth,
'type': call.get('type'),
'from': call.get('from'),
'to': call.get('to'),
'value': call.get('value', '0x0'),
'gas': int(call.get('gas', '0x0'), 16),
'gas_used': int(call.get('gasUsed', '0x0'), 16),
'success': 'error' not in call,
'error': call.get('error'),
'input': call.get('input'),
'output': call.get('output')
}
flattened.append(flat_call)
# Recursively process nested calls
if 'calls' in call:
flattened.extend(
self._flatten_calls(call['calls'], depth + 1)
)
return flattened
def _calculate_statistics(self, trace: Dict) -> Dict[str, Any]:
"""Calculate trace statistics"""
total_gas = trace['main_call']['gas_used']
sub_calls = trace['sub_calls']
stats = {
'total_gas_used': total_gas,
'num_sub_calls': len(sub_calls),
'max_depth': max([c['depth'] for c in sub_calls]) if sub_calls else 0,
'failed_calls': len([c for c in sub_calls if not c['success']]),
'unique_addresses': len(set([c['to'] for c in sub_calls if c['to']]))
}
# Gas distribution
if sub_calls:
stats['gas_distribution'] = {
'main_call_percentage': (
(trace['main_call']['gas_used'] / total_gas) * 100
),
'sub_calls_percentage': (
sum(c['gas_used'] for c in sub_calls) / total_gas * 100
)
}
return stats
def analyze_gas_usage(self, tx_hash: str) -> Dict[str, Any]:
"""Detailed gas usage analysis"""
trace = self.trace_transaction(tx_hash)
if 'error' in trace:
return trace
gas_analysis = {
'total_gas': trace['main_call']['gas_used'],
'breakdown': [],
'expensive_operations': [],
'optimization_suggestions': []
}
# Analyze each call
all_calls = [trace['main_call']] + trace['sub_calls']
for call in all_calls:
gas_percentage = (call['gas_used'] / trace['main_call']['gas_used']) * 100
gas_analysis['breakdown'].append({
'to': call['to'],
'type': call['type'],
'gas_used': call['gas_used'],
'percentage': round(gas_percentage, 2),
'depth': call.get('depth', 0)
})
# Sort by gas usage
gas_analysis['breakdown'].sort(key=lambda x: x['gas_used'], reverse=True)
gas_analysis['expensive_operations'] = gas_analysis['breakdown'][:5]
# Generate optimization suggestions
if trace['statistics']['failed_calls'] > 0:
gas_analysis['optimization_suggestions'].append(
'Failed calls detected - add validation to prevent wasted gas'
)
if trace['statistics']['max_depth'] > 3:
gas_analysis['optimization_suggestions'].append(
'Deep call stack detected - consider flattening contract architecture'
)
for op in gas_analysis['expensive_operations']:
if op['percentage'] > 30:
gas_analysis['optimization_suggestions'].append(
f"Operation to {op['to']} uses {op['percentage']}% of gas - consider optimization"
)
return gas_analysis
def trace_with_state_diff(self, tx_hash: str) -> Dict[str, Any]:
"""Trace transaction with state changes"""
trace = self.w3.provider.make_request(
'debug_traceTransaction',
[tx_hash, {
'tracer': 'prestateTracer',
'tracerConfig': {'diffMode': True}
}]
)
result = trace.get('result', {})
state_changes = {
'accounts_modified': [],
'storage_changes': [],
'balance_changes': [],
'code_changes': []
}
# Process pre-state and post-state
pre = result.get('pre', {})
post = result.get('post', {})
all_addresses = set(list(pre.keys()) + list(post.keys()))
for address in all_addresses:
pre_state = pre.get(address, {})
post_state = post.get(address, {})
# Check balance changes
if pre_state.get('balance') != post_state.get('balance'):
state_changes['balance_changes'].append({
'address': address,
'pre_balance': pre_state.get('balance', '0x0'),
'post_balance': post_state.get('balance', '0x0'),
'difference': hex(
int(post_state.get('balance', '0x0'), 16) -
int(pre_state.get('balance', '0x0'), 16)
)
})
# Check storage changes
pre_storage = pre_state.get('storage', {})
post_storage = post_state.get('storage', {})
all_slots = set(list(pre_storage.keys()) + list(post_storage.keys()))
for slot in all_slots:
if pre_storage.get(slot) != post_storage.get(slot):
state_changes['storage_changes'].append({
'address': address,
'slot': slot,
'pre_value': pre_storage.get(slot, '0x0'),
'post_value': post_storage.get(slot, '0x0')
})
state_changes['accounts_modified'].append(address)
return state_changes
def find_revert_reason(self, tx_hash: str) -> Optional[Dict[str, Any]]:
"""Extract revert reason from failed transaction"""
trace = self.trace_transaction(tx_hash)
if 'error' in trace:
return trace
# Check main call
if trace['main_call'].get('error'):
return {
'level': 'main',
'error': trace['main_call']['error'],
'decoded_reason': self._decode_revert(
trace['main_call'].get('output', '0x')
),
'to': trace['main_call']['to']
}
# Check sub-calls
for call in trace['sub_calls']:
if call.get('error'):
return {
'level': f"depth_{call['depth']}",
'error': call['error'],
'decoded_reason': self._decode_revert(
call.get('output', '0x')
),
'to': call['to']
}
return None
def _decode_revert(self, output: str) -> str:
"""Decode revert reason from output"""
if not output or output == '0x':
return 'No revert message'
# Standard revert string
if output.startswith('0x08c379a0'):
try:
# Decode string from ABI encoded data
hex_str = output[10:] # Skip function selector
str_offset = int(hex_str[0:64], 16) * 2
str_length = int(hex_str[64:128], 16) * 2
str_data = hex_str[128:128+str_length]
return bytes.fromhex(str_data).decode('utf-8')
except:
return 'Unable to decode revert string'
# Panic codes
elif output.startswith('0x4e487b71'):
panic_codes = {
'00': 'Generic compiler panic',
'01': 'Assert failed',
'11': 'Arithmetic overflow/underflow',
'12': 'Division by zero',
'21': 'Invalid enum value',
'31': 'Pop empty array',
'32': 'Array out of bounds',
'41': 'Too much memory allocated',
'51': 'Internal function call error'
}
code = output[10:12]
return panic_codes.get(code, f'Panic code: 0x{code}')
return f'Custom error: {output[:10]}'
def visualize_call_flow(self, tx_hash: str) -> str:
"""Generate ASCII visualization of call flow"""
trace = self.trace_transaction(tx_hash)
if 'error' in trace:
return "Error: " + trace['error']
lines = []
lines.append(f"Transaction: {tx_hash[:10]}...")
lines.append("=" * 60)
# Main call
main = trace['main_call']
lines.append(f"→ {main['type']} to {main['to'][:10]}...")
lines.append(f" Gas: {main['gas_used']:,} | Value: {main['value']}")
# Sub-calls
for call in trace['sub_calls']:
indent = " " * call['depth']
arrow = "→" if call['success'] else "✗"
lines.append(
f"{indent}{arrow} {call['type']} to {call['to'][:10]}... "
f"(Gas: {call['gas_used']:,})"
)
if call.get('error'):
lines.append(f"{indent} Error: {call['error']}")
lines.append("=" * 60)
lines.append(f"Total Gas Used: {main['gas_used']:,}")
lines.append(f"Sub-calls: {len(trace['sub_calls'])}")
return "\n".join(lines)
# Usage examples
tracer = TransactionTracer(w3)
# Basic trace
tx_trace = tracer.trace_transaction('0xTransactionHash')
print(f"Gas used: {tx_trace['main_call']['gas_used']}")
print(f"Sub-calls: {tx_trace['statistics']['num_sub_calls']}")
# Gas analysis
gas_analysis = tracer.analyze_gas_usage('0xTransactionHash')
print("Expensive operations:")
for op in gas_analysis['expensive_operations']:
print(f" {op['to']}: {op['gas_used']:,} gas ({op['percentage']}%)")
# Find revert reason
revert_info = tracer.find_revert_reason('0xFailedTransaction')
if revert_info:
print(f"Revert reason: {revert_info['decoded_reason']}")
# Visualize call flow
visualization = tracer.visualize_call_flow('0xTransactionHash')
print(visualization)
# State changes
state_diff = tracer.trace_with_state_diff('0xTransactionHash')
print(f"Accounts modified: {len(state_diff['accounts_modified'])}")
print(f"Storage slots changed: {len(state_diff['storage_changes'])}")
Common Use Cases​
1. DeFi Transaction Analysis​
// Analyze complex DeFi transactions
async function analyzeDeFiTransaction(txHash) {
const debugger = new TransactionDebugger(provider);
const trace = await debugger.traceTransaction(txHash);
// Identify DeFi operations
const operations = [];
for (const call of trace.subCalls) {
// Detect swap
if (call.input.startsWith('0x38ed1739')) { // swapExactTokensForTokens
operations.push({
type: 'SWAP',
protocol: call.to,
depth: call.depth,
gasUsed: call.gasUsed
});
}
// Detect liquidity operations
if (call.input.startsWith('0xe8e33700')) { // addLiquidity
operations.push({
type: 'ADD_LIQUIDITY',
protocol: call.to,
depth: call.depth,
gasUsed: call.gasUsed
});
}
// Detect lending operations
if (call.input.startsWith('0xa415bcad')) { // deposit
operations.push({
type: 'DEPOSIT',
protocol: call.to,
depth: call.depth,
gasUsed: call.gasUsed
});
}
}
return {
operations: operations,
totalOperations: operations.length,
protocolsUsed: [...new Set(operations.map(op => op.protocol))],
gasBreakdown: operations.map(op => ({
type: op.type,
gas: op.gasUsed,
percentage: ((op.gasUsed / trace.totalGasUsed) * 100).toFixed(2)
}))
};
}
2. Smart Contract Security Analysis​
// Security-focused transaction analysis
async function securityAnalysis(txHash) {
const debugger = new TransactionDebugger(provider);
const trace = await debugger.traceTransaction(txHash);
const securityFlags = {
reentrancy: false,
unexpectedETHTransfer: false,
failedCalls: [],
delegateCalls: [],
selfDestructs: [],
storageCollisions: []
};
// Check for reentrancy patterns
const callCounts = {};
for (const call of trace.subCalls) {
const key = `${call.from}-${call.to}`;
callCounts[key] = (callCounts[key] || 0) + 1;
if (callCounts[key] > 1) {
securityFlags.reentrancy = true;
}
// Track delegate calls
if (call.type === 'DELEGATECALL') {
securityFlags.delegateCalls.push({
from: call.from,
to: call.to,
depth: call.depth
});
}
// Track self-destructs
if (call.type === 'SELFDESTRUCT') {
securityFlags.selfDestructs.push(call.from);
}
// Track failed calls
if (!call.success) {
securityFlags.failedCalls.push({
to: call.to,
error: call.error,
depth: call.depth
});
}
}
return securityFlags;
}
3. Gas Optimization Recommendations​
// Generate gas optimization recommendations
async function getOptimizationRecommendations(txHash) {
const debugger = new TransactionDebugger(provider);
const gasAnalysis = await debugger.analyzeGasUsage(txHash);
const recommendations = [];
// Check for expensive patterns
for (const call of gasAnalysis.mostExpensiveCalls) {
if (call.percentage > 30) {
recommendations.push({
severity: 'high',
target: call.to,
message: `Call uses ${call.percentage}% of total gas`,
suggestion: 'Consider optimizing or caching results'
});
}
// SLOAD operations (storage reads)
if (call.gas > 2100 && call.gas < 2200) {
recommendations.push({
severity: 'medium',
target: call.to,
message: 'Multiple storage reads detected',
suggestion: 'Cache storage values in memory'
});
}
}
// Check call depth
const trace = await debugger.traceTransaction(txHash);
if (trace.callDepth > 3) {
recommendations.push({
severity: 'medium',
message: `Deep call stack (depth: ${trace.callDepth})`,
suggestion: 'Consider flattening contract architecture'
});
}
return recommendations;
}
Error Handling​
Error Type | Description | Solution |
---|---|---|
Method not available | Debug methods not enabled | Contact support for access |
Timeout | Trace execution timeout | Increase timeout in config |
Out of memory | Large trace data | Use onlyTopCall option |
async function safeTrace(txHash, options = {}) {
try {
// Start with minimal trace
let trace = await provider.send('debug_traceTransaction', [
txHash,
{
tracer: 'callTracer',
tracerConfig: {
onlyTopCall: true,
timeout: '10s'
}
}
]);
// If successful, try full trace
if (options.fullTrace) {
trace = await provider.send('debug_traceTransaction', [
txHash,
{
tracer: 'callTracer',
tracerConfig: {
onlyTopCall: false,
timeout: '30s'
}
}
]);
}
return trace;
} catch (error) {
if (error.message.includes('timeout')) {
console.error('Trace timeout - transaction too complex');
// Return partial results
return {
error: 'timeout',
partial: true
};
}
if (error.message.includes('not available')) {
console.error('Debug methods not available on this endpoint');
return {
error: 'not_available',
suggestion: 'Contact support for debug method access'
};
}
throw error;
}
}
Need help? Contact our support team for debug method access or check the IoTeX documentation.