test: inner calls for kv contracts
This commit is contained in:
105
src/__tests__/integration/data/kv-storage-inner-calls.js
Normal file
105
src/__tests__/integration/data/kv-storage-inner-calls.js
Normal 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}"`);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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>;*/
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user