Skip to content

Commit

Permalink
feat(ink!): add docs for ink client
Browse files Browse the repository at this point in the history
  • Loading branch information
voliva committed Oct 22, 2024
1 parent 7377a0f commit 9dddd35
Show file tree
Hide file tree
Showing 2 changed files with 256 additions and 0 deletions.
252 changes: 252 additions & 0 deletions docs/pages/ink.md
Original file line number Diff line number Diff line change
@@ -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] <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:
```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<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()`:

```ts
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:

```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)
}
```
4 changes: 4 additions & 0 deletions vocs.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export default defineConfig({
text: "Unsafe API",
link: "/unsafe",
},
{
text: "Ink!",
link: "/ink",
},
],
},
{
Expand Down

0 comments on commit 9dddd35

Please sign in to comment.