import { ArTransfer, ArWallet, Benchmark, Contract, ContractInteraction, createTx, DefaultEvaluationOptions, emptyTransfer, EvalStateResult, EvaluationOptions, ExecutionContext, HandlerApi, InteractionResult, InteractionTx, LoggerFactory, sleep, SmartWeave, Tags } from '@smartweave'; import { TransactionStatusResponse } from 'arweave/node/transactions'; import { NetworkInfoInterface } from 'arweave/node/network'; /** * 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 implements Contract { private readonly logger = LoggerFactory.INST.create('HandlerBasedContract'); /** * wallet connected to this contract */ protected wallet?: ArWallet; private evaluationOptions: EvaluationOptions = new DefaultEvaluationOptions(); /** * current Arweave networkInfo that will be used for all operations of the SmartWeave protocol. * Only the 'root' contract call should read this data from Arweave - all the inner calls ("child" contracts) * should reuse this data from the parent ("calling) contract. */ public networkInfo?: NetworkInfoInterface = null; constructor( readonly contractTxId: string, protected readonly smartweave: SmartWeave, // note: this will be probably used for creating contract's // call hierarchy and generating some sort of "stack trace" private readonly callingContract: Contract = null ) { this.waitForConfirmation = this.waitForConfirmation.bind(this); if (callingContract != null) { this.networkInfo = (callingContract as HandlerBasedContract).networkInfo; // sanity-check... if (this.networkInfo == null) { throw Error('Calling contract should have the network info already set!'); } } } connect(wallet: ArWallet): Contract { this.wallet = wallet; return this; } setEvaluationOptions(options: Partial): Contract { this.evaluationOptions = { ...this.evaluationOptions, ...options }; return this; } async readState( blockHeight?: number, currentTx?: { interactionTxId: string; contractTxId: string }[] ): Promise> { this.logger.info('Read state for', this.contractTxId); this.maybeClearNetworkInfo(); const { stateEvaluator } = this.smartweave; const benchmark = Benchmark.measure(); const executionContext = await this.createExecutionContext(this.contractTxId, blockHeight); this.logger.info('Execution Context', { blockHeight: executionContext.blockHeight, srcTxId: executionContext.contractDefinition.srcTxId, missingInteractions: executionContext.sortedInteractions.length }); this.logger.debug('context', benchmark.elapsed()); benchmark.reset(); const result = await stateEvaluator.eval(executionContext, currentTx || []); this.logger.debug('state', benchmark.elapsed()); return result as EvalStateResult; } async viewState( input: Input, blockHeight?: number, tags: Tags = [], transfer: ArTransfer = emptyTransfer ): Promise> { this.logger.info('View state for', this.contractTxId); this.maybeClearNetworkInfo(); if (!this.wallet) { this.logger.warn('Wallet not set.'); } const { arweave, stateEvaluator } = this.smartweave; // create execution context let executionContext = await this.createExecutionContext(this.contractTxId, blockHeight); // add block data to execution context 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 }; } // add caller info to execution context const caller = this.wallet ? await arweave.wallets.jwkToAddress(this.wallet) : ''; executionContext = { ...executionContext, caller }; // eval current state const evalStateResult = await stateEvaluator.eval(executionContext, []); this.logger.debug('Creating new interaction for view state'); // create interaction transaction const interaction: ContractInteraction = { input, caller: executionContext.caller }; this.logger.trace('interaction', interaction); // TODO: what is the best/most efficient way of creating a transaction in this case? // creating a real transaction, with multiple calls to Arweave, seems like a huge waste. // call one of the contract's view method const handleResult = await executionContext.handler.handle( executionContext, evalStateResult, interaction, { id: null, recipient: transfer.target, owner: { address: executionContext.caller }, tags: tags || [], fee: null, quantity: { winston: transfer.winstonQty }, block: executionContext.currentBlockData }, [] ); if (handleResult.type !== 'ok') { this.logger.fatal('Error while interacting with contract', { type: handleResult.type, error: handleResult.errorMessage }); } return handleResult; } async viewStateForTx(input: Input, transaction: InteractionTx): Promise> { this.logger.info(`Vies state for ${this.contractTxId}`, transaction); this.maybeClearNetworkInfo(); const { stateEvaluator } = this.smartweave; const executionContext = await this.createExecutionContextFromTx(this.contractTxId, transaction); const evalStateResult = await stateEvaluator.eval(executionContext, []); const interaction: ContractInteraction = { input, caller: executionContext.caller }; return await executionContext.handler.handle( executionContext, evalStateResult, interaction, transaction, [] ); } async writeInteraction( input: Input, tags: Tags = [], transfer: ArTransfer = emptyTransfer ): Promise { if (!this.wallet) { throw new Error("Wallet not connected. Use 'connect' method first."); } this.maybeClearNetworkInfo(); const { arweave } = this.smartweave; const interactionTx = await createTx( this.smartweave.arweave, this.wallet, this.contractTxId, input, tags, transfer.target, transfer.winstonQty ); this.logger.debug('interactionTx', interactionTx); const response = await arweave.transactions.post(interactionTx); if (response.status !== 200) { this.logger.error('Error while posting transaction', response); return null; } if (this.evaluationOptions.waitForConfirmation) { this.logger.info('Waiting for confirmation of', interactionTx.id); const benchmark = Benchmark.measure(); await this.waitForConfirmation(interactionTx.id); this.logger.info('Transaction confirmed after', benchmark.elapsed()); } return interactionTx.id; } private async waitForConfirmation(transactionId: string): Promise { const { arweave } = this.smartweave; const status = await arweave.transactions.getStatus(transactionId); if (status.confirmed === null) { this.logger.info(`Transaction ${transactionId} not yet confirmed. Waiting another 20 seconds before next check.`); await sleep(20000); await this.waitForConfirmation(transactionId); } else { this.logger.info(`Transaction ${transactionId} confirmed`, status); return status; } } private async createExecutionContext( contractTxId: string, blockHeight?: number ): Promise>> { const { arweave, definitionLoader, interactionsLoader, interactionsSorter, executorFactory } = this.smartweave; let currentNetworkInfo; const benchmark = Benchmark.measure(); // if this is a "root" call (ie. original call from SmartWeave's client) if (this.callingContract == null) { this.logger.debug('Reading network info for root call'); currentNetworkInfo = await arweave.network.getInfo(); this.networkInfo = currentNetworkInfo; } else { // if that's a call from within contract's source code this.logger.debug('Reusing network info from the calling contract'); // note: the whole execution tree should use the same network info! // this requirement was not fulfilled in the "v1" SDK - each subsequent // call to contract (from contract's source code) was loading network info independently // if the contract was evaluating for many minutes/hours, this could effectively lead to reading // state on different block heights... currentNetworkInfo = (this.callingContract as HandlerBasedContract).networkInfo; } if (blockHeight == null) { blockHeight = currentNetworkInfo.height; } this.logger.debug('network info', benchmark.elapsed()); benchmark.reset(); const [contractDefinition, interactions] = await Promise.all([ definitionLoader.load(contractTxId), // note: "eagerly" loading all of the interactions up to the current // network height (instead of the requested "blockHeight"). // as dumb as it may seem - this in fact significantly speeds up the processing // - because the InteractionsLoader (usually CacheableContractInteractionsLoader) // doesn't have to download missing interactions during the contract execution // (eg. if contract is calling different contracts on different block heights). // This basically limits the amount of interactions with Arweave GraphQL endpoint - // each such interaction takes at least ~500ms. // TODO: this could be further optimized to always load interactions only up to the "root's" call requested height interactionsLoader.load(contractTxId, 0, this.networkInfo.height) ]); this.logger.debug('contract and interactions load', benchmark.elapsed()); const sortedInteractions = await interactionsSorter.sort(interactions); const handler = (await executorFactory.create(contractDefinition)) as HandlerApi; return { contractDefinition, blockHeight, interactions, sortedInteractions, handler, smartweave: this.smartweave, contract: this, evaluationOptions: this.evaluationOptions, currentNetworkInfo }; } private async createExecutionContextFromTx( contractTxId: string, transaction: InteractionTx ): Promise>> { const benchmark = Benchmark.measure(); const { definitionLoader, interactionsLoader, interactionsSorter, executorFactory } = this.smartweave; const blockHeight = transaction.block.height; const caller = transaction.owner.address; const [contractDefinition, interactions] = await Promise.all([ definitionLoader.load(contractTxId), await interactionsLoader.load(contractTxId, 0, blockHeight) ]); const sortedInteractions = await interactionsSorter.sort(interactions); const handler = (await executorFactory.create(contractDefinition)) as HandlerApi; this.logger.debug('Creating execution context from tx:', benchmark.elapsed()); return { contractDefinition, blockHeight, interactions, sortedInteractions, handler, smartweave: this.smartweave, contract: this, evaluationOptions: this.evaluationOptions, caller }; } private maybeClearNetworkInfo() { if (this.callingContract == null) { this.logger.debug('Clearing network info for the root contract'); this.networkInfo = null; } } txId(): string { return this.contractTxId; } }