Skip to content

Ink!

Polkadot-API adds typescript definitions for ink! contracts, as well as utilities to encode and decode messages, contract storage and events.

The ink client with type support can be found at polkadot-api/ink. It's chain-agnostic, meaning it doesn't dictate which runtime APIs, storage or transactions are required. For a more integrated ink! support for specific chains, a Polkadot-API SDK for ink! contracts is currently in development.

Codegen

The first step is to generate the types from the contract metadata. The Polkadot-API CLI has a command specific for ink:

> pnpm papi ink --help
Usage: polkadot-api ink [options] [command]
 
Add, update or remove ink contracts
 
Options:
  -h, --help              display help for command
 
Commands:
  add [options] <file>    Add or update an ink contract
  remove [options] <key>  Remove an ink contract
  help [command]          display help for command

So to generate the types for a contract, run the ink add command:

> pnpm papi ink add "path to .contract or .json metadata file"

This will add the contract to the .papi subfolder, and generate the type descriptors for the ink! contract. These can be found in @polkadot-api/descriptors within an object named "contracts".

The generated code contains all the types, and also the required info from the metadata to encode and decode values.

Ink! Client

Start by creating the ink client from polkadot-api/ink. In the following example we will use a psp22 contract deployed on test AlephZero:

// Having added test AlephZero chain with `papi add`
import { contracts, testAzero } from "@polkadot-api/descriptors"
import { getInkClient } from "polkadot-api/ink"
import { createClient } from "polkadot-api"
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"
import { getWsProvider } from "polkadot-api/ws-provider/web"
 
const client = createClient(
  withPolkadotSdkCompat(
    getWsProvider("wss://aleph-zero-testnet-rpc.dwellir.com"),
  ),
)
 
// Create a psp22 ink! client
const psp22Client = getInkClient(contracts.psp22)
 
// typedAPI for test AlephZero
const typedApi = client.getTypedApi(testAzero)

Deploy contract

Use the inkClient.constructor(label: string) to get the functions to encode and decode constructor messages.

For example, a dry-run of a psp22 contract deployment:

const wasmBlob = ...; // read the contract wasm to deploy as a Uint8Array based on your JS runtime.
const code = Binary.fromBytes(wasmBlob)
 
// Takes in the constructor name (TS suggests the ones available)
const psp22Constructor = psp22Client.constructor("new")
 
// Encode the data for that constructor, also with full TS support
const constructorData = psp22Constructor.encode({
  supply: 100_000_000_000_000n,
  name: "PAPI token",
  symbol: "PAPI",
  decimals: 9,
})
 
// Generate a random salt - For demo purposes using a hardcoded ones.
const salt = Binary.fromText("Salt 100")
 
// Perform the call to the RuntimeAPI to dry-run a contract deployment.
const response = await typedApi.apis.ContractsApi.instantiate(
  ADDRESS.alice, // Origin
  0n, // Value
  undefined, // GasLimit
  undefined, // StorageDepositLimit
  Enum("Upload", code),
  constructorData,
  salt,
)
 
if (response.result.success) {
  const contractAddress = response.result.value.account_id
  console.log("Resulting address", contractAddress)
 
  // Events come in decoded and typed
  const events = psp22Client.event.filter(contractAddress, response.events)
  console.log("events", events)
 
  // The response message can also be decoded, and it's also fully typed
  const responseMessage = psp22Constructor.decode(response.result.value.result)
  console.log("Result response", responseMessage)
} else {
  console.log("dry run failed")
}

The same methods can be used to perform a deployment transaction, but through typedApi.tx.Contracts.instantiate_with_code.

Send Message

Similarly, the payload for contract messages can be encoded and decoded through the inkClient.message(label: string).

For example, to dry-run a PSP22::increase_allowance message:

// Takes in the message name (TS suggests the ones available)
const increaseAllowance = psp22Client.message("PSP22::increase_allowance")
// Encode the data for that message, also with full TS support
const messageData = increaseAllowance.encode({
  delta_value: 100_000_000n,
  spender: ADDRESS.bob,
})
const response = await typedApi.apis.ContractsApi.call(
  ADDRESS.alice, // Origin
  ADDRESS.psp22, // Contract address
  0n, // Value
  undefined, // GasLimit
  undefined, // StorageDepositLimit
  messageData,
)
 
