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

feat: smart contract with improved dx #462

Merged
merged 21 commits into from
Dec 22, 2022
Merged
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
8 changes: 3 additions & 5 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@
],
"dictionaries": ["project-words"],
"ignorePaths": [
"**/mod.generated.js",
"**/mod_bg.wasm",
"Cargo.lock",
"deno.lock",
"**/*.wasm",
"frame_metadata/raw_erc20_metadata.json",
"target",
"**/__snapshots__/*.snap",
"codegen/_output"
"**/*.contract",
"examples/smart_contract/metadata.json"
]
}
2 changes: 1 addition & 1 deletion deps/std/encoding/base58.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "https://deno.land/std@0.154.0/encoding/base58.ts"
export * from "https://deno.land/std@0.168.0/encoding/base58.ts"
149 changes: 149 additions & 0 deletions effects/contracts/call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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 }, { selector }, args]) =>
$args.encode([U.hex.decode(selector), ...args])
)
return Z.ls(
stateContractsApiCall(client)({
sender,
contractAddress,
data,
}),
$message_,
)
.next(([{ result: { data } }, { $result }]) => $result.decode(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 }, { selector }, args]) =>
$args.encode([U.hex.decode(selector), ...args])
)
const txValue = Z.ls(
stateContractsApiCall(client)({
sender,
contractAddress,
value,
data,
}),
contractAddress,
value,
data,
)
.next(([{ gasRequired }, contractAddress, value, data]) => (
{
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 }) =>
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),
}
})
}
28 changes: 28 additions & 0 deletions effects/contracts/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as $ from "../../deps/scale.ts"
import * as Z from "../../deps/zones.ts"
import { ContractMetadata, DeriveCodec } from "../../frame_metadata/mod.ts"
import { ExtrinsicEvent } from "../events.ts"

export function events(
contractMetadata: Z.$<ContractMetadata>,
events: Z.$<ExtrinsicEvent[]>,
) {
const $events = Z.ls(contractMetadata).next(([contractMetadata]) => {
return $.taggedUnion(
"type",
contractMetadata.V3.spec.events
.map((e) => [
e.label,
[
"value",
$.tuple(...e.args.map((a) => DeriveCodec(contractMetadata.V3.types)(a.type.type))),
],
]),
)
})
return Z.ls(events, $events).next(([events, $events]) => {
return events
.filter((e) => e.event?.type === "Contracts" && e.event?.value?.type === "ContractEmitted")
.map((e) => $events.decode(e.event?.value.data))
})
}
62 changes: 62 additions & 0 deletions effects/contracts/instantiate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as Z from "../../deps/zones.ts"
import {
$contractsApiInstantiateArgs,
$contractsApiInstantiateResult,
ContractMetadata,
} from "../../frame_metadata/Contract.ts"
import { 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 InstantiateProps {
sender: MultiAddress
code: Uint8Array
constructorMetadata: ContractMetadata.Constructor
salt: Uint8Array
}

export function instantiate<Client_ extends Z.Effect<Client>>(client: Client_) {
return <Props extends Z.Rec$<InstantiateProps>>(_props: Props) => {
const { code, constructorMetadata, sender } = _props as Z.Rec$Access<Props>
const value = Z.ls(_props.salt, instantiateGasEstimate(client)(_props), constructorMetadata)
.next(([salt, { gasRequired }, { selector }]) => {
// the contract address derived from the code hash and the salt
return {
type: "instantiateWithCode",
value: 0n,
gasLimit: gasRequired,
storageDepositLimit: undefined,
code,
data: U.hex.decode(selector),
salt,
}
})
return extrinsic(client)({
sender,
call: Z.rec({ type: "Contracts", value }),
})
}
}

export function instantiateGasEstimate<Client_ extends Z.Effect<Client>>(client: Client_) {
return <Props extends Z.Rec$<InstantiateProps>>(_props: Props) => {
const key = Z.rec(_props as Z.Rec$Access<Props>).next(
({ code, constructorMetadata, sender, salt }) => {
return U.hex.encode($contractsApiInstantiateArgs.encode([
sender.value!, // TODO: grab public key in cases where we're not accepting multi?
0n,
undefined,
undefined,
{ type: "Upload", value: code },
U.hex.decode(constructorMetadata.selector),
salt,
]))
},
)
return state
.call(client)("ContractsApi_instantiate", key)
.next((result) => $contractsApiInstantiateResult.decode(U.hex.decode(result)))
}
}
3 changes: 3 additions & 0 deletions effects/contracts/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./call.ts"
export * from "./events.ts"
export * from "./instantiate.ts"
7 changes: 6 additions & 1 deletion effects/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function events<Extrinsic extends SignedExtrinsic, FinalizedHash extends
}, k1_)
const events = entryRead(client)("System", "Events", [], finalizedHash)
.access("value")
.as<{ phase: { value: number } }[]>()
.as<ExtrinsicEvent[]>()
return Z
.ls(idx, events)
.next(([idx, events]) => {
Expand All @@ -33,3 +33,8 @@ export function events<Extrinsic extends SignedExtrinsic, FinalizedHash extends
})
}, k2_)
}

export interface ExtrinsicEvent {
event?: Record<string, any>
phase: { value: number }
}
1 change: 1 addition & 0 deletions effects/mod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./blockWatch.ts"
export * from "./const.ts"
export * as contracts from "./contracts/mod.ts"
export * from "./entryRead.ts"
export * from "./entryWatch.ts"
export * from "./events.ts"
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
1 change: 1 addition & 0 deletions examples/.ignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod.ts
multisig_transfer.ts
smart_contract.ts
xcm_teleport_assets.ts
Loading