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..f561d05c 100644 --- a/src/__tests__/integration/basic/pst.test.ts +++ b/src/__tests__/integration/basic/pst.test.ts @@ -7,7 +7,7 @@ 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 { InteractionCompleteEvent, Warp } from "../../../core/Warp"; import { WarpFactory } from '../../../core/WarpFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { DeployPlugin } from 'warp-contracts-plugin-deploy'; @@ -158,6 +158,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..0690168b 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( @@ -230,3 +232,25 @@ export class Warp { export interface WarpAware { set warp(warp: Warp); } + +// 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..a1706ca0 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -14,6 +14,7 @@ import { TagsParser } from './TagsParser'; import { VrfPluginFunctions } from '../../WarpPlugin'; import { BasicSortKeyCache } from '../../../cache/BasicSortKeyCache'; import { KnownErrors } from './handler/JsHandlerApi'; +import { CustomEvent } from '../../Warp'; type EvaluationProgressInput = { contractTxId: string; @@ -267,7 +268,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 +279,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..bbdf9025 100644 --- a/src/core/modules/impl/HandlerExecutorFactory.ts +++ b/src/core/modules/impl/HandlerExecutorFactory.ts @@ -11,7 +11,7 @@ import { EvalStateResult, EvaluationOptions } from '../StateEvaluator'; import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi'; import { WasmHandlerApi } from './handler/WasmHandlerApi'; import { normalizeContractSource } from './normalize-source'; -import { Warp } from '../../Warp'; +import { InteractionCompleteEvent, Warp } from '../../Warp'; import { isBrowser } from '../../../utils/utils'; import { Buffer } from 'warp-isomorphic'; import { InteractionState } from '../../../contract/states/InteractionState'; @@ -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..a8f6ae61 100644 --- a/src/core/modules/impl/handler/JsHandlerApi.ts +++ b/src/core/modules/impl/handler/JsHandlerApi.ts @@ -7,6 +7,7 @@ import { deepCopy, timeout } from '../../../../utils/utils'; import { ContractError, ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory'; import { genesisSortKey } from '../LexicographicalInteractionsSorter'; import { AbstractContractHandler } from './AbstractContractHandler'; +import { InteractionCompleteEvent } from '../../../Warp'; const INIT_FUNC_NAME = '__init'; const throwErrorWithName = (name: string, message: string) => { @@ -132,11 +133,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 +151,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 +184,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 +210,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..ea213682 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -12,6 +12,7 @@ import { } from '../HandlerExecutorFactory'; import { AbstractContractHandler } from './AbstractContractHandler'; import { NetworkCommunicationError } from '../../../../utils/utils'; +import { InteractionCompleteEvent } from "../../../Warp"; export class WasmHandlerApi extends AbstractContractHandler { constructor( @@ -55,7 +56,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 +70,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});