LogoLogo
  • WELCOME TO ALEPH ZERO
  • EXPLORE
    • About Aleph Zero
    • AlephBFT Consensus
    • The Economy of Aleph Zero
    • Where to Buy AZERO
    • Decentralized Governance on Aleph Zero
    • Ecosystem
    • Aleph Zero Foundation Treasury Management
    • Community
    • Glossary
    • Audit & Research Papers
  • USE
    • Wallets
    • Explorer
    • Ledger
    • Telegram Notifications
    • Aleph Zero Signer
      • General introduction
      • What does Signer do?
      • What are Sub-accounts and Sub-account paths?
      • Why is it critical to store your Secret Phrase in a safe place?
      • How to forget and restore accounts?
      • What are Networks?
      • What are Trusted apps?
    • Dashboard
      • Dashboard basics
      • Overview
    • Stake
      • Staking Basics
      • Staking Menu Overview
      • How to Start Staking with the Aleph Zero Dashboard
      • How to Start Staking With the Developer Wallet
      • How to start staking using Ledger hardware wallet
      • How to Change Nominations
      • How to Stop Staking
      • Staking Rewards
      • Validators
      • Commission and Foundation Nodes
      • Proxy Accounts
    • Validate
      • Validating Overview
      • Hardware requirements
      • Running an Aleph Node on Testnet
        • Downloading and running the node
        • Verifying your setup
        • Customizing your setup
        • Building and running from source [advanced]
          • Building from source
          • Set environment variables
          • Download DB snapshot
          • Running the binary
        • Appendix: Ports, addresses, validators, and archivists
      • Running an Aleph Node on Mainnet
        • Running the node
        • Building and running from source [advanced]
      • Setting your identity
      • Making the node validate
      • Securing your validator
      • Troubleshooting
      • Elections and Rewards Math
      • Testnet Validator Airdrop
      • Foundation Nomination Program
    • Using the EVM-layer
    • Governance
      • Token
      • Multisig Accounts
  • BUILD
    • Aleph Zero smart contracts basics
      • Setting up a Testnet account
      • Installing required tools
      • Creating your first contract
      • Deploying your contract to Aleph Zero Testnet
      • Extending your contract
    • Cross contract calls
      • Using references
      • Using dynamic calls
    • Migrating from Solidity
    • Writing e2e tests with ink-wrapper
    • Aleph Zero Signer integration
    • Front-end app: smart contract interaction
    • Security Course by Kudelski Security
      • ink! Developers Security Guideline
      • Lesson 1 - Getting started with ink!
      • Lesson 2 - Threat Assessment
      • Lesson 3 - Integer Overflow
      • Lesson 4 - Signed-integer
      • Lesson 5 - Role-Based Access Control
      • Lesson 6 - Address Validation
      • Lesson 7 - Smart Contract Control
    • Development on EVM-layer
  • PROTOCOL DETAILS
    • Shielder
      • Overview
      • Design against Bad Actors
      • Preliminaries - ZK-relations
      • Notes and Accounts
      • ZK-ID and Registrars
      • Anonymity Revokers
      • PoW Anonymity Revoking
      • Relayers
      • Deterministic Secret Management
      • SNARK-friendly Symmetric Encryption
      • SNARK-friendly Asymmetric Encryption
      • Cryptography
      • Token shortlist
      • User Wallet
      • Versioning
      • PoC
      • Version 0.1.0
      • Version 0.2.0
    • Common DEX
      • Common Whitepaper - Differences
      • Dutch Auctions
  • FAQ
  • Tutorials
    • Withdrawing coins from exchanges
      • How to withdraw your AZERO coins from KuCoin
      • How to withdraw your AZERO coins from MEXC Global
      • How to withdraw your AZERO coins from HTX
  • Setting up or restoring a wallet
    • How to set up or recover your AZERO account using Aleph Zero Signer
    • How to set up or recover your AZERO account using the official mainnet web wallet
    • How to set up or recover your AZERO account using Nova Wallet
    • How to set up or recover your AZERO account using SubWallet
    • How to set up or recover your AZERO account using Talisman
  • Staking
    • How to stake via a direct nomination using the Aleph Zero Dashboard
    • How to stake via a nomination pool using the Aleph Zero Dashboard
    • How to destroy a nomination pool via the Aleph Zero Dashboard
