Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

feat: add ink flipper smart contract example #461

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"frame_metadata/raw_erc20_metadata.json",
"target",
"**/__snapshots__/*.snap",
"codegen/_output"
"codegen/_output",
"examples/smart_contract"
]
}
3 changes: 2 additions & 1 deletion deps/std/encoding/base58.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "https://deno.land/[email protected]/encoding/base58.ts"
// TODO: use [email protected]/encoding/base58.ts when https://github.com/denoland/deno_std/pull/2982 is released
export * from "https://raw.githubusercontent.com/denoland/deno_std/01696ce149463f498301782ac5e9ee322a86182c/encoding/base58.ts"
5 changes: 4 additions & 1 deletion effects/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function events<Extrinsic extends SignedExtrinsic, FinalizedHash extends
}, k1_)
const events = entryRead(client)("System", "Events", [], finalizedHash)
.access("value")
.as<{ phase: { value: number } }[]>()
.as<{
event?: Record<string, any>
phase: { value: number }
}[]>()
return Z
.ls(idx, events)
.next(([idx, events]) => {
Expand Down
1 change: 1 addition & 0 deletions effects/rpc_known_methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { rpcCall, rpcSubscription } from "./rpc.ts"
// TODO: generate the following?
export namespace state {
export const getMetadata = rpcCall<[at?: U.HexHash], U.HexHash>("state_getMetadata")
export const call = rpcCall<[method: string, data: U.Hex], U.HexHash>("state_call")
export const getStorage = rpcCall<
[key: known.StorageKey, at?: U.HexHash],
known.StorageData
Expand Down
215 changes: 215 additions & 0 deletions examples/smart_contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import * as path from "http://localhost:5646/@local/deps/std/path.ts"
import * as C from "http://localhost:5646/@local/mod.ts"
import * as T from "http://localhost:5646/@local/test_util/mod.ts"
import * as U from "http://localhost:5646/@local/util/mod.ts"
import {
$contractsApiCallArgs,
$contractsApiCallResult,
$contractsApiInstantiateArgs,
$contractsApiInstantiateResult,
} from "../frame_metadata/Contract.ts"

const configFile = getFilePath("smart_contract/zombienet.toml")
const zombienet = await T.zombienet.start(configFile)
const client = zombienet.clients.byName["collator01"]!

const contract = await getContract(
getFilePath("smart_contract/flipper.wasm"),
getFilePath("smart_contract/metadata.json"),
)

const contractAddress = U.throwIfError(await instantiateContractTx().run())
console.log("Deployed Contract address", U.ss58.encode(42, contractAddress))
console.log("get message", U.throwIfError(await sendGetMessage(contractAddress).run()))
console.log("flip message in block", U.throwIfError(await sendFlipMessage(contractAddress).run()))
console.log("get message", U.throwIfError(await sendGetMessage(contractAddress).run()))

await zombienet.close()

function instantiateContractTx() {
const constructor = findContractConstructorByLabel("default")!
const salt = Uint8Array.from(Array.from([0, 0, 0, 0]), () => Math.floor(Math.random() * 16))
const value = preSubmitContractInstantiateDryRunGasEstimate(constructor, contract.wasm, salt)
.next(({ gasRequired, result: { accountId } }) => {
// the contract address derived from the code hash and the salt
console.log("Derived contract address", U.ss58.encode(42, accountId))
return {
type: "instantiateWithCode",
value: 0n,
gasLimit: {
refTime: gasRequired.refTime,
proofSize: gasRequired.proofSize,
},
storageDepositLimit: undefined,
code: contract.wasm,
data: U.hex.decode(constructor.selector),
salt,
}
})
const tx = C.extrinsic(client)({
sender: T.alice.address,
call: C.Z.rec({
type: "Contracts",
value,
}),
})
.signed(T.alice.sign)
const finalizedIn = tx.watch(({ end }) =>
(status) => {
if (typeof status !== "string" && (status.inBlock ?? status.finalized)) {
return end(status.inBlock ?? status.finalized)
} else if (C.rpc.known.TransactionStatus.isTerminal(status)) {
return end(new Error())
}
return
}
)
return C.events(tx, finalizedIn).next((events) => {
const extrinsicFailed = events.some((e) =>
e.event?.type === "System" && e.event?.value?.type === "ExtrinsicFailed"
)
if (extrinsicFailed) {
return new Error("extrinsic failed")
}
const event = events.find((e) =>
e.event?.type === "Contracts" && e.event?.value?.type === "Instantiated"
)
return event?.event?.value.contract as Uint8Array
})
}

function preSubmitContractInstantiateDryRunGasEstimate(
message: C.M.ContractMetadata.Constructor,
code: Uint8Array,
salt: Uint8Array,
) {
const key = U.hex.encode($contractsApiInstantiateArgs.encode([
T.alice.publicKey,
0n,
undefined,
undefined,
{ type: "Upload", value: code },
U.hex.decode(message.selector),
salt,
]))
return C.state.call(client)(
"ContractsApi_instantiate",
key,
)
.next((encodedResponse) => {
return $contractsApiInstantiateResult.decode(U.hex.decode(encodedResponse))
})
}

function preSubmitContractCallDryRunGasEstimate(
address: Uint8Array,
message: C.M.ContractMetadata.Message | C.M.ContractMetadata.Constructor,
) {
const key = U.hex.encode($contractsApiCallArgs.encode([
T.alice.publicKey,
address,
0n,
undefined,
undefined,
U.hex.decode(message.selector),
]))
return C.state.call(client)(
"ContractsApi_call",
key,
)
.next((encodedResponse) => {
return $contractsApiCallResult.decode(U.hex.decode(encodedResponse))
})
}

function sendGetMessage(address: Uint8Array) {
const message = findContractMessageByLabel("get")!
const key = U.hex.encode($contractsApiCallArgs.encode([
T.alice.publicKey,
address,
0n,
undefined,
undefined,
U.hex.decode(message.selector),
]))
return C.state.call(client)(
"ContractsApi_call",
key,
)
.next((encodedResponse) => {
const response = $contractsApiCallResult.decode(U.hex.decode(encodedResponse))
if (message.returnType.type === null) {
return undefined
}
return contract.deriveCodec(message.returnType.type).decode(response.result.data)
})
}

function sendFlipMessage(address: Uint8Array) {
const message = findContractMessageByLabel("flip")!
const value = preSubmitContractCallDryRunGasEstimate(address, message)
.next(({ gasRequired }) => {
return {
type: "call",
dest: C.MultiAddress.Id(address),
value: 0n,
data: U.hex.decode(message.selector),
gasLimit: {
refTime: gasRequired.refTime,
proofSize: gasRequired.proofSize,
},
storageDepositLimit: undefined,
}
})
const tx = C.extrinsic(client)({
sender: T.alice.address,
call: C.Z.rec({
type: "Contracts",
value,
}),
})
.signed(T.alice.sign)
const finalizedIn = tx.watch(({ end }) =>
(status) => {
if (typeof status !== "string" && (status.inBlock ?? status.finalized)) {
return end(status.inBlock ?? status.finalized)
} else if (C.rpc.known.TransactionStatus.isTerminal(status)) {
return end(new Error())
}
return
}
)
return C.Z.ls(finalizedIn, C.events(tx, finalizedIn)).next(([finalizedIn, events]) => {
const extrinsicFailed = events.some((e) =>
e.event?.type === "System" && e.event?.value?.type === "ExtrinsicFailed"
)
if (extrinsicFailed) {
return new Error("extrinsic failed")
}
return finalizedIn
})
}

function findContractConstructorByLabel(label: string) {
return contract.metadata.V3.spec.constructors.find((c) => c.label === label)
}

function findContractMessageByLabel(label: string) {
return contract.metadata.V3.spec.messages.find((c) => c.label === label)
}

async function getContract(wasmFile: string, metadataFile: string) {
const wasm = await Deno.readFile(wasmFile)
const metadata = C.M.ContractMetadata.normalize(JSON.parse(
await Deno.readTextFile(metadataFile),
))
const deriveCodec = C.M.DeriveCodec(metadata.V3.types)
return { wasm, metadata, deriveCodec }
}

function getFilePath(relativeFilePath: string) {
return path.join(
path.dirname(path.fromFileUrl(import.meta.url)),
relativeFilePath,
)
}
1 change: 1 addition & 0 deletions examples/smart_contract/flipper.contract

Large diffs are not rendered by default.

Binary file added examples/smart_contract/flipper.wasm
Binary file not shown.
108 changes: 108 additions & 0 deletions examples/smart_contract/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"source": {
"hash": "0x2345709e061bfa0d374eaf0c25f19c8dce43ac11362c55196e1ecf465781b750",
"language": "ink! 3.4.0",
"compiler": "rustc 1.67.0-nightly"
},
"contract": {
"name": "flipper",
"version": "0.1.0",
"authors": [
"[your_name] <[your_email]>"
]
},
"V3": {
"spec": {
"constructors": [
{
"args": [
{
"label": "init_value",
"type": {
"displayName": [
"bool"
],
"type": 0
}
}
],
"docs": [
"Constructor that initializes the `bool` value to the given `init_value`."
],
"label": "new",
"payable": false,
"selector": "0x9bae9d5e"
},
{
"args": [],
"docs": [
"Constructor that initializes the `bool` value to `false`.",
"",
"Constructors can delegate to other constructors."
],
"label": "default",
"payable": false,
"selector": "0xed4b9d1b"
}
],
"docs": [],
"events": [],
"messages": [
{
"args": [],
"docs": [
" A message that can be called on instantiated contracts.",
" This one flips the value of the stored `bool` from `true`",
" to `false` and vice versa."
],
"label": "flip",
"mutates": true,
"payable": false,
"returnType": null,
"selector": "0x633aa551"
},
{
"args": [],
"docs": [
" Simply returns the current value of our `bool`."
],
"label": "get",
"mutates": false,
"payable": false,
"returnType": {
"displayName": [
"bool"
],
"type": 0
},
"selector": "0x2f865bd9"
}
]
},
"storage": {
"struct": {
"fields": [
{
"layout": {
"cell": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000000",
"ty": 0
}
},
"name": "value"
}
]
}
},
"types": [
{
"id": 0,
"type": {
"def": {
"primitive": "bool"
}
}
}
]
}
}
24 changes: 24 additions & 0 deletions examples/smart_contract/zombienet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[relaychain]
default_image = "docker.io/paritypr/polkadot-debug:master"
default_command = "polkadot"
default_args = ["-lparachain=debug"]
chain = "rococo-local"

[[relaychain.nodes]]
name = "alice"
validator = true

[[relaychain.nodes]]
name = "bob"
validator = true

[[parachains]]
id = 1000
cumulus_based = true
chain = "contracts-rococo-local"

[parachains.collator]
name = "collator01"
image = "docker.io/parity/polkadot-parachain:latest"
command = "polkadot-parachain"
args = ["-lparachain=debug"]
Loading