diff --git a/.changeset/mighty-pens-run.md b/.changeset/mighty-pens-run.md new file mode 100644 index 00000000..4d3e9c7b --- /dev/null +++ b/.changeset/mighty-pens-run.md @@ -0,0 +1,5 @@ +--- +'guard-service': patch +--- + +Fix arbitrary order defections diff --git a/src/agreement/TxAgreement.ts b/src/agreement/TxAgreement.ts index 5bc5a47a..7d41cc0a 100644 --- a/src/agreement/TxAgreement.ts +++ b/src/agreement/TxAgreement.ts @@ -1,4 +1,8 @@ -import { EventStatus, TransactionStatus } from '../utils/constants'; +import { + EventStatus, + OrderStatus, + TransactionStatus, +} from '../utils/constants'; import { CandidateTransaction, TransactionRequest, @@ -542,7 +546,7 @@ class TxAgreement extends Communicator { tx, GuardPkHandler.getInstance().requiredSign ); - await this.updateEventOfApprovedTx(tx); + await this.updateEventOrOrderOfApprovedTx(tx); } else { if (txRecord.status === TransactionStatus.invalid) { logger.debug( @@ -573,10 +577,10 @@ class TxAgreement extends Communicator { }; /** - * updates event status for a tx + * updates event or order status for a tx * @param tx */ - protected updateEventOfApprovedTx = async ( + protected updateEventOrOrderOfApprovedTx = async ( tx: PaymentTransaction ): Promise => { try { @@ -590,9 +594,16 @@ class TxAgreement extends Communicator { tx.eventId, EventStatus.inReward ); + else if (tx.txType === TransactionType.arbitrary) + await DatabaseAction.getInstance().setOrderStatus( + tx.eventId, + OrderStatus.inProcess + ); } catch (e) { logger.warn( - `An error occurred while setting database event [${tx.eventId}] status: ${e}` + `An error occurred while setting database ${ + tx.txType === TransactionType.arbitrary ? 'order' : 'event' + } [${tx.eventId}] status: ${e}` ); logger.warn(e.stack); } diff --git a/src/api/arbitrary.ts b/src/api/arbitrary.ts index a384c089..f886e414 100644 --- a/src/api/arbitrary.ts +++ b/src/api/arbitrary.ts @@ -35,21 +35,27 @@ const orderRoute = (server: FastifySeverInstance) => { }, async (request, reply) => { const { id, chain, orderJson } = request.body; - if (!Configs.isArbitraryOrderRequestActive) + if (!Configs.isArbitraryOrderRequestActive) { reply.status(400).send({ message: `Arbitrary order request is disabled in config`, }); + return; + } - if (!SUPPORTED_CHAINS.includes(chain)) + if (!SUPPORTED_CHAINS.includes(chain)) { reply.status(400).send({ message: `Invalid value for chain (chain [${chain}] is not supported)`, }); + return; + } // id should be a 32bytes hex string - if (id.length !== 64 || !id.match(/^[0-9a-f]+$/)) + if (id.length !== 64 || !id.match(/^[0-9a-f]+$/)) { reply.status(400).send({ message: `Invalid value for id (expected 32Bytes hex string, found [${id}])`, }); + return; + } try { // try to decode order diff --git a/src/db/DatabaseHandler.ts b/src/db/DatabaseHandler.ts index e10a727b..14962679 100644 --- a/src/db/DatabaseHandler.ts +++ b/src/db/DatabaseHandler.ts @@ -11,6 +11,8 @@ import Configs from '../configs/Configs'; import { DefaultLoggerFactory } from '@rosen-bridge/abstract-logger'; import { DuplicateOrder, DuplicateTransaction } from '../utils/errors'; import GuardsErgoConfigs from '../configs/GuardsErgoConfigs'; +import { ArbitraryEntity } from './entities/ArbitraryEntity'; +import { TransactionEntity } from './entities/TransactionEntity'; const logger = DefaultLoggerFactory.getInstance().getLogger(import.meta.url); @@ -31,21 +33,35 @@ class DatabaseHandler { .txSignSemaphore.acquire() .then(async (release) => { try { - const event = await DatabaseAction.getInstance().getEventById( - newTx.eventId - ); - if ( - event === null && - newTx.txType !== TransactionType.coldStorage && - newTx.txType !== TransactionType.manual - ) { - throw new Error(`Event [${newTx.eventId}] not found`); + switch (newTx.txType) { + case TransactionType.payment: + case TransactionType.reward: { + const event = await DatabaseAction.getInstance().getEventById( + newTx.eventId + ); + if (event === null) + throw new Error(`Event [${newTx.eventId}] not found`); + await this.insertEventOrOderTx(newTx, event, null, requiredSign); + break; + } + case TransactionType.manual: { + await this.insertManualTx(newTx, requiredSign, overwrite); + break; + } + case TransactionType.coldStorage: { + await this.insertColdStorageTx(newTx, requiredSign); + break; + } + case TransactionType.arbitrary: { + const order = await DatabaseAction.getInstance().getOrderById( + newTx.eventId + ); + if (order === null) + throw new Error(`Order [${newTx.eventId}] not found`); + await this.insertEventOrOderTx(newTx, null, order, requiredSign); + break; + } } - - if (event) await this.insertEventTx(newTx, event, requiredSign); - else if (newTx.txType === TransactionType.manual) - await this.insertManualTx(newTx, requiredSign, overwrite); - else await this.insertColdStorageTx(newTx, requiredSign); release(); } catch (e) { release(); @@ -59,17 +75,31 @@ class DatabaseHandler { * if already another approved tx exists, keeps the one with loser txId * @param newTx the transaction * @param event the event trigger + * @param order the arbitrary order * @param requiredSign */ - private static insertEventTx = async ( + private static insertEventOrOderTx = async ( newTx: PaymentTransaction, - event: ConfirmedEventEntity, + event: ConfirmedEventEntity | null, + order: ArbitraryEntity | null, requiredSign: number ): Promise => { - const txs = await DatabaseAction.getInstance().getEventValidTxsByType( - event.id, - newTx.txType - ); + let txs: TransactionEntity[]; + let eventOrOrderId: string; + if (event !== null) { + txs = await DatabaseAction.getInstance().getEventValidTxsByType( + event.id, + newTx.txType + ); + eventOrOrderId = event.id; + } else if (order !== null) { + txs = await DatabaseAction.getInstance().getOrderValidTxs(order.id); + eventOrOrderId = order.id; + } else { + throw new ImpossibleBehavior( + `The Order nor the event is passed while inserting tx [${newTx.txId}]` + ); + } if (txs.length > 1) { throw new ImpossibleBehavior( `Event [${newTx.eventId}] has already more than 1 (${txs.length}) active ${newTx.txType} tx` @@ -94,7 +124,9 @@ class DatabaseHandler { ); } else { logger.warn( - `Received approval for newTx [${newTx.txId}] where its event [${event.id}] has already an advanced oldTx [${tx.txId}]` + `Received approval for newTx [${newTx.txId}] where its ${ + event !== null ? `event` : `order` + } [${eventOrOrderId}] has already an advanced oldTx [${tx.txId}]` ); } } @@ -103,7 +135,7 @@ class DatabaseHandler { newTx, event, requiredSign, - null + order ); }; diff --git a/src/transaction/TransactionProcessor.ts b/src/transaction/TransactionProcessor.ts index 00604121..6ca14750 100644 --- a/src/transaction/TransactionProcessor.ts +++ b/src/transaction/TransactionProcessor.ts @@ -11,7 +11,11 @@ import Configs from '../configs/Configs'; import { DatabaseAction } from '../db/DatabaseAction'; import { TransactionEntity } from '../db/entities/TransactionEntity'; import ChainHandler from '../handlers/ChainHandler'; -import { EventStatus, TransactionStatus } from '../utils/constants'; +import { + EventStatus, + OrderStatus, + TransactionStatus, +} from '../utils/constants'; import * as TransactionSerializer from './TransactionSerializer'; import { DefaultLoggerFactory } from '@rosen-bridge/abstract-logger'; import { NotificationHandler } from '../handlers/NotificationHandler'; @@ -222,6 +226,15 @@ class TransactionProcessor { logger.info( `Tx [${tx.txId}] is confirmed. Event [${tx.event.id}] is complete` ); + } else if (tx.type === TransactionType.arbitrary) { + // set order as complete + await DatabaseAction.getInstance().setOrderStatus( + tx.order.id, + OrderStatus.completed + ); + logger.info( + `Tx [${tx.txId}] is confirmed. Order [${tx.order.id}] is complete` + ); } else { // no need to do anything about event, just log that tx confirmed logger.info( @@ -328,6 +341,17 @@ class TransactionProcessor { `Tx [${tx.txId}] is invalid. Event [${tx.event.id}] is now waiting for reward distribution. Reason: ${invalidationDetails.reason}` ); break; + case TransactionType.arbitrary: + await DatabaseAction.getInstance().setOrderStatus( + tx.order.id, + OrderStatus.pending, + false, + invalidationDetails?.unexpected + ); + logger.info( + `Tx [${tx.txId}] is invalid. Order [${tx.order.id}] is now waiting for payment. Reason: ${invalidationDetails.reason}` + ); + break; case TransactionType.coldStorage: logger.info( `Cold storage tx [${tx.txId}] is invalid. Reason: ${invalidationDetails.reason}` diff --git a/tests/agreement/TestTxAgreement.ts b/tests/agreement/TestTxAgreement.ts index e505df34..55d4bac4 100644 --- a/tests/agreement/TestTxAgreement.ts +++ b/tests/agreement/TestTxAgreement.ts @@ -71,8 +71,8 @@ class TestTxAgreement extends TxAgreement { callSetTxAsApproved = (tx: PaymentTransaction) => this.setTxAsApproved(tx); - callUpdateEventOfApprovedTx = (tx: PaymentTransaction) => - this.updateEventOfApprovedTx(tx); + callUpdateEventOrOrderOfApprovedTx = (tx: PaymentTransaction) => + this.updateEventOrOrderOfApprovedTx(tx); getSigner = () => this.signer; } diff --git a/tests/agreement/TxAgreement.spec.ts b/tests/agreement/TxAgreement.spec.ts index 9e847e81..ec0843ed 100644 --- a/tests/agreement/TxAgreement.spec.ts +++ b/tests/agreement/TxAgreement.spec.ts @@ -13,9 +13,14 @@ import { } from '../../src/agreement/Interfaces'; import * as EventTestData from '../event/testData'; import EventSerializer from '../../src/event/EventSerializer'; -import { EventStatus, TransactionStatus } from '../../src/utils/constants'; +import { + EventStatus, + OrderStatus, + TransactionStatus, +} from '../../src/utils/constants'; import { cloneDeep } from 'lodash-es'; import TransactionVerifier from '../../src/verification/TransactionVerifier'; +import { CARDANO_CHAIN } from '@rosen-chains/cardano'; describe('TxAgreement', () => { describe('addTransactionToQueue', () => { @@ -2124,13 +2129,13 @@ describe('TxAgreement', () => { }); }); - describe('updateEventOfApprovedTx', () => { + describe('updateEventOrOrderOfApprovedTx', () => { beforeEach(async () => { await DatabaseActionMock.clearTables(); }); /** - * @target TxAgreement.updateEventOfApprovedTx should update + * @target TxAgreement.updateEventOrOrderOfApprovedTx should update * event status from pending-payment to in-payment * @dependencies * - database @@ -2160,7 +2165,7 @@ describe('TxAgreement', () => { // run test const txAgreement = new TestTxAgreement(); - await txAgreement.callUpdateEventOfApprovedTx(paymentTx); + await txAgreement.callUpdateEventOrOrderOfApprovedTx(paymentTx); // event status should be updated in db const dbEvents = (await DatabaseActionMock.allEventRecords()).map( @@ -2171,7 +2176,7 @@ describe('TxAgreement', () => { }); /** - * @target TxAgreement.updateEventOfApprovedTx should update + * @target TxAgreement.updateEventOrOrderOfApprovedTx should update * event status from pending-reward to in-reward * @dependencies * - database @@ -2201,7 +2206,7 @@ describe('TxAgreement', () => { // run test const txAgreement = new TestTxAgreement(); - await txAgreement.callUpdateEventOfApprovedTx(paymentTx); + await txAgreement.callUpdateEventOrOrderOfApprovedTx(paymentTx); // event status should be updated in db const dbEvents = (await DatabaseActionMock.allEventRecords()).map( @@ -2210,6 +2215,49 @@ describe('TxAgreement', () => { expect(dbEvents.length).toEqual(1); expect(dbEvents).to.deep.contain([eventId, EventStatus.inReward]); }); + + /** + * @target TxAgreement.updateEventOrOrderOfApprovedTx should update + * order status from pending to in-process + * @dependencies + * - database + * @scenario + * - mock testdata + * - insert mocked order into db + * - run test + * - check order status in db + * @expected + * - order status should be updated in db + */ + it('should update order status from pending to in-process', async () => { + // mock testdata + const orderId = 'order-id'; + const orderChain = CARDANO_CHAIN; + const paymentTx = mockPaymentTransaction( + TransactionType.arbitrary, + orderChain, + orderId + ); + + // insert mocked order into db + await DatabaseActionMock.insertOrderRecord( + orderId, + orderChain, + `orderJson`, + OrderStatus.pending + ); + + // run test + const txAgreement = new TestTxAgreement(); + await txAgreement.callUpdateEventOrOrderOfApprovedTx(paymentTx); + + // order status should be updated in db + const dbOrders = (await DatabaseActionMock.allOrderRecords()).map( + (order) => [order.id, order.status] + ); + expect(dbOrders.length).toEqual(1); + expect(dbOrders).to.deep.contain([orderId, OrderStatus.inProcess]); + }); }); describe('resendTransactionRequests', () => { diff --git a/tests/db/DatabaseHandler.spec.ts b/tests/db/DatabaseHandler.spec.ts index 0ca894d7..eb79d334 100644 --- a/tests/db/DatabaseHandler.spec.ts +++ b/tests/db/DatabaseHandler.spec.ts @@ -6,11 +6,15 @@ import { PaymentTransaction, TransactionType, } from '@rosen-chains/abstract-chain'; -import { EventStatus, TransactionStatus } from '../../src/utils/constants'; +import { + EventStatus, + OrderStatus, + TransactionStatus, +} from '../../src/utils/constants'; import DatabaseHandler from '../../src/db/DatabaseHandler'; import DatabaseActionMock from './mocked/DatabaseAction.mock'; -import { rosenConfig } from '../../src/configs/RosenConfig'; import GuardsErgoConfigs from '../../src/configs/GuardsErgoConfigs'; +import { CARDANO_CHAIN } from '@rosen-chains/cardano'; describe('DatabaseHandler', () => { const requiredSign = 6; @@ -42,9 +46,9 @@ describe('DatabaseHandler', () => { }); }); - describe('insertEventTx', () => { + describe('insertEventOrOderTx', () => { /** - * @target DatabaseHandler.insertEventTx should insert tx when + * @target DatabaseHandler.insertEventOrOderTx should insert tx when * there is no other tx for the event * @dependencies * - database @@ -84,7 +88,49 @@ describe('DatabaseHandler', () => { }); /** - * @target DatabaseHandler.insertEventTx should NOT insert tx when + * @target DatabaseHandler.insertEventOrOderTx should insert tx when + * there is no other tx for the order + * @dependencies + * - database + * @scenario + * - mock order and transaction + * - insert mocked order into db + * - run test (call `insertTx`) + * - check database + * @expected + * - tx should be inserted into db + */ + it('should insert tx when there is no other tx for the order', async () => { + // mock order and transaction + const orderId = 'order-id'; + const orderChain = CARDANO_CHAIN; + const tx = TxTestData.mockPaymentTransaction( + TransactionType.arbitrary, + orderChain, + orderId + ); + + // insert mocked order into db + await DatabaseActionMock.insertOrderRecord( + orderId, + orderChain, + `orderJson`, + OrderStatus.pending + ); + + // run test + await DatabaseHandler.insertTx(tx, requiredSign); + + // tx should be inserted into db + const dbTxs = (await DatabaseHandlerMock.allTxRecords()).map((tx) => [ + tx.txId, + tx.order.id, + ]); + expect(dbTxs).toEqual([[tx.txId, orderId]]); + }); + + /** + * @target DatabaseHandler.insertEventOrOderTx should NOT insert tx when * there is already an advanced tx for the event * @dependencies * - database @@ -132,7 +178,57 @@ describe('DatabaseHandler', () => { }); /** - * @target DatabaseHandler.insertEventTx should insert tx when + * @target DatabaseHandler.insertEventOrOderTx should NOT insert tx when + * there is already an advanced tx for the order + * @dependencies + * - database + * @scenario + * - mock order and two transactions + * - insert mocked order into db + * - insert one of the txs into db (with `inSign` status) + * - run test (call `insertTx`) + * - check database + * @expected + * - tx should NOT be inserted into db + */ + it('should NOT insert tx when there is already an advanced tx for the order', async () => { + // mock order and two transactions + const orderId = 'order-id'; + const orderChain = CARDANO_CHAIN; + const tx1 = TxTestData.mockPaymentTransaction( + TransactionType.arbitrary, + orderChain, + orderId + ); + const tx2 = TxTestData.mockPaymentTransaction( + TransactionType.arbitrary, + orderChain, + orderId + ); + + // insert mocked order into db + await DatabaseActionMock.insertOrderRecord( + orderId, + orderChain, + `orderJson`, + OrderStatus.pending + ); + + // insert one of the txs into db + await DatabaseHandlerMock.insertTxRecord(tx2, TransactionStatus.inSign); + + // run test + await DatabaseHandler.insertTx(tx1, requiredSign); + + // tx should NOT be inserted into db + const dbTxs = (await DatabaseHandlerMock.allTxRecords()).map( + (tx) => tx.txId + ); + expect(dbTxs).toEqual([tx2.txId]); + }); + + /** + * @target DatabaseHandler.insertEventOrOderTx should insert tx when * txId is lower than existing approved tx * @dependencies * - database @@ -194,7 +290,7 @@ describe('DatabaseHandler', () => { }); /** - * @target DatabaseHandler.insertEventTx should NOT insert tx when + * @target DatabaseHandler.insertEventOrOderTx should NOT insert tx when * txId is higher than existing approved tx * @dependencies * - database @@ -256,7 +352,7 @@ describe('DatabaseHandler', () => { }); /** - * @target DatabaseHandler.insertEventTx should update failedInSign when + * @target DatabaseHandler.insertEventOrOderTx should update failedInSign when * tx is already in database * @dependencies * - database diff --git a/tests/db/mocked/DatabaseAction.mock.ts b/tests/db/mocked/DatabaseAction.mock.ts index 1eaea011..56d91f48 100644 --- a/tests/db/mocked/DatabaseAction.mock.ts +++ b/tests/db/mocked/DatabaseAction.mock.ts @@ -424,7 +424,7 @@ class DatabaseActionMock { */ static allTxRecords = async () => { return await this.testDatabase.TransactionRepository.find({ - relations: ['event'], + relations: ['event', 'order'], }); }; diff --git a/tests/transaction/TransactionProcessor.spec.ts b/tests/transaction/TransactionProcessor.spec.ts index 918c3ae9..3f996cea 100644 --- a/tests/transaction/TransactionProcessor.spec.ts +++ b/tests/transaction/TransactionProcessor.spec.ts @@ -12,12 +12,17 @@ import { } from '../agreement/testData'; import TransactionProcessor from '../../src/transaction/TransactionProcessor'; import TestConfigs from '../testUtils/TestConfigs'; -import { EventStatus, TransactionStatus } from '../../src/utils/constants'; +import { + EventStatus, + OrderStatus, + TransactionStatus, +} from '../../src/utils/constants'; import Configs from '../../src/configs/Configs'; import * as EventTestData from '../event/testData'; import EventSerializer from '../../src/event/EventSerializer'; import TransactionProcessorMock from './TransactionProcessor.mock'; import NotificationHandlerMock from '../handlers/NotificationHandler.mock'; +import { CARDANO_CHAIN } from '@rosen-chains/cardano'; describe('TransactionProcessor', () => { const currentTimeStampSeconds = Math.round( @@ -941,6 +946,68 @@ describe('TransactionProcessor', () => { expect(dbEvents).toEqual([[eventId, EventStatus.completed]]); }); + /** + * @target TransactionProcessor.processSentTx should update tx status + * and order status to completed when it's tx is confirmed enough + * @dependencies + * - database + * - ChainHandler + * @scenario + * - mock order and transaction and insert into db + * - mock ChainHandler `getChain` + * - mock `getTxConfirmationStatus` + * - run test (call `processTransactions`) + * - check tx in database + * @expected + * - tx status should be updated to 'completed' + * - order status should be updated to 'completed' + */ + it("should update tx status and order status to completed when it's tx is confirmed enough", async () => { + // mock order and transaction and insert into db + const orderId = 'order-id'; + const chain = CARDANO_CHAIN; + const tx = mockErgoPaymentTransaction(TransactionType.arbitrary, orderId); + await DatabaseActionMock.insertOrderRecord( + orderId, + chain, + `orderJson`, + OrderStatus.pending + ); + await DatabaseActionMock.insertTxRecord(tx, TransactionStatus.sent); + + // mock ChainHandler `getChain` + ChainHandlerMock.mockChainName(chain); + // mock `getTxConfirmationStatus` + ChainHandlerMock.mockErgoFunctionReturnValue( + 'getTxConfirmationStatus', + ConfirmationStatus.ConfirmedEnough, + true + ); + + // run test + await TransactionProcessor.processTransactions(); + + // tx status should be updated to 'completed' + const dbTxs = (await DatabaseActionMock.allTxRecords()).map((tx) => [ + tx.txId, + tx.status, + tx.lastStatusUpdate, + ]); + expect(dbTxs).toEqual([ + [ + tx.txId, + TransactionStatus.completed, + currentTimeStampSeconds.toString(), + ], + ]); + + // order status should be updated to 'completed' + const dbOrders = (await DatabaseActionMock.allOrderRecords()).map( + (order) => [order.id, order.status] + ); + expect(dbOrders).toEqual([[orderId, OrderStatus.completed]]); + }); + /** * @target TransactionProcessor.processSentTx should update tx status * to completed when cold storage tx is confirmed enough @@ -1477,6 +1544,100 @@ describe('TransactionProcessor', () => { ]); }); + /** + * @target TransactionProcessor.setTransactionAsInvalid should update + * tx status to invalid and order status to pending when it's tx is invalid + * @dependencies + * - database + * - ChainHandler + * @scenario + * - mock order and transaction and insert into db + * - mock ChainHandler `getChain` + * - mock `getHeight` + * - mock `getTxRequiredConfirmation` + * - run test + * - check tx in database + * @expected + * - tx status should be updated to 'invalid' + * - order status should be updated to 'pending' + * - order firstTry should remain unchanged + * - order unexpectedFails should remain unchanged + */ + it("should update tx status to invalid and order status to pending when it's tx is invalid", async () => { + // mock order and transaction and insert into db + const orderId = 'order-id'; + const chain = CARDANO_CHAIN; + const tx = mockPaymentTransaction( + TransactionType.arbitrary, + chain, + orderId + ); + const firstTry = '1000'; + const unexpectedFails = 1; + await DatabaseActionMock.insertOrderRecord( + orderId, + chain, + `orderJson`, + OrderStatus.pending, + firstTry, + unexpectedFails + ); + await DatabaseActionMock.insertTxRecord(tx, TransactionStatus.sent, 100); + + // mock ChainHandler `getChain` + ChainHandlerMock.mockChainName(chain); + // mock `getHeight` + const mockedCurrentHeight = 111; + ChainHandlerMock.mockChainFunction( + chain, + 'getHeight', + mockedCurrentHeight, + true + ); + // mock `getTxRequiredConfirmation` + ChainHandlerMock.mockChainFunction( + chain, + 'getTxRequiredConfirmation', + 10 + ); + + // run test + const txEntity = (await DatabaseActionMock.allTxRecords())[0]; + const mockedChain = chainHandlerInstance.getChain(chain); + await TransactionProcessor.setTransactionAsInvalid( + txEntity, + mockedChain, + invalidationDetails(false) + ); + + // tx status should be updated to 'invalid' + const dbTxs = (await DatabaseActionMock.allTxRecords()).map((tx) => [ + tx.txId, + tx.status, + tx.lastStatusUpdate, + ]); + expect(dbTxs).toEqual([ + [ + tx.txId, + TransactionStatus.invalid, + currentTimeStampSeconds.toString(), + ], + ]); + + // order status should be updated to 'pending' + const dbOrders = (await DatabaseActionMock.allOrderRecords()).map( + (order) => [ + order.id, + order.status, + order.firstTry, + order.unexpectedFails, + ] + ); + expect(dbOrders).toEqual([ + [orderId, OrderStatus.pending, firstTry, unexpectedFails], + ]); + }); + /** * @target TransactionProcessor.setTransactionAsInvalid should update * tx status to invalid when cold storage tx is invalid @@ -1892,6 +2053,105 @@ describe('TransactionProcessor', () => { ]); }); + /** + * @target TransactionProcessor.setTransactionAsInvalid should update + * tx status to invalid and order status to pending and increment unexpectedFails + * when it's tx has become invalid unexpectedly + * @dependencies + * - database + * - ChainHandler + * @scenario + * - mock order and transaction and insert into db + * - mock ChainHandler `getChain` + * - mock `getHeight` + * - mock `getTxRequiredConfirmation` + * - mock NotificationHandler `notify` + * - run test + * - check tx in database + * @expected + * - tx status should be updated to 'invalid' + * - order status should be updated to 'pending' + * - order firstTry should remain unchanged + * - order unexpectedFails should be incremented + */ + it("should update tx status to invalid and order status to pending and increment unexpectedFails when it's tx has become invalid unexpectedly", async () => { + // mock order and transaction and insert into db + const orderId = 'order-id'; + const chain = CARDANO_CHAIN; + const tx = mockPaymentTransaction( + TransactionType.arbitrary, + chain, + orderId + ); + const firstTry = '1000'; + const unexpectedFails = 1; + await DatabaseActionMock.insertOrderRecord( + orderId, + chain, + `orderJson`, + OrderStatus.pending, + firstTry, + unexpectedFails + ); + await DatabaseActionMock.insertTxRecord(tx, TransactionStatus.sent, 100); + + // mock ChainHandler `getChain` + ChainHandlerMock.mockChainName(chain); + // mock `getHeight` + const mockedCurrentHeight = 111; + ChainHandlerMock.mockChainFunction( + chain, + 'getHeight', + mockedCurrentHeight, + true + ); + // mock `getTxRequiredConfirmation` + ChainHandlerMock.mockChainFunction( + chain, + 'getTxRequiredConfirmation', + 10 + ); + + // mock NotificationHandler `notify` + NotificationHandlerMock.mockNotify(); + + // run test + const txEntity = (await DatabaseActionMock.allTxRecords())[0]; + const mockedChain = chainHandlerInstance.getChain(chain); + await TransactionProcessor.setTransactionAsInvalid( + txEntity, + mockedChain, + invalidationDetails(true) + ); + + // tx status should be updated to 'invalid' + const dbTxs = (await DatabaseActionMock.allTxRecords()).map((tx) => [ + tx.txId, + tx.status, + tx.lastStatusUpdate, + ]); + expect(dbTxs).toEqual([ + [ + tx.txId, + TransactionStatus.invalid, + currentTimeStampSeconds.toString(), + ], + ]); + + // order status should be updated to 'pending-payment' + const dbOrders = (await DatabaseActionMock.allOrderRecords()).map( + (order) => [ + order.id, + order.status, + order.firstTry, + order.unexpectedFails, + ] + ); + expect(dbOrders).toEqual([ + [orderId, OrderStatus.pending, firstTry, unexpectedFails + 1], + ]); + }); + /** * @target TransactionProcessor.setTransactionAsInvalid should send * notification when tx has become invalid unexpectedly