feat: 323 add constructor for js contracts
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
"test:unit": "jest ./src/__tests__/unit",
|
||||
"test:unit:cache": "jest ./src/__tests__/unit/cache-leveldb.test.ts",
|
||||
"test:unit:cache:real": "jest ./src/__tests__/unit/cache-leveldb-real-data.test.ts",
|
||||
"test:integration": "jest ./src/__tests__/integration",
|
||||
"test:integration:basic": "jest ./src/__tests__/integration/basic",
|
||||
"test:integration:basic:load": "jest --silent=false --detectOpenHandles ./src/__tests__/integration/basic/contract-loading.test.ts ",
|
||||
"test:integration:basic:arweave": "jest ./src/__tests__/integration/basic/arweave-transactions-loading",
|
||||
@@ -127,4 +128,4 @@
|
||||
"process": false,
|
||||
"url": false
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/__tests__/integration/basic/constructor.test.ts
Normal file
267
src/__tests__/integration/basic/constructor.test.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import ArLocal from 'arlocal';
|
||||
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||
import path from 'path';
|
||||
import { Warp } from '../../../core/Warp';
|
||||
import { WarpFactory } from '../../../core/WarpFactory';
|
||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||
import { mineBlock } from '../_helpers';
|
||||
|
||||
describe('Constructor', () => {
|
||||
let contractSrc: string;
|
||||
let contractIrSrc: string;
|
||||
let helperContractSrc: string;
|
||||
let dummyContractSrc: string;
|
||||
|
||||
let wallet: JWKInterface;
|
||||
let walletAddress: string;
|
||||
|
||||
const initialState: Record<string, number> = {
|
||||
counter: 1
|
||||
};
|
||||
|
||||
let arlocal: ArLocal;
|
||||
let warp: Warp;
|
||||
|
||||
beforeAll(async () => {
|
||||
arlocal = new ArLocal(1332, false);
|
||||
await arlocal.start();
|
||||
LoggerFactory.INST.logLevel('error');
|
||||
|
||||
warp = WarpFactory.forLocal(1332).use(new DeployPlugin());
|
||||
|
||||
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||
|
||||
contractSrc = fs.readFileSync(path.join(__dirname, '../data/constructor/constructor.js'), 'utf8');
|
||||
contractIrSrc = fs.readFileSync(path.join(__dirname, '../data/constructor/constructor-internal-writes.js'), 'utf8');
|
||||
helperContractSrc = fs.readFileSync(path.join(__dirname, '../data/constructor/constructor-helper.js'), 'utf8');
|
||||
dummyContractSrc = fs.readFileSync(path.join(__dirname, '../data/constructor/constructor-dummy.js'), 'utf8');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await arlocal.stop();
|
||||
});
|
||||
|
||||
const deployContract = async ({ withConstructor = true, withKv = true, addToState = {}, src = contractSrc }) => {
|
||||
const { contractTxId } = await warp.deploy({
|
||||
wallet,
|
||||
initState: JSON.stringify({ ...initialState, ...addToState }),
|
||||
src: src,
|
||||
evaluationManifest: withConstructor
|
||||
? {
|
||||
evaluationOptions: {
|
||||
useConstructor: true,
|
||||
useKVStorage: withKv,
|
||||
internalWrites: !withKv,
|
||||
ignoreExceptions: false
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
const contract = warp
|
||||
.contract<any>(contractTxId)
|
||||
.setEvaluationOptions(
|
||||
withConstructor
|
||||
? { useConstructor: true, internalWrites: !withKv, ignoreExceptions: false }
|
||||
: { ignoreExceptions: false }
|
||||
)
|
||||
.connect(wallet);
|
||||
|
||||
await mineBlock(warp);
|
||||
|
||||
return contract;
|
||||
};
|
||||
|
||||
describe('with useConstructor = true', () => {
|
||||
describe('0 interactions', () => {
|
||||
it('should call constructor on first read state and works with next readStates', async () => {
|
||||
const contract = await deployContract({});
|
||||
|
||||
const {
|
||||
cachedValue: { state }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state.calls).toBeDefined();
|
||||
expect(state.calls).toEqual(['__init']);
|
||||
|
||||
const {
|
||||
cachedValue: { state: state2 }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state2.calls).toBeDefined();
|
||||
expect(state2.calls).toEqual(['__init']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with missing interactions', () => {
|
||||
it('should call constructor on first read state and works with next readStates', async () => {
|
||||
const contract = await deployContract({});
|
||||
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
|
||||
const {
|
||||
cachedValue: { state }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state.calls).toBeDefined();
|
||||
expect(state.calls).toEqual(['__init', 'nop']);
|
||||
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
|
||||
const {
|
||||
cachedValue: { state: state2 }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state2.calls).toBeDefined();
|
||||
expect(state2.calls).toEqual(['__init', 'nop', 'nop', 'nop']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Constructor has access to all smartweave globals', () => {
|
||||
it('should assign as caller deployer of contract', async () => {
|
||||
const contract = await deployContract({});
|
||||
|
||||
const {
|
||||
cachedValue: { state }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state.caller).toEqual(walletAddress);
|
||||
expect(state.caller2).toEqual(walletAddress);
|
||||
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
const {
|
||||
cachedValue: { state: state2 }
|
||||
} = await contract.readState();
|
||||
expect(state2.caller).toEqual(walletAddress);
|
||||
expect(state2.caller2).toEqual(walletAddress);
|
||||
});
|
||||
|
||||
it('should work with KV', async () => {
|
||||
const contract = await deployContract({});
|
||||
|
||||
await contract.readState();
|
||||
const { cachedValue: kv } = await contract.getStorageValues(['__init']);
|
||||
expect(kv.get('__init')).toEqual(contract.txId());
|
||||
});
|
||||
});
|
||||
|
||||
it('should rollback KV and state', async () => {
|
||||
const contract = await deployContract({ addToState: { fail: true } });
|
||||
|
||||
await expect(contract.readState()).rejects.toThrowError();
|
||||
const { cachedValue: kv } = await contract.getStorageValues(['__init']);
|
||||
expect(kv.get('__init')).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should fail to call __init function explicit', async () => {
|
||||
const contract = await deployContract({});
|
||||
|
||||
await expect(contract.writeInteraction({ function: '__init' }, { strict: true })).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should properly apply modifications from __init', async () => {
|
||||
const contract = await deployContract({});
|
||||
|
||||
const {
|
||||
cachedValue: { state }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state.counter).toBe(2);
|
||||
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
|
||||
const {
|
||||
cachedValue: { state: state2 }
|
||||
} = await contract.readState();
|
||||
expect(state2.counter).toStrictEqual(2);
|
||||
});
|
||||
|
||||
describe('Internal writes', () => {
|
||||
it('should throw when using internal writes in contract in __init', async () => {
|
||||
const writesInConstructorContract = await deployContract({
|
||||
src: helperContractSrc,
|
||||
withKv: false
|
||||
});
|
||||
|
||||
await expect(writesInConstructorContract.readState()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should read properly from external contract which uses constructor', async () => {
|
||||
const withConstructorContract = await deployContract({
|
||||
src: dummyContractSrc,
|
||||
withKv: false
|
||||
});
|
||||
const readExternalContract = await deployContract({
|
||||
src: contractIrSrc,
|
||||
withKv: false,
|
||||
addToState: { foreignContract: withConstructorContract.txId() }
|
||||
});
|
||||
|
||||
expect((await readExternalContract.viewState({ function: 'read' })).result).toEqual({
|
||||
originalErrorMessages: {},
|
||||
originalValidity: {},
|
||||
result: 100,
|
||||
state: {
|
||||
counter: 100
|
||||
},
|
||||
type: 'ok'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with useConstructor = false', () => {
|
||||
describe('0 interactions', () => {
|
||||
it('should not call constructor on first read state', async () => {
|
||||
const contract = await deployContract({ withConstructor: false });
|
||||
|
||||
const {
|
||||
cachedValue: { state }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state.calls).toBeUndefined();
|
||||
|
||||
const {
|
||||
cachedValue: { state: state2 }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state2.calls).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with missing interactions', () => {
|
||||
it('should not call constructor on first read state and works with next readStates', async () => {
|
||||
const contract = await deployContract({ withConstructor: false });
|
||||
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
|
||||
const {
|
||||
cachedValue: { state }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state.calls).toBeDefined();
|
||||
expect(state.calls).toEqual(['nop']);
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
await contract.writeInteraction({ function: 'nop' });
|
||||
|
||||
const {
|
||||
cachedValue: { state: state2 }
|
||||
} = await contract.readState();
|
||||
|
||||
expect(state2.calls).toBeDefined();
|
||||
expect(state2.calls).toEqual(['nop', 'nop', 'nop']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT fail to call __init function', async () => {
|
||||
const contract = await deployContract({ withConstructor: false });
|
||||
|
||||
await expect(contract.writeInteraction({ function: '__init' })).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
export async function handle(state, action) {
|
||||
if (action.input.function == '__init') {
|
||||
state.counter = 100;
|
||||
return { state };
|
||||
}
|
||||
|
||||
if (action.input.function === 'readCounter') {
|
||||
return { result: state.counter };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export async function handle(state, action) {
|
||||
if (action.input.function == '__init') {
|
||||
state.counter = 100;
|
||||
await SmartWeave.contracts.write(action.input.args.foreignContract, { function: 'write' });
|
||||
return { state };
|
||||
}
|
||||
|
||||
if (action.input.function == 'write') {
|
||||
state.counter = (state.counter || 0) + 1;
|
||||
return { state }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export async function handle(state, action) {
|
||||
state.calls = state.calls || [];
|
||||
|
||||
state.calls = [...state.calls, action.input.function]
|
||||
|
||||
if (action.input.function == '__init') {
|
||||
state.caller = action.caller;
|
||||
state.caller2 = SmartWeave.caller;
|
||||
|
||||
if (action.input.args.fail) {
|
||||
throw new ContractError("Fail on purpose")
|
||||
}
|
||||
state.counter = action.input.args.counter + 1;
|
||||
state.foreignContract = action.input.args.foreignContract;
|
||||
} else if (action.input.function == 'readCounter') {
|
||||
return { result: state.counter }
|
||||
}
|
||||
else if (action.input.function == 'read') {
|
||||
const result = await SmartWeave.contracts.viewContractState(state.foreignContract, { function: 'readCounter' });
|
||||
return { result };
|
||||
}
|
||||
|
||||
return { state }
|
||||
}
|
||||
18
src/__tests__/integration/data/constructor/constructor.js
Normal file
18
src/__tests__/integration/data/constructor/constructor.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export async function handle(state, action) {
|
||||
state.calls = state.calls || [];
|
||||
|
||||
state.calls = [...state.calls, action.input.function]
|
||||
|
||||
if (action.input.function == '__init') {
|
||||
state.caller = action.caller;
|
||||
state.caller2 = SmartWeave.caller;
|
||||
await SmartWeave.kv.put("__init", SmartWeave.transaction.id);
|
||||
|
||||
if (action.input.args.fail) {
|
||||
throw new ContractError("Fail on purpose")
|
||||
}
|
||||
state.counter = action.input.args.counter + 1;
|
||||
}
|
||||
|
||||
return { state }
|
||||
}
|
||||
@@ -30,6 +30,7 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
useVM2: false,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
});
|
||||
|
||||
@@ -69,6 +70,7 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
useVM2: true,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
});
|
||||
|
||||
@@ -102,6 +104,7 @@ describe('Evaluation options evaluator', () => {
|
||||
useKVStorage: false,
|
||||
useVM2: true,
|
||||
waitForConfirmation: false,
|
||||
useConstructor: false,
|
||||
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
|
||||
});
|
||||
|
||||
|
||||
@@ -106,9 +106,16 @@ export class EvaluationOptionsEvaluator {
|
||||
cacheEveryNInteractions: () => this.rootOptions['cacheEveryNInteractions'],
|
||||
remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'],
|
||||
remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'],
|
||||
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage']
|
||||
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'],
|
||||
useConstructor: (foreignOptions) => foreignOptions['useConstructor']
|
||||
};
|
||||
|
||||
private readonly notConflictingEvaluationOptions: (keyof EvaluationOptions)[] = [
|
||||
'useKVStorage',
|
||||
'sourceType',
|
||||
'useConstructor'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param userSetOptions evaluation options set via {@link Contract.setEvaluationOptions}
|
||||
* @param manifestOptions evaluation options from the root contract's manifest (i.e. the contract that
|
||||
@@ -118,7 +125,7 @@ export class EvaluationOptionsEvaluator {
|
||||
if (manifestOptions) {
|
||||
const errors = [];
|
||||
for (const k in manifestOptions) {
|
||||
if (['useKVStorage', 'sourceType'].includes(k)) {
|
||||
if (this.notConflictingEvaluationOptions.includes(k as keyof EvaluationOptions)) {
|
||||
continue;
|
||||
}
|
||||
if (userSetOptions[k] !== manifestOptions[k]) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export type ExecutionContext<State, Api = unknown> = {
|
||||
* A handle to the contract's "handle" function - ie. main function of the given SWC - that actually
|
||||
* performs all the computation.
|
||||
*/
|
||||
handler: Api;
|
||||
handler?: Api;
|
||||
caller?: string; // note: this is only set for "viewState" and "write" operations
|
||||
cachedState?: SortKeyCacheResult<EvalStateResult<State>>;
|
||||
requestedSortKey?: string;
|
||||
|
||||
@@ -99,7 +99,7 @@ export class EvalStateResult<State> {
|
||||
readonly state: State,
|
||||
readonly validity: Record<string, boolean>,
|
||||
readonly errorMessages: Record<string, string>
|
||||
) {}
|
||||
) { }
|
||||
}
|
||||
|
||||
export type UnsafeClientOptions = 'allow' | 'skip' | 'throw';
|
||||
@@ -151,6 +151,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
||||
remoteStateSyncEnabled = false;
|
||||
|
||||
remoteStateSyncSource = 'https://dre-1.warp.cc/contract';
|
||||
|
||||
useConstructor = false;
|
||||
}
|
||||
|
||||
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
|
||||
@@ -236,6 +238,10 @@ export interface EvaluationOptions {
|
||||
// whether a separate key-value storage should be used for the contract
|
||||
useKVStorage: boolean;
|
||||
|
||||
// If set to true, __init function will be called before any interaction on first evaluation of contract.
|
||||
// Contract has to expose __init function in handler.
|
||||
useConstructor: boolean;
|
||||
|
||||
// whether contract state should be acquired from remote source, e.g. D.R.E.
|
||||
remoteStateSyncEnabled: boolean;
|
||||
|
||||
|
||||
@@ -49,28 +49,34 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
throw new Error('Contract tx id not set in the execution context');
|
||||
}
|
||||
|
||||
const isFirstEvaluation = cachedState == null;
|
||||
let baseState = isFirstEvaluation ? executionContext.contractDefinition.initState : cachedState.cachedValue.state;
|
||||
const baseValidity = isFirstEvaluation ? {} : cachedState.cachedValue.validity;
|
||||
const baseErrorMessages = isFirstEvaluation ? {} : cachedState.cachedValue.errorMessages;
|
||||
|
||||
if (isFirstEvaluation) {
|
||||
baseState = await executionContext.handler.maybeCallStateConstructor(
|
||||
executionContext.contractDefinition.initState,
|
||||
executionContext
|
||||
);
|
||||
}
|
||||
|
||||
if (missingInteractions.length == 0) {
|
||||
this.cLogger.info(`No missing interactions ${contractTxId}`);
|
||||
if (cachedState) {
|
||||
if (!isFirstEvaluation) {
|
||||
executionContext.handler?.initState(cachedState.cachedValue.state);
|
||||
return cachedState;
|
||||
} else {
|
||||
executionContext.handler?.initState(executionContext.contractDefinition.initState);
|
||||
executionContext.handler?.initState(baseState);
|
||||
|
||||
this.cLogger.debug('Inserting initial state into cache');
|
||||
const stateToCache = new EvalStateResult(executionContext.contractDefinition.initState, {}, {});
|
||||
const stateToCache = new EvalStateResult(baseState, {}, {});
|
||||
// no real sort-key - as we're returning the initial state
|
||||
await this.cache.put(new CacheKey(contractTxId, genesisSortKey), stateToCache);
|
||||
|
||||
return new SortKeyCacheResult<EvalStateResult<State>>(genesisSortKey, stateToCache);
|
||||
}
|
||||
}
|
||||
|
||||
const baseState =
|
||||
cachedState == null ? executionContext.contractDefinition.initState : cachedState.cachedValue.state;
|
||||
|
||||
const baseValidity = cachedState == null ? {} : cachedState.cachedValue.validity;
|
||||
const baseErrorMessages = cachedState == null ? {} : cachedState.cachedValue.errorMessages;
|
||||
|
||||
// eval state for the missing transactions - starting from the latest value from cache.
|
||||
return await this.doReadState(
|
||||
missingInteractions,
|
||||
|
||||
@@ -59,7 +59,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
|
||||
// TODO: opt - reuse wasm handlers
|
||||
executionContext?.handler.initState(currentState);
|
||||
|
||||
const depth = executionContext.contract.callDepth();
|
||||
|
||||
this.logger.debug(
|
||||
@@ -229,6 +228,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
new EvalStateResult(currentState, validity, errorMessages),
|
||||
interactionData
|
||||
);
|
||||
|
||||
errorMessage = result.errorMessage;
|
||||
if (result.type !== 'ok') {
|
||||
errorMessages[missingInteraction.id] = errorMessage;
|
||||
|
||||
@@ -258,6 +258,8 @@ export interface HandlerApi<State> {
|
||||
): Promise<InteractionResult<State, Result>>;
|
||||
|
||||
initState(state: State): void;
|
||||
|
||||
maybeCallStateConstructor(initialState: State, executionContext: ExecutionContext<State>): Promise<State>;
|
||||
}
|
||||
|
||||
export type HandlerFunction<State, Input, Result> = (
|
||||
|
||||
@@ -28,6 +28,11 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
|
||||
abstract initState(state: State): void;
|
||||
|
||||
abstract maybeCallStateConstructor(
|
||||
initialState: State,
|
||||
executionContext: ExecutionContext<State, unknown>
|
||||
): Promise<State>;
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
// noop by default;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { GQLNodeInterface } from 'legacy/gqlResult';
|
||||
import { ContractDefinition } from '../../../../core/ContractDefinition';
|
||||
import { ExecutionContext } from '../../../../core/ExecutionContext';
|
||||
import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
|
||||
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
|
||||
import { deepCopy, timeout } from '../../../../utils/utils';
|
||||
import { InteractionData, InteractionResult } from '../HandlerExecutorFactory';
|
||||
import { ContractInteraction, InteractionData, InteractionResult } from '../HandlerExecutorFactory';
|
||||
import { genesisSortKey } from '../LexicographicalInteractionsSorter';
|
||||
import { AbstractContractHandler } from './AbstractContractHandler';
|
||||
|
||||
const INIT_FUNC_NAME = '__init';
|
||||
const throwErrorWithName = (name: string, message: string) => {
|
||||
const error = new Error(message);
|
||||
error.name = name;
|
||||
throw error;
|
||||
};
|
||||
enum KnownErrors {
|
||||
ContractError = 'ContractError',
|
||||
ConstructorError = 'ConstructorError'
|
||||
}
|
||||
|
||||
export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
constructor(
|
||||
swGlobal: SmartWeaveGlobal,
|
||||
@@ -21,31 +34,87 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
currentResult: EvalStateResult<State>,
|
||||
interactionData: InteractionData<Input>
|
||||
): Promise<InteractionResult<State, Result>> {
|
||||
const { interaction, interactionTx } = interactionData;
|
||||
|
||||
this.setupSwGlobal(interactionData);
|
||||
this.enableInternalWrites(executionContext, interactionTx);
|
||||
|
||||
this.assertNotConstructorCall<Input>(interaction);
|
||||
|
||||
return await this.runContractFunction(executionContext, interaction, currentResult.state);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
initState(state: State): void {}
|
||||
|
||||
async maybeCallStateConstructor<Input>(
|
||||
initialState: State,
|
||||
executionContext: ExecutionContext<State>
|
||||
): Promise<State> {
|
||||
if (this.contractDefinition.manifest?.evaluationOptions.useConstructor) {
|
||||
const interaction = {
|
||||
input: { function: INIT_FUNC_NAME, args: initialState } as Input,
|
||||
caller: this.contractDefinition.owner
|
||||
};
|
||||
const interactionTx = { ...this.contractDefinition.contractTx, sortKey: genesisSortKey };
|
||||
// this is hard corded sortKey to make KV possible
|
||||
const interactionData: InteractionData<Input> = { interaction, interactionTx };
|
||||
|
||||
this.setupSwGlobal(interactionData);
|
||||
this.disableInternalWritesForConstructor();
|
||||
|
||||
const result = await this.runContractFunction(executionContext, interaction, {} as State);
|
||||
if (result.type !== 'ok') {
|
||||
throw new Error(`Exception while calling constructor: ${JSON.stringify(interaction)}:\n${result.errorMessage}`);
|
||||
}
|
||||
return result.state;
|
||||
} else {
|
||||
return initialState;
|
||||
}
|
||||
}
|
||||
|
||||
private assertNotConstructorCall<Input>(interaction: ContractInteraction<Input>) {
|
||||
if (
|
||||
this.contractDefinition.manifest?.evaluationOptions.useConstructor &&
|
||||
interaction.input['function'] === INIT_FUNC_NAME
|
||||
) {
|
||||
throw new Error(`You have enabled {useConstructor: true} option, so you can't call function ${INIT_FUNC_NAME}`);
|
||||
}
|
||||
}
|
||||
|
||||
private disableInternalWritesForConstructor() {
|
||||
const templateErrorMessage = (op) =>
|
||||
`Can't ${op} foreign contract state: Internal writes feature is not available in constructor`;
|
||||
this.swGlobal.contracts.readContractState = () =>
|
||||
throwErrorWithName('ConstructorError', templateErrorMessage('readContractState'));
|
||||
this.swGlobal.contracts.write = () => throwErrorWithName('ConstructorError', templateErrorMessage('write'));
|
||||
this.swGlobal.contracts.refreshState = () =>
|
||||
throwErrorWithName('ConstructorError', templateErrorMessage('refreshState'));
|
||||
this.swGlobal.contracts.viewContractState = () =>
|
||||
throwErrorWithName('ConstructorError', templateErrorMessage('viewContractState'));
|
||||
}
|
||||
|
||||
private async runContractFunction<Input>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
interaction: InteractionData<Input>['interaction'],
|
||||
state: State
|
||||
) {
|
||||
const stateClone = deepCopy(state);
|
||||
const { timeoutId, timeoutPromise } = timeout(
|
||||
executionContext.evaluationOptions.maxInteractionEvaluationTimeSeconds
|
||||
);
|
||||
|
||||
try {
|
||||
const { interaction, interactionTx } = interactionData;
|
||||
|
||||
const stateCopy = deepCopy(currentResult.state);
|
||||
this.swGlobal._activeTx = interactionTx;
|
||||
this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner
|
||||
|
||||
this.assignReadContractState(executionContext, interactionTx);
|
||||
this.assignViewContractState<Input>(executionContext);
|
||||
this.assignWrite(executionContext);
|
||||
this.assignRefreshState(executionContext);
|
||||
|
||||
await this.swGlobal.kv.open();
|
||||
const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]);
|
||||
|
||||
const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateClone, interaction)]);
|
||||
|
||||
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
|
||||
await this.swGlobal.kv.commit();
|
||||
return {
|
||||
type: 'ok',
|
||||
type: 'ok' as const,
|
||||
result: handlerResult.result,
|
||||
state: handlerResult.state || currentResult.state
|
||||
state: handlerResult.state || stateClone
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,39 +123,43 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
} catch (err) {
|
||||
await this.swGlobal.kv.rollback();
|
||||
switch (err.name) {
|
||||
case 'ContractError':
|
||||
case KnownErrors.ContractError:
|
||||
return {
|
||||
type: 'error',
|
||||
type: 'error' as const,
|
||||
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!
|
||||
state: state,
|
||||
result: null
|
||||
};
|
||||
case KnownErrors.ConstructorError:
|
||||
throw Error(`ConstructorError: ${err.message}`);
|
||||
default:
|
||||
return {
|
||||
type: 'exception',
|
||||
type: 'exception' as const,
|
||||
errorMessage: `${(err && err.stack) || (err && err.message) || err}`,
|
||||
state: currentResult.state,
|
||||
state: state,
|
||||
result: null
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
await this.swGlobal.kv.close();
|
||||
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).
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
await this.swGlobal.kv.close();
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
initState(state: State): void {
|
||||
// nth to do in this impl...
|
||||
private setupSwGlobal<Input>({ interaction, interactionTx }: InteractionData<Input>) {
|
||||
this.swGlobal._activeTx = interactionTx;
|
||||
this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner
|
||||
}
|
||||
|
||||
private enableInternalWrites<Input>(
|
||||
executionContext: ExecutionContext<State, unknown>,
|
||||
interactionTx: GQLNodeInterface
|
||||
) {
|
||||
this.assignReadContractState(executionContext, interactionTx);
|
||||
this.assignViewContractState<Input>(executionContext);
|
||||
this.assignWrite(executionContext);
|
||||
this.assignRefreshState(executionContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,16 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
|
||||
}
|
||||
}
|
||||
|
||||
async maybeCallStateConstructor(
|
||||
initialState: State,
|
||||
executionContext: ExecutionContext<State, unknown>
|
||||
): Promise<State> {
|
||||
if (this.contractDefinition.manifest?.evaluationOptions.useConstructor) {
|
||||
throw Error('Constructor is not implemented for wasm');
|
||||
}
|
||||
return initialState;
|
||||
}
|
||||
|
||||
private doGetCurrentState(): State {
|
||||
switch (this.contractDefinition.srcWasmLang) {
|
||||
case 'rust': {
|
||||
|
||||
Reference in New Issue
Block a user