feat: events

This commit is contained in:
ppedziwiatr
2023-10-06 16:52:10 +02:00
committed by just_ppe
parent 8e7157efee
commit e45c5f2f7b
11 changed files with 119 additions and 25 deletions

View File

@@ -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",

View File

@@ -279,6 +279,7 @@ describe('Constructor', () => {
});
expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({
event: null,
originalErrorMessages: {},
originalValidity: {},
result: 100,

View File

@@ -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);

View File

@@ -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;

View File

@@ -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(

View File

@@ -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
) {}
}

View File

@@ -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) {

View File

@@ -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;
};

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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});