feat: transactional writes from the callee point of view
This commit is contained in:
@@ -9,10 +9,14 @@ export async function handle(state, action) {
|
||||
|
||||
if (action.input.function === 'add') {
|
||||
state.counter++;
|
||||
if (action.input.throw) {
|
||||
throw new ContractError('Error from "add" function');
|
||||
}
|
||||
return { state };
|
||||
}
|
||||
|
||||
if (action.input.function === 'addAndWrite') {
|
||||
console.log("addAndWrite");
|
||||
const result = await SmartWeave.contracts.write(action.input.contractId, {
|
||||
function: 'addAmount',
|
||||
amount: action.input.amount
|
||||
@@ -51,4 +55,5 @@ export async function handle(state, action) {
|
||||
if (action.input.function === 'justThrow') {
|
||||
throw new ContractError('Error from justThrow function');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -124,6 +124,8 @@ export async function handle(state, action) {
|
||||
}
|
||||
|
||||
if (action.input.function === 'addAmount') {
|
||||
console.log("addAmount", action.input);
|
||||
|
||||
state.counter += action.input.amount;
|
||||
return { state };
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { WarpFactory } from '../../../core/WarpFactory';
|
||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||
import { VM2Plugin } from 'warp-contracts-plugin-vm2';
|
||||
import { MemoryLevel } from 'memory-level';
|
||||
|
||||
interface ExampleContractState {
|
||||
counter: number;
|
||||
@@ -213,6 +214,81 @@ describe('Testing internal writes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('should properly commit states', () => {
|
||||
beforeAll(async () => {
|
||||
await deployContracts();
|
||||
});
|
||||
|
||||
async function currentContractEntries(contractTxId: string): Promise<[[string, string]]> {
|
||||
const storage: MemoryLevel<string, any> = await warp.stateEvaluator.getCache().storage();
|
||||
const sub = storage.sublevel(contractTxId, { valueEncoding: "json" });
|
||||
return await sub.iterator().all();
|
||||
}
|
||||
|
||||
it('should write to cache the initial state', async () => {
|
||||
expect((await calleeContract.readState()).cachedValue.state.counter).toEqual(555);
|
||||
await mineBlock(warp);
|
||||
const entries = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should write to cache at the end of evaluation (if no interactions with other contracts)', async () => {
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await mineBlock(warp);
|
||||
const entries1 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries1.length).toEqual(1);
|
||||
|
||||
await calleeContract.readState();
|
||||
const entries2 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries2.length).toEqual(2);
|
||||
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await mineBlock(warp);
|
||||
const entries3 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries3.length).toEqual(2);
|
||||
|
||||
await calleeContract.readState();
|
||||
const entries4 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries4.length).toEqual(3);
|
||||
});
|
||||
|
||||
// i.e. it should write the state from previous sort key under the sort key of the last interaction
|
||||
it('should rollback state', async () => {
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await mineBlock(warp);
|
||||
const result1 = await calleeContract.readState();
|
||||
const entries1 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries1.length).toEqual(4);
|
||||
await calleeContract.writeInteraction({ function: 'add', throw: true });
|
||||
await mineBlock(warp);
|
||||
|
||||
await calleeContract.readState();
|
||||
const entries2 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries2.length).toEqual(5);
|
||||
const lastCacheValue = await warp.stateEvaluator.getCache().getLast(calleeContract.txId());
|
||||
expect(lastCacheValue.cachedValue.state).toEqual(result1.cachedValue.state);
|
||||
expect(Object.keys(result1.cachedValue.errorMessages).length + 1).toEqual(Object.keys(lastCacheValue.cachedValue.errorMessages).length);
|
||||
|
||||
const blockHeight = (await warp.arweave.network.getInfo()).height;
|
||||
expect(lastCacheValue.sortKey).toContain(`${blockHeight}`.padStart(12, '0'));
|
||||
});
|
||||
|
||||
it('should write to cache at the end of interaction (if interaction with other contracts)', async () => {
|
||||
await calleeContract.writeInteraction({ function: 'addAndWrite', contractId: callingContract.txId(), amount: 1 });
|
||||
await calleeContract.writeInteraction({ function: 'add' });
|
||||
await mineBlock(warp);
|
||||
|
||||
await calleeContract.readState();
|
||||
const entries2 = await currentContractEntries(calleeContract.txId());
|
||||
expect(entries2.length).toEqual(7);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with read state at the end', () => {
|
||||
beforeAll(async () => {
|
||||
await deployContracts();
|
||||
|
||||
@@ -152,11 +152,12 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
/**
|
||||
Reading the state of the writing contract.
|
||||
This in turn will cause the state of THIS contract to be
|
||||
updated in uncommitted state
|
||||
updated in 'interaction state'
|
||||
*/
|
||||
let newState: EvalStateResult<unknown> = null;
|
||||
let writingContractState: SortKeyCacheResult<EvalStateResult<unknown>> = null;
|
||||
try {
|
||||
await writingContract.readState(missingInteraction.sortKey);
|
||||
writingContractState = await writingContract.readState(missingInteraction.sortKey);
|
||||
newState = contract.interactionState().get(contract.txId(), missingInteraction.sortKey);
|
||||
} catch (e) {
|
||||
// ppe: not sure why we're not handling all ContractErrors here...
|
||||
@@ -179,16 +180,25 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
||||
}
|
||||
}
|
||||
|
||||
if (newState !== null) {
|
||||
if (newState !== null && writingContractState !== null) {
|
||||
const parentValidity = writingContractState.cachedValue.validity[missingInteraction.id];
|
||||
if (parentValidity) {
|
||||
currentState = newState.state as State;
|
||||
}
|
||||
// we need to update the state in the wasm module
|
||||
// TODO: opt - reuse wasm handlers...
|
||||
executionContext?.handler.initState(currentState);
|
||||
|
||||
if (parentValidity) {
|
||||
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
|
||||
if (newState.errorMessages?.[missingInteraction.id]) {
|
||||
errorMessages[missingInteraction.id] = newState.errorMessages[missingInteraction.id];
|
||||
}
|
||||
} else {
|
||||
validity[missingInteraction.id] = false;
|
||||
errorMessages[missingInteraction.id] =
|
||||
writingContractState.cachedValue.errorMessages[missingInteraction.id];
|
||||
}
|
||||
|
||||
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
||||
if (canBeCached(missingInteraction)) {
|
||||
|
||||
Reference in New Issue
Block a user