test: inner calls for kv contracts

This commit is contained in:
ppe
2023-03-30 14:49:37 +02:00
committed by Tadeuchi
parent da60ea200c
commit 095bfa157a
6 changed files with 478 additions and 26 deletions

View File

@@ -0,0 +1,105 @@
export async function handle(state, action) {
const input = action.input;
const caller = action.caller;
if (!state.kvOps) {
// state.kvOps = {};
}
if (input.function === 'mintAdd') {
console.log('mint', input.target, input.qty);
let value = await SmartWeave.kv.get(input.target);
if (value == null) {
value = 0;
}
value += input.qty;
await SmartWeave.kv.put(input.target, value);
return {state};
}
if (input.function === 'mintAddInnerWrite') {
console.log('mint inner write', input.target, input.qty);
let value = await SmartWeave.kv.get(input.target);
if (value == null) {
value = 0;
}
value += input.qty;
await SmartWeave.kv.put(input.target, value);
console.log('after inner write', await SmartWeave.kv.get(input.target));
return {state};
}
if (input.function === 'transfer') {
const target = input.target;
const qty = input.qty;
if (!Number.isInteger(qty)) {
throw new ContractError('Invalid value for "qty". Must be an integer');
}
if (!target) {
throw new ContractError('No target specified');
}
if (qty <= 0 || caller === target) {
throw new ContractError('Invalid token transfer');
}
let callerBalance = await SmartWeave.kv.get(caller);
callerBalance = callerBalance ? callerBalance : 0;
if (callerBalance < qty) {
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
}
// Lower the token balance of the caller
callerBalance -= qty;
await SmartWeave.kv.put(caller, callerBalance);
let targetBalance = await SmartWeave.kv.get(target);
targetBalance = targetBalance ? targetBalance : 0;
targetBalance += qty;
await SmartWeave.kv.put(target, targetBalance);
// for debug or whatever
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
return {state};
}
if (input.function === 'balance') {
const target = input.target;
const ticker = state.ticker;
if (typeof target !== 'string') {
throw new ContractError('Must specify target to get balance for');
}
const result = await SmartWeave.kv.get(target);
console.log('balance', {target: input.target, balance: result});
return {result: {target, ticker, balance: result ? result : 0}};
}
if (input.function === 'innerWriteKV') {
console.log('calling', input.txId);
await SmartWeave.contracts.write(input.txId, {
function: 'mintAddInnerWrite',
target: input.target,
qty: input.qty
});
}
if (input.function === 'innerViewKV') {
const txId = input.txId;
const viewResult = await SmartWeave.contracts.viewContractState(txId, {
function: 'balance',
target: Smartweave.contract.id
});
return {}
}
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
}

View File

