From f61e83baf3f01e2471eb91117175820e93ff5226 Mon Sep 17 00:00:00 2001 From: ppe Date: Mon, 12 Dec 2022 11:52:58 +0100 Subject: [PATCH 1/5] feat: uncommitte state - removing 'currentTx' inf. loop hack --- src/contract/Contract.ts | 8 +----- src/contract/HandlerBasedContract.ts | 25 ++++++------------- src/core/modules/StateEvaluator.ts | 6 +---- .../modules/impl/CacheableStateEvaluator.ts | 21 ++-------------- .../modules/impl/DefaultStateEvaluator.ts | 23 +++++------------ .../modules/impl/HandlerExecutorFactory.ts | 1 - .../impl/handler/AbstractContractHandler.ts | 20 +++------------ src/core/modules/impl/handler/JsHandlerApi.ts | 6 ++--- .../modules/impl/handler/WasmHandlerApi.ts | 6 ++--- 9 files changed, 27 insertions(+), 89 deletions(-) diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index b8ce7bc3..b3e1f774 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -7,7 +7,6 @@ import { ArTransfer, Tags, ArWallet } from './deploy/CreateContract'; import { CustomSignature } from './Signature'; import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator'; -export type CurrentTx = { interactionTxId: string; contractTxId: string }; export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: number; total: number }; interface BundlrResponse { @@ -96,7 +95,6 @@ export interface Contract { */ readState( sortKeyOrBlockHeight?: string | number, - currentTx?: CurrentTx[], interactions?: GQLNodeInterface[] ): Promise>>; @@ -157,11 +155,7 @@ export interface Contract { transfer?: ArTransfer ): Promise>; - dryWriteFromTx( - input: Input, - transaction: GQLNodeInterface, - currentTx?: CurrentTx[] - ): Promise>; + dryWriteFromTx(input: Input, transaction: GQLNodeInterface): Promise>; /** * Writes a new "interaction" transaction - i.e. such transaction that stores input for the contract. diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 5059236d..4648549f 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -123,12 +123,10 @@ export class HandlerBasedContract implements Contract { async readState( sortKeyOrBlockHeight?: string | number, - currentTx?: CurrentTx[], interactions?: GQLNodeInterface[] ): Promise>> { this.logger.info('Read state for', { contractTxId: this._contractTxId, - currentTx, sortKeyOrBlockHeight }); const initBenchmark = Benchmark.measure(); @@ -153,7 +151,7 @@ export class HandlerBasedContract implements Contract { initBenchmark.stop(); const stateBenchmark = Benchmark.measure(); - const result = await stateEvaluator.eval(executionContext, currentTx || []); + const result = await stateEvaluator.eval(executionContext); stateBenchmark.stop(); const total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number); @@ -207,13 +205,9 @@ export class HandlerBasedContract implements Contract { return await this.callContract(input, caller, undefined, tags, transfer); } - async dryWriteFromTx( - input: Input, - transaction: GQLNodeInterface, - currentTx?: CurrentTx[] - ): Promise> { + async dryWriteFromTx(input: Input, transaction: GQLNodeInterface): Promise> { this.logger.info(`Dry-write from transaction ${transaction.id} for ${this._contractTxId}`); - return await this.callContractForTx(input, transaction, currentTx || []); + return await this.callContractForTx(input, transaction); } async writeInteraction( @@ -627,7 +621,7 @@ export class HandlerBasedContract implements Contract { }; // eval current state - const evalStateResult = await stateEvaluator.eval(executionContext, []); + const evalStateResult = await stateEvaluator.eval(executionContext); this.logger.info('Current state', evalStateResult.cachedValue.state); // create interaction transaction @@ -665,8 +659,7 @@ export class HandlerBasedContract implements Contract { const handleResult = await this.evalInteraction( { interaction, - interactionTx: dummyTx, - currentTx: [] + interactionTx: dummyTx }, executionContext, evalStateResult.cachedValue @@ -684,13 +677,12 @@ export class HandlerBasedContract implements Contract { private async callContractForTx( input: Input, - interactionTx: GQLNodeInterface, - currentTx?: CurrentTx[] + interactionTx: GQLNodeInterface ): Promise> { this.maybeResetRootContract(); const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx); - const evalStateResult = await this.warp.stateEvaluator.eval(executionContext, currentTx); + const evalStateResult = await this.warp.stateEvaluator.eval(executionContext); this.logger.debug('callContractForTx - evalStateResult', { result: evalStateResult.cachedValue.state, @@ -704,8 +696,7 @@ export class HandlerBasedContract implements Contract { const interactionData: InteractionData = { interaction, - interactionTx, - currentTx + interactionTx }; const result = await this.evalInteraction( diff --git a/src/core/modules/StateEvaluator.ts b/src/core/modules/StateEvaluator.ts index 5e26e4b5..e8217d87 100644 --- a/src/core/modules/StateEvaluator.ts +++ b/src/core/modules/StateEvaluator.ts @@ -1,5 +1,4 @@ import { SortKeyCache, SortKeyCacheResult } from '../../cache/SortKeyCache'; -import { CurrentTx } from '../../contract/Contract'; import { ExecutionContext } from '../../core/ExecutionContext'; import { GQLNodeInterface } from '../../legacy/gqlResult'; @@ -8,10 +7,7 @@ import { GQLNodeInterface } from '../../legacy/gqlResult'; * - based on the {@link ExecutionContext}. */ export interface StateEvaluator { - eval( - executionContext: ExecutionContext, - currentTx: CurrentTx[] - ): Promise>>; + eval(executionContext: ExecutionContext): Promise>>; /** * a hook that is called on each state update (i.e. after evaluating state for each interaction transaction) diff --git a/src/core/modules/impl/CacheableStateEvaluator.ts b/src/core/modules/impl/CacheableStateEvaluator.ts index aa957b96..cd5f7c1c 100644 --- a/src/core/modules/impl/CacheableStateEvaluator.ts +++ b/src/core/modules/impl/CacheableStateEvaluator.ts @@ -1,6 +1,5 @@ import Arweave from 'arweave'; import { SortKeyCache, SortKeyCacheResult, CacheKey } from '../../../cache/SortKeyCache'; -import { CurrentTx } from '../../../contract/Contract'; import { ExecutionContext } from '../../../core/ExecutionContext'; import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier'; import { GQLNodeInterface } from '../../../legacy/gqlResult'; @@ -31,8 +30,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { } async eval( - executionContext: ExecutionContext>, - currentTx: CurrentTx[] + executionContext: ExecutionContext> ): Promise>> { const cachedState = executionContext.cachedState; if (cachedState && cachedState.sortKey == executionContext.requestedSortKey) { @@ -53,20 +51,6 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { if (!contractTxId) { throw new Error('Contract tx id not set in the execution context'); } - for (const entry of currentTx || []) { - if (entry.contractTxId === executionContext.contractDefinition.txId) { - const index = missingInteractions.findIndex((tx) => tx.id === entry.interactionTxId); - if (index !== -1) { - this.cLogger.debug('Inf. Loop fix - removing interaction', { - height: missingInteractions[index].block.height, - contractTxId: entry.contractTxId, - interactionTxId: entry.interactionTxId, - sortKey: missingInteractions[index].sortKey - }); - missingInteractions.splice(index); - } - } - } if (missingInteractions.length == 0) { this.cLogger.info(`No missing interactions ${contractTxId}`); @@ -94,8 +78,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { return await this.doReadState( missingInteractions, new EvalStateResult(baseState, baseValidity, baseErrorMessages || {}), - executionContext, - currentTx + executionContext ); } diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 575d624d..5df7b129 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -3,7 +3,6 @@ import Arweave from 'arweave'; import { ProofHoHash } from '@idena/vrf-js'; import elliptic from 'elliptic'; import { SortKeyCache, SortKeyCacheResult } from '../../../cache/SortKeyCache'; -import { CurrentTx } from '../../../contract/Contract'; import { InteractionCall } from '../../ContractCallRecord'; import { ExecutionContext } from '../../../core/ExecutionContext'; import { ExecutionContextModifier } from '../../../core/ExecutionContextModifier'; @@ -36,22 +35,19 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { ) {} async eval( - executionContext: ExecutionContext>, - currentTx: CurrentTx[] + executionContext: ExecutionContext> ): Promise>> { return this.doReadState( executionContext.sortedInteractions, new EvalStateResult(executionContext.contractDefinition.initState, {}, {}), - executionContext, - currentTx + executionContext ); } protected async doReadState( missingInteractions: GQLNodeInterface[], baseState: EvalStateResult, - executionContext: ExecutionContext>, - currentTx: CurrentTx[] + executionContext: ExecutionContext> ): Promise>> { const { ignoreExceptions, stackTrace, internalWrites, cacheEveryNInteractions } = executionContext.evaluationOptions; @@ -138,7 +134,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { const interactionCall: InteractionCall = contract .getCallStack() - .addInteractionData({ interaction: null, interactionTx: missingInteraction, currentTx }); + .addInteractionData({ interaction: null, interactionTx: missingInteraction }); // creating a Contract instance for the "writing" contract const writingContract = executionContext.warp.contract(writingContractTxId, executionContext.contract, { @@ -160,13 +156,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { */ let newState = null; try { - await writingContract.readState(missingInteraction.sortKey, [ - ...(currentTx || []), - { - contractTxId: contractDefinition.txId, //not: writingContractTxId! - interactionTxId: missingInteraction.id - } - ]); + await writingContract.readState(missingInteraction.sortKey); newState = await this.internalWriteState(contractDefinition.txId, missingInteraction.sortKey); } catch (e) { if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') { @@ -235,8 +225,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { const interactionData = { interaction, - interactionTx: missingInteraction, - currentTx + interactionTx: missingInteraction }; const interactionCall: InteractionCall = contract.getCallStack().addInteractionData(interactionData); diff --git a/src/core/modules/impl/HandlerExecutorFactory.ts b/src/core/modules/impl/HandlerExecutorFactory.ts index d47f6002..ea49138e 100644 --- a/src/core/modules/impl/HandlerExecutorFactory.ts +++ b/src/core/modules/impl/HandlerExecutorFactory.ts @@ -269,7 +269,6 @@ async function getWasmModule(wasmResponse: Response, binary: Buffer): Promise { interaction?: ContractInteraction; interactionTx: GQLNodeInterface; - currentTx: { interactionTxId: string; contractTxId: string }[]; } /** diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index 88ca1408..3ebc8efd 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -1,4 +1,3 @@ -import { CurrentTx } from '../../../../contract/Contract'; import { ContractDefinition } from '../../../../core/ContractDefinition'; import { ExecutionContext } from '../../../../core/ExecutionContext'; import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; @@ -33,7 +32,7 @@ export abstract class AbstractContractHandler implements HandlerApi, currentTx: CurrentTx[]) { + protected assignWrite(executionContext: ExecutionContext) { this.swGlobal.contracts.write = async ( contractTxId: string, input: Input, @@ -60,13 +59,7 @@ export abstract class AbstractContractHandler implements HandlerApi(input, this.swGlobal._activeTx, [ - ...(currentTx || []), - { - contractTxId: this.contractDefinition.txId, - interactionTxId: this.swGlobal.transaction.id - } - ]); + const result = await calleeContract.dryWriteFromTx(input, this.swGlobal._activeTx); this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry); const shouldAutoThrow = @@ -114,7 +107,6 @@ export abstract class AbstractContractHandler implements HandlerApi( executionContext: ExecutionContext, - currentTx: CurrentTx[], currentResult: EvalStateResult, interactionTx: GQLNodeInterface ) { @@ -134,13 +126,7 @@ export abstract class AbstractContractHandler implements HandlerApi extends AbstractContractHandler { ); try { - const { interaction, interactionTx, currentTx } = interactionData; + const { interaction, interactionTx } = interactionData; const stateCopy = deepCopy(currentResult.state); this.swGlobal._activeTx = interactionTx; this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner - this.assignReadContractState(executionContext, currentTx, currentResult, interactionTx); + this.assignReadContractState(executionContext, currentResult, interactionTx); this.assignViewContractState(executionContext); - this.assignWrite(executionContext, currentTx); + this.assignWrite(executionContext); this.assignRefreshState(executionContext); const { warp } = executionContext; diff --git a/src/core/modules/impl/handler/WasmHandlerApi.ts b/src/core/modules/impl/handler/WasmHandlerApi.ts index 6f39e0a5..ac104618 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -23,16 +23,16 @@ export class WasmHandlerApi extends AbstractContractHandler { interactionData: InteractionData ): Promise> { try { - const { interaction, interactionTx, currentTx } = interactionData; + const { interaction, interactionTx } = interactionData; this.swGlobal._activeTx = interactionTx; this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner this.swGlobal.gasLimit = executionContext.evaluationOptions.gasLimit; this.swGlobal.gasUsed = 0; - this.assignReadContractState(executionContext, currentTx, currentResult, interactionTx); + this.assignReadContractState(executionContext, currentResult, interactionTx); this.assignViewContractState(executionContext); - this.assignWrite(executionContext, currentTx); + this.assignWrite(executionContext); await this.swGlobal.kv.open(); const handlerResult = await this.doHandle(interaction); From de6c831985d05b9a5bff4ebabd3101a8e2d1c88c Mon Sep 17 00:00:00 2001 From: ppe Date: Mon, 12 Dec 2022 19:00:00 +0100 Subject: [PATCH 2/5] feat: uncommitted state - readContractState --- src/contract/Contract.ts | 6 ++++++ src/contract/HandlerBasedContract.ts | 20 ++++++++++++++++++- .../modules/impl/DefaultStateEvaluator.ts | 6 +++--- .../impl/handler/AbstractContractHandler.ts | 20 +++++++++++-------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index b3e1f774..0a753f85 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -228,4 +228,10 @@ export interface Contract { isRoot(): boolean; getStorageValues(keys: string[]): Promise>>; + + getUncommittedState(contractTxId: string): EvalStateResult; + + setUncommittedState(contractTxId: string, result: EvalStateResult): void; + + hasUncommittedState(contractTxId: string): boolean; } diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 4648549f..014bc708 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -62,10 +62,14 @@ export class HandlerBasedContract implements Contract { private signature: Signature; private warpFetchWrapper: WarpFetchWrapper; + private _children: HandlerBasedContract[] = []; + + private _uncommittedStates = new Map>(); + constructor( private readonly _contractTxId: string, protected readonly warp: Warp, - private readonly _parentContract: Contract = null, + private readonly _parentContract: Contract = null, private readonly _innerCallData: InnerCallData = null ) { this.waitForConfirmation = this.waitForConfirmation.bind(this); @@ -110,11 +114,13 @@ export class HandlerBasedContract implements Contract { callingInteraction.interactionInput.foreignContractCalls[_contractTxId] = callStack; this._callStack = callStack; this._rootSortKey = _parentContract.rootSortKey; + (_parentContract as HandlerBasedContract)._children.push(this); } else { this._callDepth = 0; this._callStack = new ContractCallRecord(_contractTxId, 0); this._rootSortKey = null; this._evaluationOptions = new DefaultEvaluationOptions(); + this._children = []; } this.getCallStack = this.getCallStack.bind(this); @@ -572,6 +578,8 @@ export class HandlerBasedContract implements Contract { this._callStack = new ContractCallRecord(this.txId(), 0); this._rootSortKey = null; this.warp.interactionsLoader.clearCache(); + this._children = []; + this._uncommittedStates = new Map(); } } @@ -838,4 +846,14 @@ export class HandlerBasedContract implements Contract { await storage.close(); } } + + getUncommittedState(contractTxId: string): EvalStateResult { + return (this.getRoot() as HandlerBasedContract)._uncommittedStates.get(contractTxId); + } + setUncommittedState(contractTxId: string, result: EvalStateResult): void { + (this.getRoot() as HandlerBasedContract)._uncommittedStates.set(contractTxId, result); + } + hasUncommittedState(contractTxId: string): boolean { + return (this.getRoot() as HandlerBasedContract)._uncommittedStates.has(contractTxId); + } } diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 5df7b129..65974806 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -142,11 +142,11 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { callType: 'read' }); - await this.onContractCall( + /*await this.onContractCall( missingInteraction, executionContext, new EvalStateResult(currentState, validity, errorMessages) - ); + );*/ this.logger.debug(`${indent(depth)}Reading state of the calling contract at`, missingInteraction.sortKey); /** @@ -157,7 +157,7 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { let newState = null; try { await writingContract.readState(missingInteraction.sortKey); - newState = await this.internalWriteState(contractDefinition.txId, missingInteraction.sortKey); + //newState = await this.internalWriteState(contractDefinition.txId, missingInteraction.sortKey); } catch (e) { if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') { this.logger.warn('Skipping unsafe contract in internal write'); diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index 3ebc8efd..a2f10ad1 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -58,7 +58,6 @@ export abstract class AbstractContractHandler implements HandlerApi(input, this.swGlobal._activeTx); this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry); @@ -118,15 +117,20 @@ export abstract class AbstractContractHandler implements HandlerApi Date: Tue, 13 Dec 2022 11:08:41 +0100 Subject: [PATCH 3/5] feat: uncommitted state for internal writes --- .github/workflows/tests.yml | 4 +- package.json | 1 + .../integration/data/staking/erc-20.js | 4 + .../data/thethar/simple-thethar-contract.js | 41 ++ .../data/thethar/thethar-contract-wrc.js | 403 +++++++++++++++++ .../data/thethar/thethar-contract.js | 411 ++++++++++++++++++ .../integration/data/wrc-20/src/action.rs | 56 +++ .../data/wrc-20/src/actions/allowances.rs | 57 +++ .../data/wrc-20/src/actions/balance.rs | 23 + .../data/wrc-20/src/actions/evolve.rs | 17 + .../data/wrc-20/src/actions/mod.rs | 5 + .../data/wrc-20/src/actions/transfers.rs | 47 ++ .../integration/data/wrc-20/src/contract.rs | 38 ++ .../data/wrc-20/src/contract_utils/README.md | 4 + .../wrc-20/src/contract_utils/entrypoint.rs | 107 +++++ .../data/wrc-20/src/contract_utils/mod.rs | 5 + .../integration/data/wrc-20/src/error.rs | 10 + .../integration/data/wrc-20/src/lib.rs | 6 + .../integration/data/wrc-20/src/state.rs | 18 + .../internal-write-back.test.ts | 4 +- .../simple-broken-thethar.test.js | 150 +++++++ src/__tests__/regression/read-state.test.ts | 28 +- src/contract/Contract.ts | 7 +- src/contract/HandlerBasedContract.ts | 147 ++++--- src/core/ExecutionContext.ts | 2 +- .../modules/impl/CacheableStateEvaluator.ts | 4 - .../modules/impl/DefaultStateEvaluator.ts | 41 +- .../impl/handler/AbstractContractHandler.ts | 29 +- yarn.lock | 7 + 29 files changed, 1570 insertions(+), 106 deletions(-) create mode 100644 src/__tests__/integration/data/thethar/simple-thethar-contract.js create mode 100644 src/__tests__/integration/data/thethar/thethar-contract-wrc.js create mode 100644 src/__tests__/integration/data/thethar/thethar-contract.js create mode 100644 src/__tests__/integration/data/wrc-20/src/action.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/actions/allowances.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/actions/balance.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/actions/evolve.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/actions/mod.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/actions/transfers.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/contract.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/contract_utils/README.md create mode 100644 src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/error.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/lib.rs create mode 100644 src/__tests__/integration/data/wrc-20/src/state.rs create mode 100644 src/__tests__/integration/internal-writes/simple-broken-thethar.test.js diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d69416c2..63f1e2ed 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,8 +4,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: '18' - name: Install modules diff --git a/package.json b/package.json index 546549eb..c2924bc9 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@idena/vrf-js": "^1.0.1", "archiver": "^5.3.0", "arweave": "1.11.8", + "async-mutex": "^0.4.0", "elliptic": "^6.5.4", "events": "3.3.0", "fast-copy": "^3.0.0", diff --git a/src/__tests__/integration/data/staking/erc-20.js b/src/__tests__/integration/data/staking/erc-20.js index 8753155e..7e53ebf0 100644 --- a/src/__tests__/integration/data/staking/erc-20.js +++ b/src/__tests__/integration/data/staking/erc-20.js @@ -73,6 +73,10 @@ export function handle(state, action) { const recipient = _input.recipient; const amount = _input.amount; + if (amount == 0 ) { + throw new ContractError('TransferFromZero'); + } + const currentAllowance = _allowances[sender][_msgSender]; if (currentAllowance === undefined || currentAllowance < amount) { diff --git a/src/__tests__/integration/data/thethar/simple-thethar-contract.js b/src/__tests__/integration/data/thethar/simple-thethar-contract.js new file mode 100644 index 00000000..acd470d0 --- /dev/null +++ b/src/__tests__/integration/data/thethar/simple-thethar-contract.js @@ -0,0 +1,41 @@ +(() => { + // src/thetAR/actions/read/userOrder.ts + var create = async (state, action) => { + const param = action.input.params; + + const tokenState = await SmartWeave.contracts.readContractState(state.token); + //let orderQuantity = param.price; + let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id]; + logger.error(" CREATE Taking tokens: " + orderQuantity); + await SmartWeave.contracts.write(state.token, { function: "transferFrom", sender: action.caller, recipient: SmartWeave.contract.id, amount: orderQuantity }); + state.orders.push(orderQuantity); + + //await SmartWeave.contracts.readContractState(state.token); + return { state }; + }; + + var cancel = async (state, action) => { + const param = action.input.params; + + let orderQuantity = state.orders[param.orderId]; + logger.error("CANCEL Returning tokens: " + orderQuantity); + await SmartWeave.contracts.write(state.token, { function: "transfer", to: action.caller, amount: orderQuantity }); + + state.orders.splice(param.orderId, 1); + return { state }; + }; + + // src/thetAR/contract.ts + async function handle(state, action) { + const func = action.input.function; + switch (func) { + case "create": + return await create(state, action); + case "cancel": + return await cancel(state, action); + default: + throw new ContractError(`No function supplied or function not recognised: "${func}"`); + } + } + })(); + \ No newline at end of file diff --git a/src/__tests__/integration/data/thethar/thethar-contract-wrc.js b/src/__tests__/integration/data/thethar/thethar-contract-wrc.js new file mode 100644 index 00000000..21ee6328 --- /dev/null +++ b/src/__tests__/integration/data/thethar/thethar-contract-wrc.js @@ -0,0 +1,403 @@ +(() => { + // src/thetAR/actions/common.ts + var isAddress = (addr) => /[a-z0-9_-]{43}/i.test(addr); + var hashCheck = async (validHashs, contractTxId) => { + const tx = await SmartWeave.unsafeClient.transactions.get(contractTxId); + let SrcTxId; + tx.get("tags").forEach((tag) => { + let key = tag.get("name", { decode: true, string: true }); + if (key === "Contract-Src") { + SrcTxId = tag.get("value", { decode: true, string: true }); + } + }); + if (!SrcTxId || !isAddress(SrcTxId)) { + throw new ContractError("Cannot find valid srcTxId in contract Tx content!"); + } + const srcTx = await SmartWeave.unsafeClient.transactions.getData(SrcTxId, { decode: true, string: true }); + if (srcTx.length < 1e4 && validHashs.includes(calcHash(srcTx))) { + return true; + } + return false; + }; + var calcHash = (string) => { + var hash = 0, i, chr; + if (string.length === 0) + return hash; + for (i = 0; i < string.length; i++) { + chr = string.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return hash; + }; + var selectWeightedTokenHolder = async (balances) => { + let totalTokens = 0; + for (const address of Object.keys(balances)) { + totalTokens += balances[address]; + } + let sum = 0; + const r = await getRandomIntNumber(totalTokens); + for (const address of Object.keys(balances)) { + sum += balances[address]; + if (r <= sum && balances[address] > 0) { + return address; + } + } + return void 0; + }; + async function getRandomIntNumber(max, uniqueValue = "") { + const pseudoRandomData = SmartWeave.arweave.utils.stringToBuffer(SmartWeave.block.height + SmartWeave.block.timestamp + SmartWeave.transaction.id + uniqueValue); + const hashBytes = await SmartWeave.arweave.crypto.hash(pseudoRandomData); + const randomBigInt = bigIntFromBytes(hashBytes); + return Number(randomBigInt % BigInt(max)); + } + function bigIntFromBytes(byteArr) { + let hexString = ""; + for (const byte of byteArr) { + hexString += byte.toString(16).padStart(2, "0"); + } + return BigInt("0x" + hexString); + } + + // src/thetAR/actions/write/addPair.ts + var addPair = async (state, action) => { + const param = action.input.params; + const tokenAddress = param.tokenAddress; + const logoTx = param.logo; + const description = param.description; + if (!isAddress(tokenAddress)) { + throw new ContractError("Token address format error!"); + } + if (!isAddress(logoTx)) { + throw new ContractError("You should enter transaction id for Arweave of your logo!"); + } + if (!validDescription(description)) { + throw new ContractError("Description you enter is not valid!"); + } + if (action.caller !== state.owner) { + const txQty = SmartWeave.transaction.quantity; + const txTarget = SmartWeave.transaction.target; + if (txTarget !== state.owner) { + throw new ContractError("AddPair fee sent to wrong target!"); + } + if (SmartWeave.arweave.ar.isLessThan(txQty, SmartWeave.arweave.ar.arToWinston("10"))) { + throw new ContractError("AddPair fee not right!"); + } + if (!await hashCheck(state.tokenSrcTemplateHashs, tokenAddress)) { + throw new ContractError("Pst contract validation check failed!"); + } + } + if (state.pairInfos.map((info) => info.tokenAddress).includes(tokenAddress)) { + throw new ContractError("Pair already exists!"); + } + const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); + state.maxPairId++; + state.pairInfos.push({ + pairId: state.maxPairId, + tokenAddress, + logo: logoTx, + description, + name: tokenState.name, + symbol: tokenState.symbol, + decimals: tokenState.decimals + }); + state.orderInfos[state.maxPairId] = { + currentPrice: void 0, + orders: [] + }; + for (const user in state.userOrders) { + if (Object.prototype.hasOwnProperty.call(state.userOrders, user)) { + let userOrder2 = state.userOrders[user]; + userOrder2[state.maxPairId] = []; + } + } + return { state }; + }; + var validDescription = (desc) => /[a-z0-9_\s\:\/-]{1,128}/i.test(desc); + + // src/thetAR/actions/write/createOrder.ts + var createOrder = async (state, action) => { + const param = action.input.params; + if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { + throw new ContractError("PairId not valid!"); + } + if (param.price !== void 0 && param.price !== null) { + if (typeof param.price !== "number") { + throw new ContractError("Price must be a number!"); + } + if (param.price <= 0 || !Number.isInteger(param.price)) { + throw new ContractError("Price must be positive integer!"); + } + } + const newOrder = { + creator: action.caller, + orderId: SmartWeave.transaction.id, + direction: param.direction, + quantity: await checkOrderQuantity(state, action), + price: param.price + }; + let selectedFeeRecvr = void 0; + try { + selectedFeeRecvr = await selectWeightedTokenHolder(await tokenBalances(state.thetarTokenAddress)); + } catch { + } + const { newOrderbook, newUserOrders, transactions, currentPrice } = await matchOrder(newOrder, state.orderInfos[param.pairId].orders, state.userOrders, param.pairId, action.caller, state.feeRatio, selectedFeeRecvr); + state.orderInfos[param.pairId].orders = newOrderbook; + state.userOrders = newUserOrders; + if (!isNaN(currentPrice) && isFinite(currentPrice)) { + state.orderInfos[param.pairId].currentPrice = currentPrice; + } + for await (const tx of transactions) { + const matchedPair = state.pairInfos.find((i) => i.pairId === param.pairId); + const targetTokenAdrress = tx.tokenType === "dominent" ? state.thetarTokenAddress : matchedPair.tokenAddress; + await SmartWeave.contracts.write(targetTokenAdrress, { function: "transfer", to: tx.to, amount: tx.quantity }); + } + return { state }; + }; + var tokenBalances = async (tokenAddress) => { + return (await SmartWeave.contracts.readContractState(tokenAddress)).balances; + }; + var checkOrderQuantity = async (state, action) => { + const param = action.input.params; + let pairInfo2 = state.pairInfos.find((pair) => pair.pairId === param.pairId); + const tokenAddress = param.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; + const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); + let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id]; + await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", from: action.caller, to: SmartWeave.contract.id, amount: orderQuantity }); + if (param.direction === "buy" && param.price) { + orderQuantity = Math.floor(orderQuantity / param.price); + } + return orderQuantity; + }; + var matchOrder = async (newOrder, orderbook, userOrders, newOrderPairId, caller, feeRatio, selectedFeeRecvr) => { + let transactions = Array(); + const targetSortDirection = newOrder.direction === "buy" ? "sell" : "buy"; + let totalTradePrice = 0; + let totalTradeVolume = 0; + const reverseOrderbook = orderbook.filter((order) => order.direction === targetSortDirection).sort((a, b) => { + if (newOrder.direction === "buy") { + return a.price > b.price ? 1 : -1; + } else { + return a.price > b.price ? -1 : 1; + } + }); + const orderType = newOrder.price ? "limit" : "market"; + if (reverseOrderbook.length === 0 && orderType === "market") { + throw new ContractError(`The first order must be limit type!`); + } + const newOrderTokenType = orderType === "market" && newOrder.direction === "buy" ? "dominent" : "trade"; + for (let i = 0; i < reverseOrderbook.length; i++) { + const order = reverseOrderbook[i]; + if (orderType === "limit" && order.price !== newOrder.price) { + continue; + } + const targetPrice = order.price; + const orderAmount = order.quantity; + const newOrderAmoumt = newOrderTokenType === "trade" ? newOrder.quantity : Math.floor(newOrder.quantity / targetPrice); + const targetAmout = orderAmount < newOrderAmoumt ? orderAmount : newOrderAmoumt; + totalTradePrice += targetPrice * targetAmout; + totalTradeVolume += targetAmout; + if (targetAmout === 0) { + break; + } + const dominentFee = Math.floor(targetAmout * targetPrice * feeRatio); + const tradeFee = Math.floor(targetAmout * feeRatio); + const dominentSwap = targetAmout * targetPrice - dominentFee; + const tradeSwap = targetAmout - tradeFee; + const buyer = newOrder.direction === "buy" ? newOrder : order; + const seller = newOrder.direction === "buy" ? order : newOrder; + transactions.push({ + tokenType: "dominent", + to: seller.creator, + quantity: dominentSwap + }); + transactions.push({ + tokenType: "trade", + to: buyer.creator, + quantity: tradeSwap + }); + if (selectedFeeRecvr) { + transactions.push({ + tokenType: "dominent", + to: selectedFeeRecvr, + quantity: dominentFee + }); + transactions.push({ + tokenType: "trade", + to: selectedFeeRecvr, + quantity: tradeFee + }); + } + order.quantity -= targetAmout; + if (order.quantity === 0) { + orderbook = orderbook.filter((v) => v.orderId !== order.orderId); + } + let userOrderInfos = userOrders[order.creator][newOrderPairId]; + let matchedOrderIdx = userOrderInfos.findIndex((value) => value.orderId === order.orderId); + userOrderInfos[matchedOrderIdx].quantity -= targetAmout; + if (userOrderInfos[matchedOrderIdx].quantity === 0) { + userOrders[order.creator][newOrderPairId] = userOrderInfos.filter((v) => v.orderId !== order.orderId); + } + newOrder.quantity -= newOrderTokenType === "trade" ? targetAmout : targetAmout * targetPrice; + } + if (orderType === "market" && newOrder.quantity !== 0) { + transactions.push({ + tokenType: newOrderTokenType, + to: newOrder.creator, + quantity: newOrder.quantity + }); + newOrder.quantity = 0; + } + if (orderType === "limit" && newOrder.quantity !== 0) { + orderbook.push({ ...newOrder }); + } + if (newOrder.quantity !== 0) { + if (userOrders[caller] === void 0) { + userOrders[caller] = {}; + } + if (userOrders[caller][newOrderPairId] === void 0) { + userOrders[caller][newOrderPairId] = []; + } + userOrders[caller][newOrderPairId].push({ ...newOrder }); + } + return { + newOrderbook: orderbook, + newUserOrders: userOrders, + transactions, + currentPrice: totalTradePrice / totalTradeVolume + }; + }; + + // src/thetAR/actions/write/deposit.ts + var deposit = async (state, action) => { + logger.error("Token: " + action.input.params.token); + logger.error("Amount: " + action.input.params.amount); + await SmartWeave.contracts.write(action.input.params.token, { + function: "transferFrom", + from: action.caller, + to: SmartWeave.contract.id, + amount: action.input.params.amount + }); + return { state }; + }; + + // src/thetAR/actions/write/cancelOrder.ts + var cancelOrder = async (state, action) => { + const param = action.input.params; + const orderId = param.orderId; + const pairId = param.pairId; + if (!isAddress(orderId)) { + throw new ContractError(`OrderId not found: ${param.orderId}!`); + } + if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { + throw new ContractError("PairId not valid!"); + } + const orderInfo2 = state.userOrders[action.caller][pairId].find((v) => v.orderId === orderId); + const pairInfo2 = state.pairInfos.find((i) => i.pairId === pairId); + if (!orderInfo2) { + throw new ContractError(`Cannot get access to pairId: ${pairId}!`); + } + if (!pairInfo2) { + throw new ContractError(`Pair info record not found: ${pairId}!`); + } + const tokenAddress = orderInfo2.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; + const quantity = orderInfo2.direction === "buy" ? orderInfo2.price * orderInfo2.quantity : orderInfo2.quantity; + await SmartWeave.contracts.write(tokenAddress, { function: "transfer", to: action.caller, amount: quantity }); + let ordersForUser = state.userOrders[action.caller][pairId]; + state.userOrders[action.caller][pairId] = ordersForUser.filter((i) => i.orderId !== orderId); + let ordersForPair = state.orderInfos[pairId].orders; + state.orderInfos[pairId].orders = ordersForPair.filter((i) => i.orderId !== orderId); + return { state }; + }; + + // src/thetAR/actions/write/addTokenHash.ts + var addTokenHash = async (state, action) => { + const param = action.input.params; + const hash = param.hash; + if (action.caller !== state.owner) { + throw new ContractError("You have no permission to modify hash list!"); + } + state.tokenSrcTemplateHashs.push(hash); + return { state }; + }; + + // src/thetAR/actions/read/pairInfo.ts + var pairInfo = async (state, action) => { + const param = action.input.params; + let pairId = param.pairId; + let result; + if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { + throw new ContractError(`Invalid pairId!`); + } + result = state.pairInfos.filter((i) => i.pairId === pairId)[0]; + return { result }; + }; + + // src/thetAR/actions/read/pairInfos.ts + var pairInfos = async (state, action) => { + let result; + result = state.pairInfos; + return { result }; + }; + + // src/thetAR/actions/read/orderInfos.ts + var orderInfos = async (state, action) => { + let result; + result = state.orderInfos; + return { result }; + }; + + // src/thetAR/actions/read/orderInfo.ts + var orderInfo = async (state, action) => { + const param = action.input.params; + let pairId = param.pairId; + let result; + if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { + throw new ContractError(`Invalid pairId!`); + } + result = state.orderInfos[pairId]; + return { result }; + }; + + // src/thetAR/actions/read/userOrder.ts + var userOrder = async (state, action) => { + const param = action.input.params; + let address = param.address; + let result; + if (!isAddress(address)) { + throw new ContractError(`Invalid wallet address!`); + } + result = state.userOrders[address]; + return { result }; + }; + + // src/thetAR/contract.ts + async function handle(state, action) { + const func = action.input.function; + switch (func) { + case "addPair": + return await addPair(state, action); + case "createOrder": + return await createOrder(state, action); + case "cancelOrder": + return await cancelOrder(state, action); + case "pairInfo": + return await pairInfo(state, action); + case "pairInfos": + return await pairInfos(state, action); + case "orderInfo": + return await orderInfo(state, action); + case "orderInfos": + return await orderInfos(state, action); + case "addTokenHash": + return await addTokenHash(state, action); + case "userOrder": + return await userOrder(state, action); + case "deposit": + return await deposit(state, action); + default: + throw new ContractError(`No function supplied or function not recognised: "${func}"`); + } + } + })(); + \ No newline at end of file diff --git a/src/__tests__/integration/data/thethar/thethar-contract.js b/src/__tests__/integration/data/thethar/thethar-contract.js new file mode 100644 index 00000000..a87c45e2 --- /dev/null +++ b/src/__tests__/integration/data/thethar/thethar-contract.js @@ -0,0 +1,411 @@ +(() => { + // src/thetAR/actions/common.ts + var isAddress = (addr) => /[a-z0-9_-]{43}/i.test(addr); + var hashCheck = async (validHashs, contractTxId) => { + const tx = await SmartWeave.unsafeClient.transactions.get(contractTxId); + let SrcTxId; + tx.get("tags").forEach((tag) => { + let key = tag.get("name", { decode: true, string: true }); + if (key === "Contract-Src") { + SrcTxId = tag.get("value", { decode: true, string: true }); + } + }); + if (!SrcTxId || !isAddress(SrcTxId)) { + throw new ContractError("Cannot find valid srcTxId in contract Tx content!"); + } + const srcTx = await SmartWeave.unsafeClient.transactions.getData(SrcTxId, { decode: true, string: true }); + if (srcTx.length < 1e4 && validHashs.includes(calcHash(srcTx))) { + return true; + } + return false; + }; + var calcHash = (string) => { + var hash = 0, i, chr; + if (string.length === 0) + return hash; + for (i = 0; i < string.length; i++) { + chr = string.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return hash; + }; + var selectWeightedTokenHolder = async (balances) => { + let totalTokens = 0; + for (const address of Object.keys(balances)) { + totalTokens += balances[address]; + } + let sum = 0; + const r = await getRandomIntNumber(totalTokens); + for (const address of Object.keys(balances)) { + sum += balances[address]; + if (r <= sum && balances[address] > 0) { + return address; + } + } + return void 0; + }; + async function getRandomIntNumber(max, uniqueValue = "") { + const pseudoRandomData = SmartWeave.arweave.utils.stringToBuffer(SmartWeave.block.height + SmartWeave.block.timestamp + SmartWeave.transaction.id + uniqueValue); + const hashBytes = await SmartWeave.arweave.crypto.hash(pseudoRandomData); + const randomBigInt = bigIntFromBytes(hashBytes); + return Number(randomBigInt % BigInt(max)); + } + function bigIntFromBytes(byteArr) { + let hexString = ""; + for (const byte of byteArr) { + hexString += byte.toString(16).padStart(2, "0"); + } + return BigInt("0x" + hexString); + } + + // src/thetAR/actions/write/addPair.ts + var addPair = async (state, action) => { + const param = action.input.params; + const tokenAddress = param.tokenAddress; + const logoTx = param.logo; + const description = param.description; + if (!isAddress(tokenAddress)) { + throw new ContractError("Token address format error!"); + } + if (!isAddress(logoTx)) { + throw new ContractError("You should enter transaction id for Arweave of your logo!"); + } + if (!validDescription(description)) { + throw new ContractError("Description you enter is not valid!"); + } + if (action.caller !== state.owner) { + const txQty = SmartWeave.transaction.quantity; + const txTarget = SmartWeave.transaction.target; + if (txTarget !== state.owner) { + throw new ContractError("AddPair fee sent to wrong target!"); + } + if (SmartWeave.arweave.ar.isLessThan(txQty, SmartWeave.arweave.ar.arToWinston("10"))) { + throw new ContractError("AddPair fee not right!"); + } + if (!await hashCheck(state.tokenSrcTemplateHashs, tokenAddress)) { + throw new ContractError("Pst contract validation check failed!"); + } + } + if (state.pairInfos.map((info) => info.tokenAddress).includes(tokenAddress)) { + throw new ContractError("Pair already exists!"); + } + const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); + state.maxPairId++; + state.pairInfos.push({ + pairId: state.maxPairId, + tokenAddress, + logo: logoTx, + description, + name: tokenState.name, + symbol: tokenState.symbol, + decimals: tokenState.decimals + }); + state.orderInfos[state.maxPairId] = { + currentPrice: void 0, + orders: [] + }; + for (const user in state.userOrders) { + if (Object.prototype.hasOwnProperty.call(state.userOrders, user)) { + let userOrder2 = state.userOrders[user]; + userOrder2[state.maxPairId] = []; + } + } + return { state }; + }; + var validDescription = (desc) => /[a-z0-9_\s\:\/-]{1,128}/i.test(desc); + + // src/thetAR/actions/write/createOrder.ts + var createOrder = async (state, action) => { + const param = action.input.params; + if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { + throw new ContractError("PairId not valid!"); + } + if (param.price !== void 0 && param.price !== null) { + if (typeof param.price !== "number") { + throw new ContractError("Price must be a number!"); + } + if (param.price <= 0 || !Number.isInteger(param.price)) { + throw new ContractError("Price must be positive integer!"); + } + } + + let {orderQuantity, updatedState} = await checkOrderQuantity(state, action); + const newOrder = { + creator: action.caller, + orderId: SmartWeave.transaction.id, + direction: param.direction, + quantity: orderQuantity, + price: param.price + }; + let selectedFeeRecvr = void 0; + try { + selectedFeeRecvr = await selectWeightedTokenHolder(tokenBalances(updatedState)); + } catch { + } + const { newOrderbook, newUserOrders, transactions, currentPrice } = await matchOrder(newOrder, state.orderInfos[param.pairId].orders, state.userOrders, param.pairId, action.caller, state.feeRatio, selectedFeeRecvr); + state.orderInfos[param.pairId].orders = newOrderbook; + state.userOrders = newUserOrders; + if (!isNaN(currentPrice) && isFinite(currentPrice)) { + state.orderInfos[param.pairId].currentPrice = currentPrice; + } + for await (const tx of transactions) { + const matchedPair = state.pairInfos.find((i) => i.pairId === param.pairId); + const targetTokenAdrress = tx.tokenType === "dominent" ? state.thetarTokenAddress : matchedPair.tokenAddress; + await SmartWeave.contracts.write(targetTokenAdrress, { function: "transfer", to: tx.to, amount: tx.quantity }); + } + return { state }; + }; + var tokenBalances = (updatedState) => { + return updatedState.balances; + }; + var checkOrderQuantity = async (state, action) => { + const param = action.input.params; + let pairInfo2 = state.pairInfos.find((pair) => pair.pairId === param.pairId); + const tokenAddress = param.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; + const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); + //let orderQuantity = param.price; + let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id]; + //WASM version + //await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", from: action.caller, to: SmartWeave.contract.id, amount: orderQuantity }); + //JS version + logger.error("CREATE Taking tokens: " + orderQuantity); + updatedState = (await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", sender: action.caller, recipient: SmartWeave.contract.id, amount: orderQuantity })).state; + logger.error("STATE:", updatedState); + if (param.direction === "buy" && param.price) { + orderQuantity = Math.floor(orderQuantity / param.price); + } + return {orderQuantity, updatedState}; + }; + var matchOrder = async (newOrder, orderbook, userOrders, newOrderPairId, caller, feeRatio, selectedFeeRecvr) => { + let transactions = Array(); + const targetSortDirection = newOrder.direction === "buy" ? "sell" : "buy"; + let totalTradePrice = 0; + let totalTradeVolume = 0; + const reverseOrderbook = orderbook.filter((order) => order.direction === targetSortDirection).sort((a, b) => { + if (newOrder.direction === "buy") { + return a.price > b.price ? 1 : -1; + } else { + return a.price > b.price ? -1 : 1; + } + }); + const orderType = newOrder.price ? "limit" : "market"; + if (reverseOrderbook.length === 0 && orderType === "market") { + throw new ContractError(`The first order must be limit type!`); + } + const newOrderTokenType = orderType === "market" && newOrder.direction === "buy" ? "dominent" : "trade"; + for (let i = 0; i < reverseOrderbook.length; i++) { + const order = reverseOrderbook[i]; + if (orderType === "limit" && order.price !== newOrder.price) { + continue; + } + const targetPrice = order.price; + const orderAmount = order.quantity; + const newOrderAmoumt = newOrderTokenType === "trade" ? newOrder.quantity : Math.floor(newOrder.quantity / targetPrice); + const targetAmout = orderAmount < newOrderAmoumt ? orderAmount : newOrderAmoumt; + totalTradePrice += targetPrice * targetAmout; + totalTradeVolume += targetAmout; + if (targetAmout === 0) { + break; + } + const dominentFee = Math.floor(targetAmout * targetPrice * feeRatio); + const tradeFee = Math.floor(targetAmout * feeRatio); + const dominentSwap = targetAmout * targetPrice - dominentFee; + const tradeSwap = targetAmout - tradeFee; + const buyer = newOrder.direction === "buy" ? newOrder : order; + const seller = newOrder.direction === "buy" ? order : newOrder; + transactions.push({ + tokenType: "dominent", + to: seller.creator, + quantity: dominentSwap + }); + transactions.push({ + tokenType: "trade", + to: buyer.creator, + quantity: tradeSwap + }); + if (selectedFeeRecvr) { + transactions.push({ + tokenType: "dominent", + to: selectedFeeRecvr, + quantity: dominentFee + }); + transactions.push({ + tokenType: "trade", + to: selectedFeeRecvr, + quantity: tradeFee + }); + } + order.quantity -= targetAmout; + if (order.quantity === 0) { + orderbook = orderbook.filter((v) => v.orderId !== order.orderId); + } + let userOrderInfos = userOrders[order.creator][newOrderPairId]; + let matchedOrderIdx = userOrderInfos.findIndex((value) => value.orderId === order.orderId); + userOrderInfos[matchedOrderIdx].quantity -= targetAmout; + if (userOrderInfos[matchedOrderIdx].quantity === 0) { + userOrders[order.creator][newOrderPairId] = userOrderInfos.filter((v) => v.orderId !== order.orderId); + } + newOrder.quantity -= newOrderTokenType === "trade" ? targetAmout : targetAmout * targetPrice; + } + if (orderType === "market" && newOrder.quantity !== 0) { + transactions.push({ + tokenType: newOrderTokenType, + to: newOrder.creator, + quantity: newOrder.quantity + }); + newOrder.quantity = 0; + } + if (orderType === "limit" && newOrder.quantity !== 0) { + orderbook.push({ ...newOrder }); + } + if (newOrder.quantity !== 0) { + if (userOrders[caller] === void 0) { + userOrders[caller] = {}; + } + if (userOrders[caller][newOrderPairId] === void 0) { + userOrders[caller][newOrderPairId] = []; + } + userOrders[caller][newOrderPairId].push({ ...newOrder }); + } + return { + newOrderbook: orderbook, + newUserOrders: userOrders, + transactions, + currentPrice: totalTradePrice / totalTradeVolume + }; + }; + + // src/thetAR/actions/write/deposit.ts + var deposit = async (state, action) => { + logger.error("Token: " + action.input.params.token); + logger.error("Amount: " + action.input.params.amount); + await SmartWeave.contracts.write(action.input.params.token, { + function: "transferFrom", + from: action.caller, + to: SmartWeave.contract.id, + amount: action.input.params.amount + }); + return { state }; + }; + + // src/thetAR/actions/write/cancelOrder.ts + var cancelOrder = async (state, action) => { + const param = action.input.params; + const orderId = param.orderId; + const pairId = param.pairId; + if (!isAddress(orderId)) { + throw new ContractError(`OrderId not found: ${param.orderId}!`); + } + if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { + throw new ContractError("PairId not valid!"); + } + const orderInfo2 = state.userOrders[action.caller][pairId].find((v) => v.orderId === orderId); + const pairInfo2 = state.pairInfos.find((i) => i.pairId === pairId); + if (!orderInfo2) { + throw new ContractError(`Cannot get access to pairId: ${pairId}!`); + } + if (!pairInfo2) { + throw new ContractError(`Pair info record not found: ${pairId}!`); + } + const tokenAddress = orderInfo2.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; + const quantity = orderInfo2.direction === "buy" ? orderInfo2.price * orderInfo2.quantity : orderInfo2.quantity; + logger.error("CANCEL Returning tokens: " + quantity); + await SmartWeave.contracts.write(tokenAddress, { function: "transfer", to: action.caller, amount: quantity }); + let ordersForUser = state.userOrders[action.caller][pairId]; + state.userOrders[action.caller][pairId] = ordersForUser.filter((i) => i.orderId !== orderId); + let ordersForPair = state.orderInfos[pairId].orders; + state.orderInfos[pairId].orders = ordersForPair.filter((i) => i.orderId !== orderId); + return { state }; + }; + + // src/thetAR/actions/write/addTokenHash.ts + var addTokenHash = async (state, action) => { + const param = action.input.params; + const hash = param.hash; + if (action.caller !== state.owner) { + throw new ContractError("You have no permission to modify hash list!"); + } + state.tokenSrcTemplateHashs.push(hash); + return { state }; + }; + + // src/thetAR/actions/read/pairInfo.ts + var pairInfo = async (state, action) => { + const param = action.input.params; + let pairId = param.pairId; + let result; + if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { + throw new ContractError(`Invalid pairId!`); + } + result = state.pairInfos.filter((i) => i.pairId === pairId)[0]; + return { result }; + }; + + // src/thetAR/actions/read/pairInfos.ts + var pairInfos = async (state, action) => { + let result; + result = state.pairInfos; + return { result }; + }; + + // src/thetAR/actions/read/orderInfos.ts + var orderInfos = async (state, action) => { + let result; + result = state.orderInfos; + return { result }; + }; + + // src/thetAR/actions/read/orderInfo.ts + var orderInfo = async (state, action) => { + const param = action.input.params; + let pairId = param.pairId; + let result; + if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { + throw new ContractError(`Invalid pairId!`); + } + result = state.orderInfos[pairId]; + return { result }; + }; + + // src/thetAR/actions/read/userOrder.ts + var userOrder = async (state, action) => { + const param = action.input.params; + let address = param.address; + let result; + if (!isAddress(address)) { + throw new ContractError(`Invalid wallet address!`); + } + result = state.userOrders[address]; + return { result }; + }; + + // src/thetAR/contract.ts + async function handle(state, action) { + const func = action.input.function; + switch (func) { + case "addPair": + return await addPair(state, action); + case "createOrder": + return await createOrder(state, action); + case "cancelOrder": + return await cancelOrder(state, action); + case "pairInfo": + return await pairInfo(state, action); + case "pairInfos": + return await pairInfos(state, action); + case "orderInfo": + return await orderInfo(state, action); + case "orderInfos": + return await orderInfos(state, action); + case "addTokenHash": + return await addTokenHash(state, action); + case "userOrder": + return await userOrder(state, action); + case "deposit": + return await deposit(state, action); + default: + throw new ContractError(`No function supplied or function not recognised: "${func}"`); + } + } +})(); diff --git a/src/__tests__/integration/data/wrc-20/src/action.rs b/src/__tests__/integration/data/wrc-20/src/action.rs new file mode 100644 index 00000000..64b06ef2 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/action.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; + +use warp_wasm_utils::contract_utils::handler_result::HandlerResult; +use crate::error::ContractError; +use crate::state::State; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", tag = "function")] +pub enum Action { + Transfer { + to: String, + amount: u64, + }, + TransferFrom { + from: String, + to: String, + amount: u64 + }, + BalanceOf { + target: String + }, + TotalSupply { + }, + Approve { + spender: String, + amount: u64, + }, + Allowance { + owner: String, + spender: String + }, + Evolve { + value: String + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum QueryResponseMsg { + Balance { + ticker: String, + target: String, + balance: u64, + }, + Allowance { + ticker: String, + owner: String, + spender: String, + allowance: u64, + }, + TotalSupply { + value: u64 + } +} + +pub type ActionResult = Result, ContractError>; diff --git a/src/__tests__/integration/data/wrc-20/src/actions/allowances.rs b/src/__tests__/integration/data/wrc-20/src/actions/allowances.rs new file mode 100644 index 00000000..e052350f --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/actions/allowances.rs @@ -0,0 +1,57 @@ +use crate::state::State; +use std::collections::HashMap; +use warp_wasm_utils::contract_utils::handler_result::HandlerResult; +use crate::action::{QueryResponseMsg::Allowance, ActionResult}; +use warp_wasm_utils::contract_utils::handler_result::HandlerResult::QueryResponse; +use warp_wasm_utils::contract_utils::js_imports::{Transaction}; + +pub fn allowance(state: State, owner: String, spender: String) -> ActionResult { + Ok(QueryResponse( + Allowance { + ticker: state.symbol, + allowance: __get_allowance(&state.allowances, &owner, &spender), + owner, + spender + } + )) +} + +pub fn approve(mut state: State, spender: String, amount: u64) -> ActionResult { + let caller = Transaction::owner(); + __set_allowance(&mut state.allowances, caller, spender, amount); + Ok(HandlerResult::NewState(state)) +} + +//Following: https://users.rust-lang.org/t/use-of-pub-for-non-public-apis/40480 +// Not a part of the contract API - used internally within the crate. +#[doc(hidden)] +pub fn __set_allowance(allowances: &mut HashMap>, owner: String, spender: String, amount: u64) { + if amount > 0 { + *allowances + .entry(owner) + .or_default() + .entry(spender) + .or_default() = amount; + } else { //Prune state + match allowances.get_mut(&owner) { + Some(spender_allowances) => { + spender_allowances.remove(&spender); + if spender_allowances.is_empty() { + allowances.remove(&owner); + } + } + None => () + } + } +} + +//Following: https://users.rust-lang.org/t/use-of-pub-for-non-public-apis/40480 +// Not a part of the contract API - used internally within the crate. +#[doc(hidden)] +pub fn __get_allowance(allowances: &HashMap>, owner: &String, spender: &String) -> u64 { + return *allowances + .get(owner) + .map_or(&0, |spenders| { + spenders.get(spender).unwrap_or(&0) + }); +} diff --git a/src/__tests__/integration/data/wrc-20/src/actions/balance.rs b/src/__tests__/integration/data/wrc-20/src/actions/balance.rs new file mode 100644 index 00000000..bb0d72f0 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/actions/balance.rs @@ -0,0 +1,23 @@ +use crate::state::State; +use crate::action::{QueryResponseMsg::Balance,QueryResponseMsg::TotalSupply, ActionResult}; +use warp_wasm_utils::contract_utils::handler_result::HandlerResult::QueryResponse; + +pub fn balance_of(state: State, target: String) -> ActionResult { + Ok(QueryResponse( + Balance { + balance: *state.balances.get( & target).unwrap_or(&0), + ticker: state.symbol, + target + } + )) +} + +pub fn total_supply(state: State) -> ActionResult { + Ok(QueryResponse( + TotalSupply { + value: state.total_supply + } + )) +} + + diff --git a/src/__tests__/integration/data/wrc-20/src/actions/evolve.rs b/src/__tests__/integration/data/wrc-20/src/actions/evolve.rs new file mode 100644 index 00000000..0492dcd7 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/actions/evolve.rs @@ -0,0 +1,17 @@ +use crate::error::ContractError::{EvolveNotAllowed, OnlyOwnerCanEvolve}; +use crate::state::{State}; +use warp_wasm_utils::contract_utils::js_imports::Transaction; +use crate::action::ActionResult; +use warp_wasm_utils::contract_utils::handler_result::HandlerResult; + +pub fn evolve(mut state: State, value: String) -> ActionResult { + match state.can_evolve { + Some(can_evolve) => if can_evolve && state.owner == Transaction::owner() { + state.evolve = Option::from(value); + Ok(HandlerResult::NewState(state)) + } else { + Err(OnlyOwnerCanEvolve) + }, + None => Err(EvolveNotAllowed), + } +} diff --git a/src/__tests__/integration/data/wrc-20/src/actions/mod.rs b/src/__tests__/integration/data/wrc-20/src/actions/mod.rs new file mode 100644 index 00000000..6d5022da --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/actions/mod.rs @@ -0,0 +1,5 @@ +pub mod evolve; +pub mod balance; +pub mod transfers; +pub mod allowances; + diff --git a/src/__tests__/integration/data/wrc-20/src/actions/transfers.rs b/src/__tests__/integration/data/wrc-20/src/actions/transfers.rs new file mode 100644 index 00000000..e84e6c63 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/actions/transfers.rs @@ -0,0 +1,47 @@ +use crate::error::ContractError::{CallerBalanceNotEnough, CallerAllowanceNotEnough}; +use crate::actions::allowances::{__set_allowance, __get_allowance}; +use crate::state::State; +use crate::action::ActionResult; +use warp_wasm_utils::contract_utils::handler_result::HandlerResult; +use warp_wasm_utils::contract_utils::js_imports::{SmartWeave}; + +pub fn transfer(state: State, to: String, amount: u64) -> ActionResult { + let caller = SmartWeave::caller(); + return _transfer(state, caller, to, amount); +} + +pub fn transfer_from(mut state: State, from: String, to: String, amount: u64) -> ActionResult { + let caller = SmartWeave::caller(); + + //Checking allowance + let allowance = __get_allowance(&state.allowances, &from, &caller); + + if allowance < amount { + return Err(CallerAllowanceNotEnough(allowance)); + } + + __set_allowance(&mut state.allowances, from.to_owned(), caller, allowance - amount); + + return _transfer(state, from, to, amount); +} + +fn _transfer(mut state: State, from: String, to: String, amount: u64) -> ActionResult { + // Checking if caller has enough funds + let balances = &mut state.balances; + let from_balance = *balances.get(&from).unwrap_or(&0); + if from_balance < amount { + return Err(CallerBalanceNotEnough(from_balance)); + } + + // Update caller balance or prune state if the new value is 0 + if from_balance - amount == 0 { + balances.remove(&from); + } else { + balances.insert(from, from_balance - amount); + } + + // Update target balance + *balances.entry(to).or_insert(0) += amount; + + Ok(HandlerResult::NewState(state)) +} diff --git a/src/__tests__/integration/data/wrc-20/src/contract.rs b/src/__tests__/integration/data/wrc-20/src/contract.rs new file mode 100644 index 00000000..f623d80c --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/contract.rs @@ -0,0 +1,38 @@ +use crate::action::{Action, ActionResult}; +use crate::actions::transfers::transfer; +use crate::actions::transfers::transfer_from; +use crate::actions::balance::balance_of; +use crate::actions::balance::total_supply; +use crate::actions::allowances::approve; +use crate::actions::allowances::allowance; +use crate::actions::evolve::evolve; +use warp_wasm_utils::contract_utils::js_imports::{Block, Contract, log, SmartWeave, Transaction}; +use crate::state::State; + +pub async fn handle(current_state: State, action: Action) -> ActionResult { + + //Example of accessing functions imported from js: + log("log from contract"); + log(&("Transaction::id()".to_owned() + &Transaction::id())); + log(&("Transaction::owner()".to_owned() + &Transaction::owner())); + log(&("Transaction::target()".to_owned() + &Transaction::target())); + + log(&("Block::height()".to_owned() + &Block::height().to_string())); + log(&("Block::indep_hash()".to_owned() + &Block::indep_hash())); + log(&("Block::timestamp()".to_owned() + &Block::timestamp().to_string())); + + log(&("Contract::id()".to_owned() + &Contract::id())); + log(&("Contract::owner()".to_owned() + &Contract::owner())); + + log(&("SmartWeave::caller()".to_owned() + &SmartWeave::caller())); + + match action { + Action::Transfer { to, amount } => transfer(current_state, to, amount), + Action::TransferFrom { from, to, amount } => transfer_from(current_state, from, to, amount), + Action::BalanceOf { target } => balance_of(current_state, target), + Action::TotalSupply { } => total_supply(current_state), + Action::Approve { spender, amount } => approve(current_state, spender, amount), + Action::Allowance { owner, spender } => allowance(current_state, owner, spender), + Action::Evolve { value } => evolve(current_state, value), + } +} diff --git a/src/__tests__/integration/data/wrc-20/src/contract_utils/README.md b/src/__tests__/integration/data/wrc-20/src/contract_utils/README.md new file mode 100644 index 00000000..dadde3be --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/contract_utils/README.md @@ -0,0 +1,4 @@ +# contract_utils module + +This is a module with boilerplate code for each SmartWeave RUST contract. +**Please don't modify it unless you 100% know what you are doing!** diff --git a/src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs b/src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs new file mode 100644 index 00000000..d6b04629 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs @@ -0,0 +1,107 @@ +///////////////////////////////////////////////////// +/////////////// DO NOT MODIFY THIS FILE ///////////// +///////////////////////////////////////////////////// + +use std::cell::RefCell; + +use serde_json::Error; +use wasm_bindgen::prelude::*; + +use crate::action::{Action, QueryResponseMsg}; +use crate::contract; +use warp_wasm_utils::contract_utils::handler_result::HandlerResult; +use crate::error::ContractError; +use crate::state::State; + +/* +Note: in order do optimize communication between host and the WASM module, +we're storing the state inside the WASM module (for the time of state evaluation). +This allows to reduce the overhead of passing the state back and forth +between the host and module with each contract interaction. +In case of bigger states this overhead can be huge. +Same approach has been implemented for the AssemblyScript version. + +So the flow (from the SDK perspective) is: +1. SDK calls exported WASM module function "initState" (with lastly cached state or initial state, +if cache is empty) - which initializes the state in the WASM module. +2. SDK calls "handle" function for each of the interaction. +If given interaction was modifying the state - it is updated inside the WASM module +- but not returned to host. +3. Whenever SDK needs to know the current state (eg. in order to perform +caching or to simply get its value after evaluating all of the interactions) +- it calls WASM's module "currentState" function. + +The handle function by default does not return the new state - +it only updates it in the WASM module. +The handle function returns a value only in case of error +or calling a "view" function. + +In the future this might also allow to enhance the inner-contracts communication +- e.g. if the execution network will store the state of the contracts - as the WASM contract module memory +- it would allow to read other contract's state "directly" from WASM module memory. +*/ + +// inspired by https://github.com/dfinity/examples/blob/master/rust/basic_dao/src/basic_dao/src/lib.rs#L13 +thread_local! { + static STATE: RefCell = RefCell::default(); +} + +#[wasm_bindgen()] +pub async fn handle(interaction: JsValue) -> Option { + let result: Result, ContractError>; + let action: Result = interaction.into_serde(); + + if action.is_err() { + // cannot pass any data from action.error here - ends up with + // "FnOnce called more than once" error from wasm-bindgen for + // "foreign_call" testcase. + result = Err(ContractError::RuntimeError( + "Error while parsing input".to_string(), + )); + } else { + // not sure about clone here + let current_state = STATE.with(|service| service.borrow().clone()); + + result = contract::handle(current_state, action.unwrap()).await; + } + + if let Ok(HandlerResult::NewState(state)) = result { + STATE.with(|service| service.replace(state)); + None + } else { + Some(JsValue::from_serde(&result).unwrap()) + } +} + +#[wasm_bindgen(js_name = initState)] +pub fn init_state(state: &JsValue) { + let state_parsed: State = state.into_serde().unwrap(); + + STATE.with(|service| service.replace(state_parsed)); +} + +#[wasm_bindgen(js_name = currentState)] +pub fn current_state() -> JsValue { + // not sure if that's deterministic - which is very important for the execution network. + // TODO: perf - according to docs: + // "This is unlikely to be super speedy so it's not recommended for large payload" + // - we should minimize calls to from_serde + let current_state = STATE.with(|service| service.borrow().clone()); + JsValue::from_serde(¤t_state).unwrap() +} + +#[wasm_bindgen()] +pub fn version() -> i32 { + return 1; +} + +// Workaround for now to simplify type reading without as/loader or wasm-bindgen +// 1 = assemblyscript +// 2 = rust +// 3 = go +// 4 = swift +// 5 = c +#[wasm_bindgen] +pub fn lang() -> i32 { + return 2; +} diff --git a/src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs b/src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs new file mode 100644 index 00000000..a6ac3295 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs @@ -0,0 +1,5 @@ +///////////////////////////////////////////////////// +/////////////// DO NOT MODIFY THIS FILE ///////////// +///////////////////////////////////////////////////// + +pub mod entrypoint; diff --git a/src/__tests__/integration/data/wrc-20/src/error.rs b/src/__tests__/integration/data/wrc-20/src/error.rs new file mode 100644 index 00000000..ce3ea9cf --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/error.rs @@ -0,0 +1,10 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub enum ContractError { + RuntimeError(String), + CallerBalanceNotEnough(u64), + CallerAllowanceNotEnough(u64), + OnlyOwnerCanEvolve, + EvolveNotAllowed +} diff --git a/src/__tests__/integration/data/wrc-20/src/lib.rs b/src/__tests__/integration/data/wrc-20/src/lib.rs new file mode 100644 index 00000000..a6c5bfb5 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/lib.rs @@ -0,0 +1,6 @@ +mod state; +mod action; +mod error; +mod actions; +mod contract; +pub mod contract_utils; diff --git a/src/__tests__/integration/data/wrc-20/src/state.rs b/src/__tests__/integration/data/wrc-20/src/state.rs new file mode 100644 index 00000000..6f802523 --- /dev/null +++ b/src/__tests__/integration/data/wrc-20/src/state.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct State { + pub symbol: String, + pub name: Option, + pub decimals: u8, + pub total_supply: u64, + pub balances: HashMap, + pub allowances: HashMap>, + + //Evolve interface + pub owner: String, + pub evolve: Option, + pub can_evolve: Option +} diff --git a/src/__tests__/integration/internal-writes/internal-write-back.test.ts b/src/__tests__/integration/internal-writes/internal-write-back.test.ts index 6708327c..89572248 100644 --- a/src/__tests__/integration/internal-writes/internal-write-back.test.ts +++ b/src/__tests__/integration/internal-writes/internal-write-back.test.ts @@ -216,7 +216,7 @@ describe('Testing internal writes', () => { expect((await contractB.readState()).cachedValue.state.counter).toEqual(2060); }); - xit('should properly evaluate state with a new client', async () => { + it('should properly evaluate state with a new client', async () => { const contractA2 = WarpFactory.forLocal(port) .contract(contractATxId) .setEvaluationOptions({ internalWrites: true }) @@ -279,7 +279,7 @@ describe('Testing internal writes', () => { expect((await contractB.readState()).cachedValue.state.counter).toEqual(2060); }); - xit('should properly evaluate state with a new client', async () => { + it('should properly evaluate state with a new client', async () => { const contractA2 = WarpFactory.forLocal(port) .contract(contractATxId) .setEvaluationOptions({ internalWrites: true }) diff --git a/src/__tests__/integration/internal-writes/simple-broken-thethar.test.js b/src/__tests__/integration/internal-writes/simple-broken-thethar.test.js new file mode 100644 index 00000000..b7ac9f36 --- /dev/null +++ b/src/__tests__/integration/internal-writes/simple-broken-thethar.test.js @@ -0,0 +1,150 @@ +import fs from 'fs'; +import ArLocal from 'arlocal'; +import path from 'path'; +import {mineBlock} from '../_helpers'; +import {WarpFactory} from '../../../core/WarpFactory'; +import {LoggerFactory} from '../../../logging/LoggerFactory'; + +const PORT = 1970; + +var simpleThetharTxId; +var arlocal, arweave, warp, walletJwk; +var erc20Contract, simpleThetarContract; + +describe('flow with broken behaviour', () => { + + beforeAll(async () => { + // note: each tests suit (i.e. file with tests that Jest is running concurrently + // with another files has to have ArLocal set to a different port!) + arlocal = new ArLocal(PORT, false); + await arlocal.start(); + LoggerFactory.INST.logLevel('error'); + + warp = WarpFactory.forLocal(PORT); + ({jwk: walletJwk} = await warp.generateWallet()); + arweave = warp.arweave; + }); + + afterAll(async () => { + await arlocal.stop(); + }); + + const deployJS = async () => { + const walletAddress = await arweave.wallets.jwkToAddress(walletJwk); + + // deploy TAR pst + const erc20Src = fs.readFileSync(path.join(__dirname, '../data/staking/erc-20.js'), 'utf8'); + + const tarInit = { + symbol: 'TAR', + name: 'ThetAR exchange token', + decimals: 2, + totalSupply: 20000, + balances: { + [walletAddress]: 10000, + }, + allowances: {}, + settings: null, + owner: walletAddress, + canEvolve: true, + evolve: '', + }; + + const erc20TxId = (await warp.createContract.deploy({ + wallet: walletJwk, + initState: JSON.stringify(tarInit), + src: erc20Src, + })).contractTxId; + erc20Contract = warp.contract(erc20TxId); + erc20Contract.setEvaluationOptions({ + internalWrites: true, + allowUnsafeClient: true, + allowBigInt: true, + }).connect(walletJwk); + + // deploy thetAR contract + const simpleThetharSrc = fs.readFileSync(path.join(__dirname, '../data/thethar/simple-thethar-contract.js'), 'utf8'); + const contractInit = { + token: erc20TxId, + orders: [] + }; + + simpleThetharTxId = (await warp.createContract.deploy({ + wallet: walletJwk, + initState: JSON.stringify(contractInit), + src: simpleThetharSrc, + })).contractTxId; + simpleThetarContract = warp.contract(simpleThetharTxId); + simpleThetarContract.setEvaluationOptions({ + internalWrites: true, + allowUnsafeClient: true, + allowBigInt: true, + }).connect(walletJwk); + }; + + const create = async (quantity) => { + await erc20Contract.writeInteraction({ + function: 'approve', + spender: simpleThetharTxId, + amount: quantity + }); + + await mineBlock(warp); + + const txId = (await simpleThetarContract.writeInteraction({ + function: 'create' + })).originalTxId; + + await mineBlock(warp); + + console.log('AFTER: ', JSON.stringify(await simpleThetarContract.readState())); + } + + const cancel = async (orderId) => { + console.log('cancel order...'); + + const txId = await simpleThetarContract.writeInteraction({ + function: 'cancel', + params: { + orderId: orderId + } + }); + await mineBlock(warp); + + console.log('AFTER: ', JSON.stringify(await simpleThetarContract.readState())); + } + + + const readFull = async () => { + const warp = WarpFactory.forLocal(PORT); + + let contract = warp.contract(simpleThetharTxId); + contract.setEvaluationOptions({ + internalWrites: true, + allowUnsafeClient: true, + allowBigInt: true + }).connect(walletJwk); + + const result = await contract.readState(); + + console.log('Contract: ', JSON.stringify(result, null, " ")); + + return result; + } + + it('correctly evaluate deferred state', async () => { + await deployJS(); + await create(1); + await cancel(0); + + console.error("========= READ FULL ==========") + const result = await readFull(); + expect(result.cachedValue.state.orders.length).toEqual(0); + + const errorMessages = result.cachedValue.errorMessages; + for (let errorMessageKey in errorMessages) { + expect(errorMessages[errorMessageKey]).not.toContain('TransferFromZero'); + } + }); + +}); \ No newline at end of file diff --git a/src/__tests__/regression/read-state.test.ts b/src/__tests__/regression/read-state.test.ts index e2aeda9a..fec48a7f 100644 --- a/src/__tests__/regression/read-state.test.ts +++ b/src/__tests__/regression/read-state.test.ts @@ -52,7 +52,8 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => { console.log('readState', contractTxId); try { console.log = function () {}; // to hide any logs from contracts... - const result2 = await WarpFactory.custom( + + const warp = WarpFactory.custom( arweave, { ...defaultCacheOptions, @@ -61,13 +62,15 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => { 'mainnet' ) .useWarpGateway( - { ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }, + {...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, { ...defaultCacheOptions, inMemory: true } ) - .build() + .build(); + + const result2 = await warp .contract(contractTxId) .setEvaluationOptions({ unsafeClient: 'allow', @@ -75,9 +78,15 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => { }) .readState(blockHeight); const result2String = stringify(result2.cachedValue.state).trim(); + + await warp.stateEvaluator.getCache().prune(1); + expect(result2String).toEqual(resultString); } finally { console.log = originalConsoleLog; + const heap = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100; + const rss = Math.round((process.memoryUsage().rss / 1024 / 1024) * 100) / 100; + console.log('Memory', { heap, rss }); } }, 800000 @@ -94,7 +103,7 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => { .readFileSync(path.join(__dirname, 'test-cases', 'contracts', `${contractTxId}.json`), 'utf-8') .trim(); console.log('readState', contractTxId); - const result2 = await WarpFactory.custom( + const warp = WarpFactory.custom( arweave, { ...defaultCacheOptions, @@ -103,20 +112,21 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => { 'mainnet' ) .useWarpGateway( - { ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }, + {...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, { ...defaultCacheOptions, inMemory: true } ) - .build() - .contract(contractTxId) + .build(); + + const result2 = await warp.contract(contractTxId) .setEvaluationOptions({ useVM2: true, unsafeClient: 'allow', allowBigInt: true - }) - .readState(blockHeight); + }).readState(blockHeight); + const result2String = stringify(result2.cachedValue.state).trim(); expect(result2String).toEqual(resultString); }, diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 0a753f85..b4783861 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -95,6 +95,7 @@ export interface Contract { */ readState( sortKeyOrBlockHeight?: string | number, + caller?: string, interactions?: GQLNodeInterface[] ): Promise>>; @@ -155,7 +156,7 @@ export interface Contract { transfer?: ArTransfer ): Promise>; - dryWriteFromTx(input: Input, transaction: GQLNodeInterface): Promise>; + applyInput(input: Input, transaction: GQLNodeInterface): Promise>; /** * Writes a new "interaction" transaction - i.e. such transaction that stores input for the contract. @@ -234,4 +235,8 @@ export interface Contract { setUncommittedState(contractTxId: string, result: EvalStateResult): void; hasUncommittedState(contractTxId: string): boolean; + + resetUncommittedState(): void; + + commitStates(interaction: GQLNodeInterface): Promise; } diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 014bc708..69aa4a69 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -22,14 +22,7 @@ import { LoggerFactory } from '../logging/LoggerFactory'; import { Evolve } from '../plugins/Evolve'; import { ArweaveWrapper } from '../utils/ArweaveWrapper'; import { sleep } from '../utils/utils'; -import { - BenchmarkStats, - Contract, - CurrentTx, - InnerCallData, - WriteInteractionOptions, - WriteInteractionResponse -} from './Contract'; +import { BenchmarkStats, Contract, InnerCallData, WriteInteractionOptions, WriteInteractionResponse } from './Contract'; import { ArTransfer, ArWallet, emptyTransfer, Tags } from './deploy/CreateContract'; import { InnerWritesEvaluator } from './InnerWritesEvaluator'; import { generateMockVrf } from '../utils/vrf'; @@ -37,6 +30,7 @@ import { Signature, CustomSignature } from './Signature'; import { ContractDefinition } from '../core/ContractDefinition'; import { EvaluationOptionsEvaluator } from './EvaluationOptionsEvaluator'; import { WarpFetchWrapper } from '../core/WarpFetchWrapper'; +import { Mutex } from 'async-mutex'; /** * An implementation of {@link Contract} that is backwards compatible with current style @@ -66,6 +60,8 @@ export class HandlerBasedContract implements Contract { private _uncommittedStates = new Map>(); + private readonly mutex = new Mutex(); + constructor( private readonly _contractTxId: string, protected readonly warp: Warp, @@ -129,52 +125,71 @@ export class HandlerBasedContract implements Contract { async readState( sortKeyOrBlockHeight?: string | number, + caller?: string, interactions?: GQLNodeInterface[] ): Promise>> { this.logger.info('Read state for', { contractTxId: this._contractTxId, sortKeyOrBlockHeight }); - const initBenchmark = Benchmark.measure(); - this.maybeResetRootContract(); if (!this.isRoot() && sortKeyOrBlockHeight == null) { throw new Error('SortKey MUST be always set for non-root contract calls'); } - const { stateEvaluator } = this.warp; - const sortKey = typeof sortKeyOrBlockHeight == 'number' ? this._sorter.generateLastSortKey(sortKeyOrBlockHeight) : sortKeyOrBlockHeight; - const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions); - this.logger.info('Execution Context', { - srcTxId: executionContext.contractDefinition?.srcTxId, - missingInteractions: executionContext.sortedInteractions?.length, - cachedSortKey: executionContext.cachedState?.sortKey - }); - initBenchmark.stop(); + if (sortKey && !this.isRoot() && this.hasUncommittedState(this.txId())) { + const result = this.getUncommittedState(this.txId()); + return { + sortKey, + cachedValue: result as EvalStateResult + }; + } - const stateBenchmark = Benchmark.measure(); - const result = await stateEvaluator.eval(executionContext); - stateBenchmark.stop(); + // TODO: not sure if we should synchronize on a contract instance or contractTxId + // in the latter case, the warp instance should keep a map contractTxId -> mutex + const releaseMutex = await this.mutex.acquire(); + try { + const initBenchmark = Benchmark.measure(); + this.maybeResetRootContract(); + + const executionContext = await this.createExecutionContext(this._contractTxId, sortKey, false, interactions); + this.logger.info('Execution Context', { + srcTxId: executionContext.contractDefinition?.srcTxId, + missingInteractions: executionContext.sortedInteractions?.length, + cachedSortKey: executionContext.cachedState?.sortKey + }); + initBenchmark.stop(); - const total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number); + const stateBenchmark = Benchmark.measure(); + const result = await stateEvaluator.eval(executionContext); + stateBenchmark.stop(); - this._benchmarkStats = { - gatewayCommunication: initBenchmark.elapsed(true) as number, - stateEvaluation: stateBenchmark.elapsed(true) as number, - total - }; + const total = (initBenchmark.elapsed(true) as number) + (stateBenchmark.elapsed(true) as number); - this.logger.info('Benchmark', { - 'Gateway communication ': initBenchmark.elapsed(), - 'Contract evaluation ': stateBenchmark.elapsed(), - 'Total: ': `${total.toFixed(0)}ms` - }); + this._benchmarkStats = { + gatewayCommunication: initBenchmark.elapsed(true) as number, + stateEvaluation: stateBenchmark.elapsed(true) as number, + total + }; - return result; + this.logger.info('Benchmark', { + 'Gateway communication ': initBenchmark.elapsed(), + 'Contract evaluation ': stateBenchmark.elapsed(), + 'Total: ': `${total.toFixed(0)}ms` + }); + + if (sortKey && !this.isRoot()) { + this.setUncommittedState(this.txId(), result.cachedValue); + } + + return result; + } finally { + releaseMutex(); + } } async readStateFor( @@ -198,7 +213,7 @@ export class HandlerBasedContract implements Contract { interactionTx: GQLNodeInterface ): Promise> { this.logger.info(`View state for ${this._contractTxId}`, interactionTx); - return await this.callContractForTx(input, interactionTx); + return await this.doApplyInputOnTx(input, interactionTx); } async dryWrite( @@ -211,9 +226,9 @@ export class HandlerBasedContract implements Contract { return await this.callContract(input, caller, undefined, tags, transfer); } - async dryWriteFromTx(input: Input, transaction: GQLNodeInterface): Promise> { - this.logger.info(`Dry-write from transaction ${transaction.id} for ${this._contractTxId}`); - return await this.callContractForTx(input, transaction); + async applyInput(input: Input, transaction: GQLNodeInterface): Promise> { + this.logger.info(`Apply-input from transaction ${transaction.id} for ${this._contractTxId}`); + return await this.doApplyInputOnTx(input, transaction); } async writeInteraction( @@ -683,14 +698,25 @@ export class HandlerBasedContract implements Contract { return handleResult; } - private async callContractForTx( + private async doApplyInputOnTx( input: Input, interactionTx: GQLNodeInterface ): Promise> { this.maybeResetRootContract(); + let evalStateResult: SortKeyCacheResult>; + const executionContext = await this.createExecutionContextFromTx(this._contractTxId, interactionTx); - const evalStateResult = await this.warp.stateEvaluator.eval(executionContext); + + if (!this.isRoot() && this.hasUncommittedState(this.txId())) { + evalStateResult = { + sortKey: interactionTx.sortKey, + cachedValue: this.getUncommittedState(this.txId()) as EvalStateResult + }; + } else { + evalStateResult = await this.warp.stateEvaluator.eval(executionContext); + this.setUncommittedState(this.txId(), evalStateResult.cachedValue); + } this.logger.debug('callContractForTx - evalStateResult', { result: evalStateResult.cachedValue.state, @@ -803,15 +829,6 @@ export class HandlerBasedContract implements Contract { return this._rootSortKey; } - private getRoot(): Contract { - let result: Contract = this; - while (!result.isRoot()) { - result = result.parent(); - } - - return result; - } - getEoEvaluator(): EvaluationOptionsEvaluator { const root = this.getRoot() as HandlerBasedContract; return root._eoEvaluator; @@ -850,10 +867,38 @@ export class HandlerBasedContract implements Contract { getUncommittedState(contractTxId: string): EvalStateResult { return (this.getRoot() as HandlerBasedContract)._uncommittedStates.get(contractTxId); } + setUncommittedState(contractTxId: string, result: EvalStateResult): void { - (this.getRoot() as HandlerBasedContract)._uncommittedStates.set(contractTxId, result); + this.getRoot()._uncommittedStates.set(contractTxId, result); } + hasUncommittedState(contractTxId: string): boolean { - return (this.getRoot() as HandlerBasedContract)._uncommittedStates.has(contractTxId); + return this.getRoot()._uncommittedStates.has(contractTxId); + } + + resetUncommittedState(): void { + this.getRoot()._uncommittedStates = new Map(); + } + + async commitStates(interaction: GQLNodeInterface): Promise { + const uncommittedStates = this.getRoot()._uncommittedStates; + try { + if (uncommittedStates.size > 1) { + for (const [k, v] of uncommittedStates) { + await this.warp.stateEvaluator.putInCache(k, interaction, v); + } + } + } finally { + this.resetUncommittedState(); + } + } + + private getRoot(): HandlerBasedContract { + let result: Contract = this; + while (!result.isRoot()) { + result = result.parent(); + } + + return result as HandlerBasedContract; } } diff --git a/src/core/ExecutionContext.ts b/src/core/ExecutionContext.ts index c0a99490..67f5167c 100644 --- a/src/core/ExecutionContext.ts +++ b/src/core/ExecutionContext.ts @@ -40,7 +40,7 @@ export type ExecutionContext = { * performs all the computation. */ handler: Api; - caller?: string; // note: this is only set for "viewState" operations + caller?: string; // note: this is only set for "viewState" and "write" operations cachedState?: SortKeyCacheResult>; requestedSortKey?: string; }; diff --git a/src/core/modules/impl/CacheableStateEvaluator.ts b/src/core/modules/impl/CacheableStateEvaluator.ts index cd5f7c1c..20e9aa6b 100644 --- a/src/core/modules/impl/CacheableStateEvaluator.ts +++ b/src/core/modules/impl/CacheableStateEvaluator.ts @@ -42,10 +42,6 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { } const missingInteractions = executionContext.sortedInteractions; - - // TODO: this is tricky part, needs proper description - // for now: it prevents from infinite loop calls between calls that are making - // internal interact writes. const contractTxId = executionContext.contractDefinition.txId; // sanity check... if (!contractTxId) { diff --git a/src/core/modules/impl/DefaultStateEvaluator.ts b/src/core/modules/impl/DefaultStateEvaluator.ts index 65974806..29e7e35b 100644 --- a/src/core/modules/impl/DefaultStateEvaluator.ts +++ b/src/core/modules/impl/DefaultStateEvaluator.ts @@ -96,6 +96,9 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { if (shouldBreakAfterEvolve) { break; } + + contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages)); + const missingInteraction = missingInteractions[i]; const singleInteractionBenchmark = Benchmark.measure(); currentSortKey = missingInteraction.sortKey; @@ -125,7 +128,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { ); const isInteractWrite = this.tagsParser.isInteractWrite(missingInteraction, contractDefinition.txId); - // other contract makes write ("writing contract") on THIS contract if (isInteractWrite && internalWrites) { // evaluating txId of the contract that is writing on THIS contract @@ -137,27 +139,21 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { .addInteractionData({ interaction: null, interactionTx: missingInteraction }); // creating a Contract instance for the "writing" contract - const writingContract = executionContext.warp.contract(writingContractTxId, executionContext.contract, { + const writingContract = warp.contract(writingContractTxId, executionContext.contract, { callingInteraction: missingInteraction, callType: 'read' }); - /*await this.onContractCall( - missingInteraction, - executionContext, - new EvalStateResult(currentState, validity, errorMessages) - );*/ - this.logger.debug(`${indent(depth)}Reading state of the calling contract at`, missingInteraction.sortKey); /** Reading the state of the writing contract. This in turn will cause the state of THIS contract to be - updated in cache - see {@link ContractHandlerApi.assignWrite} + updated in uncommitted state */ - let newState = null; + let newState: EvalStateResult = null; try { await writingContract.readState(missingInteraction.sortKey); - //newState = await this.internalWriteState(contractDefinition.txId, missingInteraction.sortKey); + newState = contract.getUncommittedState(contract.txId()); } catch (e) { if (e.name == 'ContractError' && e.subtype == 'unsafeClientSkip') { this.logger.warn('Skipping unsafe contract in internal write'); @@ -176,17 +172,16 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { // loading latest state of THIS contract from cache if (newState !== null) { - currentState = newState.cachedValue.state; + currentState = newState.state as State; // we need to update the state in the wasm module executionContext?.handler.initState(currentState); - validity[missingInteraction.id] = newState.cachedValue.validity[missingInteraction.id]; - if (newState.cachedValue.errorMessages?.[missingInteraction.id]) { - errorMessages[missingInteraction.id] = newState.cachedValue.errorMessages[missingInteraction.id]; + validity[missingInteraction.id] = newState.validity[missingInteraction.id]; + if (newState.errorMessages?.[missingInteraction.id]) { + errorMessages[missingInteraction.id] = newState.errorMessages[missingInteraction.id]; } const toCache = new EvalStateResult(currentState, validity, errorMessages); - await this.onStateUpdate(missingInteraction, executionContext, toCache); if (canBeCached(missingInteraction)) { lastConfirmedTxState = { tx: missingInteraction, @@ -267,12 +262,6 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { state: toCache }; } - await this.onStateUpdate( - missingInteraction, - executionContext, - toCache, - cacheEveryNInteractions % i == 0 - ); } if (progressPlugin) { @@ -297,11 +286,17 @@ export abstract class DefaultStateEvaluator implements StateEvaluator { throw e; } } + + if (contract.isRoot()) { + await contract.commitStates(missingInteraction); + } else { + contract.setUncommittedState(contract.txId(), new EvalStateResult(currentState, validity, errorMessages)); + } } const evalStateResult = new EvalStateResult(currentState, validity, errorMessages); // state could have been fully retrieved from cache - // or there were no interactions below requested block height + // or there were no interactions below requested sort key if (lastConfirmedTxState !== null) { await this.onStateEvaluated(lastConfirmedTxState.tx, executionContext, lastConfirmedTxState.state); } diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index a2f10ad1..c0660931 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -58,7 +58,7 @@ export abstract class AbstractContractHandler implements HandlerApi(input, this.swGlobal._activeTx); + const result = await calleeContract.applyInput(input, this.swGlobal._activeTx); this.logger.debug('Cache result?:', !this.swGlobal._activeTx.dry); const shouldAutoThrow = @@ -69,7 +69,7 @@ export abstract class AbstractContractHandler implements HandlerApi implements HandlerApi implements HandlerApi implements HandlerApi) { this.swGlobal.contracts.refreshState = async () => { - const stateEvaluator = executionContext.warp.stateEvaluator; - const result = await stateEvaluator.latestAvailableState( - this.swGlobal.contract.id, - this.swGlobal._activeTx.sortKey - ); - return result?.cachedValue.state; + return executionContext.contract.getUncommittedState(this.swGlobal.contract.id)?.state; }; } } diff --git a/yarn.lock b/yarn.lock index 0952a46d..c56600e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2161,6 +2161,13 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async-mutex@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" + integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== + dependencies: + tslib "^2.4.0" + async-retry@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" From 5600310b905b19443b62ad6aec7bc01c05f98654 Mon Sep 17 00:00:00 2001 From: ppe Date: Tue, 17 Jan 2023 10:04:30 +0100 Subject: [PATCH 4/5] v1.2.48-beta.0 --- src/__tests__/regression/read-state.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/__tests__/regression/read-state.test.ts b/src/__tests__/regression/read-state.test.ts index fec48a7f..dd2f86d9 100644 --- a/src/__tests__/regression/read-state.test.ts +++ b/src/__tests__/regression/read-state.test.ts @@ -62,7 +62,7 @@ describe.each(chunked)('v1 compare.suite %#', (contracts: string[]) => { 'mainnet' ) .useWarpGateway( - {...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, + { ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }, { ...defaultCacheOptions, inMemory: true @@ -112,7 +112,7 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => { 'mainnet' ) .useWarpGateway( - {...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null}, + { ...defaultWarpGwOptions, source: SourceType.ARWEAVE, confirmationStatus: null }, { ...defaultCacheOptions, inMemory: true @@ -120,12 +120,14 @@ describe.each(chunkedVm)('v1 compare.suite (VM2) %#', (contracts: string[]) => { ) .build(); - const result2 = await warp.contract(contractTxId) + const result2 = await warp + .contract(contractTxId) .setEvaluationOptions({ useVM2: true, unsafeClient: 'allow', allowBigInt: true - }).readState(blockHeight); + }) + .readState(blockHeight); const result2String = stringify(result2.cachedValue.state).trim(); expect(result2String).toEqual(resultString); From 6cd7ce664b3568a2f3478d478ac87683e84db570 Mon Sep 17 00:00:00 2001 From: ppe Date: Thu, 19 Jan 2023 11:13:45 +0100 Subject: [PATCH 5/5] chore: cleanup --- .../data/thethar/thethar-contract-wrc.js | 403 ----------------- .../data/thethar/thethar-contract.js | 411 ------------------ .../integration/data/wrc-20/src/action.rs | 56 --- .../data/wrc-20/src/actions/allowances.rs | 57 --- .../data/wrc-20/src/actions/balance.rs | 23 - .../data/wrc-20/src/actions/evolve.rs | 17 - .../data/wrc-20/src/actions/mod.rs | 5 - .../data/wrc-20/src/actions/transfers.rs | 47 -- .../integration/data/wrc-20/src/contract.rs | 38 -- .../data/wrc-20/src/contract_utils/README.md | 4 - .../wrc-20/src/contract_utils/entrypoint.rs | 107 ----- .../data/wrc-20/src/contract_utils/mod.rs | 5 - .../integration/data/wrc-20/src/error.rs | 10 - .../integration/data/wrc-20/src/lib.rs | 6 - .../integration/data/wrc-20/src/state.rs | 18 - src/contract/HandlerBasedContract.ts | 5 +- .../impl/handler/AbstractContractHandler.ts | 20 +- src/core/modules/impl/handler/JsHandlerApi.ts | 5 +- .../modules/impl/handler/WasmHandlerApi.ts | 2 +- 19 files changed, 9 insertions(+), 1230 deletions(-) delete mode 100644 src/__tests__/integration/data/thethar/thethar-contract-wrc.js delete mode 100644 src/__tests__/integration/data/thethar/thethar-contract.js delete mode 100644 src/__tests__/integration/data/wrc-20/src/action.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/actions/allowances.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/actions/balance.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/actions/evolve.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/actions/mod.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/actions/transfers.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/contract.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/contract_utils/README.md delete mode 100644 src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/error.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/lib.rs delete mode 100644 src/__tests__/integration/data/wrc-20/src/state.rs diff --git a/src/__tests__/integration/data/thethar/thethar-contract-wrc.js b/src/__tests__/integration/data/thethar/thethar-contract-wrc.js deleted file mode 100644 index 21ee6328..00000000 --- a/src/__tests__/integration/data/thethar/thethar-contract-wrc.js +++ /dev/null @@ -1,403 +0,0 @@ -(() => { - // src/thetAR/actions/common.ts - var isAddress = (addr) => /[a-z0-9_-]{43}/i.test(addr); - var hashCheck = async (validHashs, contractTxId) => { - const tx = await SmartWeave.unsafeClient.transactions.get(contractTxId); - let SrcTxId; - tx.get("tags").forEach((tag) => { - let key = tag.get("name", { decode: true, string: true }); - if (key === "Contract-Src") { - SrcTxId = tag.get("value", { decode: true, string: true }); - } - }); - if (!SrcTxId || !isAddress(SrcTxId)) { - throw new ContractError("Cannot find valid srcTxId in contract Tx content!"); - } - const srcTx = await SmartWeave.unsafeClient.transactions.getData(SrcTxId, { decode: true, string: true }); - if (srcTx.length < 1e4 && validHashs.includes(calcHash(srcTx))) { - return true; - } - return false; - }; - var calcHash = (string) => { - var hash = 0, i, chr; - if (string.length === 0) - return hash; - for (i = 0; i < string.length; i++) { - chr = string.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; - } - return hash; - }; - var selectWeightedTokenHolder = async (balances) => { - let totalTokens = 0; - for (const address of Object.keys(balances)) { - totalTokens += balances[address]; - } - let sum = 0; - const r = await getRandomIntNumber(totalTokens); - for (const address of Object.keys(balances)) { - sum += balances[address]; - if (r <= sum && balances[address] > 0) { - return address; - } - } - return void 0; - }; - async function getRandomIntNumber(max, uniqueValue = "") { - const pseudoRandomData = SmartWeave.arweave.utils.stringToBuffer(SmartWeave.block.height + SmartWeave.block.timestamp + SmartWeave.transaction.id + uniqueValue); - const hashBytes = await SmartWeave.arweave.crypto.hash(pseudoRandomData); - const randomBigInt = bigIntFromBytes(hashBytes); - return Number(randomBigInt % BigInt(max)); - } - function bigIntFromBytes(byteArr) { - let hexString = ""; - for (const byte of byteArr) { - hexString += byte.toString(16).padStart(2, "0"); - } - return BigInt("0x" + hexString); - } - - // src/thetAR/actions/write/addPair.ts - var addPair = async (state, action) => { - const param = action.input.params; - const tokenAddress = param.tokenAddress; - const logoTx = param.logo; - const description = param.description; - if (!isAddress(tokenAddress)) { - throw new ContractError("Token address format error!"); - } - if (!isAddress(logoTx)) { - throw new ContractError("You should enter transaction id for Arweave of your logo!"); - } - if (!validDescription(description)) { - throw new ContractError("Description you enter is not valid!"); - } - if (action.caller !== state.owner) { - const txQty = SmartWeave.transaction.quantity; - const txTarget = SmartWeave.transaction.target; - if (txTarget !== state.owner) { - throw new ContractError("AddPair fee sent to wrong target!"); - } - if (SmartWeave.arweave.ar.isLessThan(txQty, SmartWeave.arweave.ar.arToWinston("10"))) { - throw new ContractError("AddPair fee not right!"); - } - if (!await hashCheck(state.tokenSrcTemplateHashs, tokenAddress)) { - throw new ContractError("Pst contract validation check failed!"); - } - } - if (state.pairInfos.map((info) => info.tokenAddress).includes(tokenAddress)) { - throw new ContractError("Pair already exists!"); - } - const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); - state.maxPairId++; - state.pairInfos.push({ - pairId: state.maxPairId, - tokenAddress, - logo: logoTx, - description, - name: tokenState.name, - symbol: tokenState.symbol, - decimals: tokenState.decimals - }); - state.orderInfos[state.maxPairId] = { - currentPrice: void 0, - orders: [] - }; - for (const user in state.userOrders) { - if (Object.prototype.hasOwnProperty.call(state.userOrders, user)) { - let userOrder2 = state.userOrders[user]; - userOrder2[state.maxPairId] = []; - } - } - return { state }; - }; - var validDescription = (desc) => /[a-z0-9_\s\:\/-]{1,128}/i.test(desc); - - // src/thetAR/actions/write/createOrder.ts - var createOrder = async (state, action) => { - const param = action.input.params; - if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { - throw new ContractError("PairId not valid!"); - } - if (param.price !== void 0 && param.price !== null) { - if (typeof param.price !== "number") { - throw new ContractError("Price must be a number!"); - } - if (param.price <= 0 || !Number.isInteger(param.price)) { - throw new ContractError("Price must be positive integer!"); - } - } - const newOrder = { - creator: action.caller, - orderId: SmartWeave.transaction.id, - direction: param.direction, - quantity: await checkOrderQuantity(state, action), - price: param.price - }; - let selectedFeeRecvr = void 0; - try { - selectedFeeRecvr = await selectWeightedTokenHolder(await tokenBalances(state.thetarTokenAddress)); - } catch { - } - const { newOrderbook, newUserOrders, transactions, currentPrice } = await matchOrder(newOrder, state.orderInfos[param.pairId].orders, state.userOrders, param.pairId, action.caller, state.feeRatio, selectedFeeRecvr); - state.orderInfos[param.pairId].orders = newOrderbook; - state.userOrders = newUserOrders; - if (!isNaN(currentPrice) && isFinite(currentPrice)) { - state.orderInfos[param.pairId].currentPrice = currentPrice; - } - for await (const tx of transactions) { - const matchedPair = state.pairInfos.find((i) => i.pairId === param.pairId); - const targetTokenAdrress = tx.tokenType === "dominent" ? state.thetarTokenAddress : matchedPair.tokenAddress; - await SmartWeave.contracts.write(targetTokenAdrress, { function: "transfer", to: tx.to, amount: tx.quantity }); - } - return { state }; - }; - var tokenBalances = async (tokenAddress) => { - return (await SmartWeave.contracts.readContractState(tokenAddress)).balances; - }; - var checkOrderQuantity = async (state, action) => { - const param = action.input.params; - let pairInfo2 = state.pairInfos.find((pair) => pair.pairId === param.pairId); - const tokenAddress = param.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; - const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); - let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id]; - await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", from: action.caller, to: SmartWeave.contract.id, amount: orderQuantity }); - if (param.direction === "buy" && param.price) { - orderQuantity = Math.floor(orderQuantity / param.price); - } - return orderQuantity; - }; - var matchOrder = async (newOrder, orderbook, userOrders, newOrderPairId, caller, feeRatio, selectedFeeRecvr) => { - let transactions = Array(); - const targetSortDirection = newOrder.direction === "buy" ? "sell" : "buy"; - let totalTradePrice = 0; - let totalTradeVolume = 0; - const reverseOrderbook = orderbook.filter((order) => order.direction === targetSortDirection).sort((a, b) => { - if (newOrder.direction === "buy") { - return a.price > b.price ? 1 : -1; - } else { - return a.price > b.price ? -1 : 1; - } - }); - const orderType = newOrder.price ? "limit" : "market"; - if (reverseOrderbook.length === 0 && orderType === "market") { - throw new ContractError(`The first order must be limit type!`); - } - const newOrderTokenType = orderType === "market" && newOrder.direction === "buy" ? "dominent" : "trade"; - for (let i = 0; i < reverseOrderbook.length; i++) { - const order = reverseOrderbook[i]; - if (orderType === "limit" && order.price !== newOrder.price) { - continue; - } - const targetPrice = order.price; - const orderAmount = order.quantity; - const newOrderAmoumt = newOrderTokenType === "trade" ? newOrder.quantity : Math.floor(newOrder.quantity / targetPrice); - const targetAmout = orderAmount < newOrderAmoumt ? orderAmount : newOrderAmoumt; - totalTradePrice += targetPrice * targetAmout; - totalTradeVolume += targetAmout; - if (targetAmout === 0) { - break; - } - const dominentFee = Math.floor(targetAmout * targetPrice * feeRatio); - const tradeFee = Math.floor(targetAmout * feeRatio); - const dominentSwap = targetAmout * targetPrice - dominentFee; - const tradeSwap = targetAmout - tradeFee; - const buyer = newOrder.direction === "buy" ? newOrder : order; - const seller = newOrder.direction === "buy" ? order : newOrder; - transactions.push({ - tokenType: "dominent", - to: seller.creator, - quantity: dominentSwap - }); - transactions.push({ - tokenType: "trade", - to: buyer.creator, - quantity: tradeSwap - }); - if (selectedFeeRecvr) { - transactions.push({ - tokenType: "dominent", - to: selectedFeeRecvr, - quantity: dominentFee - }); - transactions.push({ - tokenType: "trade", - to: selectedFeeRecvr, - quantity: tradeFee - }); - } - order.quantity -= targetAmout; - if (order.quantity === 0) { - orderbook = orderbook.filter((v) => v.orderId !== order.orderId); - } - let userOrderInfos = userOrders[order.creator][newOrderPairId]; - let matchedOrderIdx = userOrderInfos.findIndex((value) => value.orderId === order.orderId); - userOrderInfos[matchedOrderIdx].quantity -= targetAmout; - if (userOrderInfos[matchedOrderIdx].quantity === 0) { - userOrders[order.creator][newOrderPairId] = userOrderInfos.filter((v) => v.orderId !== order.orderId); - } - newOrder.quantity -= newOrderTokenType === "trade" ? targetAmout : targetAmout * targetPrice; - } - if (orderType === "market" && newOrder.quantity !== 0) { - transactions.push({ - tokenType: newOrderTokenType, - to: newOrder.creator, - quantity: newOrder.quantity - }); - newOrder.quantity = 0; - } - if (orderType === "limit" && newOrder.quantity !== 0) { - orderbook.push({ ...newOrder }); - } - if (newOrder.quantity !== 0) { - if (userOrders[caller] === void 0) { - userOrders[caller] = {}; - } - if (userOrders[caller][newOrderPairId] === void 0) { - userOrders[caller][newOrderPairId] = []; - } - userOrders[caller][newOrderPairId].push({ ...newOrder }); - } - return { - newOrderbook: orderbook, - newUserOrders: userOrders, - transactions, - currentPrice: totalTradePrice / totalTradeVolume - }; - }; - - // src/thetAR/actions/write/deposit.ts - var deposit = async (state, action) => { - logger.error("Token: " + action.input.params.token); - logger.error("Amount: " + action.input.params.amount); - await SmartWeave.contracts.write(action.input.params.token, { - function: "transferFrom", - from: action.caller, - to: SmartWeave.contract.id, - amount: action.input.params.amount - }); - return { state }; - }; - - // src/thetAR/actions/write/cancelOrder.ts - var cancelOrder = async (state, action) => { - const param = action.input.params; - const orderId = param.orderId; - const pairId = param.pairId; - if (!isAddress(orderId)) { - throw new ContractError(`OrderId not found: ${param.orderId}!`); - } - if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { - throw new ContractError("PairId not valid!"); - } - const orderInfo2 = state.userOrders[action.caller][pairId].find((v) => v.orderId === orderId); - const pairInfo2 = state.pairInfos.find((i) => i.pairId === pairId); - if (!orderInfo2) { - throw new ContractError(`Cannot get access to pairId: ${pairId}!`); - } - if (!pairInfo2) { - throw new ContractError(`Pair info record not found: ${pairId}!`); - } - const tokenAddress = orderInfo2.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; - const quantity = orderInfo2.direction === "buy" ? orderInfo2.price * orderInfo2.quantity : orderInfo2.quantity; - await SmartWeave.contracts.write(tokenAddress, { function: "transfer", to: action.caller, amount: quantity }); - let ordersForUser = state.userOrders[action.caller][pairId]; - state.userOrders[action.caller][pairId] = ordersForUser.filter((i) => i.orderId !== orderId); - let ordersForPair = state.orderInfos[pairId].orders; - state.orderInfos[pairId].orders = ordersForPair.filter((i) => i.orderId !== orderId); - return { state }; - }; - - // src/thetAR/actions/write/addTokenHash.ts - var addTokenHash = async (state, action) => { - const param = action.input.params; - const hash = param.hash; - if (action.caller !== state.owner) { - throw new ContractError("You have no permission to modify hash list!"); - } - state.tokenSrcTemplateHashs.push(hash); - return { state }; - }; - - // src/thetAR/actions/read/pairInfo.ts - var pairInfo = async (state, action) => { - const param = action.input.params; - let pairId = param.pairId; - let result; - if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { - throw new ContractError(`Invalid pairId!`); - } - result = state.pairInfos.filter((i) => i.pairId === pairId)[0]; - return { result }; - }; - - // src/thetAR/actions/read/pairInfos.ts - var pairInfos = async (state, action) => { - let result; - result = state.pairInfos; - return { result }; - }; - - // src/thetAR/actions/read/orderInfos.ts - var orderInfos = async (state, action) => { - let result; - result = state.orderInfos; - return { result }; - }; - - // src/thetAR/actions/read/orderInfo.ts - var orderInfo = async (state, action) => { - const param = action.input.params; - let pairId = param.pairId; - let result; - if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { - throw new ContractError(`Invalid pairId!`); - } - result = state.orderInfos[pairId]; - return { result }; - }; - - // src/thetAR/actions/read/userOrder.ts - var userOrder = async (state, action) => { - const param = action.input.params; - let address = param.address; - let result; - if (!isAddress(address)) { - throw new ContractError(`Invalid wallet address!`); - } - result = state.userOrders[address]; - return { result }; - }; - - // src/thetAR/contract.ts - async function handle(state, action) { - const func = action.input.function; - switch (func) { - case "addPair": - return await addPair(state, action); - case "createOrder": - return await createOrder(state, action); - case "cancelOrder": - return await cancelOrder(state, action); - case "pairInfo": - return await pairInfo(state, action); - case "pairInfos": - return await pairInfos(state, action); - case "orderInfo": - return await orderInfo(state, action); - case "orderInfos": - return await orderInfos(state, action); - case "addTokenHash": - return await addTokenHash(state, action); - case "userOrder": - return await userOrder(state, action); - case "deposit": - return await deposit(state, action); - default: - throw new ContractError(`No function supplied or function not recognised: "${func}"`); - } - } - })(); - \ No newline at end of file diff --git a/src/__tests__/integration/data/thethar/thethar-contract.js b/src/__tests__/integration/data/thethar/thethar-contract.js deleted file mode 100644 index a87c45e2..00000000 --- a/src/__tests__/integration/data/thethar/thethar-contract.js +++ /dev/null @@ -1,411 +0,0 @@ -(() => { - // src/thetAR/actions/common.ts - var isAddress = (addr) => /[a-z0-9_-]{43}/i.test(addr); - var hashCheck = async (validHashs, contractTxId) => { - const tx = await SmartWeave.unsafeClient.transactions.get(contractTxId); - let SrcTxId; - tx.get("tags").forEach((tag) => { - let key = tag.get("name", { decode: true, string: true }); - if (key === "Contract-Src") { - SrcTxId = tag.get("value", { decode: true, string: true }); - } - }); - if (!SrcTxId || !isAddress(SrcTxId)) { - throw new ContractError("Cannot find valid srcTxId in contract Tx content!"); - } - const srcTx = await SmartWeave.unsafeClient.transactions.getData(SrcTxId, { decode: true, string: true }); - if (srcTx.length < 1e4 && validHashs.includes(calcHash(srcTx))) { - return true; - } - return false; - }; - var calcHash = (string) => { - var hash = 0, i, chr; - if (string.length === 0) - return hash; - for (i = 0; i < string.length; i++) { - chr = string.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; - } - return hash; - }; - var selectWeightedTokenHolder = async (balances) => { - let totalTokens = 0; - for (const address of Object.keys(balances)) { - totalTokens += balances[address]; - } - let sum = 0; - const r = await getRandomIntNumber(totalTokens); - for (const address of Object.keys(balances)) { - sum += balances[address]; - if (r <= sum && balances[address] > 0) { - return address; - } - } - return void 0; - }; - async function getRandomIntNumber(max, uniqueValue = "") { - const pseudoRandomData = SmartWeave.arweave.utils.stringToBuffer(SmartWeave.block.height + SmartWeave.block.timestamp + SmartWeave.transaction.id + uniqueValue); - const hashBytes = await SmartWeave.arweave.crypto.hash(pseudoRandomData); - const randomBigInt = bigIntFromBytes(hashBytes); - return Number(randomBigInt % BigInt(max)); - } - function bigIntFromBytes(byteArr) { - let hexString = ""; - for (const byte of byteArr) { - hexString += byte.toString(16).padStart(2, "0"); - } - return BigInt("0x" + hexString); - } - - // src/thetAR/actions/write/addPair.ts - var addPair = async (state, action) => { - const param = action.input.params; - const tokenAddress = param.tokenAddress; - const logoTx = param.logo; - const description = param.description; - if (!isAddress(tokenAddress)) { - throw new ContractError("Token address format error!"); - } - if (!isAddress(logoTx)) { - throw new ContractError("You should enter transaction id for Arweave of your logo!"); - } - if (!validDescription(description)) { - throw new ContractError("Description you enter is not valid!"); - } - if (action.caller !== state.owner) { - const txQty = SmartWeave.transaction.quantity; - const txTarget = SmartWeave.transaction.target; - if (txTarget !== state.owner) { - throw new ContractError("AddPair fee sent to wrong target!"); - } - if (SmartWeave.arweave.ar.isLessThan(txQty, SmartWeave.arweave.ar.arToWinston("10"))) { - throw new ContractError("AddPair fee not right!"); - } - if (!await hashCheck(state.tokenSrcTemplateHashs, tokenAddress)) { - throw new ContractError("Pst contract validation check failed!"); - } - } - if (state.pairInfos.map((info) => info.tokenAddress).includes(tokenAddress)) { - throw new ContractError("Pair already exists!"); - } - const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); - state.maxPairId++; - state.pairInfos.push({ - pairId: state.maxPairId, - tokenAddress, - logo: logoTx, - description, - name: tokenState.name, - symbol: tokenState.symbol, - decimals: tokenState.decimals - }); - state.orderInfos[state.maxPairId] = { - currentPrice: void 0, - orders: [] - }; - for (const user in state.userOrders) { - if (Object.prototype.hasOwnProperty.call(state.userOrders, user)) { - let userOrder2 = state.userOrders[user]; - userOrder2[state.maxPairId] = []; - } - } - return { state }; - }; - var validDescription = (desc) => /[a-z0-9_\s\:\/-]{1,128}/i.test(desc); - - // src/thetAR/actions/write/createOrder.ts - var createOrder = async (state, action) => { - const param = action.input.params; - if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { - throw new ContractError("PairId not valid!"); - } - if (param.price !== void 0 && param.price !== null) { - if (typeof param.price !== "number") { - throw new ContractError("Price must be a number!"); - } - if (param.price <= 0 || !Number.isInteger(param.price)) { - throw new ContractError("Price must be positive integer!"); - } - } - - let {orderQuantity, updatedState} = await checkOrderQuantity(state, action); - const newOrder = { - creator: action.caller, - orderId: SmartWeave.transaction.id, - direction: param.direction, - quantity: orderQuantity, - price: param.price - }; - let selectedFeeRecvr = void 0; - try { - selectedFeeRecvr = await selectWeightedTokenHolder(tokenBalances(updatedState)); - } catch { - } - const { newOrderbook, newUserOrders, transactions, currentPrice } = await matchOrder(newOrder, state.orderInfos[param.pairId].orders, state.userOrders, param.pairId, action.caller, state.feeRatio, selectedFeeRecvr); - state.orderInfos[param.pairId].orders = newOrderbook; - state.userOrders = newUserOrders; - if (!isNaN(currentPrice) && isFinite(currentPrice)) { - state.orderInfos[param.pairId].currentPrice = currentPrice; - } - for await (const tx of transactions) { - const matchedPair = state.pairInfos.find((i) => i.pairId === param.pairId); - const targetTokenAdrress = tx.tokenType === "dominent" ? state.thetarTokenAddress : matchedPair.tokenAddress; - await SmartWeave.contracts.write(targetTokenAdrress, { function: "transfer", to: tx.to, amount: tx.quantity }); - } - return { state }; - }; - var tokenBalances = (updatedState) => { - return updatedState.balances; - }; - var checkOrderQuantity = async (state, action) => { - const param = action.input.params; - let pairInfo2 = state.pairInfos.find((pair) => pair.pairId === param.pairId); - const tokenAddress = param.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; - const tokenState = await SmartWeave.contracts.readContractState(tokenAddress); - //let orderQuantity = param.price; - let orderQuantity = tokenState.allowances[action.caller][SmartWeave.contract.id]; - //WASM version - //await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", from: action.caller, to: SmartWeave.contract.id, amount: orderQuantity }); - //JS version - logger.error("CREATE Taking tokens: " + orderQuantity); - updatedState = (await SmartWeave.contracts.write(tokenAddress, { function: "transferFrom", sender: action.caller, recipient: SmartWeave.contract.id, amount: orderQuantity })).state; - logger.error("STATE:", updatedState); - if (param.direction === "buy" && param.price) { - orderQuantity = Math.floor(orderQuantity / param.price); - } - return {orderQuantity, updatedState}; - }; - var matchOrder = async (newOrder, orderbook, userOrders, newOrderPairId, caller, feeRatio, selectedFeeRecvr) => { - let transactions = Array(); - const targetSortDirection = newOrder.direction === "buy" ? "sell" : "buy"; - let totalTradePrice = 0; - let totalTradeVolume = 0; - const reverseOrderbook = orderbook.filter((order) => order.direction === targetSortDirection).sort((a, b) => { - if (newOrder.direction === "buy") { - return a.price > b.price ? 1 : -1; - } else { - return a.price > b.price ? -1 : 1; - } - }); - const orderType = newOrder.price ? "limit" : "market"; - if (reverseOrderbook.length === 0 && orderType === "market") { - throw new ContractError(`The first order must be limit type!`); - } - const newOrderTokenType = orderType === "market" && newOrder.direction === "buy" ? "dominent" : "trade"; - for (let i = 0; i < reverseOrderbook.length; i++) { - const order = reverseOrderbook[i]; - if (orderType === "limit" && order.price !== newOrder.price) { - continue; - } - const targetPrice = order.price; - const orderAmount = order.quantity; - const newOrderAmoumt = newOrderTokenType === "trade" ? newOrder.quantity : Math.floor(newOrder.quantity / targetPrice); - const targetAmout = orderAmount < newOrderAmoumt ? orderAmount : newOrderAmoumt; - totalTradePrice += targetPrice * targetAmout; - totalTradeVolume += targetAmout; - if (targetAmout === 0) { - break; - } - const dominentFee = Math.floor(targetAmout * targetPrice * feeRatio); - const tradeFee = Math.floor(targetAmout * feeRatio); - const dominentSwap = targetAmout * targetPrice - dominentFee; - const tradeSwap = targetAmout - tradeFee; - const buyer = newOrder.direction === "buy" ? newOrder : order; - const seller = newOrder.direction === "buy" ? order : newOrder; - transactions.push({ - tokenType: "dominent", - to: seller.creator, - quantity: dominentSwap - }); - transactions.push({ - tokenType: "trade", - to: buyer.creator, - quantity: tradeSwap - }); - if (selectedFeeRecvr) { - transactions.push({ - tokenType: "dominent", - to: selectedFeeRecvr, - quantity: dominentFee - }); - transactions.push({ - tokenType: "trade", - to: selectedFeeRecvr, - quantity: tradeFee - }); - } - order.quantity -= targetAmout; - if (order.quantity === 0) { - orderbook = orderbook.filter((v) => v.orderId !== order.orderId); - } - let userOrderInfos = userOrders[order.creator][newOrderPairId]; - let matchedOrderIdx = userOrderInfos.findIndex((value) => value.orderId === order.orderId); - userOrderInfos[matchedOrderIdx].quantity -= targetAmout; - if (userOrderInfos[matchedOrderIdx].quantity === 0) { - userOrders[order.creator][newOrderPairId] = userOrderInfos.filter((v) => v.orderId !== order.orderId); - } - newOrder.quantity -= newOrderTokenType === "trade" ? targetAmout : targetAmout * targetPrice; - } - if (orderType === "market" && newOrder.quantity !== 0) { - transactions.push({ - tokenType: newOrderTokenType, - to: newOrder.creator, - quantity: newOrder.quantity - }); - newOrder.quantity = 0; - } - if (orderType === "limit" && newOrder.quantity !== 0) { - orderbook.push({ ...newOrder }); - } - if (newOrder.quantity !== 0) { - if (userOrders[caller] === void 0) { - userOrders[caller] = {}; - } - if (userOrders[caller][newOrderPairId] === void 0) { - userOrders[caller][newOrderPairId] = []; - } - userOrders[caller][newOrderPairId].push({ ...newOrder }); - } - return { - newOrderbook: orderbook, - newUserOrders: userOrders, - transactions, - currentPrice: totalTradePrice / totalTradeVolume - }; - }; - - // src/thetAR/actions/write/deposit.ts - var deposit = async (state, action) => { - logger.error("Token: " + action.input.params.token); - logger.error("Amount: " + action.input.params.amount); - await SmartWeave.contracts.write(action.input.params.token, { - function: "transferFrom", - from: action.caller, - to: SmartWeave.contract.id, - amount: action.input.params.amount - }); - return { state }; - }; - - // src/thetAR/actions/write/cancelOrder.ts - var cancelOrder = async (state, action) => { - const param = action.input.params; - const orderId = param.orderId; - const pairId = param.pairId; - if (!isAddress(orderId)) { - throw new ContractError(`OrderId not found: ${param.orderId}!`); - } - if (!(param.pairId <= state.maxPairId && param.pairId >= 0)) { - throw new ContractError("PairId not valid!"); - } - const orderInfo2 = state.userOrders[action.caller][pairId].find((v) => v.orderId === orderId); - const pairInfo2 = state.pairInfos.find((i) => i.pairId === pairId); - if (!orderInfo2) { - throw new ContractError(`Cannot get access to pairId: ${pairId}!`); - } - if (!pairInfo2) { - throw new ContractError(`Pair info record not found: ${pairId}!`); - } - const tokenAddress = orderInfo2.direction === "buy" ? state.thetarTokenAddress : pairInfo2.tokenAddress; - const quantity = orderInfo2.direction === "buy" ? orderInfo2.price * orderInfo2.quantity : orderInfo2.quantity; - logger.error("CANCEL Returning tokens: " + quantity); - await SmartWeave.contracts.write(tokenAddress, { function: "transfer", to: action.caller, amount: quantity }); - let ordersForUser = state.userOrders[action.caller][pairId]; - state.userOrders[action.caller][pairId] = ordersForUser.filter((i) => i.orderId !== orderId); - let ordersForPair = state.orderInfos[pairId].orders; - state.orderInfos[pairId].orders = ordersForPair.filter((i) => i.orderId !== orderId); - return { state }; - }; - - // src/thetAR/actions/write/addTokenHash.ts - var addTokenHash = async (state, action) => { - const param = action.input.params; - const hash = param.hash; - if (action.caller !== state.owner) { - throw new ContractError("You have no permission to modify hash list!"); - } - state.tokenSrcTemplateHashs.push(hash); - return { state }; - }; - - // src/thetAR/actions/read/pairInfo.ts - var pairInfo = async (state, action) => { - const param = action.input.params; - let pairId = param.pairId; - let result; - if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { - throw new ContractError(`Invalid pairId!`); - } - result = state.pairInfos.filter((i) => i.pairId === pairId)[0]; - return { result }; - }; - - // src/thetAR/actions/read/pairInfos.ts - var pairInfos = async (state, action) => { - let result; - result = state.pairInfos; - return { result }; - }; - - // src/thetAR/actions/read/orderInfos.ts - var orderInfos = async (state, action) => { - let result; - result = state.orderInfos; - return { result }; - }; - - // src/thetAR/actions/read/orderInfo.ts - var orderInfo = async (state, action) => { - const param = action.input.params; - let pairId = param.pairId; - let result; - if (!Number.isInteger(pairId) || pairId < 0 || pairId > state.maxPairId) { - throw new ContractError(`Invalid pairId!`); - } - result = state.orderInfos[pairId]; - return { result }; - }; - - // src/thetAR/actions/read/userOrder.ts - var userOrder = async (state, action) => { - const param = action.input.params; - let address = param.address; - let result; - if (!isAddress(address)) { - throw new ContractError(`Invalid wallet address!`); - } - result = state.userOrders[address]; - return { result }; - }; - - // src/thetAR/contract.ts - async function handle(state, action) { - const func = action.input.function; - switch (func) { - case "addPair": - return await addPair(state, action); - case "createOrder": - return await createOrder(state, action); - case "cancelOrder": - return await cancelOrder(state, action); - case "pairInfo": - return await pairInfo(state, action); - case "pairInfos": - return await pairInfos(state, action); - case "orderInfo": - return await orderInfo(state, action); - case "orderInfos": - return await orderInfos(state, action); - case "addTokenHash": - return await addTokenHash(state, action); - case "userOrder": - return await userOrder(state, action); - case "deposit": - return await deposit(state, action); - default: - throw new ContractError(`No function supplied or function not recognised: "${func}"`); - } - } -})(); diff --git a/src/__tests__/integration/data/wrc-20/src/action.rs b/src/__tests__/integration/data/wrc-20/src/action.rs deleted file mode 100644 index 64b06ef2..00000000 --- a/src/__tests__/integration/data/wrc-20/src/action.rs +++ /dev/null @@ -1,56 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use warp_wasm_utils::contract_utils::handler_result::HandlerResult; -use crate::error::ContractError; -use crate::state::State; - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase", tag = "function")] -pub enum Action { - Transfer { - to: String, - amount: u64, - }, - TransferFrom { - from: String, - to: String, - amount: u64 - }, - BalanceOf { - target: String - }, - TotalSupply { - }, - Approve { - spender: String, - amount: u64, - }, - Allowance { - owner: String, - spender: String - }, - Evolve { - value: String - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase", untagged)] -pub enum QueryResponseMsg { - Balance { - ticker: String, - target: String, - balance: u64, - }, - Allowance { - ticker: String, - owner: String, - spender: String, - allowance: u64, - }, - TotalSupply { - value: u64 - } -} - -pub type ActionResult = Result, ContractError>; diff --git a/src/__tests__/integration/data/wrc-20/src/actions/allowances.rs b/src/__tests__/integration/data/wrc-20/src/actions/allowances.rs deleted file mode 100644 index e052350f..00000000 --- a/src/__tests__/integration/data/wrc-20/src/actions/allowances.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::state::State; -use std::collections::HashMap; -use warp_wasm_utils::contract_utils::handler_result::HandlerResult; -use crate::action::{QueryResponseMsg::Allowance, ActionResult}; -use warp_wasm_utils::contract_utils::handler_result::HandlerResult::QueryResponse; -use warp_wasm_utils::contract_utils::js_imports::{Transaction}; - -pub fn allowance(state: State, owner: String, spender: String) -> ActionResult { - Ok(QueryResponse( - Allowance { - ticker: state.symbol, - allowance: __get_allowance(&state.allowances, &owner, &spender), - owner, - spender - } - )) -} - -pub fn approve(mut state: State, spender: String, amount: u64) -> ActionResult { - let caller = Transaction::owner(); - __set_allowance(&mut state.allowances, caller, spender, amount); - Ok(HandlerResult::NewState(state)) -} - -//Following: https://users.rust-lang.org/t/use-of-pub-for-non-public-apis/40480 -// Not a part of the contract API - used internally within the crate. -#[doc(hidden)] -pub fn __set_allowance(allowances: &mut HashMap>, owner: String, spender: String, amount: u64) { - if amount > 0 { - *allowances - .entry(owner) - .or_default() - .entry(spender) - .or_default() = amount; - } else { //Prune state - match allowances.get_mut(&owner) { - Some(spender_allowances) => { - spender_allowances.remove(&spender); - if spender_allowances.is_empty() { - allowances.remove(&owner); - } - } - None => () - } - } -} - -//Following: https://users.rust-lang.org/t/use-of-pub-for-non-public-apis/40480 -// Not a part of the contract API - used internally within the crate. -#[doc(hidden)] -pub fn __get_allowance(allowances: &HashMap>, owner: &String, spender: &String) -> u64 { - return *allowances - .get(owner) - .map_or(&0, |spenders| { - spenders.get(spender).unwrap_or(&0) - }); -} diff --git a/src/__tests__/integration/data/wrc-20/src/actions/balance.rs b/src/__tests__/integration/data/wrc-20/src/actions/balance.rs deleted file mode 100644 index bb0d72f0..00000000 --- a/src/__tests__/integration/data/wrc-20/src/actions/balance.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::state::State; -use crate::action::{QueryResponseMsg::Balance,QueryResponseMsg::TotalSupply, ActionResult}; -use warp_wasm_utils::contract_utils::handler_result::HandlerResult::QueryResponse; - -pub fn balance_of(state: State, target: String) -> ActionResult { - Ok(QueryResponse( - Balance { - balance: *state.balances.get( & target).unwrap_or(&0), - ticker: state.symbol, - target - } - )) -} - -pub fn total_supply(state: State) -> ActionResult { - Ok(QueryResponse( - TotalSupply { - value: state.total_supply - } - )) -} - - diff --git a/src/__tests__/integration/data/wrc-20/src/actions/evolve.rs b/src/__tests__/integration/data/wrc-20/src/actions/evolve.rs deleted file mode 100644 index 0492dcd7..00000000 --- a/src/__tests__/integration/data/wrc-20/src/actions/evolve.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::error::ContractError::{EvolveNotAllowed, OnlyOwnerCanEvolve}; -use crate::state::{State}; -use warp_wasm_utils::contract_utils::js_imports::Transaction; -use crate::action::ActionResult; -use warp_wasm_utils::contract_utils::handler_result::HandlerResult; - -pub fn evolve(mut state: State, value: String) -> ActionResult { - match state.can_evolve { - Some(can_evolve) => if can_evolve && state.owner == Transaction::owner() { - state.evolve = Option::from(value); - Ok(HandlerResult::NewState(state)) - } else { - Err(OnlyOwnerCanEvolve) - }, - None => Err(EvolveNotAllowed), - } -} diff --git a/src/__tests__/integration/data/wrc-20/src/actions/mod.rs b/src/__tests__/integration/data/wrc-20/src/actions/mod.rs deleted file mode 100644 index 6d5022da..00000000 --- a/src/__tests__/integration/data/wrc-20/src/actions/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod evolve; -pub mod balance; -pub mod transfers; -pub mod allowances; - diff --git a/src/__tests__/integration/data/wrc-20/src/actions/transfers.rs b/src/__tests__/integration/data/wrc-20/src/actions/transfers.rs deleted file mode 100644 index e84e6c63..00000000 --- a/src/__tests__/integration/data/wrc-20/src/actions/transfers.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::error::ContractError::{CallerBalanceNotEnough, CallerAllowanceNotEnough}; -use crate::actions::allowances::{__set_allowance, __get_allowance}; -use crate::state::State; -use crate::action::ActionResult; -use warp_wasm_utils::contract_utils::handler_result::HandlerResult; -use warp_wasm_utils::contract_utils::js_imports::{SmartWeave}; - -pub fn transfer(state: State, to: String, amount: u64) -> ActionResult { - let caller = SmartWeave::caller(); - return _transfer(state, caller, to, amount); -} - -pub fn transfer_from(mut state: State, from: String, to: String, amount: u64) -> ActionResult { - let caller = SmartWeave::caller(); - - //Checking allowance - let allowance = __get_allowance(&state.allowances, &from, &caller); - - if allowance < amount { - return Err(CallerAllowanceNotEnough(allowance)); - } - - __set_allowance(&mut state.allowances, from.to_owned(), caller, allowance - amount); - - return _transfer(state, from, to, amount); -} - -fn _transfer(mut state: State, from: String, to: String, amount: u64) -> ActionResult { - // Checking if caller has enough funds - let balances = &mut state.balances; - let from_balance = *balances.get(&from).unwrap_or(&0); - if from_balance < amount { - return Err(CallerBalanceNotEnough(from_balance)); - } - - // Update caller balance or prune state if the new value is 0 - if from_balance - amount == 0 { - balances.remove(&from); - } else { - balances.insert(from, from_balance - amount); - } - - // Update target balance - *balances.entry(to).or_insert(0) += amount; - - Ok(HandlerResult::NewState(state)) -} diff --git a/src/__tests__/integration/data/wrc-20/src/contract.rs b/src/__tests__/integration/data/wrc-20/src/contract.rs deleted file mode 100644 index f623d80c..00000000 --- a/src/__tests__/integration/data/wrc-20/src/contract.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::action::{Action, ActionResult}; -use crate::actions::transfers::transfer; -use crate::actions::transfers::transfer_from; -use crate::actions::balance::balance_of; -use crate::actions::balance::total_supply; -use crate::actions::allowances::approve; -use crate::actions::allowances::allowance; -use crate::actions::evolve::evolve; -use warp_wasm_utils::contract_utils::js_imports::{Block, Contract, log, SmartWeave, Transaction}; -use crate::state::State; - -pub async fn handle(current_state: State, action: Action) -> ActionResult { - - //Example of accessing functions imported from js: - log("log from contract"); - log(&("Transaction::id()".to_owned() + &Transaction::id())); - log(&("Transaction::owner()".to_owned() + &Transaction::owner())); - log(&("Transaction::target()".to_owned() + &Transaction::target())); - - log(&("Block::height()".to_owned() + &Block::height().to_string())); - log(&("Block::indep_hash()".to_owned() + &Block::indep_hash())); - log(&("Block::timestamp()".to_owned() + &Block::timestamp().to_string())); - - log(&("Contract::id()".to_owned() + &Contract::id())); - log(&("Contract::owner()".to_owned() + &Contract::owner())); - - log(&("SmartWeave::caller()".to_owned() + &SmartWeave::caller())); - - match action { - Action::Transfer { to, amount } => transfer(current_state, to, amount), - Action::TransferFrom { from, to, amount } => transfer_from(current_state, from, to, amount), - Action::BalanceOf { target } => balance_of(current_state, target), - Action::TotalSupply { } => total_supply(current_state), - Action::Approve { spender, amount } => approve(current_state, spender, amount), - Action::Allowance { owner, spender } => allowance(current_state, owner, spender), - Action::Evolve { value } => evolve(current_state, value), - } -} diff --git a/src/__tests__/integration/data/wrc-20/src/contract_utils/README.md b/src/__tests__/integration/data/wrc-20/src/contract_utils/README.md deleted file mode 100644 index dadde3be..00000000 --- a/src/__tests__/integration/data/wrc-20/src/contract_utils/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# contract_utils module - -This is a module with boilerplate code for each SmartWeave RUST contract. -**Please don't modify it unless you 100% know what you are doing!** diff --git a/src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs b/src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs deleted file mode 100644 index d6b04629..00000000 --- a/src/__tests__/integration/data/wrc-20/src/contract_utils/entrypoint.rs +++ /dev/null @@ -1,107 +0,0 @@ -///////////////////////////////////////////////////// -/////////////// DO NOT MODIFY THIS FILE ///////////// -///////////////////////////////////////////////////// - -use std::cell::RefCell; - -use serde_json::Error; -use wasm_bindgen::prelude::*; - -use crate::action::{Action, QueryResponseMsg}; -use crate::contract; -use warp_wasm_utils::contract_utils::handler_result::HandlerResult; -use crate::error::ContractError; -use crate::state::State; - -/* -Note: in order do optimize communication between host and the WASM module, -we're storing the state inside the WASM module (for the time of state evaluation). -This allows to reduce the overhead of passing the state back and forth -between the host and module with each contract interaction. -In case of bigger states this overhead can be huge. -Same approach has been implemented for the AssemblyScript version. - -So the flow (from the SDK perspective) is: -1. SDK calls exported WASM module function "initState" (with lastly cached state or initial state, -if cache is empty) - which initializes the state in the WASM module. -2. SDK calls "handle" function for each of the interaction. -If given interaction was modifying the state - it is updated inside the WASM module -- but not returned to host. -3. Whenever SDK needs to know the current state (eg. in order to perform -caching or to simply get its value after evaluating all of the interactions) -- it calls WASM's module "currentState" function. - -The handle function by default does not return the new state - -it only updates it in the WASM module. -The handle function returns a value only in case of error -or calling a "view" function. - -In the future this might also allow to enhance the inner-contracts communication -- e.g. if the execution network will store the state of the contracts - as the WASM contract module memory -- it would allow to read other contract's state "directly" from WASM module memory. -*/ - -// inspired by https://github.com/dfinity/examples/blob/master/rust/basic_dao/src/basic_dao/src/lib.rs#L13 -thread_local! { - static STATE: RefCell = RefCell::default(); -} - -#[wasm_bindgen()] -pub async fn handle(interaction: JsValue) -> Option { - let result: Result, ContractError>; - let action: Result = interaction.into_serde(); - - if action.is_err() { - // cannot pass any data from action.error here - ends up with - // "FnOnce called more than once" error from wasm-bindgen for - // "foreign_call" testcase. - result = Err(ContractError::RuntimeError( - "Error while parsing input".to_string(), - )); - } else { - // not sure about clone here - let current_state = STATE.with(|service| service.borrow().clone()); - - result = contract::handle(current_state, action.unwrap()).await; - } - - if let Ok(HandlerResult::NewState(state)) = result { - STATE.with(|service| service.replace(state)); - None - } else { - Some(JsValue::from_serde(&result).unwrap()) - } -} - -#[wasm_bindgen(js_name = initState)] -pub fn init_state(state: &JsValue) { - let state_parsed: State = state.into_serde().unwrap(); - - STATE.with(|service| service.replace(state_parsed)); -} - -#[wasm_bindgen(js_name = currentState)] -pub fn current_state() -> JsValue { - // not sure if that's deterministic - which is very important for the execution network. - // TODO: perf - according to docs: - // "This is unlikely to be super speedy so it's not recommended for large payload" - // - we should minimize calls to from_serde - let current_state = STATE.with(|service| service.borrow().clone()); - JsValue::from_serde(¤t_state).unwrap() -} - -#[wasm_bindgen()] -pub fn version() -> i32 { - return 1; -} - -// Workaround for now to simplify type reading without as/loader or wasm-bindgen -// 1 = assemblyscript -// 2 = rust -// 3 = go -// 4 = swift -// 5 = c -#[wasm_bindgen] -pub fn lang() -> i32 { - return 2; -} diff --git a/src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs b/src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs deleted file mode 100644 index a6ac3295..00000000 --- a/src/__tests__/integration/data/wrc-20/src/contract_utils/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -///////////////////////////////////////////////////// -/////////////// DO NOT MODIFY THIS FILE ///////////// -///////////////////////////////////////////////////// - -pub mod entrypoint; diff --git a/src/__tests__/integration/data/wrc-20/src/error.rs b/src/__tests__/integration/data/wrc-20/src/error.rs deleted file mode 100644 index ce3ea9cf..00000000 --- a/src/__tests__/integration/data/wrc-20/src/error.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::Serialize; - -#[derive(Serialize)] -pub enum ContractError { - RuntimeError(String), - CallerBalanceNotEnough(u64), - CallerAllowanceNotEnough(u64), - OnlyOwnerCanEvolve, - EvolveNotAllowed -} diff --git a/src/__tests__/integration/data/wrc-20/src/lib.rs b/src/__tests__/integration/data/wrc-20/src/lib.rs deleted file mode 100644 index a6c5bfb5..00000000 --- a/src/__tests__/integration/data/wrc-20/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod state; -mod action; -mod error; -mod actions; -mod contract; -pub mod contract_utils; diff --git a/src/__tests__/integration/data/wrc-20/src/state.rs b/src/__tests__/integration/data/wrc-20/src/state.rs deleted file mode 100644 index 6f802523..00000000 --- a/src/__tests__/integration/data/wrc-20/src/state.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Default)] -#[serde(rename_all = "camelCase")] -pub struct State { - pub symbol: String, - pub name: Option, - pub decimals: u8, - pub total_supply: u64, - pub balances: HashMap, - pub allowances: HashMap>, - - //Evolve interface - pub owner: String, - pub evolve: Option, - pub can_evolve: Option -} diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 69aa4a69..4b9ad476 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -865,7 +865,7 @@ export class HandlerBasedContract implements Contract { } getUncommittedState(contractTxId: string): EvalStateResult { - return (this.getRoot() as HandlerBasedContract)._uncommittedStates.get(contractTxId); + return this.getRoot()._uncommittedStates.get(contractTxId); } setUncommittedState(contractTxId: string, result: EvalStateResult): void { @@ -883,6 +883,9 @@ export class HandlerBasedContract implements Contract { async commitStates(interaction: GQLNodeInterface): Promise { const uncommittedStates = this.getRoot()._uncommittedStates; try { + // i.e. if more than root contract state is in uncommitted state + // - without this check, we would effectively cache state for each evaluated interaction + // - which is not storage-effective if (uncommittedStates.size > 1) { for (const [k, v] of uncommittedStates) { await this.warp.stateEvaluator.putInCache(k, interaction, v); diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index c0660931..a1c059b1 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -65,6 +65,7 @@ export abstract class AbstractContractHandler implements HandlerApi implements HandlerApi( - executionContext: ExecutionContext, - currentResult: EvalStateResult, - interactionTx: GQLNodeInterface - ) { + protected assignReadContractState(executionContext: ExecutionContext, interactionTx: GQLNodeInterface) { this.swGlobal.contracts.readContractState = async (contractTxId: string, returnValidity?: boolean) => { this.logger.debug('swGlobal.readContractState call:', { from: this.contractDefinition.txId, @@ -120,19 +117,6 @@ export abstract class AbstractContractHandler implements HandlerApi extends AbstractContractHandler { const stateCopy = deepCopy(currentResult.state); this.swGlobal._activeTx = interactionTx; this.swGlobal.caller = interaction.caller; // either contract tx id (for internal writes) or transaction.owner - this.assignReadContractState(executionContext, currentResult, interactionTx); + + this.assignReadContractState(executionContext, interactionTx); this.assignViewContractState(executionContext); this.assignWrite(executionContext); this.assignRefreshState(executionContext); - const { warp } = executionContext; - await this.swGlobal.kv.open(); const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]); diff --git a/src/core/modules/impl/handler/WasmHandlerApi.ts b/src/core/modules/impl/handler/WasmHandlerApi.ts index ac104618..9e6deb0f 100644 --- a/src/core/modules/impl/handler/WasmHandlerApi.ts +++ b/src/core/modules/impl/handler/WasmHandlerApi.ts @@ -30,7 +30,7 @@ export class WasmHandlerApi extends AbstractContractHandler { this.swGlobal.gasLimit = executionContext.evaluationOptions.gasLimit; this.swGlobal.gasUsed = 0; - this.assignReadContractState(executionContext, currentResult, interactionTx); + this.assignReadContractState(executionContext, interactionTx); this.assignViewContractState(executionContext); this.assignWrite(executionContext);