Powered by GitBook
On this page
  • Intro
  • Connect to a deployed smart contract
  • Query a contract state
  • Send signed transaction to a contract
  • Signing account
  • Gas estimation
  • Sign and send the transaction
  • Closing remarks

Was this helpful?

  1. BUILD

Front-end app: smart contract interaction

Learn how to query a contract's state and send signed transactions in your front-end application.

PreviousAleph Zero Signer integrationNextSecurity Course by Kudelski Security

Last updated 1 year ago

Was this helpful?

Intro

In this tutorial, you will learn how to make the front-end of your App interact with smart contracts using , more specifically:

  • how to read values stored in your smart contract

  • how to send signed transactions to your smart contract

Before we start, you may want to check out our repository. It is a simple yet comprehensive tutorial dApp that can be used to learn more about writing smart contracts in ink! and how to build your first dApp on the Aleph Zero ecosystem, or simply bootstrap your project. All the code snippets in this tutorial come from this repository.

It is also worth checking out the tutorial: it could be a good starting point for you.

For beginners: blocks like this contain some explanation about basic concepts. If you're an experienced developer, please feel free to skip them.

Connect to a deployed smart contract

For beginners: when interacting with the blockchain, you actually connect to a single node running as part of the network. The states of the nodes are constantly being synchronized by the protocol, so it doesn't matter which node you connect to.

If you're using the public Aleph Zero endpoints described below, you connect to a single endpoint that serves as an umbrella for a few nodes run by the Aleph Zero Foundation: it will automatically choose the best endpoint.

The first step will be to create an API instance to connect to a running node. For that, we need a provider: the default instance of WsProvider connects to "ws://127.0.0.1:9944" which usually is your local node's endpoint. For Aleph Zero Testnet use "wss://ws.test.azero.dev" and for Aleph Zero Mainnet: "wss://ws.azero.dev".

import { ApiPromise, WsProvider } from '@polkadot/api';
...

const APP_PROVIDER_URL = "ws://127.0.0.1:9944";

const wsProvider = new WsProvider(APP_PROVIDER_URL);
const api = await ApiPromise.create({ provider: wsProvider });

Importantly, the terms ABI and metadata are used interchangeably in this context.

import { ContractPromise } from '@polkadot/api-contract';
...
import bulletinBoardMetadata from '../metadata/metadata_bulletin_board.json';
import addresses from '../metadata/addresses.json';

const bulletin_board_address = addresses.bulletin_board_address;

const contract = new ContractPromise(
    api,
    bulletinBoardMetadata,
    bulletin_board_address
  );

Query a contract state