@@ -0,0 +1,350 @@
/* eslint-disable */
import fs from 'fs';
import ArLocal from 'arlocal';
import {JWKInterface} from 'arweave/node/lib/wallet';
import path from 'path';
import {mineBlock} from '../_helpers';
import {Contract} from '../../../contract/Contract';
import {Warp} from '../../../core/Warp';
import {WarpFactory} from '../../../core/WarpFactory';
import {LoggerFactory} from '../../../logging/LoggerFactory';
import {DeployPlugin} from 'warp-contracts-plugin-deploy';
interface ExampleContractState {
counter: number;
errorCounter: number;
}
/**
* The most basic example of writes between contracts.
* In this suite "User" is calling CallingContract.writeContract
* (which calls CalleContract.addAmount and in effect - changes its state)
* or "User" is calling CalleeContract.add() directly.
*
* Multiple combinations of both calls (with mining happening on different stages)
* are being tested.
*
* ┌──────┐
* ┌───┬───┤ User │
* │ │ └──────┘
* │ │ ┌─────────────────────────────────┐
* │ │ │CallingContract │
* │ │ ├─────────────────────────────────┤
* │ └──►│writeContract(contractId, amount)├───┐
* │ └─────────────────────────────────┘ │
* │ ┌─────────────────────────────────────────┘
* │ │ ┌─────────────────────────────────┐
* │ │ │CalleeContract │
* │ │ ├─────────────────────────────────┤
* │ └──►│addAmount(amount) │
* └──────►│add() │
* └─────────────────────────────────┘
*/
describe('Testing internal writes', () => {
let callingContractSrc: string;
let callingContractInitialState: string;
let calleeContractSrc: string;
let calleeInitialState: string;
let wallet: JWKInterface;
let walletAddress: string;
let arlocal: ArLocal;
let warp: Warp;
let calleeContract: Contract<ExampleContractState>;
let callingContract: Contract<ExampleContractState>;
let calleeTxId;
let callingTxId;
const port = 1911;
beforeAll(async () => {
// 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!)
arlocal = new ArLocal(port, false);
await arlocal.start();
LoggerFactory.INST.logLevel('error');
});
afterAll(async () => {
await arlocal.stop();
});
async function deployContracts() {
warp = WarpFactory.forLocal(port).use(new DeployPlugin());
({jwk: wallet, address: walletAddress} = await warp.generateWallet());
callingContractSrc = fs.readFileSync(path.join(__dirname, '../data/kv-storage-inner-calls.js'), 'utf8');
callingContractInitialState = fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8');
calleeContractSrc = fs.readFileSync(path.join(__dirname, '../data/kv-storage-inner-calls.js'), 'utf8');
calleeInitialState = fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8');
({contractTxId: calleeTxId} = await warp.deploy({
wallet,
initState: calleeInitialState,
src: calleeContractSrc
}));
({contractTxId: callingTxId} = await warp.deploy({
wallet,
initState: callingContractInitialState,
src: callingContractSrc
}));
calleeContract = warp
.contract<ExampleContractState>(calleeTxId)
.setEvaluationOptions({
internalWrites: true,
mineArLocalBlocks: false,
useKVStorage: true
})
.connect(wallet);
callingContract = warp
.contract<ExampleContractState>(callingTxId)
.setEvaluationOptions({
internalWrites: true,
mineArLocalBlocks: false,
useKVStorage: true
})
.connect(wallet);
await mineBlock(warp);
}
describe('with read states in between', () => {
beforeAll(async () => {
await deployContracts();
});
it('should write direct interactions', async () => {
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(100);
});
it('should write one direct and one internal interaction', async () => {
// await calleeContract.writeInteraction({function: 'add'});
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(200);
});
it('should write another direct interaction', async () => {
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(300);
});
it('should write double internal interaction with direct interaction', async () => {
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(500);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues2 = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues2.cachedValue.get(walletAddress)).toEqual(600);
});
it('should write combination of internal and direct interaction', async () => {
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(800);
});
it('should write combination of internal and direct interaction', async () => {
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(1000);
});
it('should write combination of direct and internal interaction - at one block', async () => {
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(1200);
});
it('should write combination of direct and internal interaction - on different blocks', async () => {
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(1400);
});
it('should properly evaluate state again', async () => {
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(1400);
});
});
describe('with read state at the end', () => {
beforeAll(async () => {
await deployContracts();
});
it('should properly write a combination of direct and internal interactions', async () => {
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await callingContract.writeInteraction({
function: 'innerWriteKV',
txId: calleeTxId,
target: walletAddress,
qty: 100
});
await mineBlock(warp);
await calleeContract.writeInteraction({function: 'mintAdd', target: walletAddress, qty: 100});
await mineBlock(warp);
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(1400);
});
it('should properly evaluate state again', async () => {
await calleeContract.readState();
const kvValues = await calleeContract.getStorageValues([walletAddress]);
expect(kvValues.cachedValue.get(walletAddress)).toEqual(1400);
});
});
});

View File

@@ -247,14 +247,4 @@ export interface Contract<State = unknown> {
getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, unknown>>>;
interactionState(): InteractionState;
/* getUncommittedState(contractTxId: string): EvalStateResult<unknown>;
setUncommittedState(contractTxId: string, result: EvalStateResult<unknown>): void;
hasUncommittedState(contractTxId: string): boolean;
resetUncommittedState(): void;
commitStates(interaction: GQLNodeInterface): Promise<void>;*/
}

View File

@@ -1,14 +1,14 @@
import Arweave from 'arweave';
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';
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, contract} = executionContext;
const { cachedState, contract } = executionContext;
const missingInteractions = executionContext.sortedInteractions;
if (cachedState && cachedState.sortKey == executionContext.requestedSortKey && !missingInteractions?.length) {
@@ -123,9 +123,11 @@ 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}`);
}

View File

@@ -94,7 +94,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
break;
}
contract.interactionState().setInitial(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();
@@ -293,7 +295,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
contract.interactionState().update(contract.txId(), lastConfirmedTxState.state);
if (validity[missingInteraction.id]) {
await contract.interactionState().commit(missingInteraction);
} else {
} else {
await contract.interactionState().rollback(missingInteraction);
}
}

View File

@@ -96,7 +96,10 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
}
protected assignViewContractState<Input>(executionContext: ExecutionContext<State>) {
this.swGlobal.contracts.viewContractState = async <View>(contractTxId: string, input: Input) => {
this.swGlobal.contracts.viewContractState = async <View>(
contractTxId: string,
input: Input
): Promise<InteractionResult<unknown, View>> => {
this.logger.debug('swGlobal.viewContractState call:', {
from: this.contractDefinition.txId,
to: contractTxId,