feat: enable inner contract calls for kv storage
This commit is contained in:
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
|
||||
import ArLocal from 'arlocal';
|
||||
import Arweave from 'arweave';
|
||||
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||
import {JWKInterface} from 'arweave/node/lib/wallet';
|
||||
import path from 'path';
|
||||
import { mineBlock } from '../_helpers';
|
||||
import { PstContract, PstState } from '../../../contract/PstContract';
|
||||
@@ -33,8 +33,8 @@ describe('Testing unsafe client in nested contracts with "skip" option', () => {
|
||||
warp = WarpFactory.forLocal(1667).use(new DeployPlugin());
|
||||
warpUnsafe = WarpFactory.forLocal(1667).use(new DeployPlugin());
|
||||
|
||||
({ arweave } = warp);
|
||||
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
|
||||
({arweave} = warp);
|
||||
({jwk: wallet, address: walletAddress} = await warp.generateWallet());
|
||||
|
||||
safeContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst.js'), 'utf8');
|
||||
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
|
||||
@@ -50,7 +50,7 @@ describe('Testing unsafe client in nested contracts with "skip" option', () => {
|
||||
}
|
||||
};
|
||||
|
||||
({ contractTxId } = await warp.createContract.deploy({
|
||||
({contractTxId} = await warp.createContract.deploy({
|
||||
wallet,
|
||||
initState: JSON.stringify(initialState),
|
||||
src: safeContractSrc
|
||||
@@ -62,14 +62,14 @@ describe('Testing unsafe client in nested contracts with "skip" option', () => {
|
||||
pst.connect(wallet);
|
||||
|
||||
unsafeContractSrc = fs.readFileSync(path.join(__dirname, '../data/token-pst-unsafe.js'), 'utf8');
|
||||
({ contractTxId: foreignUnsafeContractTxId } = await warp.createContract.deploy({
|
||||
({contractTxId: foreignUnsafeContractTxId} = await warp.createContract.deploy({
|
||||
wallet,
|
||||
initState: JSON.stringify(initialState),
|
||||
src: unsafeContractSrc
|
||||
}));
|
||||
await mineBlock(warp);
|
||||
|
||||
({ contractTxId: foreignSafeContractTxId } = await warp.createContract.deploy({
|
||||
({contractTxId: foreignSafeContractTxId} = await warp.createContract.deploy({
|
||||
wallet,
|
||||
initState: JSON.stringify(initialState),
|
||||
src: safeContractSrc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_LEVEL_DB_LOCATION } from '../../core/WarpFactory';
|
||||
import {DEFAULT_LEVEL_DB_LOCATION, WarpFactory} from '../../core/WarpFactory';
|
||||
import fs from 'fs';
|
||||
import { SmartWeaveGlobal } from '../../legacy/smartweave-global';
|
||||
import Arweave from 'arweave';
|
||||
@@ -6,16 +6,19 @@ import { DefaultEvaluationOptions } from '../../core/modules/StateEvaluator';
|
||||
import { LevelDbCache } from '../../cache/impl/LevelDbCache';
|
||||
import { GQLNodeInterface } from '../../legacy/gqlResult';
|
||||
import { CacheKey } from '../../cache/SortKeyCache';
|
||||
import {ContractInteractionState} from "../../contract/states/ContractInteractionState";
|
||||
|
||||
describe('KV database', () => {
|
||||
describe('with the SmartWeave Global KV implementation', () => {
|
||||
const arweave = Arweave.init({});
|
||||
const db = new LevelDbCache({
|
||||
inMemory: false,
|
||||
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/KV_TRIE_TEST_SW_GLOBAL`
|
||||
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/KV_TRIE_TEST_SW_GLOBAL`
|
||||
});
|
||||
|
||||
const sut = new SmartWeaveGlobal(arweave, { id: 'a', owner: '' }, new DefaultEvaluationOptions(), db);
|
||||
const interactionState = new ContractInteractionState(WarpFactory.forTestnet());
|
||||
|
||||
const sut = new SmartWeaveGlobal(arweave, { id: 'KV_TRIE_TEST_SW_GLOBAL', owner: '' }, new DefaultEvaluationOptions(), interactionState, db);
|
||||
|
||||
it('should set values', async () => {
|
||||
sut._activeTx = { sortKey: '123' } as GQLNodeInterface;
|
||||
@@ -26,18 +29,28 @@ describe('KV database', () => {
|
||||
|
||||
expect(await sut.kv.get('one')).toEqual({ val: 1 });
|
||||
expect(await sut.kv.get('ninety')).toBeNull();
|
||||
|
||||
await sut.kv.commit();
|
||||
await db.close();
|
||||
await interactionState.commit(sut._activeTx);
|
||||
await db.open();
|
||||
expect(await sut.kv.get('one')).toEqual({ val: 1 });
|
||||
|
||||
sut._activeTx = { sortKey: '222' } as GQLNodeInterface;
|
||||
await sut.kv.put('one', '1');
|
||||
await sut.kv.put('three', 3);
|
||||
await sut.kv.commit();
|
||||
await db.close();
|
||||
await interactionState.commit(sut._activeTx);
|
||||
await db.open();
|
||||
|
||||
sut._activeTx = { sortKey: '330' } as GQLNodeInterface;
|
||||
await sut.kv.put('one', { val: [1] });
|
||||
await sut.kv.put('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA', 23111222);
|
||||
await sut.kv.commit();
|
||||
await db.close();
|
||||
await interactionState.commit(sut._activeTx);
|
||||
await db.open();
|
||||
|
||||
expect(await sut.kv.get('foo')).toEqual('bar');
|
||||
expect(await sut.kv.get('one')).toEqual({ val: [1] });
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GQLNodeInterface } from '../legacy/gqlResult';
|
||||
import { ArTransfer, Tags, ArWallet } from './deploy/CreateContract';
|
||||
import { CustomSignature } from './Signature';
|
||||
import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
|
||||
import { InteractionState } from './states/InteractionState';
|
||||
|
||||
export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: number; total: number };
|
||||
|
||||
@@ -245,7 +246,9 @@ export interface Contract<State = unknown> {
|
||||
|
||||
getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, unknown>>>;
|
||||
|
||||
getUncommittedState(contractTxId: string): EvalStateResult<unknown>;
|
||||
interactionState(): InteractionState;
|
||||
|
||||
/* getUncommittedState(contractTxId: string): EvalStateResult<unknown>;
|
||||
|
||||
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void;
|
||||
|
||||
@@ -253,5 +256,5 @@ export interface Contract<State = unknown> {
|
||||
|
||||
resetUncommittedState(): void;
|
||||
|
||||
commitStates(interaction: GQLNodeInterface): Promise<void>;
|
||||
commitStates(interaction: GQLNodeInterface): Promise<void>;*/
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator';
|
||||
import { WarpFetchWrapper } from '../core/WarpFetchWrapper';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { TransactionStatusResponse } from '../utils/types/arweave-types';
|
||||
import { InteractionState } from './states/InteractionState';
|
||||
import { ContractInteractionState } from './states/ContractInteractionState';
|
||||
import { Crypto } from 'warp-isomorphic';
|
||||
|
||||
/**
|
||||
@@ -51,29 +53,28 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
// TODO: refactor: extract execution context logic to a separate class
|
||||
private readonly ecLogger = LoggerFactory.INST.create('ExecutionContext');
|
||||
|
||||
private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
|
||||
private readonly _callDepth: number;
|
||||
private readonly _arweaveWrapper: ArweaveWrapper;
|
||||
private readonly _mutex = new Mutex();
|
||||
|
||||
private _callStack: ContractCallRecord;
|
||||
private _evaluationOptions: EvaluationOptions;
|
||||
private _eoEvaluator: EvaluationOptionsEvaluator; // this is set after loading Contract Definition for the root contract
|
||||
private readonly _innerWritesEvaluator = new InnerWritesEvaluator();
|
||||
private readonly _callDepth: number;
|
||||
private _benchmarkStats: BenchmarkStats = null;
|
||||
private readonly _arweaveWrapper: ArweaveWrapper;
|
||||
|
||||
private _sorter: InteractionsSorter;
|
||||
private _rootSortKey: string;
|
||||
private signature: Signature;
|
||||
private warpFetchWrapper: WarpFetchWrapper;
|
||||
|
||||
private _signature: Signature;
|
||||
private _warpFetchWrapper: WarpFetchWrapper;
|
||||
private _children: HandlerBasedContract<unknown>[] = [];
|
||||
|
||||
private _uncommittedStates = new Map<string, EvalStateResult<unknown>>();
|
||||
private _interactionState;
|
||||
private _dreStates = new Map<string, SortKeyCacheResult<EvalStateResult<State>>>();
|
||||
|
||||
private readonly mutex = new Mutex();
|
||||
|
||||
constructor(
|
||||
private readonly _contractTxId: string,
|
||||
protected readonly warp: Warp,
|
||||
private readonly _parentContract: Contract<unknown> = null,
|
||||
private readonly _parentContract: Contract = null,
|
||||
private readonly _innerCallData: InnerCallData = null
|
||||
) {
|
||||
this.waitForConfirmation = this.waitForConfirmation.bind(this);
|
||||
@@ -81,9 +82,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
|
||||
if (_parentContract != null) {
|
||||
this._evaluationOptions = this.getRoot().evaluationOptions();
|
||||
if (_parentContract.evaluationOptions().useKVStorage) {
|
||||
throw new Error('Foreign writes or reads are forbidden for kv storage contracts');
|
||||
}
|
||||
this._callDepth = _parentContract.callDepth() + 1;
|
||||
const callingInteraction: InteractionCall = _parentContract
|
||||
.getCallStack()
|
||||
@@ -125,10 +123,11 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
this._rootSortKey = null;
|
||||
this._evaluationOptions = new DefaultEvaluationOptions();
|
||||
this._children = [];
|
||||
this._interactionState = new ContractInteractionState(warp);
|
||||
}
|
||||
|
||||
this.getCallStack = this.getCallStack.bind(this);
|
||||
this.warpFetchWrapper = new WarpFetchWrapper(this.warp);
|
||||
this._warpFetchWrapper = new WarpFetchWrapper(this.warp);
|
||||
}
|
||||
|
||||
async readState(
|
||||
@@ -149,8 +148,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
? this._sorter.generateLastSortKey(sortKeyOrBlockHeight)
|
||||
: sortKeyOrBlockHeight;
|
||||
|
||||
if (sortKey && !this.isRoot() && this.hasUncommittedState(this.txId())) {
|
||||
const result = this.getUncommittedState(this.txId());
|
||||
if (sortKey && !this.isRoot() && this.interactionState().has(this.txId())) {
|
||||
const result = this.interactionState().get(this.txId());
|
||||
return {
|
||||
sortKey,
|
||||
cachedValue: result as EvalStateResult<State>
|
||||
@@ -159,7 +158,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
|
||||
// TODO: not sure if we should synchronize on a contract instance or contractTxId
|
||||
// in the latter case, the warp instance should keep a map contractTxId -> mutex
|
||||
const releaseMutex = await this.mutex.acquire();
|
||||
const releaseMutex = await this._mutex.acquire();
|
||||
try {
|
||||
const initBenchmark = Benchmark.measure();
|
||||
this.maybeResetRootContract();
|
||||
@@ -191,7 +190,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
});
|
||||
|
||||
if (sortKey && !this.isRoot()) {
|
||||
this.setUncommittedState(this.txId(), result.cachedValue);
|
||||
this.interactionState().update(this.txId(), result.cachedValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -245,7 +244,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
options?: WriteInteractionOptions
|
||||
): Promise<WriteInteractionResponse | null> {
|
||||
this.logger.info('Write interaction', { input, options });
|
||||
if (!this.signature) {
|
||||
if (!this._signature) {
|
||||
throw new Error("Wallet not connected. Use 'connect' method first.");
|
||||
}
|
||||
const { arweave, interactionsLoader, environment } = this.warp;
|
||||
@@ -263,7 +262,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
|
||||
const bundleInteraction = interactionsLoader.type() == 'warp' && !effectiveDisableBundling;
|
||||
|
||||
this.signature.checkNonArweaveSigningAvailability(bundleInteraction);
|
||||
this._signature.checkNonArweaveSigningAvailability(bundleInteraction);
|
||||
|
||||
if (
|
||||
bundleInteraction &&
|
||||
@@ -334,7 +333,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
options.vrf
|
||||
);
|
||||
|
||||
const response = this.warpFetchWrapper.fetch(
|
||||
const response = this._warpFetchWrapper.fetch(
|
||||
`${stripTrailingSlash(this._evaluationOptions.sequencerUrl)}/gateway/sequencer/register`,
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -376,7 +375,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
|
||||
const interactionTx = await createInteractionTx(
|
||||
this.warp.arweave,
|
||||
this.signature.signer,
|
||||
this._signature.signer,
|
||||
this._contractTxId,
|
||||
input,
|
||||
tags,
|
||||
@@ -390,7 +389,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
if (!this._evaluationOptions.internalWrites && strict) {
|
||||
const { arweave } = this.warp;
|
||||
const caller =
|
||||
this.signature.type == 'arweave'
|
||||
this._signature.type == 'arweave'
|
||||
? await arweave.wallets.ownerToAddress(interactionTx.owner)
|
||||
: interactionTx.owner;
|
||||
const handlerResult = await this.callContract(input, 'write', caller, undefined, tags, transfer, strict, vrf);
|
||||
@@ -411,7 +410,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
}
|
||||
|
||||
connect(signature: ArWallet | CustomSignature): Contract<State> {
|
||||
this.signature = new Signature(this.warp, signature);
|
||||
this._signature = new Signature(this.warp, signature);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -526,15 +525,13 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions);
|
||||
}
|
||||
|
||||
if (!this.isRoot() && contractEvaluationOptions.useKVStorage) {
|
||||
throw new Error('Foreign read/writes cannot be performed on kv storage contracts');
|
||||
}
|
||||
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
|
||||
|
||||
handler = (await this.warp.executorFactory.create(
|
||||
contractDefinition,
|
||||
contractEvaluationOptions,
|
||||
this.warp
|
||||
this.warp,
|
||||
this.interactionState()
|
||||
)) as HandlerApi<State>;
|
||||
}
|
||||
|
||||
@@ -571,7 +568,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
}
|
||||
|
||||
private async fetchRemoteContractState(contractId: string): Promise<DREContractStatusResponse<State> | null> {
|
||||
return this.warpFetchWrapper
|
||||
return this._warpFetchWrapper
|
||||
.fetch(`${this._evaluationOptions.remoteStateSyncSource}?id=${contractId}&events=false`)
|
||||
.then((res) => {
|
||||
return res.ok ? res.json() : Promise.reject(res);
|
||||
@@ -616,7 +613,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
this._rootSortKey = null;
|
||||
this.warp.interactionsLoader.clearCache();
|
||||
this._children = [];
|
||||
this._uncommittedStates = new Map();
|
||||
this._interactionState = new ContractInteractionState(this.warp);
|
||||
this._dreStates = new Map();
|
||||
}
|
||||
}
|
||||
@@ -634,7 +631,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
): Promise<InteractionResult<State, View>> {
|
||||
this.logger.info('Call contract input', input);
|
||||
this.maybeResetRootContract();
|
||||
if (!this.signature) {
|
||||
if (!this._signature) {
|
||||
this.logger.warn('Wallet not set.');
|
||||
}
|
||||
const { arweave, stateEvaluator } = this.warp;
|
||||
@@ -648,8 +645,8 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
let effectiveCaller;
|
||||
if (caller) {
|
||||
effectiveCaller = caller;
|
||||
} else if (this.signature) {
|
||||
effectiveCaller = await this.signature.getAddress();
|
||||
} else if (this._signature) {
|
||||
effectiveCaller = await this._signature.getAddress();
|
||||
} else {
|
||||
effectiveCaller = '';
|
||||
}
|
||||
@@ -674,7 +671,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
this.logger.debug('interaction', interaction);
|
||||
const tx = await createInteractionTx(
|
||||
arweave,
|
||||
sign ? this.signature?.signer : undefined,
|
||||
sign ? this._signature?.signer : undefined,
|
||||
this._contractTxId,
|
||||
input,
|
||||
tags,
|
||||
@@ -727,14 +724,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
|
||||
const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx);
|
||||
|
||||
if (!this.isRoot() && this.hasUncommittedState(this.txId())) {
|
||||
if (!this.isRoot() && this.interactionState().has(this.txId())) {
|
||||
evalStateResult = {
|
||||
sortKey: interactionTx.sortKey,
|
||||
cachedValue: this.getUncommittedState(this.txId()) as EvalStateResult<State>
|
||||
cachedValue: this.interactionState().get(this.txId()) as EvalStateResult<State>
|
||||
};
|
||||
} else {
|
||||
evalStateResult = await this.warp.stateEvaluator.eval<State>(executionContext);
|
||||
this.setUncommittedState(this.txId(), evalStateResult.cachedValue);
|
||||
this.interactionState().update(this.txId(), evalStateResult.cachedValue);
|
||||
}
|
||||
|
||||
this.logger.debug('callContractForTx - evalStateResult', {
|
||||
@@ -819,7 +816,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- params can be anything
|
||||
async syncState(externalUrl: string, params?: any): Promise<Contract> {
|
||||
const { stateEvaluator } = this.warp;
|
||||
const response = await this.warpFetchWrapper
|
||||
const response = await this._warpFetchWrapper
|
||||
.fetch(
|
||||
`${externalUrl}?${new URLSearchParams({
|
||||
id: this._contractTxId,
|
||||
@@ -884,38 +881,20 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
}
|
||||
}
|
||||
|
||||
getUncommittedState(contractTxId: string): EvalStateResult<unknown> {
|
||||
return this.getRoot()._uncommittedStates.get(contractTxId);
|
||||
interactionState(): InteractionState {
|
||||
return this.getRoot()._interactionState;
|
||||
}
|
||||
|
||||
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void {
|
||||
this.getRoot()._uncommittedStates.set(contractTxId, result);
|
||||
}
|
||||
|
||||
hasUncommittedState(contractTxId: string): boolean {
|
||||
return this.getRoot()._uncommittedStates.has(contractTxId);
|
||||
}
|
||||
|
||||
resetUncommittedState(): void {
|
||||
this.getRoot()._uncommittedStates = new Map();
|
||||
}
|
||||
|
||||
async commitStates(interaction: GQLNodeInterface): Promise<void> {
|
||||
const uncommittedStates = this.getRoot()._uncommittedStates;
|
||||
try {
|
||||
// i.e. if more than root contract state is in uncommitted state
|
||||
// - without this check, we would effectively cache state for each evaluated interaction
|
||||
// - which is not storage-effective
|
||||
if (uncommittedStates.size > 1) {
|
||||
for (const [k, v] of uncommittedStates) {
|
||||
await this.warp.stateEvaluator.putInCache(k, interaction, v);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.resetUncommittedState();
|
||||
getRoot(): HandlerBasedContract<unknown> {
|
||||
let result: Contract = this;
|
||||
while (!result.isRoot()) {
|
||||
result = result.parent();
|
||||
}
|
||||
|
||||
return result as HandlerBasedContract<unknown>;
|
||||
}
|
||||
|
||||
|
||||
private async maybeSyncStateWithRemoteSource(
|
||||
remoteState: SortKeyCacheResult<EvalStateResult<State>>,
|
||||
upToSortKey: string,
|
||||
@@ -965,15 +944,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
return this.getRoot()._dreStates.has(contractTxId);
|
||||
}
|
||||
|
||||
private getRoot(): HandlerBasedContract<unknown> {
|
||||
let result: Contract = this;
|
||||
while (!result.isRoot()) {
|
||||
result = result.parent();
|
||||
}
|
||||
|
||||
return result as HandlerBasedContract<unknown>;
|
||||
}
|
||||
|
||||
// Call contract and verify if there are any internal writes:
|
||||
// 1. Evaluate current contract state
|
||||
// 2. Apply input as "dry-run" transaction
|
||||
|
||||
102
src/contract/states/ContractInteractionState.ts
Normal file
102
src/contract/states/ContractInteractionState.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { InteractionState } from './InteractionState';
|
||||
import { BatchDBOp } from '../../cache/SortKeyCache';
|
||||
import { EvalStateResult } from '../../core/modules/StateEvaluator';
|
||||
import { GQLNodeInterface } from '../../legacy/gqlResult';
|
||||
import { Warp } from '../../core/Warp';
|
||||
|
||||
export class ContractInteractionState implements InteractionState {
|
||||
private readonly _json = new Map<string, EvalStateResult<unknown>>();
|
||||
private readonly _initialJson = new Map<string, EvalStateResult<unknown>>();
|
||||
|
||||
private readonly _kv = new Map<string, BatchDBOp<unknown>[]>();
|
||||
|
||||
constructor(private readonly _warp: Warp) {}
|
||||
|
||||
has(contractTx): boolean {
|
||||
return this._json.has(contractTx);
|
||||
}
|
||||
|
||||
get(contractTxId: string): EvalStateResult<unknown> {
|
||||
return this._json.get(contractTxId) || null;
|
||||
}
|
||||
|
||||
getKV<T>(contractTxId: string): BatchDBOp<T>[] | null {
|
||||
return this._kv.get(contractTxId) as BatchDBOp<T>[] || null;
|
||||
}
|
||||
|
||||
// TODO. TWL good luck with this one :-)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getKVRange(contractTxId: string, key: string): unknown {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async commit(interaction: GQLNodeInterface): Promise<void> {
|
||||
if (interaction.dry) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.doStoreJson(this._json, interaction);
|
||||
await this.doStoreKV();
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
async commitKV(): Promise<void> {
|
||||
await this.doStoreKV();
|
||||
this._kv.clear();
|
||||
}
|
||||
|
||||
async rollback(interaction: GQLNodeInterface): Promise<void> {
|
||||
try {
|
||||
await this.doStoreJson(this._initialJson, interaction);
|
||||
} finally {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
setInitial(contractTxId: string, state: EvalStateResult<unknown>): void {
|
||||
// think twice here.
|
||||
this._initialJson.set(contractTxId, state);
|
||||
this._json.set(contractTxId, state);
|
||||
}
|
||||
|
||||
update(contractTxId: string, state: EvalStateResult<unknown>): void {
|
||||
this._json.set(contractTxId, state);
|
||||
}
|
||||
|
||||
updateKV(contractTxId: string, ops: BatchDBOp<unknown>[]): void {
|
||||
if (!this._kv.has(contractTxId)) {
|
||||
this._kv.set(contractTxId, ops);
|
||||
} else {
|
||||
this._kv.set(contractTxId, this._kv.get(contractTxId).concat(ops));
|
||||
}
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this._json.clear();
|
||||
this._initialJson.clear();
|
||||
this._kv.clear();
|
||||
}
|
||||
|
||||
private async doStoreJson(states: Map<string, EvalStateResult<unknown>>, interaction: GQLNodeInterface) {
|
||||
if (states.size > 1) {
|
||||
for (const [k, v] of states) {
|
||||
await this._warp.stateEvaluator.putInCache(k, interaction, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async doStoreKV(): Promise<void> {
|
||||
for (const [contractTxId, batch] of this._kv) {
|
||||
const storage = this._warp.kvStorageFactory(contractTxId);
|
||||
|
||||
try {
|
||||
await storage.open();
|
||||
await storage.batch(batch);
|
||||
} finally {
|
||||
await storage.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/contract/states/InteractionState.ts
Normal file
50
src/contract/states/InteractionState.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BatchDBOp } from '../../cache/SortKeyCache';
|
||||
import { EvalStateResult } from '../../core/modules/StateEvaluator';
|
||||
import { GQLNodeInterface } from '../../legacy/gqlResult';
|
||||
|
||||
// Handles contracts state (both the json-based and kv-based) during interaction evaluation
|
||||
export interface InteractionState {
|
||||
/**
|
||||
* Sets the state for a given contract as it is at the beginning of the interaction evaluation.
|
||||
* If the interaction evaluation of the root contract will fail (i.e. its result type is != 'ok')
|
||||
* - this initial state will be committed to the cache for this interaction.
|
||||
* In other words - all changes made during evaluation of this interaction will be rollbacked.
|
||||
*/
|
||||
setInitial(contractTxId: string, state: EvalStateResult<unknown>): void;
|
||||
|
||||
/**
|
||||
* Updates the json-state for a given contract during interaction evaluation - e.g. as a result of an internal write
|
||||
*/
|
||||
update(contractTxId: string, state: EvalStateResult<unknown>): void;
|
||||
|
||||
/**
|
||||
* Updates the kv-state for a given contract during interaction evaluation
|
||||
*/
|
||||
updateKV(contractTxId: string, ops: BatchDBOp<unknown>[]): void;
|
||||
|
||||
/**
|
||||
* commits all the state changes made for all contracts within given interaction evaluation.
|
||||
* Called by the {@link DefaultStateEvaluator} at the end every root's contract interaction evaluation
|
||||
* - IFF the result.type == 'ok'.
|
||||
*/
|
||||
commit(interaction: GQLNodeInterface): Promise<void>;
|
||||
|
||||
commitKV(): Promise<void>;
|
||||
|
||||
/**
|
||||
* rollbacks all the state changes made for all contracts within given interaction evaluation.
|
||||
* Called by the {@link DefaultStateEvaluator} at the end every root's contract interaction evaluation
|
||||
* - IFF the result.type != 'ok'.
|
||||
* This ensures atomicity of state changes withing any given interaction - also in case of internal contract calls.
|
||||
*/
|
||||
rollback(interaction: GQLNodeInterface): Promise<void>;
|
||||
|
||||
has(contractTxId: string): boolean;
|
||||
|
||||
get(contractTxId: string): EvalStateResult<unknown> | null;
|
||||
|
||||
getKV<T>(contractTxId: string): BatchDBOp<T>[] | null;
|
||||
|
||||
// TODO
|
||||
getKVRange(contractTxId: string, key: string): unknown | null;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import Arweave from 'arweave';
|
||||
import { LevelDbCache } from '../cache/impl/LevelDbCache';
|
||||
import { MemCache } from '../cache/impl/MemCache';
|
||||
import { CacheableExecutorFactory } from '../plugins/CacheableExecutorFactory';
|
||||
import { Evolve } from '../plugins/Evolve';
|
||||
import { CacheableStateEvaluator } from './modules/impl/CacheableStateEvaluator';
|
||||
import { HandlerExecutorFactory } from './modules/impl/HandlerExecutorFactory';
|
||||
@@ -122,7 +120,7 @@ export class WarpFactory {
|
||||
dbLocation: `${cacheOptions.dbLocation}/state`
|
||||
});
|
||||
|
||||
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
|
||||
const executorFactory = new HandlerExecutorFactory(arweave);
|
||||
const stateEvaluator = new CacheableStateEvaluator(arweave, stateCache, [new Evolve()]);
|
||||
|
||||
return Warp.builder(arweave, stateCache, environment)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ContractDefinition } from '../../core/ContractDefinition';
|
||||
import { EvaluationOptions } from './StateEvaluator';
|
||||
import { Warp } from '../Warp';
|
||||
import { InteractionState } from '../../contract/states/InteractionState';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ContractApi {}
|
||||
@@ -13,6 +14,7 @@ export interface ExecutorFactory<Api> {
|
||||
create<State>(
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
evaluationOptions: EvaluationOptions,
|
||||
warp: Warp
|
||||
warp: Warp,
|
||||
interactionState: InteractionState
|
||||
): Promise<Api>;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import Arweave from 'arweave';
|
||||
import { SortKeyCache, SortKeyCacheResult, CacheKey } from '../../../cache/SortKeyCache';
|
||||
import { ExecutionContext } from '../../../core/ExecutionContext';
|
||||
import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier';
|
||||
import { GQLNodeInterface } from '../../../legacy/gqlResult';
|
||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||
import { indent } from '../../../utils/utils';
|
||||
import { EvalStateResult } from '../StateEvaluator';
|
||||
import { DefaultStateEvaluator } from './DefaultStateEvaluator';
|
||||
import { HandlerApi } from './HandlerExecutorFactory';
|
||||
import { genesisSortKey } from './LexicographicalInteractionsSorter';
|
||||
import {CacheKey, SortKeyCache, SortKeyCacheResult} from '../../../cache/SortKeyCache';
|
||||
import {ExecutionContext} from '../../../core/ExecutionContext';
|
||||
import {ExecutionContextModifier} from '../../../core/ExecutionContextModifier';
|
||||
import {GQLNodeInterface} from '../../../legacy/gqlResult';
|
||||
import {LoggerFactory} from '../../../logging/LoggerFactory';
|
||||
import {indent} from '../../../utils/utils';
|
||||
import {EvalStateResult} from '../StateEvaluator';
|
||||
import {DefaultStateEvaluator} from './DefaultStateEvaluator';
|
||||
import {HandlerApi} from './HandlerExecutorFactory';
|
||||
import {genesisSortKey} from './LexicographicalInteractionsSorter';
|
||||
|
||||
/**
|
||||
* An implementation of DefaultStateEvaluator that adds caching capabilities.
|
||||
@@ -32,7 +32,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
async eval<State>(
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
||||
const cachedState = executionContext.cachedState;
|
||||
const {cachedState, contract} = executionContext;
|
||||
const missingInteractions = executionContext.sortedInteractions;
|
||||
|
||||
if (cachedState && cachedState.sortKey == executionContext.requestedSortKey && !missingInteractions?.length) {
|
||||
@@ -59,22 +59,22 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
executionContext.contractDefinition.initState,
|
||||
executionContext
|
||||
);
|
||||
await contract.interactionState().commitKV();
|
||||
}
|
||||
|
||||
if (missingInteractions.length == 0) {
|
||||
this.cLogger.info(`No missing interactions ${contractTxId}`);
|
||||
if (!isFirstEvaluation) {
|
||||
executionContext.handler?.initState(cachedState.cachedValue.state);
|
||||
return cachedState;
|
||||
} else {
|
||||
if (isFirstEvaluation) {
|
||||
executionContext.handler?.initState(baseState);
|
||||
|
||||
this.cLogger.debug('Inserting initial state into cache');
|
||||
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);
|
||||
} else {
|
||||
executionContext.handler?.initState(cachedState.cachedValue.state);
|
||||
return cachedState;
|
||||
}
|
||||
}
|
||||
// eval state for the missing transactions - starting from the latest value from cache.
|
||||
@@ -123,11 +123,9 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
contractTxId: string,
|
||||
sortKey?: string
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
|
||||
this.cLogger.debug('Searching for', { contractTxId, sortKey });
|
||||
this.cLogger.debug('Searching for', {contractTxId, sortKey});
|
||||
if (sortKey) {
|
||||
const stateCache = (await this.cache.getLessOrEqual(contractTxId, sortKey)) as SortKeyCacheResult<
|
||||
EvalStateResult<State>
|
||||
>;
|
||||
const stateCache = (await this.cache.getLessOrEqual(contractTxId, sortKey)) as SortKeyCacheResult<EvalStateResult<State>>;
|
||||
if (stateCache) {
|
||||
this.cLogger.debug(`Latest available state at ${contractTxId}: ${stateCache.sortKey}`);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
break;
|
||||
}
|
||||
|
||||
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||
contract.interactionState().setInitial(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||
|
||||
const missingInteraction = missingInteractions[i];
|
||||
const singleInteractionBenchmark = Benchmark.measure();
|
||||
@@ -150,7 +150,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
let newState: EvalStateResult<unknown> = null;
|
||||
try {
|
||||
await writingContract.readState(missingInteraction.sortKey);
|
||||
newState = contract.getUncommittedState(contract.txId());
|
||||
newState = contract.interactionState().get(contract.txId());
|
||||
} catch (e) {
|
||||
if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') {
|
||||
this.logger.warn('Skipping unsafe contract in internal write');
|
||||
@@ -290,12 +290,16 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
if (contract.isRoot()) {
|
||||
// update the uncommitted state of the root contract
|
||||
if (lastConfirmedTxState) {
|
||||
contract.setUncommittedState(contract.txId(), lastConfirmedTxState.state);
|
||||
await contract.commitStates(missingInteraction);
|
||||
contract.interactionState().update(contract.txId(), lastConfirmedTxState.state);
|
||||
if (validity[missingInteraction.id]) {
|
||||
await contract.interactionState().commit(missingInteraction);
|
||||
} else {
|
||||
await contract.interactionState().rollback(missingInteraction);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if that's an inner contract call - only update the state in the uncommitted states
|
||||
contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||
contract.interactionState().update(contract.txId(), new EvalStateResult(currentState, validity, errorMessages));
|
||||
}
|
||||
}
|
||||
const evalStateResult = new EvalStateResult<State>(currentState, validity, errorMessages);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Arweave from 'arweave';
|
||||
import { rustWasmImports, WarpContractsCrateVersion } from './wasm/rust-wasm-imports';
|
||||
import * as vm2 from 'vm2';
|
||||
import { WarpCache } from '../../../cache/WarpCache';
|
||||
import { ContractDefinition } from '../../../core/ContractDefinition';
|
||||
import { ExecutionContext } from '../../../core/ExecutionContext';
|
||||
import { GQLNodeInterface } from '../../../legacy/gqlResult';
|
||||
@@ -13,10 +12,10 @@ import { EvalStateResult, EvaluationOptions } from '../StateEvaluator';
|
||||
import { JsHandlerApi } from './handler/JsHandlerApi';
|
||||
import { WasmHandlerApi } from './handler/WasmHandlerApi';
|
||||
import { normalizeContractSource } from './normalize-source';
|
||||
import { MemCache } from '../../../cache/impl/MemCache';
|
||||
import { Warp } from '../../Warp';
|
||||
import { isBrowser } from '../../../utils/utils';
|
||||
import { Buffer } from 'warp-isomorphic';
|
||||
import { InteractionState } from '../../../contract/states/InteractionState';
|
||||
|
||||
// 'require' to fix esbuild adding same lib in both cjs and esm format
|
||||
// https://github.com/evanw/esbuild/issues/1950
|
||||
@@ -37,15 +36,13 @@ export class ContractError<T> extends Error {
|
||||
export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknown>> {
|
||||
private readonly logger = LoggerFactory.INST.create('HandlerExecutorFactory');
|
||||
|
||||
// TODO: cache compiled wasm binaries here.
|
||||
private readonly cache: WarpCache<string, WebAssembly.Module> = new MemCache();
|
||||
|
||||
constructor(private readonly arweave: Arweave) {}
|
||||
|
||||
async create<State>(
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
evaluationOptions: EvaluationOptions,
|
||||
warp: Warp
|
||||
warp: Warp,
|
||||
interactionState: InteractionState
|
||||
): Promise<HandlerApi<State>> {
|
||||
if (warp.hasPlugin('contract-blacklist')) {
|
||||
const blacklistPlugin = warp.loadPlugin<string, Promise<boolean>>('contract-blacklist');
|
||||
@@ -75,6 +72,7 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
|
||||
owner: contractDefinition.owner
|
||||
},
|
||||
evaluationOptions,
|
||||
interactionState,
|
||||
kvStorage
|
||||
);
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
? `Internal write auto error for call [${JSON.stringify(debugData)}]: ${result.errorMessage}`
|
||||
: result.errorMessage;
|
||||
|
||||
calleeContract.setUncommittedState(calleeContract.txId(), {
|
||||
calleeContract.interactionState().update(calleeContract.txId(), {
|
||||
state: result.state as State,
|
||||
validity: {
|
||||
...result.originalValidity,
|
||||
@@ -145,19 +145,21 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
|
||||
// (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
|
||||
const result = returnValidity
|
||||
? {
|
||||
state: deepCopy(stateWithValidity.cachedValue.state),
|
||||
validity: stateWithValidity.cachedValue.validity,
|
||||
errorMessages: stateWithValidity.cachedValue.errorMessages
|
||||
}
|
||||
: deepCopy(stateWithValidity.cachedValue.state);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
protected assignRefreshState(executionContext: ExecutionContext<State>) {
|
||||
this.swGlobal.contracts.refreshState = async () => {
|
||||
return executionContext.contract.getUncommittedState(this.swGlobal.contract.id)?.state;
|
||||
return executionContext.contract.interactionState().get(this.swGlobal.contract.id)?.state;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Arweave from 'arweave';
|
||||
import { EvaluationOptions } from '../core/modules/StateEvaluator';
|
||||
import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult';
|
||||
import { BatchDBOp, CacheKey, PutBatch, SortKeyCache } from '../cache/SortKeyCache';
|
||||
import {InteractionState} from "../contract/states/InteractionState";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -63,6 +64,7 @@ export class SmartWeaveGlobal {
|
||||
arweave: Arweave,
|
||||
contract: { id: string; owner: string },
|
||||
evaluationOptions: EvaluationOptions,
|
||||
interactionState: InteractionState,
|
||||
storage: SortKeyCache<any> | null
|
||||
) {
|
||||
this.gasUsed = 0;
|
||||
@@ -104,7 +106,7 @@ export class SmartWeaveGlobal {
|
||||
|
||||
this.extensions = {};
|
||||
|
||||
this.kv = new KV(storage, this.transaction);
|
||||
this.kv = new KV(storage, interactionState, this.transaction, this.contract.id);
|
||||
}
|
||||
|
||||
useGas(gas: number) {
|
||||
@@ -257,7 +259,11 @@ export class SWVrf {
|
||||
export class KV {
|
||||
private _kvBatch: BatchDBOp<any>[] = [];
|
||||
|
||||
constructor(private readonly _storage: SortKeyCache<any> | null, private readonly _transaction: SWTransaction) {}
|
||||
constructor(
|
||||
private readonly _storage: SortKeyCache<any> | null,
|
||||
private readonly _interactionState: InteractionState,
|
||||
private readonly _transaction: SWTransaction,
|
||||
private readonly _contractTxId: string) {}
|
||||
|
||||
async put(key: string, value: any): Promise<void> {
|
||||
this.checkStorageAvailable();
|
||||
@@ -272,13 +278,20 @@ export class KV {
|
||||
this.checkStorageAvailable();
|
||||
const sortKey = this._transaction.sortKey;
|
||||
|
||||
// first we're checking if the value exists in changes registered for the activeTx
|
||||
if (this._kvBatch.length > 0) {
|
||||
const putBatches = this._kvBatch.filter((batchOp) => batchOp.type === 'put') as PutBatch<any>[];
|
||||
const matchingPutBatch = putBatches.reverse().find((batchOp) => {
|
||||
return batchOp.key.key === key && batchOp.key.sortKey === sortKey;
|
||||
}) as PutBatch<V>;
|
||||
if (matchingPutBatch !== undefined) {
|
||||
return matchingPutBatch.value;
|
||||
const activeTxValue = this.findInBatches<V>(this._kvBatch, key, sortKey);
|
||||
if (activeTxValue !== undefined) {
|
||||
return activeTxValue;
|
||||
}
|
||||
}
|
||||
|
||||
// then we're checking if the values exists in the interactionState
|
||||
const interactionStateBatch = this._interactionState.getKV(this._contractTxId);
|
||||
if (interactionStateBatch?.length > 0) {
|
||||
const interactionStateValue = this.findInBatches<V>(interactionStateBatch, key, sortKey);
|
||||
if (interactionStateValue !== undefined) {
|
||||
return interactionStateValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,11 +299,18 @@ export class KV {
|
||||
return result?.cachedValue || null;
|
||||
}
|
||||
|
||||
private findInBatches<V>(batches: BatchDBOp<any>[], key: string, sortKey: string): V | undefined {
|
||||
const putBatches = batches.filter((batchOp) => batchOp.type === 'put') as PutBatch<any>[];
|
||||
const matchingPutBatch = putBatches.reverse().find((batchOp) => {
|
||||
return batchOp.key.key === key && batchOp.key.sortKey === sortKey;
|
||||
}) as PutBatch<V>;
|
||||
|
||||
return matchingPutBatch?.value;
|
||||
}
|
||||
|
||||
async commit(): Promise<void> {
|
||||
if (this._storage) {
|
||||
if (!this._transaction.dryRun) {
|
||||
await this._storage.batch(this._kvBatch);
|
||||
}
|
||||
this._interactionState.updateKV(this._contractTxId, [...this._kvBatch])
|
||||
this._kvBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import Arweave from 'arweave';
|
||||
import { LoggerFactory } from '../logging/LoggerFactory';
|
||||
import { WarpCache } from '../cache/WarpCache';
|
||||
import { ContractDefinition } from '../core/ContractDefinition';
|
||||
import { ExecutorFactory } from '../core/modules/ExecutorFactory';
|
||||
import { EvaluationOptions } from '../core/modules/StateEvaluator';
|
||||
import { Warp } from '../core/Warp';
|
||||
|
||||
/**
|
||||
* An implementation of ExecutorFactory that adds caching capabilities
|
||||
*/
|
||||
export class CacheableExecutorFactory<Api> implements ExecutorFactory<Api> {
|
||||
private readonly logger = LoggerFactory.INST.create('CacheableExecutorFactory');
|
||||
|
||||
constructor(
|
||||
private readonly arweave: Arweave,
|
||||
private readonly baseImplementation: ExecutorFactory<Api>,
|
||||
private readonly cache: WarpCache<string, Api>
|
||||
) {}
|
||||
|
||||
async create<State>(
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
evaluationOptions: EvaluationOptions,
|
||||
warp: Warp
|
||||
): Promise<Api> {
|
||||
return await this.baseImplementation.create(contractDefinition, evaluationOptions, warp);
|
||||
|
||||
// warn: do not cache on the contractDefinition.srcTxId. This might look like a good optimisation
|
||||
// (as many contracts share the same source code), but unfortunately this is causing issues
|
||||
// with the same SwGlobal object being cached for all contracts with the same source code
|
||||
// (eg. SwGlobal.contract.id field - which of course should have different value for different contracts
|
||||
// that share the same source).
|
||||
// warn#2: cache key MUST be a combination of both txId and srcTxId -
|
||||
// as "evolve" feature changes the srcTxId for the given txId...
|
||||
|
||||
// switching off caching for now
|
||||
// - https://github.com/redstone-finance/redstone-smartcontracts/issues/53
|
||||
// probably should be cached on a lower level - i.e. either handler function (for js contracts)
|
||||
// or wasm module.
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import { ContractDefinition } from '../core/ContractDefinition';
|
||||
import { ExecutorFactory } from '../core/modules/ExecutorFactory';
|
||||
import { EvaluationOptions } from '../core/modules/StateEvaluator';
|
||||
import { Warp } from '../core/Warp';
|
||||
import { InteractionState } from '../contract/states/InteractionState';
|
||||
|
||||
/**
|
||||
* An ExecutorFactory that allows to substitute original contract's source code.
|
||||
* Useful for debugging purposes (eg. to quickly add some console.logs in contract
|
||||
* Useful for debugging purposes (e.g. to quickly add some console.logs in contract
|
||||
* or to test a fix or a new feature - without the need of redeploying a new contract on Arweave);
|
||||
*
|
||||
* Not meant to be used in production env! ;-)
|
||||
@@ -20,7 +21,8 @@ export class DebuggableExecutorFactory<Api> implements ExecutorFactory<Api> {
|
||||
async create<State>(
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
evaluationOptions: EvaluationOptions,
|
||||
warp: Warp
|
||||
warp: Warp,
|
||||
interactionState: InteractionState
|
||||
): Promise<Api> {
|
||||
if (Object.prototype.hasOwnProperty.call(this.sourceCode, contractDefinition.txId)) {
|
||||
contractDefinition = {
|
||||
@@ -29,6 +31,6 @@ export class DebuggableExecutorFactory<Api> implements ExecutorFactory<Api> {
|
||||
};
|
||||
}
|
||||
|
||||
return await this.baseImplementation.create(contractDefinition, evaluationOptions, warp);
|
||||
return await this.baseImplementation.create(contractDefinition, evaluationOptions, warp, interactionState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ export class Evolve implements ExecutionContextModifier {
|
||||
const newHandler = (await executorFactory.create<State>(
|
||||
newContractDefinition,
|
||||
executionContext.evaluationOptions,
|
||||
executionContext.warp
|
||||
executionContext.warp,
|
||||
executionContext.contract.interactionState()
|
||||
)) as HandlerApi<State>;
|
||||
|
||||
//FIXME: side-effect...
|
||||
|
||||
Reference in New Issue
Block a user