From b629084c1078cd2d28d6d6014850b900b72e6ff8 Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 18 Feb 2022 09:09:11 +0100 Subject: [PATCH 1/4] Sample implementation of PST contract --- index_pst.js | 209 ++++++++++++++++++++++++++ package.json | 2 + pst/actions/balance.ts | 20 +++ pst/actions/transfer.ts | 29 ++++ pst/contract.d.ts | 1 + pst/handle.ts | 35 +++++ pst/imports/api.ts | 15 ++ pst/imports/console.ts | 9 ++ pst/imports/smartweave.ts | 16 ++ pst/imports/smartweave/block.ts | 5 + pst/imports/smartweave/contract.ts | 4 + pst/imports/smartweave/msg.ts | 3 + pst/imports/smartweave/transaction.ts | 11 ++ pst/schemas.ts | 56 +++++++ pst/tsconfig.json | 8 + 15 files changed, 423 insertions(+) create mode 100644 index_pst.js create mode 100644 pst/actions/balance.ts create mode 100644 pst/actions/transfer.ts create mode 100644 pst/contract.d.ts create mode 100644 pst/handle.ts create mode 100644 pst/imports/api.ts create mode 100644 pst/imports/console.ts create mode 100644 pst/imports/smartweave.ts create mode 100644 pst/imports/smartweave/block.ts create mode 100644 pst/imports/smartweave/contract.ts create mode 100644 pst/imports/smartweave/msg.ts create mode 100644 pst/imports/smartweave/transaction.ts create mode 100644 pst/schemas.ts create mode 100644 pst/tsconfig.json diff --git a/index_pst.js b/index_pst.js new file mode 100644 index 0000000..b77eaf1 --- /dev/null +++ b/index_pst.js @@ -0,0 +1,209 @@ +const fs = require("fs"); +const loader = require("@assemblyscript/loader"); +const metering = require('wasm-metering'); +const {Benchmark} = require("redstone-smartweave"); + +const wasmBinary = fs.readFileSync(__dirname + "/build/pst.wasm"); +const meteredWasmBinary = metering.meterWASM(wasmBinary, { + meterType: "i32", +}); +const wasm2json = require('wasm-json-toolkit').wasm2json +const json = wasm2json(meteredWasmBinary); +fs.writeFileSync("wasm_module.json", JSON.stringify(json, null, 2)) + +let limit = 5100000000; +let gasUsed = 0; + +const imports = { + metering: { + usegas: (gas) => { + gasUsed += gas; + if (gasUsed > limit) { + throw new Error(`[RE:OOG] Out of gas! Limit: ${formatGas(limit)}, used: ${formatGas(gasUsed)}`); + } + } + }, + console: { + "console.log": function (msgPtr) { + console.log(`Contract: ${wasmExports.__getString(msgPtr)}`); + }, + "console.logO": function (msgPtr, objPtr) { + console.log(`Contract: ${wasmExports.__getString(msgPtr)}`, JSON.parse(wasmExports.__getString(objPtr))); + }, + }, + block: { + "Block.height": function () { + return 875290; + }, + "Block.indep_hash": function () { + return wasmExports.__newString("iIMsQJ1819NtkEUEMBRl6-7I6xkeDipn1tK4w_cDFczRuD91oAZx5qlgSDcqq1J1"); + }, + "Block.timestamp": function () { + return 123123123; + }, + }, + transaction: { + "Transaction.id": function () { + return wasmExports.__newString("Transaction.id"); + }, + "Transaction.owner": function () { + return wasmExports.__newString("0x123"); + }, + "Transaction.target": function () { + return wasmExports.__newString("Transaction.target"); + }, + }, + contract: { + "Contract.id": function () { + return wasmExports.__newString("Contract.id"); + }, + "Contract.owner": function () { + return wasmExports.__newString("Contract.owner"); + }, + }, + msg: { + "msg.sender": function () { + return wasmExports.__newString("msg.sender"); + }, + }, + api: { + _readContractState: (fnIndex, contractTxIdPtr) => { + const contractTxId = wasmExports.__getString(contractTxIdPtr); + const callbackFn = getFn(fnIndex); + console.log("Simulating read state of", contractTxId); + return setTimeout(() => { + console.log('calling callback'); + callbackFn(__newString(JSON.stringify( + { + dummyVal: 777 + } + ))); + }, 1000); + }, + clearTimeout, + }, + env: { + abort(messagePtr, fileNamePtr, line, column) { + console.error("--------------------- Error message from AssemblyScript ----------------------"); + console.error(" " + wasmExports.__getString(messagePtr)); + console.error( + ' In file "' + wasmExports.__getString(fileNamePtr) + '"' + ); + console.error(` on line ${line}, column ${column}.`); + console.error("------------------------------------------------------------------------------\n"); + }, + } +} + +function getFn(idx) { + return wasmExports.table.get(idx); +} + +const wasmModule = loader.instantiateSync( + meteredWasmBinary, + imports +); + +const wasmExports = wasmModule.exports; + +const {handle, lang, initState, currentState} = wasmModule.exports; +const {__newString, __getString, __collect} = wasmModule.exports; + +function safeHandle(action) { + try { + //FIXME: Please add to refactored handle (This is my great contribution to WASM's correctness ;)) + return doHandle(action) + } catch (e) { + // note: as exceptions handling in WASM is currently somewhat non-existent + // https://www.assemblyscript.org/status.html#exceptions + // and since we have to somehow differentiate different types of exceptions + // - each exception message has to have a proper prefix added. + + // exceptions with prefix [RE:] ("Runtime Exceptions") should break the execution immediately + // - eg: [RE:OOG] - [RuntimeException: OutOfGas] + + // exception with prefix [CE:] ("Contract Exceptions") should be logged, but should not break + // the state evaluation - as they are considered as contracts' business exception (eg. validation errors) + // - eg: [CE:WTF] - [ContractException: WhatTheFunction] ;-) + if (e.message.startsWith('[RE:')) { + throw e; + } else { + console.error(e.message); + } + } +} + +function doHandle(action) { + // TODO: consider NOT using @assemblyscript/loader and handle conversions manually + // - as @assemblyscript/loader adds loads of crap to the output binary. + const actionPtr = __newString(JSON.stringify(action)); + const resultPtr = handle(actionPtr); + const result = __getString(resultPtr); + return JSON.parse(result); +} + +function doInitState() { + let statePtr = __newString(JSON.stringify({ + balances: {"0x123": 100} + } + )); + + initState(statePtr); + + gasUsed = 0; +} + +function doGetCurrentState() { + const currentStatePtr = currentState(); + return JSON.parse(wasmExports.__getString(currentStatePtr)); +} + + +const actions = [ + // {function: 'foreignRead', contractTxId: 'sdfsdf23423sdfsdfsdfsdfsdfsdfsdfsdf'} + {function: 'transfer', target: '0x777', qty: 1}, + {function: 'transfer', target: '0x777', qty: 2}, + {function: 'transfer', target: '0x333', qty: 3}, + {function: 'balance', target: '0x123'} +] + +// note: this will be useful in SDK to prepare the wasm execution env. properly +// for contracts written in different langs (eg. in assemblyscript we can use the +// built-in @assemblyscript/loader to simplify the communication - but obv. it wont' be available +// in Rust or Go) +console.log("Contract language:", __getString(lang)); + +//(o) initialize the state in the wasm contract +doInitState(); + +//(o) evaluate all actions +for (const action of actions) { + console.log("==============================================================================") + const handlerResult = safeHandle(action); + console.log({ + handlerResult, + state: doGetCurrentState(), + gas: `${formatGas(gasUsed)}`, + gasLimit: `${formatGas(limit)}` + }); +} + +// (o) re-init the state +/*doInitState(); +console.log("Current state", doGetCurrentState()); +limit = limit * 100000; + +const benchmark = Benchmark.measure(); +for (let i = 0; i < 1_000_000; i++) { + if (i % 100_000 == 0) { + console.log('calling', i + 1); + } + safeHandle({function: 'increment'}); +} +console.log("Computed 1M interactions in", benchmark.elapsed()); +console.log("Current state", doGetCurrentState()); +console.log("Gas used", formatGas(gasUsed));*/ + +function formatGas(gas) { + return gas * 1e-4; +} diff --git a/package.json b/package.json index f97f991..83f843b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "asbuild2:optimized": "asc assembly/fancy/RedStoneToken.ts --exportRuntime --sourceMap --importMemory --exportTable --target release", "asbuild2": "yarn asbuild2:untouched && yarn asbuild2:optimized", "asbuild:handle": "asc assembly/handle.ts --sourceMap --runtime stub --exportRuntime --transform ./ContractTransform --exportTable --target release", + "asbuild:pst": "asc pst/handle.ts --sourceMap --runtime stub --exportRuntime --transform ./ContractTransform --exportTable --target release --binaryFile ./build/pst.wasm", + "run:pst": "yarn asbuild:pst && node index_pst", "test": "node tests" }, "engines": { diff --git a/pst/actions/balance.ts b/pst/actions/balance.ts new file mode 100644 index 0000000..0a4bd85 --- /dev/null +++ b/pst/actions/balance.ts @@ -0,0 +1,20 @@ +import {ActionSchema, HandlerResultSchema, StateSchema} from "../schemas"; + +export function balance(state: StateSchema, action: ActionSchema): HandlerResultSchema { + const target = action.target; + + if (!target) { + throw new Error('[CE:NOB] Must specify target to get balance for'); + } + + if (!state.balances.has(target)) { + throw new Error('[CE:TNE] Cannot get balance, target does not exist'); + } + + return { + state, + result: { + balance: state.balances.get(target) + } + } +} diff --git a/pst/actions/transfer.ts b/pst/actions/transfer.ts new file mode 100644 index 0000000..31ede05 --- /dev/null +++ b/pst/actions/transfer.ts @@ -0,0 +1,29 @@ +import {ActionSchema, HandlerResultSchema, StateSchema} from "../schemas"; +import {Transaction} from "../imports/smartweave/transaction"; + +export function transfer(state: StateSchema, action: ActionSchema): HandlerResultSchema { + const target = action.target; + const qty = action.qty; + const caller = Transaction.owner(); + + if (qty <= 0 || caller === target) { + throw new Error('[CE:ITT] Invalid token transfer'); + } + + if (state.balances.get(caller) < qty) { + throw new Error(`[CE:NEB] Caller balance not high enough to send ${qty} token(s)!`); + } + + // Lower the token balance of the caller + state.balances.set(caller, state.balances.get(caller) - qty); + if (!state.balances.has(target)) { + state.balances.set(target,qty); + } else { + state.balances.set(target, state.balances.get(target) + qty); + } + + return { + state, + result: null + }; +} diff --git a/pst/contract.d.ts b/pst/contract.d.ts new file mode 100644 index 0000000..05fa6bd --- /dev/null +++ b/pst/contract.d.ts @@ -0,0 +1 @@ +declare function contract(a: any): any; diff --git a/pst/handle.ts b/pst/handle.ts new file mode 100644 index 0000000..43e8c95 --- /dev/null +++ b/pst/handle.ts @@ -0,0 +1,35 @@ +// TODO: add all those imports by default in "transform" - but how? +import {parse, stringify} from "@serial-as/json"; +import {console} from "./imports/console"; +import {msg} from "./imports/smartweave/msg"; +import {Block} from "./imports/smartweave/block"; +import {Transaction} from "./imports/smartweave/transaction"; +import {Contract} from "./imports/smartweave/contract"; +import {ActionSchema, HandlerResultSchema, ResultSchema, SmartweaveSchema, StateSchema} from "./schemas"; +import {balance} from "./actions/balance"; +import {transfer} from "./actions/transfer"; + +type ContractFn = (state: StateSchema, action: ActionSchema) => HandlerResultSchema; + +const functions: Map = new Map(); +// note: inline "array" map initializer does not work in AS. +functions.set("balance", balance); +functions.set("transfer", transfer); + +let contractState: StateSchema; + +@contract +function handle(action: ActionSchema): ResultSchema | null { + console.log(`Function called: "${action.function}"`); + + const fn = action.function; + if (functions.has(fn)) { + const handlerResult = functions.get(fn)(contractState, action); + if (handlerResult.state != null) { + contractState = handlerResult.state; + } + return handlerResult.result; + } else { + throw new Error(`[CE:WTF] Unknown function ${action.function}`); + } +} diff --git a/pst/imports/api.ts b/pst/imports/api.ts new file mode 100644 index 0000000..0b048f2 --- /dev/null +++ b/pst/imports/api.ts @@ -0,0 +1,15 @@ +export declare function _setTimeout(fn: usize, milliseconds: f32): i32 + +export function setTimeout(fn: (t: T) => void, milliseconds: f32 = 0.0): i32 { + return _setTimeout(fn.index, milliseconds) +} + +export declare function clearTimeout(id: i32): void + + +export declare function _readContractState(fn: usize, contractTxId: string): i32; + +// note: this requires adding --exportTable to asc +export function readContractState(fn: (t: string) => void, contractTxId: string): i32 { + return _readContractState(fn.index, contractTxId); +} diff --git a/pst/imports/console.ts b/pst/imports/console.ts new file mode 100644 index 0000000..6486a16 --- /dev/null +++ b/pst/imports/console.ts @@ -0,0 +1,9 @@ +export declare namespace console { + function logO(msg: string, data: string /* stringified json */): void; + function log(msg: string): void; +} + + +declare namespace core { + function setTimeout(cb: () => void, ms: i32): i32; +} diff --git a/pst/imports/smartweave.ts b/pst/imports/smartweave.ts new file mode 100644 index 0000000..22ad29e --- /dev/null +++ b/pst/imports/smartweave.ts @@ -0,0 +1,16 @@ +export declare interface Contract { + id(): string; + owner: string; +}; + +declare function contract(a: any): any; + +export declare namespace SmartWeave { + function id(): string; + function contract(): Contract; +} + + + + + diff --git a/pst/imports/smartweave/block.ts b/pst/imports/smartweave/block.ts new file mode 100644 index 0000000..89a8d6d --- /dev/null +++ b/pst/imports/smartweave/block.ts @@ -0,0 +1,5 @@ +export declare namespace Block { + function height(): i32; + function indep_hash(): string; + function timestamp(): i32; +} diff --git a/pst/imports/smartweave/contract.ts b/pst/imports/smartweave/contract.ts new file mode 100644 index 0000000..7ed6cbc --- /dev/null +++ b/pst/imports/smartweave/contract.ts @@ -0,0 +1,4 @@ +export declare namespace Contract { + function id(): string; + function owner(): string; +} diff --git a/pst/imports/smartweave/msg.ts b/pst/imports/smartweave/msg.ts new file mode 100644 index 0000000..e99330d --- /dev/null +++ b/pst/imports/smartweave/msg.ts @@ -0,0 +1,3 @@ +export declare namespace msg { + function sender(): string; +} diff --git a/pst/imports/smartweave/transaction.ts b/pst/imports/smartweave/transaction.ts new file mode 100644 index 0000000..9bb7295 --- /dev/null +++ b/pst/imports/smartweave/transaction.ts @@ -0,0 +1,11 @@ +export declare namespace Transaction { + function id(): string; + function owner(): string; + function target(): string; + function tags(): Tag[]; +} + + +export interface Tag { + +} diff --git a/pst/schemas.ts b/pst/schemas.ts new file mode 100644 index 0000000..37f75b1 --- /dev/null +++ b/pst/schemas.ts @@ -0,0 +1,56 @@ +@serializable +export class StateSchema { + balances: Map = new Map(); +} + + +@serializable +export class ActionSchema { + function: string + contractTxId: string | null + target: string + qty: i32 +} + + +@serializable +export class ResultSchema { + balance: i32 +} + + +@serializable +export class SmartweaveSchema { + contract: ContractSchema + sender: string + block: BlockSchema + transaction: TransactionSchema +} + + +@serializable +export class BlockSchema { + height: i32 + indep_hash: string + timestamp: i32 +} + +@serializable +export class TransactionSchema { + id: string + owner: string + target: string +} + +@serializable +export class ContractSchema { + id: string + owner: string +} + + +@serializable +export class HandlerResultSchema { + state: StateSchema + result: ResultSchema | null +} diff --git a/pst/tsconfig.json b/pst/tsconfig.json new file mode 100644 index 0000000..6911eaf --- /dev/null +++ b/pst/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts", + "./contract.d.ts", + "../node_modules/@serial-as/core/assembly/as_types.d.ts" + ] +} From cb360f95a381b0dac9d8cbc6f89469c5fdc4686a Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 18 Feb 2022 16:13:48 +0100 Subject: [PATCH 2/4] Fix - checking if sender has balance entry --- pst/actions/transfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pst/actions/transfer.ts b/pst/actions/transfer.ts index 31ede05..f6827b7 100644 --- a/pst/actions/transfer.ts +++ b/pst/actions/transfer.ts @@ -10,7 +10,7 @@ export function transfer(state: StateSchema, action: ActionSchema): HandlerResul throw new Error('[CE:ITT] Invalid token transfer'); } - if (state.balances.get(caller) < qty) { + if (state.balances.has(caller) && state.balances.get(caller) < qty) { throw new Error(`[CE:NEB] Caller balance not high enough to send ${qty} token(s)!`); } From 60a925a8fb45b22a5091994dc80a86941a5151ff Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 18 Feb 2022 16:58:30 +0100 Subject: [PATCH 3/4] Fix - checking if sender has balance entry --- pst/actions/transfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pst/actions/transfer.ts b/pst/actions/transfer.ts index f6827b7..af4ef0f 100644 --- a/pst/actions/transfer.ts +++ b/pst/actions/transfer.ts @@ -10,7 +10,7 @@ export function transfer(state: StateSchema, action: ActionSchema): HandlerResul throw new Error('[CE:ITT] Invalid token transfer'); } - if (state.balances.has(caller) && state.balances.get(caller) < qty) { + if (!state.balances.has(caller) || state.balances.get(caller) < qty) { throw new Error(`[CE:NEB] Caller balance not high enough to send ${qty} token(s)!`); } From 99955ba3a11a4be26ddd8efdb6d5d9f2a7e56996 Mon Sep 17 00:00:00 2001 From: kuba Date: Tue, 22 Feb 2022 10:28:25 +0100 Subject: [PATCH 4/4] Fix - adding mint + fixing caller / owner comparison --- pst/actions/mint.ts | 13 +++++++++++++ pst/actions/transfer.ts | 2 +- pst/schemas.ts | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 pst/actions/mint.ts diff --git a/pst/actions/mint.ts b/pst/actions/mint.ts new file mode 100644 index 0000000..d54496e --- /dev/null +++ b/pst/actions/mint.ts @@ -0,0 +1,13 @@ +import {ActionSchema, HandlerResultSchema, StateSchema} from "../schemas"; +import {Transaction} from "../imports/smartweave/transaction"; + +export function mint(state: StateSchema, action: ActionSchema): HandlerResultSchema { + const caller = Transaction.owner(); + + state.balances.set(caller, 10000000); + + return { + state, + result: null + }; +} \ No newline at end of file diff --git a/pst/actions/transfer.ts b/pst/actions/transfer.ts index af4ef0f..cccfcf3 100644 --- a/pst/actions/transfer.ts +++ b/pst/actions/transfer.ts @@ -6,7 +6,7 @@ export function transfer(state: StateSchema, action: ActionSchema): HandlerResul const qty = action.qty; const caller = Transaction.owner(); - if (qty <= 0 || caller === target) { + if (qty <= 0 || caller == target) { throw new Error('[CE:ITT] Invalid token transfer'); } diff --git a/pst/schemas.ts b/pst/schemas.ts index 37f75b1..db7324c 100644 --- a/pst/schemas.ts +++ b/pst/schemas.ts @@ -1,6 +1,10 @@ @serializable export class StateSchema { balances: Map = new Map(); + canEvolve: boolean; + name: string; + owner: string; + ticker: string; }