feat: AutoSync connected contracts when syncState #333
This commit is contained in:
197
src/__tests__/integration/basic/pst-auto-sync.test.ts
Normal file
197
src/__tests__/integration/basic/pst-auto-sync.test.ts
Normal file
@@ -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<Response>;
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
219
src/__tests__/integration/basic/pst-kv-auto-sync.test.ts
Normal file
219
src/__tests__/integration/basic/pst-kv-auto-sync.test.ts
Normal file
@@ -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<Response>;
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,18 @@ export interface WriteInteractionResponse {
|
||||
originalTxId: string;
|
||||
}
|
||||
|
||||
export interface DREContractStatusResponse<State> {
|
||||
status: string;
|
||||
contractTxId: string;
|
||||
state: State;
|
||||
validity: Record<string, boolean>;
|
||||
errorMessages: Record<string, string>;
|
||||
sortKey: string;
|
||||
timestamp: string;
|
||||
signature: string;
|
||||
stateHash: string;
|
||||
}
|
||||
|
||||
export type WarpOptions = {
|
||||
vrf?: boolean;
|
||||
disableBundling?: boolean;
|
||||
|
||||
@@ -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']
|
||||
};
|
||||
|
||||
|
||||
@@ -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<State> implements Contract<State> {
|
||||
private _children: HandlerBasedContract<unknown>[] = [];
|
||||
|
||||
private _uncommittedStates = new Map<string, EvalStateResult<unknown>>();
|
||||
private _dreStates = new Map<string, SortKeyCacheResult<EvalStateResult<State>>>();
|
||||
|
||||
private readonly mutex = new Mutex();
|
||||
|
||||
@@ -471,13 +479,14 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
const { definitionLoader, interactionsLoader, stateEvaluator } = this.warp;
|
||||
|
||||
const benchmark = Benchmark.measure();
|
||||
const cachedState = await stateEvaluator.latestAvailableState<State>(contractTxId, upToSortKey);
|
||||
let cachedState = await stateEvaluator.latestAvailableState<State>(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<State> implements Contract<State> {
|
||||
contractDefinition = await definitionLoader.load<State>(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<State> implements Contract<State> {
|
||||
return this.getRootEoEvaluator().forForeignContract(rootManifestEvalOptions);
|
||||
}
|
||||
|
||||
private async getRemoteContractState(contractId: string): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
||||
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<DREContractStatusResponse<State> | 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<State> implements Contract<State> {
|
||||
this.warp.interactionsLoader.clearCache();
|
||||
this._children = [];
|
||||
this._uncommittedStates = new Map();
|
||||
this._dreStates = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +860,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
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<State>(this._contractTxId, response.sortKey, response.state, response.validity);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -900,6 +940,55 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeSyncStateWithRemoteSource(
|
||||
remoteState: SortKeyCacheResult<EvalStateResult<State>>,
|
||||
upToSortKey: string,
|
||||
cachedState: SortKeyCacheResult<EvalStateResult<State>>
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
||||
const { stateEvaluator } = this.warp;
|
||||
if (this.isStateHigherThanAndUpTo(remoteState, cachedState?.sortKey, upToSortKey)) {
|
||||
return await stateEvaluator.syncState<State>(
|
||||
this._contractTxId,
|
||||
remoteState.sortKey,
|
||||
remoteState.cachedValue.state,
|
||||
remoteState.cachedValue.validity
|
||||
);
|
||||
}
|
||||
return cachedState;
|
||||
}
|
||||
|
||||
private isStateHigherThanAndUpTo(
|
||||
remoteState: SortKeyCacheResult<EvalStateResult<State>>,
|
||||
fromSortKey: string,
|
||||
upToSortKey: string
|
||||
) {
|
||||
return (
|
||||
remoteState &&
|
||||
(!upToSortKey || upToSortKey >= remoteState.sortKey) &&
|
||||
(!fromSortKey || remoteState.sortKey > fromSortKey)
|
||||
);
|
||||
}
|
||||
|
||||
setDREState(
|
||||
contractTxId: string,
|
||||
result: DREContractStatusResponse<State>
|
||||
): SortKeyCacheResult<EvalStateResult<State>> {
|
||||
const dreCachedState = new SortKeyCacheResult(
|
||||
result.sortKey,
|
||||
new EvalStateResult(result.state, {}, result.errorMessages)
|
||||
);
|
||||
this.getRoot()._dreStates.set(contractTxId, dreCachedState);
|
||||
return dreCachedState;
|
||||
}
|
||||
|
||||
getDreState(contractTxId: string): SortKeyCacheResult<EvalStateResult<State>> {
|
||||
return this.getRoot()._dreStates.get(contractTxId) as SortKeyCacheResult<EvalStateResult<State>>;
|
||||
}
|
||||
|
||||
hasDreState(contractTxId: string): boolean {
|
||||
return this.getRoot()._dreStates.has(contractTxId);
|
||||
}
|
||||
|
||||
private getRoot(): HandlerBasedContract<unknown> {
|
||||
let result: Contract = this;
|
||||
while (!result.isRoot()) {
|
||||
|
||||
@@ -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<string, boolean>): Promise<void>;
|
||||
syncState<State>(
|
||||
contractTxId: string,
|
||||
sortKey: string,
|
||||
state: State,
|
||||
validity: Record<string, boolean>
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>>;
|
||||
|
||||
internalWriteState<State>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -187,14 +187,15 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
await this.cache.put(new CacheKey(contractTxId, transaction.sortKey), stateToCache);
|
||||
}
|
||||
|
||||
async syncState(
|
||||
async syncState<State>(
|
||||
contractTxId: string,
|
||||
sortKey: string,
|
||||
state: unknown,
|
||||
state: State,
|
||||
validity: Record<string, boolean>
|
||||
): Promise<void> {
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>> {
|
||||
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
|
||||
|
||||
@@ -392,12 +392,12 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
state: EvalStateResult<State>
|
||||
): Promise<void>;
|
||||
|
||||
abstract syncState(
|
||||
abstract syncState<State>(
|
||||
contractTxId: string,
|
||||
sortKey: string,
|
||||
state: unknown,
|
||||
state: State,
|
||||
validity: Record<string, boolean>
|
||||
): Promise<void>;
|
||||
): Promise<SortKeyCacheResult<EvalStateResult<State>>>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
abstract dumpCache(): Promise<any>;
|
||||
|
||||
Reference in New Issue
Block a user