From 1669134cd05294e6803bc85cc793072ae7ee495a Mon Sep 17 00:00:00 2001 From: Matias Volpe Date: Mon, 19 Dec 2022 12:53:00 -0300 Subject: [PATCH] feat: add contract call effect --- effects/contracts/call.ts | 152 +++++++++++++++++++++++++++++++ effects/contracts/instantiate.ts | 4 +- effects/contracts/mod.ts | 1 + examples/smart_contract.ts | 140 ++++++++-------------------- 4 files changed, 192 insertions(+), 105 deletions(-) create mode 100644 effects/contracts/call.ts diff --git a/effects/contracts/call.ts b/effects/contracts/call.ts new file mode 100644 index 000000000..092af7ac9 --- /dev/null +++ b/effects/contracts/call.ts @@ -0,0 +1,152 @@ +import * as $ from "../../deps/scale.ts" +import * as Z from "../../deps/zones.ts" +import { + $contractsApiCallArgs, + $contractsApiCallResult, + ContractMetadata, +} from "../../frame_metadata/Contract.ts" +import { DeriveCodec, MultiAddress } from "../../frame_metadata/mod.ts" +import { Client } from "../../rpc/mod.ts" +import * as U from "../../util/mod.ts" +import { extrinsic } from "../extrinsic.ts" +import { state } from "../rpc_known_methods.ts" + +export interface CallProps { + sender: MultiAddress + contractAddress: Uint8Array + contractMetadata: ContractMetadata + message: ContractMetadata.Message + args: any[] +} + +export function call>(client: Client_) { + return >(_props: Props) => { + const { + sender, + contractAddress, + contractMetadata, + message, + args, + } = _props as Z.Rec$Access + const $message_ = $message(contractMetadata, message) + const data = Z.ls($message_, message, args).next(([{ $args }, message, args]) => { + return $args.encode([U.hex.decode(message.selector), ...args]) + }) + return Z.ls( + stateContractsApiCall(client)({ + sender, + contractAddress, + data, + }), + $message_, + ) + .next(([result, { $result }]) => { + return $result.decode(result.result.data) + }) + } +} + +export interface CallTxProps { + sender: MultiAddress + contractAddress: Uint8Array + value?: bigint + contractMetadata: ContractMetadata + message: ContractMetadata.Message + args: any[] +} + +export function callTx>(client: Client_) { + return >(_props: Props) => { + const { + sender, + contractAddress, + value, + contractMetadata, + message, + args, + } = _props as Z.Rec$Access + const $message_ = $message(contractMetadata, message) + const data = Z.ls($message_, message, args).next(([{ $args }, message, args]) => { + return $args.encode([U.hex.decode(message.selector), ...args]) + }) + const txValue = Z.ls( + stateContractsApiCall(client)({ + sender, + contractAddress, + value, + data, + }), + contractAddress, + value, + data, + ) + .next(([{ gasRequired }, contractAddress, value, data]) => { + return { + type: "call", + dest: MultiAddress.Id(contractAddress), + value: value ?? 0n, + data, + gasLimit: gasRequired, + storageDepositLimit: undefined, + } + }) + return extrinsic(client)({ + sender, + call: Z.rec({ type: "Contracts", value: txValue }), + }) + } +} + +export interface ContractsApiCallProps { + sender: MultiAddress + contractAddress: Uint8Array + value?: bigint + data: Uint8Array +} + +export function stateContractsApiCall>(client: Client_) { + return >(_props: Props) => { + const key = Z.rec(_props as Z.Rec$Access).next( + ({ sender, contractAddress, value, data }) => { + return U.hex.encode($contractsApiCallArgs.encode([ + sender.value!, // TODO: grab public key in cases where we're not accepting multi? + contractAddress, + value ?? 0n, + undefined, + undefined, + data, + ])) + }, + ) + return state + .call(client)("ContractsApi_call", key) + .next((result) => { + return $contractsApiCallResult.decode(U.hex.decode(result)) + }) + } +} + +interface MessageCodecs { + $args: $.Codec<[Uint8Array, ...unknown[]]> + $result: $.Codec +} + +function $message( + metadata: Z.$, + message: Z.$, +): Z.$ { + return Z.ls(metadata, message).next(([metadata, message]) => { + const deriveCodec = DeriveCodec(metadata.V3.types) + return { + $args: $.tuple( + // message selector + $.sizedUint8Array(U.hex.decode(message.selector).length), + // message args + ...message.args.map((arg) => deriveCodec(arg.type.type)), + ), + $result: message.returnType !== null + ? deriveCodec(message.returnType.type) + : $.constant(null), + } + }) +} diff --git a/effects/contracts/instantiate.ts b/effects/contracts/instantiate.ts index 622fd8157..90ff31a33 100644 --- a/effects/contracts/instantiate.ts +++ b/effects/contracts/instantiate.ts @@ -57,8 +57,6 @@ export function instantiateGasEstimate>(client: ) return state .call(client)("ContractsApi_instantiate", key) - .next((encodedResponse) => { - return $contractsApiInstantiateResult.decode(U.hex.decode(encodedResponse)) - }) + .next((result) => $contractsApiInstantiateResult.decode(U.hex.decode(result))) } } diff --git a/effects/contracts/mod.ts b/effects/contracts/mod.ts index 5eaee97b9..a4a0a9be5 100644 --- a/effects/contracts/mod.ts +++ b/effects/contracts/mod.ts @@ -1 +1,2 @@ +export * from "./call.ts" export * from "./instantiate.ts" diff --git a/examples/smart_contract.ts b/examples/smart_contract.ts index 9245cb639..b8a299d09 100644 --- a/examples/smart_contract.ts +++ b/examples/smart_contract.ts @@ -4,11 +4,6 @@ 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, - ContractMetadata, -} from "../frame_metadata/Contract.ts" const configFile = path.join( path.dirname(path.fromFileUrl(import.meta.url)), @@ -72,77 +67,53 @@ const contractAddress = U.throwIfError( .run(), ) -interface MessageCodecs { - $args: C.$.Codec<[Uint8Array, ...unknown[]]> - $result: C.$.Codec -} - class Contract> { - readonly deriveCodec readonly $events - readonly $messageByLabel constructor( readonly client: Client, - readonly metadata: C.M.ContractMetadata, + readonly contractMetadata: C.M.ContractMetadata, readonly contractAddress: Uint8Array, ) { - this.deriveCodec = C.M.DeriveCodec(metadata.V3.types) - this.$messageByLabel = metadata.V3.spec.messages.reduce>( - (acc, message) => { - acc[message.label] = this.#getMessageCodecs(message) - return acc - }, - {}, - ) + const deriveCodec = C.M.DeriveCodec(contractMetadata.V3.types) this.$events = C.$.taggedUnion( "type", - metadata.V3.spec.events + contractMetadata.V3.spec.events .map((e) => [ e.label, - ["value", C.$.tuple(...e.args.map((a) => this.deriveCodec(a.type.type)))], + ["value", C.$.tuple(...e.args.map((a) => deriveCodec(a.type.type)))], ]), ) } - query( - origin: Uint8Array, + call( + sender: C.MultiAddress, messageLabel: string, args: Args, ) { const message = this.#getMessageByLabel(messageLabel)! - const { $result } = this.#getMessageCodecByLabel(messageLabel) - return this.#call(origin, message, args).access("result").next((result) => - $result.decode(result.data) - ) + return C.contracts.call(client)({ + sender, + contractAddress: this.contractAddress, + contractMetadata: this.contractMetadata, + message, + args, + }) } - tx( - origin: Uint8Array, + callTx( + sender: C.MultiAddress, messageLabel: string, args: Args, sign: C.Z.$, ) { const message = this.#getMessageByLabel(messageLabel)! - const { $args } = this.#getMessageCodecByLabel(messageLabel) - const data = $args.encode([U.hex.decode(message.selector), ...args]) - const gasRequired = this.#call(origin, message, args).access("gasRequired") - const value = gasRequired.next((gasRequired) => { - return { - type: "call", - dest: C.MultiAddress.Id(this.contractAddress), - value: 0n, - data, - gasLimit: gasRequired, - storageDepositLimit: undefined, - } - }) - const tx = C.extrinsic(client)({ - sender: C.MultiAddress.Id(origin), - call: C.Z.rec({ - type: "Contracts", - value, - }), + const tx = C.contracts.callTx(client)({ + sender, + contractAddress: this.contractAddress, + contractMetadata: this.contractMetadata, + message, + args, }) .signed(sign) const finalizedIn = tx.watch(({ end }) => @@ -155,7 +126,6 @@ class Contract> { return } ) - // FIXME: extract into a contract effect util return C.Z.ls(finalizedIn, C.events(tx, finalizedIn)) .next(([finalizedIn, events]) => { const contractEvents: any[] = events @@ -167,48 +137,10 @@ class Contract> { }) } - #call( - origin: Uint8Array, - message: ContractMetadata.Message, - args: Args, - ) { - const { $args } = this.#getMessageCodecByLabel(message.label) - const data = $args.encode([U.hex.decode(message.selector), ...args]) - const callData = U.hex.encode($contractsApiCallArgs.encode([ - origin, - this.contractAddress, - 0n, - undefined, - undefined, - data, - ])) - return C.state - .call(client)("ContractsApi_call", callData) - .next((encodedResponse) => $contractsApiCallResult.decode(U.hex.decode(encodedResponse))) - } - // TODO: codegen each contract message as a method #getMessageByLabel(label: string) { - return this.metadata.V3.spec.messages.find((c) => c.label === label) - } - - #getMessageCodecs(message: C.M.ContractMetadata.Message): MessageCodecs { - return { - $args: C.$.tuple( - // message selector - C.$.sizedUint8Array(U.hex.decode(message.selector).length), - // message args - ...message.args.map((arg) => this.deriveCodec(arg.type.type)), - ), - $result: message.returnType !== null - ? this.deriveCodec(message.returnType.type) - : C.$.constant(null), - } - } - - #getMessageCodecByLabel(label: string) { - return this.$messageByLabel[label]! + return this.contractMetadata.V3.spec.messages.find((c) => c.label === label) } } @@ -216,40 +148,44 @@ const prefix = U.throwIfError(await C.const(client)("System", "SS58Prefix").acce console.log("Deployed Contract address", U.ss58.encode(prefix, contractAddress)) const flipperContract = new Contract(T.polkadot, metadata, contractAddress) -console.log(".get", await flipperContract.query(T.alice.publicKey, "get", []).run()) +console.log(".get", await flipperContract.call(T.alice.address, "get", []).run()) console.log( "block hash and events", - U.throwIfError(await flipperContract.tx(T.alice.publicKey, "flip", [], T.alice.sign).run())[0], + U.throwIfError( + await flipperContract.callTx(T.alice.address, "flip", [], T.alice.sign).run(), + )[0], ) -console.log(".get", await flipperContract.query(T.alice.publicKey, "get", []).run()) -console.log(".get_count", await flipperContract.query(T.alice.publicKey, "get_count", []).run()) +console.log(".get", await flipperContract.call(T.alice.address, "get", []).run()) +console.log(".get_count", await flipperContract.call(T.alice.address, "get_count", []).run()) console.log( ".inc block hash", - U.throwIfError(await flipperContract.tx(T.alice.publicKey, "inc", [], T.alice.sign).run())[0], + U.throwIfError(await flipperContract.callTx(T.alice.address, "inc", [], T.alice.sign).run())[0], ) console.log( ".inc block hash", - U.throwIfError(await flipperContract.tx(T.alice.publicKey, "inc", [], T.alice.sign).run())[0], + U.throwIfError(await flipperContract.callTx(T.alice.address, "inc", [], T.alice.sign).run())[0], ) -console.log(".get_count", await flipperContract.query(T.alice.publicKey, "get_count", []).run()) +console.log(".get_count", await flipperContract.call(T.alice.address, "get_count", []).run()) console.log( ".inc_by(3) block hash", - U.throwIfError(await flipperContract.tx(T.alice.publicKey, "inc_by", [3], T.alice.sign).run())[0], + U.throwIfError( + await flipperContract.callTx(T.alice.address, "inc_by", [3], T.alice.sign).run(), + )[0], ) -console.log(".get_count", await flipperContract.query(T.alice.publicKey, "get_count", []).run()) +console.log(".get_count", await flipperContract.call(T.alice.address, "get_count", []).run()) console.log( ".inc_by_with_event(3) contract events", U.throwIfError( - await flipperContract.tx(T.alice.publicKey, "inc_by_with_event", [3], T.alice.sign).run(), + await flipperContract.callTx(T.alice.address, "inc_by_with_event", [3], T.alice.sign).run(), )[2], ) console.log( ".method_returning_tuple(2,true)", - await flipperContract.query(T.alice.publicKey, "method_returning_tuple", [2, true]).run(), + await flipperContract.call(T.alice.address, "method_returning_tuple", [2, true]).run(), ) console.log( ".method_returning_struct(3,false)", - await flipperContract.query(T.alice.publicKey, "method_returning_struct", [3, false]).run(), + await flipperContract.call(T.alice.address, "method_returning_struct", [3, false]).run(), ) await zombienet.close()