How to Inspect Smart Contract Transactions on Hedera Using Mirror Nodes
Headshot
Jan 22, 2023
by Ed Marquez
Developer Relations Engineer
Screen Shot 2022 01 27 at 9 52 08 PM
by Francesco Coacci
Developer Evangelist

The goal of this tutorial is to help smart contract developers understand the traceability information for contract transactions that is provided by the mirror nodes. Specifically, you will learn how to view contract actions, state changes, and logs. Understanding this information and knowing where to get it can simplify the process of inspecting and debugging smart contracts on Hedera.

For detailed information about the development and implementation of this traceability information, check out HIP-513: Smart Contract Traceability Extension and HIP-435: Record Stream V6.

Try It Yourself

You Will Use These Tools

Let’s Inspect an Example: The Client Calls a Contract that Calls Another Contract

There are three entities in this scenario: Operator and two smart contracts.

Your testnet credentials from the Hedera portal should be used for the operator variables, which are used to initialize the Hedera client that submits transactions to the network and gets confirmations.

  • The code in index.js initializes a Hedera client (Operator). It also deploys and executes the two contracts
  • The contract Counter.sol has a function named increment() that increases the state variable count and emits an event when called
  • The contract CallCounter.sol has a state variable that stores the address of the counter contract (counterAddr). It also has the functions, counterIncrement() and getCount(). These functions use an interface (ICounter.sol) to call increment() and read the value of count in Counter.sol

The expected behavior with this setup is that Operator executes the counterIncrement() function in CallCounter.sol, which in turn executes the increment() function in Counter.sol, which in turn increases the state variable count and emits an event. You will then view information related to all those activities from the mirror nodes.

A for loop is used to make this chain of calls occur three times every time index.js executes.

index.js
  • index.js
  • Counter.sol
  • CallCounter.sol
Code Snippet Background
console.clear();
import { Client, AccountId, PrivateKey, Hbar, ContractFunctionParameters } from "@hashgraph/sdk";

import * as queries from "./utils/queries.js";
import * as contracts from "./utils/contractOperations.js";
import counterContract from "./contracts/Counter.json" assert { type: "json" };
import counterCallerContract from "./contracts/CounterCaller.json" assert { type: "json" };

import dotenv from "dotenv";
dotenv.config();

const operatorId = AccountId.fromString(process.env.OPERATOR_ID);
const operatorKey = PrivateKey.fromString(process.env.OPERATOR_PVKEY);
const network = process.env.HEDERA_NETWORK;

const client = Client.forNetwork(network).setOperator(operatorId, operatorKey);
client.setDefaultMaxTransactionFee(new Hbar(1000));
client.setMaxQueryPayment(new Hbar(50));

async function main() {
	// STEP 1 ===================================
	console.log(`\nSTEP 1 ===================================\n`);
	console.log(`- Deploying contracts...\n`);

	// Deploy the called contract (counter)
	let gasLim = 8000000;
	const bytecode = counterContract.object;
	const params = [];
	const [calledContractId, calledContractAddress] = await contracts.deployContractFcn(bytecode, params, gasLim, client);
	console.log(`- Contract ID: ${calledContractId}`);
	console.log(`- Contract ID in Solidity address format: ${calledContractAddress}`);

	// Deploy the caller contract (counter caller)
	const bytecode1 = counterCallerContract.object;
	const params1 = new ContractFunctionParameters().addAddress(calledContractAddress);
	const [callerContractId, callerContractAddress] = await contracts.deployContractFcn(bytecode1, params1, gasLim, client);
	console.log(`- Contract ID: ${callerContractId}`);
	console.log(`- Contract ID in Solidity address format: ${callerContractAddress}`);

	// STEP 2 ===================================
	console.log(`\nSTEP 2 ===================================\n`);
	console.log(`- Executing the caller contract...\n`);

	let idx = 0;
	const runs = 3;
	const incrementRec = [];
	for (idx; idx < runs; idx++) {
		// Execute the caller contract
		incrementRec[idx] = await contracts.executeContractFcn(callerContractId, "counterIncrement", gasLim, client);
		console.log(`- Contract execution: ${incrementRec[idx].receipt.status} \n`);
	}

	// Check a Mirror Node Explorer for the last contract execution
	const [incrementInfo, incrementExpUrl] = await queries.mirrorTxQueryFcn(incrementRec[runs - 1], network);
	console.log(`- See details in mirror node explorer: \n${incrementExpUrl}`);

	console.log(`
====================================================
 THE END - NOW JOIN: https://hedera.com/discord
====================================================\n`);
}
main();

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;


contract Counter {
    uint public count;
    
    event CountIncrement(address indexed _from, uint count);

    function increment() external {
        count += 1;
        emit CountIncrement(msg.sender, count);
    }
}

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "./ICounter.sol";

contract CallCounter {
    address counterAddr;

    constructor (address _counter) {
       counterAddr = _counter;
    }

    function counterIncrement() external {
        return ICounter(counterAddr).increment();
    }

    function getCount() external view returns (uint) {
        return ICounter(counterAddr).count();
    }

}

This is the console output of index.js:

STEP 1 ===================================
- Deploying contracts...

- Contract ID: 0.0.2505
- Contract ID in Solidity address format: 00000000000000000000000000000000000009c9
- Contract ID: 0.0.2507
- Contract ID in Solidity address format: 00000000000000000000000000000000000009cb

