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') {
|
if (action.input.function === 'add') {
|
||||||
state.counter++;
|
state.counter++;
|
||||||
|
if (action.input.throw) {
|
||||||
|
throw new ContractError('Error from "add" function');
|
||||||
|
}
|
||||||
return { state };
|
return { state };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.input.function === 'addAndWrite') {
|
if (action.input.function === 'addAndWrite') {
|
||||||
|
console.log("addAndWrite");
|
||||||
const result = await SmartWeave.contracts.write(action.input.contractId, {
|
const result = await SmartWeave.contracts.write(action.input.contractId, {
|
||||||
function: 'addAmount',
|
function: 'addAmount',
|
||||||
amount: action.input.amount
|
amount: action.input.amount
|
||||||
@@ -51,4 +55,5 @@ export async function handle(state, action) {
|
|||||||
if (action.input.function === 'justThrow') {
|
if (action.input.function === 'justThrow') {
|
||||||
throw new ContractError('Error from justThrow function');
|
throw new ContractError('Error from justThrow function');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ export async function handle(state, action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action.input.function === 'addAmount') {
|
if (action.input.function === 'addAmount') {
|
||||||
|
console.log("addAmount", action.input);
|
||||||
|
|
||||||
state.counter += action.input.amount;
|
state.counter += action.input.amount;
|
||||||
return { state };
|
return { state };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { WarpFactory } from '../../../core/WarpFactory';
|
|||||||
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
import { LoggerFactory } from '../../../logging/LoggerFactory';
|
||||||
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
import { DeployPlugin } from 'warp-contracts-plugin-deploy';
|
||||||
import { VM2Plugin } from 'warp-contracts-plugin-vm2';
|
import { VM2Plugin } from 'warp-contracts-plugin-vm2';
|
||||||
|
import { MemoryLevel } from 'memory-level';
|
||||||
|
|
||||||
interface ExampleContractState {
|
interface ExampleContractState {
|
||||||
counter: number;
|
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', () => {
|
describe('with read state at the end', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await deployContracts();
|
await deployContracts();
|
||||||
|
|||||||
@@ -152,11 +152,12 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
|
|||||||
/**
|
/**
|
||||||
Reading the state of the writing contract.
|
Reading the state of the writing contract.
|
||||||
This in turn will cause the state of THIS contract to be
|
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 newState: EvalStateResult<unknown> = null;
|
||||||
|
let writingContractState: SortKeyCacheResult<EvalStateResult<unknown>> = null;
|
||||||
try {
|
try {
|
||||||
await writingContract.readState(missingInteraction.sortKey);
|
writingContractState = await writingContract.readState(missingInteraction.sortKey);
|
||||||
newState = contract.interactionState().get(contract.txId(), missingInteraction.sortKey);
|
newState = contract.interactionState().get(contract.txId(), missingInteraction.sortKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ppe: not sure why we're not handling all ContractErrors here...
|
// 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;
|
currentState = newState.state as State;
|
||||||
|
}
|
||||||
// we need to update the state in the wasm module
|
// we need to update the state in the wasm module
|
||||||
// TODO: opt - reuse wasm handlers...
|
// TODO: opt - reuse wasm handlers...
|
||||||
executionContext?.handler.initState(currentState);
|
executionContext?.handler.initState(currentState);
|
||||||
|
|
||||||
|
if (parentValidity) {
|
||||||
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
|
validity[missingInteraction.id] = newState.validity[missingInteraction.id];
|
||||||
if (newState.errorMessages?.[missingInteraction.id]) {
|
if (newState.errorMessages?.[missingInteraction.id]) {
|
||||||
errorMessages[missingInteraction.id] = 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);
|
const toCache = new EvalStateResult(currentState, validity, errorMessages);
|
||||||
if (canBeCached(missingInteraction)) {
|
if (canBeCached(missingInteraction)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user