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 SystemEvent
s 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)
}