refactor: handler api refactor
This commit is contained in:
148
src/core/modules/impl/handler/AbstractContractHandler.ts
Normal file
148
src/core/modules/impl/handler/AbstractContractHandler.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
ContractDefinition,
|
||||
CurrentTx,
|
||||
deepCopy,
|
||||
EvalStateResult,
|
||||
ExecutionContext,
|
||||
GQLNodeInterface,
|
||||
HandlerApi,
|
||||
InteractionData,
|
||||
InteractionResult,
|
||||
LoggerFactory,
|
||||
SmartWeaveGlobal
|
||||
} from '@warp';
|
||||
|
||||
export abstract class AbstractContractHandler<State> implements HandlerApi<State> {
|
||||
protected logger = LoggerFactory.INST.create('ContractHandler');
|
||||
|
||||
protected constructor(
|
||||
protected readonly swGlobal: SmartWeaveGlobal,
|
||||
protected readonly contractDefinition: ContractDefinition<State>
|
||||
) {
|
||||
this.assignReadContractState = this.assignReadContractState.bind(this);
|
||||
this.assignViewContractState = this.assignViewContractState.bind(this);
|
||||
this.assignWrite = this.assignWrite.bind(this);
|
||||
this.assignRefreshState = this.assignRefreshState.bind(this);
|
||||
}
|
||||
|
||||
abstract handle<Input, Result>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentResult: EvalStateResult<State>,
|
||||
interactionData: InteractionData<Input>
|
||||
): Promise<InteractionResult<State, Result>>;
|
||||
|
||||
abstract initState(state: State): void;
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
// noop by default;
|
||||
}
|
||||
|
||||
protected assignWrite(executionContext: ExecutionContext<State>, currentTx: CurrentTx[]) {
|
||||
this.swGlobal.contracts.write = async <Input = unknown>(
|
||||
contractTxId: string,
|
||||
input: Input
|
||||
): Promise<InteractionResult<unknown, unknown>> => {
|
||||
if (!executionContext.evaluationOptions.internalWrites) {
|
||||
throw new Error("Internal writes feature switched off. Change EvaluationOptions.internalWrites flag to 'true'");
|
||||
}
|
||||
|
||||
this.logger.debug('swGlobal.write call:', {
|
||||
from: this.contractDefinition.txId,
|
||||
to: contractTxId,
|
||||
input
|
||||
});
|
||||
|
||||
// The contract that we want to call and modify its state
|
||||
const calleeContract = executionContext.warp.contract(
|
||||
contractTxId,
|
||||
executionContext.contract,
|
||||
this.swGlobal._activeTx
|
||||
);
|
||||
|
||||
const result = await calleeContract.dryWriteFromTx<Input>(input, this.swGlobal._activeTx, [
|
||||
...(currentTx || []),
|
||||
{
|
||||
contractTxId: this.contractDefinition.txId,
|
||||
interactionTxId: this.swGlobal.transaction.id
|
||||
}
|
||||
]);
|
||||
|
||||
this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry);
|
||||
await executionContext.warp.stateEvaluator.onInternalWriteStateUpdate(this.swGlobal._activeTx, contractTxId, {
|
||||
state: result.state as State,
|
||||
validity: {
|
||||
...result.originalValidity,
|
||||
[this.swGlobal._activeTx.id]: result.type == 'ok'
|
||||
},
|
||||
errorMessages: {
|
||||
...result.originalErrorMessages,
|
||||
[this.swGlobal._activeTx.id]: result.errorMessage
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
protected assignViewContractState<Input>(executionContext: ExecutionContext<State>) {
|
||||
this.swGlobal.contracts.viewContractState = async <View>(contractTxId: string, input: any) => {
|
||||
this.logger.debug('swGlobal.viewContractState call:', {
|
||||
from: this.contractDefinition.txId,
|
||||
to: contractTxId,
|
||||
input
|
||||
});
|
||||
const childContract = executionContext.warp.contract(
|
||||
contractTxId,
|
||||
executionContext.contract,
|
||||
this.swGlobal._activeTx
|
||||
);
|
||||
|
||||
return await childContract.viewStateForTx(input, this.swGlobal._activeTx);
|
||||
};
|
||||
}
|
||||
|
||||
protected assignReadContractState<Input>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentTx: CurrentTx[],
|
||||
currentResult: EvalStateResult<State>,
|
||||
interactionTx: GQLNodeInterface
|
||||
) {
|
||||
this.swGlobal.contracts.readContractState = async (contractTxId: string, returnValidity?: boolean) => {
|
||||
this.logger.debug('swGlobal.readContractState call:', {
|
||||
from: this.contractDefinition.txId,
|
||||
to: contractTxId,
|
||||
sortKey: interactionTx.sortKey,
|
||||
transaction: this.swGlobal.transaction.id
|
||||
});
|
||||
|
||||
const { stateEvaluator } = executionContext.warp;
|
||||
const childContract = executionContext.warp.contract(contractTxId, executionContext.contract, interactionTx);
|
||||
|
||||
await stateEvaluator.onContractCall(interactionTx, executionContext, currentResult);
|
||||
|
||||
const stateWithValidity = await childContract.readState(interactionTx.sortKey, [
|
||||
...(currentTx || []),
|
||||
{
|
||||
contractTxId: this.contractDefinition.txId,
|
||||
interactionTxId: this.swGlobal.transaction.id
|
||||
}
|
||||
]);
|
||||
// TODO: it should be up to the client's code to decide which part of the result to use
|
||||
// (by simply using destructuring operator)...
|
||||
// but this (i.e. returning always stateWithValidity from here) would break backwards compatibility
|
||||
// in current contract's source code..:/
|
||||
return returnValidity ? deepCopy(stateWithValidity) : deepCopy(stateWithValidity.state);
|
||||
};
|
||||
}
|
||||
|
||||
protected assignRefreshState(executionContext: ExecutionContext<State>) {
|
||||
this.swGlobal.contracts.refreshState = async () => {
|
||||
const stateEvaluator = executionContext.warp.stateEvaluator;
|
||||
const result = await stateEvaluator.latestAvailableState(
|
||||
this.swGlobal.contract.id,
|
||||
this.swGlobal._activeTx.sortKey
|
||||
);
|
||||
return result?.cachedValue.state;
|
||||
};
|
||||
}
|
||||
}
|
||||
90
src/core/modules/impl/handler/JsHandlerApi.ts
Normal file
90
src/core/modules/impl/handler/JsHandlerApi.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
ContractDefinition,
|
||||
deepCopy,
|
||||
EvalStateResult,
|
||||
ExecutionContext,
|
||||
InteractionData,
|
||||
InteractionResult,
|
||||
SmartWeaveGlobal,
|
||||
timeout
|
||||
} from '@warp';
|
||||
import { AbstractContractHandler } from './AbstractContractHandler';
|
||||
|
||||
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
constructor(
|
||||
swGlobal: SmartWeaveGlobal,
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
// eslint-disable-next-line
|
||||
private readonly contractFunction: Function
|
||||
) {
|
||||
super(swGlobal, contractDefinition);
|
||||
}
|
||||
|
||||
async handle<Input, Result>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentResult: EvalStateResult<State>,
|
||||
interactionData: InteractionData<Input>
|
||||
): Promise<InteractionResult<State, Result>> {
|
||||
const { timeoutId, timeoutPromise } = timeout(
|
||||
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
|
||||
);
|
||||
|
||||
try {
|
||||
const { interaction, interactionTx, currentTx } = interactionData;
|
||||
|
||||
const stateCopy = deepCopy(currentResult.state, executionContext.evaluationOptions.useFastCopy);
|
||||
this.swGlobal._activeTx = interactionTx;
|
||||
this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner
|
||||
this.assignReadContractState<Input>(executionContext, currentTx, currentResult, interactionTx);
|
||||
this.assignViewContractState<Input>(executionContext);
|
||||
this.assignWrite(executionContext, currentTx);
|
||||
this.assignRefreshState(executionContext);
|
||||
|
||||
const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]);
|
||||
|
||||
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
|
||||
return {
|
||||
type: 'ok',
|
||||
result: handlerResult.result,
|
||||
state: handlerResult.state || currentResult.state
|
||||
};
|
||||
}
|
||||
|
||||
// Will be caught below as unexpected exception.
|
||||
throw new Error(`Unexpected result from contract: ${JSON.stringify(handlerResult)}`);
|
||||
} catch (err) {
|
||||
switch (err.name) {
|
||||
case 'ContractError':
|
||||
return {
|
||||
type: 'error',
|
||||
errorMessage: err.message,
|
||||
state: currentResult.state,
|
||||
// note: previous version was writing error message to a "result" field,
|
||||
// which fucks-up the HandlerResult type definition -
|
||||
// HandlerResult.result had to be declared as 'Result | string' - and that led to a poor dev exp.
|
||||
// TODO: this might be breaking change!
|
||||
result: null
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: 'exception',
|
||||
errorMessage: `${(err && err.stack) || (err && err.message) || err}`,
|
||||
state: currentResult.state,
|
||||
result: null
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
if (timeoutId !== null) {
|
||||
// it is important to clear the timeout promise
|
||||
// - promise.race won't "cancel" it automatically if the "handler" promise "wins"
|
||||
// - and this would ofc. cause a waste in cpu cycles
|
||||
// (+ Jest complains about async operations not being stopped properly).
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initState(state: State): void {
|
||||
// nth to do in this impl...
|
||||
}
|
||||
}
|
||||
168
src/core/modules/impl/handler/WasmHandlerApi.ts
Normal file
168
src/core/modules/impl/handler/WasmHandlerApi.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/* eslint-disable */
|
||||
import {
|
||||
ContractDefinition,
|
||||
CurrentTx,
|
||||
deepCopy,
|
||||
EvalStateResult,
|
||||
ExecutionContext,
|
||||
GQLNodeInterface,
|
||||
HandlerApi,
|
||||
InteractionData,
|
||||
InteractionResult,
|
||||
LoggerFactory,
|
||||
WarpLogger,
|
||||
SmartWeaveGlobal
|
||||
} from '@warp';
|
||||
import stringify from 'safe-stable-stringify';
|
||||
import { AbstractContractHandler } from './AbstractContractHandler';
|
||||
|
||||
export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
constructor(
|
||||
swGlobal: SmartWeaveGlobal,
|
||||
// eslint-disable-next-line
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
private readonly wasmExports: any
|
||||
) {
|
||||
super(swGlobal, contractDefinition);
|
||||
}
|
||||
|
||||
async handle<Input, Result>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentResult: EvalStateResult<State>,
|
||||
interactionData: InteractionData<Input>
|
||||
): Promise<InteractionResult<State, Result>> {
|
||||
try {
|
||||
const { interaction, interactionTx, currentTx } = interactionData;
|
||||
|
||||
this.swGlobal._activeTx = interactionTx;
|
||||
this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner
|
||||
this.swGlobal.gasLimit = executionContext.evaluationOptions.gasLimit;
|
||||
this.swGlobal.gasUsed = 0;
|
||||
|
||||
this.assignReadContractState<Input>(executionContext, currentTx, currentResult, interactionTx);
|
||||
this.assignWrite(executionContext, currentTx);
|
||||
|
||||
const handlerResult = await this.doHandle(interaction);
|
||||
|
||||
return {
|
||||
type: 'ok',
|
||||
result: handlerResult,
|
||||
state: this.doGetCurrentState(), // TODO: return only at the end of evaluation and when caching is required
|
||||
gasUsed: this.swGlobal.gasUsed
|
||||
};
|
||||
} catch (e) {
|
||||
// note: as exceptions handling in WASM is currently somewhat non-existent
|
||||
// https://www.assemblyscript.org/status.html#exceptions
|
||||
// and since we have to somehow differentiate different types of exceptions
|
||||
// - each exception message has to have a proper prefix added.
|
||||
|
||||
// exceptions with prefix [RE:] ("Runtime Exceptions") should break the execution immediately
|
||||
// - eg: [RE:OOG] - [RuntimeException: OutOfGas]
|
||||
|
||||
// exception with prefix [CE:] ("Contract Exceptions") should be logged, but should not break
|
||||
// the state evaluation - as they are considered as contracts' business exception (eg. validation errors)
|
||||
// - eg: [CE:ITT] - [ContractException: InvalidTokenTransfer]
|
||||
const result = {
|
||||
errorMessage: e.message,
|
||||
state: currentResult.state,
|
||||
result: null
|
||||
};
|
||||
if (e.message.startsWith('[RE:')) {
|
||||
this.logger.fatal(e);
|
||||
return {
|
||||
...result,
|
||||
type: 'exception'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...result,
|
||||
type: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initState(state: State): void {
|
||||
switch (this.contractDefinition.srcWasmLang) {
|
||||
case 'assemblyscript': {
|
||||
const statePtr = this.wasmExports.__newString(stringify(state));
|
||||
this.wasmExports.initState(statePtr);
|
||||
break;
|
||||
}
|
||||
case 'rust': {
|
||||
this.wasmExports.initState(state);
|
||||
break;
|
||||
}
|
||||
case 'go': {
|
||||
this.wasmExports.initState(stringify(state));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Support for ${this.contractDefinition.srcWasmLang} not implemented yet.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async doHandle(action: any): Promise<any> {
|
||||
switch (this.contractDefinition.srcWasmLang) {
|
||||
case 'assemblyscript': {
|
||||
const actionPtr = this.wasmExports.__newString(stringify(action.input));
|
||||
const resultPtr = this.wasmExports.handle(actionPtr);
|
||||
const result = this.wasmExports.__getString(resultPtr);
|
||||
|
||||
return JSON.parse(result);
|
||||
}
|
||||
case 'rust': {
|
||||
let handleResult = await this.wasmExports.handle(action.input);
|
||||
if (!handleResult) {
|
||||
return;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(handleResult, 'Ok')) {
|
||||
return handleResult.Ok;
|
||||
} else {
|
||||
this.logger.debug('Error from rust', handleResult.Err);
|
||||
let errorKey;
|
||||
let errorArgs = '';
|
||||
if (typeof handleResult.Err === 'string' || handleResult.Err instanceof String) {
|
||||
errorKey = handleResult.Err;
|
||||
} else {
|
||||
errorKey = Object.keys(handleResult.Err)[0];
|
||||
errorArgs = ' ' + handleResult.Err[errorKey];
|
||||
}
|
||||
|
||||
if (errorKey == 'RuntimeError') {
|
||||
throw new Error(`[RE:RE]${errorArgs}`);
|
||||
} else {
|
||||
throw new Error(`[CE:${errorKey}${errorArgs}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'go': {
|
||||
const result = await this.wasmExports.handle(stringify(action.input));
|
||||
return JSON.parse(result);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Support for ${this.contractDefinition.srcWasmLang} not implemented yet.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doGetCurrentState(): State {
|
||||
switch (this.contractDefinition.srcWasmLang) {
|
||||
case 'assemblyscript': {
|
||||
const currentStatePtr = this.wasmExports.currentState();
|
||||
return JSON.parse(this.wasmExports.__getString(currentStatePtr));
|
||||
}
|
||||
case 'rust': {
|
||||
return this.wasmExports.currentState();
|
||||
}
|
||||
case 'go': {
|
||||
const result = this.wasmExports.currentState();
|
||||
return JSON.parse(result);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Support for ${this.contractDefinition.srcWasmLang} not implemented yet.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user