From fa7b7c8af17416350834389d26e64ea6df89843e Mon Sep 17 00:00:00 2001 From: ppedziwiatr Date: Fri, 6 Oct 2023 16:52:10 +0200 Subject: [PATCH] feat: events --- .../integration/basic/constructor.test.ts | 1 + src/__tests__/integration/basic/pst.test.ts | 39 ++++++++++++++++++- src/__tests__/integration/data/token-pst.js | 10 +++++ src/core/Warp.ts | 2 + src/core/modules/StateEvaluator.ts | 22 +++++++++++ .../modules/impl/DefaultStateEvaluator.ts | 10 ++++- .../modules/impl/HandlerExecutorFactory.ts | 9 +---- src/core/modules/impl/handler/JsHandlerApi.ts | 33 ++++++++++++---- .../modules/impl/handler/WasmHandlerApi.ts | 9 +++-- tools/contract.ts | 6 +-- 10 files changed, 118 insertions(+), 23 deletions(-) diff --git a/src/__tests__/integration/basic/constructor.test.ts b/src/__tests__/integration/basic/constructor.test.ts index 76a5a43d..19102764 100644 --- a/src/__tests__/integration/basic/constructor.test.ts +++ b/src/__tests__/integration/basic/constructor.test.ts @@ -279,6 +279,7 @@ describe('Constructor', () => { }); expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({ + event: null, originalErrorMessages: {}, originalValidity: {}, result: 100, diff --git a/src/__tests__/integration/basic/pst.test.ts b/src/__tests__/integration/basic/pst.test.ts index 1fe6983a..a9938ee4 100644 --- a/src/__tests__/integration/basic/pst.test.ts +++ b/src/__tests__/integration/basic/pst.test.ts @@ -7,11 +7,12 @@ import path from 'path'; import { mineBlock } from '../_helpers'; import { PstState, PstContract } from '../../../contract/PstContract'; import { InteractionResult } from '../../../core/modules/impl/HandlerExecutorFactory'; -import { Warp } from '../../../core/Warp'; +import { Warp } from "../../../core/Warp"; import { WarpFactory } from '../../../core/WarpFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { DeployPlugin } from 'warp-contracts-plugin-deploy'; import { VM2Plugin } from 'warp-contracts-plugin-vm2'; +import { InteractionCompleteEvent } from "../../../core/modules/StateEvaluator"; describe('Testing the Profit Sharing Token', () => { let contractSrc: string; @@ -158,6 +159,42 @@ describe('Testing the Profit Sharing Token', () => { expect(result.state.balances[overwrittenCaller]).toEqual(1000 - 333); }); + it('should properly dispatch en event', async () => { + + let handlerCalled = false; + const interactionResult = await pst.writeInteraction({ + function: 'dispatchEvent', + }); + + await mineBlock(warp); + + warp.eventTarget.addEventListener("interactionCompleted", interactionCompleteHandler); + + await pst.readState(); + + expect(handlerCalled).toBeTruthy(); + + function interactionCompleteHandler(event: CustomEvent) { + expect(event.type).toEqual('interactionCompleted'); + expect(event.detail.contractTxId).toEqual(pst.txId()); + expect(event.detail.caller).toEqual(walletAddress); + expect(event.detail.transactionId).toEqual(interactionResult.originalTxId); + expect(event.detail.data).toEqual({ + value1: "foo", + value2: "bar" + } + ); + expect(event.detail.input).toEqual({ + function: 'dispatchEvent' + } + ); + handlerCalled = true; + warp.eventTarget.removeEventListener("interactionCompleted", interactionCompleteHandler); + } + + + }); + describe('when in strict mode', () => { it('should properly extract owner from signature, using arweave wallet', async () => { const startBalance = (await pst.currentBalance(walletAddress)).balance; diff --git a/src/__tests__/integration/data/token-pst.js b/src/__tests__/integration/data/token-pst.js index f3d1252f..9a0960f6 100644 --- a/src/__tests__/integration/data/token-pst.js +++ b/src/__tests__/integration/data/token-pst.js @@ -37,6 +37,16 @@ export async function handle(state, action) { return {state}; } + if (input.function === 'dispatchEvent') { + return { + state, + event: { + value1: 'foo', + value2: 'bar' + } + } + } + if (input.function === 'balance') { const target = input.target; const ticker = state.ticker; diff --git a/src/core/Warp.ts b/src/core/Warp.ts index ee68a04b..1258fe38 100644 --- a/src/core/Warp.ts +++ b/src/core/Warp.ts @@ -66,6 +66,7 @@ export class Warp { readonly testing: Testing; kvStorageFactory: KVStorageFactory; whoAmI: string; + eventTarget: EventTarget; private readonly plugins: Map> = new Map(); @@ -84,6 +85,7 @@ export class Warp { dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}` }); }; + this.eventTarget = new EventTarget(); } static builder( diff --git a/src/core/modules/StateEvaluator.ts b/src/core/modules/StateEvaluator.ts index 4aa42d20..ea8f4148 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -243,3 +243,25 @@ export interface EvaluationOptions { whitelistSources: string[]; } + +// https://github.com/nodejs/node/issues/40678 duh... +export class CustomEvent extends Event { + readonly detail: T; + + constructor(message, data) { + super(message, data); + this.detail = data.detail; + } +} + +export class InteractionCompleteEvent { + constructor( + readonly contractTxId: string, + readonly transactionId: string, + readonly caller: string, + readonly input: Input, + readonly blockTimestamp: number, + readonly blockHeight: number, + readonly data: T // eslint-disable-next-line @typescript-eslint/no-empty-function + ) {} +} diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 1dfc20e6..0ee8b51b 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -267,7 +267,8 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.errorMessage}`); } - validity[missingInteraction.id] = result.type === 'ok'; + const isValidInteraction = result.type === 'ok'; + validity[missingInteraction.id] = isValidInteraction; currentState = result.state; const toCache = new EvalStateResult(currentState, validity, errorMessages); @@ -277,6 +278,13 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { state: toCache }; } + + const event = result.event; + if (event) { + warp.eventTarget.dispatchEvent( + new CustomEvent(isValidInteraction ? 'interactionCompleted' : 'interactionFailed', { detail: event }) + ); + } } if (progressPlugin) { diff --git a/src/core/modules/impl/HandlerExecutorFactory.ts b/src/core/modules/impl/HandlerExecutorFactory.ts index 6673d4d5..34680d5b 100644 --- a/src/core/modules/impl/HandlerExecutorFactory.ts +++ b/src/core/modules/impl/HandlerExecutorFactory.ts @@ -7,7 +7,7 @@ import { SmartWeaveGlobal } from '../../../legacy/smartweave-global'; import { Benchmark } from '../../../logging/Benchmark'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { ExecutorFactory } from '../ExecutorFactory'; -import { EvalStateResult, EvaluationOptions } from '../StateEvaluator'; +import { EvalStateResult, EvaluationOptions, InteractionCompleteEvent } from '../StateEvaluator'; import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi'; import { WasmHandlerApi } from './handler/WasmHandlerApi'; import { normalizeContractSource } from './normalize-source'; @@ -269,15 +269,10 @@ export interface HandlerApi { maybeCallStateConstructor(initialState: State, executionContext: ExecutionContext): Promise; } -export type HandlerFunction = ( - state: State, - interaction: ContractInteraction -) => Promise>; - -// TODO: change to XOR between result and state? export type HandlerResult = { result: Result; state: State; + event: InteractionCompleteEvent; gasUsed?: number; }; diff --git a/src/core/modules/impl/handler/JsHandlerApi.ts b/src/core/modules/impl/handler/JsHandlerApi.ts index af30b8d2..f0b021e9 100644 --- a/src/core/modules/impl/handler/JsHandlerApi.ts +++ b/src/core/modules/impl/handler/JsHandlerApi.ts @@ -1,7 +1,7 @@ import { GQLNodeInterface } from 'legacy/gqlResult'; import { ContractDefinition } from '../../../../core/ContractDefinition'; import { ExecutionContext } from '../../../../core/ExecutionContext'; -import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; +import { EvalStateResult, InteractionCompleteEvent } from '../../../../core/modules/StateEvaluator'; import { SWBlock, SmartWeaveGlobal, SWTransaction, SWVrf } from '../../../../legacy/smartweave-global'; import { deepCopy, timeout } from '../../../../utils/utils'; import { ContractError, ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory'; @@ -132,11 +132,11 @@ export class JsHandlerApi extends AbstractContractHandler { }; } - private async runContractFunction( + private async runContractFunction( executionContext: ExecutionContext, interaction: InteractionData['interaction'], state: State - ) { + ): Promise> { const stateClone = deepCopy(state); const { timeoutId, timeoutPromise } = timeout( executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds @@ -150,10 +150,26 @@ export class JsHandlerApi extends AbstractContractHandler { if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) { await this.swGlobal.kv.commit(); + + let interactionEvent: InteractionCompleteEvent = null; + + if (handlerResult.event) { + interactionEvent = { + contractTxId: this.swGlobal.contract.id, + transactionId: this.swGlobal.transaction.id, + caller: interaction.caller, + input: interaction.input, + blockTimestamp: this.swGlobal.block.timestamp, + blockHeight: this.swGlobal.block.height, + data: handlerResult.event + }; + } + return { type: 'ok' as const, result: handlerResult.result, - state: handlerResult.state || stateClone + state: handlerResult.state || stateClone, + event: interactionEvent }; } @@ -167,7 +183,8 @@ export class JsHandlerApi extends AbstractContractHandler { type: 'error' as const, errorMessage: err.message, state: state, - result: null + result: null, + event: null }; case KnownErrors.ConstructorError: // if that's the contract that we want to evaluate 'directly' - we need to stop evaluation immediately, @@ -192,14 +209,16 @@ export class JsHandlerApi extends AbstractContractHandler { type: 'error' as const, errorMessage: err.message, state: state, - result: null + result: null, + event: null }; default: return { type: 'exception' as const, errorMessage: `${(err && err.stack) || (err && err.message) || err}`, state: state, - result: null + result: null, + event: null }; } } finally { diff --git a/src/core/modules/impl/handler/WasmHandlerApi.ts b/src/core/modules/impl/handler/WasmHandlerApi.ts index c5f06b0b..bd7c16d2 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -55,7 +55,8 @@ export class WasmHandlerApi extends AbstractContractHandler { type: 'ok', result: handlerResult, state: this.doGetCurrentState(), // TODO: return only at the end of evaluation and when caching is required - gasUsed: this.swGlobal.gasUsed + gasUsed: this.swGlobal.gasUsed, + event: null }; } catch (e) { await this.swGlobal.kv.rollback(); @@ -68,14 +69,16 @@ export class WasmHandlerApi extends AbstractContractHandler { return { ...result, error: e.error, - type: 'error' + type: 'error', + event: null }; } else if (e instanceof NetworkCommunicationError) { throw e; } else { return { ...result, - type: 'exception' + type: 'exception', + event: null }; } } finally { diff --git a/tools/contract.ts b/tools/contract.ts index a79817a5..bc0fd709 100644 --- a/tools/contract.ts +++ b/tools/contract.ts @@ -5,14 +5,13 @@ import {JWKInterface} from "arweave/web/lib/wallet"; import fs from "fs"; import { ArweaveGQLTxsFetcher } from "../src/core/modules/impl/ArweaveGQLTxsFetcher"; import { EventEmitter } from "node:events"; -import { EvaluationProgressPlugin } from "warp-contracts-plugin-evaluation-progress"; const logger = LoggerFactory.INST.create('Contract'); //LoggerFactory.use(new TsLogFactory()); //LoggerFactory.INST.logLevel('error'); -LoggerFactory.INST.logLevel('none'); +LoggerFactory.INST.logLevel('debug'); LoggerFactory.INST.logLevel('info', 'DefaultStateEvaluator'); const eventEmitter = new EventEmitter(); @@ -31,13 +30,12 @@ async function main() { try { const contract = warp - .contract("KTzTXT_ANmF84fWEKHzWURD1LWd9QaFR9yfYUwH2Lxw") + .contract("BaAP2wyqSiF7Eqw3vcBvVss3C0H8i1NGQFgMY6nGpnk") .setEvaluationOptions({ maxCallDepth: 5, maxInteractionEvaluationTimeSeconds: 10000, allowBigInt: true, unsafeClient: 'skip', - internalWrites: true, }); const result = await contract.readState(); console.dir(result.cachedValue.state, {depth: null});