From 4e00c288d2eb8603324e1358de9e157a92dc47b0 Mon Sep 17 00:00:00 2001 From: Asia Date: Tue, 3 Dec 2024 14:03:24 +0100 Subject: [PATCH 1/3] QuickJSPlugin: read external data --- warp-contracts-plugin-quickjs/package.json | 5 +- .../src/QuickJsHandlerApi.ts | 57 +++++-- .../src/eval/QuickJsEvaluator.ts | 44 +++++- .../src/eval/evalCode/decorator.ts | 13 ++ warp-contracts-plugin-quickjs/src/index.ts | 17 +- warp-contracts-plugin-quickjs/src/utils.ts | 2 +- warp-contracts-plugin-quickjs/yarn.lock | 146 +++++++++++++----- 7 files changed, 213 insertions(+), 71 deletions(-) diff --git a/warp-contracts-plugin-quickjs/package.json b/warp-contracts-plugin-quickjs/package.json index f5e2a61..381e348 100644 --- a/warp-contracts-plugin-quickjs/package.json +++ b/warp-contracts-plugin-quickjs/package.json @@ -1,6 +1,6 @@ { "name": "warp-contracts-plugin-quickjs", - "version": "1.1.12", + "version": "1.1.12-external.0", "license": "MIT", "main": "./build/cjs/index.js", "module": "./build/esm/index.js", @@ -38,10 +38,11 @@ "warp-contracts-plugin-deploy": "^1.0.13" }, "dependencies": { + "@permaweb/aoconnect": "^0.0.62", "@redstone-finance/protocol": "0.5.4", "fast-copy": "^3.0.2", "pngjs": "7.0.0", - "quickjs-emscripten": "^0.29.2", + "quickjs-emscripten": "^0.31.0", "seedrandom": "^3.0.5", "warp-contracts": "1.4.40" }, diff --git a/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts b/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts index e21f30e..d5a7f9f 100644 --- a/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts +++ b/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts @@ -1,5 +1,5 @@ import { QuickJSContext, QuickJSHandle, QuickJSRuntime, QuickJSWASMModule } from 'quickjs-emscripten'; -import {AoInteractionResult, InteractionResult, LoggerFactory, QuickJsPluginMessage, Tag} from 'warp-contracts'; +import { AoInteractionResult, InteractionResult, LoggerFactory, QuickJsPluginMessage, Tag } from 'warp-contracts'; import { errorEvalAndDispose } from './utils'; export class QuickJsHandlerApi { @@ -8,14 +8,18 @@ export class QuickJsHandlerApi { constructor( private readonly vm: QuickJSContext, private readonly runtime: QuickJSRuntime, - private readonly quickJS: QuickJSWASMModule, + private readonly isSourceAsync: boolean ) {} - async handle(message: QuickJsPluginMessage, env: ProcessEnv, state?: State): Promise> { + async handle( + message: QuickJsPluginMessage, + env: ProcessEnv, + state?: State + ): Promise> { if (state) { this.initState(state); } - return this.runContractFunction(message, env); + return await this.runContractFunction(message, env); } initState(state: State): void { @@ -27,13 +31,21 @@ export class QuickJsHandlerApi { } } - private async runContractFunction(message: QuickJsPluginMessage, env: ProcessEnv): InteractionResult { + private async runContractFunction( + message: QuickJsPluginMessage, + env: ProcessEnv + ): InteractionResult { try { - const evalInteractionResult = this.vm.evalCode(`__handleDecorator(${JSON.stringify(message)}, ${JSON.stringify(env)})`); + const evalInteractionResult = this.isSourceAsync + ? await this.evalInteractionAsync(message, env) + : this.evalInteractionSync(message, env); if (evalInteractionResult.error) { errorEvalAndDispose('interaction', this.logger, this.vm, evalInteractionResult.error); } else { const result: AoInteractionResult = this.disposeResult(evalInteractionResult); + if (this.isSourceAsync) { + this.vm.runtime.executePendingJobs(); + } const state = this.currentState() as State; return { Memory: null, @@ -69,6 +81,20 @@ export class QuickJsHandlerApi { } } + private async evalInteractionAsync(message: QuickJsPluginMessage, env: ProcessEnv) { + const result = this.vm.evalCode(`(async () => { + return await __handleDecorator(${JSON.stringify(message)}, ${JSON.stringify(env)}) + })()`); + const promiseHandle = this.vm.unwrapResult(result); + const evalInteractionResult = await this.vm.resolvePromise(promiseHandle); + promiseHandle.dispose(); + return evalInteractionResult; + } + + private evalInteractionSync(message: QuickJsPluginMessage, env: ProcessEnv) { + return this.vm.evalCode(`__handleDecorator(${JSON.stringify(message)}, ${JSON.stringify(env)})`); + } + currentBinaryState(state: State): Buffer { const currentState = state || this.currentState(); return Buffer.from(JSON.stringify(currentState)); @@ -99,19 +125,18 @@ export class QuickJsHandlerApi { resultValue.dispose(); return result; } - } // https://cookbook_ao.g8way.io/concepts/processes.html export type ProcessEnv = { Process: { - Id: string, - Owner: string, - Tags: { name: string, value: string }[] - }, + Id: string; + Owner: string; + Tags: { name: string; value: string }[]; + }; Module: { - Id: string, - Owner: string, - Tags: { name: string, value: string }[] - } -} + Id: string; + Owner: string; + Tags: { name: string; value: string }[]; + }; +}; diff --git a/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts b/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts index 978305f..cf04eeb 100644 --- a/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts +++ b/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts @@ -1,8 +1,8 @@ -import { QuickJSContext } from 'quickjs-emscripten'; -import { LoggerFactory } from 'warp-contracts'; +import { QuickJSContext, QuickJSHandle } from 'quickjs-emscripten'; +import { HandlerBasedContract, LoggerFactory } from 'warp-contracts'; import { PNG } from 'pngjs'; import seedrandom from 'seedrandom'; -import { SignedDataPackage } from "@redstone-finance/protocol" +import { SignedDataPackage } from '@redstone-finance/protocol'; export class QuickJsEvaluator { private readonly logger = LoggerFactory.INST.create('QuickJsEvaluator'); @@ -39,7 +39,7 @@ export class QuickJsEvaluator { const randomHandle = this.vm.newFunction('random', (...args) => { const nativeArgs = args.map(this.vm.dump); const message = nativeArgs[0]; - const uniqueValue = nativeArgs.length > 1 ? "" + nativeArgs[1] : '' + const uniqueValue = nativeArgs.length > 1 ? '' + nativeArgs[1] : ''; const rng = seedrandom(message.Signature + uniqueValue); return this.vm.newNumber(rng()); }); @@ -50,6 +50,42 @@ export class QuickJsEvaluator { randomHandle.dispose(); } + dummyPromiseEval() { + const dummyPromiseEval = this.vm.newFunction('dummyPromise', () => { + const promise = this.vm.newPromise(); + promise.resolve(this.vm.newString('')); + promise.settled.then(this.vm.runtime.executePendingJobs); + return promise.handle; + }); + this.vm.setProp(this.vm.global, 'dummyPromise', dummyPromiseEval); + dummyPromiseEval.dispose(); + } + + evalExternal() { + const readExternalHandle = this.vm.newFunction('readExternal', (processIdHandle, actionHandle) => { + const promise = this.vm.newPromise(); + this.readExternal(processIdHandle, actionHandle).then((result) => { + promise.resolve(this.vm.newString(JSON.stringify(result) || '')); + }); + promise.settled.then(this.vm.runtime.executePendingJobs); + return promise.handle; + }); + this.vm.setProp(this.vm.global, 'readExternal', readExternalHandle); + readExternalHandle.dispose(); + } + + async readExternal(processIdHandle: QuickJSHandle, actionHandle: QuickJSHandle) { + const processId = this.vm.getString(processIdHandle); + const action = this.vm.getString(actionHandle); + const { dryrun } = await import('@permaweb/aoconnect'); + const readRes = await dryrun({ + process: processId, + tags: [{ name: 'Action', value: action }], + data: '1234' + }); + return JSON.parse(readRes.Messages[0].Data); + } + evalRedStone() { const recoverSignerAddressHandle = this.vm.newFunction('recoverSignerAddress', (...args) => { const nativeArgs = args.map(this.vm.dump); diff --git a/warp-contracts-plugin-quickjs/src/eval/evalCode/decorator.ts b/warp-contracts-plugin-quickjs/src/eval/evalCode/decorator.ts index b479c17..1588195 100644 --- a/warp-contracts-plugin-quickjs/src/eval/evalCode/decorator.ts +++ b/warp-contracts-plugin-quickjs/src/eval/evalCode/decorator.ts @@ -1,3 +1,16 @@ +export const asyncDecorateProcessFn = (processCode: string) => { + return ` + ${processCode} + + async function __handleDecorator(message, env) { + ao.init(env); + currentMessage = message; + await handle(currentState, message); + return JSON.stringify(ao.outbox); + } + `; +}; + export const decorateProcessFn = (processCode: string) => { return ` ${processCode} diff --git a/warp-contracts-plugin-quickjs/src/index.ts b/warp-contracts-plugin-quickjs/src/index.ts index a961957..a2a6a7b 100644 --- a/warp-contracts-plugin-quickjs/src/index.ts +++ b/warp-contracts-plugin-quickjs/src/index.ts @@ -15,7 +15,7 @@ import { newVariant } from 'quickjs-emscripten'; import { QuickJsHandlerApi } from './QuickJsHandlerApi'; -import { decorateProcessFn } from './eval/evalCode/decorator'; +import { asyncDecorateProcessFn, decorateProcessFn } from './eval/evalCode/decorator'; import { globals } from './eval/evalCode/globals'; import { WasmModuleConfig } from './types'; import { vmIntrinsics } from './utils'; @@ -37,23 +37,22 @@ export class QuickJsPlugin implements WarpPlugin> { - ({ - QuickJS: this.QuickJS, - runtime: this.runtime, - vm: this.vm - } = await this.configureWasmModule(input.binaryType)); + ({ QuickJS: this.QuickJS, runtime: this.runtime, vm: this.vm } = await this.configureWasmModule(input.binaryType)); this.setRuntimeOptions(); const quickJsEvaluator = new QuickJsEvaluator(this.vm); - + const isSourceAsync = input.contractSource.search('async') > -1; + const processDecorator = isSourceAsync ? asyncDecorateProcessFn : decorateProcessFn; quickJsEvaluator.evalSeedRandom(); quickJsEvaluator.evalGlobalsCode(globals); - quickJsEvaluator.evalHandleFnCode(decorateProcessFn, input.contractSource); + quickJsEvaluator.evalHandleFnCode(processDecorator, input.contractSource); quickJsEvaluator.evalLogging(); quickJsEvaluator.evalPngJS(); quickJsEvaluator.evalRedStone(); + quickJsEvaluator.evalExternal(); + quickJsEvaluator.dummyPromiseEval(); - return new QuickJsHandlerApi(this.vm, this.runtime, this.QuickJS); + return new QuickJsHandlerApi(this.vm, this.runtime, isSourceAsync); } setRuntimeOptions() { diff --git a/warp-contracts-plugin-quickjs/src/utils.ts b/warp-contracts-plugin-quickjs/src/utils.ts index ba16ee6..0caf54d 100644 --- a/warp-contracts-plugin-quickjs/src/utils.ts +++ b/warp-contracts-plugin-quickjs/src/utils.ts @@ -23,7 +23,7 @@ export const vmIntrinsics = { ...DefaultIntrinsics, Date: false, Proxy: false, - Promise: false, + Promise: true, MapSet: false, BigFloat: false, BigInt: true, diff --git a/warp-contracts-plugin-quickjs/yarn.lock b/warp-contracts-plugin-quickjs/yarn.lock index 22ba2a9..462b602 100644 --- a/warp-contracts-plugin-quickjs/yarn.lock +++ b/warp-contracts-plugin-quickjs/yarn.lock @@ -1046,10 +1046,10 @@ resolved "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.28.0.tgz" integrity sha512-OzEGuyeJuOzIbdMkFiLrTuKrvSAzksp+N1yfBrTimnApb/il6nH/UeontqLeEFm++iH9xWqYNFuwVZ/R0GeNEg== -"@jitl/quickjs-ffi-types@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.29.2.tgz#91aaa6be43471b5c8fc3a85f94582c86d345ba2e" - integrity sha512-069uQTiEla2PphXg6UpyyJ4QXHkTj3S9TeXgaMCd8NDYz3ODBw5U/rkg6fhuU8SMpoDrWjEzybmV5Mi2Pafb5w== +"@jitl/quickjs-ffi-types@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.31.0.tgz#1f5cbec1c1ec03eba860842ea59914db3cf3c69a" + integrity sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw== "@jitl/quickjs-singlefile-mjs-release-sync@^0.28.0": version "0.28.0" @@ -1058,33 +1058,33 @@ dependencies: "@jitl/quickjs-ffi-types" "0.28.0" -"@jitl/quickjs-wasmfile-debug-asyncify@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.29.2.tgz#ddf58d1ab90b94a70a73b93f95c4e17f16d0a2a9" - integrity sha512-YdRw2414pFkxzyyoJGv81Grbo9THp/5athDMKipaSBNNQvFE9FGRrgE9tt2DT2mhNnBx1kamtOGj0dX84Yy9bg== +"@jitl/quickjs-wasmfile-debug-asyncify@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.31.0.tgz#0a707fe7ed25e0d5986c8346ae02788a648c9c63" + integrity sha512-YkdzQdr1uaftFhgEnTRjTTZHk2SFZdpWO7XhOmRVbi6CEVsH9g5oNF8Ta1q3OuSJHRwwT8YsuR1YzEiEIJEk6w== dependencies: - "@jitl/quickjs-ffi-types" "0.29.2" + "@jitl/quickjs-ffi-types" "0.31.0" -"@jitl/quickjs-wasmfile-debug-sync@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.29.2.tgz#b1a9c0f9371de2e8e67f8d025c0d9c9b7360f175" - integrity sha512-VgisubjyPMWEr44g+OU0QWGyIxu7VkApkLHMxdORX351cw22aLTJ+Z79DJ8IVrTWc7jh4CBPsaK71RBQDuVB7w== +"@jitl/quickjs-wasmfile-debug-sync@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.31.0.tgz#aac7f901c4df7fdf3aea456c66dbf38f48b0564e" + integrity sha512-8XvloaaWBONqcHXYs5tWOjdhQVxzULilIfB2hvZfS6S+fI4m2+lFiwQy7xeP8ExHmiZ7D8gZGChNkdLgjGfknw== dependencies: - "@jitl/quickjs-ffi-types" "0.29.2" + "@jitl/quickjs-ffi-types" "0.31.0" -"@jitl/quickjs-wasmfile-release-asyncify@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.29.2.tgz#51c23bfd93d56d9eaab47ee937838c91376b093a" - integrity sha512-sf3luCPr8wBVmGV6UV8Set+ie8wcO6mz5wMvDVO0b90UVCKfgnx65A1JfeA+zaSGoaFyTZ3sEpXSGJU+6qJmLw== +"@jitl/quickjs-wasmfile-release-asyncify@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.31.0.tgz#00b11f65b540e7c9472bd08fb499adac77f4a227" + integrity sha512-uz0BbQYTxNsFkvkurd7vk2dOg57ElTBLCuvNtRl4rgrtbC++NIndD5qv2+AXb6yXDD3Uy1O2PCwmoaH0eXgEOg== dependencies: - "@jitl/quickjs-ffi-types" "0.29.2" + "@jitl/quickjs-ffi-types" "0.31.0" -"@jitl/quickjs-wasmfile-release-sync@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.29.2.tgz#7a9a055739bfc6261203701a46e2c627cfabe6f2" - integrity sha512-UFIcbY3LxBRUjEqCHq3Oa6bgX5znt51V5NQck8L2US4u989ErasiMLUjmhq6UPC837Sjqu37letEK/ZpqlJ7aA== +"@jitl/quickjs-wasmfile-release-sync@0.31.0": + version "0.31.0" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.31.0.tgz#66939a090f4a5132e25e15b76e76d09c25f92ea6" + integrity sha512-hYduecOByj9AsAfsJhZh5nA6exokmuFC8cls39+lYmTCGY51bgjJJJwReEu7Ff7vBWaQCL6TeDdVlnp2WYz0jw== dependencies: - "@jitl/quickjs-ffi-types" "0.29.2" + "@jitl/quickjs-ffi-types" "0.31.0" "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" @@ -1152,6 +1152,35 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@permaweb/ao-scheduler-utils@~0.0.25": + version "0.0.25" + resolved "https://registry.yarnpkg.com/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.25.tgz#50c96a411d59f58010ace322d6abd3cd5e11e4cd" + integrity sha512-b0UYSTgnLMIYLScrfNBgcqK7ZMmd78L3J0Jz4RIsIq2P5PtkdRqQ7fYqLlltg7bD1f3dvl4TkO1925ED4ei7LA== + dependencies: + lru-cache "^10.2.2" + ramda "^0.30.0" + zod "^3.23.5" + +"@permaweb/aoconnect@^0.0.62": + version "0.0.62" + resolved "https://registry.yarnpkg.com/@permaweb/aoconnect/-/aoconnect-0.0.62.tgz#404d9f506b70385027447a9bd7d3d51b93104ce2" + integrity sha512-e42yASru6ze09rugKzi2yD3E57OK0xSbjDM5TI7gKnDVMo8JHweiLCntglItJ44vuNUA7Sdool83v4bmEohaZw== + dependencies: + "@permaweb/ao-scheduler-utils" "~0.0.25" + "@permaweb/protocol-tag-utils" "~0.0.2" + buffer "^6.0.3" + debug "^4.3.7" + hyper-async "^1.1.2" + mnemonist "^0.39.8" + ramda "^0.30.1" + warp-arbundles "^1.0.4" + zod "^3.23.8" + +"@permaweb/protocol-tag-utils@~0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@permaweb/protocol-tag-utils/-/protocol-tag-utils-0.0.2.tgz#c8a2eddf7da15d70a6e60aecff839730cb59aee3" + integrity sha512-2IiKu71W7pkHKIzxabCGQ5q8DSppZaE/sPcPF2hn+OWwfe04M7b5X5LHRXQNPRuxHWuioieGdPQb3F7apOlffQ== + "@randlabs/communication-bridge@1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@randlabs/communication-bridge/-/communication-bridge-1.0.1.tgz" @@ -2473,6 +2502,13 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" +debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + dedent@^1.0.0: version "1.5.1" resolved "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz" @@ -3276,6 +3312,11 @@ human-signals@^5.0.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +hyper-async@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/hyper-async/-/hyper-async-1.1.2.tgz#b9a83be36e726bface6f4a5b84f1a1a25bf19e6a" + integrity sha512-cnpOgKa+5FZOaccTtjduac1FrZuSc38/ftCp3vYJdUMt+7c+uvGDKLDK4MTNK8D3aFjIeveVrPcSgUPvzZLopg== + iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -4087,6 +4128,11 @@ loupe@^2.3.6, loupe@^2.3.7: dependencies: get-func-name "^2.0.1" +lru-cache@^10.2.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" @@ -4261,6 +4307,13 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.2" +mnemonist@^0.39.8: + version "0.39.8" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.8.tgz#9078cd8386081afd986cca34b52b5d84ea7a4d38" + integrity sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ== + dependencies: + obliterator "^2.0.1" + module-error@^1.0.1, module-error@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz" @@ -4271,7 +4324,7 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4485,6 +4538,11 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" +obliterator@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" + integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== + once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -4809,23 +4867,28 @@ queue-microtask@^1.2.2, queue-microtask@^1.2.3: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quickjs-emscripten-core@0.29.2: - version "0.29.2" - resolved "https://registry.yarnpkg.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.29.2.tgz#ad9bef103fce7d8ba879510905824e33119a62f3" - integrity sha512-jEAiURW4jGqwO/fW01VwlWqa2G0AJxnN5FBy1xnVu8VIVhVhiaxUfCe+bHqS6zWzfjFm86HoO40lzpteusvyJA== +quickjs-emscripten-core@0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.31.0.tgz#22cbccec207adb6ce486ec1e90730499815e3e15" + integrity sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ== dependencies: - "@jitl/quickjs-ffi-types" "0.29.2" + "@jitl/quickjs-ffi-types" "0.31.0" -quickjs-emscripten@^0.29.2: - version "0.29.2" - resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.29.2.tgz#da6c7afb543cc5b4854eaca457778cfae5069f8b" - integrity sha512-SlvkvyZgarReu2nr4rkf+xz1vN0YDUz7sx4WHz8LFtK6RNg4/vzAGcFjE7nfHYBEbKrzfIWvKnMnxZkctQ898w== +quickjs-emscripten@^0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.31.0.tgz#6dbd7dbb5ddf84cf3eb28c3d1411a8300affe3a9" + integrity sha512-K7Yt78aRPLjPcqv3fIuLW1jW3pvwO21B9pmFOolsjM/57ZhdVXBr51GqJpalgBlkPu9foAvhEAuuQPnvIGvLvQ== dependencies: - "@jitl/quickjs-wasmfile-debug-asyncify" "0.29.2" - "@jitl/quickjs-wasmfile-debug-sync" "0.29.2" - "@jitl/quickjs-wasmfile-release-asyncify" "0.29.2" - "@jitl/quickjs-wasmfile-release-sync" "0.29.2" - quickjs-emscripten-core "0.29.2" + "@jitl/quickjs-wasmfile-debug-asyncify" "0.31.0" + "@jitl/quickjs-wasmfile-debug-sync" "0.31.0" + "@jitl/quickjs-wasmfile-release-asyncify" "0.31.0" + "@jitl/quickjs-wasmfile-release-sync" "0.31.0" + quickjs-emscripten-core "0.31.0" + +ramda@^0.30.0, ramda@^0.30.1: + version "0.30.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.1.tgz#7108ac95673062b060025052cd5143ae8fc605bf" + integrity sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw== randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" @@ -5837,3 +5900,8 @@ zip-stream@^4.1.0: archiver-utils "^3.0.4" compress-commons "^4.1.2" readable-stream "^3.6.0" + +zod@^3.23.5, zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== From b8cb149f9c9bc612900d525a17c99c764ce51f7a Mon Sep 17 00:00:00 2001 From: Asia Date: Wed, 4 Dec 2024 13:40:21 +0100 Subject: [PATCH 2/3] review fixes --- warp-contracts-plugin-quickjs/package.json | 2 +- .../src/QuickJsHandlerApi.ts | 7 +- .../src/eval/QuickJsEvaluator.ts | 39 +++++++-- warp-contracts-plugin-quickjs/src/index.ts | 21 ++--- warp-contracts-plugin-quickjs/src/utils.ts | 12 ++- .../tools/quickjs-promise.mjs | 83 +++++++++++++++++++ 6 files changed, 137 insertions(+), 27 deletions(-) create mode 100644 warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs diff --git a/warp-contracts-plugin-quickjs/package.json b/warp-contracts-plugin-quickjs/package.json index 381e348..2135763 100644 --- a/warp-contracts-plugin-quickjs/package.json +++ b/warp-contracts-plugin-quickjs/package.json @@ -1,6 +1,6 @@ { "name": "warp-contracts-plugin-quickjs", - "version": "1.1.12-external.0", + "version": "1.1.13", "license": "MIT", "main": "./build/cjs/index.js", "module": "./build/esm/index.js", diff --git a/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts b/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts index d5a7f9f..b7744c1 100644 --- a/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts +++ b/warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts @@ -1,4 +1,4 @@ -import { QuickJSContext, QuickJSHandle, QuickJSRuntime, QuickJSWASMModule } from 'quickjs-emscripten'; +import { QuickJSContext, QuickJSHandle, QuickJSRuntime } from 'quickjs-emscripten'; import { AoInteractionResult, InteractionResult, LoggerFactory, QuickJsPluginMessage, Tag } from 'warp-contracts'; import { errorEvalAndDispose } from './utils'; @@ -19,7 +19,7 @@ export class QuickJsHandlerApi { if (state) { this.initState(state); } - return await this.runContractFunction(message, env); + return this.runContractFunction(message, env); } initState(state: State): void { @@ -43,9 +43,6 @@ export class QuickJsHandlerApi { errorEvalAndDispose('interaction', this.logger, this.vm, evalInteractionResult.error); } else { const result: AoInteractionResult = this.disposeResult(evalInteractionResult); - if (this.isSourceAsync) { - this.vm.runtime.executePendingJobs(); - } const state = this.currentState() as State; return { Memory: null, diff --git a/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts b/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts index cf04eeb..0f7d9e7 100644 --- a/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts +++ b/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts @@ -3,6 +3,9 @@ import { HandlerBasedContract, LoggerFactory } from 'warp-contracts'; import { PNG } from 'pngjs'; import seedrandom from 'seedrandom'; import { SignedDataPackage } from '@redstone-finance/protocol'; +import { timeout } from '../utils'; + +const TIMEOUT_ASYNC_OPERATIONS = 10000; export class QuickJsEvaluator { private readonly logger = LoggerFactory.INST.create('QuickJsEvaluator'); @@ -64,9 +67,13 @@ export class QuickJsEvaluator { evalExternal() { const readExternalHandle = this.vm.newFunction('readExternal', (processIdHandle, actionHandle) => { const promise = this.vm.newPromise(); - this.readExternal(processIdHandle, actionHandle).then((result) => { - promise.resolve(this.vm.newString(JSON.stringify(result) || '')); - }); + this.readExternal(processIdHandle, actionHandle) + .then((result) => { + promise.resolve(this.vm.newString(result) || ''); + }) + .catch((error) => { + promise.reject(this.vm.newString(error?.message) || `External read threw an error.`); + }); promise.settled.then(this.vm.runtime.executePendingJobs); return promise.handle; }); @@ -78,12 +85,26 @@ export class QuickJsEvaluator { const processId = this.vm.getString(processIdHandle); const action = this.vm.getString(actionHandle); const { dryrun } = await import('@permaweb/aoconnect'); - const readRes = await dryrun({ - process: processId, - tags: [{ name: 'Action', value: action }], - data: '1234' - }); - return JSON.parse(readRes.Messages[0].Data); + try { + const result = await Promise.race<{ + Output: any; + Messages: any[]; + Spawns: any[]; + Error?: any; + }>([ + dryrun({ + process: processId, + tags: [{ name: 'Action', value: action }], + data: '1234' + }), + timeout(TIMEOUT_ASYNC_OPERATIONS, 'Dryrun operation timed out after 10 seconds') + ]); + return result.Messages[0].Data; + } catch (error) { + const errorMessage = (error as Error).message; + this.logger.error(errorMessage); + throw new Error(errorMessage); + } } evalRedStone() { diff --git a/warp-contracts-plugin-quickjs/src/index.ts b/warp-contracts-plugin-quickjs/src/index.ts index a2a6a7b..457acde 100644 --- a/warp-contracts-plugin-quickjs/src/index.ts +++ b/warp-contracts-plugin-quickjs/src/index.ts @@ -25,8 +25,6 @@ export const DELIMITER = '|||'; const MEMORY_LIMIT = 1024 * 640; const MAX_STACK_SIZE = 1024 * 320; const INTERRUPT_CYCLES = 1024; -const MEMORY_INITIAL_PAGE_SIZE = 64 * 1024; -const MEMORY_MAXIMUM_PAGE_SIZE = 2048; export class QuickJsPlugin implements WarpPlugin>> { private readonly logger = LoggerFactory.INST.create('QuickJsPlugin'); @@ -37,11 +35,10 @@ export class QuickJsPlugin implements WarpPlugin> { - ({ QuickJS: this.QuickJS, runtime: this.runtime, vm: this.vm } = await this.configureWasmModule(input.binaryType)); + const isSourceAsync = input.contractSource.search('async') > -1; + ({ QuickJS: this.QuickJS, runtime: this.runtime, vm: this.vm } = await this.configureWasmModule(isSourceAsync)); this.setRuntimeOptions(); - const quickJsEvaluator = new QuickJsEvaluator(this.vm); - const isSourceAsync = input.contractSource.search('async') > -1; const processDecorator = isSourceAsync ? asyncDecorateProcessFn : decorateProcessFn; quickJsEvaluator.evalSeedRandom(); quickJsEvaluator.evalGlobalsCode(globals); @@ -49,9 +46,10 @@ export class QuickJsPlugin implements WarpPlugin implements WarpPlugin { + async configureWasmModule(isSourceAsync: boolean): Promise { try { const initialWasmMemory = new WebAssembly.Memory({ initial: 256, //*65536 @@ -79,7 +77,10 @@ export class QuickJsPlugin implements WarpPlugin { - const error = vm.dump(evalError); + const error = vm.dump(evalError) || vm.getString(evalError); evalError.dispose(); logger.error(`${evalType} eval failed: ${JSON.stringify(error)}`); @@ -23,7 +23,7 @@ export const vmIntrinsics = { ...DefaultIntrinsics, Date: false, Proxy: false, - Promise: true, + Promise: false, MapSet: false, BigFloat: false, BigInt: true, @@ -50,3 +50,11 @@ export const splitBuffer = (buffer: Buffer, delimiter: string) => { splitted.push(buffer.slice(start)); return splitted; }; + +export const timeout = (ms: number, message: string): Promise => { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error(message)); + }, ms); + }); +}; diff --git a/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs b/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs new file mode 100644 index 0000000..b3cd244 --- /dev/null +++ b/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs @@ -0,0 +1,83 @@ +import { newQuickJSWASMModule, newVariant, RELEASE_SYNC } from 'quickjs-emscripten'; +import { dryrun } from '@permaweb/aoconnect'; + +const mem = new WebAssembly.Memory({ + initial: 256, //*65536 + maximum: 2048 //*65536 +}); +const variant = newVariant(RELEASE_SYNC, { + wasmMemory: mem +}); +const QuickJS = await newQuickJSWASMModule(variant); + +const vm = QuickJS.newContext(); + +const timeout = (ms, message) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(new Error(message)); + }, ms); + }); +}; + +const readExternalHandle = vm.newFunction('readExternal', () => { + const promise = vm.newPromise(); + readExternal() + .then((result) => { + promise.resolve(vm.newString(result) || ''); + }) + .catch((error) => { + promise.reject(vm.newString(error.message) || ''); + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; +}); +readExternalHandle.consume((handle) => vm.setProp(vm.global, 'readExternal', handle)); + +async function readExternal() { + const result = await Promise.race([ + fakedryrun({ + process: 'iWM-odlyQHopPECpyz465p7ED8lm5d3hyyWtijKhie4', + tags: [{ name: 'Action', value: 'Read-Hollow' }], + data: '1234' + }), + timeout(1000, 'Operation timed out after 10 seconds') + ]); + return result.Messages[0].Data; +} +function fakedryrun(config) { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + Messages: [{ Data: JSON.stringify({ ap: { Price: '100', Quantity: '49' } }) }] + }); + }, 20000); + }); +} + +const result = vm.evalCode(`(async () => { + const content = await readExternal() + return content; +})()`); +const promiseHandle = vm.unwrapResult(result); +const resolvedResult = await vm.resolvePromise(promiseHandle); +promiseHandle.dispose(); + +if (resolvedResult.error) { + // const errorMessage = vm.getString(resolvedResult.error); + const error = vm.dump(resolvedResult) || vm.getString(resolvedResult.error); + resolvedResult.error.dispose(); + console.log(`eval failed: ${JSON.stringify(error)}`); + + throw new EvalError(`eval failed.`, { + name: error.name, + evalMessage: error.message, + stack: error.stack + }); +} else { + const resultValue = resolvedResult.value; + const stringValue = vm.getString(resultValue); + const result = stringValue === 'undefined' ? undefined : JSON.parse(vm.getString(resultValue)); + resultValue.dispose(); + console.log('Result:', result); +} From 857f729586018747e972f53d7f901301917d6582 Mon Sep 17 00:00:00 2001 From: Asia Date: Thu, 5 Dec 2024 14:56:50 +0100 Subject: [PATCH 3/3] clear timeout in promise race --- warp-contracts-plugin-quickjs/package.json | 2 +- .../src/eval/QuickJsEvaluator.ts | 7 ++++++- warp-contracts-plugin-quickjs/src/utils.ts | 12 +++++++++--- .../tools/quickjs-promise.mjs | 13 +++++++++---- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/warp-contracts-plugin-quickjs/package.json b/warp-contracts-plugin-quickjs/package.json index 2135763..b0fde57 100644 --- a/warp-contracts-plugin-quickjs/package.json +++ b/warp-contracts-plugin-quickjs/package.json @@ -1,6 +1,6 @@ { "name": "warp-contracts-plugin-quickjs", - "version": "1.1.13", + "version": "1.1.14", "license": "MIT", "main": "./build/cjs/index.js", "module": "./build/esm/index.js", diff --git a/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts b/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts index 0f7d9e7..c10adab 100644 --- a/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts +++ b/warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts @@ -85,6 +85,10 @@ export class QuickJsEvaluator { const processId = this.vm.getString(processIdHandle); const action = this.vm.getString(actionHandle); const { dryrun } = await import('@permaweb/aoconnect'); + const { timeoutId, timeoutPromise } = timeout( + TIMEOUT_ASYNC_OPERATIONS, + 'Dryrun operation timed out after 10 seconds' + ); try { const result = await Promise.race<{ Output: any; @@ -97,8 +101,9 @@ export class QuickJsEvaluator { tags: [{ name: 'Action', value: action }], data: '1234' }), - timeout(TIMEOUT_ASYNC_OPERATIONS, 'Dryrun operation timed out after 10 seconds') + timeoutPromise ]); + if (timeoutId) clearTimeout(timeoutId); return result.Messages[0].Data; } catch (error) { const errorMessage = (error as Error).message; diff --git a/warp-contracts-plugin-quickjs/src/utils.ts b/warp-contracts-plugin-quickjs/src/utils.ts index d7413fe..bf78041 100644 --- a/warp-contracts-plugin-quickjs/src/utils.ts +++ b/warp-contracts-plugin-quickjs/src/utils.ts @@ -51,10 +51,16 @@ export const splitBuffer = (buffer: Buffer, delimiter: string) => { return splitted; }; -export const timeout = (ms: number, message: string): Promise => { - return new Promise((resolve, reject) => { - setTimeout(() => { +export const timeout = ( + ms: number, + message: string +): { timeoutId: string | number | NodeJS.Timeout | undefined; timeoutPromise: Promise } => { + let timeoutId: string | number | NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + clearTimeout(timeoutId); reject(new Error(message)); }, ms); }); + return { timeoutId, timeoutPromise }; }; diff --git a/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs b/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs index b3cd244..9064ffc 100644 --- a/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs +++ b/warp-contracts-plugin-quickjs/tools/quickjs-promise.mjs @@ -13,11 +13,14 @@ const QuickJS = await newQuickJSWASMModule(variant); const vm = QuickJS.newContext(); const timeout = (ms, message) => { - return new Promise((resolve, reject) => { - setTimeout(() => { + let timeoutId = null; + const timeoutPromise = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + clearTimeout(timeoutId); reject(new Error(message)); }, ms); }); + return { timeoutId, timeoutPromise }; }; const readExternalHandle = vm.newFunction('readExternal', () => { @@ -35,14 +38,16 @@ const readExternalHandle = vm.newFunction('readExternal', () => { readExternalHandle.consume((handle) => vm.setProp(vm.global, 'readExternal', handle)); async function readExternal() { + const { timeoutId, timeoutPromise } = timeout(1000, 'Operation timed out after 10 seconds'); const result = await Promise.race([ fakedryrun({ process: 'iWM-odlyQHopPECpyz465p7ED8lm5d3hyyWtijKhie4', tags: [{ name: 'Action', value: 'Read-Hollow' }], data: '1234' }), - timeout(1000, 'Operation timed out after 10 seconds') + timeoutPromise ]); + if (timeoutId) clearTimeout(timeoutId); return result.Messages[0].Data; } function fakedryrun(config) { @@ -51,7 +56,7 @@ function fakedryrun(config) { resolve({ Messages: [{ Data: JSON.stringify({ ap: { Price: '100', Quantity: '49' } }) }] }); - }, 20000); + }, 10000); }); }