Skip to content

Commit

Permalink
feat: events
Browse files Browse the repository at this point in the history
  • Loading branch information
ppedziwiatr committed Oct 16, 2023
1 parent 8e7157e commit bccd8f1
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 25 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/integration/basic/constructor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ describe('Constructor', () => {
});

expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({
event: null,
originalErrorMessages: {},
originalValidity: {},
result: 100,
Expand Down
34 changes: 34 additions & 0 deletions src/__tests__/integration/basic/pst.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/integration/data/token-pst.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/core/Warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -84,6 +85,7 @@ export class Warp {
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`
});
};
this.eventTarget = new EventTarget();
}

static builder(
Expand Down
23 changes: 23 additions & 0 deletions src/core/modules/StateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {}
}
14 changes: 11 additions & 3 deletions src/core/modules/impl/DefaultStateEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 2 additions & 7 deletions src/core/modules/impl/HandlerExecutorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
};

Expand Down
34 changes: 27 additions & 7 deletions src/core/modules/impl/handler/JsHandlerApi.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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
};
}

Expand All @@ -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,
Expand All @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions src/core/modules/impl/handler/WasmHandlerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
Expand Down
6 changes: 2 additions & 4 deletions tools/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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});
Expand Down

0 comments on commit bccd8f1

Please sign in to comment.