if (response.result.success) {
  // Events come in decoded and typed
  const events = psp22Client.event.filter(ADDRESS.psp22, response.events)
  console.log("events", events)
 
  // The response message can also be decoded, and it's also fully typed
  const responseMessage = increaseAllowance.decode(response.result.value)
  console.log("Result response", responseMessage)
} else {
  console.log("dry run failed")
}

Events

Every message or contract deployment generates SystemEvents that are available through the regular RuntimeApi's response.events or through transactionResult.events. These can be filtered using the Events Filter API.

Ink! has its own SystemEvent, Contracts.ContractEmitted which can contain events emitted within the contract, but the payload is SCALE encoded, with a type definition declared in the metadata.

Polkadot-API's inkClient offers an API to decode a specific ink! event, and also to filter all the Contracts.ContractEmitted events from a list and return them already decoded:

type InkEvent =data: Binary };
type SystemEvent = { type: string, value: unknown };
interface InkEventInterface<E> {
  // For v5 events, we need the event's `signatureTopic` to decode it.
  decode: (value: InkEvent, signatureTopic: string) => E
  // For v4 events, the index within the metadata is used instead.
  decode: (value: InkEvent) => E
 
  filter: (
    address: string, // Contract address
    events: Array<
      // Accepts events coming from Runtime-APIs.
      | { event: SystemEvent; topics: Binary[] }
      // Also accepts events coming from transactions.
      | (SystemEvent & { topics: Binary[] })
    >,
  ) => Array<E>
}

See the previous examples, as they also include event filtering.

Storage

The inkClient also offers an API to encode storage keys and decode their result.

The storage of a contract is defined through a StorageLayout in the contract's metadata. Depending on the type of each value, the values can be accessed directly from the storage root, or they might need a separate call.

For instance, a storage layout that's just nested structs, will have all the contract's storage accessible directly from the root, and the decoder will return a regular JS object.

But if somewhere inside there's a Vector, a HashMap, or other unbounded structures, then that value is not accessible from the root, and must be queried separately.

To get the codecs for a specific storage query, use inkClient.storage():

interface StorageCodecs<K, V> {
  // key arg can be omitted if that storage entry takes no keys
  encode: (key: K) => Binary,
  decode: (data: Binary) => V
}
interface StorageInterface<D extends StorageDescriptors> {
  // path can be omitted to target the root storage (equivalent to path = "")
  storage: <P extends PathsOf<D>>(path: P) => StorageCodecs<D[P]['key'], D[P]['value']>
}

For example, to read the root storage of psp22:

const psp22Root = psp22Client.storage();
 
const response = await typedApi.apis.ContractsApi.get_storage(
  ADDRESS.psp22,
  // Root doesn't need key, so we just encode without any argument.
  psp22Root.encode(),
)
 
if (response.success && response.value) {
  const decoded = psp22Root.decode(response.value)
  console.log("storage", decoded)
  // The values are typed
  console.log('decimals', decoded.decimals)
  console.log('total supply', decoded.data.total_supply)
 
  // Note that `decoded.data.balances` is not defined (nor included in the type), because
  // even though it's defined in the layout as a property of `data.balances`, it's a HashMap,
  // so it's not returned through the root storage key.
} else {
  console.log("error", response.value)
}

And to get the balances for a particular address, we have to do a separate storage query:

// Pass in the path to the storage root to query (TS also autosuggest the possible values)
const psp22Balances = psp22Client.storage("data.balances");
 
const response = await typedApi.apis.ContractsApi.get_storage(
  ADDRESS.psp22,
  // Balances is a HashMap, needs a key, which is the address to get the balance for
  psp22Balances.encode(ADDRESS.alice),
)
 
if (response.success && response.value) {
  const decoded = psp22Balances.decode(response.value)
  console.log("alice balance", decoded)
} else {
  console.log("error", response.value)
}