Under the hood, querying a contract is an extrinsic dry run. It is not submitted on the chain, however, it requires to specify the gasLimit which refers to the maximum resources used by a contract call. Because we are not submitting an extrinsic to the chain (we're only performing a dry-run), it is safe to use sufficiently high values to ensure that the call won’t be reverted. For contract calls, Substrate uses a 2-dimensional weight (gas) system which consists of:

  • refTime: the amount of computational time used for execution, in picoseconds;

  • proofSize: the amount of storage in bytes that a transaction is allowed to read.

import { BN, BN_ONE } from '@polkadot/util';
import type { WeightV2 } from '@polkadot/types/interfaces';
...
const MAX_CALL_WEIGHT = new BN(5_000_000_000_000).isub(BN_ONE);

const readOnlyGasLimit = api.registry.createType('WeightV2', {
    refTime: new BN(1_000_000_000_000),
    proofSize: MAX_CALL_WEIGHT,
  }) as WeightV2;

In this example, we call the contract method getByAccount which takes one argument accountId. For simplicity, we are using the contract address as the caller.

...
const {
  gasConsumed,
  gasRequired,
  storageDeposit,
  result,
  output,
  debugMessage,
} = await contract.query.getByAccount(
  contract.address, // caller address
  {
    gasLimit: readOnlyGasLimit,
  },
  accountId
);
if (result.isOk && output) {
  console.log(output.toHuman());
}
if (result.isErr) {
  console.log(result.toHuman());
}

Send signed transaction to a contract

For beginners: we can assume that, in the context of smart contracts, a signed transaction will be needed whenever we want to call a method that modifies the smart contract's state. These will be the methods that use &mut self as the first argument.

Signing account

import {
  web3Enable,
  web3Accounts,
} from "@polkadot/extension-dapp";

const APP_NAME = 'Aleph Zero The Bulletin Board';
const injectedExtensions = await web3Enable(APP_NAME);

// Return list of accounts imported from extensions
const accounts: InjectedAccountWithMeta[] = await web3Accounts();
// For simplicity, here we assume that there's at least one account in the connected wallet extensions 
const userAccount = accounts[0];

Gas estimation

In practice, when estimating gasLimit using gasRequired you may want to add some margin to ensure the transaction won't be reverted due to insufficient gasLimit.

For beginners: the term message below refers to the methods of the smart contract marked with the #[ink(message)] macro: these will be the contract's methods that are callable from outside of the contract. In most cases, the terms [contract] message and method will be used interchangeably.

...
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

const toContractAbiMessage = (
  contractPromise: ContractPromise,
  message: string
): Result<AbiMessage, string> => {
  const value = contractPromise.abi.messages.find((m) => m.method === message);

  if (!value) {
    const messages = contractPromise?.abi.messages
      .map((m) => m.method)
      .join(", ");

    const error = `"${message}" not found in metadata.spec.messages: [${messages}]`;
    console.error(error);

    return { ok: false, error };
  }

  return { ok: true, value };
};

The getGasLimit(...) function below estimates the gasRequired for this contract call which should be sufficient to cover the gas fees when submitting the actual extrinsic. There are also other contract call options which can be adjusted:

  • storageDepositLimit - The maximum amount of balance that can be charged/reserved for the storage consumed.

  • value - The balance (in native currency, AZERO in our case) to transfer from the caller to the contract. Non-zero values here apply only to methods marked with the #[ink(message, payable)] macro.

import type { Weight, ContractExecResult } from "@polkadot/types/interfaces";
import { AbiMessage, ContractOptions } from "@polkadot/api-contract/types";
...
const getGasLimit = async (
  api: ApiPromise,
  userAddress: string,
  message: string,
  contract: ContractPromise,
  options = {} as ContractOptions,
  args = [] as unknown[]
  // temporarily type is Weight instead of WeightV2 until polkadot-js type `ContractExecResult` will be changed to WeightV2
): Promise<Result<Weight, string>> => {
  const abiMessage = toContractAbiMessage(contract, message);
  if (!abiMessage.ok) return abiMessage;

  const { value, gasLimit, storageDepositLimit } = options;

  const { gasConsumed, gasRequired, storageDeposit, debugMessage, result } =
    await api.call.contractsApi.call<ContractExecResult>(
      userAddress,
      contract.address,
      value ?? new BN(0),
      gasLimit ?? null,
      storageDepositLimit ?? null,
      abiMessage.value.toU8a(args)
    );
 
  return { ok: true, value: gasRequired };
};

Sign and send the transaction

Function sendPost(...) puts all the pieces together. It calls the payable method on the contract post(expiresAfter: u32, postText: String) and handles the result.

...
const sendPost = async (
  expiresAfter: number,
  postText: string,
  totalPrice: number,
  api: ApiPromise,
  userAccount: InjectedAccountWithMeta
): Promise<void> => {
  if (!userAccount.meta.source) return;

  const contract = new ContractPromise(
    api,
    bulletinBoardMetadata,
    addresses.bulletin_board_address
  );

  const injector = await web3FromSource(userAccount.meta.source);

  const options: ContractOptions = {
    value: totalPrice,
  };
  const gasLimitResult = await getGasLimit(
    api,
    userAccount.address,
    "post",
    contract,
    options,
    [expiresAfter, postText]
  );

  if (!gasLimitResult.ok) {
    console.log(gasLimitResult.error);
    return;
  }

  const { value: gasLimit } = gasLimitResult;

  const tx = contract.tx.post(
    {
      value: totalPrice, // amount of native token to be transferred
      gasLimit,
    },
    expiresAfter,
    postText
  );

  await tx
    .signAndSend(
      userAccount.address,
      { signer: injector.signer },
      ({ events = [], status }) => {
        events.forEach(({ event }) => {
          const { method } = event;
          if (method === "ExtrinsicSuccess" && status.type === "InBlock") {
            console.log("Success!");
          } else if (method === "ExtrinsicFailed") {
            console.log(`An error occured: ${method}.`);
          }
        });
      }
    )
    .catch((error) => {
      console.log(`An error occured: ${error}.`);
    });
};

Closing remarks

We hope that this tutorial helped you learn more about how to interact with smart contracts in the front-end application. You shouldn't stop here! Check out other resources that may help you extend your dApp:

The @polkadot/api-contract comes with 4 general helpers (see the ). Here, we are using ContractPromise which allows us to interact with already deployed contracts.

For beginners: the contract's ABI (Application Binary Interface) is a JSON file describing, among other things, what methods are available, what selectors they have, what are the parameters and return types and so on. It also contains the docstrings you put in your code that will be displayed by the if you choose to use it.

To create the contract-api instance, we will need the contract's address and ABI. The contract ABI can be found in the artifacts generated when building the contract (see ).

For beginners: for the purpose of this tutorial, we can assume that an extrinsic is the same as a transaction that you send to the blockchain. If you want to read more, please refer to the .

The concept of gas (fees for executing transactions) is approached slightly differently to what you may be used to from EVM-based chains: gas is a two-dimensional value and there's no concept of gas price (it is calculated based only on how much resources are used + an optional tip). To learn more, please refer to the .

Contract query results in . If the query is successful, you can extract the return value from the output object. See how it can be done .

To sign a transaction, we need a wallet. To keep this example simple, we will use to retrieve wallet providers added to the app page and assume that there’s at least one account. See the tutorial for more information.

To sign and send a transaction to a contract, we should estimate values for gasLimit. Although the unused gas is refunded after the call, it is good practice to specify a reasonable gasLimit for transactions. As shown , this (gasRequired) can be estimated with contract.query.[method]. Here is an alternative way of how to do it using api.call.contractsApi.call<ContractExecResult>.

The first step is to get for the contract method. The function below searches for the method in the contract ABI and returns it if found.

To learn more about how to handle transaction events see . Also, see how it is done in the .

When running , the blocks are never finalized since it does not use a finality mechanism by default. On a live network such as Aleph Zero Testnet, you should expect a ‘Finalized’ transaction status soon after the ‘InBlock’ status.

If you want to run a local chain that will work exactly as the Testnet, you can use script, which will bootstrap a small chain locally.

: A permissionless, open-source solution that serves as both a wallet adapter and a bridge wallet, enabling connections through QR codes or deep links.

: Generate Typescript wrappers around your smart contract!

: A full-stack dApp boilerplate for ink! smart contracts with an integrated front-end.

@polkadot/api-contract
Bulletin Board Example
Aleph Zero Signer Integration
docs
Contracts UI
Substrate documentation on extrinsics
Substrate documentation of transaction fees
ContractCallOutcome
here
@polkadot/extension-dapp
Aleph Zero Signer Integration
AbiMessage
Transaction Subscriptions
Bulletin Board Example
substrate-contract-node
the ./scripts/run_nodes.sh
Nightly-connect
Typechain
ink!athon
here
Creating your first contract