diff --git a/services/blockchain-indexer/methods/dataService/transactions.js b/services/blockchain-indexer/methods/dataService/transactions.js index 519477ffdd..e2c5819e66 100644 --- a/services/blockchain-indexer/methods/dataService/transactions.js +++ b/services/blockchain-indexer/methods/dataService/transactions.js @@ -33,6 +33,7 @@ module.exports = [ address: { optional: true, type: 'string' }, senderAddress: { optional: true, type: 'string' }, recipientAddress: { optional: true, type: 'string' }, + receivingChainID: { optional: true, type: 'string' }, timestamp: { optional: true, type: 'string' }, nonce: { optional: true, type: 'string' }, blockID: { optional: true, type: 'string' }, diff --git a/services/blockchain-indexer/shared/dataService/business/pendingTransactions.js b/services/blockchain-indexer/shared/dataService/business/pendingTransactions.js index 0d423f19aa..552a182c5f 100644 --- a/services/blockchain-indexer/shared/dataService/business/pendingTransactions.js +++ b/services/blockchain-indexer/shared/dataService/business/pendingTransactions.js @@ -92,6 +92,7 @@ const validateParams = async params => { if (params.senderAddress) validatedParams.senderAddress = params.senderAddress; if (params.recipientAddress) validatedParams.recipientAddress = params.recipientAddress; if (params.moduleCommand) validatedParams.moduleCommand = params.moduleCommand; + if (params.receivingChainID) validatedParams.receivingChainID = params.receivingChainID; if (params.sort) validatedParams.sort = params.sort; return validatedParams; @@ -132,7 +133,9 @@ const getPendingTransactions = async params => { transaction.sender.address === validatedParams.address || transaction.params.recipientAddress === validatedParams.address) && (!validatedParams.moduleCommand || - transaction.moduleCommand === validatedParams.moduleCommand), + transaction.moduleCommand === validatedParams.moduleCommand) && + (!validatedParams.receivingChainID || + transaction.params.receivingChainID === validatedParams.receivingChainID), ); pendingTransactions.data = filteredPendingTxs diff --git a/services/blockchain-indexer/shared/database/schema/transactions.js b/services/blockchain-indexer/shared/database/schema/transactions.js index f3dc4b6d9f..44348c7e42 100644 --- a/services/blockchain-indexer/shared/database/schema/transactions.js +++ b/services/blockchain-indexer/shared/database/schema/transactions.js @@ -46,6 +46,7 @@ module.exports = { amount: { type: 'range' }, data: { type: 'key' }, senderAddress: { type: 'key' }, + receivingChainID: { type: 'key' }, executionStatus: { type: 'key' }, }, purge: {}, diff --git a/services/blockchain-indexer/tests/unit/shared/constants/pendingTransactions.js b/services/blockchain-indexer/tests/unit/shared/constants/pendingTransactions.js new file mode 100644 index 0000000000..e399b15e2c --- /dev/null +++ b/services/blockchain-indexer/tests/unit/shared/constants/pendingTransactions.js @@ -0,0 +1,58 @@ +const mockSenderAddress = 'lskvq67zzev53sa6ozt39ft3dsmwxxztb7h29275k'; +const mockRecipientAddress = 'lskyvvam5rxyvbvofxbdfcupxetzmqxu22phm4yuo'; + +const mockSenderAccountDetails = { + name: 'genesis', + publicKey: '3972849f2ab66376a68671c10a00e8b8b67d880434cc65b04c6ed886dfa91c2c', +}; + +const mockPendingTransactions = [ + { + module: 'token', + command: 'transfer', + fee: '100000000', + nonce: '1', + senderPublicKey: '3972849f2ab66376a68671c10a00e8b8b67d880434cc65b04c6ed886dfa91c2c', + signatures: [ + 'c7fd1abf9a552fa9c91b4121c87ae2c97cb0fc0aecc87d0ee8b1aa742238eef4a6815ddba31e21144c9652a7bd5c05577ae1100eac34fba43da6fc4879b8f206', + ], + params: { + tokenID: '0000000000000000', + amount: '100000000000', + recipientAddress: 'lskyvvam5rxyvbvofxbdfcupxetzmqxu22phm4yuo', + data: '', + }, + id: 'd96c777b67576ddf4cd933a97a60b4311881e68e3c8bef1393ac0020ec8a506c', + size: 167, + minFee: '166000', + }, + { + module: 'token', + command: 'transferCrossChain', + fee: '100000000', + nonce: '1', + senderPublicKey: '3972849f2ab66376a68671c10a00e8b8b67d880434cc65b04c6ed886dfa91c2c', + signatures: [ + 'c7fd1abf9a552fa9c91b4121c87ae2c97cb0fc0aecc87d0ee8b1aa742238eef4a6815ddba31e21144c9652a7bd5c05577ae1100eac34fba43da6fc4879b8f206', + ], + params: { + tokenID: '0000000000000000', + amount: '100000000000', + recipientAddress: 'lskyvvam5rxyvbvofxbdfcupxetzmqxu22phm4yuo', + receivingChainID: '02000000', + data: '', + messageFee: '10000000', + messageFeeTokenID: '0200000000000000', + }, + id: 'd96c777b67576ddf4cd933a97a60b4311881e68e3c8bef1393ac0020ec8a506d', + size: 167, + minFee: '166000', + }, +]; + +module.exports = { + mockPendingTransactions, + mockSenderAddress, + mockRecipientAddress, + mockSenderAccountDetails, +}; diff --git a/services/blockchain-indexer/tests/unit/shared/dataservice/business/pendingTransactions.test.js b/services/blockchain-indexer/tests/unit/shared/dataservice/business/pendingTransactions.test.js index bea5b3ef9a..84a46b4217 100644 --- a/services/blockchain-indexer/tests/unit/shared/dataservice/business/pendingTransactions.test.js +++ b/services/blockchain-indexer/tests/unit/shared/dataservice/business/pendingTransactions.test.js @@ -14,8 +14,11 @@ * */ const { - validateParams, -} = require('../../../../../shared/dataService/business/pendingTransactions'); + mockPendingTransactions, + mockSenderAddress, + mockSenderAccountDetails, + mockRecipientAddress, +} = require('../../constants/pendingTransactions'); jest.mock('lisk-service-framework', () => { const actual = jest.requireActual('lisk-service-framework'); @@ -36,6 +39,24 @@ jest.mock('lisk-service-framework', () => { }; }); +jest.mock('../../../../../shared/utils/request', () => ({ + requestConnector: jest.fn(() => mockPendingTransactions), +})); + +jest.mock('../../../../../shared/utils/account', () => ({ + getLisk32AddressFromPublicKey: jest.fn(() => mockSenderAddress), +})); + +jest.mock('../../../../../shared/dataService/utils/account', () => ({ + getIndexedAccountInfo: jest.fn(() => mockSenderAccountDetails), +})); + +const { + validateParams, + loadAllPendingTransactions, + getPendingTransactions, +} = require('../../../../../shared/dataService/business/pendingTransactions'); + describe('Test validateParams method', () => { it('should return validated params when called with valid params', async () => { const params = { @@ -61,3 +82,80 @@ describe('Test validateParams method', () => { expect(() => validateParams(null)).rejects.toThrow(); }); }); + +describe('Test getPendingTransactions method', () => { + beforeAll(async () => { + await loadAllPendingTransactions(); + }); + + it('should return all pending transactions without any filters', async () => { + const params = { + sort: 'id:asc', + offset: 0, + limit: 10, + }; + + const result = await getPendingTransactions(params); + expect(result.data.length).toBe(mockPendingTransactions.length); + }); + + it('should return pending transactions with tx id', async () => { + const params = { + id: mockPendingTransactions[0].id, + sort: 'id:asc', + offset: 0, + limit: 10, + }; + + const result = await getPendingTransactions(params); + expect(result.data.length).toBe(1); + }); + + it('should return pending transactions with recipientAddress', async () => { + const params = { + recipientAddress: mockRecipientAddress, + sort: 'id:asc', + offset: 0, + limit: 10, + }; + + const result = await getPendingTransactions(params); + + let txCountWithParams = 0; + mockPendingTransactions.forEach(transaction => { + if (transaction.params && transaction.params.recipientAddress === mockRecipientAddress) { + txCountWithParams++; + } + }); + + expect(result.data.length).toBe(txCountWithParams); + }); + + it('should return pending transactions with receivingChainID', async () => { + const params = { + receivingChainID: '02000000', + sort: 'id:asc', + offset: 0, + limit: 10, + }; + + const result = await getPendingTransactions(params); + + let txCountWithParams = 0; + mockPendingTransactions.forEach(transaction => { + if (transaction.params && transaction.params.receivingChainID === '02000000') { + txCountWithParams++; + } + }); + + expect(result.data.length).toBe(txCountWithParams); + }); + + it('should throw ValidationException for invalid parameters', async () => { + const params = { + nonce: 123, + }; + + await expect(validateParams(params)).rejects.toThrow(); + }); +}); diff --git a/services/blockchain-indexer/tests/unit/shared/dataservice/business/transactionEstimateFees.test.js b/services/blockchain-indexer/tests/unit/shared/dataservice/business/transactionEstimateFees.test.js index a89795ccbc..e27abe40d7 100644 --- a/services/blockchain-indexer/tests/unit/shared/dataservice/business/transactionEstimateFees.test.js +++ b/services/blockchain-indexer/tests/unit/shared/dataservice/business/transactionEstimateFees.test.js @@ -36,9 +36,6 @@ const mockedPOSConstantsFilePath = resolve( const mockedFeeEstimateFilePath = resolve( `${__dirname}/../../../../../shared/dataService/business/feeEstimates`, ); -const mockedRTransactionsDryRunFilePath = resolve( - `${__dirname}../../../../../../shared/dataService/business/transactionsDryRun`, -); const mockedNetworkFilePath = resolve( `${__dirname}/../../../../../shared/dataService/business/network`, ); @@ -65,6 +62,14 @@ const { mockRegisterValidatorTxResult, } = require('../../constants/transactionEstimateFees'); +const { tokenHasUserAccount } = require('../../../../../shared/dataService/business/token'); + +const { + dryRunTransactions, +} = require('../../../../../shared/dataService/business/transactionsDryRun'); + +const { requestConnector } = require('../../../../../shared/utils/request'); + jest.mock('lisk-service-framework', () => { const actual = jest.requireActual('lisk-service-framework'); return { @@ -135,43 +140,98 @@ jest.mock('../../../../../shared/dataService/business/schemas', () => { }); jest.mock('../../../../../shared/dataService/business/token', () => ({ - tokenHasUserAccount() { - return { - data: { isExists: true }, - meta: {}, - }; - }, - getTokenConstants() { - return { - data: { - extraCommandFees: { - userAccountInitializationFee: '5000000', - escrowAccountInitializationFee: '5000000', - }, + tokenHasUserAccount: jest.fn().mockImplementation(() => ({ + data: { isExists: true }, + meta: {}, + })), + getTokenConstants: jest.fn().mockImplementation(() => ({ + data: { + extraCommandFees: { + userAccountInitializationFee: '5000000', + escrowAccountInitializationFee: '5000000', }, - meta: {}, - }; - }, - getTokenBalances() { - return { - data: [{ availableBalance: '500000000000' }], - meta: {}, - }; - }, + }, + meta: {}, + })), + getTokenBalances: jest.fn().mockImplementation(() => ({ + data: [{ availableBalance: '500000000000' }], + meta: {}, + })), +})); + +jest.mock('../../../../../shared/dataService/business/transactionsDryRun', () => ({ + dryRunTransactions: jest.fn(), +})); + +jest.mock('../../../../../shared/utils/request', () => ({ + requestConnector: jest.fn(), })); describe('getCcmBuffer', () => { - it('should return null if the transaction is not token transferCrossChain', () => { + it('should return null if the transaction is not token transferCrossChain', async () => { const { getCcmBuffer } = require(mockedTransactionFeeEstimatesFilePath); expect(getCcmBuffer(mockRegisterValidatorTxRequestConnector.transaction)).resolves.toBeNull(); }); - it.todo("Add test cases for 'if (!ccmSendSuccess)' scenarios"); + it('should return ccmbuffer if the transaction is transferCrossChain with CCM success event', async () => { + requestConnector.mockResolvedValue('CCM buffer'); + dryRunTransactions.mockReturnValueOnce({ + data: { events: [{ name: 'ccmSendSuccess', data: { ccm: 'hello' } }] }, + }); + + const { getCcmBuffer } = require(mockedTransactionFeeEstimatesFilePath); + expect(() => getCcmBuffer(mockTransferCrossChainTxRequestConnector)).not.toThrow(); + }); + + it('should throw validation error if the transaction is transferCrossChain without CCM success event', async () => { + const mockErrorMessage = 'validation Error'; + requestConnector.mockResolvedValue('CCM buffer'); + dryRunTransactions + .mockReturnValueOnce({ + data: { events: [] }, + }) + .mockReturnValueOnce({ + data: { errorMessage: mockErrorMessage }, + }); + + const { getCcmBuffer } = require(mockedTransactionFeeEstimatesFilePath); + expect(() => getCcmBuffer(mockTransferCrossChainTxRequestConnector)).rejects.toThrow( + mockErrorMessage, + ); + }); + + it('should throw validation error if the transaction is transferCrossChain with CCM success failed', async () => { + requestConnector.mockResolvedValue('CCM buffer'); + dryRunTransactions + .mockReturnValueOnce({ + data: { events: [{ name: 'ccmSentFailed', code: 1 }] }, + }) + .mockReturnValueOnce({ + data: { events: [{ name: 'ccmSentFailed', code: 1 }] }, + }); + + const { getCcmBuffer } = require(mockedTransactionFeeEstimatesFilePath); + expect(() => getCcmBuffer(mockTransferCrossChainTxRequestConnector)).rejects.toThrow(); + }); + + it('should return empty buffer if the transaction is transferCrossChain without CCM success failed', async () => { + requestConnector.mockResolvedValue('CCM buffer'); + dryRunTransactions + .mockReturnValueOnce({ + data: { events: [] }, + }) + .mockReturnValueOnce({ + data: { events: [] }, + }); + + const { getCcmBuffer } = require(mockedTransactionFeeEstimatesFilePath); + await expect(getCcmBuffer(mockTransferCrossChainTxRequestConnector)).resolves.toStrictEqual( + Buffer.from('', 'hex'), + ); + }); }); describe('validateUserHasTokenAccount', () => { - it.todo('Fix the mocks for validateUserHasTokenAccount dependencies and enable the tests'); - it('should return undefined if user has token account initialized', () => { const { validateUserHasTokenAccount } = require(mockedTransactionFeeEstimatesFilePath); const { tokenID, recipientAddress } = mockTransferCrossChainTxRequest.transaction.params; @@ -179,15 +239,11 @@ describe('validateUserHasTokenAccount', () => { expect(validateUserHasTokenAccount(tokenID, recipientAddress)).resolves.toBeUndefined(); }); - xit("should throw an error if user doesn't have token account initialized", async () => { - jest.mock('../../../../../shared/dataService/business/token', () => ({ - async tokenHasUserAccount() { - return { - data: { isExists: false }, - meta: {}, - }; - }, - })); + it("should throw an error if user doesn't have token account initialized", async () => { + tokenHasUserAccount.mockReturnValueOnce({ + data: { isExists: false }, + meta: {}, + }); const { validateUserHasTokenAccount } = require(mockedTransactionFeeEstimatesFilePath); const { tokenID, recipientAddress } = mockTransferCrossChainTxRequest.transaction.params; @@ -571,24 +627,15 @@ describe('Test transaction fees estimates', () => { }); describe('Test estimateTransactionFees method', () => { - afterEach(() => jest.clearAllMocks()); - jest.resetModules(); - // Mock the dependencies const { calcAdditionalFees } = require(mockedTransactionFeeEstimatesFilePath); const { calcMessageFee } = require(mockedTransactionFeeEstimatesFilePath); const { getAuthAccountInfo } = require(mockedAuthFilePath); const { getLisk32AddressFromPublicKey } = require(mockedAccountFilePath); - const { requestConnector } = require(mockedRequestFilePath); const { getPosConstants } = require(mockedPOSConstantsFilePath); const { getFeeEstimates } = require(mockedFeeEstimateFilePath); - const { dryRunTransactions } = require(mockedRTransactionsDryRunFilePath); const { getNetworkStatus } = require(mockedNetworkFilePath); - jest.mock(mockedRTransactionsDryRunFilePath, () => ({ - dryRunTransactions: jest.fn(), - })); - jest.mock(mockedAuthFilePath, () => ({ getAuthAccountInfo: jest.fn(), })); @@ -615,11 +662,6 @@ describe('Test transaction fees estimates', () => { }; }); - jest.mock(mockedRequestFilePath, () => ({ - requestConnector: jest.fn(), - requestFeeEstimator: jest.fn(), - })); - jest.mock(mockedPOSConstantsFilePath, () => ({ getPosConstants: jest.fn(), })); @@ -878,9 +920,6 @@ describe('Test transaction fees estimates', () => { }); describe('Test getNumberOfSignatures method', () => { - // Mock the dependencies - const { requestConnector } = require(mockedRequestFilePath); - jest.mock(mockedRequestFilePath, () => ({ requestConnector: jest.fn(), })); diff --git a/services/gateway/apis/http-version3/methods/transactions.js b/services/gateway/apis/http-version3/methods/transactions.js index 97b7efb7a1..a2f89c6f20 100644 --- a/services/gateway/apis/http-version3/methods/transactions.js +++ b/services/gateway/apis/http-version3/methods/transactions.js @@ -48,6 +48,7 @@ module.exports = { max: 41, pattern: regex.ADDRESS_LISK32, }, + receivingChainID: { optional: true, type: 'string', pattern: regex.CHAIN_ID }, blockID: { optional: true, type: 'string', min: 64, max: 64, pattern: regex.HASH_SHA256 }, height: { optional: true, type: 'string', min: 1, pattern: regex.HEIGHT_RANGE }, timestamp: { optional: true, type: 'string', min: 1, pattern: regex.TIMESTAMP_RANGE }, diff --git a/services/gateway/apis/http-version3/swagger/parameters/transactions.json b/services/gateway/apis/http-version3/swagger/parameters/transactions.json index adf68f75ef..cc6f0ab813 100644 --- a/services/gateway/apis/http-version3/swagger/parameters/transactions.json +++ b/services/gateway/apis/http-version3/swagger/parameters/transactions.json @@ -40,6 +40,14 @@ "minLength": 41, "maxLength": 41 }, + "receivingChainID": { + "name": "receivingChainID", + "in": "query", + "description": "Chain ID for the receiving chain in the case of cross-chain token transfers.", + "type": "string", + "minLength": 8, + "maxLength": 8 + }, "senderAddress": { "name": "senderAddress", "in": "query", diff --git a/services/gateway/sources/version3/transactions.js b/services/gateway/sources/version3/transactions.js index d4eb955fbc..3da02c23db 100644 --- a/services/gateway/sources/version3/transactions.js +++ b/services/gateway/sources/version3/transactions.js @@ -23,6 +23,7 @@ module.exports = { moduleCommand: '=,string', senderAddress: '=,string', recipientAddress: '=,string', + receivingChainID: '=,string', address: '=,string', blockID: '=,string', height: '=,string', diff --git a/services/gateway/tests/constants/generateDocs.js b/services/gateway/tests/constants/generateDocs.js index 5c3a8176b0..c6b657119b 100644 --- a/services/gateway/tests/constants/generateDocs.js +++ b/services/gateway/tests/constants/generateDocs.js @@ -689,6 +689,9 @@ const createApiDocsExpectedResponse = { { $ref: '#/parameters/recipientAddress', }, + { + $ref: '#/parameters/receivingChainID', + }, { $ref: '#/parameters/blockID', }, diff --git a/tests/integration/api_v3/constants/invalidInputs.js b/tests/integration/api_v3/constants/invalidInputs.js index 20034d703e..b5ab894f18 100644 --- a/tests/integration/api_v3/constants/invalidInputs.js +++ b/tests/integration/api_v3/constants/invalidInputs.js @@ -43,6 +43,12 @@ const invalidNames = [ '______%', ]; +const invalidChainIDs = [ + '0000000G', // contains invalid character 'G' + '0000000?', // contains invalid character '?' + '0 OR 1=1', // SQL injection +]; + const invalidTokenIDs = [ '0123456789abcdefG', // contains invalid character 'G' '0123456789abcdef?', // contains invalid character '?' @@ -75,6 +81,7 @@ module.exports = { invalidNames, invalidNamesCSV, invalidTokenIDs, + invalidChainIDs, invalidTokenIDCSV, invalidChainIDCSV, invalidPartialSearches, diff --git a/tests/integration/api_v3/http/transactions.test.js b/tests/integration/api_v3/http/transactions.test.js index 2e06b43dc6..ed0a0d0444 100644 --- a/tests/integration/api_v3/http/transactions.test.js +++ b/tests/integration/api_v3/http/transactions.test.js @@ -18,11 +18,14 @@ import { TRANSACTION_EXECUTION_STATUSES } from '../../../schemas/api_v3/constant import { invalidAddresses, invalidBlockIDs, + invalidChainIDs, invalidLimits, invalidOffsets, } from '../constants/invalidInputs'; import { waitMs } from '../../../helpers/utils'; +jest.setTimeout(1200000); + const config = require('../../../config'); const { api } = require('../../../helpers/api'); @@ -41,32 +44,55 @@ const baseAddress = config.SERVICE_ENDPOINT; const baseUrl = `${baseAddress}/api/v3`; const endpoint = `${baseUrl}/transactions`; +const fetchTxWithRetry = async txEndpoint => { + let retries = 10; + + while (retries > 0) { + try { + const response = await api.get(txEndpoint); + const [tx] = response.data; + + if (tx) { + return { + success: true, + data: tx, + }; + } + } catch (error) { + console.error(`Error fetching transactions. Retries left: ${retries}`); + + // Delay by 3 sec + await waitMs(3000); + } + retries--; + } + + return { + success: false, + }; +}; + describe('Transactions API', () => { let refTransaction; beforeAll(async () => { - let retries = 10; - let success = false; - - while (retries > 0 && !success) { - try { - const response = await api.get(`${endpoint}?limit=1&moduleCommand=token:transfer`); - [refTransaction] = response.data; - - if (refTransaction) { - success = true; - } - } catch (error) { - console.error(`Error fetching transactions. Retries left: ${retries}`); - retries--; + const crossChainTxRes = await fetchTxWithRetry( + `${endpoint}?limit=1&moduleCommand=token:transferCrossChain`, + ); + + // Try to fetch transfer transaction on same chain, incase no transactions with transfer cross chain + if (!crossChainTxRes.success) { + const sameChainTxRes = await fetchTxWithRetry( + `${endpoint}?limit=1&moduleCommand=token:transfer`, + ); - // Delay by 3 sec - await waitMs(3000); + if (!sameChainTxRes.success) { + throw new Error('Failed to fetch transactions after 10 retries'); + } else { + refTransaction = sameChainTxRes.data; } - } - - if (!success) { - throw new Error('Failed to fetch transactions after 10 retries'); + } else { + refTransaction = crossChainTxRes.data; } }); @@ -346,6 +372,39 @@ describe('Transactions API', () => { }); }); + describe('Retrieve transaction list by receivingChainID', () => { + it('should return transactions when called with receivingChainID', async () => { + if (refTransaction.params.receivingChainID) { + const response = await api.get( + `${endpoint}?receivingChainID=${refTransaction.params.receivingChainID}`, + ); + expect(response).toMap(goodRequestSchema); + expect(response.data).toBeInstanceOf(Array); + expect(response.data.length).toBeGreaterThanOrEqual(1); + expect(response.data.length).toBeLessThanOrEqual(10); + response.data.forEach((transaction, i) => { + expect(transaction).toMap(transactionSchema); + expect(transaction.params.receivingChainID).toEqual( + refTransaction.params.receivingChainID, + ); + if (i > 0) { + const prevTx = response.data[i]; + const prevTxTimestamp = prevTx.block.timestamp; + expect(prevTxTimestamp).toBeGreaterThanOrEqual(transaction.block.timestamp); + } + }); + expect(response.meta).toMap(metaSchema); + } + }); + + it('should throw error when called with invalid receivingChainID', async () => { + for (let i = 0; i < invalidChainIDs.length; i++) { + const response = await api.get(`${endpoint}?receivingChainID=${invalidChainIDs[i]}`, 400); + expect(response).toMap(badRequestSchema); + } + }); + }); + describe('Retrieve transaction list by address', () => { it('should return transactions when called with known address', async () => { const response = await api.get(`${endpoint}?address=${refTransaction.sender.address}`); diff --git a/tests/integration/api_v3/rpc/transactions.test.js b/tests/integration/api_v3/rpc/transactions.test.js index 81ef9eaa91..520050d4dc 100644 --- a/tests/integration/api_v3/rpc/transactions.test.js +++ b/tests/integration/api_v3/rpc/transactions.test.js @@ -18,11 +18,14 @@ import { TRANSACTION_EXECUTION_STATUSES } from '../../../schemas/api_v3/constant import { invalidAddresses, invalidBlockIDs, + invalidChainIDs, invalidLimits, invalidOffsets, } from '../constants/invalidInputs'; import { waitMs } from '../../../helpers/utils'; +jest.setTimeout(1200000); + const config = require('../../../config'); const { request } = require('../../../helpers/socketIoRpcRequest'); @@ -44,32 +47,51 @@ const { const wsRpcUrl = `${config.SERVICE_ENDPOINT}/rpc-v3`; const getTransactions = async params => request(wsRpcUrl, 'get.transactions', params); +const fetchTxWithRetry = async params => { + let retries = 10; + + while (retries > 0) { + try { + const response = await getTransactions({ limit: 1, ...params }); + const [tx] = response.result.data; + + if (tx) { + return { + success: true, + data: tx, + }; + } + } catch (error) { + console.error(`Error fetching transactions. Retries left: ${retries}`); + + // Delay by 3 sec + await waitMs(3000); + } + retries--; + } + + return { + success: false, + }; +}; + describe('Method get.transactions', () => { let refTransaction; beforeAll(async () => { - let retries = 10; - let success = false; - - while (retries > 0 && !success) { - try { - const response = await getTransactions({ moduleCommand: 'token:transfer', limit: 1 }); - [refTransaction] = response.result.data; + const crossChainTxRes = await fetchTxWithRetry({ moduleCommand: 'token:transferCrossChain' }); - if (refTransaction) { - success = true; - } - } catch (error) { - console.error(`Error fetching transactions. Retries left: ${retries}`); - retries--; + // Try to fetch transfer transaction on same chain, incase no transactions with transfer cross chain + if (!crossChainTxRes.success) { + const sameChainTxRes = await fetchTxWithRetry({ moduleCommand: 'token:transfer' }); - // Delay by 3 sec - await waitMs(3000); + if (!sameChainTxRes.success) { + throw new Error('Failed to fetch transactions after 10 retries'); + } else { + refTransaction = sameChainTxRes.data; } - } - - if (!success) { - throw new Error('Failed to fetch transactions after 10 retries'); + } else { + refTransaction = crossChainTxRes.data; } }); @@ -276,6 +298,41 @@ describe('Method get.transactions', () => { }); }); + describe('is able to retrieve list of transactions using receivingChainID', () => { + it('should return transactions when called with known receivingChainID', async () => { + if (refTransaction.params.receivingChainID) { + const response = await getTransactions({ + receivingChainID: refTransaction.params.receivingChainID, + }); + expect(response).toMap(jsonRpcEnvelopeSchema); + const { result } = response; + expect(result.data).toBeInstanceOf(Array); + expect(result.data.length).toBeGreaterThanOrEqual(1); + expect(result.data.length).toBeLessThanOrEqual(10); + expect(response.result).toMap(resultEnvelopeSchema); + result.data.forEach((transaction, i) => { + expect(transaction).toMap(transactionSchema); + expect(transaction.params.receivingChainID).toEqual( + refTransaction.params.receivingChainID, + ); + if (i > 0) { + const prevTx = result.data[i]; + const prevTxTimestamp = prevTx.block.timestamp; + expect(prevTxTimestamp).toBeGreaterThanOrEqual(transaction.block.timestamp); + } + }); + expect(result.meta).toMap(metaSchema); + } + }); + + it('should throw error when called with invalid receivingChainID', async () => { + for (let i = 0; i < invalidChainIDs.length; i++) { + const response = await getTransactions({ receivingChainID: invalidChainIDs[i] }); + expect(response).toMap(invalidParamsSchema); + } + }); + }); + describe('is able to retrieve list of transactions by address', () => { it('should return transactions when called with known address', async () => { const response = await getTransactions({ address: refTransaction.sender.address });