Skip to content

Commit

Permalink
feat(ink-sdk): add docs for ink sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
voliva committed Dec 30, 2024
1 parent 8258f87 commit 9f574bb
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 5 deletions.
2 changes: 1 addition & 1 deletion docs/pages/ink.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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.
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, see [Ink! SDK](/sdks/ink-sdk).

## Codegen

Expand Down
230 changes: 230 additions & 0 deletions docs/pages/sdks/ink-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Ink! SDK

The Ink! SDK is a library for interacting with smart contracts, built on top of the [Ink! Client](/ink).

## Getting Started

Begin by generating the type definitions for your chain and contract. For example, using a PSP22 contract on the test Aleph Zero network:

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

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

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

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

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

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

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

```ts
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](#contract-deployer), but it's done directly from the contract instance:

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

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

```ts
const aliceBalance = await storage.getNested("data.balances", ALICE);
if (aliceBalance.success) {
console.log("Alice balance", aliceBalance.value);
}
```
9 changes: 9 additions & 0 deletions docs/pages/sdks/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Polkadot-API SDKs

The Polkadot-API library is designed to be modular and chain-agnostic, meaning it does not include any business-level logic. It reads the chain metadata to understand how to interact with the chain, but it is up to the developer to define its specific use and purpose.

With its modular design philosophy, Polkadot-API enables the creation of additional libraries that add new layers of abstraction, significantly enhancing the developer experience when building dApps. These additional libraries are referred to as "Polkadot-API SDKs."

These SDKs abstract implementation details from the pallets, elevating chain interactions to a higher level of abstraction. They handle complexities such as transaction weight calculation, associated fees, and determining the best transaction (or set of transactions) for performing specific actions.

We have created several SDKs, documented in the following sections, to simplify common interactions when working with Substrate-based chains. These SDKs not only streamline development but can also serve as examples for anyone interested in creating their own SDKs.
15 changes: 11 additions & 4 deletions vocs.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,8 @@ export default defineConfig({
},
{
text: "Typed API",
link: "/typed",
items: [
{
text: "API",
link: "/typed",
},
{
text: "Constants",
link: "/typed/constants",
Expand Down Expand Up @@ -97,6 +94,16 @@ export default defineConfig({
},
],
},
{
text: "PAPI SDKs",
link: "/sdks/intro",
items: [
{
text: "Ink! SDK",
link: "/sdks/ink-sdk",
},
],
},
{
text: "Chain-specific documentation",
link: "https://chains.papi.how",
Expand Down

0 comments on commit 9f574bb

Please sign in to comment.