feat: feature switch for internal writes
This commit is contained in:
committed by
Piotr Pędziwiatr
parent
5791c81b4f
commit
fbbbb26c56
@@ -96,8 +96,18 @@ describe('Testing internal writes', () => {
|
||||
src: contractBSrc
|
||||
});
|
||||
|
||||
contractA = smartweave.contract(contractATxId).connect(wallet);
|
||||
contractB = smartweave.contract(contractBTxId).connect(wallet);
|
||||
contractA = smartweave
|
||||
.contract(contractATxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
contractB = smartweave
|
||||
.contract(contractBTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
|
||||
await mine();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ interface ExampleContractState {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The most basic example of writes between contracts.
|
||||
* In this suite "User" is calling CallingContract.writeContract
|
||||
@@ -97,8 +96,18 @@ describe('Testing internal writes', () => {
|
||||
src: callingContractSrc
|
||||
});
|
||||
|
||||
calleeContract = smartweave.contract<ExampleContractState>(calleeTxId).connect(wallet);
|
||||
callingContract = smartweave.contract(callingTxId).connect(wallet);
|
||||
calleeContract = smartweave
|
||||
.contract<ExampleContractState>(calleeTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
callingContract = smartweave
|
||||
.contract(callingTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
|
||||
await mine();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Contract, LoggerFactory, SmartWeave, SmartWeaveNodeFactory } from '@sma
|
||||
import path from 'path';
|
||||
import { TsLogFactory } from '../../../logging/node/TsLogFactory';
|
||||
|
||||
|
||||
/**
|
||||
* This verifies whether combination of read and write state works properly.
|
||||
* 1. User calls ContractA.writeContractCheck(amount)
|
||||
@@ -109,8 +108,18 @@ describe('Testing internal writes', () => {
|
||||
src: callingContractSrc
|
||||
});
|
||||
|
||||
calleeContract = smartweave.contract(calleeTxId).connect(wallet);
|
||||
callingContract = smartweave.contract(callingTxId).connect(wallet);
|
||||
calleeContract = smartweave
|
||||
.contract(calleeTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
callingContract = smartweave
|
||||
.contract(callingTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
|
||||
await mine();
|
||||
}
|
||||
|
||||
@@ -116,9 +116,24 @@ describe('Testing internal writes', () => {
|
||||
src: contractBSrc
|
||||
});
|
||||
|
||||
contractA = smartweave.contract(contractATxId).connect(wallet);
|
||||
contractB = smartweave.contract(contractBTxId).connect(wallet);
|
||||
contractC = smartweave.contract(contractCTxId).connect(wallet);
|
||||
contractA = smartweave
|
||||
.contract(contractATxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
contractB = smartweave
|
||||
.contract(contractBTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
contractC = smartweave
|
||||
.contract(contractCTxId)
|
||||
.setEvaluationOptions({
|
||||
internalWrites: true
|
||||
})
|
||||
.connect(wallet);
|
||||
|
||||
await mine();
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ describe('Testing internal writes', () => {
|
||||
let contractBTxId;
|
||||
let contractCTxId;
|
||||
|
||||
|
||||
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!)
|
||||
@@ -111,9 +110,9 @@ describe('Testing internal writes', () => {
|
||||
src: contractBSrc
|
||||
});
|
||||
|
||||
contractA = smartweave.contract(contractATxId).connect(wallet);
|
||||
contractB = smartweave.contract(contractBTxId).connect(wallet);
|
||||
contractC = smartweave.contract(contractCTxId).connect(wallet);
|
||||
contractA = smartweave.contract(contractATxId).setEvaluationOptions({ internalWrites: true }).connect(wallet);
|
||||
contractB = smartweave.contract(contractBTxId).setEvaluationOptions({ internalWrites: true }).connect(wallet);
|
||||
contractC = smartweave.contract(contractCTxId).setEvaluationOptions({ internalWrites: true }).connect(wallet);
|
||||
|
||||
await mine();
|
||||
}
|
||||
|
||||
@@ -79,8 +79,14 @@ describe('Testing internal writes', () => {
|
||||
src: stakingContractSrc
|
||||
});
|
||||
|
||||
tokenContract = smartweave.contract(tokenContractTxId).connect(wallet);
|
||||
stakingContract = smartweave.contract(stakingContractTxId).connect(wallet);
|
||||
tokenContract = smartweave
|
||||
.contract(tokenContractTxId)
|
||||
.setEvaluationOptions({ internalWrites: true })
|
||||
.connect(wallet);
|
||||
stakingContract = smartweave
|
||||
.contract(stakingContractTxId)
|
||||
.setEvaluationOptions({ internalWrites: true })
|
||||
.connect(wallet);
|
||||
|
||||
await mine();
|
||||
}
|
||||
@@ -129,12 +135,11 @@ describe('Testing internal writes', () => {
|
||||
});
|
||||
await mine();
|
||||
|
||||
expect((await stakingContract.readState()).state.stakes).toEqual({
|
||||
});
|
||||
expect((await stakingContract.readState()).state.stakes).toEqual({});
|
||||
|
||||
const tokenState = (await tokenContract.readState()).state;
|
||||
expect(tokenState.balances).toEqual({
|
||||
[walletAddress]: 10000,
|
||||
[walletAddress]: 10000
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,8 +183,6 @@ describe('Testing internal writes', () => {
|
||||
[stakingContractTxId]: 1000
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('with read states at the end', () => {
|
||||
@@ -240,7 +243,6 @@ describe('Testing internal writes', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
async function mine() {
|
||||
|
||||
@@ -146,20 +146,22 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
}
|
||||
const { arweave } = this.smartweave;
|
||||
|
||||
await this.callContract(input, undefined, tags, transfer);
|
||||
const callStack: ContractCallStack = this.getCallStack();
|
||||
const innerWrites = this.innerWritesEvaluator.eval(callStack);
|
||||
this.logger.debug('Input', input);
|
||||
this.logger.debug('Callstack', callStack.print());
|
||||
if (this.evaluationOptions.internalWrites) {
|
||||
await this.callContract(input, undefined, tags, transfer);
|
||||
const callStack: ContractCallStack = this.getCallStack();
|
||||
const innerWrites = this.innerWritesEvaluator.eval(callStack);
|
||||
this.logger.debug('Input', input);
|
||||
this.logger.debug('Callstack', callStack.print());
|
||||
|
||||
innerWrites.forEach((contractTxId) => {
|
||||
tags.push({
|
||||
name: SmartWeaveTags.INTERACT_WRITE,
|
||||
value: contractTxId
|
||||
innerWrites.forEach((contractTxId) => {
|
||||
tags.push({
|
||||
name: SmartWeaveTags.INTERACT_WRITE,
|
||||
value: contractTxId
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.logger.debug('Tags with inner calls', tags);
|
||||
this.logger.debug('Tags with inner calls', tags);
|
||||
}
|
||||
|
||||
const interactionTx = await createTx(
|
||||
this.smartweave.arweave,
|
||||
@@ -287,7 +289,12 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
// (eg. if contract is calling different contracts on different block heights).
|
||||
// This basically limits the amount of interactions with Arweave GraphQL endpoint -
|
||||
// each such interaction takes at least ~500ms.
|
||||
interactionsLoader.load(contractTxId, cachedBlockHeight + 1, this.rootBlockHeight || this.networkInfo.height)
|
||||
interactionsLoader.load(
|
||||
contractTxId,
|
||||
cachedBlockHeight + 1,
|
||||
this.rootBlockHeight || this.networkInfo.height,
|
||||
this.evaluationOptions
|
||||
)
|
||||
]);
|
||||
this.logger.debug('contract and interactions load', benchmark.elapsed());
|
||||
sortedInteractions = await interactionsSorter.sort(interactions);
|
||||
@@ -338,7 +345,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
if (cachedBlockHeight != blockHeight) {
|
||||
[contractDefinition, interactions] = await Promise.all([
|
||||
definitionLoader.load<State>(contractTxId),
|
||||
await interactionsLoader.load(contractTxId, 0, blockHeight)
|
||||
await interactionsLoader.load(contractTxId, 0, blockHeight, this.evaluationOptions)
|
||||
]);
|
||||
sortedInteractions = await interactionsSorter.sort(interactions);
|
||||
} else {
|
||||
@@ -466,7 +473,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
|
||||
|
||||
const interaction: ContractInteraction<Input> = {
|
||||
input,
|
||||
caller: this.callingContract.txId()//executionContext.caller
|
||||
caller: this.callingContract.txId() //executionContext.caller
|
||||
};
|
||||
|
||||
const interactionData: InteractionData<Input> = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GQLEdgeInterface } from '@smartweave';
|
||||
import { EvaluationOptions, GQLEdgeInterface } from '@smartweave';
|
||||
|
||||
/**
|
||||
* Implementors of this interface add functionality of loading contract's interaction transactions.
|
||||
@@ -7,5 +7,10 @@ import { GQLEdgeInterface } from '@smartweave';
|
||||
* Note: InteractionsLoaders are not responsible for sorting interaction transactions!
|
||||
*/
|
||||
export interface InteractionsLoader {
|
||||
load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]>;
|
||||
load(
|
||||
contractId: string,
|
||||
fromBlockHeight: number,
|
||||
toBlockHeight: number,
|
||||
evaluationOptions: EvaluationOptions
|
||||
): Promise<GQLEdgeInterface[]>;
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
|
||||
|
||||
updateCacheForEachInteraction = true;
|
||||
|
||||
enhancedValidity = false;
|
||||
|
||||
stackTrace = {
|
||||
saveState: false
|
||||
};
|
||||
|
||||
internalWrites: false;
|
||||
}
|
||||
|
||||
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some of the features.
|
||||
@@ -96,12 +96,13 @@ export interface EvaluationOptions {
|
||||
// and caches it maybe more suitable to cache only after state has been fully evaluated)
|
||||
updateCacheForEachInteraction: boolean;
|
||||
|
||||
// enhanced validity report with error/exception messages included
|
||||
enhancedValidity: boolean;
|
||||
|
||||
// a set of options that control the behaviour of the stack trace generator
|
||||
stackTrace: {
|
||||
// whether output state should be saved for each interaction in the stack trace (may result in huuuuge json files!)
|
||||
saveState: boolean;
|
||||
};
|
||||
|
||||
// a new, experimental enhancement of the protocol that allows for interactWrites from
|
||||
// smart contract's source code.
|
||||
internalWrites: boolean;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
|
||||
|
||||
private assignWrite(executionContext: ExecutionContext<State>, currentTx: CurrentTx[]) {
|
||||
this.swGlobal.contracts.write = async <Input = unknown>(contractTxId: string, input: Input) => {
|
||||
if (!executionContext.evaluationOptions.internalWrites) {
|
||||
throw new Error("Internal writes feature switched off. Change EvaluationOptions.internalWrites flag to 'true'");
|
||||
}
|
||||
|
||||
this.logger.debug('swGlobal.write call:', {
|
||||
from: this.contractDefinition.txId,
|
||||
to: contractTxId,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
Benchmark,
|
||||
Benchmark, EvaluationOptions,
|
||||
GQLEdgeInterface,
|
||||
GQLResultInterface,
|
||||
GQLTransactionsResultInterface,
|
||||
@@ -64,8 +64,13 @@ export class ContractInteractionsLoader implements InteractionsLoader {
|
||||
|
||||
constructor(private readonly arweave: Arweave) {}
|
||||
|
||||
async load(contractTxId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
|
||||
this.logger.debug('Loading interactions for', contractTxId);
|
||||
async load(
|
||||
contractId: string,
|
||||
fromBlockHeight: number,
|
||||
toBlockHeight: number,
|
||||
evaluationOptions: EvaluationOptions
|
||||
): Promise<GQLEdgeInterface[]> {
|
||||
this.logger.debug('Loading interactions for', contractId);
|
||||
const mainTransactionsVariables: ReqVariables = {
|
||||
tags: [
|
||||
{
|
||||
@@ -74,7 +79,7 @@ export class ContractInteractionsLoader implements InteractionsLoader {
|
||||
},
|
||||
{
|
||||
name: SmartWeaveTags.CONTRACT_TX_ID,
|
||||
values: [contractTxId]
|
||||
values: [contractId]
|
||||
}
|
||||
],
|
||||
blockFilter: {
|
||||
@@ -83,35 +88,35 @@ export class ContractInteractionsLoader implements InteractionsLoader {
|
||||
},
|
||||
first: MAX_REQUEST
|
||||
};
|
||||
const mainInteractions = await this.loadPages(mainTransactionsVariables);
|
||||
this.logger.debug('Main Interactions:', mainInteractions);
|
||||
this.logger.debug('Main interactions length:', mainInteractions.length);
|
||||
|
||||
let innerWritesVariables: ReqVariables = {
|
||||
tags: [
|
||||
{
|
||||
name: SmartWeaveTags.INTERACT_WRITE,
|
||||
values: [contractTxId]
|
||||
}
|
||||
],
|
||||
blockFilter: {
|
||||
min: fromBlockHeight,
|
||||
max: toBlockHeight
|
||||
},
|
||||
first: MAX_REQUEST
|
||||
};
|
||||
const innerWritesInteractions = await this.loadPages(innerWritesVariables);
|
||||
this.logger.debug('Inner Writes Interactions', innerWritesInteractions);
|
||||
this.logger.debug('Inner writes interactions length:', innerWritesInteractions.length);
|
||||
const allInteractions = mainInteractions.concat(innerWritesInteractions);
|
||||
let interactions = await this.loadPages(mainTransactionsVariables);
|
||||
|
||||
if (evaluationOptions.internalWrites) {
|
||||
let innerWritesVariables: ReqVariables = {
|
||||
tags: [
|
||||
{
|
||||
name: SmartWeaveTags.INTERACT_WRITE,
|
||||
values: [contractId]
|
||||
}
|
||||
],
|
||||
blockFilter: {
|
||||
min: fromBlockHeight,
|
||||
max: toBlockHeight
|
||||
},
|
||||
first: MAX_REQUEST
|
||||
};
|
||||
const innerWritesInteractions = await this.loadPages(innerWritesVariables);
|
||||
this.logger.debug('Inner writes interactions length:', innerWritesInteractions.length);
|
||||
interactions = interactions.concat(innerWritesInteractions);
|
||||
}
|
||||
|
||||
this.logger.debug('All loaded interactions:', {
|
||||
from: fromBlockHeight,
|
||||
to: toBlockHeight,
|
||||
loaded: allInteractions.length
|
||||
loaded: interactions.length
|
||||
});
|
||||
|
||||
return allInteractions;
|
||||
return interactions;
|
||||
}
|
||||
|
||||
private async loadPages(variables: ReqVariables) {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { BlockHeightKey, BlockHeightSwCache, GQLEdgeInterface, InteractionsLoader, LoggerFactory } from '@smartweave';
|
||||
import {
|
||||
BlockHeightKey,
|
||||
BlockHeightSwCache,
|
||||
EvaluationOptions,
|
||||
GQLEdgeInterface,
|
||||
InteractionsLoader,
|
||||
LoggerFactory
|
||||
} from '@smartweave';
|
||||
|
||||
/**
|
||||
* This implementation of the {@link InteractionsLoader} tries to limit the amount of interactions
|
||||
@@ -13,7 +20,12 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
|
||||
private readonly cache: BlockHeightSwCache<GQLEdgeInterface[]>
|
||||
) {}
|
||||
|
||||
async load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
|
||||
async load(
|
||||
contractId: string,
|
||||
fromBlockHeight: number,
|
||||
toBlockHeight: number,
|
||||
evaluationOptions: EvaluationOptions
|
||||
): Promise<GQLEdgeInterface[]> {
|
||||
this.logger.debug('Loading interactions', {
|
||||
contractId,
|
||||
fromBlockHeight,
|
||||
@@ -38,7 +50,12 @@ export class CacheableContractInteractionsLoader implements InteractionsLoader {
|
||||
cachedValue
|
||||
});
|
||||
|
||||
const missingInteractions = await this.baseImplementation.load(contractId, cachedHeight + 1, toBlockHeight);
|
||||
const missingInteractions = await this.baseImplementation.load(
|
||||
contractId,
|
||||
cachedHeight + 1,
|
||||
toBlockHeight,
|
||||
evaluationOptions
|
||||
);
|
||||
|
||||
const result = cachedValue
|
||||
.filter((interaction: GQLEdgeInterface) => interaction.node.block.height >= fromBlockHeight)
|
||||
|
||||
@@ -47,7 +47,12 @@ export class FromFileInteractionsLoader implements InteractionsLoader {
|
||||
});
|
||||
}
|
||||
|
||||
async load(contractId: string, fromBlockHeight: number, toBlockHeight: number): Promise<GQLEdgeInterface[]> {
|
||||
async load(
|
||||
contractId: string,
|
||||
fromBlockHeight: number,
|
||||
toBlockHeight: number,
|
||||
evaluationOptions: EvaluationOptions
|
||||
): Promise<GQLEdgeInterface[]> {
|
||||
return this.transactions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ async function main() {
|
||||
console.log(
|
||||
`\n[${contractTxs.indexOf(contractTx) + 1} / ${contractTxs.length}] loading interactions of the ${contractTxId}`
|
||||
);
|
||||
const interactions = await transactionsLoader.load(contractTxId, 0, 779826);
|
||||
const interactions = await transactionsLoader.load(contractTxId, 0, 779826, evaluationOptions);
|
||||
console.log(`${contractTxId}: ${interactions.length}`);
|
||||
|
||||
result[contractTxId] = interactions.length;
|
||||
|
||||
@@ -21,7 +21,12 @@ async function main() {
|
||||
|
||||
const transactionsLoader = new ContractInteractionsLoader(arweave);
|
||||
|
||||
const result = await transactionsLoader.load('Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY', 0, 779820);
|
||||
const result = await transactionsLoader.load(
|
||||
'Daj-MNSnH55TDfxqC7v4eq0lKzVIwh98srUaWqyuZtY',
|
||||
0,
|
||||
779820,
|
||||
evaluationOptions
|
||||
);
|
||||
|
||||
console.log(result.length);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user