STEP 2 ===================================
- Executing the caller contract...
- Contract execution: SUCCESS
- Contract execution: SUCCESS
- Contract execution: SUCCESS

- See details in mirror node explorer:
https://hashscan.io/previewnet/transaction/1674246551.782213192?tid=0.0.1132-1674246540-672254772

====================================================
🎉🎉 THE END - NOW JOIN: https://hedera.com/discord 
====================================================


Each tab below shows the helper functions used to deploy and execute the contracts (contracts.deployContractFcn and contracts.executeContractFcn) and to query the information about the 3rd transaction from the mirror nodes (queries.mirrorTxQueryFcn). These functions are reusable in case you need to perform those activities in the future.

DeployContractFcn
  • DeployContractFcn
  • ExecuteContractFcn
  • MirrorTxQueryFcn
Code Snippet Background
export async function deployContractFcn(bytecode, params, gasLim, client) {
	const contractCreateTx = new ContractCreateFlow().setBytecode(bytecode).setConstructorParameters(params).setGas(gasLim);
	const contractCreateSubmit = await contractCreateTx.execute(client);
	const contractCreateRx = await contractCreateSubmit.getReceipt(client);
	const contractId = contractCreateRx.contractId;
	const contractAddress = contractId.toSolidityAddress();
	return [contractId, contractAddress];
}

export async function executeContractFcn(cId, fcnName, gasLim, client) {
	const contractExecuteTx = new ContractExecuteTransaction().setContractId(cId).setGas(gasLim).setFunction(fcnName);
	const contractExecuteSubmit = await contractExecuteTx.execute(client);
	const contractExecuteRec = await contractExecuteSubmit.getRecord(client);
	return contractExecuteRec;
}
export async function mirrorTxQueryFcn(txRec, network) {
	// Query a mirror node for information about the transaction
	const delay = (ms) => new Promise((res) => setTimeout(res, ms));
	await delay(10000); // Wait for 10 seconds before querying a mirror node to allow for info propagation

	const txTimestamp = txRec.consensusTimestamp;
	const txIdRaw = txRec.transactionId;
	const txIdPretty = prettify(txIdRaw.toString());
	const mirrorNodeExplorerUrl = `https://hashscan.io/${network}/transaction/${txTimestamp}?tid=${txIdPretty}`;
	const mirrorNodeRestApi = `https://${network}.mirrornode.hedera.com/api/v1/transactions/${txIdPretty}`;
	let mQuery = [];
	try {
		mQuery = await axios.get(mirrorNodeRestApi);
	} catch {}
	return [mQuery, mirrorNodeExplorerUrl];
}

function prettify(txIdRaw) {
	const a = txIdRaw.split("@");
	const b = a[1].split(".");
	return `${a[0]}-${b[0]}-${b[1]}`;
}

After deployment and execution, we can see information for all these entities and transactions in HashScan. Keep in mind that previewnet and testnet are periodically reset for maintenance purposes, so you may not see these same exact entries in the future – screenshots are included for reference.

Contract Traceability Image 1

Contract Results

The contract results section provides summary information of whether the transaction completed successfully or not, the entities involved, gas values, and more.

Contract Results in HashScan:

Contract Traceability Image 2

Mirror Node REST API for Results:

https://previewnet.mirrornode.hedera.com/api/v1/contracts/0.0.2507/results

Contract Actions

The Actions section (Call Trace in HashScan) highlights all the interactions from entity to entity (account calls contract, contract calls contract, etc.) This can help you understand the sequence of calls and operations in your application. In HashScan, you can expand all the calls involved in a transaction to see details like entity IDs, gas values, HBAR transferred, and more.

In our example, you can see that the account 0.0.1132 calls the contract 0.0.2507 (CallCounter.sol), which then calls the contract 0.0.2505 (Counter.sol)

Contract Actions in HashScan:

Contract Traceability Image 3

Mirror Node REST API for Actions: https://previewnet.mirrornode.hedera.com/api/v1/contracts/results/0xb739e3c90b8cf9dcc823aff3175bd61efa2167c7072c1103cde0aad3f2295815/actions

State Changes

State changes can also be inspected with mirror nodes. In HashScan, you can see the values read and written to state for a given address.

In our example, for the third contract execution, you see that the value of 3 is written to the state of contract 0.0.2505 (Counter.sol) and the contract 0.0.2507 reads the address of the counter contract.

Contract State in HashScan:

Contract Traceability Image 4

Mirror Node REST API for State:

https://previewnet.mirrornode.hedera.com/api/v1/contracts/0.0.2505/state

https://previewnet.mirrornode.hedera.com/api/v1/contracts/0.0.2507/state

Logs

Mirror nodes also provide information about events, which is captured in the logs section.

As a refresher, when an event is emitted, it stores the arguments in a special on-chain data structure called the transaction log. Logs are composed of topics and data.

For a detailed explanation of how to access event information from smart contracts, be sure to read the tutorial How to Get Event Information from Hedera Smart Contracts.

Contract Logs on HashScan:

Contract Traceability Image 5

Mirror Node REST API for Logs:

https://previewnet.mirrornode.hedera.com/api/v1/contracts/0.0.2505/results/logs

Summary

Now you know how to inspect smart contract transactions using the traceability information provided by the mirror nodes. You can try this example with the other officially supported SDKs for Java, Go, and Swift.

Continue Learning