feat: events
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -279,6 +279,7 @@ describe('Constructor', () => {
|
||||
});
|
||||
|
||||
expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({
|
||||
event: null,
|
||||
originalErrorMessages: {},
|
||||
originalValidity: {},
|
||||
result: 100,
|
||||
|
||||
@@ -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<InteractionCompleteEvent>) {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -66,6 +66,7 @@ export class Warp {
|
||||
readonly testing: Testing;
|
||||
kvStorageFactory: KVStorageFactory;
|
||||
whoAmI: string;
|
||||
eventTarget: EventTarget;
|
||||
|
||||
private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map();
|
||||
|
||||
@@ -84,6 +85,7 @@ export class Warp {
|
||||
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`
|
||||
});
|
||||
};
|
||||
this.eventTarget = new EventTarget();
|
||||
}
|
||||
|
||||
static builder(
|
||||
|
||||
@@ -243,3 +243,26 @@ export interface EvaluationOptions {
|
||||
|
||||
whitelistSources: string[];
|
||||
}
|
||||
|
||||
// https://github.com/nodejs/node/issues/40678 duh...
|
||||
export class CustomEvent<T = unknown> extends Event {
|
||||
readonly detail: T;
|
||||
|
||||
constructor(message, data) {
|
||||
super(message, data);
|
||||
this.detail = data.detail;
|
||||
}
|
||||
}
|
||||
|
||||
export class InteractionCompleteEvent<Input = unknown, T = unknown> {
|
||||
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
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<State> {
|
||||
maybeCallStateConstructor(initialState: State, executionContext: ExecutionContext<State>): Promise<State>;
|
||||
}
|
||||
|
||||
export type HandlerFunction<State, Input, Result> = (
|
||||
state: State,
|
||||
interaction: ContractInteraction<Input>
|
||||
) => Promise<HandlerResult<State, Result>>;
|
||||
|
||||
// TODO: change to XOR between result and state?
|
||||
export type HandlerResult<State, Result> = {
|
||||
result: Result;
|
||||
state: State;
|
||||
event: InteractionCompleteEvent;
|
||||
gasUsed?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<State> extends AbstractContractHandler<State> {
|
||||
};
|
||||
}
|
||||
|
||||
private async runContractFunction<Input>(
|
||||
private async runContractFunction<Input, Result>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
interaction: InteractionData<Input>['interaction'],
|
||||
state: State
|
||||
) {
|
||||
): Promise<InteractionResult<State, Result>> {
|
||||
const stateClone = deepCopy(state);
|
||||
const { timeoutId, timeoutPromise } = timeout(
|
||||
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
|
||||
@@ -150,10 +150,27 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
|
||||
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<State> extends AbstractContractHandler<State> {
|
||||
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<State> extends AbstractContractHandler<State> {
|
||||
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 {
|
||||
|
||||
@@ -55,7 +55,8 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
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<State> extends AbstractContractHandler<State> {
|
||||
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 {
|
||||
|
||||
@@ -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});
|
||||
|
||||
Reference in New Issue
Block a user