feat: events
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "warp-contracts",
|
"name": "warp-contracts",
|
||||||
"version": "1.4.19",
|
"version": "1.4.20-beta.0",
|
||||||
"description": "An implementation of the SmartWeave smart contract protocol.",
|
"description": "An implementation of the SmartWeave smart contract protocol.",
|
||||||
"types": "./lib/types/index.d.ts",
|
"types": "./lib/types/index.d.ts",
|
||||||
"main": "./lib/cjs/index.js",
|
"main": "./lib/cjs/index.js",
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ describe('Constructor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({
|
expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({
|
||||||
|
event: null,
|
||||||
originalErrorMessages: {},
|
originalErrorMessages: {},
|
||||||
originalValidity: {},
|
originalValidity: {},
|
||||||
result: 100,
|
result: 100,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { WarpFactory } from '../../../core/WarpFactory';
|
|||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||||
import { VM2Plugin } from 'warp-contracts-plugin-vm2';
|
import { VM2Plugin } from 'warp-contracts-plugin-vm2';
|
||||||
|
import { InteractionCompleteEvent } from '../../../core/modules/StateEvaluator';
|
||||||
|
|
||||||
describe('Testing the Profit Sharing Token', () => {
|
describe('Testing the Profit Sharing Token', () => {
|
||||||
let contractSrc: string;
|
let contractSrc: string;
|
||||||
@@ -116,6 +117,39 @@ describe('Testing the Profit Sharing Token', () => {
|
|||||||
expect(resultVM.target).toEqual('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M');
|
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 () => {
|
it("should properly evolve contract's source code", async () => {
|
||||||
expect((await pst.currentState()).balances[walletAddress]).toEqual(555114);
|
expect((await pst.currentState()).balances[walletAddress]).toEqual(555114);
|
||||||
expect((await pstVM.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};
|
return {state};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.function === 'dispatchEvent') {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
event: {
|
||||||
|
value1: 'foo',
|
||||||
|
value2: 'bar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (input.function === 'balance') {
|
if (input.function === 'balance') {
|
||||||
const target = input.target;
|
const target = input.target;
|
||||||
const ticker = state.ticker;
|
const ticker = state.ticker;
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class Warp {
|
|||||||
readonly testing: Testing;
|
readonly testing: Testing;
|
||||||
kvStorageFactory: KVStorageFactory;
|
kvStorageFactory: KVStorageFactory;
|
||||||
whoAmI: string;
|
whoAmI: string;
|
||||||
|
eventTarget: EventTarget;
|
||||||
|
|
||||||
private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map();
|
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}`
|
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
this.eventTarget = new EventTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
static builder(
|
static builder(
|
||||||
|
|||||||
@@ -243,3 +243,26 @@ export interface EvaluationOptions {
|
|||||||
|
|
||||||
whitelistSources: string[];
|
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 { Benchmark } from '../../../logging/Benchmark';
|
||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
import { indent } from '../../../utils/utils';
|
import { indent } from '../../../utils/utils';
|
||||||
import { EvalStateResult, StateEvaluator } from '../StateEvaluator';
|
import { EvalStateResult, StateEvaluator, CustomEvent } from '../StateEvaluator';
|
||||||
import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExecutorFactory';
|
import { ContractInteraction, HandlerApi, InteractionResult } from './HandlerExecutorFactory';
|
||||||
import { TagsParser } from './TagsParser';
|
import { TagsParser } from './TagsParser';
|
||||||
import { VrfPluginFunctions } from '../../WarpPlugin';
|
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}`);
|
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;
|
currentState = result.state;
|
||||||
|
|
||||||
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
||||||
@@ -277,6 +278,13 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
state: toCache
|
state: toCache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event = result.event;
|
||||||
|
if (event) {
|
||||||
|
warp.eventTarget.dispatchEvent(
|
||||||
|
new CustomEvent(isValidInteraction ? 'interactionCompleted' : 'interactionFailed', { detail: event })
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressPlugin) {
|
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 {
|
try {
|
||||||
return JSON.parse(inputTag.value);
|
return JSON.parse(inputTag.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SmartWeaveGlobal } from '../../../legacy/smartweave-global';
|
|||||||
import { Benchmark } from '../../../logging/Benchmark';
|
import { Benchmark } from '../../../logging/Benchmark';
|
||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
import { ExecutorFactory } from '../ExecutorFactory';
|
import { ExecutorFactory } from '../ExecutorFactory';
|
||||||
import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
|
import { EvalStateResult, EvaluationOptions, InteractionCompleteEvent } from '../StateEvaluator';
|
||||||
import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi';
|
import { JsHandlerApi, KnownErrors } from './handler/JsHandlerApi';
|
||||||
import { WasmHandlerApi } from './handler/WasmHandlerApi';
|
import { WasmHandlerApi } from './handler/WasmHandlerApi';
|
||||||
import { normalizeContractSource } from './normalize-source';
|
import { normalizeContractSource } from './normalize-source';
|
||||||
@@ -269,15 +269,10 @@ export interface HandlerApi<State> {
|
|||||||
maybeCallStateConstructor(initialState: State, executionContext: ExecutionContext<State>): Promise<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> = {
|
export type HandlerResult<State, Result> = {
|
||||||
result: Result;
|
result: Result;
|
||||||
state: State;
|
state: State;
|
||||||
|
event: InteractionCompleteEvent;
|
||||||
gasUsed?: number;
|
gasUsed?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { GQLNodeInterface } from 'legacy/gqlResult';
|
import { GQLNodeInterface } from 'legacy/gqlResult';
|
||||||
import { ContractDefinition } from '../../../../core/ContractDefinition';
|
import { ContractDefinition } from '../../../../core/ContractDefinition';
|
||||||
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
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 { SWBlock, SmartWeaveGlobal, SWTransaction, SWVrf } from '../../../../legacy/smartweave-global';
|
||||||
import { deepCopy, timeout } from '../../../../utils/utils';
|
import { deepCopy, timeout } from '../../../../utils/utils';
|
||||||
import { ContractError, ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory';
|
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>,
|
executionContext: ExecutionContext<State>,
|
||||||
interaction: InteractionData<Input>['interaction'],
|
interaction: InteractionData<Input>['interaction'],
|
||||||
state: State
|
state: State
|
||||||
) {
|
): Promise<InteractionResult<State, Result>> {
|
||||||
const stateClone = deepCopy(state);
|
const stateClone = deepCopy(state);
|
||||||
const { timeoutId, timeoutPromise } = timeout(
|
const { timeoutId, timeoutPromise } = timeout(
|
||||||
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
|
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
|
||||||
@@ -150,10 +150,27 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
|
|
||||||
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
|
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
|
||||||
await this.swGlobal.kv.commit();
|
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 {
|
return {
|
||||||
type: 'ok' as const,
|
type: 'ok' as const,
|
||||||
result: handlerResult.result,
|
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,
|
type: 'error' as const,
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
state: state,
|
state: state,
|
||||||
result: null
|
result: null,
|
||||||
|
event: null
|
||||||
};
|
};
|
||||||
case KnownErrors.ConstructorError:
|
case KnownErrors.ConstructorError:
|
||||||
// if that's the contract that we want to evaluate 'directly' - we need to stop evaluation immediately,
|
// 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,
|
type: 'error' as const,
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
state: state,
|
state: state,
|
||||||
result: null
|
result: null,
|
||||||
|
event: null
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
type: 'exception' as const,
|
type: 'exception' as const,
|
||||||
errorMessage: `${(err && err.stack) || (err && err.message) || err}`,
|
errorMessage: `${(err && err.stack) || (err && err.message) || err}`,
|
||||||
state: state,
|
state: state,
|
||||||
result: null
|
result: null,
|
||||||
|
event: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
type: 'ok',
|
type: 'ok',
|
||||||
result: handlerResult,
|
result: handlerResult,
|
||||||
state: this.doGetCurrentState(), // TODO: return only at the end of evaluation and when caching is required
|
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) {
|
} catch (e) {
|
||||||
await this.swGlobal.kv.rollback();
|
await this.swGlobal.kv.rollback();
|
||||||
@@ -68,14 +69,16 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
|||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
error: e.error,
|
error: e.error,
|
||||||
type: 'error'
|
type: 'error',
|
||||||
|
event: null
|
||||||
};
|
};
|
||||||
} else if (e instanceof NetworkCommunicationError) {
|
} else if (e instanceof NetworkCommunicationError) {
|
||||||
throw e;
|
throw e;
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
type: 'exception'
|
type: 'exception',
|
||||||
|
event: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import {JWKInterface} from "arweave/web/lib/wallet";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { ArweaveGQLTxsFetcher } from "../src/core/modules/impl/ArweaveGQLTxsFetcher";
|
import { ArweaveGQLTxsFetcher } from "../src/core/modules/impl/ArweaveGQLTxsFetcher";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { EvaluationProgressPlugin } from "warp-contracts-plugin-evaluation-progress";
|
|
||||||
|
|
||||||
const logger = LoggerFactory.INST.create('Contract');
|
const logger = LoggerFactory.INST.create('Contract');
|
||||||
|
|
||||||
//LoggerFactory.use(new TsLogFactory());
|
//LoggerFactory.use(new TsLogFactory());
|
||||||
//LoggerFactory.INST.logLevel('error');
|
//LoggerFactory.INST.logLevel('error');
|
||||||
|
|
||||||
LoggerFactory.INST.logLevel('none');
|
LoggerFactory.INST.logLevel('debug');
|
||||||
LoggerFactory.INST.logLevel('info', 'DefaultStateEvaluator');
|
LoggerFactory.INST.logLevel('info', 'DefaultStateEvaluator');
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
const eventEmitter = new EventEmitter();
|
||||||
@@ -31,13 +30,12 @@ async function main() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const contract = warp
|
const contract = warp
|
||||||
.contract("KTzTXT_ANmF84fWEKHzWURD1LWd9QaFR9yfYUwH2Lxw")
|
.contract("BaAP2wyqSiF7Eqw3vcBvVss3C0H8i1NGQFgMY6nGpnk")
|
||||||
.setEvaluationOptions({
|
.setEvaluationOptions({
|
||||||
maxCallDepth: 5,
|
maxCallDepth: 5,
|
||||||
maxInteractionEvaluationTimeSeconds: 10000,
|
maxInteractionEvaluationTimeSeconds: 10000,
|
||||||
allowBigInt: true,
|
allowBigInt: true,
|
||||||
unsafeClient: 'skip',
|
unsafeClient: 'skip',
|
||||||
internalWrites: true,
|
|
||||||
});
|
});
|
||||||
const result = await contract.readState();
|
const result = await contract.readState();
|
||||||
console.dir(result.cachedValue.state, {depth: null});
|
console.dir(result.cachedValue.state, {depth: null});
|
||||||
|
|||||||
Reference in New Issue
Block a user