refactor: client api refactor
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { LoggerFactory, SwClientFactory } from '@smartweave';
|
import { LoggerFactory, SmartWeaveFactory } from '@smartweave';
|
||||||
import Arweave from 'arweave';
|
import Arweave from 'arweave';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -14,7 +14,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
const logger = LoggerFactory.INST.create(__filename);
|
const logger = LoggerFactory.INST.create(__filename);
|
||||||
LoggerFactory.INST.logLevel('silly', 'benchmark');
|
LoggerFactory.INST.logLevel('silly', 'benchmark');
|
||||||
const swcClient = SwClientFactory.fileCacheClient(arweave);
|
const swcClient = SmartWeaveFactory.fileCacheClient(arweave);
|
||||||
|
|
||||||
const contractTxId = 'OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU';
|
const contractTxId = 'OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU';
|
||||||
// Kyve:
|
// Kyve:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import Arweave from 'arweave';
|
import Arweave from 'arweave';
|
||||||
import { SwClientFactory, LoggerFactory } from '@smartweave';
|
import { SmartWeaveFactory, LoggerFactory } from '@smartweave';
|
||||||
|
|
||||||
const contracts = [
|
const contracts = [
|
||||||
'gepLlre8wG8K3C15rNjpdKZZv_9pWsurRoEB6ir_EC4',
|
'gepLlre8wG8K3C15rNjpdKZZv_9pWsurRoEB6ir_EC4',
|
||||||
@@ -23,15 +23,14 @@ async function main() {
|
|||||||
logging: false // Enable network request logging
|
logging: false // Enable network request logging
|
||||||
});
|
});
|
||||||
|
|
||||||
const swcClient = SwClientFactory.memCacheClient(arweave);
|
const smartWeave = SmartWeaveFactory.memCached(arweave);
|
||||||
LoggerFactory.INST.logLevel('trace');
|
const contract1 = smartWeave.contract('W_njBtwDRyltjVU1RizJtZfF0S_4X3aSrrrA0HUEhUs');
|
||||||
|
const contract2 = smartWeave.contract('TMkCZKYO3GwcTLEKGgWSJegIlYCHi_UArtG0unCi2xA');
|
||||||
|
LoggerFactory.INST.logLevel('debug');
|
||||||
|
|
||||||
const contractTxId = 'W_njBtwDRyltjVU1RizJtZfF0S_4X3aSrrrA0HUEhUs';
|
await contract1.readState();
|
||||||
const contractTxId2 = 'TMkCZKYO3GwcTLEKGgWSJegIlYCHi_UArtG0unCi2xA';
|
|
||||||
|
|
||||||
await swcClient.readState(contractTxId);
|
await contract2.readState();
|
||||||
|
|
||||||
await swcClient.readState(contractTxId2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
DebuggableExecutorFactory,
|
DebuggableExecutorFactory,
|
||||||
EvalStateResult,
|
EvalStateResult,
|
||||||
EvolveCompatibleState,
|
EvolveCompatibleState,
|
||||||
HandlerBasedSwcClient,
|
HandlerBasedContract,
|
||||||
HandlerExecutorFactory,
|
HandlerExecutorFactory,
|
||||||
LexicographicalInteractionsSorter,
|
LexicographicalInteractionsSorter,
|
||||||
LoggerFactory,
|
LoggerFactory,
|
||||||
@@ -55,7 +55,7 @@ async function readContractState() {
|
|||||||
'OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU': changedSrc
|
'OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU': changedSrc
|
||||||
});
|
});
|
||||||
|
|
||||||
const swcClient = new HandlerBasedSwcClient(
|
const swcClient = new HandlerBasedContract(
|
||||||
arweave,
|
arweave,
|
||||||
new ContractDefinitionLoader<ProvidersRegistryState>(arweave, new MemCache()),
|
new ContractDefinitionLoader<ProvidersRegistryState>(arweave, new MemCache()),
|
||||||
new ContractInteractionsLoader(arweave),
|
new ContractInteractionsLoader(arweave),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Arweave from 'arweave';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Transaction from 'arweave/node/lib/transaction';
|
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';
|
import { readContract } from 'smartweave';
|
||||||
|
|
||||||
const diffStateToVerify = [
|
const diffStateToVerify = [
|
||||||
@@ -51,7 +51,7 @@ async function main() {
|
|||||||
|
|
||||||
const errorContractTxIds = [];
|
const errorContractTxIds = [];
|
||||||
|
|
||||||
const swcClient = SwClientFactory.fileCacheClient(arweave, 'cache');
|
const swcClient = SmartWeaveFactory.fileCacheClient(arweave, 'cache');
|
||||||
|
|
||||||
const contractsBlacklist = [
|
const contractsBlacklist = [
|
||||||
'jFInOjLc_FFt802OmUObIIOlY1xNKvomzUTkoUpyP9U', // readContract very long evaluation
|
'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';
|
import { JWKInterface } from 'arweave/node/lib/wallet';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base interface to be implemented by SmartWeave Contracts clients.
|
* 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.
|
* Returns state of the contract at required blockHeight.
|
||||||
* Similar to {@link readContract} from the current version.
|
* Similar to {@link readContract} from the current version.
|
||||||
*/
|
*/
|
||||||
readState<State = any>(
|
readState(
|
||||||
contractTxId: string,
|
|
||||||
blockHeight?: number,
|
blockHeight?: number,
|
||||||
currentTx?: { interactionTxId: string; contractTxId: string }[],
|
currentTx?: { interactionTxId: string; contractTxId: string }[],
|
||||||
evaluationOptions?: EvaluationOptions
|
evaluationOptions?: EvaluationOptions
|
||||||
@@ -26,13 +20,11 @@ export interface Contract {
|
|||||||
* Returns the view of the state, computed by the SWC.
|
* Returns the view of the state, computed by the SWC.
|
||||||
* Similar to the {@link interactRead} from the current SDK version.
|
* Similar to the {@link interactRead} from the current SDK version.
|
||||||
*/
|
*/
|
||||||
viewState<Input = any, View = any>(
|
viewState<Input, View>(
|
||||||
contractTxId: string,
|
|
||||||
input: Input,
|
input: Input,
|
||||||
wallet: JWKInterface,
|
|
||||||
blockHeight?: number,
|
blockHeight?: number,
|
||||||
evaluationOptions?: EvaluationOptions
|
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.
|
* 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 -
|
* note: calling "interactRead" from withing contract's source code was not previously possible -
|
||||||
* this is a new feature.
|
* this is a new feature.
|
||||||
*/
|
*/
|
||||||
viewStateForTx<Input = any, View = any>(
|
viewStateForTx<Input, View>(
|
||||||
contractTxId: string,
|
|
||||||
input: Input,
|
input: Input,
|
||||||
transaction: InteractionTx,
|
transaction: InteractionTx,
|
||||||
evaluationOptions?: EvaluationOptions
|
evaluationOptions?: EvaluationOptions
|
||||||
): Promise<InteractionResult<any, View>>;
|
): Promise<InteractionResult<State, View>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a new "interaction" transaction - ie. such transaction that stores input for the contract.
|
* 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.
|
* its source code, info about owner, initial state, etc.
|
||||||
* See ContractDefinition type for more details regarding what data is being loaded.
|
* See ContractDefinition type for more details regarding what data is being loaded.
|
||||||
*/
|
*/
|
||||||
export interface DefinitionLoader<State = any> {
|
export interface DefinitionLoader {
|
||||||
load(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>>;
|
load<State>(contractTxId: string, forcedSrcTxId?: string): Promise<ContractDefinition<State>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EvaluationOptions, GQLEdgeInterface, Contract } from '@smartweave';
|
import { Contract, ContractDefinition, EvaluationOptions, GQLEdgeInterface, SmartWeave } from '@smartweave';
|
||||||
import { BlockData } from 'arweave/node/blocks';
|
|
||||||
import { NetworkInfoInterface } from 'arweave/node/network';
|
import { NetworkInfoInterface } from 'arweave/node/network';
|
||||||
|
import { BlockData } from 'arweave/node/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* current execution context of the contract - contains all elements
|
* 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
|
* 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).
|
* (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>;
|
contractDefinition: ContractDefinition<State>;
|
||||||
handler: Api;
|
|
||||||
blockHeight: number;
|
blockHeight: number;
|
||||||
interactions: GQLEdgeInterface[];
|
interactions: GQLEdgeInterface[];
|
||||||
sortedInteractions: GQLEdgeInterface[];
|
sortedInteractions: GQLEdgeInterface[];
|
||||||
client: Contract;
|
|
||||||
evaluationOptions: EvaluationOptions;
|
evaluationOptions: EvaluationOptions;
|
||||||
currentNetworkInfo?: NetworkInfoInterface;
|
currentNetworkInfo?: NetworkInfoInterface;
|
||||||
currentBlockData?: BlockData;
|
currentBlockData?: BlockData;
|
||||||
caller?: string; // note: this is only set for "viewState" operations
|
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...
|
* really not a fan of this feature...
|
||||||
@@ -6,6 +6,6 @@ import { ExecutionContext } from '@smartweave';
|
|||||||
* This adds ability to modify current execution context based
|
* This adds ability to modify current execution context based
|
||||||
* on state - example (and currently only) use case is the "evolve" feature...
|
* on state - example (and currently only) use case is the "evolve" feature...
|
||||||
*/
|
*/
|
||||||
export interface ExecutionContextModifier<State> {
|
export interface ExecutionContextModifier {
|
||||||
modify(state: State, executionContext: ExecutionContext<State, any>): Promise<ExecutionContext<State, any>>;
|
modify<State>(state: State, executionContext: ExecutionContext<State>): Promise<ExecutionContext<State>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ContractDefinition } from '@smartweave';
|
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" -
|
* An interface for all the factories that produce SmartWeave contracts "executors" -
|
||||||
* i.e. objects that are responsible for actually running the contract's code.
|
* i.e. objects that are responsible for actually running the contract's code.
|
||||||
*/
|
*/
|
||||||
export interface ExecutorFactory<State = any, Api = any> {
|
export interface ExecutorFactory<Api> {
|
||||||
create(contractDefinition: ContractDefinition<State>): Promise<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
|
* Implementors of this class are responsible for evaluating contract's state
|
||||||
* - based on the execution context.
|
* - based on the execution context.
|
||||||
*/
|
*/
|
||||||
export interface StateEvaluator<State = unknown, Api = unknown> {
|
export interface StateEvaluator {
|
||||||
eval(
|
eval<State>(
|
||||||
executionContext: ExecutionContext<State, Api>,
|
executionContext: ExecutionContext<State>,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
): Promise<EvalStateResult<State>>;
|
): Promise<EvalStateResult<State>>;
|
||||||
|
|
||||||
onStateUpdate(
|
onStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
currentInteraction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EvalStateResult<State = unknown> {
|
export class EvalStateResult<State> {
|
||||||
constructor(readonly state: State, readonly validity: Record<string, boolean>) {}
|
constructor(readonly state: State, readonly validity: Record<string, boolean>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line:max-classes-per-file
|
// tslint:disable-next-line:max-classes-per-file
|
||||||
export class DefaultEvaluationOptions implements EvaluationOptions {
|
export class DefaultEvaluationOptions implements EvaluationOptions {
|
||||||
// default = false - "fail-fast" approach, otherwise we can end-up with a broken state and
|
// default = true - still cannot decide whether true or false should be the default
|
||||||
// not even notice that there was an exception in state evaluation.
|
// "false" may lead to some fairly simple attacks on contract, if the contract
|
||||||
// Current SDK version simply moves to next interaction in this case and ignores exception
|
// does not properly validate input data
|
||||||
// - I believe this is not a proper behaviour.
|
// "true" may lead to wrongly calculated state, even without noticing..
|
||||||
ignoreExceptions = true;
|
ignoreExceptions = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,26 @@ import Transaction from 'arweave/web/lib/transaction';
|
|||||||
|
|
||||||
const logger = LoggerFactory.INST.create(__filename);
|
const logger = LoggerFactory.INST.create(__filename);
|
||||||
|
|
||||||
export class ContractDefinitionLoader<State = any> implements DefinitionLoader<State> {
|
export class ContractDefinitionLoader implements DefinitionLoader {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly arweave: Arweave,
|
private readonly arweave: Arweave,
|
||||||
// TODO: cache should be removed from the core layer and implemented in a wrapper of the core implementation
|
// 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)) {
|
if (!forcedSrcTxId && this.cache?.contains(contractTxId)) {
|
||||||
logger.debug('ContractDefinitionLoader: Hit from cache!');
|
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);
|
this.cache?.put(contractTxId, contract);
|
||||||
|
|
||||||
return 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 contractTx = await this.arweave.transactions.get(contractTxId);
|
||||||
const owner = await this.arweave.wallets.ownerToAddress(contractTx.owner);
|
const owner = await this.arweave.wallets.ownerToAddress(contractTx.owner);
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import {
|
|||||||
Benchmark,
|
Benchmark,
|
||||||
ContractInteraction,
|
ContractInteraction,
|
||||||
EvalStateResult,
|
EvalStateResult,
|
||||||
|
EvolveCompatibleState,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
ExecutionContextModifier,
|
ExecutionContextModifier,
|
||||||
GQLEdgeInterface,
|
GQLEdgeInterface,
|
||||||
GQLNodeInterface,
|
GQLNodeInterface,
|
||||||
GQLTagInterface,
|
GQLTagInterface,
|
||||||
HandlerApi,
|
HandlerApi,
|
||||||
HandlerResult,
|
InteractionResult,
|
||||||
LoggerFactory,
|
LoggerFactory,
|
||||||
SmartWeaveTags,
|
SmartWeaveTags,
|
||||||
StateEvaluator
|
StateEvaluator
|
||||||
@@ -18,14 +19,14 @@ import Arweave from 'arweave';
|
|||||||
const logger = LoggerFactory.INST.create(__filename);
|
const logger = LoggerFactory.INST.create(__filename);
|
||||||
|
|
||||||
// FIXME: currently this is tightly coupled with the HandlerApi
|
// 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(
|
constructor(
|
||||||
private readonly arweave: Arweave,
|
private readonly arweave: Arweave,
|
||||||
private readonly executionContextModifiers: ExecutionContextModifier<State>[] = []
|
private readonly executionContextModifiers: ExecutionContextModifier[] = []
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async eval(
|
async eval<State, Api>(
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
): Promise<EvalStateResult<State>> {
|
): Promise<EvalStateResult<State>> {
|
||||||
return this.doReadState(
|
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[],
|
missingInteractions: GQLEdgeInterface[],
|
||||||
baseState: EvalStateResult<State>,
|
baseState: EvalStateResult<State>,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
): Promise<EvalStateResult<State>> {
|
): Promise<EvalStateResult<State>> {
|
||||||
const evaluationOptions = executionContext.evaluationOptions;
|
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]`
|
`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) {
|
for (const missingInteraction of missingInteractions) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`${missingInteraction.node.id}: ${missingInteractions.indexOf(missingInteraction) + 1}/${
|
`${missingInteraction.node.id}: ${missingInteractions.indexOf(missingInteraction) + 1}/${
|
||||||
@@ -72,20 +77,14 @@ export class DefaultStateEvaluator<State = unknown> implements StateEvaluator<St
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const interaction: ContractInteraction = {
|
const interaction: ContractInteraction<unknown> = {
|
||||||
input,
|
input,
|
||||||
caller: currentInteraction.owner.address
|
caller: currentInteraction.owner.address
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await executionContext.handler.handle(
|
const result = await handler.handle(executionContext, currentState, interaction, currentInteraction, currentTx);
|
||||||
executionContext,
|
|
||||||
currentState,
|
|
||||||
interaction,
|
|
||||||
currentInteraction,
|
|
||||||
currentTx
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logResult(result, currentInteraction);
|
this.logResult<State>(result, currentInteraction);
|
||||||
|
|
||||||
if (result.type === 'exception' && evaluationOptions.ignoreExceptions !== true) {
|
if (result.type === 'exception' && evaluationOptions.ignoreExceptions !== true) {
|
||||||
throw new Error(`Exception while processing ${JSON.stringify(interaction)}:\n${result.result}`);
|
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);
|
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);
|
return new EvalStateResult<State>(currentState, validity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private logResult(
|
private logResult<State>(result: InteractionResult<State, unknown>, currentTx: GQLNodeInterface) {
|
||||||
result: HandlerResult<State> & { type: 'ok' | 'error' | 'exception' },
|
|
||||||
currentTx: GQLNodeInterface
|
|
||||||
) {
|
|
||||||
if (result.type === 'exception') {
|
if (result.type === 'exception') {
|
||||||
logger.error(`${result.result}`);
|
logger.error(`${result.result}`);
|
||||||
logger.error(`Executing of interaction: ${currentTx.id} threw exception.`);
|
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,
|
missingInteraction: GQLEdgeInterface,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>
|
executionContext: ExecutionContext<State>
|
||||||
): GQLTagInterface {
|
): GQLTagInterface {
|
||||||
const contractIndex = missingInteraction.node.tags.findIndex(
|
const contractIndex = missingInteraction.node.tags.findIndex(
|
||||||
(tag) => tag.name === SmartWeaveTags.CONTRACT_TX_ID && tag.value === executionContext.contractDefinition.txId
|
(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];
|
return missingInteraction.node.tags[contractIndex + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
onStateUpdate(
|
onStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
currentInteraction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
) {
|
) {
|
||||||
// noop
|
// noop
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
export interface HandlerApi<State> {
|
export interface HandlerApi<State> {
|
||||||
handle<Input, Result>(
|
handle<Input, Result>(
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: State,
|
state: State,
|
||||||
interaction: ContractInteraction<Input>,
|
interaction: ContractInteraction<Input>,
|
||||||
interactionTx: InteractionTx,
|
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
|
* A factory that produces handlers that are compatible with the "current" style of
|
||||||
* writing SW contracts (ie. using "handle" function).
|
* 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) {}
|
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 normalizedSource = HandlerExecutorFactory.normalizeContractSource(contractDefinition.src);
|
||||||
|
|
||||||
const swGlobal = new SmartWeaveGlobal(this.arweave, {
|
const swGlobal = new SmartWeaveGlobal(this.arweave, {
|
||||||
@@ -40,26 +40,26 @@ export class HandlerExecutorFactory<State = any> implements ExecutorFactory<Stat
|
|||||||
owner: contractDefinition.owner
|
owner: contractDefinition.owner
|
||||||
});
|
});
|
||||||
const contractFunction = new Function(normalizedSource);
|
const contractFunction = new Function(normalizedSource);
|
||||||
const handler = contractFunction(swGlobal, BigNumber, clarity) as HandlerFunction<State>;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async handle<Input>(
|
async handle<Input, Result>(
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: State,
|
state: State,
|
||||||
interaction: ContractInteraction<Input>,
|
interaction: ContractInteraction<Input>,
|
||||||
interactionTx: InteractionTx,
|
interactionTx: InteractionTx,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
): Promise<InteractionResult<State>> {
|
): Promise<InteractionResult<State, Result>> {
|
||||||
try {
|
try {
|
||||||
|
const handler = contractFunction(swGlobal, BigNumber, clarity) as HandlerFunction<State, Input, Result>;
|
||||||
const stateCopy = JSON.parse(JSON.stringify(state));
|
const stateCopy = JSON.parse(JSON.stringify(state));
|
||||||
swGlobal._activeTx = interactionTx;
|
swGlobal._activeTx = interactionTx;
|
||||||
logger.debug(`SmartWeave.contract.id: ${swGlobal.contract.id}`, swGlobal.contract.id);
|
logger.debug(`SmartWeave.contract.id: ${swGlobal.contract.id}`, swGlobal.contract.id);
|
||||||
|
|
||||||
self.assignReadContractState(swGlobal, contractDefinition, interaction, executionContext, currentTx);
|
self.assignReadContractState<Input, State>(swGlobal, contractDefinition, executionContext, currentTx);
|
||||||
self.assignViewContractState(swGlobal, contractDefinition, executionContext);
|
self.assignViewContractState<Input, State>(swGlobal, contractDefinition, executionContext);
|
||||||
|
|
||||||
const handlerResult = await handler(stateCopy, interaction);
|
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,
|
swGlobal: SmartWeaveGlobal,
|
||||||
contractDefinition: ContractDefinition<State>,
|
contractDefinition: ContractDefinition<State>,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>
|
executionContext: ExecutionContext<State>
|
||||||
) {
|
) {
|
||||||
swGlobal.contracts.viewContractState = async <View>(contractTxId: string, input: any) => {
|
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,
|
from: contractDefinition.txId,
|
||||||
to: contractTxId,
|
to: contractTxId,
|
||||||
input
|
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,
|
swGlobal: SmartWeaveGlobal,
|
||||||
contractDefinition: ContractDefinition<State>,
|
contractDefinition: ContractDefinition<State>,
|
||||||
interaction: ContractInteraction<Input>,
|
executionContext: ExecutionContext<State>,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
) {
|
) {
|
||||||
swGlobal.contracts.readContractState = async (contractTxId: string, height?: number, returnValidity?: boolean) => {
|
swGlobal.contracts.readContractState = async (contractTxId: string, height?: number, returnValidity?: boolean) => {
|
||||||
logger.debug('swGlobal.readContractState call: ', {
|
logger.debug('swGlobal.readContractState call: ', {
|
||||||
from: contractDefinition.txId,
|
from: contractDefinition.txId,
|
||||||
to: contractTxId,
|
to: contractTxId
|
||||||
interaction
|
|
||||||
});
|
});
|
||||||
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 || []),
|
...(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
|
// 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,
|
state: State,
|
||||||
interaction: ContractInteraction<Input>
|
interaction: ContractInteraction<Input>
|
||||||
) => Promise<HandlerResult<State, Result>>;
|
) => Promise<HandlerResult<State, Result>>;
|
||||||
|
|
||||||
// TODO: change to XOR between result and state?
|
// 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';
|
type: 'ok' | 'error' | 'exception';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContractInteraction<Input = any> = {
|
export type ContractInteraction<Input> = {
|
||||||
input: Input;
|
input: Input;
|
||||||
caller: string;
|
caller: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export * from './InteractionsLoader';
|
|||||||
export * from './InteractionsSorter';
|
export * from './InteractionsSorter';
|
||||||
export * from './StateEvaluator';
|
export * from './StateEvaluator';
|
||||||
export * from './SmartWeaveTags';
|
export * from './SmartWeaveTags';
|
||||||
export * from './types';
|
export * from './ExecutionContext';
|
||||||
|
export * from './ContractDefinition';
|
||||||
|
|
||||||
export * from './impl/BlockHeightInteractionsSorter';
|
export * from './impl/BlockHeightInteractionsSorter';
|
||||||
export * from './impl/ContractDefinitionLoader';
|
export * from './impl/ContractDefinitionLoader';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// - using https://www.npmjs.com/package/tsc-alias plugin
|
// - using https://www.npmjs.com/package/tsc-alias plugin
|
||||||
export * from '@smartweave/logging'; // this needs to be the first exported element.
|
export * from '@smartweave/logging'; // this needs to be the first exported element.
|
||||||
export * from '@smartweave/core';
|
export * from '@smartweave/core';
|
||||||
export * from '@smartweave/client';
|
export * from '@smartweave/contract';
|
||||||
export * from '@smartweave/cache';
|
export * from '@smartweave/cache';
|
||||||
export * from '@smartweave/plugins';
|
export * from '@smartweave/plugins';
|
||||||
export * from '@smartweave/legacy';
|
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
|
* 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(
|
constructor(
|
||||||
arweave: Arweave,
|
arweave: Arweave,
|
||||||
private readonly baseImplementation: ExecutorFactory<State, Api>,
|
private readonly baseImplementation: ExecutorFactory<Api>,
|
||||||
private readonly cache: SwCache<string, 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
|
// 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
|
// (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
|
// 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
|
// (eg. SwGlobal.contract.id field - which of course should have different value for different contracts
|
||||||
// with the same source).
|
// that share the same source).
|
||||||
const cacheKey = contractDefinition.txId;
|
const cacheKey = contractDefinition.txId;
|
||||||
if (!this.cache.contains(cacheKey)) {
|
if (!this.cache.contains(cacheKey)) {
|
||||||
logger.debug('Updating executor factory cache');
|
logger.debug('Updating executor factory cache');
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ const logger = LoggerFactory.INST.create(__filename);
|
|||||||
/**
|
/**
|
||||||
* An implementation of DefaultStateEvaluator that adds caching capabilities
|
* An implementation of DefaultStateEvaluator that adds caching capabilities
|
||||||
*/
|
*/
|
||||||
export class CacheableStateEvaluator<State> extends DefaultStateEvaluator<State> {
|
export class CacheableStateEvaluator extends DefaultStateEvaluator {
|
||||||
constructor(
|
constructor(
|
||||||
arweave: Arweave,
|
arweave: Arweave,
|
||||||
private readonly cache: BlockHeightSwCache<EvalStateResult<State>>,
|
private readonly cache: BlockHeightSwCache<EvalStateResult<unknown>>,
|
||||||
executionContextModifiers: ExecutionContextModifier<State>[] = []
|
executionContextModifiers: ExecutionContextModifier[] = []
|
||||||
) {
|
) {
|
||||||
super(arweave, executionContextModifiers);
|
super(arweave, executionContextModifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
async eval(
|
async eval<State, Api>(
|
||||||
executionContext: ExecutionContext<State, any>,
|
executionContext: ExecutionContext<State>,
|
||||||
currentTx: { interactionTxId: string; contractTxId: string }[]
|
currentTx: { interactionTxId: string; contractTxId: string }[]
|
||||||
): Promise<EvalStateResult<State>> {
|
): Promise<EvalStateResult<State>> {
|
||||||
const requestedBlockHeight = executionContext.blockHeight;
|
const requestedBlockHeight = executionContext.blockHeight;
|
||||||
@@ -42,7 +42,10 @@ export class CacheableStateEvaluator<State> extends DefaultStateEvaluator<State>
|
|||||||
// if there was anything to cache...
|
// if there was anything to cache...
|
||||||
if (sortedInteractionsUpToBlock.length > 0) {
|
if (sortedInteractionsUpToBlock.length > 0) {
|
||||||
// get latest available cache for the requested block height
|
// 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) {
|
if (cachedState != null) {
|
||||||
logger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
|
logger.debug(`Cached state for ${executionContext.contractDefinition.txId}`, {
|
||||||
@@ -101,9 +104,9 @@ export class CacheableStateEvaluator<State> extends DefaultStateEvaluator<State>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onStateUpdate(
|
onStateUpdate<State>(
|
||||||
currentInteraction: GQLNodeInterface,
|
currentInteraction: GQLNodeInterface,
|
||||||
executionContext: ExecutionContext<State, HandlerApi<State>>,
|
executionContext: ExecutionContext<State>,
|
||||||
state: EvalStateResult<State>
|
state: EvalStateResult<State>
|
||||||
) {
|
) {
|
||||||
this.cache.put(
|
this.cache.put(
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import { ContractDefinition, ExecutorFactory } from '@smartweave/core';
|
|||||||
*
|
*
|
||||||
* Not meant to be used in production env! ;-)
|
* 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(
|
constructor(
|
||||||
private readonly baseImplementation: ExecutorFactory<State, Api>,
|
private readonly baseImplementation: ExecutorFactory<Api>,
|
||||||
private readonly sourceCode: { [key: string]: string }
|
private readonly sourceCode: { [key: string]: string }
|
||||||
) {
|
) {
|
||||||
// contract source code before default "normalization"
|
// 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)) {
|
if (Object.prototype.hasOwnProperty.call(this.sourceCode, contractDefinition.txId)) {
|
||||||
contractDefinition = {
|
contractDefinition = {
|
||||||
...contractDefinition,
|
...contractDefinition,
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import {
|
|||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
ExecutionContextModifier,
|
ExecutionContextModifier,
|
||||||
ExecutorFactory,
|
ExecutorFactory,
|
||||||
|
HandlerApi,
|
||||||
LoggerFactory,
|
LoggerFactory,
|
||||||
SmartWeaveError,
|
SmartWeaveError,
|
||||||
SmartWeaveErrorType
|
SmartWeaveErrorType
|
||||||
} from '@smartweave';
|
} from '@smartweave';
|
||||||
|
|
||||||
export interface EvolveCompatibleState {
|
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..
|
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..
|
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.
|
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(
|
constructor(
|
||||||
private readonly definitionLoader: DefinitionLoader<State>,
|
private readonly definitionLoader: DefinitionLoader,
|
||||||
private readonly executorFactory: ExecutorFactory<State, Api>
|
private readonly executorFactory: ExecutorFactory<HandlerApi<unknown>>
|
||||||
) {
|
) {
|
||||||
this.modify = this.modify.bind(this);
|
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;
|
const contractTxId = executionContext.contractDefinition.txId;
|
||||||
logger.debug(`trying to evolve for: ${contractTxId}`);
|
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 currentSrcTxId = executionContext.contractDefinition.srcTxId;
|
||||||
|
|
||||||
const settings =
|
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.
|
// note: from my understanding - this variable holds the id of the transaction with updated source code.
|
||||||
const evolve: string = state.evolve || settings.get('evolve');
|
const evolve: string = state.evolve || settings.get('evolve');
|
||||||
@@ -68,9 +80,9 @@ export class Evolve<State extends EvolveCompatibleState, Api> implements Executi
|
|||||||
if (currentSrcTxId !== evolve) {
|
if (currentSrcTxId !== evolve) {
|
||||||
try {
|
try {
|
||||||
// note: that's really nasty IMO - loading original contract definition, but forcing different sourceTxId...
|
// note: that's really nasty IMO - loading original contract definition, but forcing different sourceTxId...
|
||||||
logger.info(`Evolving to: ${evolve}`);
|
logger.info('Evolving to: ', evolve);
|
||||||
const newContractDefinition = await this.definitionLoader.load(contractTxId, evolve);
|
const newContractDefinition = await this.definitionLoader.load<State>(contractTxId, evolve);
|
||||||
const newHandler = await this.executorFactory.create(newContractDefinition);
|
const newHandler = await this.executorFactory.create<State>(newContractDefinition);
|
||||||
|
|
||||||
const modifiedContext = {
|
const modifiedContext = {
|
||||||
...executionContext,
|
...executionContext,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@smartweave/cache": ["cache/index"],
|
"@smartweave/cache": ["cache/index"],
|
||||||
"@smartweave/client": ["client/index"],
|
"@smartweave/contract": ["contract/index"],
|
||||||
"@smartweave/core": ["core/index"],
|
"@smartweave/core": ["core/index"],
|
||||||
"@smartweave/legacy": ["legacy/index"],
|
"@smartweave/legacy": ["legacy/index"],
|
||||||
"@smartweave/plugins": ["plugins/index"],
|
"@smartweave/plugins": ["plugins/index"],
|
||||||
|
|||||||
Reference in New Issue
Block a user