refactor: client api refactor

This commit is contained in:
ppedziwiatr
2021-08-25 12:06:49 +02:00
parent d8fd584a0b
commit 671a97b7ca
31 changed files with 516 additions and 527 deletions

View File

@@ -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:

View File

@@ -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();

View File

@@ -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),

View File

@@ -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

View File

@@ -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
};
}
}

View File

@@ -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;
}
}

View File

@@ -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
);
}
}

View File

@@ -1,3 +0,0 @@
export * from './HandlerBasedSwcClient';
export * from './Contract';
export * from './SwClientFactory';

View File

@@ -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);
}

View 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;
}
}

View 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);
}
}

View 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
);
}
}

View 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
View File

@@ -0,0 +1,5 @@
export * from './Contract';
export * from './HandlerBasedContract';
export * from './SmartWeaveFactory';
export * from './SmartWeave';
export * from './SmartWeaveBuilder';

View 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;
};

View File

@@ -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>>;
}

View File

@@ -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;
};

View File

@@ -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>>;
}

View File

@@ -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>;
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
})
};

View File

@@ -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');

View File

@@ -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(

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"],