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

Commit

Permalink
feat: add contract call effect
Browse files Browse the repository at this point in the history
  • Loading branch information
kratico committed Dec 19, 2022
1 parent 46ae535 commit 1669134
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 105 deletions.
152 changes: 152 additions & 0 deletions effects/contracts/call.ts
Original file line number Diff line number Diff line change
@@ -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_ extends Z.Effect<Client>>(client: Client_) {
return <Props extends Z.Rec$<CallProps>>(_props: Props) => {
const {
sender,
contractAddress,
contractMetadata,
message,
args,
} = _props as Z.Rec$Access<Props>
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_ extends Z.Effect<Client>>(client: Client_) {
return <Props extends Z.Rec$<CallTxProps>>(_props: Props) => {
const {
sender,
contractAddress,
value,
contractMetadata,
message,
args,
} = _props as Z.Rec$Access<Props>
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_ extends Z.Effect<Client>>(client: Client_) {
return <Props extends Z.Rec$<ContractsApiCallProps>>(_props: Props) => {
const key = Z.rec(_props as Z.Rec$Access<Props>).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<any>
}

function $message(
metadata: Z.$<ContractMetadata>,
message: Z.$<ContractMetadata.Message>,
): Z.$<MessageCodecs> {
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),
}
})
}
4 changes: 1 addition & 3 deletions effects/contracts/instantiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ export function instantiateGasEstimate<Client_ extends Z.Effect<Client>>(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)))
}
}
1 change: 1 addition & 0 deletions effects/contracts/mod.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./call.ts"
export * from "./instantiate.ts"
140 changes: 38 additions & 102 deletions examples/smart_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -72,77 +67,53 @@ const contractAddress = U.throwIfError(
.run(),
)

interface MessageCodecs {
$args: C.$.Codec<[Uint8Array, ...unknown[]]>
$result: C.$.Codec<any>
}

class Contract<Client extends C.Z.Effect<C.rpc.Client>> {
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<Record<string, MessageCodecs>>(
(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<Args extends any[]>(
origin: Uint8Array,
call<Args extends any[]>(
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<Args extends unknown[]>(
origin: Uint8Array,
callTx<Args extends unknown[]>(
sender: C.MultiAddress,
messageLabel: string,
args: Args,
sign: C.Z.$<C.M.Signer>,
) {
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 }) =>
Expand All @@ -155,7 +126,6 @@ class Contract<Client extends C.Z.Effect<C.rpc.Client>> {
return
}
)
// FIXME: extract into a contract effect util
return C.Z.ls(finalizedIn, C.events(tx, finalizedIn))
.next(([finalizedIn, events]) => {
const contractEvents: any[] = events
Expand All @@ -167,89 +137,55 @@ class Contract<Client extends C.Z.Effect<C.rpc.Client>> {
})
}

#call<Args extends any[]>(
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)
}
}

const prefix = U.throwIfError(await C.const(client)("System", "SS58Prefix").access("value").run())
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()

0 comments on commit 1669134

Please sign in to comment.