debug_traceCall
Executes a call and returns detailed trace information without creating an actual transaction on the blockchain. This method is invaluable for testing contract interactions, debugging issues, and estimating gas consumption without spending any gas.
Premium Method
This method is available on premium plans with debug API access. Contact our team for access.
When to Use This Method​
debug_traceCall
is essential for:
- Contract Testing - Test contract methods without spending gas
- Debugging Failed Transactions - Understand why a transaction would fail
- Gas Optimization - Analyze gas consumption before executing
- Security Analysis - Trace potential attack vectors safely
- Integration Testing - Validate contract interactions before deployment
- What-If Analysis - Simulate different scenarios without commitment
Parameters​
1. transaction
(required)​
Transaction call object with the following fields:
Field | Type | Required | Description |
---|---|---|---|
from | Address | No | The address the transaction is sent from |
to | Address | Yes | The address the transaction is directed to |
gas | Quantity | No | Gas provided for the transaction execution |
gasPrice | Quantity | No | Gas price for each paid gas |
value | Quantity | No | Value sent with this transaction |
data | Data | No | Method signature and encoded parameters |
2. blockNumber
(required)​
- Type:
QUANTITY|TAG
- Description: Block number or tag (
"latest"
,"earliest"
,"pending"
) - Example:
"latest"
or"0x5bad55"
3. tracer
(optional)​
- Type:
Object
- Description: Tracer configuration object
- Fields:
tracer
: The type of tracer (callTracer
,prestateTracer
, or custom)tracerConfig
: Additional configuration options
Returns​
Trace object containing:
Field | Type | Description |
---|---|---|
failed | Boolean | Whether the call execution failed |
gas | Quantity | Gas consumed by the execution |
returnValue | Data | Return data from the call |
structLogs | Array | Detailed execution steps (if using default tracer) |
from | Address | Sender address (with callTracer) |
to | Address | Recipient address (with callTracer) |
type | String | Call type (CALL, CREATE, etc.) |
input | Data | Call input data |
output | Data | Call output data |
error | String | Error message if execution failed |
revertReason | String | Decoded revert reason if available |
calls | Array | Nested calls (with callTracer) |
Implementation Examples​
- cURL
- JavaScript
- Python
- Go
curl -X POST https://api-arbitrum-mainnet.n.dwellir.com/YOUR_API_KEY \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "debug_traceCall",
"params": [
{
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb5",
"to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"gas": "0x30d40",
"gasPrice": "0x3b9aca00",
"value": "0x0",
"data": "0x70a08231000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0beb5"
},
"latest",
{
"tracer": "callTracer",
"tracerConfig": {
"withLog": true
}
}
],
"id": 1
}'
// Trace an ERC20 balanceOf call
async function traceERC20Balance(tokenAddress, holderAddress) {
const balanceOfSignature = '0x70a08231'; // balanceOf(address)
const paddedAddress = holderAddress.slice(2).padStart(64, '0');
const callData = balanceOfSignature + paddedAddress;
const response = await fetch('https://api-arbitrum-mainnet.n.dwellir.com/YOUR_API_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'debug_traceCall',
params: [
{
from: holderAddress,
to: tokenAddress,
data: callData,
gas: '0x30d40'
},
'latest',
{
tracer: 'callTracer',
tracerConfig: {
withLog: true
}
}
],
id: 1
})
});
const data = await response.json();
if (data.error) {
throw new Error(`RPC Error: ${data.error.message}`);
}
// Parse the balance from the output
const output = data.result.output;
const balance = parseInt(output, 16);
return {
balance,
gasUsed: parseInt(data.result.gasUsed, 16),
success: !data.result.error
};
}
// Trace a complex DeFi interaction
async function traceSwap(routerAddress, swapData) {
const response = await fetch('https://api-arbitrum-mainnet.n.dwellir.com/YOUR_API_KEY', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method: 'debug_traceCall',
params: [
{
from: swapData.from,
to: routerAddress,
value: swapData.value || '0x0',
data: swapData.calldata,
gas: '0x7a120' // 500,000 gas
},
'latest',
{
tracer: 'callTracer',
tracerConfig: {
onlyTopCall: false,
withLog: true
}
}
],
id: 1
})
});
const data = await response.json();
const trace = data.result;
// Analyze the swap path
const swapPath = [];
if (trace.calls) {
trace.calls.forEach(call => {
if (call.type === 'CALL' && call.to) {
swapPath.push({
to: call.to,
value: call.value,
gasUsed: parseInt(call.gasUsed, 16)
});
}
});
}
return {
success: !trace.error,
gasUsed: parseInt(trace.gasUsed, 16),
output: trace.output,
swapPath,
error: trace.error || trace.revertReason
};
}
import requests
import json
from web3 import Web3
def trace_call(tx_params, block='latest', tracer_type='callTracer'):
"""
Trace a call without creating a transaction
"""
url = 'https://api-arbitrum-mainnet.n.dwellir.com/YOUR_API_KEY'
payload = {
"jsonrpc": "2.0",
"method": "debug_traceCall",
"params": [
tx_params,
block,
{
"tracer": tracer_type,
"tracerConfig": {
"withLog": True,
"onlyTopCall": False
}
}
],
"id": 1
}
response = requests.post(url, json=payload)
data = response.json()
if 'error' in data:
raise Exception(f"RPC Error: {data['error']['message']}")
return data['result']
# Test an ERC20 transfer
def test_token_transfer(token_address, from_addr, to_addr, amount):
"""
Test an ERC20 transfer without executing it
"""
# Encode transfer(address,uint256)
w3 = Web3()
transfer_data = '0xa9059cbb' # transfer method signature
transfer_data += w3.to_hex(w3.to_bytes(hexstr=to_addr[2:].zfill(64)))[2:]
transfer_data += w3.to_hex(w3.to_bytes(amount))[2:].zfill(64)
tx_params = {
'from': from_addr,
'to': token_address,
'data': transfer_data,
'gas': hex(100000)
}
try:
trace = trace_call(tx_params)
if trace.get('error'):
print(f"Transfer would fail: {trace['error']}")
if trace.get('revertReason'):
print(f"Revert reason: {trace['revertReason']}")
return False
gas_used = int(trace.get('gasUsed', '0x0'), 16)
print(f"Transfer would succeed")
print(f"Gas required: {gas_used:,}")
# Check if output indicates success (usually 0x1 for bool true)
if trace.get('output') == '0x0000000000000000000000000000000000000000000000000000000000000001':
print("ERC20 transfer returns: true")
return True
except Exception as e:
print(f"Error testing transfer: {e}")
return False
# Analyze gas usage for different input sizes
def analyze_gas_scaling(contract_address, method_signatures):
"""
Analyze how gas scales with different inputs
"""
results = []
for method_sig, test_inputs in method_signatures.items():
for input_data in test_inputs:
tx_params = {
'to': contract_address,
'data': method_sig + input_data,
'gas': hex(10000000) # High gas limit for testing
}
try:
trace = trace_call(tx_params)
gas_used = int(trace.get('gasUsed', '0x0'), 16)
results.append({
'method': method_sig[:10],
'input_size': len(input_data) // 2, # bytes
'gas_used': gas_used,
'success': not trace.get('error')
})
except Exception as e:
results.append({
'method': method_sig[:10],
'input_size': len(input_data) // 2,
'error': str(e)
})
# Analyze results
for result in results:
if 'gas_used' in result:
gas_per_byte = result['gas_used'] / result['input_size'] if result['input_size'] > 0 else 0
print(f"Method {result['method']}: {result['gas_used']:,} gas for {result['input_size']} bytes")
print(f" Gas per byte: {gas_per_byte:.2f}")
return results
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"net/http"
"strings"
)
type CallParams struct {
From string `json:"from,omitempty"`
To string `json:"to"`
Gas string `json:"gas,omitempty"`
GasPrice string `json:"gasPrice,omitempty"`
Value string `json:"value,omitempty"`
Data string `json:"data,omitempty"`
}
type TraceConfig struct {
Tracer string `json:"tracer"`
TracerConfig map[string]interface{} `json:"tracerConfig"`
}
type CallTrace struct {
Type string `json:"type"`
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
Gas string `json:"gas"`
GasUsed string `json:"gasUsed"`
Input string `json:"input"`
Output string `json:"output"`
Error string `json:"error,omitempty"`
Calls []CallTrace `json:"calls,omitempty"`
}
func traceCall(params CallParams, block string) (*CallTrace, error) {
url := "https://api-arbitrum-mainnet.n.dwellir.com/YOUR_API_KEY"
config := TraceConfig{
Tracer: "callTracer",
TracerConfig: map[string]interface{}{
"withLog": true,
"onlyTopCall": false,
},
}
payload := map[string]interface{}{
"jsonrpc": "2.0",
"method": "debug_traceCall",
"params": []interface{}{params, block, config},
"id": 1,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, err
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result struct {
Result *CallTrace `json:"result"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if result.Error != nil {
return nil, fmt.Errorf("RPC error: %s", result.Error.Message)
}
return result.Result, nil
}
// Test ERC20 balanceOf call
func checkTokenBalance(tokenAddr, holderAddr string) (*big.Int, error) {
// balanceOf(address) method signature
methodSig := "70a08231"
// Pad address to 32 bytes
paddedAddr := strings.TrimPrefix(holderAddr, "0x")
paddedAddr = fmt.Sprintf("%064s", paddedAddr)
callData := "0x" + methodSig + paddedAddr
params := CallParams{
From: holderAddr,
To: tokenAddr,
Data: callData,
Gas: "0x30d40",
}
trace, err := traceCall(params, "latest")
if err != nil {
return nil, err
}
if trace.Error != "" {
return nil, fmt.Errorf("call failed: %s", trace.Error)
}
// Parse balance from output
output := strings.TrimPrefix(trace.Output, "0x")
balance := new(big.Int)
balance.SetString(output, 16)
gasUsed := new(big.Int)
gasUsed.SetString(strings.TrimPrefix(trace.GasUsed, "0x"), 16)
fmt.Printf("Balance check successful:\n")
fmt.Printf(" Balance: %s\n", balance.String())
fmt.Printf(" Gas Used: %s\n", gasUsed.String())
return balance, nil
}
// Simulate contract deployment
func simulateDeployment(bytecode string, from string) error {
params := CallParams{
From: from,
Data: bytecode,
Gas: "0x7a120", // 500,000 gas
}
trace, err := traceCall(params, "latest")
if err != nil {
return err
}
if trace.Error != "" {
return fmt.Errorf("deployment would fail: %s", trace.Error)
}
gasUsed := new(big.Int)
gasUsed.SetString(strings.TrimPrefix(trace.GasUsed, "0x"), 16)
fmt.Printf("Deployment simulation:\n")
fmt.Printf(" Gas Required: %s\n", gasUsed.String())
fmt.Printf(" Contract Address: %s\n", calculateContractAddress(from))
return nil
}
func calculateContractAddress(from string) string {
// Simplified contract address calculation
// In production, use proper RLP encoding and nonce
return "0x..." // Placeholder
}
func main() {
// Example: Check USDC balance
usdcAddr := "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
holderAddr := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb5"
balance, err := checkTokenBalance(usdcAddr, holderAddr)
if err != nil {
fmt.Printf("Error checking balance: %v\n", err)
} else {
fmt.Printf("USDC Balance: %s\n", balance.String())
}
}
Response Example​
Successful Response​
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"from": "0x742d35cc6634c0532925a3b844bc9e7595f0beb5",
"to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"gas": "0x30d40",
"gasUsed": "0x6d60",
"input": "0x70a08231000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0beb5",
"output": "0x00000000000000000000000000000000000000000000000000000000000f4240",
"type": "CALL",
"value": "0x0",
"calls": [
{
"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
"to": "0x4200000000000000000000000000000000000001",
"gas": "0x2a8d8",
"gasUsed": "0x2108",
"input": "0x",
"output": "0x",
"type": "STATICCALL",
"value": "0x0"
}
]
}
}
Common Use Cases​
1. Pre-Transaction Validation​
Validate transactions before sending:
async function validateTransaction(txParams) {
try {
const trace = await traceCall(txParams, 'latest');
if (trace.error) {
console.log('Transaction will fail:', trace.error);
// Decode revert reason
if (trace.revertReason) {
console.log('Reason:', trace.revertReason);
}
return {
valid: false,
reason: trace.error,
gasEstimate: null
};
}
const gasUsed = parseInt(trace.gasUsed, 16);
const safeGasLimit = Math.ceil(gasUsed * 1.2); // 20% buffer
return {
valid: true,
gasEstimate: safeGasLimit,
output: trace.output
};
} catch (error) {
return {
valid: false,
reason: error.message,
gasEstimate: null
};
}
}
2. Smart Contract Testing​
Test contract methods without deployment:
def test_contract_method(contract_address, method_name, params, caller='0x0000000000000000000000000000000000000000'):
"""
Test a contract method without executing
"""
# Encode the method call
from eth_abi import encode_abi
from eth_utils import function_signature_to_4byte_selector
# Get method signature
method_sig = function_signature_to_4byte_selector(method_name)
# Encode parameters
encoded_params = encode_abi(params['types'], params['values'])
call_data = '0x' + method_sig.hex() + encoded_params.hex()
tx_params = {
'from': caller,
'to': contract_address,
'data': call_data,
'gas': hex(1000000)
}
trace = trace_call(tx_params)
# Analyze the result
if trace.get('error'):
return {
'success': False,
'error': trace['error'],
'gas_used': 0
}
return {
'success': True,
'output': trace.get('output'),
'gas_used': int(trace.get('gasUsed', '0x0'), 16),
'internal_calls': len(trace.get('calls', []))
}
# Example: Test a swap function
result = test_contract_method(
contract_address='0x1234...',
method_name='swap(address,address,uint256)',
params={
'types': ['address', 'address', 'uint256'],
'values': [token_a, token_b, amount]
}
)
3. Gas Optimization Analysis​
Compare gas usage of different approaches:
async function compareGasUsage(implementations) {
const results = [];
for (const impl of implementations) {
const trace = await traceCall({
from: impl.from,
to: impl.contract,
data: impl.calldata,
gas: '0xf4240' // 1M gas
}, 'latest');
results.push({
name: impl.name,
gasUsed: parseInt(trace.gasUsed, 16),
success: !trace.error,
callDepth: countCallDepth(trace),
storageAccess: countStorageOps(trace)
});
}
// Sort by gas efficiency
results.sort((a, b) => a.gasUsed - b.gasUsed);
console.log('Gas Usage Comparison:');
results.forEach((r, i) => {
console.log(`${i + 1}. ${r.name}: ${r.gasUsed.toLocaleString()} gas`);
console.log(` Call depth: ${r.callDepth}, Storage ops: ${r.storageAccess}`);
});
return results;
}
function countCallDepth(trace, depth = 0) {
if (!trace.calls || trace.calls.length === 0) return depth;
let maxDepth = depth;
for (const call of trace.calls) {
const callDepth = countCallDepth(call, depth + 1);
maxDepth = Math.max(maxDepth, callDepth);
}
return maxDepth;
}
4. Security Analysis​
Check for reentrancy and other issues:
def analyze_reentrancy_risk(contract_address, method_data):
"""
Analyze potential reentrancy vulnerabilities
"""
trace = trace_call({
'to': contract_address,
'data': method_data,
'gas': hex(5000000)
})
# Look for external calls before state changes
external_calls = []
state_changes = []
def analyze_calls(call_trace, depth=0):
if call_trace.get('type') == 'CALL':
external_calls.append({
'depth': depth,
'to': call_trace.get('to'),
'value': call_trace.get('value', '0x0')
})
if call_trace.get('type') == 'SSTORE':
state_changes.append({
'depth': depth,
'location': call_trace.get('to')
})
for subcall in call_trace.get('calls', []):
analyze_calls(subcall, depth + 1)
analyze_calls(trace)
# Check for external calls before state changes
risky_patterns = []
for ext_call in external_calls:
for state_change in state_changes:
if ext_call['depth'] < state_change['depth']:
risky_patterns.append({
'external_call': ext_call,
'state_change': state_change,
'risk': 'Potential reentrancy'
})
return {
'external_calls': len(external_calls),
'state_changes': len(state_changes),
'risky_patterns': risky_patterns
}
Error Handling​
Common errors and solutions:
Error Code | Description | Solution |
---|---|---|
-32602 | Invalid params | Check transaction object format |
-32000 | Execution reverted | Transaction would fail; check revert reason |
-32603 | Internal error | Reduce gas limit or simplify call |
3 | Execution reverted | EVM reverted the execution |
-32005 | Timeout | Call is too complex; optimize or increase timeout |
Error Handling Example​
async function safeTraceCall(params, block = 'latest') {
try {
const trace = await traceCall(params, block);
if (trace.error) {
// Parse common error patterns
if (trace.error.includes('insufficient funds')) {
throw new Error('Account has insufficient balance');
}
if (trace.error.includes('gas required exceeds allowance')) {
throw new Error('Gas limit too low');
}
if (trace.revertReason) {
throw new Error(`Reverted: ${trace.revertReason}`);
}
throw new Error(trace.error);
}
return trace;
} catch (error) {
// Handle RPC errors
if (error.code === -32602) {
throw new Error('Invalid transaction parameters');
}
if (error.code === -32000) {
throw new Error('Transaction execution failed');
}
throw error;
}
}
Best Practices​
- Always Specify Gas Limit: Prevent infinite loops
- Use Appropriate Block: Test against the latest state
- Cache Results: Traces for past blocks are immutable
- Handle Reverts Gracefully: Parse and display revert reasons
- Validate Input Data: Ensure correct encoding before tracing
Related Methods​
eth_call
- Execute calls without tracingeth_estimateGas
- Estimate gas without full tracedebug_traceTransaction
- Trace executed transactionsdebug_traceBlockByHash
- Trace entire blocks
Need help? Contact our support team for assistance with debug methods.