Interacting with Wasm Smart Contracts on Astar Network using TypeScript and Astar.js

Nikhil Ranjan
Astar Network
Published in
6 min readApr 13, 2023

WebAssembly (Wasm) is a binary instruction format designed as a portable target for the compilation of high-level languages like ink! and Ask. In the context of blockchain, Wasm smart contracts are self-executing agreements written in a language that compiles to WebAssembly, allowing them to be executed on the blockchain in a highly efficient and secure manner. Astar Network provides an alternative to the more traditional Ethereum Virtual Machine (EVM) based smart contracts by supporting Wasm contracts. This offers greater flexibility, better performance, and a broader range of supported languages.

Ink! is a Rust-based domain-specific language (DSL) designed for writing smart contracts on the Substrate and Polkadot platforms. It was developed by Parity Technologies to make it easier for developers to create secure and efficient smart contracts that compile to WebAssembly. The ink! language builds on Rust’s safety features, such as its strong type system and memory safety, to reduce the risk of vulnerabilities in smart contracts.

Using ink!, developers can create custom smart contracts tailored to their specific use cases and requirements. These contracts can then be deployed to Astar, Shiden and Shibuya Networks, enabling a wide range of decentralized applications (Dapps) and use cases.

Interacting with contract requires Weight. For ink! the term Weight refers to the resources used by a contract call. It’s important for smart contracts that the caller has to pay for any utilized resource. Those resources can be either storage space (for storing something in the contract’s storage) or computational time (for executing the contract and its logic).

The term Weight encompasses both of these resources: refTime, proofSize. The terms hereby refer to:

refTime: The amount of computational time that can be used by the contract, in picoseconds.

proofSize: The amount of storage space that can be used by the contract, in bytes.

refTime comes from “reference time”, referring to Substrate’s Weights system, where computation time is benchmarked on reference hardware. You can read more details here.

For ink!, Weight is a synonym to Gas, we just use this term to make on boarding easier for developers from other smart contract ecosystems.

Specifically these terms come from Substrate’s Weights V2 system, which was recently introduced.

When transactions are executed or data is stored on-chain, the activity changes the state of the chain and consumes blockchain resources. Because the resources available to a blockchain are limited, it’s important to manage how operations on-chain consume them.

In addition to being limited in practical terms — such as storage capacity — blockchain resources represent a potential attack vector for malicious users. For example, a malicious user might attempt to overload the network with messages to stop the network from producing new blocks. To protect blockchain resources from being drained or overloaded, you need to manage how they are made available and how they are consumed.

The resources to be aware of include:

  • Memory usage
  • Storage input and output
  • Computation
  • Transaction and block size
  • State database size

To interact with the smart contract from your TypeScript/JavaScript application you can use the Astar.js library. Astar.js is a library that provides a set of tools for interacting with Astar Network. It is a wrapper around the Polkadot.js API.

In this article, we will explore how to interact with Wasm smart contracts on the Astar Network using TypeScript and the Astar.js library. We will cover the following steps:

  1. Connect to Astar Network RPC
  2. Get the contract instance
  3. Get the initial gas limits from system block weights
  4. Dry run the contract transaction to estimate gas and storage deposit
  5. Send the transaction to the network

Prerequisites

Ensure that you have already created your ink! smart contract using Swanky and generated the contract ABI JSON file.

Step 1: Connect to Astar Network RPC

To connect to the Astar Network RPC, you need to import ApiPromise and WsProvider classes. ApiPromise instance represents connection to the blockchain rpc node which can be used to query data and send transactions to the blockchain. You can use the following TypeScript code:

import { ApiPromise } from '@polkadot/api';
import { WsProvider } from '@polkadot/rpc-provider';
import { options } from '@astar-network/astar-api';

async function main() {
const provider = new WsProvider('wss://shiden.api.onfinality.io/public-ws');
const api = new ApiPromise(options({ provider }));
await api.isReady;
// Use the api. For example:
console.log((await api.rpc.system.properties()).toHuman());
process.exit(0);
}

main()

Step 2: Get the Contract Instance

You have to get the ContractPromise class instanace to interact with the smart contract and call the methods on the contract from your dapp. You will need ABI json generated during contract compilation. You can use the following TypeScript code to connect:

import { Abi, ContractPromise } from '@polkadot/api-contract'
// Import the ABI JSON file generated after compiling the contract
import ABI from './artifacts/lottery.json'

const abi = new Abi(ABI, api.registry.getChainProperties())
// Initialise the contract class
const contract = new ContractPromise(api, abi, address)

Step 3: Get the Initial Gas Limits from System Block Weights

To get the initial gas limits from the system block weights, you can use the following TypeScript code:

const gasLimit =
api.registry.createType(
'WeightV2',
api.consts.system.blockWeights['maxBlock']
)

api.consts.system.blockWeights[‘maxBlock’] is good enough as initial gas.

Step 4: Dry Run the Contract Transaction to Estimate Gas and Storage Deposit

The following TypeScript code demonstrates how to dry run a contract transaction and estimate gas and storage deposit:

const { gasRequired, storageDeposit, result } = await contract.query.[contract transaction method](
account.address,
{
gasLimit: gasLimit,
value: new BN('1000000000000000000') // Value sent to method. In this example 1 ASTAR
}
)

// Error handling
if (result.isErr) {
let error = ''
if (result.asErr.isModule) {
const dispatchError = api.registry.findMetaError(result.asErr.asModule)
console.log('error', dispatchError.name)
error = dispatchError.docs.length ? dispatchError.docs.concat().toString() : dispatchError.name
} else {
error = result.asErr.toString()
}

console.log(error)
return
}

// result might be ok but if flags include Revert
// then method will revert as assertions are failing in contract
// so this case is also an error
if (result.isOk) {
const flags = result.asOk.flags.toHuman()
if (flags.includes('Revert')) {
console.log('Revert')
console.log(result.toHuman())
const type = contract.abi.messages[index of contract method in abi].returnType
const typeName = type?.lookupName || type?.type || ''
const error = contract.abi.registry.createTypeUnsafe(typeName, [result.asOk.data]).toHuman()

console.log(error)
return
}
}

After a dry run we can use values returned by the query within transactions. Usage of reftime and proofSize are fairly straightforward but storageDeposit is a bit tricky. It is a fee charged for increasing the contract state size. If the contract state size is decreased, then storageDeposit is refunded, so we have to check if storageDeposit.isCharge is true or false. If true, then we have to add storageDepositLimit to storageDeposit.asCharge otherwise null.

We can obtain an exact error from the Wasm contract with some extra code. We have to perform a dry run of the contract with the same parameters and check result.Transaction, which will revert if result.isErr is true. However, the transaction will also revert if result.isOk is true and flags include the revert string. To obtain the error we need to use api.registry.findMetaError(result.asErr.asModule). If docs are set in the contract code, the error string can be retreived from dispatchError.

Step 5. Send the transaction to the network

Send the transaction to the node using gasRequired and storageDeposit values returned in the dry run

await contract.tx
.[contract method]({
// gasRequired is returned in the dry run step
gasLimit: gasRequired,
// in case storage deposit is charged for the transaction set this as storageDeposit
storageDepositLimit: storageDeposit.isCharge ? storageDeposit.asCharge.toString() : null,
value: new BN('1000000000000000000')
})
.signAndSend(account.address, (res) => {
if (res.status.isInBlock) {
console.log('in a block')
}
if (res.status.isFinalized) {
console.log('finalized')
}
})

This concludes the steps to interact with a Wasm smart contract using Astar.js. Here is a tech talk showing a live demo for this:

References

--

--