diff --git a/lib/externalApis/tenderdash/blockchainListener/BlockchainListener.js b/lib/externalApis/tenderdash/BlockchainListener.js similarity index 62% rename from lib/externalApis/tenderdash/blockchainListener/BlockchainListener.js rename to lib/externalApis/tenderdash/BlockchainListener.js index df489343a..fe3b3d2d9 100644 --- a/lib/externalApis/tenderdash/blockchainListener/BlockchainListener.js +++ b/lib/externalApis/tenderdash/BlockchainListener.js @@ -1,5 +1,3 @@ -const crypto = require('crypto'); - const EventEmitter = require('events'); const TX_QUERY = 'tm.event = \'Tx\''; @@ -27,15 +25,6 @@ class BlockchainListener extends EventEmitter { return `transaction:${transactionHashString}`; } - /** - * - * @param transactionHashString - * @return {string} - */ - static getTransactionAddedToTheBlockEventName(transactionHashString) { - return `blockTransactionAdded:${transactionHashString}`; - } - /** * Subscribe to blocks and transaction results */ @@ -54,23 +43,7 @@ class BlockchainListener extends EventEmitter { // Emit blocks and contained transactions this.wsClient.subscribe(NEW_BLOCK_QUERY); - this.wsClient.on(NEW_BLOCK_QUERY, (message) => { - this.emit(EVENTS.NEW_BLOCK, message); - - // Emit transaction hashes from block - message.data.value.block.data.txs.forEach((base64tx) => { - const transaction = Buffer.from(base64tx, 'base64'); - const hashString = crypto.createHash('sha256') - .update(transaction) - .digest() - .toString('hex'); - - this.emit( - BlockchainListener.getTransactionAddedToTheBlockEventName(hashString), - transaction, - ); - }); - }); + this.wsClient.on(NEW_BLOCK_QUERY, message => this.emit(EVENTS.NEW_BLOCK, message)); } } diff --git a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment.js b/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment.js deleted file mode 100644 index 25909f924..000000000 --- a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment.js +++ /dev/null @@ -1,54 +0,0 @@ -const BlockchainListener = require('../BlockchainListener'); - -/** - * @typedef {waitForTransactionCommitment} - * @param {BlockchainListener} blockchainListener - * @param {string} hashString - * @return {{ - * promise: Promise, - * detach: Function - * }} - */ -function waitForTransactionCommitment(blockchainListener, hashString) { - const txInBlockTopic = BlockchainListener - .getTransactionAddedToTheBlockEventName(hashString.toLowerCase()); - - let txInBlockHandler; - let newBlockHandler; - - // Note that this will resolve only after two blocks. That is because the first block will - // flip the 'seenTransaction' toggle to true, and transaction will become provable - // only on the next block after the block it was included into - const promise = new Promise(((resolve) => { - let seenTransaction = false; - - txInBlockHandler = () => { - seenTransaction = true; - }; - - newBlockHandler = () => { - if (!seenTransaction) { - return; - } - - blockchainListener.off(BlockchainListener.EVENTS.NEW_BLOCK, newBlockHandler); - - resolve(); - }; - - blockchainListener.once(txInBlockTopic, txInBlockHandler); - blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, newBlockHandler); - })); - - const detach = () => { - blockchainListener.off(txInBlockTopic, txInBlockHandler); - blockchainListener.off(BlockchainListener.EVENTS.NEW_BLOCK, newBlockHandler); - }; - - return { - promise, - detach, - }; -} - -module.exports = waitForTransactionCommitment; diff --git a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.js b/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.js deleted file mode 100644 index f00704ae6..000000000 --- a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.js +++ /dev/null @@ -1,69 +0,0 @@ -const TransactionWaitPeriodExceededError = require('../../../../errors/TransactionWaitPeriodExceededError'); -const TransactionErrorResult = require('./transactionResult/TransactionErrorResult'); - -/** - * @param {waitForTransactionResult} waitForTransactionResult - * @param {waitForTransactionCommitment} waitForTransactionCommitment - * @return {waitForTransactionToBeProvable} - */ -function waitForTransactionToBeProvableFactory( - waitForTransactionResult, - waitForTransactionCommitment, -) { - /** - * Returns result for a transaction or rejects after a timeout - * - * @typedef {waitForTransactionToBeProvable} - * @param {BlockchainListener} blockchainListener - * @param {string} hashString - transaction hash to resolve data for - * @param {number} [timeout] - timeout to reject after - * @return {Promise} - */ - function waitForTransactionToBeProvable(blockchainListener, hashString, timeout = 60000) { - // Wait for transaction result - const { - promise: transactionResultPromise, - detach: detachTransactionResult, - } = waitForTransactionResult(blockchainListener, hashString); - - // Wait for transaction is committed to a block and proofs are available - const { - promise: transactionCommitmentPromise, - detach: detachTransactionCommitment, - } = waitForTransactionCommitment(blockchainListener, hashString); - - return Promise.race([ - - // Wait for transaction results and commitment - - Promise.all([ - transactionResultPromise, - transactionCommitmentPromise, - ]).then(([transactionResult]) => transactionResult) - .catch((e) => { - // Stop waiting for next block and return transaction error result - if (e instanceof TransactionErrorResult) { - return Promise.resolve(e); - } - - return Promise.reject(e); - }), - - // Throw wait period exceeded error after timeout - - new Promise((resolve, reject) => { - setTimeout(() => { - // Detaching handlers - detachTransactionResult(); - detachTransactionCommitment(); - - reject(new TransactionWaitPeriodExceededError(hashString)); - }, timeout); - }), - ]); - } - - return waitForTransactionToBeProvable; -} - -module.exports = waitForTransactionToBeProvableFactory; diff --git a/lib/externalApis/tenderdash/waitForHeightFactory.js b/lib/externalApis/tenderdash/waitForHeightFactory.js new file mode 100644 index 000000000..1787c316d --- /dev/null +++ b/lib/externalApis/tenderdash/waitForHeightFactory.js @@ -0,0 +1,41 @@ +const BlockchainListener = require('./BlockchainListener'); + +/** + * @param {BlockchainListener} blockchainListener + */ +function waitForHeightFactory(blockchainListener) { + let currentHeight = 0; + + blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, (message) => { + currentHeight = parseInt(message.data.value.block.header.height, 10); + }); + + /** + * @typedef {waitForHeight} + * @param {number} height + * @return {Promise} + */ + function waitForHeight(height) { + return new Promise((resolve) => { + if (currentHeight >= height) { + resolve(); + + return; + } + + const handler = () => { + if (currentHeight >= height) { + blockchainListener.off(BlockchainListener.EVENTS.NEW_BLOCK, handler); + + resolve(); + } + }; + + blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, handler); + }); + } + + return waitForHeight; +} + +module.exports = waitForHeightFactory; diff --git a/lib/externalApis/tenderdash/waitForTransactionToBeProvable/getExistingTransactionResult.js b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/getExistingTransactionResult.js new file mode 100644 index 000000000..25d6f7c52 --- /dev/null +++ b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/getExistingTransactionResult.js @@ -0,0 +1,45 @@ +const TransactionOkResult = require('./transactionResult/TransactionOkResult'); +const TransactionErrorResult = require('./transactionResult/TransactionErrorResult'); +const RPCError = require('../../../rpcServer/RPCError'); + +/** + * @param {RpcClient} rpcClient + * @return {getExistingTransactionResult} + */ +function getExistingTransactionResultFactory(rpcClient) { + /** + * @typedef {getExistingTransactionResult} + * @param {string} hashString + * @return {Promise} + */ + async function getExistingTransactionResult(hashString) { + const hash = Buffer.from(hashString, 'hex'); + + const params = { hash: hash.toString('base64') }; + + const { result, error } = await rpcClient.request('tx', params); + + // Handle JSON RPC error + if (error) { + throw new RPCError( + error.code || -32602, + error.message || 'Internal error', + error.data, + ); + } + + const TransactionResultClass = result.tx_result.code === 0 + ? TransactionOkResult + : TransactionErrorResult; + + return new TransactionResultClass( + result.tx_result, + parseInt(result.height, 10), + Buffer.from(result.tx, 'base64'), + ); + } + + return getExistingTransactionResult; +} + +module.exports = getExistingTransactionResultFactory; diff --git a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/AbstractTransactionResult.js b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/AbstractTransactionResult.js similarity index 56% rename from lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/AbstractTransactionResult.js rename to lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/AbstractTransactionResult.js index 1cf57278e..8771fdb81 100644 --- a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/AbstractTransactionResult.js +++ b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/AbstractTransactionResult.js @@ -1,10 +1,12 @@ class AbstractTransactionResult { /** - * @param {Object} deliverResult + * @param {Object} result + * @param {number} height * @param {Buffer} transaction */ - constructor(deliverResult, transaction) { - this.deliverResult = deliverResult; + constructor(result, height, transaction) { + this.deliverResult = result; + this.height = height; this.transaction = transaction; } @@ -13,10 +15,19 @@ class AbstractTransactionResult { * * @return {Object} */ - getDeliverResult() { + getResult() { return this.deliverResult; } + /** + * Get transaction block height + * + * @return {number} + */ + getHeight() { + return this.height; + } + /** * Get transaction * diff --git a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult.js b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult.js similarity index 100% rename from lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult.js rename to lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult.js diff --git a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionOkResult.js b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionOkResult.js similarity index 100% rename from lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionOkResult.js rename to lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionOkResult.js diff --git a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult.js b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult.js similarity index 75% rename from lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult.js rename to lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult.js index 846cdad61..602cb320a 100644 --- a/lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult.js +++ b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult.js @@ -16,23 +16,25 @@ function waitForTransactionResult(blockchainListener, hashString) { let handler; - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve) => { handler = ({ data: { value: { TxResult: txResult } } }) => { blockchainListener.off(topic, handler); - const { result: deliverResult, tx } = txResult; + const { result: deliverResult, tx, height } = txResult; const txBuffer = Buffer.from(tx, 'base64'); + let TransactionResultClass = TransactionOkResult; if (deliverResult && deliverResult.code !== undefined && deliverResult.code !== 0) { - // If a transaction result is error we don't need to wait for next block - return reject( - new TransactionErrorResult(deliverResult, txBuffer), - ); + TransactionResultClass = TransactionErrorResult; } - return resolve( - new TransactionOkResult(deliverResult, txBuffer), + resolve( + new TransactionResultClass( + deliverResult, + parseInt(height, 10), + txBuffer, + ), ); }; diff --git a/lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.js b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.js new file mode 100644 index 000000000..9fab7dd87 --- /dev/null +++ b/lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.js @@ -0,0 +1,77 @@ +const TransactionWaitPeriodExceededError = require('../../../errors/TransactionWaitPeriodExceededError'); +const TransactionOkResult = require('./transactionResult/TransactionOkResult'); + +/** + * @param {waitForTransactionResult} waitForTransactionResult + * @param {getExistingTransactionResult} getExistingTransactionResult + * @param {waitForHeight} waitForHeight + * @return {waitForTransactionToBeProvable} + */ +function waitForTransactionToBeProvableFactory( + waitForTransactionResult, + getExistingTransactionResult, + waitForHeight, +) { + /** + * Returns result for a transaction or rejects after a timeout + * + * @typedef {waitForTransactionToBeProvable} + * @param {BlockchainListener} blockchainListener + * @param {string} hashString - transaction hash to resolve data for + * @param {number} [timeout] - timeout to reject after + * @return {Promise} + */ + function waitForTransactionToBeProvable(blockchainListener, hashString, timeout = 60000) { + const { + promise: waitForTransactionResultPromise, + detach: detachTransactionResult, + } = waitForTransactionResult(blockchainListener, hashString); + + const existingTransactionResultPromise = getExistingTransactionResult(hashString); + + const transactionResultPromise = Promise.race([ + // Try to fetch existing tx result + existingTransactionResultPromise.then((result) => { + // Do not wait for upcoming result if existing is present + detachTransactionResult(); + + return result; + }).catch((error) => { + // Do not resolve promise and wait for results if transaction is not found + if (error.code === -32603 && error.data === `tx (${hashString}) not found`) { + return new Promise(() => {}); + } + + return Promise.reject(error); + }), + + // Wait for upcoming results if transaction result doesn't not exist yet + waitForTransactionResultPromise, + ]); + + return Promise.race([ + // Wait for transaction results and commitment + transactionResultPromise.then(async (result) => { + if (result instanceof TransactionOkResult) { + await waitForHeight(result.getHeight() + 1); + } + + return result; + }), + + // Throw wait period exceeded error after timeout + new Promise((resolve, reject) => { + setTimeout(() => { + // Detaching handlers + detachTransactionResult(); + + reject(new TransactionWaitPeriodExceededError(hashString)); + }, timeout); + }), + ]); + } + + return waitForTransactionToBeProvable; +} + +module.exports = waitForTransactionToBeProvableFactory; diff --git a/lib/grpcServer/handlers/platform/platformHandlersFactory.js b/lib/grpcServer/handlers/platform/platformHandlersFactory.js index 2bd97c8da..e660a9a3a 100644 --- a/lib/grpcServer/handlers/platform/platformHandlersFactory.js +++ b/lib/grpcServer/handlers/platform/platformHandlersFactory.js @@ -68,9 +68,10 @@ const waitForStateTransitionResultHandlerFactory = require( ); const fetchProofForStateTransitionFactory = require('../../../externalApis/drive/fetchProofForStateTransitionFactory'); -const waitForTransactionToBeProvableFactory = require('../../../externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory'); -const waitForTransactionResult = require('../../../externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult'); -const waitForTransactionCommitment = require('../../../externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment'); +const waitForTransactionToBeProvableFactory = require('../../../externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory'); +const waitForTransactionResult = require('../../../externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult'); +const waitForHeightFactory = require('../../../externalApis/tenderdash/waitForHeightFactory'); +const getExistingTransactionResultFactory = require('../../../externalApis/tenderdash/waitForTransactionToBeProvable/getExistingTransactionResult'); /** * @param {jaysonClient} rpcClient @@ -189,9 +190,16 @@ function platformHandlersFactory( // waitForStateTransitionResult const fetchProofForStateTransition = fetchProofForStateTransitionFactory(driveClient); + const getExistingTransactionResult = getExistingTransactionResultFactory( + rpcClient, + ); + + const waitForHeight = waitForHeightFactory(blockchainListener); + const waitForTransactionToBeProvable = waitForTransactionToBeProvableFactory( waitForTransactionResult, - waitForTransactionCommitment, + getExistingTransactionResult, + waitForHeight, ); const waitForStateTransitionResultHandler = waitForStateTransitionResultHandlerFactory( diff --git a/lib/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js b/lib/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js index 85203c35d..1e70c1765 100644 --- a/lib/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js +++ b/lib/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js @@ -18,7 +18,7 @@ const { const cbor = require('cbor'); const TransactionWaitPeriodExceededError = require('../../../errors/TransactionWaitPeriodExceededError'); -const TransactionErrorResult = require('../../../externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult'); +const TransactionErrorResult = require('../../../externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult'); /** * @@ -98,7 +98,7 @@ function waitForStateTransitionResultHandlerFactory( const response = new WaitForStateTransitionResultResponse(); if (result instanceof TransactionErrorResult) { - const error = createStateTransitionDeliverError(result.getDeliverResult()); + const error = createStateTransitionDeliverError(result.getResult()); response.setError(error); diff --git a/scripts/api.js b/scripts/api.js index 76287fa85..b18012c91 100644 --- a/scripts/api.js +++ b/scripts/api.js @@ -29,7 +29,7 @@ const rpcServer = require('../lib/rpcServer/server'); const DriveClient = require('../lib/externalApis/drive/DriveClient'); const insightAPI = require('../lib/externalApis/insight'); const dashCoreRpcClient = require('../lib/externalApis/dashcore/rpc'); -const BlockchainListener = require('../lib/externalApis/tenderdash/blockchainListener/BlockchainListener'); +const BlockchainListener = require('../lib/externalApis/tenderdash/BlockchainListener'); const DriveStateRepository = require('../lib/dpp/DriveStateRepository'); const coreHandlersFactory = require( diff --git a/test/integration/externalApis/tenderdash/BlockchainListener.spec.js b/test/integration/externalApis/tenderdash/BlockchainListener.spec.js index a6f1d8138..08dbf3a2a 100644 --- a/test/integration/externalApis/tenderdash/BlockchainListener.spec.js +++ b/test/integration/externalApis/tenderdash/BlockchainListener.spec.js @@ -1,17 +1,15 @@ const EventEmitter = require('events'); const crypto = require('crypto'); -const BlockchainListener = require('../../../../lib/externalApis/tenderdash/blockchainListener/BlockchainListener'); + +const BlockchainListener = require('../../../../lib/externalApis/tenderdash/BlockchainListener'); describe('BlockchainListener', () => { let sinon; let wsClientMock; let blockchainListener; let txQueryMessageMock; - let blockMessageMock; let transactionHash; - let txBase64Mock; - let txBufferMock; - let emptyBlockMessage; + let blockMessageMock; beforeEach(function beforeEach() { ({ sinon } = this); @@ -24,8 +22,7 @@ describe('BlockchainListener', () => { sinon.spy(blockchainListener, 'off'); sinon.spy(blockchainListener, 'emit'); - txBase64Mock = 'aaaa'; - txBufferMock = Buffer.from(txBase64Mock, 'base64'); + const txBase64Mock = 'aaaa'; transactionHash = crypto.createHash('sha256') .update(Buffer.from(txBase64Mock, 'base64')) .digest() @@ -38,18 +35,6 @@ describe('BlockchainListener', () => { }; blockMessageMock = { - data: { - value: { - block: { - data: { - txs: [txBase64Mock], - }, - }, - }, - }, - }; - - emptyBlockMessage = { data: { value: { block: { @@ -63,19 +48,12 @@ describe('BlockchainListener', () => { }); describe('.getTransactionEventName', () => { - it('should work', () => { + it('should return event name', () => { const topic = BlockchainListener.getTransactionEventName(transactionHash); expect(topic).to.be.equal(`transaction:${transactionHash}`); }); }); - describe('.getTransactionAddedToTheBlockEventName', () => { - it('should work', () => { - const topic = BlockchainListener.getTransactionAddedToTheBlockEventName(transactionHash); - expect(topic).to.be.equal(`blockTransactionAdded:${transactionHash}`); - }); - }); - describe('#start', () => { it('should subscribe to transaction events from WS client', () => { expect(wsClientMock.subscribe).to.be.calledTwice(); @@ -87,33 +65,26 @@ describe('BlockchainListener', () => { ); }); - it('should emit transaction hash when transaction is added to the block', (done) => { - const topic = BlockchainListener.getTransactionEventName(transactionHash); - blockchainListener.on(topic, (message) => { - expect(message).to.be.deep.equal(txQueryMessageMock); - done(); - }); - - wsClientMock.emit(BlockchainListener.TX_QUERY, txQueryMessageMock); - }); + it('should emit block when new block is arrived', (done) => { + blockchainListener.on(BlockchainListener.EVENTS.NEW_BLOCK, (message) => { + expect(message).to.be.deep.equal(blockMessageMock); - it('should emit transaction buffer when received a block with this tx from WS connection', (done) => { - const topic = BlockchainListener.getTransactionAddedToTheBlockEventName(transactionHash); - blockchainListener.on(topic, (transactionBuffer) => { - expect(transactionBuffer).to.be.deep.equal(txBufferMock); done(); }); wsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, blockMessageMock); }); - it('should not emit any transaction hashes if block contents are empty', (done) => { - wsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, emptyBlockMessage); + it('should emit transaction when transaction is arrived', (done) => { + const topic = BlockchainListener.getTransactionEventName(transactionHash); + + blockchainListener.on(topic, (message) => { + expect(message).to.be.deep.equal(txQueryMessageMock); - setTimeout(() => { - expect(blockchainListener.on).to.not.be.called(); done(); - }, 100); + }); + + wsClientMock.emit(BlockchainListener.TX_QUERY, txQueryMessageMock); }); }); }); diff --git a/test/integration/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js b/test/integration/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js index fd56040a9..44d16042d 100644 --- a/test/integration/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js +++ b/test/integration/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory.js @@ -21,20 +21,20 @@ const getIdentityCreateTransitionFixture = require('@dashevo/dpp/lib/test/fixtur const { EventEmitter } = require('events'); const cbor = require('cbor'); -const BlockchainListener = require('../../../../../lib/externalApis/tenderdash/blockchainListener/BlockchainListener'); +const BlockchainListener = require('../../../../../lib/externalApis/tenderdash/BlockchainListener'); const GrpcCallMock = require('../../../../../lib/test/mock/GrpcCallMock'); const fetchProofForStateTransitionFactory = require('../../../../../lib/externalApis/drive/fetchProofForStateTransitionFactory'); -const waitForTransactionToBeProvableFactory = require('../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory'); -const waitForTransactionResult = require('../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult'); -const waitForTransactionCommitment = require('../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment'); +const waitForTransactionToBeProvableFactory = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory'); +const waitForTransactionResult = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult'); const waitForStateTransitionResultHandlerFactory = require('../../../../../lib/grpcServer/handlers/platform/waitForStateTransitionResultHandlerFactory'); +const waitForHeightFactory = require('../../../../../lib/externalApis/tenderdash/waitForHeightFactory'); describe('waitForStateTransitionResultHandlerFactory', () => { let call; let waitForStateTransitionResultHandler; - let driveClinetMock; + let driveClientMock; let tenderDashWsClientMock; let blockchainListener; let dppMock; @@ -43,13 +43,13 @@ describe('waitForStateTransitionResultHandlerFactory', () => { let wsMessagesFixture; let stateTransitionFixture; let request; - let emptyBlockFixture; - let blockWithTxFixture; let fetchProofForStateTransition; let waitForTransactionToBeProvable; + let transactionNotFoundError; beforeEach(function beforeEach() { - hash = Buffer.from('56458F2D8A8617EA322931B72C103CDD93820004E534295183A6EF215B93C76E', 'hex'); + const hashString = '56458F2D8A8617EA322931B72C103CDD93820004E534295183A6EF215B93C76E'; + hash = Buffer.from(hashString, 'hex'); wsMessagesFixture = { success: { @@ -104,22 +104,6 @@ describe('waitForStateTransitionResultHandlerFactory', () => { }, }, }; - emptyBlockFixture = { - data: { value: { block: { data: { txs: [] } } } }, - }; - blockWithTxFixture = { - data: { - value: { - block: { - data: { - txs: [ - wsMessagesFixture.success.data.value.TxResult.tx, - ], - }, - }, - }, - }, - }; proofFixture = { rootTreeProof: Buffer.alloc(1, 1), @@ -139,18 +123,30 @@ describe('waitForStateTransitionResultHandlerFactory', () => { dppMock = createDPPMock(this.sinon); dppMock.stateTransition.createFromBuffer.resolves(stateTransitionFixture); - driveClinetMock = { + driveClientMock = { fetchProofs: this.sinon.stub().resolves({ identitiesProof: proofFixture }), }; blockchainListener = new BlockchainListener(tenderDashWsClientMock); blockchainListener.start(); - fetchProofForStateTransition = fetchProofForStateTransitionFactory(driveClinetMock); + fetchProofForStateTransition = fetchProofForStateTransitionFactory(driveClientMock); + + const waitForHeight = waitForHeightFactory( + blockchainListener, + ); + + transactionNotFoundError = new Error(); + + transactionNotFoundError.code = -32603; + transactionNotFoundError.data = `tx (${hashString}) not found`; + + const getExistingTransactionResult = this.sinon.stub().rejects(transactionNotFoundError); waitForTransactionToBeProvable = waitForTransactionToBeProvableFactory( waitForTransactionResult, - waitForTransactionCommitment, + getExistingTransactionResult, + waitForHeight, ); waitForStateTransitionResultHandler = waitForStateTransitionResultHandlerFactory( @@ -169,10 +165,14 @@ describe('waitForStateTransitionResultHandlerFactory', () => { tenderDashWsClientMock.emit('tm.event = \'Tx\'', wsMessagesFixture.success); }, 10); setTimeout(() => { - tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, blockWithTxFixture); + tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, { + data: { value: { block: { header: { height: '145' } } } }, + }); }, 10); setTimeout(() => { - tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, emptyBlockFixture); + tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, { + data: { value: { block: { header: { height: '146' } } } }, + }); }, 10); const result = await promise; @@ -191,10 +191,14 @@ describe('waitForStateTransitionResultHandlerFactory', () => { tenderDashWsClientMock.emit('tm.event = \'Tx\'', wsMessagesFixture.success); }, 10); setTimeout(() => { - tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, blockWithTxFixture); + tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, { + data: { value: { block: { header: { height: '145' } } } }, + }); }, 10); setTimeout(() => { - tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, emptyBlockFixture); + tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, { + data: { value: { block: { header: { height: '146' } } } }, + }); }, 10); const result = await promise; @@ -210,7 +214,7 @@ describe('waitForStateTransitionResultHandlerFactory', () => { expect(rootTreeProof).to.deep.equal(proofFixture.rootTreeProof); expect(storeTreeProof).to.deep.equal(proofFixture.storeTreeProof); - expect(driveClinetMock.fetchProofs).to.be.calledOnceWithExactly({ + expect(driveClientMock.fetchProofs).to.be.calledOnceWithExactly({ identityIds: stateTransitionFixture.getModifiedDataIds(), }); }); @@ -240,8 +244,6 @@ describe('waitForStateTransitionResultHandlerFactory', () => { process.nextTick(() => { tenderDashWsClientMock.emit('tm.event = \'Tx\'', wsMessagesFixture.error); - tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, blockWithTxFixture); - tenderDashWsClientMock.emit(BlockchainListener.NEW_BLOCK_QUERY, emptyBlockFixture); }); }); @@ -261,10 +263,16 @@ describe('waitForStateTransitionResultHandlerFactory', () => { }); it('should throw DeadlineExceededGrpcError after the timeout', async () => { + const hashString = 'ABFF'; + request = new WaitForStateTransitionResultRequest(); - const stHash = Buffer.from('abff', 'hex'); + + const stHash = Buffer.from(hashString, 'hex'); + request.setStateTransitionHash(stHash); + transactionNotFoundError.data = `tx (${hashString}) not found`; + call.request = WaitForStateTransitionResultRequest.deserializeBinary(request.serializeBinary()); try { @@ -273,9 +281,9 @@ describe('waitForStateTransitionResultHandlerFactory', () => { expect.fail('should throw an error'); } catch (e) { expect(e).to.be.instanceOf(DeadlineExceededGrpcError); - expect(e.getMessage()).to.equal('Waiting period for state transition ABFF exceeded'); + expect(e.getMessage()).to.equal(`Waiting period for state transition ${hashString} exceeded`); expect(e.getRawMetadata()).to.be.deep.equal({ - stateTransitionHash: 'ABFF', + stateTransitionHash: hashString, }); } }); diff --git a/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment.spec.js b/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment.spec.js deleted file mode 100644 index 233bdb003..000000000 --- a/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -const EventEmitter = require('events'); - -const waitForTransactionCommitment = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionCommitment'); - -const BlockchainListener = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/BlockchainListener'); - -describe('waitForTransactionCommitment', () => { - let blockchainListenerMock; - let hashString; - let txInBlockTopic; - - beforeEach(function beforeEach() { - blockchainListenerMock = new EventEmitter(); - - this.sinon.spy(blockchainListenerMock); - - hashString = 'abc'; - - txInBlockTopic = BlockchainListener - .getTransactionAddedToTheBlockEventName(hashString.toLowerCase()); - }); - - it('should resolve promise after block with transaction and one more', async () => { - const { promise } = waitForTransactionCommitment(blockchainListenerMock, hashString); - - // Emit new block - blockchainListenerMock.emit(BlockchainListener.EVENTS.NEW_BLOCK); - - // Assert promise is pending - expect(await Promise.race([promise, true])).to.equal(true); - - // Emit new block with transaction - blockchainListenerMock.emit(txInBlockTopic); - - // Assert promise is pending - expect(await Promise.race([promise, true])).to.equal(true); - - // Emit next block - blockchainListenerMock.emit(BlockchainListener.EVENTS.NEW_BLOCK); - - const result = await promise; - - expect(result).to.be.undefined(); - - expect(blockchainListenerMock.off).to.be.calledOnceWith(BlockchainListener.EVENTS.NEW_BLOCK); - expect(blockchainListenerMock.once).to.be.calledOnceWith(txInBlockTopic); - }); - - it('should remove listeners on detach', () => { - const { detach } = waitForTransactionCommitment(blockchainListenerMock, hashString); - - detach(); - - expect(blockchainListenerMock.off).to.be.calledTwice(); - expect(blockchainListenerMock.off.withArgs(txInBlockTopic)).to.be.called(); - expect(blockchainListenerMock.off.withArgs(BlockchainListener.EVENTS.NEW_BLOCK)).to.be.called(); - }); -}); diff --git a/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult.spec.js b/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult.spec.js deleted file mode 100644 index 793127c80..000000000 --- a/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -const EventEmitter = require('events'); - -const waitForTransactionResult = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionResult'); - -const BlockchainListener = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/BlockchainListener'); - -const TransactionOkResult = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionOkResult'); -const TransactionErrorResult = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult'); - -describe('waitForTransactionResult', () => { - let blockchainListenerMock; - let hashString; - let topic; - let tx; - - beforeEach(function beforeEach() { - blockchainListenerMock = new EventEmitter(); - - this.sinon.spy(blockchainListenerMock); - - hashString = 'abc'; - - topic = BlockchainListener.getTransactionEventName(hashString); - - tx = 'aGVsbG8h'; - }); - - it('should resolve promise with TransactionOkResult when transaction result is emitted', async () => { - const { promise } = waitForTransactionResult(blockchainListenerMock, hashString); - - const result = { - code: 0, - }; - - const data = { data: { value: { TxResult: { result, tx } } } }; - - blockchainListenerMock.emit(topic, data); - - const transactionResult = await promise; - - expect(transactionResult).to.be.instanceOf(TransactionOkResult); - expect(transactionResult.getDeliverResult()).to.equal(result); - expect(transactionResult.getTransaction()).to.deep.equal(Buffer.from(tx, 'base64')); - - expect(blockchainListenerMock.off).to.be.calledOnceWith(topic); - }); - - it('should reject promise with TransactionErrorResult when transaction result is emitted', async () => { - const { promise } = waitForTransactionResult(blockchainListenerMock, hashString); - - const result = { - code: 1, - }; - - const data = { data: { value: { TxResult: { result, tx } } } }; - - blockchainListenerMock.emit(topic, data); - - try { - await promise; - - expect.fail('should throw TransactionErrorResult'); - } catch (transactionResult) { - expect(transactionResult).to.be.instanceOf(TransactionErrorResult); - expect(transactionResult.getDeliverResult()).to.equal(result); - expect(transactionResult.getTransaction()).to.deep.equal(Buffer.from(tx, 'base64')); - } - - expect(blockchainListenerMock.off).to.be.calledOnceWith(topic); - }); - - it('should remove listeners on detach', () => { - const { detach } = waitForTransactionResult(blockchainListenerMock, hashString); - - detach(); - - expect(blockchainListenerMock.off).to.be.calledOnceWith(topic); - }); -}); diff --git a/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.spec.js b/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.spec.js deleted file mode 100644 index 91a700697..000000000 --- a/test/unit/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.spec.js +++ /dev/null @@ -1,138 +0,0 @@ -const waitForTransactionToBeProvableFactory = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory'); - -const TransactionOkResult = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionOkResult'); -const TransactionErrorResult = require('../../../../../../lib/externalApis/tenderdash/blockchainListener/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult'); -const TransactionWaitPeriodExceededError = require('../../../../../../lib/errors/TransactionWaitPeriodExceededError'); - -describe('waitForTransactionToBeProvableFactory', () => { - let waitForTransactionToBeProvable; - let waitForTransactionResultMock; - let waitForTransactionResultResponse; - let waitForTransactionCommitmentMock; - let waitForTransactionCommitmentResponse; - let blockchainListenerMock; - let hashString; - let timeout; - - beforeEach(function beforeEach() { - blockchainListenerMock = { }; - hashString = 'abc'; - timeout = 60000; - - waitForTransactionResultResponse = { - promise: null, - detach: this.sinon.stub(), - }; - - waitForTransactionResultMock = this.sinon.stub().returns( - waitForTransactionResultResponse, - ); - - waitForTransactionCommitmentResponse = { - promise: null, - detach: this.sinon.stub(), - }; - - waitForTransactionCommitmentMock = this.sinon.stub().returns( - waitForTransactionCommitmentResponse, - ); - - waitForTransactionToBeProvable = waitForTransactionToBeProvableFactory( - waitForTransactionResultMock, - waitForTransactionCommitmentMock, - ); - }); - - it('should return TransactionOkResult and wait for proofs', async () => { - const expectedResult = new TransactionOkResult({}, Buffer.alloc(0)); - - waitForTransactionResultResponse.promise = Promise.resolve(expectedResult); - - waitForTransactionCommitmentResponse.promise = Promise.resolve(); - - const actualResult = await waitForTransactionToBeProvable( - blockchainListenerMock, - hashString, - timeout, - ); - - expect(actualResult).to.equal(expectedResult); - - expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( - blockchainListenerMock, - hashString, - ); - - expect(waitForTransactionCommitmentMock).to.be.calledOnceWithExactly( - blockchainListenerMock, - hashString, - ); - - expect(waitForTransactionResultResponse.detach).to.not.be.called(); - expect(waitForTransactionCommitmentResponse.detach).to.not.be.called(); - }); - - it('should return TransactionErrorResult', async () => { - const expectedResult = new TransactionErrorResult({}, Buffer.alloc(0)); - - waitForTransactionResultResponse.promise = Promise.reject(expectedResult); - - waitForTransactionCommitmentResponse.promise = new Promise(() => {}); - - const actualResult = await waitForTransactionToBeProvable( - blockchainListenerMock, - hashString, - timeout, - ); - - expect(actualResult).to.equal(expectedResult); - - expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( - blockchainListenerMock, - hashString, - ); - - expect(waitForTransactionCommitmentMock).to.be.calledOnceWithExactly( - blockchainListenerMock, - hashString, - ); - - expect(waitForTransactionResultResponse.detach).to.not.be.called(); - expect(waitForTransactionCommitmentResponse.detach).to.not.be.called(); - }); - - it('should throw TransactionWaitPeriodExceededError', async () => { - timeout = 5; - - waitForTransactionResultResponse.promise = new Promise(() => {}); - - waitForTransactionCommitmentResponse.promise = new Promise(() => {}); - - try { - await waitForTransactionToBeProvable( - blockchainListenerMock, - hashString, - timeout, - ); - - expect.fail('should throw TransactionWaitPeriodExceededError'); - } catch (e) { - expect(e).to.be.instanceOf(TransactionWaitPeriodExceededError); - - expect(e.getTransactionHash()).to.equal(hashString); - - expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( - blockchainListenerMock, - hashString, - ); - - expect(waitForTransactionCommitmentMock).to.be.calledOnceWithExactly( - blockchainListenerMock, - hashString, - ); - - expect(waitForTransactionResultResponse.detach).to.be.calledOnce(); - expect(waitForTransactionCommitmentResponse.detach).to.be.calledOnce(); - } - }); -}); diff --git a/test/unit/externalApis/tenderdash/waitForHeightFactory.spec.js b/test/unit/externalApis/tenderdash/waitForHeightFactory.spec.js new file mode 100644 index 000000000..b95ffe04d --- /dev/null +++ b/test/unit/externalApis/tenderdash/waitForHeightFactory.spec.js @@ -0,0 +1,47 @@ +const EventEmitter = require('events'); + +const waitForHeightFactory = require('../../../../lib/externalApis/tenderdash/waitForHeightFactory'); +const BlockchainListener = require('../../../../lib/externalApis/tenderdash/BlockchainListener'); + +describe('waitForHeightFactory', () => { + let blockchainListenerMock; + let waitForHeight; + let blockMessageMock; + + beforeEach(() => { + blockchainListenerMock = new EventEmitter(); + + blockMessageMock = { + data: { + value: { + block: { + header: { + height: '123', + }, + data: { + txs: [], + }, + }, + }, + }, + }; + + waitForHeight = waitForHeightFactory( + blockchainListenerMock, + ); + }); + + it('should resolve promise when the current block height is getting equal to specified height', () => { + const promise = waitForHeight(123); + + blockchainListenerMock.emit(BlockchainListener.EVENTS.NEW_BLOCK, blockMessageMock); + + expect(promise).to.be.fulfilled(); + }); + + it('should resolve promise if specified height is equal or higher than the current block height', () => { + const promise = waitForHeight(120); + + expect(promise).to.be.fulfilled(); + }); +}); diff --git a/test/unit/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult.spec.js b/test/unit/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult.spec.js new file mode 100644 index 000000000..a47bbf20d --- /dev/null +++ b/test/unit/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult.spec.js @@ -0,0 +1,80 @@ +const EventEmitter = require('events'); + +const waitForTransactionResult = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionResult'); + +const BlockchainListener = require('../../../../../lib/externalApis/tenderdash/BlockchainListener'); + +const TransactionOkResult = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionOkResult'); +const TransactionErrorResult = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult'); + +describe('waitForTransactionResult', () => { + let blockchainListenerMock; + let hashString; + let topic; + let tx; + let height; + + beforeEach(function beforeEach() { + blockchainListenerMock = new EventEmitter(); + + this.sinon.spy(blockchainListenerMock); + + hashString = 'abc'; + + topic = BlockchainListener.getTransactionEventName(hashString); + + tx = 'aGVsbG8h'; + + height = 100; + }); + + it('should resolve TransactionOkResult when transaction result is emitted', async () => { + const { promise } = waitForTransactionResult(blockchainListenerMock, hashString); + + const result = { + code: 0, + }; + + const data = { data: { value: { TxResult: { result, tx, height } } } }; + + blockchainListenerMock.emit(topic, data); + + const transactionResult = await promise; + + expect(transactionResult).to.be.instanceOf(TransactionOkResult); + expect(transactionResult.getResult()).to.equal(result); + expect(transactionResult.getHeight()).to.equal(100); + expect(transactionResult.getTransaction()).to.deep.equal(Buffer.from(tx, 'base64')); + + expect(blockchainListenerMock.off).to.be.calledOnceWith(topic); + }); + + it('should resolve TransactionErrorResult when transaction result is emitted', async () => { + const { promise } = waitForTransactionResult(blockchainListenerMock, hashString); + + const result = { + code: 1, + }; + + const data = { data: { value: { TxResult: { result, tx, height } } } }; + + blockchainListenerMock.emit(topic, data); + + const transactionResult = await promise; + + expect(transactionResult).to.be.instanceOf(TransactionErrorResult); + expect(transactionResult.getResult()).to.equal(result); + expect(transactionResult.getHeight()).to.equal(100); + expect(transactionResult.getTransaction()).to.deep.equal(Buffer.from(tx, 'base64')); + + expect(blockchainListenerMock.off).to.be.calledOnceWith(topic); + }); + + it('should remove listeners on detach', () => { + const { detach } = waitForTransactionResult(blockchainListenerMock, hashString); + + detach(); + + expect(blockchainListenerMock.off).to.be.calledOnceWith(topic); + }); +}); diff --git a/test/unit/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.spec.js b/test/unit/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.spec.js new file mode 100644 index 000000000..5bd1041b4 --- /dev/null +++ b/test/unit/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory.spec.js @@ -0,0 +1,203 @@ +const waitForTransactionToBeProvableFactory = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/waitForTransactionToBeProvableFactory'); + +const TransactionOkResult = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionOkResult'); +const TransactionErrorResult = require('../../../../../lib/externalApis/tenderdash/waitForTransactionToBeProvable/transactionResult/TransactionErrorResult'); +const TransactionWaitPeriodExceededError = require('../../../../../lib/errors/TransactionWaitPeriodExceededError'); + +describe('waitForTransactionToBeProvableFactory', () => { + let waitForTransactionToBeProvable; + let waitForTransactionResultMock; + let waitForTransactionResultResponse; + let getExistingTransactionResultMock; + let blockchainListenerMock; + let waitForHeightMock; + let hashString; + let timeout; + let height; + let okResult; + let errorResult; + let transactionNotFoundError; + + beforeEach(function beforeEach() { + blockchainListenerMock = { }; + hashString = 'abc'; + timeout = 60000; + height = 100; + + getExistingTransactionResultMock = this.sinon.stub(); + + waitForTransactionResultResponse = { + promise: null, + detach: this.sinon.stub(), + }; + + waitForTransactionResultMock = this.sinon.stub().returns( + waitForTransactionResultResponse, + ); + + waitForHeightMock = this.sinon.stub().resolves(); + + waitForTransactionToBeProvable = waitForTransactionToBeProvableFactory( + waitForTransactionResultMock, + getExistingTransactionResultMock, + waitForHeightMock, + ); + + okResult = new TransactionOkResult({}, height, Buffer.alloc(0)); + errorResult = new TransactionErrorResult({}, height, Buffer.alloc(0)); + + transactionNotFoundError = new Error(); + + transactionNotFoundError.code = -32603; + transactionNotFoundError.data = `tx (${hashString}) not found`; + }); + + it('should return existing transaction ok result when next block arrived', async () => { + getExistingTransactionResultMock.resolves(okResult); + + waitForTransactionResultResponse.promise = new Promise(() => {}); + + waitForHeightMock.promise = Promise.resolve(); + + const actualResult = await waitForTransactionToBeProvable( + blockchainListenerMock, + hashString, + timeout, + ); + + expect(actualResult).to.equal(okResult); + + expect(getExistingTransactionResultMock).to.be.calledOnceWithExactly( + hashString, + ); + + expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( + blockchainListenerMock, + hashString, + ); + + expect(waitForHeightMock).to.be.calledOnceWithExactly( + height + 1, + ); + + expect(waitForTransactionResultResponse.detach).to.be.called(); + }); + + it('should return existing transaction error result and do not wait for next block', async () => { + getExistingTransactionResultMock.resolves(errorResult); + + waitForTransactionResultResponse.promise = new Promise(() => {}); + + const actualResult = await waitForTransactionToBeProvable( + blockchainListenerMock, + hashString, + timeout, + ); + + expect(actualResult).to.equal(errorResult); + + expect(getExistingTransactionResultMock).to.be.calledOnceWithExactly( + hashString, + ); + + expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( + blockchainListenerMock, + hashString, + ); + + expect(waitForHeightMock).to.not.be.called(); + + expect(waitForTransactionResultResponse.detach).to.be.called(); + }); + + it('should return upcoming transaction ok result when next block arrived', async () => { + getExistingTransactionResultMock.rejects(transactionNotFoundError); + + waitForTransactionResultResponse.promise = Promise.resolve(okResult); + + const actualResult = await waitForTransactionToBeProvable( + blockchainListenerMock, + hashString, + timeout, + ); + + expect(actualResult).to.equal(okResult); + + expect(getExistingTransactionResultMock).to.be.calledOnceWithExactly( + hashString, + ); + + expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( + blockchainListenerMock, + hashString, + ); + + expect(waitForHeightMock).to.be.calledOnceWithExactly( + height + 1, + ); + + expect(waitForTransactionResultResponse.detach).to.not.be.called(); + }); + + it('should return upcoming transaction error result and do not wait for next block', async () => { + getExistingTransactionResultMock.rejects(transactionNotFoundError); + + waitForTransactionResultResponse.promise = Promise.resolve(errorResult); + + const actualResult = await waitForTransactionToBeProvable( + blockchainListenerMock, + hashString, + timeout, + ); + + expect(actualResult).to.equal(errorResult); + + expect(getExistingTransactionResultMock).to.be.calledOnceWithExactly( + hashString, + ); + + expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( + blockchainListenerMock, + hashString, + ); + + expect(waitForHeightMock).to.not.be.called(); + + expect(waitForTransactionResultResponse.detach).to.not.be.called(); + }); + + it('should throw TransactionWaitPeriodExceededError on timeout', async () => { + timeout = 5; + + getExistingTransactionResultMock.rejects(transactionNotFoundError); + + waitForTransactionResultResponse.promise = new Promise(() => {}); + + try { + await waitForTransactionToBeProvable( + blockchainListenerMock, + hashString, + timeout, + ); + + expect.fail('should throw TransactionWaitPeriodExceededError'); + } catch (e) { + expect(e).to.be.instanceOf(TransactionWaitPeriodExceededError); + + expect(e.getTransactionHash()).to.equal(hashString); + + expect(getExistingTransactionResultMock).to.be.calledOnceWithExactly( + hashString, + ); + + expect(waitForTransactionResultMock).to.be.calledOnceWithExactly( + blockchainListenerMock, + hashString, + ); + + expect(waitForHeightMock).to.not.be.called(); + + expect(waitForTransactionResultResponse.detach).to.be.calledOnce(); + } + }); +});