Skip to content

Commit

Permalink
feat(ink): add typescript support for ink interactions (#752)
Browse files Browse the repository at this point in the history
  • Loading branch information
voliva authored Oct 7, 2024
1 parent 6573699 commit 14634be
Show file tree
Hide file tree
Showing 26 changed files with 720 additions and 53 deletions.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions examples/ink/.papi/polkadot-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
"wsUrl": "wss://aleph-zero-testnet-rpc.dwellir.com",
"metadata": ".papi/metadata/testAzero.scale"
}
},
"ink": {
"escrow": ".papi/contracts/escrow.json",
"psp22": ".papi/contracts/psp22.json"
}
}
Binary file removed examples/ink/bun.lockb
Binary file not shown.
75 changes: 35 additions & 40 deletions examples/ink/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Binary, createClient } from "polkadot-api"
import { getWsProvider } from "polkadot-api/ws-provider/web"
import { contracts, testAzero } from "@polkadot-api/descriptors"
import { createClient } from "polkadot-api"
import { getInkClient } from "polkadot-api/ink"
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"
import { getInkLookup, getInkDynamicBuilder } from "@polkadot-api/ink-contracts"
import { testAzero } from "@polkadot-api/descriptors"
import escrow from "./escrow.json"
import psp22 from "./psp22.json"
import { getWsProvider } from "polkadot-api/ws-provider/web"

