Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QuickJSPlugin: read external data #33

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions warp-contracts-plugin-quickjs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "warp-contracts-plugin-quickjs",
"version": "1.1.12",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

"version": "1.1.12-external.0",
"license": "MIT",
"main": "./build/cjs/index.js",
"module": "./build/esm/index.js",
Expand Down Expand Up @@ -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"
},
Expand Down
57 changes: 41 additions & 16 deletions warp-contracts-plugin-quickjs/src/QuickJsHandlerApi.ts
Original file line number Diff line number Diff line change
@@ -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<State> {
Expand All @@ -8,14 +8,18 @@ export class QuickJsHandlerApi<State> {
constructor(
private readonly vm: QuickJSContext,
private readonly runtime: QuickJSRuntime,
private readonly quickJS: QuickJSWASMModule,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the QuickJSWASMModule import can also now be removed?

private readonly isSourceAsync: boolean
) {}

async handle<Result>(message: QuickJsPluginMessage, env: ProcessEnv, state?: State): Promise<InteractionResult<State, Result>> {
async handle<Result>(
message: QuickJsPluginMessage,
env: ProcessEnv,
state?: State
): Promise<InteractionResult<State, Result>> {
if (state) {
this.initState(state);
}
return this.runContractFunction(message, env);
return await this.runContractFunction(message, env);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically await is not needed here

}

initState(state: State): void {
Expand All @@ -27,13 +31,21 @@ export class QuickJsHandlerApi<State> {
}
}

private async runContractFunction<Result>(message: QuickJsPluginMessage, env: ProcessEnv): InteractionResult<State, Result> {
private async runContractFunction<Result>(
message: QuickJsPluginMessage,
env: ProcessEnv
): InteractionResult<State, Result> {
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<Result> = this.disposeResult(evalInteractionResult);
if (this.isSourceAsync) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it required here? shouldn't it be only called on the host function side?

this.vm.runtime.executePendingJobs();
}
const state = this.currentState() as State;
return {
Memory: null,
Expand Down Expand Up @@ -69,6 +81,20 @@ export class QuickJsHandlerApi<State> {
}
}

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are erros how handled? e.g. what if dry-run timeouts?

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));
Expand Down Expand Up @@ -99,19 +125,18 @@ export class QuickJsHandlerApi<State> {
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 }[];
};
};
44 changes: 40 additions & 4 deletions warp-contracts-plugin-quickjs/src/eval/QuickJsEvaluator.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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());
});
Expand All @@ -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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we also somehow handle errors here and pass some error info to the quickjs?

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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why dynamic import?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already discussed

const readRes = await dryrun({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the default timeout for the dryrun? maybe we should it limit to sth. like 10s?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont know what's the default but i've added some logic so the error is thrown if dryrun is executing more than 10s

process: processId,
tags: [{ name: 'Action', value: action }],
data: '1234'
});
return JSON.parse(readRes.Messages[0].Data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe there is no point in parsing the response if we need to stringify it again to pass it to the quickjs..

}

evalRedStone() {
const recoverSignerAddressHandle = this.vm.newFunction('recoverSignerAddress', (...args) => {
const nativeArgs = args.map(this.vm.dump);
Expand Down
13 changes: 13 additions & 0 deletions warp-contracts-plugin-quickjs/src/eval/evalCode/decorator.ts
Original file line number Diff line number Diff line change
@@ -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}
Expand Down
17 changes: 8 additions & 9 deletions warp-contracts-plugin-quickjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,23 +37,22 @@ export class QuickJsPlugin<State> implements WarpPlugin<QuickJsPluginInput, Prom
constructor(private readonly quickJsOptions: QuickJsOptions) {}

async process(input: QuickJsPluginInput): Promise<QuickJsHandlerApi<State>> {
({
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't evalExternal and dummyPromiseEval also be called only if isSourceAsync?

quickJsEvaluator.dummyPromiseEval();

return new QuickJsHandlerApi(this.vm, this.runtime, this.QuickJS);
return new QuickJsHandlerApi(this.vm, this.runtime, isSourceAsync);
}

setRuntimeOptions() {
Expand Down
2 changes: 1 addition & 1 deletion warp-contracts-plugin-quickjs/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const vmIntrinsics = {
...DefaultIntrinsics,
Date: false,
Proxy: false,
Promise: false,
Promise: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we set this dynamically based on isSourceAsync?

MapSet: false,
BigFloat: false,
BigInt: true,
Expand Down
Loading