diff --git a/src/__tests__/integration/basic/pst-auto-sync.test.ts b/src/__tests__/integration/basic/pst-auto-sync.test.ts new file mode 100644 index 0000000..423bdbd --- /dev/null +++ b/src/__tests__/integration/basic/pst-auto-sync.test.ts @@ -0,0 +1,197 @@ +import fs from 'fs'; + +import ArLocal from 'arlocal'; +import Arweave from 'arweave'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import path from 'path'; +import { mineBlock } from '../_helpers'; +import { PstState, PstContract } from '../../../contract/PstContract'; +import { Warp } from '../../../core/Warp'; +import { WarpFactory } from '../../../core/WarpFactory'; +import { LoggerFactory } from '../../../logging/LoggerFactory'; +import { DeployPlugin } from "warp-contracts-plugin-deploy"; + +// note: each tests suit (i.e. file with tests that Jest is running concurrently +// with another files has to have ArLocal set to a different port!) +const AR_PORT = 1825; + +describe('Testing the Profit Sharing Token', () => { + let contractSrc: string; + + let wallet: JWKInterface; + let walletAddress: string; + + let initialState: PstState; + + let arweave: Arweave; + let arlocal: ArLocal; + let warp: Warp; + let contractTxId: string; + let pst: PstContract; + + const actualFetch = global.fetch + let responseData = { + sortKey: "", + state: {} + } + let firstSortKey = '' + const remoteCalls = { + total: 0, + measure: function() { + const self = this; + const start = self.total; + return { + diff: () => self.total - start + } + } + } + + const localWarp = async function() { + if (!arlocal) { + arlocal = new ArLocal(AR_PORT, false); + await arlocal.start(); + } + return WarpFactory.forLocal(AR_PORT).use(new DeployPlugin()); + } + + const autoSyncPst = async function() { + const autoSyncPst = (await localWarp()).pst(contractTxId); + autoSyncPst.setEvaluationOptions({ + remoteStateSyncEnabled: true + }) + return autoSyncPst; + } + + beforeAll(async () => { + LoggerFactory.INST.logLevel('error'); + + warp = await localWarp(); + ({ arweave } = warp); + ({ jwk: wallet, address: walletAddress } = await warp.generateWallet()); + + contractSrc = 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')); + + initialState = { + ...stateFromFile, + ...{ + owner: walletAddress, + balances: { + ...stateFromFile.balances, + [walletAddress]: 555669 + } + } + }; + + // deploying contract using the new SDK. + ({ contractTxId } = await warp.deploy({ + wallet, + initState: JSON.stringify(initialState), + src: contractSrc + })); + + // connecting to the PST contract + pst = warp.pst(contractTxId); + + // connecting wallet to the PST contract + pst.connect(wallet); + + await mineBlock(warp); + + jest + .spyOn(global, 'fetch') + .mockImplementation( async (input: string, init) => { + if (input.includes(pst.evaluationOptions().remoteStateSyncSource)) { + remoteCalls.total++; + return Promise.resolve({ json: () => Promise.resolve(responseData), ok: true, status: 200 }) as Promise; + } + return actualFetch(input, init); + }) + }); + + afterAll(async () => { + await arlocal.stop(); + jest.restoreAllMocks(); + }); + + it('should read pst state and balance data', async () => { + const dreCalls = remoteCalls.measure(); + const state = await pst.readState(); + firstSortKey = state.sortKey; + responseData.sortKey = state.sortKey; + responseData.state = state.cachedValue.state; + + expect(state.cachedValue.state).toEqual(initialState); + const syncPst = await autoSyncPst(); + expect(await syncPst.currentState()).toEqual(initialState); + + expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000); + expect((await syncPst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000); + expect((await pst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).balance).toEqual(23111222); + expect((await syncPst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).balance).toEqual(23111222); + expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669); + expect((await syncPst.currentBalance(walletAddress)).balance).toEqual(555669); + expect(dreCalls.diff()).toEqual(4); + }); + + it('should properly transfer tokens and ignore remote source', async () => { + const dreCalls = remoteCalls.measure(); + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 100 + }); + + await mineBlock(warp); + const state = await pst.readState(); + responseData.sortKey = state.sortKey; + responseData.state = state.cachedValue.state; + + expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 100); + expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 100); + expect(dreCalls.diff()).toEqual(0); + }); + + it('should transfer tokens and read state from remote source', async () => { + const dreCalls = remoteCalls.measure(); + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 450 + }); + + await mineBlock(warp); + + expect((await pst.currentState()).balances[walletAddress]).toEqual(555669 - 550); + expect((await pst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 550); + + const syncPst = await autoSyncPst(); + expect((await syncPst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000 + 100); + expect((await syncPst.currentState()).balances[walletAddress]).toEqual(555669 - 100); + + syncPst.setEvaluationOptions({ + remoteStateSyncEnabled: false + }) + expect((await syncPst.currentState()).balances[walletAddress]).toEqual(555669 - 550); + expect((await syncPst.currentState()).balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual( + 10000000 + 550); + expect(dreCalls.diff()).toEqual(2); + }); + + it('should read pst state for previous sortKey', async () => { + const dreCalls = remoteCalls.measure(); + + const state = await pst.readState(firstSortKey); + expect(state.cachedValue.state.balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000); + expect(state.cachedValue.state.balances['33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA']).toEqual(23111222); + + expect(state.cachedValue.state).toEqual(initialState); + const syncPst = await autoSyncPst(); + + + const syncState = (await syncPst.readState(firstSortKey)); + expect(await syncState.cachedValue.state).toEqual(initialState); + expect(syncState.cachedValue.state.balances['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M']).toEqual(10000000); + expect(syncState.cachedValue.state.balances['33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA']).toEqual(23111222); + expect(dreCalls.diff()).toEqual(1); + }); + +}); diff --git a/src/__tests__/integration/basic/pst-kv-auto-sync.test.ts b/src/__tests__/integration/basic/pst-kv-auto-sync.test.ts new file mode 100644 index 0000000..3151980 --- /dev/null +++ b/src/__tests__/integration/basic/pst-kv-auto-sync.test.ts @@ -0,0 +1,219 @@ +import fs from 'fs'; + +import ArLocal from 'arlocal'; +import Arweave from 'arweave'; +import { JWKInterface } from 'arweave/node/lib/wallet'; +import path from 'path'; +import { mineBlock } from '../_helpers'; +import { PstState, PstContract } from '../../../contract/PstContract'; +import { Warp } from '../../../core/Warp'; +import { DEFAULT_LEVEL_DB_LOCATION, defaultCacheOptions, WarpFactory } from "../../../core/WarpFactory"; +import { LoggerFactory } from '../../../logging/LoggerFactory'; +import { DeployPlugin } from "warp-contracts-plugin-deploy"; + +// note: each tests suit (i.e. file with tests that Jest is running concurrently +// with another files has to have ArLocal set to a different port!) +const AR_PORT = 1826; + + +describe('Testing the Profit Sharing Token', () => { + let contractSrc: string; + + let wallet: JWKInterface; + let walletAddress: string; + + let initialState: PstState; + + let arweave: Arweave; + let arlocal: ArLocal; + let warp: Warp; + let contractTxId: string; + let pst: PstContract; + + const actualFetch = global.fetch + let responseData = { + sortKey: "", + state: {} + } + const remoteCalls = { + total: 0, + measure: function() { + const self = this; + const start = self.total; + return { + diff: () => self.total - start + } + } + } + + const localWarp = async function() { + if (!arlocal) { + arlocal = new ArLocal(AR_PORT, false); + await arlocal.start(); + + arweave = Arweave.init({ + host: 'localhost', + port: AR_PORT, + protocol: 'http' + }) + } + + return WarpFactory.forLocal(AR_PORT, arweave, { ...defaultCacheOptions, inMemory: true }).use(new DeployPlugin()); + } + + const autoSyncPst = async function() { + // const autoSyncPst = warp.pst(contractTxId); + const autoSyncPst = (await localWarp()).pst(contractTxId); + autoSyncPst.setEvaluationOptions({ + remoteStateSyncEnabled: true + }) + return autoSyncPst; + } + + beforeAll(async () => { + LoggerFactory.INST.logLevel('error'); + + warp = await localWarp(); + ({ jwk: wallet, address: walletAddress } = await warp.generateWallet()); + + contractSrc = fs.readFileSync(path.join(__dirname, '../data/kv-storage.js'), 'utf8'); + const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8')); + + initialState = { + ...stateFromFile, + ...{ + owner: walletAddress + } + }; + + // deploying contract using the new SDK. + ({ contractTxId } = await warp.deploy({ + wallet, + initState: JSON.stringify(initialState), + src: contractSrc, + evaluationManifest: { + evaluationOptions: { + useKVStorage: true, + } + } + })); + + pst = warp.pst(contractTxId); + pst.connect(wallet); + + await mineBlock(warp); + + jest + .spyOn(global, 'fetch') + .mockImplementation( async (input: string, init) => { + if (input.includes(pst.evaluationOptions().remoteStateSyncSource)) { + remoteCalls.total++; + return Promise.resolve({ json: () => Promise.resolve(responseData), ok: true, status: 200 }) as Promise; + } + return actualFetch(input, init); + }) + }); + + afterAll(async () => { + await arlocal.stop(); + jest.restoreAllMocks(); + fs.rmSync(`${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`, { recursive: true }); + }); + + it('should initialize', async () => { + // this is done to "initialize" the state + await pst.writeInteraction({ + function: 'mint', + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 10000000 + }); + await mineBlock(warp); + + await pst.writeInteraction({ + function: 'mint', + target: '33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA', + qty: 23111222 + }); + await mineBlock(warp); + + await pst.writeInteraction({ + function: 'mint', + target: walletAddress, + qty: 555669 + }); + await mineBlock(warp); + }); + + it('should properly transfer tokens and ignore remote source', async () => { + const dreCalls = remoteCalls.measure(); + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 100 + }); + + await mineBlock(warp); + const state = await pst.readState(); + responseData.sortKey = state.sortKey; + responseData.state = state.cachedValue.state; + + expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669 - 100); + expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000 + 100); + expect(dreCalls.diff()).toEqual(0); + }); + + it('should properly transfer tokens and read state from remote source', async () => { + const dreCalls = remoteCalls.measure(); + await pst.transfer({ + target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M', + qty: 400 + }); + + await mineBlock(warp); + + expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669 - 500); + expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000 + 500); + + expect(dreCalls.diff()).toEqual(0); + }); + + it('should properly read storage value', async () => { + expect((await pst.getStorageValues([walletAddress])).cachedValue.get(walletAddress)).toEqual(555669 - 500); + expect( + (await pst.getStorageValues(['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'])).cachedValue.get( + 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M' + ) + ).toEqual(10000000 + 500); + expect( + (await pst.getStorageValues(['33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'])).cachedValue.get( + '33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA' + ) + ).toEqual(23111222); + expect((await pst.getStorageValues(['foo'])).cachedValue.get('foo')).toBeNull(); + fs.rmSync(`${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`, { recursive: true }); + }); + + it('should properly calculate kv storage with auto sync', async () => { + const dreCalls = remoteCalls.measure(); + + const syncPst = await autoSyncPst(); + + expect(await syncPst.currentState()).toEqual(initialState); + expect((await syncPst.currentBalance(walletAddress)).balance).toEqual(555669 - 500); + expect((await syncPst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000 + 500); + + expect((await syncPst.getStorageValues([walletAddress])).cachedValue.get(walletAddress)).toEqual(555669 - 500); + expect( + (await syncPst.getStorageValues(['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'])).cachedValue.get( + 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M' + ) + ).toEqual(10000000 + 500); + expect( + (await syncPst.getStorageValues(['33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'])).cachedValue.get( + '33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA' + ) + ).toEqual(23111222); + expect((await syncPst.getStorageValues(['foo'])).cachedValue.get('foo')).toBeNull(); + expect(dreCalls.diff()).toEqual(0); + }); + +}); diff --git a/src/__tests__/unit/evaluation-options.test.ts b/src/__tests__/unit/evaluation-options.test.ts index cd2b569..c9e9bd2 100644 --- a/src/__tests__/unit/evaluation-options.test.ts +++ b/src/__tests__/unit/evaluation-options.test.ts @@ -17,6 +17,8 @@ describe('Evaluation options evaluator', () => { maxCallDepth: 7, maxInteractionEvaluationTimeSeconds: 60, mineArLocalBlocks: true, + remoteStateSyncEnabled: false, + remoteStateSyncSource: "https://dre-1.warp.cc/contract", sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/', sourceType: SourceType.BOTH, stackTrace: { @@ -54,6 +56,8 @@ describe('Evaluation options evaluator', () => { maxCallDepth: 7, maxInteractionEvaluationTimeSeconds: 60, mineArLocalBlocks: true, + remoteStateSyncEnabled: false, + remoteStateSyncSource: "https://dre-1.warp.cc/contract", sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/', sourceType: SourceType.BOTH, stackTrace: { @@ -85,7 +89,8 @@ describe('Evaluation options evaluator', () => { maxCallDepth: 5, maxInteractionEvaluationTimeSeconds: 60, mineArLocalBlocks: true, - sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/', + remoteStateSyncEnabled: false, + remoteStateSyncSource: "https://dre-1.warp.cc/contract", sequencerUrl: 'https://d1o5nlqr4okus2.cloudfront.net/', sourceType: 'both', stackTrace: { saveState: false diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 13f2033..aaedfd5 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -21,6 +21,18 @@ export interface WriteInteractionResponse { originalTxId: string; } +export interface DREContractStatusResponse { + status: string; + contractTxId: string; + state: State; + validity: Record; + errorMessages: Record; + sortKey: string; + timestamp: string; + signature: string; + stateHash: string; +} + export type WarpOptions = { vrf?: boolean; disableBundling?: boolean; diff --git a/src/contract/EvaluationOptionsEvaluator.ts b/src/contract/EvaluationOptionsEvaluator.ts index ddf5883..b4be55c 100644 --- a/src/contract/EvaluationOptionsEvaluator.ts +++ b/src/contract/EvaluationOptionsEvaluator.ts @@ -104,6 +104,8 @@ export class EvaluationOptionsEvaluator { walletBalanceUrl: () => this.rootOptions['walletBalanceUrl'], mineArLocalBlocks: () => this.rootOptions['mineArLocalBlocks'], cacheEveryNInteractions: () => this.rootOptions['cacheEveryNInteractions'], + remoteStateSyncEnabled: () => this.rootOptions['remoteStateSyncEnabled'], + remoteStateSyncSource: () => this.rootOptions['remoteStateSyncSource'], useKVStorage: (foreignOptions) => foreignOptions['useKVStorage'] }; diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 379c0ed..10a536b 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -20,7 +20,14 @@ import { LoggerFactory } from '../logging/LoggerFactory'; import { Evolve } from '../plugins/Evolve'; import { ArweaveWrapper } from '../utils/ArweaveWrapper'; import { sleep } from '../utils/utils'; -import { BenchmarkStats, Contract, InnerCallData, WriteInteractionOptions, WriteInteractionResponse } from './Contract'; +import { + BenchmarkStats, + Contract, + DREContractStatusResponse, + InnerCallData, + WriteInteractionOptions, + WriteInteractionResponse +} from './Contract'; import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract'; import { InnerWritesEvaluator } from './InnerWritesEvaluator'; import { generateMockVrf } from '../utils/vrf'; @@ -58,6 +65,7 @@ export class HandlerBasedContract implements Contract { private _children: HandlerBasedContract[] = []; private _uncommittedStates = new Map>(); + private _dreStates = new Map>>(); private readonly mutex = new Mutex(); @@ -471,13 +479,14 @@ export class HandlerBasedContract implements Contract { const { definitionLoader, interactionsLoader, stateEvaluator } = this.warp; const benchmark = Benchmark.measure(); - const cachedState = await stateEvaluator.latestAvailableState(contractTxId, upToSortKey); + let cachedState = await stateEvaluator.latestAvailableState(contractTxId, upToSortKey); this.logger.debug('cache lookup', benchmark.elapsed()); benchmark.reset(); const evolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state); - let handler, contractDefinition, sortedInteractions, contractEvaluationOptions; + let handler, contractDefinition, contractEvaluationOptions, remoteState; + let sortedInteractions = interactions || []; this.logger.debug('Cached state', cachedState, upToSortKey); @@ -502,22 +511,29 @@ export class HandlerBasedContract implements Contract { contractDefinition = await definitionLoader.load(contractTxId, evolvedSrcTxId); contractEvaluationOptions = this.resolveEvaluationOptions(contractDefinition.manifest?.evaluationOptions); - sortedInteractions = interactions - ? interactions - : await interactionsLoader.load( - contractTxId, - cachedState?.sortKey, - this.getToSortKey(upToSortKey), - contractEvaluationOptions - ); + if (contractEvaluationOptions.remoteStateSyncEnabled && !contractEvaluationOptions.useKVStorage) { + remoteState = await this.getRemoteContractState(contractTxId); + cachedState = await this.maybeSyncStateWithRemoteSource(remoteState, upToSortKey, cachedState); + } - // (2) ...but we still need to return only interactions up to original "upToSortKey" + if (!remoteState && sortedInteractions.length == 0) { + sortedInteractions = await interactionsLoader.load( + contractTxId, + cachedState?.sortKey, + this.getToSortKey(upToSortKey), + contractEvaluationOptions + ); + } + + // we still need to return only interactions up to original "upToSortKey" if (cachedState?.sortKey) { sortedInteractions = sortedInteractions.filter((i) => i.sortKey.localeCompare(cachedState?.sortKey) > 0); } + if (upToSortKey) { sortedInteractions = sortedInteractions.filter((i) => i.sortKey.localeCompare(upToSortKey) <= 0); } + this.logger.debug('contract and interactions load', benchmark.elapsed()); if (this.isRoot() && sortedInteractions.length) { // note: if the root contract has zero interactions, it still should be safe @@ -563,6 +579,29 @@ export class HandlerBasedContract implements Contract { return this.getRootEoEvaluator().forForeignContract(rootManifestEvalOptions); } + private async getRemoteContractState(contractId: string): Promise>> { + if (this.hasDreState(contractId)) { + return this.getDreState(contractId); + } else { + const dreResponse = await this.fetchRemoteContractState(contractId); + if (dreResponse != null) { + return this.setDREState(contractId, dreResponse); + } + return null; + } + } + + private async fetchRemoteContractState(contractId: string): Promise | null> { + return this.warpFetchWrapper + .fetch(`${this._evaluationOptions.remoteStateSyncSource}?id=${contractId}&events=false`) + .then((res) => { + return res.ok ? res.json() : Promise.reject(res); + }) + .catch((error) => { + throw new Error(`Unable to read contract state from DRE. ${error.status}. ${error.body?.message}`); + }); + } + private getToSortKey(upToSortKey?: string) { if (this._parentContract?.rootSortKey) { if (!upToSortKey) { @@ -599,6 +638,7 @@ export class HandlerBasedContract implements Contract { this.warp.interactionsLoader.clearCache(); this._children = []; this._uncommittedStates = new Map(); + this._dreStates = new Map(); } } @@ -820,7 +860,7 @@ export class HandlerBasedContract implements Contract { throw new Error(`Unable to retrieve state. ${error.status}: ${error.body?.message}`); }); - await stateEvaluator.syncState(this._contractTxId, response.sortKey, response.state, response.validity); + await stateEvaluator.syncState(this._contractTxId, response.sortKey, response.state, response.validity); return this; } @@ -900,6 +940,55 @@ export class HandlerBasedContract implements Contract { } } + private async maybeSyncStateWithRemoteSource( + remoteState: SortKeyCacheResult>, + upToSortKey: string, + cachedState: SortKeyCacheResult> + ): Promise>> { + const { stateEvaluator } = this.warp; + if (this.isStateHigherThanAndUpTo(remoteState, cachedState?.sortKey, upToSortKey)) { + return await stateEvaluator.syncState( + this._contractTxId, + remoteState.sortKey, + remoteState.cachedValue.state, + remoteState.cachedValue.validity + ); + } + return cachedState; + } + + private isStateHigherThanAndUpTo( + remoteState: SortKeyCacheResult>, + fromSortKey: string, + upToSortKey: string + ) { + return ( + remoteState && + (!upToSortKey || upToSortKey >= remoteState.sortKey) && + (!fromSortKey || remoteState.sortKey > fromSortKey) + ); + } + + setDREState( + contractTxId: string, + result: DREContractStatusResponse + ): SortKeyCacheResult> { + const dreCachedState = new SortKeyCacheResult( + result.sortKey, + new EvalStateResult(result.state, {}, result.errorMessages) + ); + this.getRoot()._dreStates.set(contractTxId, dreCachedState); + return dreCachedState; + } + + getDreState(contractTxId: string): SortKeyCacheResult> { + return this.getRoot()._dreStates.get(contractTxId) as SortKeyCacheResult>; + } + + hasDreState(contractTxId: string): boolean { + return this.getRoot()._dreStates.has(contractTxId); + } + private getRoot(): HandlerBasedContract { let result: Contract = this; while (!result.isRoot()) { diff --git a/src/core/modules/StateEvaluator.ts b/src/core/modules/StateEvaluator.ts index ee6a2d4..0653060 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -68,7 +68,12 @@ export interface StateEvaluator { /** * allows to syncState with an external state source (like Warp Distributed Execution Network) */ - syncState(contractTxId: string, sortKey: string, state: unknown, validity: Record): Promise; + syncState( + contractTxId: string, + sortKey: string, + state: State, + validity: Record + ): Promise>>; internalWriteState( contractTxId: string, @@ -142,6 +147,10 @@ export class DefaultEvaluationOptions implements EvaluationOptions { cacheEveryNInteractions = -1; useKVStorage = false; + + remoteStateSyncEnabled = false; + + remoteStateSyncSource = 'https://dre-1.warp.cc/contract'; } // an interface for the contract EvaluationOptions - can be used to change the behaviour of some features. @@ -226,4 +235,10 @@ export interface EvaluationOptions { // whether a separate key-value storage should be used for the contract useKVStorage: boolean; + + // whether contract state should be acquired from remote source, e.g. D.R.E. + remoteStateSyncEnabled: boolean; + + // remote source for fetching most recent contract state, only applicable if remoteStateSyncEnabled is set to true + remoteStateSyncSource: string; } diff --git a/src/core/modules/impl/CacheableStateEvaluator.ts b/src/core/modules/impl/CacheableStateEvaluator.ts index 75ea0cd..cdb14a4 100644 --- a/src/core/modules/impl/CacheableStateEvaluator.ts +++ b/src/core/modules/impl/CacheableStateEvaluator.ts @@ -187,14 +187,15 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { await this.cache.put(new CacheKey(contractTxId, transaction.sortKey), stateToCache); } - async syncState( + async syncState( contractTxId: string, sortKey: string, - state: unknown, + state: State, validity: Record - ): Promise { + ): Promise>> { const stateToCache = new EvalStateResult(state, validity, {}); await this.cache.put(new CacheKey(contractTxId, sortKey), stateToCache); + return new SortKeyCacheResult(sortKey, stateToCache); } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index fa02053..51bf77d 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -392,12 +392,12 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { state: EvalStateResult ): Promise; - abstract syncState( + abstract syncState( contractTxId: string, sortKey: string, - state: unknown, + state: State, validity: Record - ): Promise; + ): Promise>>; // eslint-disable-next-line @typescript-eslint/no-explicit-any abstract dumpCache(): Promise;