const ADDRESS = {
alice: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
Expand All @@ -19,16 +17,15 @@ const client = createClient(
)

const typedApi = client.getTypedApi(testAzero)

const escrowBuilder = getInkDynamicBuilder(getInkLookup(escrow as any))
const psp22Builder = getInkDynamicBuilder(getInkLookup(psp22 as any))
const escrow = getInkClient(contracts.escrow)
const psp22 = getInkClient(contracts.psp22)

// Storage query
{
console.log("Query storage of contract")
const storage = await typedApi.apis.ContractsApi.get_storage(
ADDRESS.escrow,
Binary.fromHex(escrow.storage.root.root_key),
escrow.storage.rootKey,
)

console.log(
Expand All @@ -37,68 +34,66 @@ const psp22Builder = getInkDynamicBuilder(getInkLookup(psp22 as any))
)

if (storage.success && storage.value) {
const decoded = escrowBuilder
.buildStorageRoot()
.dec(storage.value.asBytes())
console.log("decoded", decoded)
const decoded = escrow.storage.decodeRoot(storage.value)
console.log("storage nft", decoded.nft)
console.log("storage price", decoded.price)
console.log("storage seller", decoded.seller)
}
}

// Send non-payable message
{
console.log("IncreaseAllowance")
const increaseAllowance = psp22Builder.buildMessage(
"PSP22::increase_allowance",
)
const psp22Event = psp22Builder.buildEvent()
const increaseAllowance = psp22.message("PSP22::increase_allowance")

const result = await typedApi.apis.ContractsApi.call(
const response = await typedApi.apis.ContractsApi.call(
ADDRESS.alice,
ADDRESS.psp22,
0n,
undefined,
undefined,
Binary.fromBytes(
increaseAllowance.call.enc({
spender: ADDRESS.psp22,
delta_value: 1000000n,
}),
),
increaseAllowance.encode({
spender: ADDRESS.psp22,
delta_value: 1000000n,
}),
)

if (result.result.success) {
console.log(increaseAllowance.value.dec(result.result.value.data.asBytes()))
const contractEvents = result.events
?.filter(
(v) =>
v.event.type === "Contracts" &&
v.event.value.type === "ContractEmitted",
)
.map((v) => v.event.value.value as { contract: string; data: Binary })
if (response.result.success) {
console.log(increaseAllowance.decode(response.result.value))
console.log(psp22.event.filter(ADDRESS.psp22, response.events))
} else {
console.log(
contractEvents?.map((evt) => psp22Event.dec(evt.data.asBytes())),
response.result.value,
response.gas_consumed,
response.gas_required,
)
} else {
console.log(result.result.value, result.gas_consumed, result.gas_required)
}
}

// Send payable message
{
console.log("Deposit 100 funds")
const depositFunds = escrowBuilder.buildMessage("deposit_funds")
const depositFunds = escrow.message("deposit_funds")

const result = await typedApi.apis.ContractsApi.call(
ADDRESS.alice,
ADDRESS.escrow,
100_000_000_000_000n,
undefined,
undefined,
Binary.fromBytes(depositFunds.call.enc({})),
depositFunds.encode(),
)

if (result.result.success) {
console.log(depositFunds.value.dec(result.result.value.data.asBytes()))
const decoded = depositFunds.decode(result.result.value)
if (decoded.success) {
console.log("outer success")
if (!decoded.value.success) {
console.log("inner error", decoded.value.value.type)
}
} else {
console.log("outer error", decoded.value.type)
}
} else {
console.log(result.result.value, result.gas_required)
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dependencies": {
"@commander-js/extra-typings": "^12.1.0",
"@polkadot-api/codegen": "workspace:*",
"@polkadot-api/ink-contracts": "workspace:*",
"@polkadot-api/json-rpc-provider": "workspace:*",
"@polkadot-api/known-chains": "workspace:*",
"@polkadot-api/metadata-compatibility": "workspace:*",
Expand Down
24 changes: 22 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Option, program } from "@commander-js/extra-typings"
import type { add, generate, remove, update } from "./commands"
import type { add, generate, ink, remove, update } from "./commands"
import * as knownChains from "@polkadot-api/known-chains"

export type Commands = {
add: typeof add
generate: typeof generate
remove: typeof remove
update: typeof update
ink: typeof ink
}

export function getCli({ add, generate, remove, update }: Commands) {
export function getCli({ add, generate, remove, update, ink }: Commands) {
program.name("polkadot-api").description("Polkadot API CLI")

const config = new Option("--config <filename>", "Source for the config file")
Expand Down Expand Up @@ -63,5 +64,24 @@ export function getCli({ add, generate, remove, update }: Commands) {
.option("--skip-codegen", "Skip running codegen after removing")
.action(remove)

const inkCommand = program
.command("ink")
.description("Add, update or remove ink contracts")
inkCommand
.command("add")
.description("Add or update an ink contract")
.argument("<file>", ".contract or .json metadata file for the contract")
.option("-k, --key <key>", "Key identifier for the contract")
.addOption(config)
.option("--skip-codegen", "Skip running codegen after updating")
.action(ink.add)
inkCommand
.command("remove")
.description("Remove an ink contract")
.argument("<key>", "Key identifier for the contract to remove")
.addOption(config)
.option("--skip-codegen", "Skip running codegen after updating")
.action(ink.remove)

return program
}
55 changes: 51 additions & 4 deletions packages/cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { getMetadata } from "@/metadata"
import { readPapiConfig } from "@/papiConfig"
import { generateMultipleDescriptors } from "@polkadot-api/codegen"
import {
generateInkTypes,
generateMultipleDescriptors,
} from "@polkadot-api/codegen"
import {
EntryPointCodec,
TypedefCodec,
Expand All @@ -26,6 +29,7 @@ import { CommonOptions } from "./commonOptions"
import { spawn } from "child_process"
import { readPackage } from "read-pkg"
import { detectPackageManager } from "../packageManager"
import { getInkLookup } from "@polkadot-api/ink-contracts"

export interface GenerateOptions extends CommonOptions {
clientLibrary?: string
Expand Down Expand Up @@ -56,22 +60,28 @@ export async function generate(opts: GenerateOptions) {
})),
)

console.log(`Generating descriptors`)
await cleanDescriptorsPackage(config.descriptorPath)
const descriptorsDir = join(process.cwd(), config.descriptorPath)

const clientPath = opts.clientLibrary ?? "polkadot-api"

const whitelist = opts.whitelist ? await readWhitelist(opts.whitelist) : null
const descriptorSrcDir = join(descriptorsDir, "src")
const hash = await outputCodegen(
chains,
join(descriptorsDir, "src"),
descriptorSrcDir,
clientPath,
whitelist,
)
await replacePackageJson(descriptorsDir, hash)

if (config.ink) {
outputInkCodegen(config.ink, descriptorSrcDir)
}

await replacePackageJson(descriptorsDir, hash)
await compileCodegen(descriptorsDir)
await fs.rm(join(descriptorsDir, "src"), { recursive: true })
await fs.rm(descriptorSrcDir, { recursive: true })
await runInstall()
await flushBundlerCache()
}
Expand Down Expand Up @@ -213,6 +223,43 @@ export default content
return hash
}

async function outputInkCodegen(
contracts: Record<string, string>,
outputFolder: string,
) {
console.log("Generating ink! types")

const contractsFolder = join(outputFolder, "contracts")
if (!existsSync(contractsFolder))
await fs.mkdir(contractsFolder, { recursive: true })

const imports: string[] = []
for (const [key, metadata] of Object.entries(contracts)) {
try {
const types = generateInkTypes(
getInkLookup(JSON.parse(await fs.readFile(metadata, "utf-8"))),
)
await fs.writeFile(join(contractsFolder, `${key}.ts`), types)
imports.push(`export { descriptor as ${key} } from './${key}'`)
} catch (ex) {
console.error("Exception when generating descriptors for contract " + key)
console.error(ex)
}
}

await fs.writeFile(
join(contractsFolder, `index.ts`),
imports.join("\n") + "\n",
)

fs.appendFile(
join(outputFolder, "index.ts"),
`
export * as contracts from './contracts';
`,
)
}

async function compileCodegen(packageDir: string) {
const srcDir = join(packageDir, "src")
const outDir = join(packageDir, "dist")
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./add"
export * from "./generate"
export * from "./ink"
export * from "./remove"
export * from "./update"
69 changes: 69 additions & 0 deletions packages/cli/src/commands/ink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
defaultConfig,
papiFolder,
readPapiConfig,
writePapiConfig,
} from "@/papiConfig"
import { existsSync } from "node:fs"
import * as fs from "node:fs/promises"
import { join } from "node:path"
import { CommonOptions } from "./commonOptions"
import { generate } from "./generate"

export interface InkAddOptions extends CommonOptions {
key?: string
}

export const ink = {
async add(file: string, options: InkAddOptions) {
const metadata = JSON.parse(await fs.readFile(file, "utf-8"))
// Remove wasm blob if it's there
delete metadata.source?.wasm

const key = options.key || metadata.contract.name
const config = (await readPapiConfig(options.config)) ?? defaultConfig
const inkConfig = (config.ink ||= {})
if (key in inkConfig) {
console.warn(`Replacing existing ${key} config`)
}

const contractsFolder = join(papiFolder, "contracts")
if (!existsSync(contractsFolder)) {
await fs.mkdir(contractsFolder, { recursive: true })
}
const fileName = join(contractsFolder, key + ".json")
await fs.writeFile(fileName, JSON.stringify(metadata, null, 2))

inkConfig[key] = fileName
await writePapiConfig(options.config, config)

if (!options.skipCodegen) {
generate({
config: options.config,
})
}
},
async remove(key: string, options: CommonOptions) {
const config = (await readPapiConfig(options.config)) ?? defaultConfig
const inkConfig = (config.ink ||= {})
if (!(key in inkConfig)) {
console.log(`${key} contract not found in config`)
return
}

const fileName = inkConfig[key]
delete inkConfig[key]

if (existsSync(fileName)) {
await fs.rm(fileName)
}

await writePapiConfig(options.config, config)

if (!options.skipCodegen) {
generate({
config: options.config,
})
}
},
}
4 changes: 2 additions & 2 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { getCli } from "./cli"
import { add, generate, remove, update } from "./commands"
import { add, generate, ink, remove, update } from "./commands"

const program = getCli({ add, generate, remove, update })
const program = getCli({ add, generate, remove, update, ink })
program.parse()
1 change: 1 addition & 0 deletions packages/cli/src/papiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type PapiConfig = {
version: 0
descriptorPath: string
entries: Record<string, EntryConfig>
ink?: Record<string, string>
}

export const papiFolder = ".papi"
Expand Down
9 changes: 9 additions & 0 deletions packages/client/ink/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "polkadot-api_ink",
"types": "../dist/reexports/ink.d.ts",
"module": "../dist/esm/reexports/ink.mjs",
"import": "../dist/esm/reexports/ink.mjs",
"browser": "../dist/esm/reexports/ink.mjs",
"require": "../dist/reexports/ink.js",
"default": "../dist/reexports/ink.js"
}
Loading

0 comments on commit 14634be

Please sign in to comment.