refactor: client api refactor
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable */
|
||||
import { LoggerFactory, SwClientFactory } from '@smartweave';
|
||||
import { LoggerFactory, SmartWeaveFactory } from '@smartweave';
|
||||
import Arweave from 'arweave';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -14,7 +14,7 @@ async function main() {
|
||||
});
|
||||
const logger = LoggerFactory.INST.create(__filename);
|
||||
LoggerFactory.INST.logLevel('silly', 'benchmark');
|
||||
const swcClient = SwClientFactory.fileCacheClient(arweave);
|
||||
const swcClient = SmartWeaveFactory.fileCacheClient(arweave);
|
||||
|
||||
const contractTxId = 'OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU';
|
||||
// Kyve:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
import Arweave from 'arweave';
|
||||
import { SwClientFactory, LoggerFactory } from '@smartweave';
|
||||
import { SmartWeaveFactory, LoggerFactory } from '@smartweave';
|
||||
|
||||
const contracts = [
|
||||
'gepLlre8wG8K3C15rNjpdKZZv_9pWsurRoEB6ir_EC4',
|
||||
@@ -23,15 +23,14 @@ async function main() {
|
||||
logging: false // Enable network request logging
|
||||
});
|
||||
|
||||
const swcClient = SwClientFactory.memCacheClient(arweave);
|
||||
LoggerFactory.INST.logLevel('trace');
|
||||
const smartWeave = SmartWeaveFactory.memCached(arweave);
|
||||
const contract1 = smartWeave.contract('W_njBtwDRyltjVU1RizJtZfF0S_4X3aSrrrA0HUEhUs');
|
||||
const contract2 = smartWeave.contract('TMkCZKYO3GwcTLEKGgWSJegIlYCHi_UArtG0unCi2xA');
|
||||
LoggerFactory.INST.logLevel('debug');
|
||||
|
||||
const contractTxId = 'W_njBtwDRyltjVU1RizJtZfF0S_4X3aSrrrA0HUEhUs';
|
||||
const contractTxId2 = 'TMkCZKYO3GwcTLEKGgWSJegIlYCHi_UArtG0unCi2xA';
|
||||
await contract1.readState();
|
||||
|
||||
await swcClient.readState(contractTxId);
|
||||
|
||||
await swcClient.readState(contractTxId2);
|
||||
await contract2.readState();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
DebuggableExecutorFactory,
|
||||
EvalStateResult,
|
||||
EvolveCompatibleState,
|
||||
HandlerBasedSwcClient,
|
||||
HandlerBasedContract,
|
||||
HandlerExecutorFactory,
|
||||
LexicographicalInteractionsSorter,
|
||||
LoggerFactory,
|
||||
@@ -55,7 +55,7 @@ async function readContractState() {
|
||||
'OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU': changedSrc
|
||||
});
|
||||
|
||||
const swcClient = new HandlerBasedSwcClient(
|
||||
const swcClient = new HandlerBasedContract(
|
||||
arweave,
|
||||
new ContractDefinitionLoader<ProvidersRegistryState>(arweave, new MemCache()),
|
||||
new ContractInteractionsLoader(arweave),
|
||||
|
||||
@@ -3,7 +3,7 @@ import Arweave from 'arweave';
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import Transaction from 'arweave/node/lib/transaction';
|
||||
import { GQLEdgeInterface, GQLResultInterface, LoggerFactory, SwClientFactory } from '@smartweave';
|
||||
import { GQLEdgeInterface, GQLResultInterface, LoggerFactory, SmartWeaveFactory } from '@smartweave';
|
||||
import { readContract } from 'smartweave';
|
||||
|
||||
const diffStateToVerify = [
|
||||
@@ -51,7 +51,7 @@ async function main() {
|
||||
|
||||
const errorContractTxIds = [];
|
||||
|
||||
const swcClient = SwClientFactory.fileCacheClient(arweave, 'cache');
|
||||
const swcClient = SmartWeaveFactory.fileCacheClient(arweave, 'cache');
|
||||
|
||||
const contractsBlacklist = [
|
||||
'jFInOjLc_FFt802OmUObIIOlY1xNKvomzUTkoUpyP9U', // readContract very long evaluation
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import Arweave from 'arweave';
|
||||
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||
import {
|
||||
Benchmark,
|
||||
ContractInteraction,
|
||||
DefaultEvaluationOptions,
|
||||
DefinitionLoader,
|
||||
EvalStateResult,
|
||||
EvaluationOptions,
|
||||
ExecutionContext,
|
||||
ExecutorFactory,
|
||||
HandlerApi,
|
||||
InteractionResult,
|
||||
InteractionsLoader,
|
||||
InteractionsSorter,
|
||||
InteractionTx,
|
||||
LoggerFactory,
|
||||
StateEvaluator,
|
||||
Contract
|
||||
} from '@smartweave';
|
||||
|
||||
const logger = LoggerFactory.INST.create(__filename);
|
||||
|
||||
/**
|
||||
* An implementation of {@link Contract} that is backwards compatible with current style
|
||||
* of writing SW contracts (ie. using the "handle" function).
|
||||
*
|
||||
* It requires {@link ExecutorFactory} that is using {@link HandlerApi} generic type.
|
||||
*/
|
||||
export class HandlerBasedSwcClient implements Contract {
|
||||
constructor(
|
||||
private readonly arweave: Arweave,
|
||||
private readonly definitionLoader: DefinitionLoader,
|
||||
private readonly interactionsLoader: InteractionsLoader,
|
||||
private readonly executorFactory: ExecutorFactory<any, HandlerApi<any>>,
|
||||
private readonly stateEvaluator: StateEvaluator,
|
||||
private readonly interactionsSorter: InteractionsSorter
|
||||
) {}
|
||||
|
||||
async readState<State>(
|
||||
contractTxId: string,
|
||||
blockHeight?: number,
|
||||
currentTx?: { interactionTxId: string; contractTxId: string }[],
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<EvalStateResult<State>> {
|
||||
logger.info(`Read state for ${contractTxId}`);
|
||||
const benchmark = Benchmark.measure();
|
||||
const executionContext = await this.createExecutionContext(contractTxId, blockHeight, evaluationOptions);
|
||||
logger.debug('Creating execution context', benchmark.elapsed());
|
||||
|
||||
benchmark.reset();
|
||||
const result = await this.stateEvaluator.eval(executionContext, currentTx || []);
|
||||
logger.debug(`Evaluating ${contractTxId} state`, benchmark.elapsed());
|
||||
|
||||
return result as EvalStateResult<State>;
|
||||
}
|
||||
|
||||
async viewState<Input, View>(
|
||||
contractTxId: string,
|
||||
input: Input,
|
||||
wallet: JWKInterface,
|
||||
blockHeight?: number,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<InteractionResult<any, View>> {
|
||||
logger.info(`View state for ${contractTxId}`);
|
||||
const benchmark = Benchmark.measure();
|
||||
let executionContext = await this.createExecutionContext(contractTxId, blockHeight, evaluationOptions);
|
||||
logger.debug('Creating execution context', benchmark.elapsed());
|
||||
|
||||
if (!executionContext.currentBlockData) {
|
||||
const currentBlockData = executionContext.currentNetworkInfo
|
||||
? // trying to optimise calls to arweave as much as possible...
|
||||
await this.arweave.blocks.get(executionContext.currentNetworkInfo.current)
|
||||
: await this.arweave.blocks.getCurrent();
|
||||
|
||||
executionContext = {
|
||||
...executionContext,
|
||||
currentBlockData
|
||||
};
|
||||
}
|
||||
|
||||
const caller = await this.arweave.wallets.jwkToAddress(wallet);
|
||||
executionContext = {
|
||||
...executionContext,
|
||||
caller
|
||||
};
|
||||
|
||||
benchmark.reset();
|
||||
const evalStateResult = await this.stateEvaluator.eval(executionContext, []);
|
||||
logger.debug(`Evaluating ${contractTxId} state`, benchmark.elapsed());
|
||||
|
||||
const interaction: ContractInteraction = {
|
||||
input,
|
||||
caller: executionContext.caller
|
||||
};
|
||||
|
||||
// TODO: what is the best way to create a transaction in this case?
|
||||
return await executionContext.handler.handle<Input, View>(
|
||||
executionContext,
|
||||
evalStateResult.state,
|
||||
interaction,
|
||||
{
|
||||
id: null,
|
||||
recipient: null,
|
||||
owner: {
|
||||
address: executionContext.caller
|
||||
},
|
||||
tags: [],
|
||||
fee: null,
|
||||
quantity: null,
|
||||
block: executionContext.currentBlockData
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
async viewStateForTx<Input, View>(
|
||||
contractTxId: string,
|
||||
input: Input,
|
||||
transaction: InteractionTx,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<InteractionResult<any, View>> {
|
||||
logger.info(`Vies state for ${contractTxId}`, transaction);
|
||||
const benchmark = Benchmark.measure();
|
||||
const executionContext = await this.createExecutionContextFromTx(contractTxId, transaction);
|
||||
logger.debug('Creating execution context', benchmark.elapsed());
|
||||
|
||||
benchmark.reset();
|
||||
const evalStateResult = await this.stateEvaluator.eval(executionContext, []);
|
||||
logger.debug(`Evaluating ${contractTxId} state`, benchmark.elapsed());
|
||||
|
||||
const interaction: ContractInteraction = {
|
||||
input,
|
||||
caller: executionContext.caller
|
||||
};
|
||||
|
||||
return await executionContext.handler.handle<Input, View>(
|
||||
executionContext,
|
||||
evalStateResult.state,
|
||||
interaction,
|
||||
transaction,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
async writeInteraction<Input>(contractTxId: string, wallet: JWKInterface, input: Input) {
|
||||
// TODO: currently it simply routes to the "old" version, but there isn't much to refactor here...
|
||||
//return await interactWrite(this.arweave, wallet, contractTxId, input);
|
||||
}
|
||||
|
||||
private async createExecutionContext<State = any>(
|
||||
contractTxId: string,
|
||||
blockHeight?: number,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<ExecutionContext<State, HandlerApi<State>>> {
|
||||
let currentNetworkInfo;
|
||||
|
||||
if (blockHeight == null) {
|
||||
// FIXME: this should be done only once for the whole execution!
|
||||
// - how to implement this without using some "global", singleton-based provider?
|
||||
currentNetworkInfo = await this.arweave.network.getInfo();
|
||||
blockHeight = currentNetworkInfo.height;
|
||||
}
|
||||
|
||||
if (evaluationOptions == null) {
|
||||
evaluationOptions = new DefaultEvaluationOptions();
|
||||
}
|
||||
|
||||
const contractDefinition = await this.definitionLoader.load(contractTxId);
|
||||
const handler = await this.executorFactory.create(contractDefinition);
|
||||
const interactions = await this.interactionsLoader.load(contractTxId, blockHeight);
|
||||
const sortedInteractions = await this.interactionsSorter.sort(interactions);
|
||||
|
||||
return {
|
||||
contractDefinition,
|
||||
handler,
|
||||
blockHeight,
|
||||
interactions,
|
||||
sortedInteractions,
|
||||
client: this,
|
||||
evaluationOptions,
|
||||
currentNetworkInfo
|
||||
};
|
||||
}
|
||||
|
||||
private async createExecutionContextFromTx<State = any>(
|
||||
contractTxId: string,
|
||||
transaction: InteractionTx,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<ExecutionContext<State, HandlerApi<State>>> {
|
||||
const blockHeight = transaction.block.height;
|
||||
const caller = transaction.owner.address;
|
||||
const contractDefinition = await this.definitionLoader.load(contractTxId);
|
||||
const handler = await this.executorFactory.create(contractDefinition);
|
||||
const interactions = await this.interactionsLoader.load(contractTxId, blockHeight);
|
||||
const sortedInteractions = await this.interactionsSorter.sort(interactions);
|
||||
|
||||
if (evaluationOptions == null) {
|
||||
evaluationOptions = new DefaultEvaluationOptions();
|
||||
}
|
||||
|
||||
return {
|
||||
contractDefinition,
|
||||
handler,
|
||||
blockHeight,
|
||||
interactions,
|
||||
sortedInteractions,
|
||||
client: this,
|
||||
evaluationOptions,
|
||||
caller
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { DefinitionLoader, ExecutorFactory, InteractionsLoader, StateEvaluator } from '@core';
|
||||
import Arweave from 'arweave';
|
||||
import { Contract } from '@client';
|
||||
|
||||
export class SmartWeave {
|
||||
constructor(
|
||||
private readonly arweave: Arweave,
|
||||
private readonly definitionLoader: DefinitionLoader,
|
||||
private readonly executorFactory: ExecutorFactory,
|
||||
private readonly interactionsLoader: InteractionsLoader,
|
||||
private readonly interactionsSorter: InteractionsLoader,
|
||||
private readonly stateEvaluator: StateEvaluator
|
||||
) {}
|
||||
|
||||
contract(contractTxId: string): Contract {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import Arweave from 'arweave';
|
||||
import { HandlerBasedSwcClient, Contract } from './index';
|
||||
import {
|
||||
CacheableContractInteractionsLoader,
|
||||
CacheableExecutorFactory,
|
||||
CacheableStateEvaluator,
|
||||
Evolve
|
||||
} from '@smartweave/plugins';
|
||||
import {
|
||||
ContractDefinitionLoader,
|
||||
ContractInteractionsLoader,
|
||||
DefaultStateEvaluator,
|
||||
EvalStateResult,
|
||||
HandlerExecutorFactory,
|
||||
LexicographicalInteractionsSorter
|
||||
} from '@smartweave/core';
|
||||
import { BsonFileBlockHeightSwCache, MemBlockHeightSwCache, MemCache } from '@smartweave/cache';
|
||||
|
||||
/**
|
||||
* A factory that simplifies the process of creating different versions of {@link Contract}.
|
||||
* All versions have the {@link Evolve} plugin...erm, plugged in ;-).
|
||||
*
|
||||
* TODO: add builders (or BUIDLers ;-)) that would simplify the process of customizing SwcClient behaviour
|
||||
* TODO: consider introducing some IoC container (like `inversify`),
|
||||
* but without forcing to use it (as some developers may be allergic to IoC/DI concepts ;-))
|
||||
* - this would probably require some consultations within the community.
|
||||
*/
|
||||
export class SwClientFactory {
|
||||
/**
|
||||
* Returns a {@link Contract} that is using mem cache for all layers.
|
||||
*/
|
||||
static memCacheClient(arweave: Arweave): Contract {
|
||||
const definitionLoader = new ContractDefinitionLoader<any>(arweave, new MemCache());
|
||||
|
||||
const interactionsLoader = new CacheableContractInteractionsLoader(
|
||||
new ContractInteractionsLoader(arweave),
|
||||
new MemBlockHeightSwCache()
|
||||
);
|
||||
|
||||
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
|
||||
|
||||
const stateEvaluator = new CacheableStateEvaluator(arweave, new MemBlockHeightSwCache<EvalStateResult>(), [
|
||||
new Evolve(definitionLoader, executorFactory)
|
||||
]);
|
||||
|
||||
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
||||
|
||||
return new HandlerBasedSwcClient(
|
||||
arweave,
|
||||
definitionLoader,
|
||||
interactionsLoader,
|
||||
executorFactory,
|
||||
stateEvaluator,
|
||||
interactionsSorter
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Contract} that is using file-based cache for {@link StateEvaluator} layer
|
||||
* and mem cache for the rest.
|
||||
*/
|
||||
static fileCacheClient(arweave: Arweave, cacheBasePath?: string): Contract {
|
||||
const definitionLoader = new ContractDefinitionLoader<any>(arweave, new MemCache());
|
||||
|
||||
const interactionsLoader = new CacheableContractInteractionsLoader(
|
||||
new ContractInteractionsLoader(arweave),
|
||||
new MemBlockHeightSwCache()
|
||||
);
|
||||
|
||||
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
|
||||
|
||||
const stateEvaluator = new CacheableStateEvaluator(
|
||||
arweave,
|
||||
new BsonFileBlockHeightSwCache<EvalStateResult>(cacheBasePath),
|
||||
[new Evolve(definitionLoader, executorFactory)]
|
||||
);
|
||||
|
||||
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
||||
|
||||
return new HandlerBasedSwcClient(
|
||||
arweave,
|
||||
definitionLoader,
|
||||
interactionsLoader,
|
||||
executorFactory,
|
||||
stateEvaluator,
|
||||
interactionsSorter
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Contract} that (yup, you've guessed it!) does not use any caches.
|
||||
* This one is gonna be slooow...
|
||||
*/
|
||||
static noCacheClient(arweave: Arweave): Contract {
|
||||
const definitionLoader = new ContractDefinitionLoader(arweave);
|
||||
const interactionsLoader = new ContractInteractionsLoader(arweave);
|
||||
const executorFactory = new HandlerExecutorFactory(arweave);
|
||||
const stateEvaluator = new DefaultStateEvaluator(arweave, [new Evolve(definitionLoader, executorFactory)]);
|
||||
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
||||
|
||||
return new HandlerBasedSwcClient(
|
||||
arweave,
|
||||
definitionLoader,
|
||||
interactionsLoader,
|
||||
executorFactory,
|
||||
stateEvaluator,
|
||||
interactionsSorter
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './HandlerBasedSwcClient';
|
||||
export * from './Contract';
|
||||
export * from './SwClientFactory';
|
||||
@@ -1,22 +1,16 @@
|
||||
import { EvalStateResult, EvaluationOptions, InteractionResult, InteractionTx } from '@smartweave';
|
||||
import { EvalStateResult, EvaluationOptions, HandlerApi, InteractionResult, InteractionTx } from '@smartweave';
|
||||
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||
|
||||
/**
|
||||
* A base interface to be implemented by SmartWeave Contracts clients.
|
||||
*
|
||||
* TODO: still to decide - whether create separate SwcClient for each contract (i.e. each contractTxId)
|
||||
* - and stop passing `contractTxId` param in the interface methods
|
||||
* OR
|
||||
* keep it as is (one instance of client can be used for interaction with multiple contracts
|
||||
* - this introduces some issues with generic types - as we cannot declare `State` and `Api' types at interface level).
|
||||
*/
|
||||
export interface Contract {
|
||||
export interface Contract<State = unknown> {
|
||||
connect(wallet: JWKInterface): Contract<State>;
|
||||
/**
|
||||
* Returns state of the contract at required blockHeight.
|
||||
* Similar to {@link readContract} from the current version.
|
||||
*/
|
||||
readState<State = any>(
|
||||
contractTxId: string,
|
||||
readState(
|
||||
blockHeight?: number,
|
||||
currentTx?: { interactionTxId: string; contractTxId: string }[],
|
||||
evaluationOptions?: EvaluationOptions
|
||||
@@ -26,13 +20,11 @@ export interface Contract {
|
||||
* Returns the view of the state, computed by the SWC.
|
||||
* Similar to the {@link interactRead} from the current SDK version.
|
||||
*/
|
||||
viewState<Input = any, View = any>(
|
||||
contractTxId: string,
|
||||
viewState<Input, View>(
|
||||
input: Input,
|
||||
wallet: JWKInterface,
|
||||
blockHeight?: number,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<InteractionResult<any, View>>;
|
||||
): Promise<InteractionResult<State, View>>;
|
||||
|
||||
/**
|
||||
* A version of the viewState method to be used from within the contract's source code.
|
||||
@@ -41,15 +33,14 @@ export interface Contract {
|
||||
* note: calling "interactRead" from withing contract's source code was not previously possible -
|
||||
* this is a new feature.
|
||||
*/
|
||||
viewStateForTx<Input = any, View = any>(
|
||||
contractTxId: string,
|
||||
viewStateForTx<Input, View>(
|
||||
input: Input,
|
||||
transaction: InteractionTx,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<InteractionResult<any, View>>;
|
||||
): Promise<InteractionResult<State, View>>;
|
||||
|
||||
/**
|
||||
* Writes a new "interaction" transaction - ie. such transaction that stores input for the contract.
|
||||
*/
|
||||
writeInteraction<Input = any>(contractTxId: string, wallet: JWKInterface, input: Input);
|
||||
writeInteraction<Input>(input: Input);
|
||||
}
|
||||
205
src/contract/HandlerBasedContract.ts
Normal file
205
src/contract/HandlerBasedContract.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||
import {
|
||||
Contract,
|
||||
ContractInteraction,
|
||||
DefaultEvaluationOptions,
|
||||
EvalStateResult,
|
||||
EvaluationOptions,
|
||||
ExecutionContext,
|
||||
ExecutorFactory,
|
||||
HandlerApi,
|
||||
InteractionResult,
|
||||
InteractionTx,
|
||||
LoggerFactory,
|
||||
SmartWeave
|
||||
} from '@smartweave';
|
||||
|
||||
const logger = LoggerFactory.INST.create(__filename);
|
||||
|
||||
/**
|
||||
* An implementation of {@link Contract} that is backwards compatible with current style
|
||||
* of writing SW contracts (ie. using the "handle" function).
|
||||
*
|
||||
* It requires {@link ExecutorFactory} that is using {@link HandlerApi} generic type.
|
||||
*/
|
||||
export class HandlerBasedContract<State> implements Contract<State> {
|
||||
private wallet?: JWKInterface;
|
||||
|
||||
constructor(
|
||||
private readonly contractTxId: string,
|
||||
private readonly smartweave: SmartWeave,
|
||||
private readonly parentContract: Contract = null
|
||||
) {}
|
||||
|
||||
async readState(
|
||||
blockHeight?: number,
|
||||
currentTx?: { interactionTxId: string; contractTxId: string }[],
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<EvalStateResult<State>> {
|
||||
logger.info('Read state for %s', this.contractTxId);
|
||||
const { stateEvaluator } = this.smartweave;
|
||||
const executionContext = await this.createExecutionContext(this.contractTxId, blockHeight, evaluationOptions);
|
||||
const result = await stateEvaluator.eval(executionContext, currentTx || []);
|
||||
|
||||
return result as EvalStateResult<State>;
|
||||
}
|
||||
|
||||
async viewState<Input, View>(
|
||||
input: Input,
|
||||
blockHeight?: number,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<InteractionResult<State, View>> {
|
||||
if (!this.wallet) {
|
||||
throw new Error("Wallet not connected. Use 'connect' method first.");
|
||||
}
|
||||
|
||||
logger.info('View state for %s', this.contractTxId);
|
||||
const { arweave, stateEvaluator } = this.smartweave;
|
||||
let executionContext = await this.createExecutionContext(this.contractTxId, blockHeight, evaluationOptions);
|
||||
|
||||
if (!executionContext.currentBlockData) {
|
||||
const currentBlockData = executionContext.currentNetworkInfo
|
||||
? // trying to optimise calls to arweave as much as possible...
|
||||
await arweave.blocks.get(executionContext.currentNetworkInfo.current)
|
||||
: await arweave.blocks.getCurrent();
|
||||
|
||||
executionContext = {
|
||||
...executionContext,
|
||||
currentBlockData
|
||||
};
|
||||
}
|
||||
|
||||
const caller = await arweave.wallets.jwkToAddress(this.wallet);
|
||||
executionContext = {
|
||||
...executionContext,
|
||||
caller
|
||||
};
|
||||
|
||||
const evalStateResult = await stateEvaluator.eval(executionContext, []);
|
||||
|
||||
const interaction: ContractInteraction<Input> = {
|
||||
input,
|
||||
caller: executionContext.caller
|
||||
};
|
||||
|
||||
const handler = (await this.smartweave.executorFactory.create(
|
||||
executionContext.contractDefinition
|
||||
)) as HandlerApi<State>;
|
||||
|
||||
// TODO: what is the best way to create a transaction in this case?
|
||||
return await handler.handle<Input, View>(
|
||||
executionContext,
|
||||
evalStateResult.state,
|
||||
interaction,
|
||||
{
|
||||
id: null,
|
||||
recipient: null,
|
||||
owner: {
|
||||
address: executionContext.caller
|
||||
},
|
||||
tags: [],
|
||||
fee: null,
|
||||
quantity: null,
|
||||
block: executionContext.currentBlockData
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
async viewStateForTx<Input, View>(
|
||||
input: Input,
|
||||
transaction: InteractionTx,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<InteractionResult<State, View>> {
|
||||
logger.info('Vies state for %s %o', this.contractTxId, transaction);
|
||||
const { stateEvaluator } = this.smartweave;
|
||||
|
||||
const executionContext = await this.createExecutionContextFromTx(this.contractTxId, transaction);
|
||||
const evalStateResult = await stateEvaluator.eval(executionContext, []);
|
||||
|
||||
const interaction: ContractInteraction<Input> = {
|
||||
input,
|
||||
caller: executionContext.caller
|
||||
};
|
||||
|
||||
const handler = (await this.smartweave.executorFactory.create(
|
||||
executionContext.contractDefinition
|
||||
)) as HandlerApi<State>;
|
||||
|
||||
return await handler.handle<Input, View>(executionContext, evalStateResult.state, interaction, transaction, []);
|
||||
}
|
||||
|
||||
async writeInteraction<Input>(input: Input) {
|
||||
// TODO: currently it simply routes to the "old" version, but there isn't much to refactor here...
|
||||
//return await interactWrite(this.arweave, wallet, contractTxId, input);
|
||||
}
|
||||
|
||||
private async createExecutionContext(
|
||||
contractTxId: string,
|
||||
blockHeight?: number,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<ExecutionContext<State>> {
|
||||
const { arweave, definitionLoader, interactionsLoader, interactionsSorter } = this.smartweave;
|
||||
|
||||
let currentNetworkInfo;
|
||||
|
||||
if (blockHeight == null) {
|
||||
// FIXME: this should be done only once for the whole execution!
|
||||
// - how to implement this without using some "global", singleton-based provider?
|
||||
currentNetworkInfo = await arweave.network.getInfo();
|
||||
blockHeight = currentNetworkInfo.height;
|
||||
}
|
||||
|
||||
if (evaluationOptions == null) {
|
||||
evaluationOptions = new DefaultEvaluationOptions();
|
||||
}
|
||||
|
||||
const contractDefinition = await definitionLoader.load<State>(contractTxId);
|
||||
const interactions = await interactionsLoader.load(contractTxId, blockHeight);
|
||||
const sortedInteractions = await interactionsSorter.sort(interactions);
|
||||
|
||||
return {
|
||||
contractDefinition,
|
||||
blockHeight,
|
||||
interactions,
|
||||
sortedInteractions,
|
||||
smartweave: this.smartweave,
|
||||
contract: this,
|
||||
evaluationOptions,
|
||||
currentNetworkInfo
|
||||
};
|
||||
}
|
||||
|
||||
private async createExecutionContextFromTx(
|
||||
contractTxId: string,
|
||||
transaction: InteractionTx,
|
||||
evaluationOptions?: EvaluationOptions
|
||||
): Promise<ExecutionContext<State>> {
|
||||
const { definitionLoader, executorFactory, interactionsLoader, interactionsSorter } = this.smartweave;
|
||||
const blockHeight = transaction.block.height;
|
||||
const caller = transaction.owner.address;
|
||||
const contractDefinition = await definitionLoader.load<State>(contractTxId);
|
||||
const interactions = await interactionsLoader.load(contractTxId, blockHeight);
|
||||
const sortedInteractions = await interactionsSorter.sort(interactions);
|
||||
|
||||
if (evaluationOptions == null) {
|
||||
evaluationOptions = new DefaultEvaluationOptions();
|
||||
}
|
||||
|
||||
return {
|
||||
contractDefinition,
|
||||
blockHeight,
|
||||
interactions,
|
||||
sortedInteractions,
|
||||
smartweave: this.smartweave,
|
||||
contract: this,
|
||||
evaluationOptions,
|
||||
caller
|
||||
};
|
||||
}
|
||||
|
||||
connect(wallet: JWKInterface): Contract<State> {
|
||||
this.wallet = wallet;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
32
src/contract/SmartWeave.ts
Normal file
32
src/contract/SmartWeave.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
DefinitionLoader,
|
||||
ExecutorFactory,
|
||||
HandlerApi,
|
||||
InteractionsLoader,
|
||||
InteractionsSorter,
|
||||
StateEvaluator
|
||||
} from '@smartweave/core';
|
||||
import Arweave from 'arweave';
|
||||
import { Contract, HandlerBasedContract, SmartWeaveBuilder } from '@smartweave/contract';
|
||||
|
||||
/**
|
||||
* The "motherboard" ;-)
|
||||
*/
|
||||
export class SmartWeave {
|
||||
constructor(
|
||||
readonly arweave: Arweave,
|
||||
readonly definitionLoader: DefinitionLoader,
|
||||
readonly interactionsLoader: InteractionsLoader,
|
||||
readonly interactionsSorter: InteractionsSorter,
|
||||
readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>,
|
||||
readonly stateEvaluator: StateEvaluator
|
||||
) {}
|
||||
|
||||
static builder(arweave: Arweave) {
|
||||
return new SmartWeaveBuilder(arweave);
|
||||
}
|
||||
|
||||
contract<State>(contractTxId: string, parent?: Contract): Contract<State> {
|
||||
return new HandlerBasedContract<State>(contractTxId, this, parent);
|
||||
}
|
||||
}
|
||||
56
src/contract/SmartWeaveBuilder.ts
Normal file
56
src/contract/SmartWeaveBuilder.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import Arweave from 'arweave';
|
||||
import {
|
||||
DefinitionLoader,
|
||||
ExecutorFactory,
|
||||
HandlerApi,
|
||||
InteractionsLoader,
|
||||
InteractionsSorter,
|
||||
SmartWeave,
|
||||
StateEvaluator
|
||||
} from '@smartweave';
|
||||
|
||||
export class SmartWeaveBuilder {
|
||||
private _definitionLoader?: DefinitionLoader;
|
||||
private _interactionsLoader?: InteractionsLoader;
|
||||
private _interactionsSorter?: InteractionsSorter;
|
||||
private _executorFactory?: ExecutorFactory<HandlerApi<unknown>>;
|
||||
private _stateEvaluator?: StateEvaluator;
|
||||
|
||||
constructor(private readonly _arweave: Arweave) {}
|
||||
|
||||
public setDefinitionLoader(value: DefinitionLoader): SmartWeaveBuilder {
|
||||
this._definitionLoader = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setInteractionsLoader(value: InteractionsLoader): SmartWeaveBuilder {
|
||||
this._interactionsLoader = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setInteractionsSorter(value: InteractionsSorter): SmartWeaveBuilder {
|
||||
this._interactionsSorter = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setExecutorFactory(value: ExecutorFactory<HandlerApi<unknown>>): SmartWeaveBuilder {
|
||||
this._executorFactory = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public setStateEvaluator(value: StateEvaluator): SmartWeaveBuilder {
|
||||
this._stateEvaluator = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
return new SmartWeave(
|
||||
this._arweave,
|
||||
this._definitionLoader,
|
||||
this._interactionsLoader,
|
||||
this._interactionsSorter,
|
||||
this._executorFactory,
|
||||
this._stateEvaluator
|
||||
);
|
||||
}
|
||||
}
|
||||
57
src/contract/SmartWeaveFactory.ts
Normal file
57
src/contract/SmartWeaveFactory.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import Arweave from 'arweave';
|
||||
import { HandlerBasedContract, Contract, SmartWeave } from './index';
|
||||
import {
|
||||
CacheableContractInteractionsLoader,
|
||||
CacheableExecutorFactory,
|
||||
CacheableStateEvaluator,
|
||||
Evolve
|
||||
} from '@smartweave/plugins';
|
||||
import {
|
||||
ContractDefinitionLoader,
|
||||
ContractInteractionsLoader,
|
||||
DefaultStateEvaluator,
|
||||
EvalStateResult,
|
||||
HandlerApi,
|
||||
HandlerExecutorFactory,
|
||||
LexicographicalInteractionsSorter
|
||||
} from '@smartweave/core';
|
||||
import { BsonFileBlockHeightSwCache, MemBlockHeightSwCache, MemCache } from '@smartweave/cache';
|
||||
|
||||
/**
|
||||
* A factory that simplifies the process of creating different versions of {@link Contract}.
|
||||
* All versions have the {@link Evolve} plugin...erm, plugged in ;-).
|
||||
*
|
||||
* TODO: add builders (or BUIDLers ;-)) that would simplify the process of customizing SwcClient behaviour
|
||||
* TODO: consider introducing some IoC container (like `inversify`),
|
||||
* but without forcing to use it (as some developers may be allergic to IoC/DI concepts ;-))
|
||||
* - this would probably require some consultations within the community.
|
||||
*/
|
||||
export class SmartWeaveFactory {
|
||||
/**
|
||||
* Returns a {@link Contract} that is using mem cache for all layers.
|
||||
*/
|
||||
static memCached(arweave: Arweave): SmartWeave {
|
||||
const definitionLoader = new ContractDefinitionLoader(arweave, new MemCache());
|
||||
|
||||
const interactionsLoader = new CacheableContractInteractionsLoader(
|
||||
new ContractInteractionsLoader(arweave),
|
||||
new MemBlockHeightSwCache()
|
||||
);
|
||||
|
||||
const executorFactory = new CacheableExecutorFactory(arweave, new HandlerExecutorFactory(arweave), new MemCache());
|
||||
|
||||
const stateEvaluator = new CacheableStateEvaluator(arweave, new MemBlockHeightSwCache<EvalStateResult<unknown>>(), [
|
||||
new Evolve(definitionLoader, executorFactory)
|
||||
]);
|
||||
|
||||
const interactionsSorter = new LexicographicalInteractionsSorter(arweave);
|
||||
|
||||
return SmartWeave.builder(arweave)
|
||||
.setDefinitionLoader(definitionLoader)
|
||||
.setInteractionsLoader(interactionsLoader)
|
||||
.setInteractionsSorter(interactionsSorter)
|
||||
.setExecutorFactory(executorFactory)
|
||||
.setStateEvaluator(stateEvaluator)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
5
src/contract/index.ts
Normal file
5
src/contract/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './Contract';
|
||||
export * from './HandlerBasedContract';
|
||||
export * from './SmartWeaveFactory';
|
||||
export * from './SmartWeave';
|
||||
export * from './SmartWeaveBuilder';
|
||||
11
src/core/ContractDefinition.ts
Normal file
11
src/core/ContractDefinition.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* contains all data and meta-data of the given contact.
|
||||
*/
|
||||
export type ContractDefinition<State> = {
|
||||
txId: string;
|
||||
srcTxId: string;
|
||||
src: string;
|
||||
initState: State;
|
||||
minFee: string;
|
||||
owner: string;
|
||||
};
|
||||
@@ -5,6 +5,6 @@ import { ContractDefinition } from '@smartweave';
|
||||
* its source code, info about owner, initial state, etc.
|
||||
* See ContractDefinition type for more details regarding what data is being loaded.
|
||||
*/
|
||||
export interface DefinitionLoader<State = any> {
|
||||
load(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>>;
|
||||
export interface DefinitionLoader {
|
||||
load<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EvaluationOptions, GQLEdgeInterface, Contract } from '@smartweave';
|
||||
import { BlockData } from 'arweave/node/blocks';
|
||||
import { Contract, ContractDefinition, EvaluationOptions, GQLEdgeInterface, SmartWeave } from '@smartweave';
|
||||
import { NetworkInfoInterface } from 'arweave/node/network';
|
||||
import { BlockData } from 'arweave/node/blocks';
|
||||
|
||||
/**
|
||||
* current execution context of the contract - contains all elements
|
||||
@@ -9,27 +9,15 @@ import { NetworkInfoInterface } from 'arweave/node/network';
|
||||
* contract's definition - which is very time consuming) multiple times
|
||||
* (eg. multiple calls to "loadContract" in "interactRead" in the current version of the SW SDK).
|
||||
*/
|
||||
export type ExecutionContext<State = any, Api = any> = {
|
||||
export type ExecutionContext<State> = {
|
||||
smartweave: SmartWeave;
|
||||
contract: Contract<State>;
|
||||
contractDefinition: ContractDefinition<State>;
|
||||
handler: Api;
|
||||
blockHeight: number;
|
||||
interactions: GQLEdgeInterface[];
|
||||
sortedInteractions: GQLEdgeInterface[];
|
||||
client: Contract;
|
||||
evaluationOptions: EvaluationOptions;
|
||||
currentNetworkInfo?: NetworkInfoInterface;
|
||||
currentBlockData?: BlockData;
|
||||
caller?: string; // note: this is only set for "viewState" operations
|
||||
};
|
||||
|
||||
/**
|
||||
* contains all data and meta-data of the given contact.
|
||||
*/
|
||||
export type ContractDefinition<State = any> = {
|
||||
txId: string;
|
||||
srcTxId: string;
|
||||
src: string;
|
||||
initState: State;
|
||||
minFee: string;
|
||||
owner: string;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExecutionContext } from '@smartweave';
|
||||
import { EvolveCompatibleState, ExecutionContext } from '@smartweave';
|
||||
|
||||
/**
|
||||
* really not a fan of this feature...
|
||||
@@ -6,6 +6,6 @@ import { ExecutionContext } from '@smartweave';
|
||||
* This adds ability to modify current execution context based
|
||||
* on state - example (and currently only) use case is the "evolve" feature...
|
||||
*/
|
||||
export interface ExecutionContextModifier<State> {
|
||||
modify(state: State, executionContext: ExecutionContext<State, any>): Promise<ExecutionContext<State, any>>;
|
||||
export interface ExecutionContextModifier {
|
||||
modify<State>(state: State, executionContext: ExecutionContext<State>): Promise<ExecutionContext<State>>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ContractDefinition } from '@smartweave';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface ContractApi {}
|
||||
|
||||
/**
|
||||
* An interface for all the factories that produce SmartWeave contracts "executors" -
|
||||
* i.e. objects that are responsible for actually running the contract's code.
|
||||
*/
|
||||
export interface ExecutorFactory<State = any, Api = any> {
|
||||
create(contractDefinition: ContractDefinition<State>): Promise<Api>;
|
||||
export interface ExecutorFactory<Api> {
|
||||
create<State>(contractDefinition: ContractDefinition<State>): Promise<Api>;
|
||||
}
|
||||
|
||||
@@ -4,29 +4,29 @@ import { ExecutionContext, GQLNodeInterface, HandlerApi } from '@smartweave';
|
||||
* Implementors of this class are responsible for evaluating contract's state
|
||||
* - based on the execution context.
|
||||
*/
|
||||
export interface StateEvaluator<State = unknown, Api = unknown> {
|
||||
eval(
|
||||
executionContext: ExecutionContext<State, Api>,
|
||||
export interface StateEvaluator {
|
||||
eval<State>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||
): Promise<EvalStateResult<State>>;
|
||||
|
||||
onStateUpdate(
|
||||
onStateUpdate<State>(
|
||||
currentInteraction: GQLNodeInterface,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
executionContext: ExecutionContext<State>,
|
||||
state: EvalStateResult<State>
|
||||
);
|
||||
}
|
||||
|
||||
export class EvalStateResult<State = unknown> {
|
||||
export class EvalStateResult<State> {
|
||||
constructor(readonly state: State, readonly validity: Record<string, boolean>) {}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
export class DefaultEvaluationOptions implements EvaluationOptions {
|
||||
// default = false - "fail-fast" approach, otherwise we can end-up with a broken state and
|
||||
// not even notice that there was an exception in state evaluation.
|
||||
// Current SDK version simply moves to next interaction in this case and ignores exception
|
||||
// - I believe this is not a proper behaviour.
|
||||
// default = true - still cannot decide whether true or false should be the default
|
||||
// "false" may lead to some fairly simple attacks on contract, if the contract
|
||||
// does not properly validate input data
|
||||
// "true" may lead to wrongly calculated state, even without noticing..
|
||||
ignoreExceptions = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,26 +4,26 @@ import Transaction from 'arweave/web/lib/transaction';
|
||||
|
||||
const logger = LoggerFactory.INST.create(__filename);
|
||||
|
||||
export class ContractDefinitionLoader<State = any> implements DefinitionLoader<State> {
|
||||
export class ContractDefinitionLoader implements DefinitionLoader {
|
||||
constructor(
|
||||
private readonly arweave: Arweave,
|
||||
// TODO: cache should be removed from the core layer and implemented in a wrapper of the core implementation
|
||||
private readonly cache?: SwCache<string, ContractDefinition<State>>
|
||||
private readonly cache?: SwCache<string, ContractDefinition<unknown>>
|
||||
) {}
|
||||
|
||||
async load(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
|
||||
async load<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
|
||||
if (!forcedSrcTxId && this.cache?.contains(contractTxId)) {
|
||||
logger.debug('ContractDefinitionLoader: Hit from cache!');
|
||||
return Promise.resolve(this.cache?.get(contractTxId));
|
||||
return Promise.resolve(this.cache?.get(contractTxId) as ContractDefinition<State>);
|
||||
}
|
||||
|
||||
const contract = await this.doLoad(contractTxId, forcedSrcTxId);
|
||||
const contract = await this.doLoad<State>(contractTxId, forcedSrcTxId);
|
||||
this.cache?.put(contractTxId, contract);
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async doLoad(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
|
||||
async doLoad<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>> {
|
||||
const contractTx = await this.arweave.transactions.get(contractTxId);
|
||||
const owner = await this.arweave.wallets.ownerToAddress(contractTx.owner);
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import {
|
||||
Benchmark,
|
||||
ContractInteraction,
|
||||
EvalStateResult,
|
||||
EvolveCompatibleState,
|
||||
ExecutionContext,
|
||||
ExecutionContextModifier,
|
||||
GQLEdgeInterface,
|
||||
GQLNodeInterface,
|
||||
GQLTagInterface,
|
||||
HandlerApi,
|
||||
HandlerResult,
|
||||
InteractionResult,
|
||||
LoggerFactory,
|
||||
SmartWeaveTags,
|
||||
StateEvaluator
|
||||
@@ -18,14 +19,14 @@ import Arweave from 'arweave';
|
||||
const logger = LoggerFactory.INST.create(__filename);
|
||||
|
||||
// FIXME: currently this is tightly coupled with the HandlerApi
|
||||
export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<State, HandlerApi<State>> {
|
||||
export class DefaultStateEvaluator implements StateEvaluator {
|
||||
constructor(
|
||||
private readonly arweave: Arweave,
|
||||
private readonly executionContextModifiers: ExecutionContextModifier<State>[] = []
|
||||
private readonly executionContextModifiers: ExecutionContextModifier[] = []
|
||||
) {}
|
||||
|
||||
async eval(
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
async eval<State, Api>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||
): Promise<EvalStateResult<State>> {
|
||||
return this.doReadState(
|
||||
@@ -36,10 +37,10 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
||||
);
|
||||
}
|
||||
|
||||
protected async doReadState(
|
||||
protected async doReadState<State, Api>(
|
||||
missingInteractions: GQLEdgeInterface[],
|
||||
baseState: EvalStateResult<State>,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||
): Promise<EvalStateResult<State>> {
|
||||
const evaluationOptions = executionContext.evaluationOptions;
|
||||
@@ -51,6 +52,10 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
||||
`Evaluating state for ${executionContext.contractDefinition.txId} [${missingInteractions.length} non-cached of ${executionContext.sortedInteractions.length} all]`
|
||||
);
|
||||
|
||||
const handler: HandlerApi<State> = (await executionContext.smartweave.executorFactory.create<State>(
|
||||
executionContext.contractDefinition
|
||||
)) as HandlerApi<State>;
|
||||
|
||||
for (const missingInteraction of missingInteractions) {
|
||||
logger.debug(
|
||||
`${missingInteraction.node.id}: ${missingInteractions.indexOf(missingInteraction) + 1}/${
|
||||
@@ -72,20 +77,14 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
||||
continue;
|
||||
}
|
||||
|
||||
const interaction: ContractInteraction = {
|
||||
const interaction: ContractInteraction<unknown> = {
|
||||
input,
|
||||
caller: currentInteraction.owner.address
|
||||
};
|
||||
|
||||
const result = await executionContext.handler.handle(
|
||||
executionContext,
|
||||
currentState,
|
||||
interaction,
|
||||
currentInteraction,
|
||||
currentTx
|
||||
);
|
||||
const result = await handler.handle(executionContext, currentState, interaction, currentInteraction, currentTx);
|
||||
|
||||
this.logResult(result, currentInteraction);
|
||||
this.logResult<State>(result, currentInteraction);
|
||||
|
||||
if (result.type === 'exception' && evaluationOptions.ignoreExceptions !== true) {
|
||||
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.result}`);
|
||||
@@ -101,16 +100,13 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
||||
executionContext = await modify(currentState, executionContext);
|
||||
}
|
||||
|
||||
this.onStateUpdate(currentInteraction, executionContext, new EvalStateResult(currentState, validity));
|
||||
this.onStateUpdate<State>(currentInteraction, executionContext, new EvalStateResult(currentState, validity));
|
||||
}
|
||||
|
||||
return new EvalStateResult<State>(currentState, validity);
|
||||
}
|
||||
|
||||
private logResult(
|
||||
result: HandlerResult<State> & { type: 'ok' | 'error' | 'exception' },
|
||||
currentTx: GQLNodeInterface
|
||||
) {
|
||||
private logResult<State>(result: InteractionResult<State, unknown>, currentTx: GQLNodeInterface) {
|
||||
if (result.type === 'exception') {
|
||||
logger.error(`${result.result}`);
|
||||
logger.error(`Executing of interaction: ${currentTx.id} threw exception.`);
|
||||
@@ -130,9 +126,9 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
||||
}
|
||||
}
|
||||
|
||||
private findInputTag(
|
||||
private findInputTag<State>(
|
||||
missingInteraction: GQLEdgeInterface,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>
|
||||
executionContext: ExecutionContext<State>
|
||||
): GQLTagInterface {
|
||||
const contractIndex = missingInteraction.node.tags.findIndex(
|
||||
(tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID && tag.value === executionContext.contractDefinition.txId
|
||||
@@ -141,9 +137,9 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
||||
return missingInteraction.node.tags[contractIndex + 1];
|
||||
}
|
||||
|
||||
onStateUpdate(
|
||||
onStateUpdate<State>(
|
||||
currentInteraction: GQLNodeInterface,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
executionContext: ExecutionContext<State>,
|
||||
state: EvalStateResult<State>
|
||||
) {
|
||||
// noop
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
*/
|
||||
export interface HandlerApi<State> {
|
||||
handle<Input, Result>(
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
executionContext: ExecutionContext<State>,
|
||||
state: State,
|
||||
interaction: ContractInteraction<Input>,
|
||||
interactionTx: InteractionTx,
|
||||
@@ -29,10 +29,10 @@ const logger = LoggerFactory.INST.create(__filename);
|
||||
* A factory that produces handlers that are compatible with the "current" style of
|
||||
* writing SW contracts (ie. using "handle" function).
|
||||
*/
|
||||
export class HandlerExecutorFactory<State = any> implements ExecutorFactory<State, HandlerApi<State>> {
|
||||
export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknown>> {
|
||||
constructor(private readonly arweave: Arweave) {}
|
||||
|
||||
async create(contractDefinition: ContractDefinition<State>): Promise<HandlerApi<State>> {
|
||||
async create<State>(contractDefinition: ContractDefinition<State>): Promise<HandlerApi<State>> {
|
||||
const normalizedSource = HandlerExecutorFactory.normalizeContractSource(contractDefinition.src);
|
||||
|
||||
const swGlobal = new SmartWeaveGlobal(this.arweave, {
|
||||
@@ -40,26 +40,26 @@ export class HandlerExecutorFactory<State = any> implements ExecutorFactory<Stat
|
||||
owner: contractDefinition.owner
|
||||
});
|
||||
const contractFunction = new Function(normalizedSource);
|
||||
const handler = contractFunction(swGlobal, BigNumber, clarity) as HandlerFunction<State>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
async handle<Input>(
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
async handle<Input, Result>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
state: State,
|
||||
interaction: ContractInteraction<Input>,
|
||||
interactionTx: InteractionTx,
|
||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||
): Promise<InteractionResult<State>> {
|
||||
): Promise<InteractionResult<State, Result>> {
|
||||
try {
|
||||
const handler = contractFunction(swGlobal, BigNumber, clarity) as HandlerFunction<State, Input, Result>;
|
||||
const stateCopy = JSON.parse(JSON.stringify(state));
|
||||
swGlobal._activeTx = interactionTx;
|
||||
logger.debug(`SmartWeave.contract.id: ${swGlobal.contract.id}`, swGlobal.contract.id);
|
||||
|
||||
self.assignReadContractState(swGlobal, contractDefinition, interaction, executionContext, currentTx);
|
||||
self.assignViewContractState(swGlobal, contractDefinition, executionContext);
|
||||
self.assignReadContractState<Input, State>(swGlobal, contractDefinition, executionContext, currentTx);
|
||||
self.assignViewContractState<Input, State>(swGlobal, contractDefinition, executionContext);
|
||||
|
||||
const handlerResult = await handler(stateCopy, interaction);
|
||||
|
||||
@@ -93,37 +93,42 @@ export class HandlerExecutorFactory<State = any> implements ExecutorFactory<Stat
|
||||
};
|
||||
}
|
||||
|
||||
private assignViewContractState<Input>(
|
||||
private assignViewContractState<Input, State>(
|
||||
swGlobal: SmartWeaveGlobal,
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>
|
||||
executionContext: ExecutionContext<State>
|
||||
) {
|
||||
swGlobal.contracts.viewContractState = async <View>(contractTxId: string, input: any) => {
|
||||
logger.debug('swGlobal.viewContractState call:', {
|
||||
throw new Error('TODO implement');
|
||||
/*logger.debug('swGlobal.viewContractState call: %o', {
|
||||
from: contractDefinition.txId,
|
||||
to: contractTxId,
|
||||
input
|
||||
});
|
||||
return await executionContext.client.viewStateForTx(contractTxId, input, swGlobal._activeTx);
|
||||
return await executionContext.contract.viewStateForTx(contractTxId, input, swGlobal._activeTx);*/
|
||||
};
|
||||
}
|
||||
|
||||
private assignReadContractState<Input>(
|
||||
private assignReadContractState<Input, State>(
|
||||
swGlobal: SmartWeaveGlobal,
|
||||
contractDefinition: ContractDefinition<State>,
|
||||
interaction: ContractInteraction<Input>,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||
) {
|
||||
swGlobal.contracts.readContractState = async (contractTxId: string, height?: number, returnValidity?: boolean) => {
|
||||
logger.debug('swGlobal.readContractState call: ', {
|
||||
from: contractDefinition.txId,
|
||||
to: contractTxId,
|
||||
interaction
|
||||
to: contractTxId
|
||||
});
|
||||
const stateWithValidity = await executionContext.client.readState(contractTxId, height || swGlobal.block.height, [
|
||||
const requestedHeight = height || swGlobal.block.height;
|
||||
const childContract = executionContext.smartweave.contract(contractTxId);
|
||||
|
||||
const stateWithValidity = await childContract.readState(requestedHeight, [
|
||||
...(currentTx || []),
|
||||
{ contractTxId: contractDefinition.txId, interactionTxId: swGlobal.transaction.id }
|
||||
{
|
||||
contractTxId: contractDefinition.txId,
|
||||
interactionTxId: swGlobal.transaction.id
|
||||
}
|
||||
]);
|
||||
|
||||
// TODO: it should be up to the client's code to decide which part of the result to use
|
||||
@@ -163,19 +168,22 @@ export class HandlerExecutorFactory<State = any> implements ExecutorFactory<Stat
|
||||
}
|
||||
}
|
||||
|
||||
export type HandlerFunction<State, Input = any, Result = any> = (
|
||||
export type HandlerFunction<State, Input, Result> = (
|
||||
state: State,
|
||||
interaction: ContractInteraction<Input>
|
||||
) => Promise<HandlerResult<State, Result>>;
|
||||
|
||||
// TODO: change to XOR between result and state?
|
||||
export type HandlerResult<State = any, Result = any> = { result: Result; state: State };
|
||||
export type HandlerResult<State, Result> = {
|
||||
result: string | Result;
|
||||
state: State;
|
||||
};
|
||||
|
||||
export type InteractionResult<State = any, Result = any> = HandlerResult<State, Result> & {
|
||||
export type InteractionResult<State, Result> = HandlerResult<State, Result> & {
|
||||
type: 'ok' | 'error' | 'exception';
|
||||
};
|
||||
|
||||
export type ContractInteraction<Input = any> = {
|
||||
export type ContractInteraction<Input> = {
|
||||
input: Input;
|
||||
caller: string;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ export * from './InteractionsLoader';
|
||||
export * from './InteractionsSorter';
|
||||
export * from './StateEvaluator';
|
||||
export * from './SmartWeaveTags';
|
||||
export * from './types';
|
||||
export * from './ExecutionContext';
|
||||
export * from './ContractDefinition';
|
||||
|
||||
export * from './impl/BlockHeightInteractionsSorter';
|
||||
export * from './impl/ContractDefinitionLoader';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// - using https://www.npmjs.com/package/tsc-alias plugin
|
||||
export * from '@smartweave/logging'; // this needs to be the first exported element.
|
||||
export * from '@smartweave/core';
|
||||
export * from '@smartweave/client';
|
||||
export * from '@smartweave/contract';
|
||||
export * from '@smartweave/cache';
|
||||
export * from '@smartweave/plugins';
|
||||
export * from '@smartweave/legacy';
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ISettingsParam } from 'tslog/src/index';
|
||||
import { Logger } from 'tslog';
|
||||
|
||||
export const defaultLoggerOptions: ISettingsParam = {
|
||||
printLogMessageInNewLine: true,
|
||||
setCallerAsLoggerName: false,
|
||||
displayFunctionName: false,
|
||||
overwriteConsole: true,
|
||||
logLevelsColors: {
|
||||
'0': 'grey',
|
||||
'1': 'white',
|
||||
'2': 'cyan',
|
||||
'3': 'blue',
|
||||
'4': 'yellowBright',
|
||||
'5': 'red',
|
||||
'6': 'redBright'
|
||||
},
|
||||
minLevel: 'debug'
|
||||
};
|
||||
|
||||
export const log = {
|
||||
cache: new Logger({
|
||||
...defaultLoggerOptions
|
||||
}),
|
||||
client: new Logger({
|
||||
...defaultLoggerOptions
|
||||
}),
|
||||
core: new Logger({
|
||||
...defaultLoggerOptions
|
||||
}),
|
||||
plugins: new Logger({
|
||||
...defaultLoggerOptions
|
||||
})
|
||||
};
|
||||
@@ -8,19 +8,19 @@ const logger = LoggerFactory.INST.create(__filename);
|
||||
/**
|
||||
* An implementation of ExecutorFactory that adds caching capabilities
|
||||
*/
|
||||
export class CacheableExecutorFactory<State, Api> implements ExecutorFactory<State, Api> {
|
||||
export class CacheableExecutorFactory<Api> implements ExecutorFactory<Api> {
|
||||
constructor(
|
||||
arweave: Arweave,
|
||||
private readonly baseImplementation: ExecutorFactory<State, Api>,
|
||||
private readonly baseImplementation: ExecutorFactory<Api>,
|
||||
private readonly cache: SwCache<string, Api>
|
||||
) {}
|
||||
|
||||
async create(contractDefinition: ContractDefinition<State>): Promise<Api> {
|
||||
async create<State>(contractDefinition: ContractDefinition<State>): Promise<Api> {
|
||||
// warn: do not cache on the contractDefinition.srcTxId. This might look like a good optimisation
|
||||
// (as many contracts share the same source code), but unfortunately this is causing issues
|
||||
// with the same SwGlobal object being cached for all contracts with the same source code
|
||||
// (eg. SwGlobal.contract.id field - which of course should have different value for contracts
|
||||
// with the same source).
|
||||
// (eg. SwGlobal.contract.id field - which of course should have different value for different contracts
|
||||
// that share the same source).
|
||||
const cacheKey = contractDefinition.txId;
|
||||
if (!this.cache.contains(cacheKey)) {
|
||||
logger.debug('Updating executor factory cache');
|
||||
|
||||
@@ -15,17 +15,17 @@ const logger = LoggerFactory.INST.create(__filename);
|
||||
/**
|
||||
* An implementation of DefaultStateEvaluator that adds caching capabilities
|
||||
*/
|
||||
export class CacheableStateEvaluator<State> extends DefaultStateEvaluator<State> {
|
||||
export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||
constructor(
|
||||
arweave: Arweave,
|
||||
private readonly cache: BlockHeightSwCache<EvalStateResult<State>>,
|
||||
executionContextModifiers: ExecutionContextModifier<State>[] = []
|
||||
private readonly cache: BlockHeightSwCache<EvalStateResult<unknown>>,
|
||||
executionContextModifiers: ExecutionContextModifier[] = []
|
||||
) {
|
||||
super(arweave, executionContextModifiers);
|
||||
}
|
||||
|
||||
async eval(
|
||||
executionContext: ExecutionContext<State, any>,
|
||||
async eval<State, Api>(
|
||||
executionContext: ExecutionContext<State>,
|
||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||
): Promise<EvalStateResult<State>> {
|
||||
const requestedBlockHeight = executionContext.blockHeight;
|
||||
@@ -42,7 +42,10 @@ export class CacheableStateEvaluator<State> extends DefaultStateEvaluator<State>
|
||||
// if there was anything to cache...
|
||||
if (sortedInteractionsUpToBlock.length > 0) {
|
||||
// get latest available cache for the requested block height
|
||||
cachedState = this.cache.getLessOrEqual(executionContext.contractDefinition.txId, requestedBlockHeight);
|
||||
cachedState = this.cache.getLessOrEqual(
|
||||
executionContext.contractDefinition.txId,
|
||||
requestedBlockHeight
|
||||
) as BlockHeightCacheResult<EvalStateResult<State>>;
|
||||
|
||||
if (cachedState != null) {
|
||||
logger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
|
||||
@@ -101,9 +104,9 @@ export class CacheableStateEvaluator<State> extends DefaultStateEvaluator<State>
|
||||
);
|
||||
}
|
||||
|
||||
onStateUpdate(
|
||||
onStateUpdate<State>(
|
||||
currentInteraction: GQLNodeInterface,
|
||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
||||
executionContext: ExecutionContext<State>,
|
||||
state: EvalStateResult<State>
|
||||
) {
|
||||
this.cache.put(
|
||||
|
||||
@@ -7,15 +7,15 @@ import { ContractDefinition, ExecutorFactory } from '@smartweave/core';
|
||||
*
|
||||
* Not meant to be used in production env! ;-)
|
||||
*/
|
||||
export class DebuggableExecutorFactory<State, Api> implements ExecutorFactory<State, Api> {
|
||||
export class DebuggableExecutorFactory<Api> implements ExecutorFactory<Api> {
|
||||
constructor(
|
||||
private readonly baseImplementation: ExecutorFactory<State, Api>,
|
||||
private readonly baseImplementation: ExecutorFactory<Api>,
|
||||
private readonly sourceCode: { [key: string]: string }
|
||||
) {
|
||||
// contract source code before default "normalization"
|
||||
}
|
||||
|
||||
async create(contractDefinition: ContractDefinition<State>): Promise<Api> {
|
||||
async create<State>(contractDefinition: ContractDefinition<State>): Promise<Api> {
|
||||
if (Object.prototype.hasOwnProperty.call(this.sourceCode, contractDefinition.txId)) {
|
||||
contractDefinition = {
|
||||
...contractDefinition,
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
ExecutionContext,
|
||||
ExecutionContextModifier,
|
||||
ExecutorFactory,
|
||||
HandlerApi,
|
||||
LoggerFactory,
|
||||
SmartWeaveError,
|
||||
SmartWeaveErrorType
|
||||
} from '@smartweave';
|
||||
|
||||
export interface EvolveCompatibleState {
|
||||
settings: never[]; // some..erm..settings?
|
||||
settings: any[]; // some..erm..settings?
|
||||
canEvolve: boolean; // whether contract is allowed to evolve. seems to default to true..
|
||||
evolve: string; // the transaction id of the Arweave transaction with the updated source code. odd naming convention..
|
||||
}
|
||||
@@ -34,21 +35,32 @@ without the need of hard-coding contract's txId in the client's source code.
|
||||
This also makes it easier to audit given contract - as you keep all its versions in one place.
|
||||
*/
|
||||
|
||||
export class Evolve<State extends EvolveCompatibleState, Api> implements ExecutionContextModifier<State> {
|
||||
function isEvolveCompatible(state: any): state is EvolveCompatibleState {
|
||||
const settings =
|
||||
state.settings && isIterable(state.settings) ? new Map<string, any>(state.settings) : new Map<string, any>();
|
||||
|
||||
return state.evolve !== undefined || settings.has('evolve');
|
||||
}
|
||||
|
||||
export class Evolve implements ExecutionContextModifier {
|
||||
constructor(
|
||||
private readonly definitionLoader: DefinitionLoader<State>,
|
||||
private readonly executorFactory: ExecutorFactory<State, Api>
|
||||
private readonly definitionLoader: DefinitionLoader,
|
||||
private readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>
|
||||
) {
|
||||
this.modify = this.modify.bind(this);
|
||||
}
|
||||
|
||||
async modify(state: State, executionContext: ExecutionContext<State, Api>): Promise<ExecutionContext<State, Api>> {
|
||||
async modify<State>(state: State, executionContext: ExecutionContext<State>): Promise<ExecutionContext<State>> {
|
||||
const contractTxId = executionContext.contractDefinition.txId;
|
||||
logger.debug(`trying to evolve for: ${contractTxId}`);
|
||||
if (!isEvolveCompatible(state)) {
|
||||
logger.verbose('State is not evolve compatible');
|
||||
return executionContext;
|
||||
}
|
||||
const currentSrcTxId = executionContext.contractDefinition.srcTxId;
|
||||
|
||||
const settings =
|
||||
state.settings && isIterable(state.settings) ? new Map<string, never>(state.settings) : new Map<string, never>();
|
||||
state.settings && isIterable(state.settings) ? new Map<string, any>(state.settings) : new Map<string, any>();
|
||||
|
||||
// note: from my understanding - this variable holds the id of the transaction with updated source code.
|
||||
const evolve: string = state.evolve || settings.get('evolve');
|
||||
@@ -68,9 +80,9 @@ export class Evolve<State extends EvolveCompatibleState, Api> implements Executi
|
||||
if (currentSrcTxId !== evolve) {
|
||||
try {
|
||||
// note: that's really nasty IMO - loading original contract definition, but forcing different sourceTxId...
|
||||
logger.info(`Evolving to: ${evolve}`);
|
||||
const newContractDefinition = await this.definitionLoader.load(contractTxId, evolve);
|
||||
const newHandler = await this.executorFactory.create(newContractDefinition);
|
||||
logger.info('Evolving to: ', evolve);
|
||||
const newContractDefinition = await this.definitionLoader.load<State>(contractTxId, evolve);
|
||||
const newHandler = await this.executorFactory.create<State>(newContractDefinition);
|
||||
|
||||
const modifiedContext = {
|
||||
...executionContext,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@smartweave/cache": ["cache/index"],
|
||||
"@smartweave/client": ["client/index"],
|
||||
"@smartweave/contract": ["contract/index"],
|
||||
"@smartweave/core": ["core/index"],
|
||||
"@smartweave/legacy": ["legacy/index"],
|
||||
"@smartweave/plugins": ["plugins/index"],
|
||||
|
||||
Reference in New Issue
Block a user