diff --git a/docker-compose.yml b/docker-compose.yml index f8d69e309a..dfc939ea9c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.3" +version: '3.3' services: nats: @@ -458,7 +458,6 @@ services: networks: services_network: - volumes: mysql-primary-data: redis-data: diff --git a/jenkins/mysql-with-replication/docker-compose.yml b/jenkins/mysql-with-replication/docker-compose.yml index a79a8d6164..d62a0b9864 100644 --- a/jenkins/mysql-with-replication/docker-compose.yml +++ b/jenkins/mysql-with-replication/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.3" +version: '3.3' services: mysql-primary: @@ -77,6 +77,5 @@ services: networks: services_network: - volumes: mysql-primary-data: diff --git a/jenkins/redis/docker-compose.yml b/jenkins/redis/docker-compose.yml index 00713d2527..c571e6cf94 100644 --- a/jenkins/redis/docker-compose.yml +++ b/jenkins/redis/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.3" +version: '3.3' services: redis_service_dev: diff --git a/services/blockchain-app-registry/jobs/metadata.js b/services/blockchain-app-registry/jobs/metadata.js index 19da8ab6af..dfc962fb9d 100644 --- a/services/blockchain-app-registry/jobs/metadata.js +++ b/services/blockchain-app-registry/jobs/metadata.js @@ -31,7 +31,7 @@ module.exports = [ await syncWithRemoteRepo(); logger.info('Database has been successfully synchronized.'); } catch (err) { - logger.warn(`Refreshing blockchain application metadata failed due to: ${err.message}`); + logger.warn(`Refreshing blockchain application metadata failed due to: ${err.message}.`); } }, }, diff --git a/services/blockchain-connector/methods/interoperability.js b/services/blockchain-connector/methods/interoperability.js index b15aad13d0..4617abac88 100644 --- a/services/blockchain-connector/methods/interoperability.js +++ b/services/blockchain-connector/methods/interoperability.js @@ -17,6 +17,7 @@ const { getChainAccount, getMainchainID, getChannel, + getChainRegistrationFee, } = require('../shared/sdk'); const regex = require('../shared/utils/regex'); @@ -41,4 +42,9 @@ module.exports = [ chainID: { optional: false, type: 'string', pattern: regex.CHAIN_ID }, }, }, + { + name: 'getChainRegistrationFee', + controller: async () => getChainRegistrationFee(), + params: {}, + }, ]; diff --git a/services/blockchain-connector/shared/sdk/index.js b/services/blockchain-connector/shared/sdk/index.js index d90eee8c4e..824c472194 100644 --- a/services/blockchain-connector/shared/sdk/index.js +++ b/services/blockchain-connector/shared/sdk/index.js @@ -97,6 +97,7 @@ const { getChainAccount, getMainchainID, getChannel, + getRegistrationFee, } = require('./interoperability'); const { getLegacyAccount } = require('./legacy'); @@ -210,6 +211,7 @@ module.exports = { getChainAccount, getMainchainID, getChannel, + getChainRegistrationFee: getRegistrationFee, // Legacy getLegacyAccount, diff --git a/services/blockchain-connector/shared/sdk/interoperability.js b/services/blockchain-connector/shared/sdk/interoperability.js index a5e5ac63d4..1af44329d3 100644 --- a/services/blockchain-connector/shared/sdk/interoperability.js +++ b/services/blockchain-connector/shared/sdk/interoperability.js @@ -24,6 +24,7 @@ const { getNodeInfo } = require('./endpoints_1'); const logger = Logger(); let mainchainID; +let registrationFee; const getChainAccount = async (chainID) => { try { @@ -72,8 +73,24 @@ const getChannel = async (chainID) => { } }; +const getRegistrationFee = async () => { + try { + if (!registrationFee) { + registrationFee = await invokeEndpoint('interoperability_getRegistrationFee'); + } + return registrationFee; + } catch (err) { + if (err.message.includes(timeoutMessage)) { + throw new TimeoutException('Request timed out when calling \'getRegistrationFee\'.'); + } + logger.warn(`Error returned when invoking 'interoperability_getRegistrationFee'.\n${err.stack}`); + throw err; + } +}; + module.exports = { getChainAccount, getMainchainID, getChannel, + getRegistrationFee, }; diff --git a/services/blockchain-indexer/shared/constants.js b/services/blockchain-indexer/shared/constants.js index 4df019de37..cb7fd6568a 100644 --- a/services/blockchain-indexer/shared/constants.js +++ b/services/blockchain-indexer/shared/constants.js @@ -139,6 +139,8 @@ const COMMAND = Object.freeze({ CHANGE_COMMISSION: 'changeCommission', TRANSFER: 'transfer', TRANSFER_CROSS_CHAIN: 'transferCrossChain', + REGISTER_SIDECHAIN: 'registerSidechain', + REGISTER_MAINCHAIN: 'registerMainchain', }); const LENGTH_CHAIN_ID = 4 * 2; // Each byte is represented with 2 nibbles @@ -147,6 +149,9 @@ const PATTERN_ANY_TOKEN_ID = '*'; const PATTERN_ANY_CHAIN_TOKEN_ID = '*'.repeat(LENGTH_TOKEN_LOCAL_ID); const LENGTH_TOKEN_ID = LENGTH_CHAIN_ID + LENGTH_TOKEN_LOCAL_ID; const LENGTH_NETWORK_ID = 1 * 2; // Each byte is represented with 2 nibbles +const LENGTH_BYTE_SIGNATURE = 64; +const LENGTH_BYTE_ID = 32; +const DEFAULT_NUM_OF_SIGNATURES = 1; const MAX_COMMISSION = BigInt('10000'); @@ -205,4 +210,7 @@ module.exports = { KV_STORE_KEY, TRANSACTION_STATUS, TRANSACTION_VERIFY_RESULT, + LENGTH_BYTE_SIGNATURE, + LENGTH_BYTE_ID, + DEFAULT_NUM_OF_SIGNATURES, }; diff --git a/services/blockchain-indexer/shared/dataService/business/interoperability/constants.js b/services/blockchain-indexer/shared/dataService/business/interoperability/constants.js index 7b78578604..08c4af9918 100644 --- a/services/blockchain-indexer/shared/dataService/business/interoperability/constants.js +++ b/services/blockchain-indexer/shared/dataService/business/interoperability/constants.js @@ -13,6 +13,10 @@ * Removal or modification of this copyright notice is prohibited. * */ +const { requestConnector } = require('../../../utils/request'); + +let moduleConstants; + const APP_STATUS = { ACTIVE: 'active', REGISTERED: 'registered', @@ -26,7 +30,23 @@ const CHAIN_STATUS = Object.freeze({ 2: 'terminated', }); +const getInteroperabilityConstants = async () => { + if (typeof moduleConstants === 'undefined') { + const registrationFee = await requestConnector('getChainRegistrationFee'); + + moduleConstants = { + chainRegistrationFee: registrationFee.fee, + }; + } + + return { + data: moduleConstants, + meta: {}, + }; +}; + module.exports = { APP_STATUS, CHAIN_STATUS, + getInteroperabilityConstants, }; diff --git a/services/blockchain-indexer/shared/dataService/business/transactionsEstimateFees.js b/services/blockchain-indexer/shared/dataService/business/transactionsEstimateFees.js index a4f1ac2f33..3e2cbde07f 100644 --- a/services/blockchain-indexer/shared/dataService/business/transactionsEstimateFees.js +++ b/services/blockchain-indexer/shared/dataService/business/transactionsEstimateFees.js @@ -21,14 +21,21 @@ const { const { HTTP, Exceptions: { ValidationException }, + Logger, } = require('lisk-service-framework'); -const { getAuthAccountInfo } = require('./auth'); const { resolveMainchainServiceURL, resolveChannelInfo } = require('./mainchain'); const { dryRunTransactions } = require('./transactionsDryRun'); const { tokenHasUserAccount, getTokenConstants } = require('./token'); -const { MODULE, COMMAND, EVENT } = require('../../constants'); +const { + MODULE, + COMMAND, + EVENT, + LENGTH_BYTE_SIGNATURE, + LENGTH_BYTE_ID, + DEFAULT_NUM_OF_SIGNATURES, +} = require('../../constants'); const { getLisk32AddressFromPublicKey } = require('../../utils/account'); const { parseToJSONCompatObj } = require('../../utils/parser'); @@ -36,12 +43,12 @@ const { requestConnector } = require('../../utils/request'); const config = require('../../../config'); const { getPosConstants } = require('./pos/constants'); +const { getInteroperabilityConstants } = require('./interoperability/constants'); const { getFeeEstimates } = require('./feeEstimates'); const regex = require('../../regex'); -const SIZE_BYTE_SIGNATURE = 64; -const SIZE_BYTE_ID = 32; -const DEFAULT_NUM_OF_SIGNATURES = 1; +const logger = Logger(); + const { bufferBytesLength: BUFFER_BYTES_LENGTH } = config.estimateFees; const OPTIONAL_TRANSACTION_PROPERTIES = Object.freeze({ @@ -53,11 +60,11 @@ const OPTIONAL_TRANSACTION_PROPERTIES = Object.freeze({ propName: 'signatures', defaultValue: (params) => new Array(params.numberOfSignatures) .fill() - .map(() => getRandomBytes(SIZE_BYTE_SIGNATURE).toString('hex')), + .map(() => getRandomBytes(LENGTH_BYTE_SIGNATURE).toString('hex')), }, ID: { propName: 'id', - defaultValue: () => getRandomBytes(SIZE_BYTE_ID).toString('hex'), + defaultValue: () => getRandomBytes(LENGTH_BYTE_ID).toString('hex'), }, }); @@ -84,36 +91,58 @@ const mockOptionalProperties = (inputObject, inputObjectOptionalProps, additiona return inputObject; }; -const mockTransaction = async (_transaction, authAccountInfo) => { - const transaction = _.cloneDeep(_transaction); +const filterOptionalProps = (inputObject, optionalProps) => _.omit(inputObject, optionalProps); + +const mockTransaction = async (_transaction, numberOfSignatures) => { + if (!_transaction) throw new Error('No transaction passed.'); - const numberOfSignatures = (authAccountInfo.mandatoryKeys.length - + authAccountInfo.optionalKeys.length) || DEFAULT_NUM_OF_SIGNATURES; + const transactionCopy = _.cloneDeep(_transaction); + const txWithRequiredProps = filterOptionalProps( + transactionCopy, + Object.values(OPTIONAL_TRANSACTION_PROPERTIES).map(optionalProp => optionalProp.propName), + ); const mockedTransaction = mockOptionalProperties( - transaction, + txWithRequiredProps, OPTIONAL_TRANSACTION_PROPERTIES, { numberOfSignatures }, ); - const channelInfo = transaction.module === MODULE.TOKEN - && transaction.command === COMMAND.TRANSFER_CROSS_CHAIN - ? await resolveChannelInfo(transaction.params.receivingChainID) + const channelInfo = txWithRequiredProps.module === MODULE.TOKEN + && txWithRequiredProps.command === COMMAND.TRANSFER_CROSS_CHAIN + ? await resolveChannelInfo(txWithRequiredProps.params.receivingChainID) : {}; const { messageFeeTokenID } = channelInfo; const mockedTransactionParams = messageFeeTokenID ? mockOptionalProperties( - transaction.params, + filterOptionalProps( + txWithRequiredProps.params, + Object + .values(OPTIONAL_TRANSACTION_PARAMS_PROPERTIES) + .map(optionalProp => optionalProp.propName), + ), OPTIONAL_TRANSACTION_PARAMS_PROPERTIES, { messageFeeTokenID }, ) - : transaction.params; + : txWithRequiredProps.params; return { ...mockedTransaction, params: mockedTransactionParams }; }; +const getNumberOfSignatures = async (address) => { + try { + const authAccountInfo = await requestConnector('getAuthAccount', { address }); + const numberOfSignatures = (authAccountInfo.mandatoryKeys.length + + authAccountInfo.optionalKeys.length) || DEFAULT_NUM_OF_SIGNATURES; + return numberOfSignatures; + } catch (error) { + logger.warn(`Error while retrieving auth information for the account ${address}.`); + return DEFAULT_NUM_OF_SIGNATURES; + } +}; + const getCcmBuffer = async (transaction) => { if (transaction.module !== MODULE.TOKEN || transaction.command !== COMMAND.TRANSFER_CROSS_CHAIN) return null; @@ -202,6 +231,14 @@ const calcAdditionalFees = async (transaction) => { }; additionalFees.total += BigInt(posConstants.data.validatorRegistrationFee); } + } else if (transaction.module === MODULE.INTEROPERABILITY) { + if ([COMMAND.REGISTER_MAINCHAIN, COMMAND.REGISTER_SIDECHAIN].includes(transaction.command)) { + const interoperabilityConstants = await getInteroperabilityConstants(); + additionalFees.fee = { + chainRegistrationFee: interoperabilityConstants.data.chainRegistrationFee, + }; + additionalFees.total += BigInt(interoperabilityConstants.data.chainRegistrationFee); + } } return additionalFees; @@ -226,9 +263,9 @@ const estimateTransactionFees = async params => { } const senderAddress = getLisk32AddressFromPublicKey(params.transaction.senderPublicKey); - const { data: authAccountInfo } = await getAuthAccountInfo({ address: senderAddress }); + const numberOfSignatures = await getNumberOfSignatures(senderAddress); - const trxWithMockProps = await mockTransaction(params.transaction, authAccountInfo); + const trxWithMockProps = await mockTransaction(params.transaction, numberOfSignatures); const additionalFees = await calcAdditionalFees(trxWithMockProps); let formattedTransaction = await requestConnector( 'formatTransaction', @@ -293,18 +330,9 @@ const estimateTransactionFees = async params => { const { minFee, size } = formattedTransaction; - const estimateMinFee = (() => { - const totalAdditionalFees = Object.entries(additionalFees.fee).reduce((acc, [k, v]) => { - if (k.toLowerCase().endsWith('fee')) { - return acc + BigInt(v); - } - return acc; - }, BigInt(0)); - - // TODO: Remove BUFFER_BYTES_LENGTH support after https://github.com/LiskHQ/lisk-service/issues/1604 is complete - return BigInt(minFee) + totalAdditionalFees - + BigInt(BUFFER_BYTES_LENGTH * feeEstimatePerByte.minFeePerByte); - })(); + // TODO: Remove BUFFER_BYTES_LENGTH support after RC is tagged + const estimatedMinFee = BigInt(minFee) + + BigInt(BUFFER_BYTES_LENGTH * feeEstimatePerByte.minFeePerByte); // Populate the response with transaction minimum fee information estimateTransactionFeesRes.data = { @@ -313,7 +341,7 @@ const estimateTransactionFees = async params => { ...estimateTransactionFeesRes.data.transaction, fee: { tokenID: feeEstimatePerByte.feeTokenID, - minimum: estimateMinFee.toString(), + minimum: estimatedMinFee.toString(), }, }, }; @@ -335,11 +363,11 @@ const estimateTransactionFees = async params => { }, }; - // Add priority only when fee values are not 0 + // Add priority only when the priority fee values are non-zero const { low, med, high } = feeEstimatePerByte; if (low !== 0 || med !== 0 || high !== 0) { const dynamicFeeEstimates = calcDynamicFeeEstimates( - feeEstimatePerByte, estimateMinFee, size); + feeEstimatePerByte, estimatedMinFee, size); estimateTransactionFeesRes.transaction.fee.priority = dynamicFeeEstimates; } @@ -354,4 +382,6 @@ module.exports = { calcDynamicFeeEstimates, mockTransaction, calcAdditionalFees, + filterOptionalProps, + getNumberOfSignatures, }; diff --git a/services/blockchain-indexer/shared/dataService/pos/validators.js b/services/blockchain-indexer/shared/dataService/pos/validators.js index 432225de1a..5a89ee6c00 100644 --- a/services/blockchain-indexer/shared/dataService/pos/validators.js +++ b/services/blockchain-indexer/shared/dataService/pos/validators.js @@ -83,7 +83,7 @@ const computeValidatorStatus = async () => { const MIN_ELIGIBLE_VOTE_WEIGHT = Transactions.convertLSKToBeddows('1000'); - const latestBlock = getLastBlock(); + const latestBlock = await getLastBlock(); const generatorsList = await business.getGenerators(); const generatorMap = new Map(generatorsList.map(generator => [generator.address, generator])); diff --git a/services/blockchain-indexer/tests/unit/shared/constants/transactionEstimateFees.js b/services/blockchain-indexer/tests/unit/shared/constants/transactionEstimateFees.js index 2bd53a4f36..0f5c7cb19b 100644 --- a/services/blockchain-indexer/tests/unit/shared/constants/transactionEstimateFees.js +++ b/services/blockchain-indexer/tests/unit/shared/constants/transactionEstimateFees.js @@ -143,8 +143,10 @@ const mockInteroperabilityRegisterSidechainTxResult = { breakdown: { fee: { minimum: { - byteFee: '167000', - additionalFees: {}, + byteFee: '166000', + additionalFees: { + chainRegistrationFee: '100000000', + }, }, }, }, @@ -156,7 +158,7 @@ const mockTxResult = { transaction: { fee: { tokenID: '0400000000000000', - minimum: '130001', + minimum: '130000', }, params: {}, }, @@ -304,6 +306,18 @@ const mockEscrowAccountExistsRequestConnector = { }, }; +const mockAuthAccountInfo = { numberOfSignatures: 0, mandatoryKeys: [], optionalKeys: [] }; + +const mockAuthInfoForMultisigAccount = { + ...mockAuthAccountInfo, + mandatoryKeys: [ + '4d9c2774f1c98accafb8554c164ce5689f66a32d768b64a9f694d5bd51dc1b4d', + ], + optionalKeys: [ + 'b1353e202043ead83083ce8b7eb3a9d04fb49cdcf8c73c0e81567d55d114c076', + ], +}; + module.exports = { mockTxRequest, mockTransferCrossChainTxRequest, @@ -316,6 +330,8 @@ module.exports = { mockEscrowAccountExistsRequestConnector, mockTransferCrossChainTxrequestConnector, mockTransferCrossChainTxResult, + mockAuthAccountInfo, + mockAuthInfoForMultisigAccount, mockInteroperabilitySubmitMainchainCrossChainUpdateTxRequest, mockInteroperabilitySubmitMainchainCrossChainUpdateTxResult, 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 8e5c63eb22..204c074a8e 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 @@ -43,6 +43,8 @@ const { mockInteroperabilitySubmitMainchainCrossChainUpdateTxResult, mockInteroperabilityRegisterSidechainTxRequest, mockInteroperabilityRegisterSidechainTxResult, + mockAuthAccountInfo, + mockAuthInfoForMultisigAccount, } = require('../../constants/transactionEstimateFees'); jest.mock('lisk-service-framework', () => { @@ -221,7 +223,10 @@ describe('Test transaction fees estimates', () => { mockTransaction, } = require(mockedTransactionFeeEstimatesFilePath); - const transaction = await mockTransaction(inputTransaction, authAccountInfo); + const transaction = await mockTransaction( + inputTransaction, + authAccountInfo.numberOfSignatures, + ); expect(transaction).toMatchObject(inputTransaction); }); @@ -232,7 +237,7 @@ describe('Test transaction fees estimates', () => { const transaction = await mockTransaction( inputMultisigTransaction, - authInfoForMultisigAccount, + authInfoForMultisigAccount.numberOfSignatures, ); const expectedResponse = { @@ -252,7 +257,7 @@ describe('Test transaction fees estimates', () => { } = require(mockedTransactionFeeEstimatesFilePath); const { id, ...remParams } = inputTransaction; - const transaction = await mockTransaction(remParams, authAccountInfo); + const transaction = await mockTransaction(remParams, authAccountInfo.numberOfSignatures); const expectedResponse = { ...inputTransaction, @@ -268,7 +273,7 @@ describe('Test transaction fees estimates', () => { } = require(mockedTransactionFeeEstimatesFilePath); const { fee, ...remParams } = inputTransaction; - const transaction = await mockTransaction(remParams, authAccountInfo); + const transaction = await mockTransaction(remParams, authAccountInfo.numberOfSignatures); const expectedResponse = { ...inputTransaction, @@ -284,7 +289,10 @@ describe('Test transaction fees estimates', () => { } = require(mockedTransactionFeeEstimatesFilePath); const { signatures, ...remParams } = inputMultisigTransaction; - const transaction = await mockTransaction(remParams, authInfoForMultisigAccount); + const transaction = await mockTransaction( + remParams, + authInfoForMultisigAccount.numberOfSignatures, + ); const expectedResponse = { ...inputMultisigTransaction, @@ -304,7 +312,7 @@ describe('Test transaction fees estimates', () => { const transaction = await mockTransaction( { ...inputTransaction, params: remTransactionParams }, - authAccountInfo, + authAccountInfo.numberOfSignatures, ); const expectedResponse = { @@ -324,7 +332,7 @@ describe('Test transaction fees estimates', () => { const transaction = await mockTransaction( { ...inputTransaction, params: remTransactionParams }, - authAccountInfo, + authAccountInfo.numberOfSignatures, ); const expectedResponse = { @@ -340,7 +348,7 @@ describe('Test transaction fees estimates', () => { mockTransaction, } = require(mockedTransactionFeeEstimatesFilePath); - expect(async () => mockTransaction(undefined)).rejects.toThrow(TypeError); + expect(async () => mockTransaction(undefined)).rejects.toThrow(); }); it('should throw error when transaction is null', async () => { @@ -348,11 +356,12 @@ describe('Test transaction fees estimates', () => { mockTransaction, } = require(mockedTransactionFeeEstimatesFilePath); - expect(async () => mockTransaction(null)).rejects.toThrow(TypeError); + expect(async () => mockTransaction(null)).rejects.toThrow(); }); }); describe('Test estimateTransactionFees method', () => { + afterEach(() => jest.clearAllMocks()); jest.resetModules(); // Mock the dependencies @@ -429,6 +438,7 @@ describe('Test transaction fees estimates', () => { getLisk32AddressFromPublicKey.mockReturnValue(mockTxsenderAddress); getAuthAccountInfo.mockResolvedValue(mockTxAuthAccountInfo); requestConnector + .mockReturnValueOnce(mockAuthAccountInfo) .mockReturnValueOnce(mockEscrowAccountExistsRequestConnector) .mockReturnValueOnce(mockTransferCrossChainTxrequestConnector) .mockReturnValueOnce('encoded CCM Object'); @@ -463,11 +473,9 @@ describe('Test transaction fees estimates', () => { }); it('should throw when getAuthAccountInfo fails', async () => { - getAuthAccountInfo.mockRejectedValue('Error'); - // Mock the return values of the functions getLisk32AddressFromPublicKey.mockReturnValue(mockTxsenderAddress); - requestConnector.mockResolvedValue(mockTxrequestConnector); + requestConnector.mockRejectedValue('Error'); getFeeEstimates.mockReturnValue(mockTxFeeEstimate); calcAdditionalFees.mockResolvedValue({}); calcMessageFee.mockResolvedValue({}); @@ -566,7 +574,7 @@ describe('Test transaction fees estimates', () => { getAuthAccountInfo.mockResolvedValue(mockTxAuthAccountInfo); requestConnector .mockReturnValueOnce(mockTxrequestConnector) - .mockReturnValue({ userAccount: '1', escrowAccount: '0' }); + .mockReturnValue({ userAccount: '1', escrowAccount: '0', fee: '100000000', minFee: '166000', size: 166 }); getFeeEstimates.mockReturnValue(mockTxFeeEstimate); calcAdditionalFees.mockResolvedValue({}); calcMessageFee.mockResolvedValue({}); @@ -585,4 +593,85 @@ describe('Test transaction fees estimates', () => { }); }); }); + + describe('Test filterOptionalProps method', () => { + const optionalProps = ['id', 'fee', 'signatures']; + + it('should return object with required properties', async () => { + const { + filterOptionalProps, + } = require(mockedTransactionFeeEstimatesFilePath); + + const input = { + module: 'token', + command: 'transferCrossChain', + fee: '217000', + nonce: '7', + senderPublicKey: '3972849f2ab66376a68671c10a00e8b8b67d880434cc65b04c6ed886dfa91c2c', + id: '0448028b3b0717776d4db10fc64dacf8096298377b31f740d73c9e9859ea4a26', + signatures: [ + '7185e6e035c7804c5cbb166cb85581bc9beafd0fe2ecac9b83cb514c32ddc64ac546f16a1fba372f74493261658d1c8deac234e37b8ca23c4fd396e44e33fd0d', + ], + }; + + const expectedResponse = { + module: 'token', + command: 'transferCrossChain', + nonce: '7', + senderPublicKey: '3972849f2ab66376a68671c10a00e8b8b67d880434cc65b04c6ed886dfa91c2c', + }; + + const txWithRequiredProps = filterOptionalProps(input, optionalProps); + expect(txWithRequiredProps).toMatchObject(expectedResponse); + }); + + it('should return an object when input is undefined', async () => { + const { + filterOptionalProps, + } = require(mockedTransactionFeeEstimatesFilePath); + + const txWithRequiredProps = filterOptionalProps(undefined, optionalProps); + expect(Object.getOwnPropertyNames(txWithRequiredProps).length).toBe(0); + }); + + it('should return an object when input is null', async () => { + const { + filterOptionalProps, + } = require(mockedTransactionFeeEstimatesFilePath); + + const txWithRequiredProps = filterOptionalProps(null, optionalProps); + expect(Object.getOwnPropertyNames(txWithRequiredProps).length).toBe(0); + }); + }); + + describe('Test getNumberOfSignatures method', () => { + // Mock the dependencies + const { requestConnector } = require(mockedRequestFilePath); + + jest.mock(mockedRequestFilePath, () => ({ + requestConnector: jest.fn(), + })); + + it('should return number of signatures as 1 for non-multiSig account', async () => { + // Mock the return values of the functions + requestConnector.mockReturnValueOnce(mockAuthAccountInfo); + + const { getNumberOfSignatures } = require(mockedTransactionFeeEstimatesFilePath); + + // Call the function + const result = await getNumberOfSignatures(mockTxsenderAddress); + expect(result).toEqual(1); + }); + + it('should return number of signatures as 2 for multiSig account', async () => { + // Mock the return values of the functions + requestConnector.mockReturnValueOnce(mockAuthInfoForMultisigAccount); + + const { getNumberOfSignatures } = require(mockedTransactionFeeEstimatesFilePath); + + // Call the function + const result = await getNumberOfSignatures(mockTxsenderAddress); + expect(result).toEqual(2); + }); + }); }); diff --git a/services/gateway/apis/http-version3/methods/transactionsDryRun.js b/services/gateway/apis/http-version3/methods/transactionsDryRun.js index 0fdb362ce3..37513de10f 100644 --- a/services/gateway/apis/http-version3/methods/transactionsDryRun.js +++ b/services/gateway/apis/http-version3/methods/transactionsDryRun.js @@ -38,7 +38,7 @@ module.exports = { nonce: { type: 'string', min: 1, pattern: regex.NONCE }, senderPublicKey: { type: 'string', pattern: regex.PUBLIC_KEY }, signatures: { type: 'array', optional: true, min: 0, items: { type: 'string', pattern: regex.HASH_SHA512 } }, - params: { type: 'object', optional: false, minProps: 1 }, + params: { type: 'object', optional: false, minProps: 0 }, }, }, ], diff --git a/services/gateway/apis/http-version3/methods/transactionsEstimateFees.js b/services/gateway/apis/http-version3/methods/transactionsEstimateFees.js index 4c18cd9c36..da539d54aa 100644 --- a/services/gateway/apis/http-version3/methods/transactionsEstimateFees.js +++ b/services/gateway/apis/http-version3/methods/transactionsEstimateFees.js @@ -29,13 +29,16 @@ module.exports = { optional: false, type: 'object', props: { + id: { optional: true, type: 'string', pattern: regex.HASH_SHA256 }, module: { optional: false, type: 'string', pattern: regex.MODULE }, command: { optional: false, type: 'string', pattern: regex.COMMAND }, + fee: { optional: true, type: 'string', pattern: regex.FEE }, nonce: { optional: false, type: 'string', pattern: regex.NONCE }, senderPublicKey: { optional: false, type: 'string', pattern: regex.PUBLIC_KEY }, - signatures: { optional: true, type: 'array', min: 1, items: { type: 'string', pattern: regex.HASH_SHA512 } }, - params: { optional: false, type: 'object' }, + signatures: { optional: true, type: 'array', min: 0, items: { type: 'string', pattern: regex.HASH_SHA512 } }, + params: { optional: false, type: 'object', minProps: 0 }, }, + altSwaggerKey: 'transactionEstimateFees', }, }, get schema() { diff --git a/services/gateway/apis/http-version3/swagger/definitions/transactions.json b/services/gateway/apis/http-version3/swagger/definitions/transactions.json index 1130b1eb5c..b7b0b6eb15 100644 --- a/services/gateway/apis/http-version3/swagger/definitions/transactions.json +++ b/services/gateway/apis/http-version3/swagger/definitions/transactions.json @@ -564,6 +564,9 @@ }, "transactionEstimateFeesRequestBody": { "type": "object", + "required": [ + "transaction" + ], "properties": { "transaction": { "type": "object", @@ -576,6 +579,11 @@ "params" ], "properties": { + "id": { + "type": "string", + "example": "f9593f101c4acafc3ede650ab4c10fa2ecb59b225813eddbbb17b47e96932e9b", + "description": "Unique identifier of the transaction." + }, "module": { "type": "string", "example": "token", @@ -590,6 +598,11 @@ "type": "string", "example": "0" }, + "fee": { + "type": "string", + "example": "1000000", + "description": "Transaction fee." + }, "senderPublicKey": { "type": "string", "example": "a3f96c50d0446220ef2f98240898515cbba8155730679ca35326d98dcfb680f0", @@ -598,7 +611,6 @@ "signatures": { "type": "array", "description": "An array representing signature(s) of the transaction sender.", - "minItems": 1, "items": { "type": "string" }, @@ -633,6 +645,16 @@ "type": "string", "example": "Cross chain transfer tx", "description": "Transaction data." + }, + "messageFee": { + "type": "string", + "example": "1000000", + "description": "Fee to be used for transaction processing in the receiving chain." + }, + "messageFeeTokenID": { + "type": "string", + "example": "0000000000000000", + "description": "Token used for cross-chain message fee." } } } @@ -782,6 +804,11 @@ "example": "10000000", "description": "The extra fee to be supplied for initializing an escrow token account." }, + "chainRegistrationFee": { + "type": "string", + "example": "1000000000", + "description": "The extra fee to be supplied for a chain registration transaction." + }, "bufferBytes": { "type": "string", "example": "6000", diff --git a/tests/integration/api_v3/constants/transactionsDryRun.js b/tests/integration/api_v3/constants/transactionsDryRun.js index 8b25006689..103fbd47ca 100644 --- a/tests/integration/api_v3/constants/transactionsDryRun.js +++ b/tests/integration/api_v3/constants/transactionsDryRun.js @@ -93,6 +93,19 @@ const UNSIGNED_TRANSACTION_OBJECT = { id: 'd96c777b67576ddf4cd933a97a60b4311881e68e3c8bef1393ac0020ec8a506c', }; +const TRANSACTION_OBJECT_VALID_WITH_REQUIRED_PROPS = { + module: 'token', + command: 'transfer', + nonce: '1', + senderPublicKey: '3972849f2ab66376a68671c10a00e8b8b67d880434cc65b04c6ed886dfa91c2c', + params: { + amount: '1000000000000', + recipientAddress: 'lskv6v53emsaen6cwbbk226wusdpa6ojdonunka4x', + data: '', + tokenID: '0400000000000000', + }, +}; + module.exports = { TRANSACTION_ENCODED_VALID, TRANSACTION_ENCODED_INVALID, @@ -101,4 +114,5 @@ module.exports = { TRANSACTION_OBJECT_VALID, TRANSACTION_OBJECT_PENDING, UNSIGNED_TRANSACTION_OBJECT, + TRANSACTION_OBJECT_VALID_WITH_REQUIRED_PROPS, }; diff --git a/tests/integration/api_v3/http/transactionsEstimateFees.test.js b/tests/integration/api_v3/http/transactionsEstimateFees.test.js index d19a855675..8261457fe9 100644 --- a/tests/integration/api_v3/http/transactionsEstimateFees.test.js +++ b/tests/integration/api_v3/http/transactionsEstimateFees.test.js @@ -18,6 +18,7 @@ const { api } = require('../../../helpers/api'); const { TRANSACTION_OBJECT_VALID, TRANSACTION_ENCODED_VALID, + TRANSACTION_OBJECT_VALID_WITH_REQUIRED_PROPS, } = require('../constants/transactionsDryRun'); const { transactionsMap } = require('../constants/transactionsEstimateFees'); @@ -49,11 +50,19 @@ const getSumOfMetaValues = (meta) => { }; describe('Post estimate-fees transactions API', () => { - it('should return transaction fees with valid transaction object', async () => { + it('should return transaction fees with valid transaction object with all properties', async () => { const response = await api.post(endpoint, { transaction: TRANSACTION_OBJECT_VALID }); expect(response).toMap(transactionEstimateFees); }); + it('should return transaction fees with valid transaction object with only required properties', async () => { + const response = await api.post( + endpoint, + { transaction: TRANSACTION_OBJECT_VALID_WITH_REQUIRED_PROPS }, + ); + expect(response).toMap(transactionEstimateFees); + }); + it('should return transaction fees with valid transaction object without id', async () => { const { id, ...remTransactionObject } = TRANSACTION_OBJECT_VALID; const response = await api.post(endpoint, { transaction: remTransactionObject }); diff --git a/tests/integration/api_v3/rpc/transactionsEstimateFees.test.js b/tests/integration/api_v3/rpc/transactionsEstimateFees.test.js index b6d1e8071b..44806a7e99 100644 --- a/tests/integration/api_v3/rpc/transactionsEstimateFees.test.js +++ b/tests/integration/api_v3/rpc/transactionsEstimateFees.test.js @@ -17,6 +17,7 @@ const config = require('../../../config'); const { TRANSACTION_OBJECT_VALID, TRANSACTION_ENCODED_VALID, + TRANSACTION_OBJECT_VALID_WITH_REQUIRED_PROPS, } = require('../constants/transactionsDryRun'); const { transactionsMap } = require('../constants/transactionsEstimateFees'); @@ -52,7 +53,7 @@ const wsRpcUrl = `${config.SERVICE_ENDPOINT}/rpc-v3`; const calculateTransactionFees = async params => request(wsRpcUrl, 'post.transactions.estimate-fees', params); describe('Method post.transactions.estimate-fees', () => { - it('should return transaction fees when called with valid transaction object', async () => { + it('should return transaction fees when called with valid transaction object with all properties', async () => { const response = await calculateTransactionFees({ transaction: TRANSACTION_OBJECT_VALID }); expect(response).toMap(jsonRpcEnvelopeSchema); @@ -60,6 +61,16 @@ describe('Method post.transactions.estimate-fees', () => { expect(result).toMap(transactionEstimateFees); }); + it('should return transaction fees when called with valid transaction object with only required properties', async () => { + const response = await calculateTransactionFees({ + transaction: TRANSACTION_OBJECT_VALID_WITH_REQUIRED_PROPS, + }); + expect(response).toMap(jsonRpcEnvelopeSchema); + + const { result } = response; + expect(result).toMap(transactionEstimateFees); + }); + it('should return transaction fees when called with valid transaction object without id', async () => { const { id, ...remTransactionObject } = TRANSACTION_OBJECT_VALID; const response = await calculateTransactionFees({ transaction: remTransactionObject });