Skip to content

Commit 70a1e87

Browse files
authored
Merge pull request #33 from polkadot-api/vo-ink
feat(ink!): add docs for ink client
2 parents 7377a0f + 9dddd35 commit 70a1e87

File tree

2 files changed

+256
-0
lines changed

2 files changed

+256
-0
lines changed

docs/pages/ink.md

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Ink!
2+
3+
Polkadot-API adds typescript definitions for ink! contracts, as well as utilities to encode and decode messages, contract storage and events.
4+
5+
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.
6+
7+
## Codegen
8+
9+
The first step is to generate the types from the contract metadata. The Polkadot-API CLI has a command specific for ink:
10+
11+
```sh
12+
> pnpm papi ink --help
13+
Usage: polkadot-api ink [options] [command]
14+
15+
Add, update or remove ink contracts
16+
17+
Options:
18+
-h, --help display help for command
19+
20+
Commands:
21+
add [options] <file> Add or update an ink contract
22+
remove [options] <key> Remove an ink contract
23+
help [command] display help for command
24+
```
25+
26+
So to generate the types for a contract, run the `ink add` command:
27+
```sh
28+
> pnpm papi ink add "path to .contract or .json metadata file"
29+
```
30+
31+
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".
32+
33+
The generated code contains all the types, and also the required info from the metadata to encode and decode values.
34+
35+
:::warning
36+
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.
37+
:::
38+
39+
## Ink! Client
40+
41+
Start by creating the ink client from `polkadot-api/ink`. In the following example we will use a psp22 contract deployed on test AlephZero:
42+
43+
```ts
44+
// Having added test AlephZero chain with `papi add`
45+
import { contracts, testAzero } from "@polkadot-api/descriptors"
46+
import { getInkClient } from "polkadot-api/ink"
47+
import { createClient } from "polkadot-api"
48+
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"
49+
import { getWsProvider } from "polkadot-api/ws-provider/web"
50+
51+
const client = createClient(
52+
withPolkadotSdkCompat(
53+
getWsProvider("wss://aleph-zero-testnet-rpc.dwellir.com"),
54+
),
55+
)
56+
57+
// Create a psp22 ink! client
58+
const psp22Client = getInkClient(contracts.psp22)
59+
60+
// typedAPI for test AlephZero
61+
const typedApi = client.getTypedApi(testAzero)
62+
```
63+
64+
### Deploy contract
65+
66+
Use the `inkClient.constructor(label: string)` to get the functions to encode and decode constructor messages.
67+
68+
For example, a dry-run of a psp22 contract deployment:
69+
70+
```ts
71+
const wasmBlob = ...; // read the contract wasm to deploy as a Uint8Array based on your JS runtime.
72+
const code = Binary.fromBytes(wasmBlob)
73+
74+
// Takes in the constructor name (TS suggests the ones available)
75+
const psp22Constructor = psp22Client.constructor("new")
76+
77+
// Encode the data for that constructor, also with full TS support
78+
const constructorData = psp22Constructor.encode({
79+
supply: 100_000_000_000_000n,
80+
name: "PAPI token",
81+
symbol: "PAPI",
82+
decimals: 9,
83+
})
84+
85+
// Generate a random salt - For demo purposes using a hardcoded ones.
86+
const salt = Binary.fromText("Salt 100")
87+
88+
// Perform the call to the RuntimeAPI to dry-run a contract deployment.
89+
const response = await typedApi.apis.ContractsApi.instantiate(
90+
ADDRESS.alice, // Origin
91+
0n, // Value
92+
undefined, // GasLimit
93+
undefined, // StorageDepositLimit
94+
Enum("Upload", code),
95+
constructorData,
96+
salt,
97+
)
98+
99+
if (response.result.success) {
100+
const contractAddress = response.result.value.account_id
101+
console.log("Resulting address", contractAddress)
102+
103+
// Events come in decoded and typed
104+
const events = psp22Client.event.filter(contractAddress, response.events)
105+
console.log("events", events)
106+
107+
// The response message can also be decoded, and it's also fully typed
108+
const responseMessage = psp22Constructor.decode(response.result.value.result)
109+
console.log("Result response", responseMessage)
110+
} else {
111+
console.log("dry run failed")
112+
}
113+
```
114+
115+
The same methods can be used to perform a deployment transaction, but through `typedApi.tx.Contracts.instantiate_with_code`.
116+
117+
### Send Message
118+
119+
Similarly, the payload for contract messages can be encoded and decoded through the `inkClient.message(label: string)`.
120+
121+
For example, to dry-run a PSP22::increase_allowance message:
122+
123+
```ts
124+
// Takes in the message name (TS suggests the ones available)
125+
const increaseAllowance = psp22Client.message("PSP22::increase_allowance")
126+
// Encode the data for that message, also with full TS support
127+
const messageData = increaseAllowance.encode({
128+
delta_value: 100_000_000n,
129+
spender: ADDRESS.bob,
130+
})
131+
const response = await typedApi.apis.ContractsApi.call(
132+
ADDRESS.alice, // Origin
133+
ADDRESS.psp22, // Contract address
134+
0n, // Value
135+
undefined, // GasLimit
136+
undefined, // StorageDepositLimit
137+
messageData,
138+
)
139+
140+
if (response.result.success) {
141+
// Events come in decoded and typed
142+
const events = psp22Client.event.filter(ADDRESS.psp22, response.events)
143+
console.log("events", events)
144+
145+
// The response message can also be decoded, and it's also fully typed
146+
const responseMessage = increaseAllowance.decode(response.result.value)
147+
console.log("Result response", responseMessage)
148+
} else {
149+
console.log("dry run failed")
150+
}
151+
```
152+
153+
### Events
154+
155+
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).
156+
157+
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.
158+
159+
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:
160+
161+
```ts
162+
type InkEvent = { data: Binary };
163+
type SystemEvent = { type: string, value: unknown };
164+
interface InkEventInterface<E> {
165+
// For v5 events, we need the event's `signatureTopic` to decode it.
166+
decode: (value: InkEvent, signatureTopic: string) => E
167+
// For v4 events, the index within the metadata is used instead.
168+
decode: (value: InkEvent) => E
169+
170+
filter: (
171+
address: string, // Contract address
172+
events: Array<
173+
// Accepts events coming from Runtime-APIs.
174+
| { event: SystemEvent; topics: Binary[] }
175+
// Also accepts events coming from transactions.
176+
| (SystemEvent & { topics: Binary[] })
177+
>,
178+
) => Array<E>
179+
}
180+
```
181+
182+
See the previous examples, as they also include event filtering.
183+
184+
### Storage
185+
186+
The `inkClient` also offers an API to encode storage keys and decode their result.
187+
188+
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.
189+
190+
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.
191+
192+
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.
193+
194+
To get the codecs for a specific storage query, use `inkClient.storage()`:
195+
196+
```ts
197+
interface StorageCodecs<K, V> {
198+
// key arg can be omitted if that storage entry takes no keys
199+
encode: (key: K) => Binary,
200+
decode: (data: Binary) => V
201+
}
202+
interface StorageInterface<D extends StorageDescriptors> {
203+
// path can be omitted to target the root storage (equivalent to path = "")
204+
storage: <P extends PathsOf<D>>(path: P) => StorageCodecs<D[P]['key'], D[P]['value']>
205+
}
206+
```
207+
208+
For example, to read the root storage of psp22:
209+
210+
```ts
211+
const psp22Root = psp22Client.storage();
212+
213+
const response = await typedApi.apis.ContractsApi.get_storage(
214+
ADDRESS.psp22,
215+
// Root doesn't need key, so we just encode without any argument.
216+
psp22Root.encode(),
217+
)
218+
219+
if (response.success && response.value) {
220+
const decoded = psp22Root.decode(response.value)
221+
console.log("storage", decoded)
222+
// The values are typed
223+
console.log('decimals', decoded.decimals)
224+
console.log('total supply', decoded.data.total_supply)
225+
226+
// Note that `decoded.data.balances` is not defined (nor included in the type), because
227+
// even though it's defined in the layout as a property of `data.balances`, it's a HashMap,
228+
// so it's not returned through the root storage key.
229+
} else {
230+
console.log("error", response.value)
231+
}
232+
```
233+
234+
And to get the balances for a particular address, we have to do a separate storage query:
235+
236+
```ts
237+
// Pass in the path to the storage root to query (TS also autosuggest the possible values)
238+
const psp22Balances = psp22Client.storage("data.balances");
239+
240+
const response = await typedApi.apis.ContractsApi.get_storage(
241+
ADDRESS.psp22,
242+
// Balances is a HashMap, needs a key, which is the address to get the balance for
243+
psp22Balances.encode(ADDRESS.alice),
244+
)
245+
246+
if (response.success && response.value) {
247+
const decoded = psp22Balances.decode(response.value)
248+
console.log("alice balance", decoded)
249+
} else {
250+
console.log("error", response.value)
251+
}
252+
```

vocs.config.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ export default defineConfig({
9191
text: "Unsafe API",
9292
link: "/unsafe",
9393
},
94+
{
95+
text: "Ink!",
96+
link: "/ink",
97+
},
9498
],
9599
},
96100
{

0 commit comments

Comments
 (0)