Ink! SDK
The Ink! SDK is a library for interacting with smart contracts, built on top of the Ink! Client.
Getting Started
Install the sdk through your package manager:
pnpm i @polkadot-api/sdk-ink
Begin by generating the type definitions for your chain and contract. For example, using a PSP22 contract on the test Aleph Zero network:
pnpm papi add -w wss://aleph-zero-testnet-rpc.dwellir.com testAzero
pnpm papi ink add ./psp22.json # Path to the .contract or .json metadata file
This process uses the name defined in the contract metadata to export it as a property of contracts
in @polkadot-api/descriptors
. For this example, the contract name is "psp22." You can now instantiate the Ink! SDK:
import { contracts, testAzero } from "@polkadot-api/descriptors"
import { createInkSdk } from "@polkadot-api/sdk-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"),
),
)
const typedApi = client.getTypedApi(testAzero)
const psp22Sdk = createInkSdk(typedApi, contracts.psp22)
The SDK provides two main functions for different workflows:
getDeployer(code: Binary)
: Returns an API for deploying contracts.getContract(address: SS58String)
: Returns an API for interacting with deployed contracts.
Contract Deployer
import { Binary } from "polkadot-api";
const wasmBlob = ...; // Uint8Array of the contract WASM blob.
const code = Binary.fromBytes(wasmBlob);
const psp22Deployer = psp22Sdk.getDeployer(code);
The deployer API supports two methods: one for dry-running deployments and another for actual deployments.
When deploying, the SDK calculates all parameters, requiring only the constructor arguments defined in the contract.
To calculate the gas limit required, the SDK also needs the origin account the transaction will be sent from. So it either uses the origin account or the gas limit if you already have it.
The "salt" parameter ensures unique contract deployments. By default, it is empty, but you can provide a custom value for deploying multiple instances of the same contract.
// Deploy psp22
const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
const tx = psp22Deployer.deploy("new", {
origin: ALICE,
data: {
supply: 1_000_000_000_000n,
decimals: 9,
},
})
// `tx` is a regular transaction that can be sent with `.signSubmitAndWatch`, `.signAndSubmit`, etc.
const result = await tx.signAndSubmit(aliceSigner)
// To get the resulting address the contract was deployed to, we can pass the events back into the SDK:
const data: {
address: string
contractEvents: EventDescriptor[]
} | null = psp22Sdk.readDeploymentEvents(ALICE, result.events)
Dry-running takes in the same arguments, but returns a Promise with the result directly instead:
const dryRunResult = await psp22Deployer.deploy("new", {
origin: ALICE,
data: {
supply: 1_000_000_000_000n,
decimals: 9,
},
})
if (dryRunResult.success) {
console.log(dryRunResult.value)
/*
dryRunResult.value has:
{
// Resulting address of contract
address: SS58String;
// Events that would be generated when deploying
events: EventDescriptor[];
// Weight required
gasRequired: Gas;
}
*/
}
Contract API
The contract API targets a specific instance of a contract by address, providing multiple interaction functions:
const PSP22_INSTANCE = "5F69jP7VwzCp6pGZ93mv9FkAhwnwz4scR4J9asNeSgFPUGLq"
const psp22Contract = psp22Sdk.getContract(PSP22_INSTANCE)
// You optionally can make sure the hash hasn't changed by checking compatibility
if (!(await psp22Contract.isCompatible())) {
throw new Error("Contract has changed")
}
Query
Sending a query (also known as dry-running a message), can be sent directly through the .query()
method, passing the name of the message, origin and arguments:
console.log("Get balance of ALICE")
const result = await psp22Contract.query("PSP22::balance_of", {
origin: ALICE,
data: {
owner: ALICE,
},
})
if (result.success) {
console.log("balance of alice", result.value.response)
console.log("events", result.value.events)
} else {
console.log("error", result.value)
}
Send
Sending a message requires signing a transaction, which can be created with the .send()
method:
console.log("Increase allowance")
const allowanceTxResult = await psp22Contract
.send("PSP22::increase_allowance", {
origin: ADDRESS.alice,
data,
})
.signAndSubmit(aliceSigner)
if (allowanceTxResult.ok) {
console.log("block", allowanceTxResult.block)
// The events generated by this contract can also be filtered using `filterEvents`:
console.log("events", psp22Contract.filterEvents(allowanceTxResult.events))
} else {
console.log("error", allowanceTxResult.dispatchError)
}
Redeploy
Contract instances can also be redeployed without the need to have the actual WASM blob. This is similar to using the Contract Deployer, but it's done directly from the contract instance:
const salt = Binary.fromHex("0x00")
const result = await psp22Contract.dryRunRedeploy("new", {
data,
origin: ADDRESS.alice,
options: {
salt,
},
})
if (result.success) console.log("redeploy dry run", result)
const txResult = await psp22Contract
.redeploy("new", {
data,
origin: ADDRESS.alice,
options: {
salt,
},
})
.signAndSubmit(signer)
const deployment = psp22Sdk.readDeploymentEvents(ADDRESS.alice, txResult.events)
Storage API
The storage of a contract is a tree structure, where you can query the values that are singletons, but for those that can grow into lists, maps, etc. they have to be queried separately.
This SDK has full typescript support for storage. You start by selecting where to begin from the tree, and you'll get back an object with the data within that tree.
const storage = psp22Contract.getStorage()
const root = await storage.getRoot()
if (root.success) {
/* result.value is what the psp22 contract has defined as the root of its storage
{
name: Option<string>,
symbol: Option<string>,
decimals: number,
data: {
total_supply: bigint,
allowances: (key: [SS58String, SS58String]) => Promise<Result<bigint>>,
balances: (key: SS58String) => Promise<Result<bigint>>
}
}
*/
}
If inside of that subtree there are storage entries that have to be queried separately, the SDK turns those properties into functions that query that. In the example above, data.allowances
and data.balances
are maps that require one specific key to perform the query, so they have been turned into async functions.
If you don't need the data of the root and just want to query for a specific balance directly, you can get any nested subtree by calling .getNested()
:
const aliceBalance = await storage.getNested("data.balances", ALICE)
if (aliceBalance.success) {
console.log("Alice balance", aliceBalance.value)
}