diff --git a/docs/pages/ink.md b/docs/pages/ink.md new file mode 100644 index 00000000..4498064d --- /dev/null +++ b/docs/pages/ink.md @@ -0,0 +1,252 @@ +# 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: + +```sh +> 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] Add or update an ink contract + remove [options] Remove an ink contract + help [command] display help for command +``` + +So to generate the types for a contract, run the `ink add` command: +```sh +> 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. + +:::warning +The descriptors exported from `@polkadot-api/descriptors` must always be treated as black boxes, passed directly as inputs to the ink client. The type structure or the internals is subject to change without a major version bump. +::: + +## 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: + +```ts +// 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: + +```ts +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: + +```ts +// 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](/typed/events#filter). + +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: + +```ts +type InkEvent = { data: Binary }; +type SystemEvent = { type: string, value: unknown }; +interface InkEventInterface { + // 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 +} +``` + +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()`: + +```ts +interface StorageCodecs { + // key arg can be omitted if that storage entry takes no keys + encode: (key: K) => Binary, + decode: (data: Binary) => V +} +interface StorageInterface { + // path can be omitted to target the root storage (equivalent to path = "") + storage:

>(path: P) => StorageCodecs +} +``` + +For example, to read the root storage of psp22: + +```ts +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: + +```ts +// 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) +} +``` diff --git a/vocs.config.tsx b/vocs.config.tsx index 89d2126d..cc000972 100644 --- a/vocs.config.tsx +++ b/vocs.config.tsx @@ -91,6 +91,10 @@ export default defineConfig({ text: "Unsafe API", link: "/unsafe", }, + { + text: "Ink!", + link: "/ink", + }, ], }, {