Developer Fundamentals

-- IV. States and Views

Learn how to retrieve data from smart contracts on VeChainThor, use ABIs, simulate transactions, and simplify function calls using the contract loader.

Interacting with Smart Contracts

Smart contracts can expose variables and functions for sharing their stored data publicly. To communicate with a contract, it is essential to have the interface definition, which can be retrieved from either a JSON file or the function definition in the source code.

Example Data

The example used below will utilize the VTHO contract, which manages VeChain's VTHO Token.

Smart Contract Address:

0x0000000000000000000000000000456e65726779

The contract's source code can be found on GitHub.

Its Application Binary Interface (ABI) is available on b32, a repository that gathers publicly available interfaces for VeChain projects. You can either:

a. Hardcode the ABI into the script
b. Import the ABI from a local energyAbi.json file (get the JSON file from the public repository)
c. Fetch the public ABI from the repository

Retrieving information from a smart contract involves calling a function, which can access variables, view functions, and even functions that alter the state for simulation purposes.

In this example, we'll fetch the ABI from the public repository.

To call a function within a contract, use contracts.executeCall, which requires the following parameters:

  • The contract's address as the first argument

  • The function ABI as the second argument

  • The function data as the third argument

A fourth parameter is optional and allows the user to provide the options for executing a contract call within a blockchain environment.

For example, if you want to access a basic variable name, such as the name of the VTHO contract, you can utilize the code snippet below:

import { ThorClient } from '@vechain/sdk-network';
const thor = ThorClient.at('https://mainnet.vechain.org');

// Import public ABI
const publicabi = await fetch('https://raw.githubusercontent.com/vechain/b32/master/ABIs/energy.json');
const energyAbi = await publicabi.json();

const vtho = thor.contracts.load(
  '0x0000000000000000000000000000456e65726779',
  energyAbi
);

// read name of contract
const name = await vtho.read.name();
console.log('Name', name);

With Parameters

When calling a function with parameters, the parameters should be passed as a list in the third argument. For instance, to check the balance of a specific address:

// read balance of an address
const balanceNow = await vtho.read.balanceOf(
  '0x0000000000000000000000000000000000000000'
);
console.log('Balance Now', balanceNow);

To retrieve data from a previous block, you can specify the block number or id by passing in a revision option:

vtho.setContractReadOptions({ revision: 12345678 });
const balancePast = await vtho.read.balanceOf(
  '0x0000000000000000000000000000000000000000'
);
console.log('Balance Past', balancePast);
vtho.setContractReadOptions({ revision: null });

Simulate Transaction

If a function could change the state, it would require a transaction. To check the success of a transaction, you can invoke the function first and examine the output or handle any potential errors.

For example, simulating a transfer that returns true if the caller has at least 1 VTHO:

const transfer = await vtho.read.transfer(
  '0x0000000000000000000000000000456e65726779',
  '1'
);
console.log('Transfer Test', transfer);

If the transaction encounters an error, the method call will also throw an error, which needs to be handled appropriately.

Test It Yourself

The snippet below shows how you can make a request to the blockchain and the name of a specific contract:

import { ThorClient } from '@vechain/sdk-network';

const thor = ThorClient.at('https://mainnet.vechain.org');

const publicabi = await fetch('https://raw.githubusercontent.com/vechain/b32/master/ABIs/energy.json');
const energyAbi = await publicabi.json();

const vtho = thor.contracts.load(
  '0x0000000000000000000000000000456e65726779',
  energyAbi
);

// read name of contract
const name = await vtho.read.name();
console.log('Name', name);

// read balance of an address
const balanceNow = await vtho.read.balanceOf(
  '0x0000000000000000000000000000000000000000'
);
console.log('Balance Now', balanceNow);

// simulate a transfer
const transfer = await vtho.read.transfer(
  '0x0000000000000000000000000000456e65726779',
  '1'
);
console.log('Transfer Test', transfer);

Challenge Your Skills

Try the snippet above on your own to read historical data, simulate a transaction using the examples given earlier in this lesson.

contracts.load(address, abi)

To simplify interaction, a dynamic object can be created that can interact with passing less of the repeating arguments.

For example, contracts.read.name() can load the name without the need to pass the function signature, address, and thorClient every time.

Create Contract Object

To create a contract object, it needs to be created from the ThorClient:

const thor = ThorClient.at('https://mainnet.vechain.org');

// contract ABI is same as in example above
const vtho = thor.contracts.load(
    '0x0000000000000000000000000000456e65726779',
    contractABI
);

The Contract-Loader always requires a JSON ABI Definition. Fragments are not supported.

'Read' Functions

Function calls are encapsulated within a sub-object named 'read'. This enables calling the contract for variable content, viewing functions, or performing simple transaction simulations.

// read balance of an address
vtho.setContractReadOptions({ revision: "12345678" });
const balancePast = await vtho.read.balanceOf(
  '0x0000000000000000000000000000000000000000'
);
console.log('Balance Past', balancePast);

Test It Yourself

The snippet below puts this lesson together and illustrates how you can make an elaborate request to the blockchain and receive data directly from a contract.

This is a full request that includes:

  • Read the name of the contract

  • Read the balance of a specific address

  • Read the past balance of that address

  • Simulate a transfer

  • Simulate the failure of a transfer

import { ThorClient } from '@vechain/sdk-network';
const thor = ThorClient.at('https://mainnet.vechain.org');

const publicabi = await fetch('https://raw.githubusercontent.com/vechain/b32/master/ABIs/energy.json');
const energyAbi = await publicabi.json();

const vtho = thor.contracts.load(
  '0x0000000000000000000000000000456e65726779',
  energyAbi
);

// read name of contract
const name = await vtho.read.name();
console.log('Name', name);

// read balance of an address
const balanceNow = await vtho.read.balanceOf(
  '0x0000000000000000000000000000000000000000'
);
console.log('Balance Now', balanceNow);

// read balance of an address in the past
vtho.setContractReadOptions({ revision: 12345678 });
const balancePast = await vtho.read.balanceOf(
  '0x0000000000000000000000000000000000000000'
);
console.log('Balance Past', balancePast);
vtho.setContractReadOptions({ revision: null });

// simulate a transfer
const transfer = await vtho.read.transfer(
  '0x0000000000000000000000000000456e65726779',
  '1'
);
console.log('Transfer Test', transfer);

// failing simulation of transfer
try {
  vtho.setContractReadOptions({
    caller: '0x0000000000000000000000000000000000000003',
  });
  const failingTransfer = await vtho.read.transfer(
    '0x0000000000000000000000000000456e65726779',
    '1'
  );
  console.log('Failed Transfer Test', failingTransfer);
} catch (err) {
  const errorDecoder = ErrorDecoder.create();
  const decodedError = await errorDecoder.decode(err);
  console.log(`Revert reason: ${decodedError.reason}`);
}