diff --git a/package.json b/package.json index fd0d33f0..584c0bc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "warp-contracts", - "version": "1.4.19", + "version": "1.4.20-beta.0", "description": "An implementation of the SmartWeave smart contract protocol.", "types": "./lib/types/index.d.ts", "main": "./lib/cjs/index.js", 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..1574dac3 100644 --- a/src/__tests__/integration/basic/pst.test.ts +++ b/src/__tests__/integration/basic/pst.test.ts @@ -12,6 +12,7 @@ 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; @@ -116,6 +117,39 @@ describe('Testing the Profit Sharing Token', () => { expect(resultVM.target).toEqual('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'); }); + 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.sortKey).not.toBeNull(); + expect(event.detail.input).not.toBeNull(); + expect(event.detail.blockHeight).toBeGreaterThan(0); + expect(event.detail.blockTimestamp).toBeGreaterThan(0); + expect(event.detail.data).toEqual({ + value1: 'foo', + value2: 'bar' + }); + expect(event.detail.input).toEqual({ + function: 'dispatchEvent' + }); + handlerCalled = true; + warp.eventTarget.removeEventListener('interactionCompleted', interactionCompleteHandler); + } + }); + it("should properly evolve contract's source code", async () => { expect((await pst.currentState()).balances[walletAddress]).toEqual(555114); expect((await pstVM.currentState()).balances[walletAddress]).toEqual(555114); 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..0785a810 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -243,3 +243,26 @@ 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 sortKey: 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..5217eb33 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -8,7 +8,7 @@ import { GQLNodeInterface, GQLTagInterface } from '../../../legacy/gqlResult'; import { Benchmark } from '../../../logging/Benchmark'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import { indent } from '../../../utils/utils'; -import { EvalStateResult, StateEvaluator } from '../StateEvaluator'; +import { EvalStateResult, StateEvaluator, CustomEvent } from '../StateEvaluator'; import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExecutorFactory'; import { TagsParser } from './TagsParser'; import { VrfPluginFunctions } from '../../WarpPlugin'; @@ -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) { @@ -358,7 +366,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { } } - private parseInput(inputTag: GQLTagInterface): unknown | null { + private parseInput(inputTag: GQLTagInterface): { function: string } | null { try { return JSON.parse(inputTag.value); } catch (e) { 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..482251fc 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,27 @@ 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, + sortKey: this.swGlobal.transaction.sortKey, + 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..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});