diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1cac9ac3..11385fd0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -98,7 +98,7 @@ jobs: DEFAULT_SERVER: https://node1.mainnet.hathor.network/v1a/ VOIDED_TX_OFFSET: 5 WS_DOMAIN: ws.wallet-service.hathor.network - AUTH_SECRET: "" + AUTH_SECRET: "foobar" WALLET_SERVICE_LAMBDA_ENDPOINT: "" FIREBASE_PROJECT_ID: "" FIREBASE_PRIVATE_KEY_ID: "" @@ -114,6 +114,9 @@ jobs: ALERT_MANAGER_REGION: us-east-1 ALERT_MANAGER_TOPIC: alert-topic PUSH_ALLOWED_PROVIDERS: "" + REDIS_URL: redis://127.0.0.1:6379 + REDIS_PASSWORD: "" + IS_OFFLINE: 'true' - name: Run integration tests on the daemon run: | diff --git a/db/migrations/20250306170811-node_version_proxy.js b/db/migrations/20250306170811-node_version_proxy.js new file mode 100644 index 00000000..9e5e0e65 --- /dev/null +++ b/db/migrations/20250306170811-node_version_proxy.js @@ -0,0 +1,33 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + // Dropping the table and recreating it is easier than deleting the fields then adding the new + // one, there is also no danger of losing any data since this is fetched from the fullnode + await queryInterface.dropTable('version_data'); + await queryInterface.createTable('version_data', { + id: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + primaryKey: true, + defaultValue: 1, + }, + timestamp: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + data: { + type: Sequelize.TEXT, + allowNull: false, + }, + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.dropTable('version_data'); + // The "old" table structure was copied from ./20210706175820-create-version-data.js + const { up } = require('./20210706175820-create-version-data'); + await up(queryInterface, Sequelize); + } +}; diff --git a/package.json b/package.json index 911a7b95..497129d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet-service", - "version": "1.7.0", + "version": "1.8.0", "workspaces": [ "packages/common", "packages/daemon", @@ -33,7 +33,7 @@ "@aws-sdk/client-apigatewaymanagementapi": "3.540.0", "@aws-sdk/client-lambda": "3.540.0", "@aws-sdk/client-sqs": "3.540.0", - "@hathor/wallet-lib": "0.39.0", + "@hathor/wallet-lib": "1.14.1", "@wallet-service/common": "1.5.0", "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.5", diff --git a/packages/common/__tests__/events/nftCreationTx.ts b/packages/common/__tests__/events/nftCreationTx.ts index 0c91445b..4fcd98bb 100644 --- a/packages/common/__tests__/events/nftCreationTx.ts +++ b/packages/common/__tests__/events/nftCreationTx.ts @@ -12,7 +12,9 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { Context } from 'aws-lambda'; -import { Transaction, TxOutput } from '../../src/types'; +import { TxOutput } from '../../src/types'; +// FIXME: import from lib path on HathorLib +import { HistoryTransaction } from '@hathor/wallet-lib/lib/models/types'; /** * A sample transaction for a NFT creation, as obtained by a wallet's history methods @@ -115,12 +117,12 @@ export const nftCreationTx = { /** * Gets a copy of the `nftCreationTx` in the Wallet Service's Transaction format. */ -export function getTransaction(): Transaction { - const result = { +export function getTransaction(): HistoryTransaction { + return { tx_id: nftCreationTx.tx_id, nonce: 1, timestamp: nftCreationTx.timestamp, - signal_bits: nftCreationTx.signal_bits, + signalBits: nftCreationTx.signal_bits, version: nftCreationTx.version, weight: nftCreationTx.weight, parents: nftCreationTx.parents, @@ -150,12 +152,11 @@ export function getTransaction(): Transaction { token_data: o.token_data, locked: false, })) as TxOutput[], - height: 8, token_name: nftCreationTx.token_name, token_symbol: nftCreationTx.token_symbol, + is_voided: nftCreationTx.is_voided, + tokens: nftCreationTx.tokens, }; - - return result; } /** diff --git a/packages/common/__tests__/utils/nft.utils.test.ts b/packages/common/__tests__/utils/nft.utils.test.ts index c678bdb4..99f2e5e2 100644 --- a/packages/common/__tests__/utils/nft.utils.test.ts +++ b/packages/common/__tests__/utils/nft.utils.test.ts @@ -1,8 +1,7 @@ -// @ts-ignore: Using old wallet-lib version, no types exported -import hathorLib from '@hathor/wallet-lib'; +import hathorLib, { constants, Network } from '@hathor/wallet-lib'; import { mockedAddAlert } from './alerting.utils.mock'; import { NftUtils } from '@src/utils/nft.utils'; -import { Severity } from '@src/types'; +import { FullNodeTransaction, Severity } from '@src/types'; import { getHandlerContext, getTransaction } from '../events/nftCreationTx'; import { LambdaClient as LambdaClientMock, @@ -12,16 +11,11 @@ import { Logger } from 'winston'; jest.mock('winston', () => { class FakeLogger { - warn() { - return jest.fn(); - } - error() { - return jest.fn(); - } - info() { - return jest.fn(); - } - }; + warn = jest.fn(); + error = jest.fn(); + info = jest.fn(); + debug = jest.fn(); + } return { Logger: FakeLogger, @@ -49,23 +43,106 @@ jest.mock('@src/utils/index.utils', () => { const network = new hathorLib.Network('testnet'); const logger = new Logger(); +// Real event data from production +const REAL_NFT_EVENT_DATA = { + 'hash': '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866', + 'nonce': 257857, + 'timestamp': 1741649846, + 'signal_bits': 0, + 'version': 2, + 'weight': 17.30946054227969, + 'inputs': [ + { + 'tx_id': '00000000ba6f3fc01a3e8561f2905c50c98422e7112604a8971bdaba1535e797', + 'index': 1, + 'spent_output': { + 'value': 4, + 'token_data': 0, + 'script': 'dqkUWDMJLPqtb9X+jPcBSP6WLg6NIC6IrA==', + 'decoded': { + 'type': 'P2PKH', + 'address': 'WWiPUqkLJbb6YMQRgHWPMBS6voJjpeWqas', + 'timelock': null + } + } + } + ], + 'outputs': [ + { + 'value': 1, + 'token_data': 0, + 'script': 'C2lwZnM6Ly8xMTExrA==', + 'decoded': null + }, + { + 'value': 2, + 'token_data': 0, + 'script': 'dqkUFUs/hBsLnxy5Jd94WWV24BCmIhmIrA==', + 'decoded': { + 'type': 'P2PKH', + 'address': 'WQcdDHZriSQwE4neuzf9UW2xJkdhEqrt7F', + 'timelock': null + } + }, + { + 'value': 1, + 'token_data': 1, + 'script': 'dqkUhM3YhAjNc5p/oqX+yqEYcX+miNmIrA==', + 'decoded': { + 'type': 'P2PKH', + 'address': 'WanEffTDdFo8giEj2CuNqGWsEeWjU7crnF', + 'timelock': null + } + } + ], + 'tokens': [ + '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866' + ], + 'token_name': 'Test', + 'token_symbol': 'TST' +}; + +// Create the transformed version of the event as it would be processed +const createTransformedEvent = (fullNodeData = REAL_NFT_EVENT_DATA) => { + return NftUtils.transformFullNodeTxForNftDetection(fullNodeData); +}; + describe('shouldInvokeNftHandlerForTx', () => { + let originalEnv: NodeJS.ProcessEnv; + let isNftTransactionSpy: jest.SpyInstance; + + beforeEach(() => { + originalEnv = process.env; + process.env = { ...originalEnv }; + isNftTransactionSpy = jest.spyOn(NftUtils, 'isTransactionNFTCreation').mockReturnValue(true); + }); + + afterEach(() => { + process.env = originalEnv; + isNftTransactionSpy.mockRestore(); + }); + it('should return false for a NFT transaction if the feature is disabled', () => { expect.hasAssertions(); - // Preparation + // Setting up const tx = getTransaction(); - const isNftTransaction = NftUtils.isTransactionNFTCreation(tx, network, logger); - expect(isNftTransaction).toStrictEqual(true); + const network = { + name: 'testnet', + } as unknown as Network; + // Explicitly disable the feature + process.env.NFT_AUTO_REVIEW_ENABLED = 'false'; expect(process.env.NFT_AUTO_REVIEW_ENABLED).not.toStrictEqual('true'); // Execution - // @ts-ignore - const result = NftUtils.shouldInvokeNftHandlerForTx(tx, network, logger); + const shouldInvoke = NftUtils.shouldInvokeNftHandlerForTx(tx, network, logger); - // Assertion - expect(result).toBe(false); + // Since NFT_AUTO_REVIEW_ENABLED is false, the function should return false + // without even checking if it's an NFT + expect(shouldInvoke).toStrictEqual(false); + // The spy should not be called when feature is disabled + expect(isNftTransactionSpy).toHaveBeenCalledTimes(0); }); it('should return true for a NFT transaction if the feature is enabled', () => { @@ -96,7 +173,6 @@ describe('isTransactionNFTCreation', () => { // Preparing mocks const spyCreateTx = jest.spyOn(hathorLib.helpersUtils, 'createTxFromHistoryObject'); - spyCreateTx.mockImplementation(() => ({})); let tx; let result; @@ -230,14 +306,14 @@ describe('_updateMetadata', () => { }; const mLambdaClient = new LambdaClientMock({}); (mLambdaClient.send as jest.Mocked).mockImplementation(async () => { - if (failureCount < 4) { - ++failureCount; - return { - StatusCode: 500, - Payload: 'failurePayload', - }; - } - return expectedLambdaResponse; + if (failureCount < 4) { + ++failureCount; + return { + StatusCode: 500, + Payload: 'failurePayload', + }; + } + return expectedLambdaResponse; }); const result = await NftUtils._updateMetadata('sampleUid', { sampleData: 'fake' }, 5, logger); @@ -265,16 +341,36 @@ describe('_updateMetadata', () => { }); // eslint-disable-next-line jest/valid-expect - expect(NftUtils._updateMetadata('sampleUid', { sampleData: 'fake' }, network, logger)) + expect(NftUtils._updateMetadata('sampleUid', { sampleData: 'fake' }, 3, logger)) .rejects.toThrow(new Error('Metadata update failed for tx_id: sampleUid.')); }); }); describe('invokeNftHandlerLambda', () => { - it('should return the lambda response on success', async () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = process.env; + process.env = { + ...originalEnv, + NFT_AUTO_REVIEW_ENABLED: 'true', + WALLET_SERVICE_LAMBDA_ENDPOINT: 'http://localhost:3000', + AWS_REGION: 'us-east-1' + }; + + // Reset mocks + const mLambdaClient = new LambdaClientMock({}); + (mLambdaClient.send as jest.Mocked).mockReset(); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should successfully invoke the lambda handler', async () => { expect.hasAssertions(); - // Building the mock lambda + // Mock successful lambda response const expectedLambdaResponse: InvokeCommandOutput = { StatusCode: 202, $metadata: {} @@ -282,31 +378,59 @@ describe('invokeNftHandlerLambda', () => { const mLambdaClient = new LambdaClientMock({}); (mLambdaClient.send as jest.Mocked).mockImplementationOnce(async () => expectedLambdaResponse); - await expect(NftUtils.invokeNftHandlerLambda('sampleUid', 'local', logger)).resolves.toBeUndefined(); + const result = await NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', logger); + + // Method should return void + expect(result).toBeUndefined(); + + // Verify Lambda client was constructed correctly + expect(LambdaClientMock).toHaveBeenCalledWith({ + endpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, + region: process.env.AWS_REGION, + }); + + // Verify the lambda was invoked with correct parameters + expect(mLambdaClient.send).toHaveBeenCalledTimes(1); }); - it('should throw when payload response status is invalid', async () => { + it('should throw error and add alert when lambda invocation fails', async () => { expect.hasAssertions(); - // Building the mock lambda - const mLambdaClient = new LambdaClientMock({}); + // Mock failed lambda response const expectedLambdaResponse: InvokeCommandOutput = { StatusCode: 500, $metadata: {} }; - (mLambdaClient.send as jest.Mocked).mockImplementation(() => expectedLambdaResponse); + const mLambdaClient = new LambdaClientMock({}); + (mLambdaClient.send as jest.Mocked).mockResolvedValueOnce(expectedLambdaResponse); - await expect(NftUtils.invokeNftHandlerLambda('sampleUid', 'local', logger)) - .rejects.toThrow(new Error('onNewNftEvent lambda invoke failed for tx: sampleUid')); + await expect(NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', logger)) + .rejects.toThrow('onNewNftEvent lambda invoke failed for tx: test-tx-id'); + // Verify alert was added expect(mockedAddAlert).toHaveBeenCalledWith( 'Error on NFTHandler lambda', 'Erroed on invokeNftHandlerLambda invocation', Severity.MINOR, - { TxId: 'sampleUid' }, + { TxId: 'test-tx-id' }, logger, ); }); + + it('should not invoke lambda when NFT_AUTO_REVIEW_ENABLED is not true', async () => { + expect.hasAssertions(); + + // Disable NFT auto review + process.env.NFT_AUTO_REVIEW_ENABLED = 'false'; + + const result = await NftUtils.invokeNftHandlerLambda('test-tx-id', 'local', logger); + + // Method should return void + expect(result).toBeUndefined(); + + // Logger.debug should be called + expect(logger.debug).toHaveBeenCalledWith('NFT auto review is disabled. Skipping lambda invocation.'); + }); }); describe('minor helpers', () => { @@ -320,3 +444,478 @@ describe('minor helpers', () => { expect(c.succeed('pass')).toBeUndefined(); }); }); + +describe('transaction transformation compatibility', () => { + let isNftTransactionSpy: jest.SpyInstance; + + beforeEach(() => { + isNftTransactionSpy = jest.spyOn(NftUtils, 'isTransactionNFTCreation').mockReturnValue(true); + }); + + afterEach(() => { + isNftTransactionSpy.mockRestore(); + }); + + it('should correctly transform fullNodeData to a format compatible with shouldInvokeNftHandlerForTx', () => { + expect.hasAssertions(); + + // Set up environment variables + const originalEnv = process.env; + process.env = { ...originalEnv, NFT_AUTO_REVIEW_ENABLED: 'true' }; + + try { + const fullNodeData: FullNodeTransaction = { + hash: 'test-hash', + version: constants.CREATE_TOKEN_TX_VERSION, + token_name: 'Test NFT', + token_symbol: 'TNFT', + tokens: ['token1', 'token2'], + inputs: [{ + tx_id: 'input-tx-1', + index: 0, + spent_output: { + token_data: (1 & hathorLib.constants.TOKEN_INDEX_MASK) + 1, // First token + value: 100, + script: 'script1', + decoded: { + type: 'P2PKH', + address: 'addr1', + timelock: null + } + } + }], + outputs: [{ + token_data: (0 & hathorLib.constants.TOKEN_INDEX_MASK) + 1, // HTR token + value: 100, + script: 'script2', + decoded: { + type: 'P2PKH', + address: 'addr2', + timelock: null, + } + }], + nonce: 0, + signal_bits: 1, + timestamp: 0, + weight: 18.2, + }; + + const txFromEvent = NftUtils.transformFullNodeTxForNftDetection(fullNodeData); + const txFromEvent2 = { + ...fullNodeData, + tx_id: fullNodeData.hash, + inputs: fullNodeData.inputs.map((input) => { + const tokenIndex = (input.spent_output.token_data & hathorLib.constants.TOKEN_INDEX_MASK) - 1; + + return { + token: tokenIndex < 0 ? hathorLib.constants.NATIVE_TOKEN_UID : fullNodeData.tokens[tokenIndex], + value: input.spent_output.value, + token_data: input.spent_output.token_data, + script: input.spent_output.script, + decoded: { + ...input.spent_output.decoded, + }, + tx_id: input.tx_id, + index: input.index + }; + }), + outputs: fullNodeData.outputs.map((output) => { + const tokenIndex = (output.token_data & hathorLib.constants.TOKEN_INDEX_MASK) - 1; + return { + ...output, + decoded: output.decoded ? output.decoded : {}, + spent_by: null, + token: tokenIndex < 0 ? hathorLib.constants.NATIVE_TOKEN_UID : fullNodeData.tokens[tokenIndex], + }; + }), + }; + + // Test that the transformed transaction is valid for NFT handling + const network = { name: 'testnet' }; + const result = NftUtils.shouldInvokeNftHandlerForTx(txFromEvent, network as unknown as hathorLib.Network, logger); + expect(result).toBe(true); + expect(isNftTransactionSpy).toHaveBeenCalledTimes(1); + } finally { + // Restore original values + process.env = originalEnv; + } + }); + + it('should correctly process a real event from production', () => { + expect.hasAssertions(); + + // Set up environment variables + const originalEnv = process.env; + process.env = { ...originalEnv, NFT_AUTO_REVIEW_ENABLED: 'true' }; + + try { + // Use the real event data constant + const fullNodeData = REAL_NFT_EVENT_DATA; + + // Transform the data using our helper function + const txFromEvent = createTransformedEvent(fullNodeData); + + // Verify token handling is correct + // First input should be HTR token + expect(txFromEvent.inputs[0].token).toBe(hathorLib.constants.NATIVE_TOKEN_UID); + + // First and second outputs should be HTR tokens + expect(txFromEvent.outputs[0].token).toBe(hathorLib.constants.NATIVE_TOKEN_UID); + expect(txFromEvent.outputs[1].token).toBe(hathorLib.constants.NATIVE_TOKEN_UID); + + // Third output should be the NFT token + expect(txFromEvent.outputs[2].token).toBe(fullNodeData.tokens[0]); + + // Check null decoded field is properly handled + expect(txFromEvent.outputs[0].decoded).toStrictEqual({}); + + // Mock network for validation + const mockNetwork = { + name: 'testnet', + }; + + // Test that this transaction is detected as an NFT + expect(isNftTransactionSpy).toHaveBeenCalledTimes(0); + const shouldInvoke = NftUtils.shouldInvokeNftHandlerForTx(txFromEvent, mockNetwork as unknown as hathorLib.Network, logger); + expect(shouldInvoke).toBe(true); + expect(isNftTransactionSpy).toHaveBeenCalledTimes(1); + } finally { + // Restore environment and constants + process.env = originalEnv; + } + }); +}); + +describe('processNftEvent', () => { + let originalEnv: NodeJS.ProcessEnv; + let invokeNftLambdaSpy: jest.SpyInstance; + let shouldInvokeSpy: jest.SpyInstance; + + beforeEach(() => { + // Save original env + originalEnv = process.env; + process.env = { + ...originalEnv, + NFT_AUTO_REVIEW_ENABLED: 'true', + WALLET_SERVICE_LAMBDA_ENDPOINT: 'http://localhost:3000', + AWS_REGION: 'us-east-1' + }; + + // Set up spies + invokeNftLambdaSpy = jest.spyOn(NftUtils, 'invokeNftHandlerLambda').mockResolvedValue(); + shouldInvokeSpy = jest.spyOn(NftUtils, 'shouldInvokeNftHandlerForTx').mockReturnValue(true); + + // Reset mocks + const mLambdaClient = new LambdaClientMock({}); + (mLambdaClient.send as jest.Mocked).mockReset(); + }); + + afterEach(() => { + // Clean up + process.env = originalEnv; + jest.restoreAllMocks(); + }); + + it('should invoke the NFT handler for a valid NFT event', async () => { + expect.hasAssertions(); + + // Real event data from production + const eventData = { + hash: '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866', + nonce: 257857, + timestamp: 1741649846, + signal_bits: 0, + version: 2, + weight: 17.30946054227969, + inputs: [ + { + tx_id: '00000000ba6f3fc01a3e8561f2905c50c98422e7112604a8971bdaba1535e797', + index: 1, + spent_output: { + value: 4, + token_data: 0, + script: 'dqkUWDMJLPqtb9X+jPcBSP6WLg6NIC6IrA==', + decoded: { + type: 'P2PKH', + address: 'WWiPUqkLJbb6YMQRgHWPMBS6voJjpeWqas', + timelock: null + } + } + } + ], + outputs: [ + { + value: 1, + token_data: 0, + script: 'C2lwZnM6Ly8xMTExrA==', + decoded: null + }, + { + value: 2, + token_data: 0, + script: 'dqkUFUs/hBsLnxy5Jd94WWV24BCmIhmIrA==', + decoded: { + type: 'P2PKH', + address: 'WQcdDHZriSQwE4neuzf9UW2xJkdhEqrt7F', + timelock: null + } + }, + { + value: 1, + token_data: 1, + script: 'dqkUhM3YhAjNc5p/oqX+yqEYcX+miNmIrA==', + decoded: { + type: 'P2PKH', + address: 'WanEffTDdFo8giEj2CuNqGWsEeWjU7crnF', + timelock: null + } + } + ], + tokens: [ + '000041f860a327969fa03685ed05cf316fc941708c53801cf81f426ac4a55866' + ], + token_name: 'Test', + token_symbol: 'TST', + }; + + // Mock network + const mockNetwork = { name: 'testnet' }; + + // Call the method + const result = await NftUtils.processNftEvent( + eventData, + 'test-stage', + mockNetwork as unknown as hathorLib.Network, + logger + ); + + // Verify result is true (successful invocation) + expect(result).toBe(true); + + // Verify shouldInvokeNftHandlerForTx was called with properly transformed tx + expect(shouldInvokeSpy).toHaveBeenCalledTimes(1); + const callArg = shouldInvokeSpy.mock.calls[0][0]; + + // Verify the transaction was transformed correctly + expect(callArg).toMatchObject({ + tx_id: eventData.hash, + version: eventData.version, + token_name: eventData.token_name, + token_symbol: eventData.token_symbol, + }); + + // Verify outputs were transformed correctly + expect(callArg.outputs.length).toBe(eventData.outputs.length); + expect(callArg.outputs[0].spent_by).toBeNull(); + expect(callArg.outputs[0].decoded).toEqual({}); + expect(callArg.outputs[0].token).toBe(hathorLib.constants.NATIVE_TOKEN_UID); + + // Verify the lambda was invoked with the correct parameters + expect(invokeNftLambdaSpy).toHaveBeenCalledTimes(1); + expect(invokeNftLambdaSpy).toHaveBeenCalledWith( + eventData.hash, + 'test-stage', + logger + ); + }); + + it('should not invoke the NFT handler when NFT_AUTO_REVIEW_ENABLED is false', async () => { + expect.hasAssertions(); + + process.env.NFT_AUTO_REVIEW_ENABLED = 'false'; + + const eventData = { ...REAL_NFT_EVENT_DATA }; + const mockNetwork = { name: 'testnet' }; + const result = await NftUtils.processNftEvent( + eventData, + 'test-stage', + mockNetwork as unknown as hathorLib.Network, + logger + ); + + expect(result).toBe(false); + expect(shouldInvokeSpy).not.toHaveBeenCalled(); + expect(invokeNftLambdaSpy).not.toHaveBeenCalled(); + }); + + it('should return false when shouldInvokeNftHandlerForTx returns false', async () => { + expect.hasAssertions(); + + shouldInvokeSpy.mockReturnValue(false); + + const eventData = { ...REAL_NFT_EVENT_DATA }; + const mockNetwork = { name: 'testnet' }; + const result = await NftUtils.processNftEvent( + eventData, + 'test-stage', + mockNetwork as unknown as hathorLib.Network, + logger + ); + + expect(result).toBe(false); + expect(shouldInvokeSpy).toHaveBeenCalledTimes(1); + expect(invokeNftLambdaSpy).not.toHaveBeenCalled(); + }); + + it('should handle errors from invokeNftHandlerLambda', async () => { + expect.hasAssertions(); + + // Make invokeNftHandlerLambda throw an error + invokeNftLambdaSpy.mockRejectedValue(new Error('Lambda invocation failed')); + + // Use the real event data constant + const eventData = { ...REAL_NFT_EVENT_DATA }; + + // Mock network + const mockNetwork = { name: 'testnet' }; + + // Call the method - it should not throw + const result = await NftUtils.processNftEvent( + eventData, + 'test-stage', + mockNetwork as unknown as hathorLib.Network, + logger + ); + + // Verify result is false (failed invocation) + expect(result).toBe(false); + + // Verify shouldInvokeNftHandlerForTx was called + expect(shouldInvokeSpy).toHaveBeenCalledTimes(1); + + // Verify the lambda was invoked + expect(invokeNftLambdaSpy).toHaveBeenCalledTimes(1); + + // Verify error was logged + expect(logger.error).toHaveBeenCalled(); + }); + + it('should return false for non-token-creation transactions', async () => { + expect.hasAssertions(); + + // Use the real event data constant with a non-matching version + const eventData = { + ...REAL_NFT_EVENT_DATA, + version: 1 // Different from CREATE_TOKEN_TX_VERSION + }; + + // Mock network + const mockNetwork = { name: 'testnet' }; + + // Call the method + const result = await NftUtils.processNftEvent( + eventData, + 'test-stage', + mockNetwork as unknown as hathorLib.Network, + logger + ); + + // Verify result is false (non-token-creation tx) + expect(result).toBe(false); + + // Verify shouldInvokeNftHandlerForTx was NOT called + expect(shouldInvokeSpy).not.toHaveBeenCalled(); + + // Verify the lambda was NOT invoked + expect(invokeNftLambdaSpy).not.toHaveBeenCalled(); + + // Verify debug message was logged + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining('not a token creation transaction') + ); + }); +}); + +it('should perform full NFT processing with real event data and no mocks', async () => { + expect.hasAssertions(); + + // Set up spies + const shouldInvokeSpy = jest.spyOn(NftUtils, 'shouldInvokeNftHandlerForTx').mockReturnValue(true); + + try { + // Set up required environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + NFT_AUTO_REVIEW_ENABLED: 'true', + WALLET_SERVICE_LAMBDA_ENDPOINT: 'http://localhost:3000', + AWS_REGION: 'us-east-1' + }; + + // Mock the Lambda client to prevent actual AWS calls + const mockSend = jest.fn().mockImplementation(() => { + return Promise.resolve({ StatusCode: 202 }); + }); + const mockLambdaClient = { + send: mockSend + }; + (LambdaClientMock as jest.Mock).mockImplementation(() => mockLambdaClient); + + try { + // Use the real event data constant + const eventData = { + ...REAL_NFT_EVENT_DATA, + metadata: { + hash: REAL_NFT_EVENT_DATA.hash, + spent_outputs: [ + { index: 0, tx_ids: [] }, + { index: 1, tx_ids: [] }, + { index: 2, tx_ids: [] } + ], + conflict_with: [], + voided_by: [], + received_by: [], + children: ['000000000007e794cd3e660e34af838c063370c5127f19ccab444052c2b5dadf'], + twins: [], + accumulated_weight: 17.30946054227969, + score: 0, + first_block: '000000000007e794cd3e660e34af838c063370c5127f19ccab444052c2b5dadf', + height: 0, + validation: 'full' + } + } as any; // Type assertion to avoid TypeScript errors + + // Mock network for validation + const mockNetwork = { + name: 'testnet', + }; + + // Process the NFT event + const result = await NftUtils.processNftEvent( + eventData, + 'test-stage', + mockNetwork as unknown as hathorLib.Network, + logger + ); + + // Verify the result is true (successful processing) + expect(result).toBe(true); + + // Verify shouldInvokeNftHandlerForTx was called + expect(shouldInvokeSpy).toHaveBeenCalledTimes(1); + + // Verify Lambda client was constructed correctly + expect(LambdaClientMock).toHaveBeenCalledWith({ + endpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, + region: process.env.AWS_REGION, + }); + + // Verify Lambda was invoked with correct parameters + expect(mockSend).toHaveBeenCalledTimes(1); + + // The format may be different when mocking, so use a more flexible test + expect(mockSend).toHaveBeenCalled(); + // The function used to invoke lambda was called + expect(mockSend.mock.calls.length).toBe(1); + + // Since we have full control over the mock, we know the lambda was invoked correctly + // We've already verified the result is true, which means the lambda was invoked successfully + } finally { + // Restore original values + process.env = originalEnv; + } + } finally { + // Restore spies + shouldInvokeSpy.mockRestore(); + } +}); diff --git a/packages/common/__tests__/utils/wallet.utils.test.ts b/packages/common/__tests__/utils/wallet.utils.test.ts new file mode 100644 index 00000000..6316ec65 --- /dev/null +++ b/packages/common/__tests__/utils/wallet.utils.test.ts @@ -0,0 +1,29 @@ +import { isDecodedValid } from '@src/utils/wallet.utils'; + +describe('walletUtils', () => { + it('should validate common invalid inputs', () => { + expect.hasAssertions(); + + expect(isDecodedValid({})).toBeFalsy(); + expect(isDecodedValid(false)).toBeFalsy(); + expect(isDecodedValid(null)).toBeFalsy(); + expect(isDecodedValid(undefined)).toBeFalsy(); + expect(isDecodedValid({ + address: 'addr1', + type: 'PPK', + })).toBeTruthy(); + }); + + it('should validate requiredKeys', () => { + expect.hasAssertions(); + + expect(isDecodedValid({ + address: 'addr1', + type: 'PPK', + }, ['address', 'type'])).toBeTruthy(); + + expect(isDecodedValid({ + address: 'addr1', + }, ['address', 'type'])).toBeFalsy(); + }); +}); diff --git a/packages/common/package.json b/packages/common/package.json index 1edea33c..10f44304 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -8,7 +8,7 @@ "test": "jest --runInBand --collectCoverage --detectOpenHandles --forceExit" }, "peerDependencies": { - "@hathor/wallet-lib": "0.39.0" + "@hathor/wallet-lib": "1.14.1" }, "dependencies": { "@aws-sdk/client-lambda": "3.540.0", diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 561de83b..55521504 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -10,9 +10,8 @@ * https://github.com/HathorNetwork/ops-tools/blob/master/docs/on-call/guide.md#alert-severitypriority */ -// @ts-ignore import { constants } from '@hathor/wallet-lib'; -import { isAuthority } from './utils/wallet.utils'; +import { isAuthority, isDecodedValid } from './utils/wallet.utils'; export interface StringMap { [x: string]: T; @@ -377,7 +376,7 @@ export class TokenBalanceMap { * @returns The TokenBalanceMap object */ static fromTxOutput(output: TxOutput): TokenBalanceMap { - if (!output.decoded) { + if (!isDecodedValid(output.decoded)) { throw new Error('Output has no decoded script'); } const token = output.token; @@ -436,3 +435,34 @@ export class TokenBalanceMap { return obj; } } + +// The output structure in full node events is similar to TxOutput but with some differences +export interface FullNodeOutput extends Omit { + // In full node data, decoded can be null + decoded: DecodedOutput | null; +} + +// The input structure in full node events is different - it contains a reference to the spent output +export interface FullNodeInput extends Omit { + // Instead of having these fields directly, it has a spent_output property + spent_output: FullNodeOutput; +} + +// The FullNodeTransaction interface represents a transaction as it comes from the full node +// which has a slightly different structure than our internal Transaction type +export interface FullNodeTransaction extends Omit { + // From full node events we get 'hash' instead of 'tx_id' + hash: string; + // The input and output structures are different from our internal Transaction type + inputs: FullNodeInput[]; + outputs: FullNodeOutput[]; + // Additional fields specific to full node events + tokens: string[]; + parents?: string[]; + metadata?: { + hash: string; + voided_by: string[]; + first_block: null | string; + height: number; + }; +} diff --git a/packages/common/src/utils/alerting.utils.ts b/packages/common/src/utils/alerting.utils.ts index 213642de..46e9dfc6 100644 --- a/packages/common/src/utils/alerting.utils.ts +++ b/packages/common/src/utils/alerting.utils.ts @@ -61,7 +61,7 @@ export const addAlert = async ( try { await client.send(command); - } catch(err) { + } catch (err) { logger.error('[ALERT] Erroed while sending message to the alert sqs queue', err); } }; diff --git a/packages/common/src/utils/nft.utils.ts b/packages/common/src/utils/nft.utils.ts index dd87a44d..92cae4fb 100644 --- a/packages/common/src/utils/nft.utils.ts +++ b/packages/common/src/utils/nft.utils.ts @@ -7,10 +7,12 @@ import { LambdaClient, InvokeCommand, InvokeCommandOutput } from '@aws-sdk/client-lambda'; import { addAlert } from './alerting.utils'; -import { Transaction, Severity } from '../types'; -// @ts-ignore +import { Severity } from '../types'; import { Network, constants, CreateTokenTransaction, helpersUtils } from '@hathor/wallet-lib'; +// FIXME: import from lib path on HathorLib +import type { HistoryTransaction } from '@hathor/wallet-lib/lib/models/types'; import { Logger } from 'winston'; +import { FullNodeTransaction, FullNodeInput, FullNodeOutput } from '../types'; /** * A helper for generating and updating a NFT Token's metadata. @@ -29,27 +31,23 @@ export class NftUtils { * * TODO: Remove the logger param after we unify the logger from both projects */ - static shouldInvokeNftHandlerForTx(tx: Transaction, network: Network, logger: Logger): boolean { + static shouldInvokeNftHandlerForTx(tx: HistoryTransaction, network: Network, logger: Logger): boolean { return isNftAutoReviewEnabled() && this.isTransactionNFTCreation(tx, network, logger); } /** * Returns if the transaction in the parameter is a NFT Creation. - * @param {Transaction} tx - * @returns {boolean} - * - * TODO: change tx type to HistoryTransaction * TODO: Remove the logger param after we unify the logger from both projects */ - static isTransactionNFTCreation(tx: any, network: Network, logger: Logger): boolean { - /* - * To fully check if a transaction is a NFT creation, we need to instantiate a new Transaction object in the lib. - * So first we do some very fast checks to filter the bulk of the requests for NFTs with minimum processing. - */ + static isTransactionNFTCreation(tx: HistoryTransaction, network: Network, logger: Logger): boolean { + /* + * To fully check if a transaction is a NFT creation, we need to instantiate a new Transaction object in the lib. + * So first we do some very fast checks to filter the bulk of the requests for NFTs with minimum processing. + */ if ( tx.version !== constants.CREATE_TOKEN_TX_VERSION // Must be a token creation tx - || !tx.token_name // Must have a token name - || !tx.token_symbol // Must have a token symbol + || !tx.token_name // Must have a token name + || !tx.token_symbol // Must have a token symbol ) { return false; } @@ -93,7 +91,7 @@ export class NftUtils { endpoint: process.env.EXPLORER_SERVICE_LAMBDA_ENDPOINT, region: process.env.AWS_REGION, }); - const command = new InvokeCommand({ + const command = new InvokeCommand({ FunctionName: `hathor-explorer-service-${process.env.EXPLORER_SERVICE_STAGE}-create_or_update_dag_metadata`, InvocationType: 'Event', Payload: JSON.stringify({ @@ -148,12 +146,23 @@ export class NftUtils { * This is to improve the failure tolerance on this non-critical step of the sync loop. */ static async invokeNftHandlerLambda(txId: string, stage: string, logger: Logger): Promise { + // Check for required environment variables + if (!process.env.WALLET_SERVICE_LAMBDA_ENDPOINT || !process.env.AWS_REGION) { + throw new Error('Environment variables WALLET_SERVICE_LAMBDA_ENDPOINT and AWS_REGION are not set.'); + } + + // Skip if NFT auto review is disabled + if (!isNftAutoReviewEnabled()) { + logger.debug('NFT auto review is disabled. Skipping lambda invocation.'); + return; + } + const client = new LambdaClient({ endpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, region: process.env.AWS_REGION, }); // invoke lambda asynchronously to metadata update - const command = new InvokeCommand({ + const command = new InvokeCommand({ FunctionName: `hathor-wallet-service-${stage}-onNewNftEvent`, InvocationType: 'Event', Payload: JSON.stringify({ nftUid: txId }), @@ -173,4 +182,104 @@ export class NftUtils { throw new Error(`onNewNftEvent lambda invoke failed for tx: ${txId}`); } } + + /** + * Process a new NFT event by transforming the data, checking if it should invoke the NFT handler, + * and invoking the lambda if appropriate. + */ + static async processNftEvent( + eventData: FullNodeTransaction, + stage: string, + network: Network, + logger: Logger + ): Promise { + // Early return if NFT auto review is disabled + if (!isNftAutoReviewEnabled()) { + logger.debug('NFT auto review is disabled. Skipping NFT handler invocation.'); + return false; + } + + // Early return if not a token creation transaction + if (eventData.version !== constants.CREATE_TOKEN_TX_VERSION) { + logger.debug(`Transaction version ${eventData.version} is not a token creation transaction (${constants.CREATE_TOKEN_TX_VERSION}). Skipping NFT handler invocation.`); + return false; + } + + try { + // Transform the data to a format compatible with shouldInvokeNftHandlerForTx + const transformedTx = NftUtils.transformFullNodeTxForNftDetection(eventData); + + // Check if we should invoke the NFT handler for this transaction + if (NftUtils.shouldInvokeNftHandlerForTx(transformedTx, network, logger)) { + // Get the transaction hash + const txId = eventData.hash; + + // Invoke the lambda function + await NftUtils.invokeNftHandlerLambda(txId, stage, logger); + return true; + } + + return false; + } catch (error) { + logger.error(`Error processing NFT event: ${error}`); + return false; + } + } + + /** + * Transform transaction data from the full node to a format compatible with the NFT detection logic. + */ + static transformFullNodeTxForNftDetection(fullNodeData: FullNodeTransaction): HistoryTransaction { + // Create a new object with the required properties + let transformedTx: HistoryTransaction = { + tx_id: fullNodeData.hash, // Add tx_id for compatibility + version: fullNodeData.version, + tokens: fullNodeData.tokens, + inputs: fullNodeData.inputs.map((input: FullNodeInput) => { + // Extract the token index from token_data using hathor's TOKEN_INDEX_MASK + // The token_data field contains both the token index and other flags + // TOKEN_INDEX_MASK is used to isolate just the token index bits + // We subtract 1 because token indexes are 1-based in token_data but 0-based in the tokens array + const tokenIndex = (input.spent_output.token_data & constants.TOKEN_INDEX_MASK) - 1; + + return { + tx_id: input.tx_id, + index: input.index, + token: tokenIndex < 0 ? constants.NATIVE_TOKEN_UID : fullNodeData.tokens[tokenIndex], + token_data: input.spent_output.token_data, + value: input.spent_output.value, + script: input.spent_output.script, + decoded: input.spent_output.decoded || {}, + }; + }), + outputs: fullNodeData.outputs.map((output: FullNodeOutput) => { + // Extract the token index from token_data using the same bit masking technique + // A negative result means it's the HTR token (index < 0) + // A positive result is an index into the tokens array (custom tokens) + const tokenIndex = (output.token_data & constants.TOKEN_INDEX_MASK) - 1; + + return { + value: output.value, + token_data: output.token_data, + script: output.script, + token: tokenIndex < 0 ? constants.NATIVE_TOKEN_UID : fullNodeData.tokens[tokenIndex], + decoded: output.decoded || {}, + spent_by: null, + }; + }), + signalBits: fullNodeData.signal_bits, + weight: fullNodeData.weight, + timestamp: fullNodeData.timestamp, + is_voided: !!fullNodeData.voided, + nonce: fullNodeData.nonce, + parents: fullNodeData.parents ?? [], + }; + + if (fullNodeData.token_name && fullNodeData.token_symbol) { + transformedTx.token_name = fullNodeData.token_name; + transformedTx.token_symbol = fullNodeData.token_symbol; + } + + return transformedTx; + } } diff --git a/packages/common/src/utils/wallet.utils.ts b/packages/common/src/utils/wallet.utils.ts index 66a4c43b..e2edf273 100644 --- a/packages/common/src/utils/wallet.utils.ts +++ b/packages/common/src/utils/wallet.utils.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// @ts-ignore import { constants } from '@hathor/wallet-lib'; /** @@ -18,3 +17,19 @@ import { constants } from '@hathor/wallet-lib'; export const isAuthority = (tokenData: number): boolean => ( (tokenData & constants.TOKEN_AUTHORITY_MASK) > 0 ); + +/** + * Checks if a decoded output object is valid (not null, undefined or empty object). + * + * @param decoded - The decoded output object to check + * @param requiredKeys - A list of keys to check + * @returns true if the decoded object is valid, false otherwise + */ +export const isDecodedValid = (decoded: any, requiredKeys: string[] = []): boolean => { + return (decoded != null + && typeof decoded === 'object' + && Object.keys(decoded).length > 0) + && requiredKeys.reduce((state, key: string) => ( + state && decoded[key] != null + ), true); +}; diff --git a/packages/daemon/__tests__/db/index.test.ts b/packages/daemon/__tests__/db/index.test.ts index 930ed492..d61fc123 100644 --- a/packages/daemon/__tests__/db/index.test.ts +++ b/packages/daemon/__tests__/db/index.test.ts @@ -71,7 +71,6 @@ import { import { isAuthority } from '@wallet-service/common'; import { DbTxOutput, StringMap, TokenInfo, WalletStatus } from '../../src/types'; import { Authorities, TokenBalanceMap } from '@wallet-service/common'; -// @ts-ignore import { constants } from '@hathor/wallet-lib'; import { generateAddresses } from '../../src/utils'; diff --git a/packages/daemon/__tests__/integration/balances.test.ts b/packages/daemon/__tests__/integration/balances.test.ts index b6e4d95d..be99e9d4 100644 --- a/packages/daemon/__tests__/integration/balances.test.ts +++ b/packages/daemon/__tests__/integration/balances.test.ts @@ -71,7 +71,7 @@ let mysql: Connection; beforeAll(async () => { try { mysql = await getDbConnection(); - } catch(e) { + } catch (e) { console.error('Failed to establish db connection', e); throw e; } @@ -290,6 +290,7 @@ describe('empty script scenario', () => { // @ts-ignore await transitionUntilEvent(mysql, machine, EMPTY_SCRIPT_LAST_EVENT); const addressBalances = await fetchAddressBalances(mysql); + // @ts-ignore expect(validateBalances(addressBalances, emptyScriptBalances)); }); diff --git a/packages/daemon/__tests__/services/services.test.ts b/packages/daemon/__tests__/services/services.test.ts index 1390f79e..1c87fc9b 100644 --- a/packages/daemon/__tests__/services/services.test.ts +++ b/packages/daemon/__tests__/services/services.test.ts @@ -120,6 +120,7 @@ jest.mock('@wallet-service/common', () => { NftUtils: { shouldInvokeNftHandlerForTx: jest.fn().mockReturnValue(false), invokeNftHandlerLambda: jest.fn(), + processNftEvent: jest.fn().mockReturnValue(Promise.resolve()), }, }; }); diff --git a/packages/daemon/__tests__/utils/wallet.test.ts b/packages/daemon/__tests__/utils/wallet.test.ts index 4d60c86e..b3584a26 100644 --- a/packages/daemon/__tests__/utils/wallet.test.ts +++ b/packages/daemon/__tests__/utils/wallet.test.ts @@ -25,9 +25,9 @@ describe('prepareOutputs', () => { token_data: 0, script: 'dqkUCU1EY3YLi8WURhDOEsspok4Y0XiIrA==', decoded: { - type: 'P2PKH', - address: 'H7NK2gjt5oaHzBEPoiH7y3d1NcPQi3Tr2F', - timelock: null, + type: 'P2PKH', + address: 'H7NK2gjt5oaHzBEPoiH7y3d1NcPQi3Tr2F', + timelock: null, } }, { value: 1, diff --git a/packages/daemon/package.json b/packages/daemon/package.json index d772c757..fcbaca36 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -46,7 +46,7 @@ "typescript": "4.9.5" }, "peerDependencies": { - "@hathor/wallet-lib": "0.39.0", + "@hathor/wallet-lib": "1.14.1", "@wallet-service/common": "1.5.0" }, "dependencies": { diff --git a/packages/daemon/src/config.ts b/packages/daemon/src/config.ts index 4f6e7814..9055dd37 100644 --- a/packages/daemon/src/config.ts +++ b/packages/daemon/src/config.ts @@ -49,7 +49,7 @@ export const STAGE = process.env.STAGE ?? 'local'; export const FULLNODE_PEER_ID = process.env.FULLNODE_PEER_ID; export const FULLNODE_HOST = process.env.FULLNODE_HOST; export const STREAM_ID = process.env.STREAM_ID; -export const NETWORK = process.env.NETWORK; +export const NETWORK = String(process.env.NETWORK); /* The network name that comes from the fullnode events might be different from * the network we should use to derive addresses, e.g. testnet-golf instead of * testnet @@ -84,7 +84,7 @@ export const HEALTHCHECK_SERVER_API_KEY = process.env.HEALTHCHECK_SERVER_API_KEY export const HEALTHCHECK_PING_INTERVAL = parseInt(process.env.HEALTHCHECK_PING_INTERVAL ?? '10000', 10); // 10 seconds // Other -export const USE_SSL = process.env.USE_SSL; +export const USE_SSL = process.env.USE_SSL === 'true'; // Reorg size thresholds for different alert levels export const REORG_SIZE_INFO = parseInt(process.env.REORG_SIZE_INFO ?? '1', 10); diff --git a/packages/daemon/src/services/index.ts b/packages/daemon/src/services/index.ts index 89f11e61..aa8ff9a7 100644 --- a/packages/daemon/src/services/index.ts +++ b/packages/daemon/src/services/index.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -// @ts-ignore import hathorLib from '@hathor/wallet-lib'; import { Connection as MysqlConnection } from 'mysql2/promise'; import axios from 'axios'; @@ -30,6 +29,7 @@ import { Transaction, TokenBalanceMap, TxOutputWithIndex, + isDecodedValid, } from '@wallet-service/common'; import { prepareOutputs, @@ -44,7 +44,6 @@ import { validateAddressBalances, getWalletBalancesForTx, getFullnodeHttpUrl, - sendMessageSQS, generateAddresses, sendRealtimeTx, } from '../utils'; @@ -135,8 +134,8 @@ export const metadataDiff = async (_context: Context, event: Event) => { } if (first_block - && first_block.length - && first_block.length > 0) { + && first_block.length + && first_block.length > 0) { if (!dbTx.height) { return { type: METADATA_DIFF_EVENT_TYPES.TX_FIRST_BLOCK, @@ -163,7 +162,7 @@ export const metadataDiff = async (_context: Context, event: Event) => { }; export const isBlock = (version: number): boolean => version === hathorLib.constants.BLOCK_VERSION - || version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION; + || version === hathorLib.constants.MERGED_MINED_BLOCK_VERSION; export const handleVertexAccepted = async (context: Context, _event: Event) => { const mysql = await getDbConnection(); @@ -183,6 +182,8 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { throw new Error('No block reward lock set'); } + const fullNodeData = fullNodeEvent.event.data; + const { hash, metadata, @@ -196,7 +197,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { token_name, token_symbol, parents, - } = fullNodeEvent.event.data; + } = fullNodeData; const dbTx: DbTransaction | null = await getTransactionById(mysql, hash); @@ -217,7 +218,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const txOutputs: TxOutputWithIndex[] = prepareOutputs(outputs, tokens); const txInputs: TxInput[] = prepareInputs(inputs, tokens); - let heightlock = null; + let heightlock: number | null = null; if (isBlock(version)) { if (typeof height !== 'number' && !height) { throw new Error('Block with no height set in metadata.'); @@ -238,7 +239,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const blockRewardOutput = outputs[0]; // add miner to the miners table - if (blockRewardOutput.decoded) { + if (isDecodedValid(blockRewardOutput.decoded, ['address'])) { await addMiner(mysql, blockRewardOutput.decoded.address, hash); } @@ -303,21 +304,21 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const addressesPerWallet = Object.entries(addressWalletMap).reduce( (result: StringMap<{ addresses: string[], walletDetails: Wallet }>, [address, wallet]: [string, Wallet]) => { - const { walletId } = wallet; - - // Initialize the array if the walletId is not yet a key in result - if (!result[walletId]) { - result[walletId] = { - addresses: [], - walletDetails: wallet, + const { walletId } = wallet; + + // Initialize the array if the walletId is not yet a key in result + if (!result[walletId]) { + result[walletId] = { + addresses: [], + walletDetails: wallet, + } } - } - // Add the current key to the array - result[walletId].addresses.push(address); + // Add the current key to the array + result[walletId].addresses.push(address); - return result; - }, {}); + return result; + }, {}); const seenWallets = Object.keys(addressesPerWallet); @@ -409,13 +410,10 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { const network = new hathorLib.Network(NETWORK); - // Validating for NFTs only after the tx is successfully added - if (NftUtils.shouldInvokeNftHandlerForTx(txData, network, logger)) { - // This process is not critical, so we run it in a fire-and-forget manner, not waiting for the promise. - // In case of errors, just log the asynchronous exception and take no action on it. - NftUtils.invokeNftHandlerLambda(txData.tx_id, STAGE, logger) - .catch((err: unknown) => logger.error('[ALERT] Error on nftHandlerLambda invocation', err)); - } + // Call to process the data for NFT handling (if applicable) + // This process is not critical, so we run it in a fire-and-forget manner, not waiting for the promise. + NftUtils.processNftEvent(fullNodeData, STAGE, network, logger) + .catch((err: unknown) => logger.error('[ALERT] Error processing NFT event', err)); } await dbUpdateLastSyncedEvent(mysql, fullNodeEvent.event.id); @@ -423,7 +421,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { await mysql.commit(); } catch (e) { await mysql.rollback(); - logger.error('Error handling vertex accepted', { + console.error('Error handling vertex accepted', { error: (e as Error).message, stack: (e as Error).stack, }); @@ -618,13 +616,13 @@ export const updateLastSyncedEvent = async (context: Context) => { const lastEventId = context.event.event.id; if (lastDbSyncedEvent - && lastDbSyncedEvent.last_event_id > lastEventId) { - logger.error('Tried to store an event lower than the one on the database', { - lastEventId, - lastDbSyncedEvent: JSON.stringify(lastDbSyncedEvent), - }); - mysql.destroy(); - throw new Error('Event lower than stored one.'); + && lastDbSyncedEvent.last_event_id > lastEventId) { + logger.error('Tried to store an event lower than the one on the database', { + lastEventId, + lastDbSyncedEvent: JSON.stringify(lastDbSyncedEvent), + }); + mysql.destroy(); + throw new Error('Event lower than stored one.'); } await dbUpdateLastSyncedEvent(mysql, lastEventId); diff --git a/packages/daemon/src/types/token.ts b/packages/daemon/src/types/token.ts index e384707e..c53da8a9 100644 --- a/packages/daemon/src/types/token.ts +++ b/packages/daemon/src/types/token.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -// @ts-ignore -import hathorLib from '@hathor/wallet-lib'; +import { constants } from '@hathor/wallet-lib'; export class TokenInfo { id: string; @@ -23,9 +22,10 @@ export class TokenInfo { this.symbol = symbol; this.transactions = transactions || 0; - const hathorConfig = hathorLib.constants.HATHOR_TOKEN_CONFIG; + // XXX: get config from settings? + const hathorConfig = constants.DEFAULT_NATIVE_TOKEN_CONFIG; - if (this.id === hathorConfig.uid) { + if (this.id === constants.NATIVE_TOKEN_UID) { this.name = hathorConfig.name; this.symbol = hathorConfig.symbol; } diff --git a/packages/daemon/src/utils/wallet.ts b/packages/daemon/src/utils/wallet.ts index 11a2b0bc..f9752269 100644 --- a/packages/daemon/src/utils/wallet.ts +++ b/packages/daemon/src/utils/wallet.ts @@ -5,8 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -// @ts-ignore -import hathorLib, { constants, Output } from '@hathor/wallet-lib'; +import hathorLib, { constants, Output, walletUtils, addressUtils } from '@hathor/wallet-lib'; import { Connection as MysqlConnection } from 'mysql2/promise'; import { strict as assert } from 'assert'; import { @@ -28,6 +27,7 @@ import { TxInput, TxOutput, TokenBalanceMap, + isDecodedValid, } from '@wallet-service/common'; import { fetchAddressBalance, @@ -40,8 +40,6 @@ import { updateWalletLockedBalance, } from '../db'; import logger from '../logger'; -// @ts-ignore -import { walletUtils } from '@hathor/wallet-lib'; import { stringMapIterator } from './helpers'; /** @@ -66,20 +64,21 @@ export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOu const preparedOutputs: [number, TxOutputWithIndex[]] = outputs.reduce( ([currIndex, newOutputs]: [number, TxOutputWithIndex[]], _output: EventTxOutput): [number, TxOutputWithIndex[]] => { + // XXX: Output typing makes no sense here, maybe we should convert from Output to the wallet-service's own TxOutput const output = new Output(_output.value, Buffer.from(_output.script, 'base64'), { tokenData: _output.token_data, }); - let token = '00'; + let token = constants.NATIVE_TOKEN_UID; if (!output.isTokenHTR()) { token = tokens[output.getTokenIndex()]; } // @ts-ignore output.token = token; - if (!_output.decoded - || _output.decoded.type === null - || _output.decoded.type === undefined) { + if (!isDecodedValid(_output.decoded) + || _output.decoded.type === null + || _output.decoded.type === undefined) { console.log('Decode failed, skipping..'); return [currIndex + 1, newOutputs]; } @@ -95,7 +94,7 @@ export const prepareOutputs = (outputs: EventTxOutput[], tokens: string[]): TxOu }; // @ts-ignore - return [ currIndex + 1, [ ...newOutputs, finalOutput, ], ]; + return [currIndex + 1, [...newOutputs, finalOutput,],]; }, [0, []], ); @@ -126,7 +125,7 @@ export const getAddressBalanceMap = ( const addressBalanceMap = {}; for (const input of inputs) { - if (!input.decoded) { + if (!isDecodedValid(input.decoded)) { // If we're unable to decode the script, we will also be unable to // calculate the balance, so just skip this input. continue; @@ -142,7 +141,7 @@ export const getAddressBalanceMap = ( } for (const output of outputs) { - if (!output.decoded) { + if (!isDecodedValid(output.decoded)) { throw new Error('Output has no decoded script'); } @@ -185,7 +184,7 @@ export const unlockUtxos = async (mysql: MysqlConnection, utxos: DbTxOutput[], u decoded, locked: false, // set authority bit if necessary - token_data: utxo.authorities > 0 ? hathorLib.constants.TOKEN_AUTHORITY_MASK : 0, + token_data: utxo.authorities > 0 ? constants.TOKEN_AUTHORITY_MASK : 0, // we don't care about spent_by and script spent_by: null, script: '', @@ -285,7 +284,7 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput const utxo: Output = new Output(output.value, Buffer.from(output.script, 'base64'), { tokenData: output.token_data, }); - let token = '00'; + let token = hathorLib.constants.NATIVE_TOKEN_UID; if (!utxo.isTokenHTR()) { token = tokens[utxo.getTokenIndex()]; } @@ -298,9 +297,10 @@ export const prepareInputs = (inputs: EventTxInput[], tokens: string[]): TxInput // @ts-ignore script: utxo.script, token, - decoded: output.decoded ? { + decoded: isDecodedValid(output.decoded, ['type', 'address']) ? { type: output.decoded.type, address: output.decoded.address, + // timelock might actually be null, so don't pass it to requiredKeys timelock: output.decoded.timelock, } : null, }; @@ -531,7 +531,11 @@ export const generateAddresses = async ( // (more details in https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#Change) // so we derive our xpub to this path and use it to get the addresses const derivedXpub = walletUtils.xpubDeriveChild(xpubkey, 0); - const addrMap = walletUtils.getAddresses(derivedXpub, startIndex, count, network); + const addrMap: StringMap = {}; + for (let index = startIndex; index < startIndex + count; index++) { + const address = addressUtils.deriveAddressFromXPubP2PKH(derivedXpub, index, network); + addrMap[address.base58] = index; + } return addrMap; }; diff --git a/packages/wallet-service/README.md b/packages/wallet-service/README.md index a185cc40..ba29aa41 100644 --- a/packages/wallet-service/README.md +++ b/packages/wallet-service/README.md @@ -58,7 +58,7 @@ DB_ENDPOINT=localhost DB_NAME=wallet_service DB_USER=my_user DB_PASS=password123 -REDIS_HOST=localhost +REDIS_URL=localhost REDIS_PORT=6379 AUTH_SECRET=foobar EXPLORER_SERVICE_LAMBDA_ENDPOINT=http://localhost:3001 diff --git a/packages/wallet-service/package.json b/packages/wallet-service/package.json index 87e75d00..e8cadf78 100644 --- a/packages/wallet-service/package.json +++ b/packages/wallet-service/package.json @@ -40,12 +40,13 @@ "winston": "3.13.0" }, "peerDependencies": { - "@hathor/wallet-lib": "0.39.0", + "@hathor/wallet-lib": "1.14.1", "@wallet-service/common": "1.5.0" }, "devDependencies": { "@types/aws-lambda": "8.10.95", - "@types/jest": "^29.5.13", + "@types/jest": "29.5.13", + "@types/joi": "17.2.3", "@types/node": "18.0.4", "@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/parser": "3.3.0", diff --git a/packages/wallet-service/src/api/auth.ts b/packages/wallet-service/src/api/auth.ts index 22c4463d..cb9baeb0 100644 --- a/packages/wallet-service/src/api/auth.ts +++ b/packages/wallet-service/src/api/auth.ts @@ -30,6 +30,7 @@ import middy from '@middy/core'; import cors from '@middy/http-cors'; import createDefaultLogger from '@src/logger'; import { Logger } from 'winston'; +import config from '@src/config'; const EXPIRATION_TIME_IN_SECONDS = 1800; @@ -141,7 +142,7 @@ export const tokenHandler: APIGatewayProxyHandler = middy(async (event) => { addr: address.toString(), wid: walletId, }, - process.env.AUTH_SECRET, + config.authSecret, { expiresIn: EXPIRATION_TIME_IN_SECONDS, jwtid: uuid4(), @@ -200,7 +201,7 @@ export const bearerAuthorizer: APIGatewayTokenAuthorizerHandler = middy(async (e try { data = jwt.verify( sanitizedToken, - process.env.AUTH_SECRET, + config.authSecret, ); } catch (e) { // XXX: find a way to return specific error to frontend or make all errors Unauthorized? diff --git a/packages/wallet-service/src/api/healthcheck.ts b/packages/wallet-service/src/api/healthcheck.ts index ead57fad..babad766 100644 --- a/packages/wallet-service/src/api/healthcheck.ts +++ b/packages/wallet-service/src/api/healthcheck.ts @@ -12,11 +12,10 @@ import fullnode from '@src/fullnode'; import { closeDbConnection, getDbConnection } from '@src/utils'; import { APIGatewayProxyHandler } from 'aws-lambda'; import { getRedisClient, ping } from '@src/redis'; +import config from '@src/config'; const mysql = getDbConnection(); -const HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE = Number(process.env.HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE ?? 5) - const checkDatabaseHeight: HealthcheckCallbackResponse = async () => { try { const [currentHeight, fullnodeStatus] = await Promise.all([ @@ -26,10 +25,10 @@ const checkDatabaseHeight: HealthcheckCallbackResponse = async () => { const currentFullnodeHeight = fullnodeStatus['dag']['best_block']['height']; - if (currentFullnodeHeight - currentHeight < HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE) { + if (currentFullnodeHeight - currentHeight < config.healthCheckMaximumHeightDifference) { return new HealthcheckCallbackResponse({ status: HealthcheckStatus.PASS, - output: `Database and fullnode heights are within ${HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE} blocks difference`, + output: `Database and fullnode heights are within ${config.healthCheckMaximumHeightDifference} blocks difference`, }); } else { return new HealthcheckCallbackResponse({ @@ -142,4 +141,4 @@ export const getHealthcheck: APIGatewayProxyHandler = middy(async (event) => { statusCode: response.getHttpStatusCode(), body: response.toJson(), }; -}); \ No newline at end of file +}); diff --git a/packages/wallet-service/src/api/tokens.ts b/packages/wallet-service/src/api/tokens.ts index fa58e565..3bd891aa 100644 --- a/packages/wallet-service/src/api/tokens.ts +++ b/packages/wallet-service/src/api/tokens.ts @@ -68,7 +68,7 @@ export const getTokenDetails = middy(walletIdProxyHandler(async (walletId, event const tokenId = value.token_id; const tokenInfo: TokenInfo = await getTokenInformation(mysql, tokenId); - if (tokenId === constants.HATHOR_TOKEN_CONFIG.uid) { + if (tokenId === constants.NATIVE_TOKEN_UID) { const details = [{ message: 'Invalid tokenId', }]; diff --git a/packages/wallet-service/src/api/totalSupply.ts b/packages/wallet-service/src/api/totalSupply.ts index 283aed53..5698bb2d 100644 --- a/packages/wallet-service/src/api/totalSupply.ts +++ b/packages/wallet-service/src/api/totalSupply.ts @@ -19,7 +19,7 @@ import cors from '@middy/http-cors'; import hathorLib from '@hathor/wallet-lib'; import Joi from 'joi'; -const htrToken = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; +const htrToken = hathorLib.constants.NATIVE_TOKEN_UID; const mysql = getDbConnection(); const paramsSchema = Joi.object({ tokenId: Joi.string() diff --git a/packages/wallet-service/src/api/txProposalCreate.ts b/packages/wallet-service/src/api/txProposalCreate.ts index 9bf67930..8b40abeb 100644 --- a/packages/wallet-service/src/api/txProposalCreate.ts +++ b/packages/wallet-service/src/api/txProposalCreate.ts @@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import Joi from 'joi'; import { ApiError } from '@src/api/errors'; -import { maybeRefreshWalletConstants, walletIdProxyHandler } from '@src/commons'; +import { walletIdProxyHandler } from '@src/commons'; import { createTxProposal, getUtxos, @@ -28,7 +28,9 @@ import { closeDbAndGetError } from '@src/api/utils'; import { closeDbConnection, getDbConnection, getUnixTimestamp } from '@src/utils'; import middy from '@middy/core'; import cors from '@middy/http-cors'; -import hathorLib from '@hathor/wallet-lib'; +import { constants, Network, Transaction, helpersUtils } from '@hathor/wallet-lib'; +import { getFullnodeData } from '@src/nodeConfig'; +import config from '@src/config'; const mysql = getDbConnection(); @@ -42,7 +44,7 @@ const bodySchema = Joi.object({ * This lambda is called by API Gateway on POST /txproposals */ export const create = middy(walletIdProxyHandler(async (walletId, event) => { - await maybeRefreshWalletConstants(mysql); + const versionData = await getFullnodeData(mysql); const eventBody = (function parseBody(body) { try { @@ -67,9 +69,9 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { } const body = value; - const tx: hathorLib.Transaction = hathorLib.helpersUtils.createTxFromHex(body.txHex, new hathorLib.Network(process.env.NETWORK)); + const tx: Transaction = helpersUtils.createTxFromHex(body.txHex, new Network(config.network)); - if (tx.outputs.length > hathorLib.transaction.getMaxOutputsConstant()) { + if (tx.outputs.length > versionData.maxNumberOutputs) { return closeDbAndGetError(mysql, ApiError.TOO_MANY_OUTPUTS, { outputs: tx.outputs.length }); } @@ -110,7 +112,7 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { return closeDbAndGetError(mysql, ApiError.INPUTS_ALREADY_USED); } - if (inputUtxos.length > hathorLib.transaction.getMaxInputsConstant()) { + if (inputUtxos.length > versionData.maxNumberOutputs) { return closeDbAndGetError(mysql, ApiError.TOO_MANY_INPUTS, { inputs: inputUtxos.length }); } @@ -127,7 +129,7 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { // XXX We should store in address table the path of the address, not the index // For now we return the hardcoded path with only the address index as variable // The client will be prepared to receive any path when we add this in the service in the future - const addressPath = `m/44'/${hathorLib.constants.HATHOR_BIP44_CODE}'/0'/0/${addressDetail.index}`; + const addressPath = `m/44'/${constants.HATHOR_BIP44_CODE}'/0'/0/${addressDetail.index}`; return { txId: utxo.txId, index: utxo.index, addressPath }; }); diff --git a/packages/wallet-service/src/api/txProposalSend.ts b/packages/wallet-service/src/api/txProposalSend.ts index fcd42f38..8925dd98 100644 --- a/packages/wallet-service/src/api/txProposalSend.ts +++ b/packages/wallet-service/src/api/txProposalSend.ts @@ -28,6 +28,7 @@ import { import { closeDbAndGetError } from '@src/api/utils'; import middy from '@middy/core'; import cors from '@middy/http-cors'; +import config from '@src/config'; const mysql = getDbConnection(); @@ -91,7 +92,7 @@ export const send: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (wa const now = getUnixTimestamp(); const txProposalInputs = await getTxProposalInputs(mysql, txProposalId); - const tx = hathorLib.helpersUtils.createTxFromHex(txHex, new hathorLib.Network(process.env.NETWORK)); + const tx = hathorLib.helpersUtils.createTxFromHex(txHex, new hathorLib.Network(config.network)); if (tx.inputs.length !== txProposalInputs.length) { return closeDbAndGetError(mysql, ApiError.TX_PROPOSAL_NO_MATCH); diff --git a/packages/wallet-service/src/api/txhistory.ts b/packages/wallet-service/src/api/txhistory.ts index 5540b051..ddeebae5 100644 --- a/packages/wallet-service/src/api/txhistory.ts +++ b/packages/wallet-service/src/api/txhistory.ts @@ -19,9 +19,10 @@ import { walletIdProxyHandler } from '@src/commons'; import middy from '@middy/core'; import cors from '@middy/http-cors'; import Joi from 'joi'; +import config from '@src/config'; -const MAX_COUNT = parseInt(process.env.TX_HISTORY_MAX_COUNT || '50', 10); -const htrToken = hathorLib.constants.HATHOR_TOKEN_CONFIG.uid; +const MAX_COUNT = config.txHistoryMaxCount; +const htrToken = hathorLib.constants.NATIVE_TOKEN_UID; const paramsSchema = Joi.object({ token_id: Joi.string() diff --git a/packages/wallet-service/src/api/version.ts b/packages/wallet-service/src/api/version.ts index 64fc00f7..342c5580 100644 --- a/packages/wallet-service/src/api/version.ts +++ b/packages/wallet-service/src/api/version.ts @@ -8,18 +8,12 @@ import { APIGatewayProxyHandler } from 'aws-lambda'; import 'source-map-support/register'; -import { - getVersionData, -} from '@src/db'; -import { - FullNodeVersionData, -} from '@src/types'; import { closeDbConnection, getDbConnection, } from '@src/utils'; import { warmupMiddleware } from '@src/api/utils'; -import { maybeRefreshWalletConstants } from '@src/commons'; +import { getRawFullnodeData } from '@src/nodeConfig' import middy from '@middy/core'; import cors from '@middy/http-cors'; @@ -31,9 +25,7 @@ const mysql = getDbConnection(); * This lambda is called by API Gateway on GET /version */ export const get: APIGatewayProxyHandler = middy(async () => { - await maybeRefreshWalletConstants(mysql); - - const versionData: FullNodeVersionData = await getVersionData(mysql); + const versionData = await getRawFullnodeData(mysql); await closeDbConnection(mysql); diff --git a/packages/wallet-service/src/api/wallet.ts b/packages/wallet-service/src/api/wallet.ts index d709e7d3..96cdc938 100644 --- a/packages/wallet-service/src/api/wallet.ts +++ b/packages/wallet-service/src/api/wallet.ts @@ -40,10 +40,11 @@ import Joi from 'joi'; import createDefaultLogger from '@src/logger'; import { Severity } from '@wallet-service/common/src/types'; import { addAlert } from '@wallet-service/common/src/utils/alerting.utils'; +import config from '@src/config'; const mysql = getDbConnection(); -const MAX_LOAD_WALLET_RETRIES: number = parseInt(process.env.MAX_LOAD_WALLET_RETRIES || '5', 10); +const MAX_LOAD_WALLET_RETRIES: number = config.maxLoadWalletRetries; /* * Get the status of a wallet @@ -67,7 +68,7 @@ export const get: APIGatewayProxyHandler = middy(walletIdProxyHandler(async (wal // If the env requires to validate the first address // then we must set the firstAddress field as required -const shouldConfirmFirstAddress = process.env.CONFIRM_FIRST_ADDRESS === 'true'; +const shouldConfirmFirstAddress = config.confirmFirstAddress; const firstAddressJoi = shouldConfirmFirstAddress ? Joi.string().required() : Joi.string(); const loadBodySchema = Joi.object({ @@ -92,14 +93,14 @@ const loadBodySchema = Joi.object({ /* istanbul ignore next */ export const invokeLoadWalletAsync = async (xpubkey: string, maxGap: number): Promise => { const client = new LambdaClient({ - endpoint: process.env.STAGE === 'dev' + endpoint: config.stage === 'dev' ? 'http://localhost:3002' - : `https://lambda.${process.env.AWS_REGION}.amazonaws.com`, - region: process.env.AWS_REGION, + : `https://lambda.${config.awsRegion}.amazonaws.com`, + region: config.awsRegion, }); const command = new InvokeCommand({ // FunctionName is composed of: service name - stage - function name - FunctionName: `${process.env.SERVICE_NAME}-${process.env.STAGE}-loadWalletAsync`, + FunctionName: `${config.serviceName}-${config.stage}-loadWalletAsync`, InvocationType: 'Event', Payload: JSON.stringify({ xpubkey, maxGap }), }); @@ -276,7 +277,7 @@ export const load: APIGatewayProxyHandler = middy(async (event) => { const xpubkeyStr = value.xpubkey; const authXpubkeyStr = value.authXpubkey; - const maxGap = parseInt(process.env.MAX_ADDRESS_GAP, 10); + const maxGap = config.maxAddressGap; const timestamp = value.timestamp; const xpubkeySignature = value.xpubkeySignature; @@ -442,6 +443,7 @@ export const loadWalletFailed: Handler = async (event) => { ); } } catch (e) { + logger.error('Error during loadWalletFailed', e); await addAlert( 'Failed to handle loadWalletFailed event', `Failed to process the loadWalletFailed event. This indicates that wallets failed to load and we weren't able to recover, please check the logs as soon as possible.`, diff --git a/packages/wallet-service/src/commons.ts b/packages/wallet-service/src/commons.ts index 809ce4cd..341de854 100644 --- a/packages/wallet-service/src/commons.ts +++ b/packages/wallet-service/src/commons.ts @@ -19,8 +19,6 @@ import { unlockUtxos as dbUnlockUtxos, updateAddressLockedBalance, updateWalletLockedBalance, - getVersionData, - updateVersionData, getBlockByHeight, getTxsAfterHeight, markTxsAsVoided, @@ -47,7 +45,6 @@ import { Wallet, Block, WalletTokenBalance, - FullNodeVersionData, AddressBalance, AddressTotalBalance, WalletProxyHandler, @@ -64,16 +61,13 @@ import { } from '@wallet-service/common/src/types'; import { addAlert } from '@wallet-service/common/src/utils/alerting.utils'; -import { - getUnixTimestamp, - isTxVoided, -} from '@src/utils'; +import { isTxVoided } from '@src/utils'; import hathorLib from '@hathor/wallet-lib'; import { stringMapIterator, WalletBalanceMapConverter } from '@src/db/utils'; +import config from '@src/config'; -const VERSION_CHECK_MAX_DIFF = 60 * 60 * 1000; // 1 hour -const WARN_MAX_REORG_SIZE = parseInt(process.env.WARN_MAX_REORG_SIZE || '100', 10); +const WARN_MAX_REORG_SIZE = config.warnMaxReorgSize; /** * Update the unlocked/locked balances for addresses and wallets connected to the given UTXOs. @@ -294,46 +288,6 @@ export const getWalletBalances = async ( return balances; }; -/** - * Updates the wallet-lib constants if needed. - * - * @returns {Promise} A promise that resolves when the wallet-lib constants have been set. - */ -export const maybeRefreshWalletConstants = async (mysql: ServerlessMysql): Promise => { - const lastVersionData: FullNodeVersionData = await getVersionData(mysql); - const now = getUnixTimestamp(); - - if (!lastVersionData || now - lastVersionData.timestamp > VERSION_CHECK_MAX_DIFF) { - // Query and update versions - const apiResponse = await hathorLib.version.checkApiVersion(); - const versionData: FullNodeVersionData = { - timestamp: now, - version: apiResponse.version, - network: apiResponse.network, - minWeight: apiResponse.min_weight, - minTxWeight: apiResponse.min_tx_weight, - minTxWeightCoefficient: apiResponse.min_tx_weight_coefficient, - minTxWeightK: apiResponse.min_tx_weight_k, - tokenDepositPercentage: apiResponse.token_deposit_percentage, - rewardSpendMinBlocks: apiResponse.reward_spend_min_blocks, - maxNumberInputs: apiResponse.max_number_inputs, - maxNumberOutputs: apiResponse.max_number_outputs, - }; - - await updateVersionData(mysql, versionData); - } else { - hathorLib.transaction.updateTransactionWeightConstants( - lastVersionData.minTxWeight, - lastVersionData.minTxWeightCoefficient, - lastVersionData.minTxWeightK, - ); - hathorLib.tokens.updateDepositPercentage(lastVersionData.tokenDepositPercentage); - hathorLib.transaction.updateMaxInputsConstant(lastVersionData.maxNumberInputs); - hathorLib.transaction.updateMaxOutputsConstant(lastVersionData.maxNumberOutputs); - hathorLib.wallet.updateRewardLockConstant(lastVersionData.rewardSpendMinBlocks); - } -}; - /** * Searches our blocks database for the last block that is not voided. * diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts new file mode 100644 index 00000000..78589f31 --- /dev/null +++ b/packages/wallet-service/src/config.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { EnvironmentConfig } from '@src/types'; +import { EnvironmentConfigSchema } from '@src/schemas'; +import { Severity, addAlert } from '@wallet-service/common'; + +export function loadEnvConfig(): EnvironmentConfig { + const config: EnvironmentConfig = { + defaultServer: process.env.DEFAULT_SERVER ?? 'https://node1.mainnet.hathor.network/v1a/', + stage: process.env.STAGE, + network: process.env.NETWORK, + serviceName: process.env.SERVICE_NAME, + maxAddressGap: Number.parseInt(process.env.MAX_ADDRESS_GAP, 10), + voidedTxOffset: Number.parseInt(process.env.VOIDED_TX_OFFSET, 10), + confirmFirstAddress: process.env.CONFIRM_FIRST_ADDRESS === 'true', + wsDomain: process.env.WS_DOMAIN, + dbEndpoint: process.env.DB_ENDPOINT, + dbName: process.env.DB_NAME, + dbUser: process.env.DB_USER, + dbPort: Number.parseInt(process.env.DB_PORT, 10), + dbPass: process.env.DB_PASS, + redisUrl: process.env.REDIS_URL, + redisPassword: process.env.REDIS_PASSWORD, + authSecret: process.env.AUTH_SECRET, + walletServiceLambdaEndpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, + pushNotificationEnabled: process.env.PUSH_NOTIFICATION_ENABLED === 'true', + pushAllowedProviders: process.env.PUSH_ALLOWED_PROVIDERS, + isOffline: process.env.IS_OFFLINE === 'true', + txHistoryMaxCount: Number.parseInt(process.env.TX_HISTORY_MAX_COUNT || '50', 10), + healthCheckMaximumHeightDifference: Number.parseInt(process.env.HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE ?? '5', 10), + + awsRegion: process.env.AWS_REGION, + + firebaseProjectId: process.env.FIREBASE_PROJECT_ID, + firebasePrivateKeyId: process.env.FIREBASE_PRIVATE_KEY_ID, + firebaseClientEmail: process.env.FIREBASE_CLIENT_EMAIL, + firebaseClientId: process.env.FIREBASE_CLIENT_ID, + firebaseAuthUri: process.env.FIREBASE_AUTH_URI, + firebaseTokenUri: process.env.FIREBASE_TOKEN_URI, + firebaseAuthProviderX509CertUrl: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, + firebaseClientX509CertUrl: process.env.FIREBASE_CLIENT_X509_CERT_URL, + firebasePrivateKey: process.env.FIREBASE_PRIVATE_KEY, + + maxLoadWalletRetries: Number.parseInt(process.env.MAX_LOAD_WALLET_RETRIES || '5', 10), + logLevel: process.env.LOG_LEVEL || 'info', + createNftMaxRetries: Number.parseInt(process.env.CREATE_NFT_MAX_RETRIES || '3', 10), + warnMaxReorgSize: Number.parseInt(process.env.WARN_MAX_REORG_SIZE || '100', 10), + }; + + if (process.env.NODE_ENV === 'test') { + return config; + } + + const { value, error } = EnvironmentConfigSchema.validate(config); + if (error) { + addAlert( + 'Environment config ', + error.message, + Severity.CRITICAL, + null, + // @ts-ignore: cannot import logger on this file, creates an import cycle + { error: console.error }, + ); + throw error; + } + + return value; +}; + +export default loadEnvConfig(); diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index da913f5e..5ba752ca 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -29,7 +29,6 @@ import { Wallet, WalletStatus, WalletTokenBalance, - FullNodeVersionData, Block, Tx, AddressBalance, @@ -39,6 +38,7 @@ import { PushDevice, TxByIdToken, PushDeviceSettings, + FullNodeApiVersionResponse, } from '@src/types'; import { getUnixTimestamp, @@ -57,6 +57,7 @@ import { addAlert } from '@wallet-service/common/src/utils/alerting.utils'; import { TxInput, Severity } from '@wallet-service/common/src/types'; import { Logger } from 'winston'; import createDefaultLogger from '@src/logger'; +import { FullnodeVersionSchema } from '@src/schemas'; const logger: Logger = createDefaultLogger(); @@ -1584,22 +1585,14 @@ export const getWalletUnlockedUtxos = async ( * Update latest version_data on the database * * @param mysql - Database connection + * @param timestamp - Unix timestamp to mark when the data was fetched * @param data - Latest version data to store */ -export const updateVersionData = async (mysql: ServerlessMysql, data: FullNodeVersionData): Promise => { +export const updateVersionData = async (mysql: ServerlessMysql, timestamp: number, data: FullNodeApiVersionResponse): Promise => { const entry = { id: 1, - timestamp: data.timestamp, - version: data.version, - network: data.network, - min_weight: data.minWeight, - min_tx_weight: data.minTxWeight, - min_tx_weight_coefficient: data.minTxWeightCoefficient, - min_tx_weight_k: data.minTxWeightK, - token_deposit_percentage: data.tokenDepositPercentage, - reward_spend_min_blocks: data.rewardSpendMinBlocks, - max_number_inputs: data.maxNumberInputs, - max_number_outputs: data.maxNumberOutputs, + timestamp, + data: JSON.stringify(data), }; await mysql.query( @@ -1614,30 +1607,28 @@ export const updateVersionData = async (mysql: ServerlessMysql, data: FullNodeVe * @param mysql - Database connection * @returns */ -export const getVersionData = async (mysql: ServerlessMysql): Promise => { +export const getVersionData = async (mysql: ServerlessMysql): Promise<{ timestamp: number, data: FullNodeApiVersionResponse } | null> => { const results: DbSelectResult = await mysql.query('SELECT * FROM `version_data` WHERE id = 1 LIMIT 1;'); if (results.length > 0) { const data = results[0]; - const entry: FullNodeVersionData = { + const entry: FullNodeApiVersionResponse = JSON.parse(data.data as string); + const { error } = FullnodeVersionSchema.validate(entry); + if (error) { + throw error; + } + + return { timestamp: data.timestamp as number, - version: data.version as string, - network: data.network as string, - minWeight: data.min_weight as number, - minTxWeight: data.min_tx_weight as number, - minTxWeightCoefficient: data.min_tx_weight_coefficient as number, - minTxWeightK: data.min_tx_weight_k as number, - tokenDepositPercentage: data.token_deposit_percentage as number, - rewardSpendMinBlocks: data.reward_spend_min_blocks as number, - maxNumberInputs: data.max_number_inputs as number, - maxNumberOutputs: data.max_number_outputs as number, + data: entry as FullNodeApiVersionResponse, }; - - return entry; } - return null; + return { + timestamp: 0, + data: null, + }; }; /** @@ -3093,7 +3084,7 @@ export const getPushDeviceSettingsList = async ( * @param mysql - Database connection * @returns - total of stale device from now */ -export const countStalePushDevices = async (mysql): Promise => { +export const countStalePushDevices = async (mysql: ServerlessMysql): Promise => { const [{ count }] = await mysql.query( ` SELECT COUNT(device_id) as count @@ -3108,7 +3099,7 @@ export const countStalePushDevices = async (mysql): Promise => { * * @param mysql - Database connection */ -export const deleteStalePushDevices = async (mysql) => { +export const deleteStalePushDevices = async (mysql: ServerlessMysql) => { await mysql.query( ` DELETE diff --git a/packages/wallet-service/src/fullnode.ts b/packages/wallet-service/src/fullnode.ts index 318e610c..3e024ed2 100644 --- a/packages/wallet-service/src/fullnode.ts +++ b/packages/wallet-service/src/fullnode.ts @@ -6,8 +6,12 @@ */ import axios from 'axios'; +import Joi from 'joi'; +import config from '@src/config'; +import { FullNodeApiVersionResponse } from '@src/types'; +import { FullnodeVersionSchema } from '@src/schemas'; -export const BASE_URL = process.env.DEFAULT_SERVER; +export const BASE_URL = config.defaultServer; export const TIMEOUT = 10000; /** @@ -15,13 +19,26 @@ export const TIMEOUT = 10000; * * @param baseURL - The base URL for the full-node. Defaults to `env.DEFAULT_SERVER` */ -export const create = (baseURL = BASE_URL): any => { +export const create = (baseURL = BASE_URL) => { const api = axios.create({ baseURL, headers: {}, timeout: TIMEOUT, }); + const version = async (): Promise => { + const response = await api.get('version', { + data: null, + headers: { 'content-type': 'application/json' }, + }); + const { value, error } = FullnodeVersionSchema.validate(response.data); + if (error) { + throw new Error(error.message); + } + + return value as FullNodeApiVersionResponse; + }; + const downloadTx = async (txId: string) => { const response = await api.get(`transaction?id=${txId}`, { data: null, @@ -74,6 +91,7 @@ export const create = (baseURL = BASE_URL): any => { return { api, // exported so we can mock it on the tests + version, downloadTx, getConfirmationData, queryGraphvizNeighbours, diff --git a/packages/wallet-service/src/logger.ts b/packages/wallet-service/src/logger.ts index 57d01871..0e582362 100644 --- a/packages/wallet-service/src/logger.ts +++ b/packages/wallet-service/src/logger.ts @@ -1,7 +1,8 @@ import { createLogger, format, transports, Logger } from 'winston'; +import config from '@src/config'; const createDefaultLogger = (): Logger => createLogger({ - level: process.env.LOG_LEVEL || 'info', + level: config.logLevel, format: format.json(), transports: [ new transports.Console(), diff --git a/packages/wallet-service/src/mempool.ts b/packages/wallet-service/src/mempool.ts index 4ed2e7d3..4b9f2a53 100644 --- a/packages/wallet-service/src/mempool.ts +++ b/packages/wallet-service/src/mempool.ts @@ -22,6 +22,7 @@ import { import createDefaultLogger from '@src/logger'; import { Severity } from '@wallet-service/common/src/types'; import { addAlert } from '@wallet-service/common/src/utils/alerting.utils'; +import config from '@src/config'; const mysql = getDbConnection(); @@ -36,7 +37,7 @@ const mysql = getDbConnection(); export const onHandleOldVoidedTxs = async (): Promise => { const logger = createDefaultLogger(); - const VOIDED_TX_OFFSET: number = parseInt(process.env.VOIDED_TX_OFFSET, 10) * 60; // env is in minutes + const VOIDED_TX_OFFSET: number = config.voidedTxOffset * 60; // env is in minutes const bestBlock: Block = await getLatestBlockByHeight(mysql); const bestBlockTimestamp = bestBlock.timestamp; @@ -44,7 +45,7 @@ export const onHandleOldVoidedTxs = async (): Promise => { // Fetch voided transactions that are older than 20m const voidedTransactions: Tx[] = await getMempoolTransactionsBeforeDate(mysql, date); - logger.debug(`Found ${voidedTransactions.length} voided transactions older than ${process.env.VOIDED_TX_OFFSET}m from the best block`, { + logger.debug(`Found ${voidedTransactions.length} voided transactions older than ${config.voidedTxOffset}m from the best block`, { voidedTransactions, }); diff --git a/packages/wallet-service/src/metrics.ts b/packages/wallet-service/src/metrics.ts index bdf45e72..22f8849c 100644 --- a/packages/wallet-service/src/metrics.ts +++ b/packages/wallet-service/src/metrics.ts @@ -11,13 +11,14 @@ import 'source-map-support/register'; import { getLatestHeight } from '@src/db'; import { closeDbConnection, getDbConnection } from '@src/utils'; +import config from '@src/config'; const mysql = getDbConnection(); // Default labels const defaultLabels = { - network: process.env.NETWORK, - environment: process.env.STAGE, + network: config.network, + environment: config.stage, }; promClient.register.setDefaultLabels(defaultLabels); diff --git a/packages/wallet-service/src/nodeConfig.ts b/packages/wallet-service/src/nodeConfig.ts new file mode 100644 index 00000000..189506e4 --- /dev/null +++ b/packages/wallet-service/src/nodeConfig.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { getUnixTimestamp } from '@src/utils'; +import { ServerlessMysql } from 'serverless-mysql'; +import { getVersionData, updateVersionData } from '@src/db'; +import { FullNodeVersionData, FullNodeApiVersionResponse } from '@src/types'; +import fullnode from '@src/fullnode'; + +const VERSION_CHECK_MAX_DIFF = 60 * 60; // 1 hour + +/** + * Get fullnode version data as an Object exactly as the fullnode sent it. + * Will get from database if the cached version data is valid. + */ +export async function getRawFullnodeData(mysql: ServerlessMysql): Promise { + const { + timestamp, + data: lastVersionData, + } = await getVersionData(mysql); + const now = getUnixTimestamp(); + + if (!lastVersionData || now - timestamp > VERSION_CHECK_MAX_DIFF) { + const versionData = await fullnode.version(); + await updateVersionData(mysql, timestamp, versionData); + return versionData; + } + + return lastVersionData; +} + +/** + * Convert the raw version data from the fullnode to a camel-cased object. + */ +export function convertApiVersionData(data: FullNodeApiVersionResponse): FullNodeVersionData { + return { + version: data.version, + network: data.network, + minWeight: data.min_weight, + minTxWeight: data.min_tx_weight, + minTxWeightCoefficient: data.min_tx_weight_coefficient, + minTxWeightK: data.min_tx_weight_k, + tokenDepositPercentage: data.token_deposit_percentage, + rewardSpendMinBlocks: data.reward_spend_min_blocks, + maxNumberInputs: data.max_number_inputs, + maxNumberOutputs: data.max_number_outputs, + decimalPlaces: data.decimal_places, + nativeTokenName: data.native_token.name, + nativeTokenSymbol: data.native_token.symbol, + }; +} + +/** + * Gets the converted fullnode version data. + * Will get from database if the cached version data is valid. + */ +export async function getFullnodeData(mysql: ServerlessMysql): Promise { + const data = await getRawFullnodeData(mysql); + return convertApiVersionData(data); +} diff --git a/packages/wallet-service/src/redis.ts b/packages/wallet-service/src/redis.ts index 02328a68..20456b83 100644 --- a/packages/wallet-service/src/redis.ts +++ b/packages/wallet-service/src/redis.ts @@ -5,10 +5,11 @@ import { import redis from 'redis'; import { promisify } from 'util'; +import config from '@src/config'; const redisConfig: RedisConfig = { - url: process.env.REDIS_URL, - password: process.env.REDIS_PASSWORD, + url: config.redisUrl, + password: config.redisPassword, }; export const svcPrefix = 'walletsvc'; diff --git a/packages/wallet-service/src/schemas.ts b/packages/wallet-service/src/schemas.ts new file mode 100644 index 00000000..042ba759 --- /dev/null +++ b/packages/wallet-service/src/schemas.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Joi from 'joi'; +import { FullNodeApiVersionResponse, EnvironmentConfig } from '@src/types'; + +export const FullnodeVersionSchema = Joi.object({ + version: Joi.string().min(1).required(), + network: Joi.string().min(1).required(), + min_weight: Joi.number().integer().positive().required(), + min_tx_weight: Joi.number().integer().positive().required(), + min_tx_weight_coefficient: Joi.number().positive().required(), + min_tx_weight_k: Joi.number().integer().positive().required(), + token_deposit_percentage: Joi.number().positive().required(), + reward_spend_min_blocks: Joi.number().integer().positive().required(), + max_number_inputs: Joi.number().integer().positive().required(), + max_number_outputs: Joi.number().integer().positive().required(), + decimal_places: Joi.number().integer().positive().required(), + genesis_block_hash: Joi.string().min(1).required(), + genesis_tx1_hash: Joi.string().hex().length(64).required(), + genesis_tx2_hash: Joi.string().hex().length(64).required(), + native_token: Joi.object({ + name: Joi.string().min(1).max(30).required(), + symbol: Joi.string().min(1).max(5).required(), + }), +}).unknown(true); + +export const EnvironmentConfigSchema = Joi.object({ + defaultServer: Joi.string().required(), + stage: Joi.string().required(), + network: Joi.string().required(), + serviceName: Joi.string().required(), + maxAddressGap: Joi.number().required(), + voidedTxOffset: Joi.number().required(), + confirmFirstAddress: Joi.boolean().required(), + wsDomain: Joi.string().required(), + dbEndpoint: Joi.string().required(), + dbName: Joi.string().required(), + dbUser: Joi.string().required(), + dbPass: Joi.string().required(), + dbPort: Joi.number().required(), + redisUrl: Joi.string().required(), + redisPassword: Joi.string(), + authSecret: Joi.string().required(), + walletServiceLambdaEndpoint: Joi.string().required(), + pushNotificationEnabled: Joi.boolean().required(), + pushAllowedProviders: Joi.string().required(), + isOffline: Joi.boolean().required(), + txHistoryMaxCount: Joi.number().required(), + healthCheckMaximumHeightDifference: Joi.number().required(), + awsRegion: Joi.string().required(), + + firebaseProjectId: Joi.string().required(), + firebasePrivateKeyId: Joi.string().required(), + firebaseClientEmail: Joi.string().required(), + firebaseClientId: Joi.string().required(), + firebaseAuthUri: Joi.string().required(), + firebaseTokenUri: Joi.string().required(), + firebaseAuthProviderX509CertUrl: Joi.string().required(), + firebaseClientX509CertUrl: Joi.string().required(), + firebasePrivateKey: Joi.string().allow(null).required(), + + maxLoadWalletRetries: Joi.number().required(), + logLevel: Joi.string().required(), + createNftMaxRetries: Joi.number().required(), + warnMaxReorgSize: Joi.number().required(), +}); diff --git a/packages/wallet-service/src/txProcessor.ts b/packages/wallet-service/src/txProcessor.ts index a3748243..57c37e59 100644 --- a/packages/wallet-service/src/txProcessor.ts +++ b/packages/wallet-service/src/txProcessor.ts @@ -9,8 +9,9 @@ import { Handler } from 'aws-lambda'; import 'source-map-support/register'; import createDefaultLogger from '@src/logger'; import { NftUtils } from '@wallet-service/common/src/utils/nft.utils'; +import config from '@src/config'; -export const CREATE_NFT_MAX_RETRIES: number = parseInt(process.env.CREATE_NFT_MAX_RETRIES || '3', 10); +export const CREATE_NFT_MAX_RETRIES: number = config.createNftMaxRetries; /** * This intermediary handler is responsible for making the final validations and calling diff --git a/packages/wallet-service/src/types.ts b/packages/wallet-service/src/types.ts index dde54bd7..e2c8d811 100644 --- a/packages/wallet-service/src/types.ts +++ b/packages/wallet-service/src/types.ts @@ -39,8 +39,52 @@ export enum TxProposalStatus { CANCELLED = 'cancelled', } +/** + * wallet-service environment config. + */ +export interface EnvironmentConfig { + defaultServer: string; + stage: string; + network: string; + serviceName: string; + maxAddressGap: number; + voidedTxOffset: number; + confirmFirstAddress: boolean; + wsDomain: string; + dbEndpoint: string; + dbName: string; + dbUser: string; + dbPass: string; + dbPort: number; + redisUrl: string; + redisPassword: string; + authSecret: string; + walletServiceLambdaEndpoint: string; + pushNotificationEnabled: boolean; + pushAllowedProviders: string; + isOffline: boolean; + txHistoryMaxCount: number; + healthCheckMaximumHeightDifference: number; + awsRegion: string; + firebaseProjectId: string; + firebasePrivateKeyId: string; + firebaseClientEmail: string; + firebaseClientId: string; + firebaseAuthUri: string; + firebaseTokenUri: string; + firebaseAuthProviderX509CertUrl: string; + firebaseClientX509CertUrl: string; + firebasePrivateKey: string|null; + maxLoadWalletRetries: number; + logLevel: string; + createNftMaxRetries: number; + warnMaxReorgSize: number; +}; + +/** + * Fullnode converted version data. + */ export interface FullNodeVersionData { - timestamp: number; version: string; network: string; minWeight: number; @@ -51,6 +95,30 @@ export interface FullNodeVersionData { rewardSpendMinBlocks: number; maxNumberInputs: number; maxNumberOutputs: number; + decimalPlaces: number; + nativeTokenName: string; + nativeTokenSymbol: string; +} + +/** + * Fullnode API response. + */ +export interface FullNodeApiVersionResponse { + version: string; + network: string; + min_weight: number; + min_tx_weight: number; + min_tx_weight_coefficient: number; // float + min_tx_weight_k: number; + token_deposit_percentage: number; // float + reward_spend_min_blocks: number; + max_number_inputs: number; + max_number_outputs: number; + decimal_places: number; + genesis_block_hash: string, + genesis_tx1_hash: string, + genesis_tx2_hash: string, + native_token: { name: string, symbol: string}; } export interface TxProposal { @@ -111,9 +179,9 @@ export class TokenInfo { this.symbol = symbol; this.transactions = transactions || 0; - const hathorConfig = hathorLib.constants.HATHOR_TOKEN_CONFIG; + const hathorConfig = hathorLib.constants.DEFAULT_NATIVE_TOKEN_CONFIG; - if (this.id === hathorConfig.uid) { + if (this.id === hathorLib.constants.NATIVE_TOKEN_UID) { this.name = hathorConfig.name; this.symbol = hathorConfig.symbol; } diff --git a/packages/wallet-service/src/utils.ts b/packages/wallet-service/src/utils.ts index dea16943..06809ab1 100644 --- a/packages/wallet-service/src/utils.ts +++ b/packages/wallet-service/src/utils.ts @@ -15,54 +15,16 @@ import * as bitcoin from 'bitcoinjs-lib'; import * as bitcoinMessage from 'bitcoinjs-message'; import * as ecc from 'tiny-secp256k1'; import BIP32Factory from 'bip32'; +import config from '@src/config'; const bip32 = BIP32Factory(ecc); -/* TODO: We should remove this as soon as the wallet-lib is refactored -* (https://github.com/HathorNetwork/hathor-wallet-lib/issues/122) -*/ -export class CustomStorage { - store: unknown; - - constructor() { - this.preStart(); - } - - getItem(key: string): string { - return this.store[key]; - } - - setItem(key: string, value: string): string { - this.store[key] = value; - - return value; - } - - removeItem(key: string): string { - delete this.store[key]; - - return key; - } - - clear(): void { - this.store = {}; - } - - preStart(): void { - this.store = { - 'wallet:server': process.env.DEFAULT_SERVER || hathorLib.constants.DEFAULT_SERVER, - 'wallet:defaultServer': process.env.DEFAULT_SERVER || hathorLib.constants.DEFAULT_SERVER, - }; - } -} - -hathorLib.network.setNetwork(process.env.NETWORK); -hathorLib.storage.setStore(new CustomStorage()); +hathorLib.network.setNetwork(config.network); const libNetwork = hathorLib.network.getNetwork(); const hathorNetwork = { messagePrefix: '\x18Hathor Signed Message:\n', - bech32: hathorLib.network.bech32prefix, + bech32: libNetwork.bech32prefix, bip32: { public: libNetwork.xpubkey, private: libNetwork.xprivkey, @@ -117,19 +79,19 @@ export const getUnixTimestamp = (): number => ( export const getDbConnection = (): ServerlessMysql => ( serverlessMysql({ config: { - host: process.env.DB_ENDPOINT, - database: process.env.DB_NAME, - user: process.env.DB_USER, - port: parseInt(process.env.DB_PORT, 10), + host: config.dbEndpoint, + database: config.dbName, + user: config.dbUser, + port: config.dbPort, // TODO if not on local env, get IAM token // https://aws.amazon.com/blogs/database/iam-role-based-authentication-to-amazon-aurora-from-serverless-applications/ - password: process.env.DB_PASS, + password: config.dbPass, }, }) ); export const closeDbConnection = async (mysql: ServerlessMysql): Promise => { - if (process.env.STAGE === 'local') { + if (config.stage === 'local') { // mysql.end() leaves the function hanging in the local environment. Some issues: // https://github.com/jeremydaly/serverless-mysql/issues/61 // https://github.com/jeremydaly/serverless-mysql/issues/79 diff --git a/packages/wallet-service/src/utils/pushnotification.utils.ts b/packages/wallet-service/src/utils/pushnotification.utils.ts index 9aa5362c..8e5ef5af 100644 --- a/packages/wallet-service/src/utils/pushnotification.utils.ts +++ b/packages/wallet-service/src/utils/pushnotification.utils.ts @@ -11,6 +11,7 @@ import { Severity } from '@wallet-service/common/src/types'; import fcmAdmin, { credential, messaging, ServiceAccount } from 'firebase-admin'; import { MulticastMessage } from 'firebase-admin/messaging'; import createDefaultLogger from '@src/logger'; +import config from '@src/config'; import { assertEnvVariablesExistence } from '@wallet-service/common/src/utils/index.utils'; import { addAlert } from '@wallet-service/common/src/utils/alerting.utils'; @@ -44,7 +45,7 @@ try { } export function buildFunctionName(functionName: string): string { - return `hathor-wallet-service-${process.env.STAGE}-${functionName}`; + return `hathor-wallet-service-${config.stage}-${functionName}`; } export enum FunctionName { @@ -52,19 +53,19 @@ export enum FunctionName { ON_TX_PUSH_NOTIFICATION_REQUESTED = 'txPushRequested', } -const STAGE = process.env.STAGE; -const AWS_REGION = process.env.AWS_REGION; -const WALLET_SERVICE_LAMBDA_ENDPOINT = process.env.WALLET_SERVICE_LAMBDA_ENDPOINT; +const STAGE = config.stage; +const AWS_REGION = config.awsRegion; +const WALLET_SERVICE_LAMBDA_ENDPOINT = config.walletServiceLambdaEndpoint; const SEND_NOTIFICATION_FUNCTION_NAME = buildFunctionName(FunctionName.SEND_NOTIFICATION_TO_DEVICE); const ON_TX_PUSH_NOTIFICATION_REQUESTED_FUNCTION_NAME = buildFunctionName(FunctionName.ON_TX_PUSH_NOTIFICATION_REQUESTED); -const FIREBASE_PROJECT_ID = process.env.FIREBASE_PROJECT_ID; -const FIREBASE_PRIVATE_KEY_ID = process.env.FIREBASE_PRIVATE_KEY_ID; -const FIREBASE_CLIENT_EMAIL = process.env.FIREBASE_CLIENT_EMAIL; -const FIREBASE_CLIENT_ID = process.env.FIREBASE_CLIENT_ID; -const FIREBASE_AUTH_URI = process.env.FIREBASE_AUTH_URI; -const FIREBASE_TOKEN_URI = process.env.FIREBASE_TOKEN_URI; -const FIREBASE_AUTH_PROVIDER_X509_CERT_URL = process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL; -const FIREBASE_CLIENT_X509_CERT_URL = process.env.FIREBASE_CLIENT_X509_CERT_URL; +const FIREBASE_PROJECT_ID = config.firebaseProjectId; +const FIREBASE_PRIVATE_KEY_ID = config.firebasePrivateKeyId; +const FIREBASE_CLIENT_EMAIL = config.firebaseClientEmail; +const FIREBASE_CLIENT_ID = config.firebaseClientId; +const FIREBASE_AUTH_URI = config.firebaseAuthUri; +const FIREBASE_TOKEN_URI = config.firebaseTokenUri; +const FIREBASE_AUTH_PROVIDER_X509_CERT_URL = config.firebaseAuthProviderX509CertUrl; +const FIREBASE_CLIENT_X509_CERT_URL = config.firebaseClientX509CertUrl; const FIREBASE_PRIVATE_KEY = (() => { try { /** @@ -73,7 +74,7 @@ const FIREBASE_PRIVATE_KEY = (() => { * the escaped line break with an unescaped line break. * https://github.com/gladly-team/next-firebase-auth/discussions/95#discussioncomment-2891225 */ - const privateKey = process.env.FIREBASE_PRIVATE_KEY; + const privateKey = config.firebasePrivateKey; return privateKey ? privateKey.replace(/\\n/gm, '\n') : null; @@ -84,7 +85,7 @@ const FIREBASE_PRIVATE_KEY = (() => { })(); /** Local feature toggle that disable the push notification by default */ -const PUSH_NOTIFICATION_ENABLED = process.env.PUSH_NOTIFICATION_ENABLED; +const PUSH_NOTIFICATION_ENABLED = config.pushNotificationEnabled; /** * Controls which providers are allowed to send notification when it is enabled * @example @@ -101,7 +102,7 @@ const PUSH_NOTIFICATION_ENABLED = process.env.PUSH_NOTIFICATION_ENABLED; * ``` * */ const PUSH_ALLOWED_PROVIDERS = (() => { - const providers = process.env.PUSH_ALLOWED_PROVIDERS; + const providers = config.pushAllowedProviders; if (!providers) { // If no providers are set, we allow android by default, but alert the environment variable is empty logger.error('[ALERT] env.PUSH_ALLOWED_PROVIDERS is empty.'); @@ -112,7 +113,8 @@ const PUSH_ALLOWED_PROVIDERS = (() => { export const isPushProviderAllowed = (provider: string): boolean => PUSH_ALLOWED_PROVIDERS.includes(provider); -export const isPushNotificationEnabled = (): boolean => PUSH_NOTIFICATION_ENABLED === 'true'; +// XXX: PUSH_NOTIFICATION_ENABLED already is a boolean, this became an identity function +export const isPushNotificationEnabled = (): boolean => PUSH_NOTIFICATION_ENABLED; const serviceAccount = { type: 'service_account', diff --git a/packages/wallet-service/src/ws/utils.ts b/packages/wallet-service/src/ws/utils.ts index f933a186..1c28dc33 100644 --- a/packages/wallet-service/src/ws/utils.ts +++ b/packages/wallet-service/src/ws/utils.ts @@ -11,6 +11,7 @@ import { } from '@aws-sdk/client-apigatewaymanagementapi'; import { Logger } from 'winston'; import createDefaultLogger from '@src/logger'; +import config from '@src/config'; import util from 'util'; import { Severity } from '@wallet-service/common/src/types'; @@ -24,7 +25,7 @@ export const connectionInfoFromEvent = ( ): WsConnectionInfo => { const logger: Logger = createDefaultLogger(); const connID = event.requestContext.connectionId; - if (process.env.IS_OFFLINE === 'true') { + if (config.isOffline) { // This will enter when running the service on serverless offline mode return { id: connID, @@ -32,7 +33,7 @@ export const connectionInfoFromEvent = ( }; } - const domain = process.env.WS_DOMAIN; + const domain = config.wsDomain; if (!domain) { addAlert( diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index 4290358a..260457f7 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -1,5 +1,6 @@ import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda'; +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; import { get as addressesGet, checkMine } from '@src/api/addresses'; import { get as newAddressesGet } from '@src/api/newAddresses'; import { get as balancesGet } from '@src/api/balances'; @@ -19,18 +20,20 @@ import { get as walletGet, load as walletLoad, loadWallet, + loadWalletFailed, changeAuthXpub, } from '@src/api/wallet'; import { updateVersionData, + createWallet, } from '@src/db'; import * as Wallet from '@src/api/wallet'; import * as Db from '@src/db'; import { ApiError } from '@src/api/errors'; import { closeDbConnection, getDbConnection, getUnixTimestamp, getWalletId } from '@src/utils'; import { STATUS_CODE_TABLE } from '@src/api/utils'; -import { WalletStatus, FullNodeVersionData } from '@src/types'; -import { walletUtils, constants, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; +import { WalletStatus, FullNodeApiVersionResponse } from '@src/types'; +import { walletUtils, addressUtils, constants, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; import bitcore from 'bitcore-lib'; import { ADDRESSES, @@ -52,10 +55,11 @@ import { makeGatewayEventWithAuthorizer, getAuthData, getXPrivKeyFromSeed, + makeLoadWalletFailedSNSEvent, } from '@tests/utils'; import fullnode from '@src/fullnode'; import { getHealthcheck } from '@src/api/healthcheck'; -import { ping } from "@src/redis"; +import { Severity } from '@wallet-service/common'; // Monkey patch bitcore-lib @@ -793,7 +797,8 @@ test('POST /wallet', async () => { // get the first address const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); - const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddress = firstAddressData.base58; // Wrong first address event = makeGatewayEvent({}, JSON.stringify({ @@ -926,7 +931,8 @@ test('POST /wallet should fail with ApiError.WALLET_MAX_RETRIES when max retries // get the first address const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); - const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddress = firstAddressData.base58; // we need signatures for both the account path and the purpose path: const now = Math.floor(Date.now() / 1000); @@ -1152,12 +1158,60 @@ test('changeAuthXpub should fail if signatures do not match', async () => { expect(returnBody.details[0].message).toBe('Signatures are not valid'); }); +test('PUT /wallet/auth should fail if we cannot confirm the firstAddress', async () => { + expect.hasAssertions(); + + // get the first address + const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); + + const firstAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddress = firstAddressData.base58; + // Get address at wrong derivation index + const secondAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 1, process.env.NETWORK); + const secondAddress = secondAddressData.base58; + + // we need signatures for both the account path and the purpose path: + const now = Math.floor(Date.now() / 1000); + const walletId = getWalletId(XPUBKEY); + const xpriv = getXPrivKeyFromSeed(TEST_SEED, { + passphrase: '', + networkName: process.env.NETWORK, + }); + + const wallet = await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 20); + + // account path + const accountDerivationIndex = '0\''; + + const derivedPrivKey = walletUtils.deriveXpriv(xpriv, accountDerivationIndex); + const address = derivedPrivKey.publicKey.toAddress(network.getNetwork()).toString(); + const message = new bitcore.Message(String(now).concat(walletId).concat(address)); + + const event = makeGatewayEvent({}, JSON.stringify({ + xpubkey: XPUBKEY, + xpubkeySignature: 'xpubkey-signature', + authXpubkey: AUTH_XPUBKEY, + authXpubkeySignature: 'auth-xpubkey-signature', + firstAddress: ADDRESSES[1], + timestamp: Math.floor(Date.now() / 1000), + })); + + const result = await changeAuthXpub(event, null, null) as APIGatewayProxyResult; + const returnBody = JSON.parse(result.body as string); + + expect(result.statusCode).toBe(400); + expect(returnBody.success).toBe(false); + expect(returnBody.error).toBe('invalid-payload'); + expect(returnBody.message).toBe(`Expected first address to be ${secondAddress} but it is ${firstAddress}`); +}, 30000); + test('PUT /wallet/auth should change the auth_xpub only after validating both the xpub and the auth_xpubkey', async () => { expect.hasAssertions(); // get the first address const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); - const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddress = firstAddressData.base58; // we need signatures for both the account path and the purpose path: const now = Math.floor(Date.now() / 1000); @@ -1181,7 +1235,8 @@ test('loadWallet API should fail if a wrong signature is sent', async () => { expect.hasAssertions(); const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); - const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddress = firstAddressData.base58; const now = Math.floor(Date.now() / 1000); const walletId = getWalletId(XPUBKEY); @@ -1323,6 +1378,27 @@ test('loadWallet should update wallet status to ERROR if an error occurs', async expect(wallet.status).toStrictEqual(WalletStatus.ERROR); }, 30000); +test('loadWalletFailed should create alert if xpubkey is missing', async () => { + expect.hasAssertions(); + + const event = makeLoadWalletFailedSNSEvent(1, XPUBKEY, 'a-req-01', 'an-error-01'); + event.Records[0].Sns.Message = '{}'; + mockedAddAlert.mockReset(); + + await loadWalletFailed(event, null, null); + + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Wallet failed to load, but no xpubkey received.', + `An event reached loadWalletFailed lambda but the xpubkey was not sent. This indicates that a wallet has failed to load and we weren't able to recover, please check the logs as soon as possible.`, + Severity.MAJOR, + { + RequestID: 'a-req-01', + ErrorMessage: 'an-error-01' + }, + expect.anything(), + ); +}); + test('GET /wallet/tokens', async () => { expect.hasAssertions(); @@ -1522,7 +1598,7 @@ test('GET /wallet/tokens/token_id/details', async () => { expect(returnBody.details.authorities.melt).toStrictEqual(true); expect(returnBody.details.tokenInfo).toStrictEqual(token2); - event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.HATHOR_TOKEN_CONFIG.uid }); + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.NATIVE_TOKEN_UID }); result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; returnBody = JSON.parse(result.body as string); @@ -1539,11 +1615,12 @@ test('GET /wallet/tokens/token_id/details', async () => { ] `); - const oldHathorTokenConfig = constants.HATHOR_TOKEN_CONFIG.uid; + const oldHathorTokenConfig = constants.NATIVE_TOKEN_UID; - constants.HATHOR_TOKEN_CONFIG.uid = TX_IDS[4]; + // @ts-ignore + constants.NATIVE_TOKEN_UID = TX_IDS[4]; - event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.HATHOR_TOKEN_CONFIG.uid }); + event = makeGatewayEventWithAuthorizer('my-wallet', { token_id: constants.NATIVE_TOKEN_UID }); result = await getTokenDetails(event, null, null) as APIGatewayProxyResult; returnBody = JSON.parse(result.body as string); @@ -1551,7 +1628,8 @@ test('GET /wallet/tokens/token_id/details', async () => { expect(returnBody.success).toBe(false); expect(returnBody.details).toStrictEqual([{ message: 'Invalid tokenId' }]); - constants.HATHOR_TOKEN_CONFIG.uid = oldHathorTokenConfig; + // @ts-ignore + constants.NATIVE_TOKEN_UID = oldHathorTokenConfig; }); test('GET /wallet/utxos', async () => { @@ -1587,21 +1665,26 @@ test('DELETE /tx/proposal/{txProposalId}', async () => { test('GET /version', async () => { expect.hasAssertions(); - const mockData: FullNodeVersionData = { - timestamp: 1614875031449, + const mockData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', - minWeight: 14, - minTxWeight: 14, - minTxWeightCoefficient: 1.6, - minTxWeightK: 100, - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: 300, - maxNumberInputs: 255, - maxNumberOutputs: 255, + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + genesis_tx1_hash: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + genesis_tx2_hash: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc', + native_token: { name: 'Hathor', symbol: 'HTR'}, }; - await updateVersionData(mysql, mockData); + const ts = getUnixTimestamp() + await updateVersionData(mysql, ts, mockData); const event = makeGatewayEvent({}); const result = await getVersionDataGet(event, null, null) as APIGatewayProxyResult; diff --git a/packages/wallet-service/tests/commons.test.ts b/packages/wallet-service/tests/commons.test.ts index b385b418..9e551a59 100644 --- a/packages/wallet-service/tests/commons.test.ts +++ b/packages/wallet-service/tests/commons.test.ts @@ -5,18 +5,18 @@ import { markLockedOutputs, unlockUtxos, unlockTimelockedUtxos, - maybeRefreshWalletConstants, searchForLatestValidBlock, getWalletBalancesForTx, } from '@src/commons'; import { - FullNodeVersionData, Authorities, Balance, TokenBalanceMap, DbTxOutput, Block, + FullNodeApiVersionResponse, } from '@src/types'; +import fullnode from '@src/fullnode'; import { TxInput, TxOutput, @@ -50,6 +50,7 @@ import { } from '@src/db'; import * as Utils from '@src/utils'; import hathorLib from '@hathor/wallet-lib'; +import { convertApiVersionData, getFullnodeData } from '@src/nodeConfig'; const mysql = getDbConnection(); const OLD_ENV = process.env; @@ -510,82 +511,75 @@ test('unlockTimelockedUtxos', async () => { await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 5000, 0, null, 3, 0b10, 0)).resolves.toBe(true); }); -test('maybeRefreshWalletConstants with an uninitialized version_data database should call hathorLib.version.checkApiVersion()', async () => { +test('getFullnodeData with an uninitialized version_data database should call the version api', async () => { expect.hasAssertions(); - const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); + const mockData = { + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: '1234567812345678123456781234567812345678123456781234567812345678', + genesis_tx1_hash: 'abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd', + genesis_tx2_hash: 'd00d500fd00d500fd00d500fd00d500fd00d500fd00d500fd00d500fd00d500f', + native_token: { name: 'Hathor', symbol: 'HTR'}, + }; - const mockGet = jest.fn(() => Promise.resolve({ - data: { - success: true, - version: '0.38.0', - network: 'mainnet', - min_weight: 14, - min_tx_weight: 14, - min_tx_weight_coefficient: 1.6, - min_tx_weight_k: 100, - token_deposit_percentage: 0.01, - reward_spend_min_blocks: 300, - max_number_inputs: 255, - max_number_outputs: 255, - }, + const spy = jest.spyOn(fullnode.api, 'get'); + spy.mockImplementation(() => Promise.resolve({ + status: 200, + data: mockData, })); - spy.mockReturnValue({ - post: () => Promise.resolve({ - data: { - success: true, - }, - }), - get: mockGet, - }); + await mysql.query('DELETE FROM`version_data`'); - await maybeRefreshWalletConstants(mysql); + const data = await getFullnodeData(mysql); + console.log(JSON.stringify(data)); - expect(mockGet).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith('version', expect.any(Object)); }); -test('maybeRefreshWalletConstants with an initialized version_data database should query data from the database', async () => { +test('getFullnodeData with an initialized version_data database should query data from the database', async () => { expect.hasAssertions(); const axiosSpy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); const mockGet = jest.fn(() => Promise.resolve({ data: {} })); + // @ts-ignore axiosSpy.mockReturnValue({ get: mockGet }); - const mockedVersionData: FullNodeVersionData = { - timestamp: new Date().getTime(), - version: '0.38.0', - network: 'mainnet', - minWeight: 14, - minTxWeight: 14, - minTxWeightCoefficient: 1.6, - minTxWeightK: 100, - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: 300, - maxNumberInputs: 255, - maxNumberOutputs: 255, - }; - - await updateVersionData(mysql, mockedVersionData); - - await maybeRefreshWalletConstants(mysql); + const mockedVersionData: FullNodeApiVersionResponse = { + version: '0.38.0', + network: 'mainnet', + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR'}, + }; - const { - txMinWeight, - txWeightCoefficient, - txMinWeightK, - } = hathorLib.transaction.getTransactionWeightConstants(); + await updateVersionData(mysql, new Date().getTime(), mockedVersionData); - const maxNumberInputs = hathorLib.transaction.getMaxInputsConstant(); - const maxNumberOutputs = hathorLib.transaction.getMaxOutputsConstant(); + const data = await getFullnodeData(mysql); expect(mockGet).toHaveBeenCalledTimes(0); - expect(txMinWeight).toStrictEqual(mockedVersionData.minTxWeight); - expect(txWeightCoefficient).toStrictEqual(mockedVersionData.minTxWeightCoefficient); - expect(txMinWeightK).toStrictEqual(mockedVersionData.minTxWeightK); - expect(maxNumberInputs).toStrictEqual(mockedVersionData.maxNumberInputs); - expect(maxNumberOutputs).toStrictEqual(mockedVersionData.maxNumberOutputs); + expect(data).toEqual(convertApiVersionData(mockedVersionData)); }); test('searchForLatestValidBlock should find the first voided block', async () => { diff --git a/packages/wallet-service/tests/db.test.ts b/packages/wallet-service/tests/db.test.ts index 7b32e0b7..857e85e3 100644 --- a/packages/wallet-service/tests/db.test.ts +++ b/packages/wallet-service/tests/db.test.ts @@ -98,12 +98,12 @@ import { TokenInfo, TxProposalStatus, WalletStatus, - FullNodeVersionData, Tx, DbTxOutput, PushDevice, PushProvider, Block, + FullNodeApiVersionResponse, } from '@src/types'; import { Severity } from '@wallet-service/common/src/types'; import { isAuthority } from '@wallet-service/common/src/utils/wallet.utils'; @@ -1586,33 +1586,38 @@ test('createTxProposal, updateTxProposal, getTxProposal, countUnsentTxProposals, test('updateVersionData', async () => { expect.hasAssertions(); - const mockData: FullNodeVersionData = { - timestamp: 1614875031449, + const mockData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', - minWeight: 14, - minTxWeight: 14, - minTxWeightCoefficient: 1.6, - minTxWeightK: 100, - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: 300, - maxNumberInputs: 255, - maxNumberOutputs: 255, + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: '0000000000000000000000000000000000000000000000000000000000000000', + genesis_tx1_hash: '1111111111111111111111111111111111111111111111111111111111111111', + genesis_tx2_hash: '2222222222222222222222222222222222222222222222222222222222222222', + native_token: { name: 'Hathor', symbol: 'HTR'}, }; - const mockData2: FullNodeVersionData = { + const mockData2: FullNodeApiVersionResponse = { ...mockData, version: '0.39.1', }; - const mockData3: FullNodeVersionData = { + const mockData3: FullNodeApiVersionResponse = { ...mockData, version: '0.39.2', }; - await updateVersionData(mysql, mockData); - await updateVersionData(mysql, mockData2); - await updateVersionData(mysql, mockData3); + const ts = getUnixTimestamp(); + await updateVersionData(mysql, ts, mockData); + await updateVersionData(mysql, ts, mockData2); + await updateVersionData(mysql, ts, mockData3); await expect( checkVersionDataTable(mysql, mockData3), @@ -1622,25 +1627,30 @@ test('updateVersionData', async () => { test('getVersionData', async () => { expect.hasAssertions(); - const mockData: FullNodeVersionData = { - timestamp: 1614875031449, + const mockData: FullNodeApiVersionResponse = { version: '0.38.0', network: 'mainnet', - minWeight: 14, - minTxWeight: 14, - minTxWeightCoefficient: 1.6, - minTxWeightK: 100, - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: 300, - maxNumberInputs: 255, - maxNumberOutputs: 255, + min_weight: 14, + min_tx_weight: 14, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR'}, }; - await updateVersionData(mysql, mockData); + const ts = getUnixTimestamp(); + await updateVersionData(mysql, ts, mockData); - const versionData: FullNodeVersionData = await getVersionData(mysql); + const { data } = await getVersionData(mysql); - expect(Object.entries(versionData).toString()).toStrictEqual(Object.entries(mockData).toString()); + expect(Object.entries(data).toString()).toStrictEqual(Object.entries(mockData).toString()); }); test('fetchAddressTxHistorySum', async () => { @@ -3246,7 +3256,7 @@ describe('getPushDeviceSettingsList', () => { // register wallets that will not be queried const loadWallet = (eachDevice) => createWallet(mysql, eachDevice.walletId, XPUBKEY, AUTH_XPUBKEY, 5); - await devicesToLoad.forEach(loadWallet); + await Promise.all(devicesToLoad.map(loadWallet)); // register devices related to the loaded wallets const loadDevice = (eachDevice) => registerPushDevice(mysql, { @@ -3256,7 +3266,7 @@ describe('getPushDeviceSettingsList', () => { enablePush: eachDevice.enablePush, enableShowAmounts: eachDevice.enableShowAmounts, }); - await devicesToLoad.forEach(loadDevice); + await Promise.all(devicesToLoad.map(loadDevice)); // get settings querying only devices not loaded on database, resulting on empty list const notRegisteredWalletIdList = devicesToNotLoad.map((each) => each.walletId); @@ -3308,7 +3318,7 @@ describe('getPushDeviceSettingsList', () => { // register wallets to be used by registered devices const loadWallet = (eachDevice) => createWallet(mysql, eachDevice.walletId, XPUBKEY, AUTH_XPUBKEY, 5); - await devicesToLoad.forEach(loadWallet); + await Promise.all(devicesToLoad.map(loadWallet)); // register devices related to the loaded wallets const loadDevice = (eachDevice) => registerPushDevice(mysql, { @@ -3318,7 +3328,7 @@ describe('getPushDeviceSettingsList', () => { enablePush: eachDevice.enablePush, enableShowAmounts: eachDevice.enableShowAmounts, }); - await devicesToLoad.forEach(loadDevice); + await Promise.all(devicesToLoad.map(loadDevice)); // get settings, query be all wallets of deviceCandidates, some are loaded on database, some are not const walletIdList = deviceCandidates.map((each) => each.walletId); @@ -3381,7 +3391,7 @@ describe('getPushDeviceSettingsList', () => { // register wallets, load all the wallets related to devicesToLoad const loadWallet = (eachDevice) => createWallet(mysql, eachDevice.walletId, XPUBKEY, AUTH_XPUBKEY, 5); - await devicesToLoad.forEach(loadWallet); + await Promise.all(devicesToLoad.map(loadWallet)); // register devices, register all the devices const loadDevice = (eachDevice) => registerPushDevice(mysql, { @@ -3391,7 +3401,7 @@ describe('getPushDeviceSettingsList', () => { enablePush: eachDevice.enablePush, enableShowAmounts: eachDevice.enableShowAmounts, }); - await devicesToLoad.forEach(loadDevice); + await Promise.all(devicesToLoad.map(loadDevice)); // get settings, get every device registered const walletIdList = devicesToLoad.map((each) => each.walletId); diff --git a/packages/wallet-service/tests/env.test.ts b/packages/wallet-service/tests/env.test.ts new file mode 100644 index 00000000..5b724c60 --- /dev/null +++ b/packages/wallet-service/tests/env.test.ts @@ -0,0 +1,33 @@ +import config, { loadEnvConfig } from '@src/config'; + +test('Configuration should load correctly during tests', () => { + expect.hasAssertions(); + + const oldValue = process.env.CONFIRM_FIRST_ADDRESS; + try { + process.env.CONFIRM_FIRST_ADDRESS = 'true'; + jest.resetModules(); + const loadedConfig = loadEnvConfig(); + expect(loadedConfig).toStrictEqual(config); + + expect(config.confirmFirstAddress).toEqual(true); + } finally { + process.env.CONFIRM_FIRST_ADDRESS = oldValue; + jest.resetModules(); + } +}); + +test('loadEnvConfig should get the config from the env', () => { + expect.hasAssertions(); + + const oldNetwork = process.env.NETWORK; + process.env.NETWORK = 'unknown unexisting network'; + + try { + const loadedConfig = loadEnvConfig(); + expect(loadedConfig.network).toEqual('unknown unexisting network'); + } finally { + process.env.NETWORK = oldNetwork; + } + +}); diff --git a/packages/wallet-service/tests/txProposal.test.ts b/packages/wallet-service/tests/txProposal.test.ts index c618722e..b42a0c3c 100644 --- a/packages/wallet-service/tests/txProposal.test.ts +++ b/packages/wallet-service/tests/txProposal.test.ts @@ -25,8 +25,7 @@ import { APIGatewayProxyResult } from 'aws-lambda'; import { ApiError } from '@src/api/errors'; -import hathorLib from '@hathor/wallet-lib'; -import CreateTokenTransaction from '@hathor/wallet-lib/lib/models/create_token_transaction'; +import hathorLib, { CreateTokenTransaction } from '@hathor/wallet-lib'; const defaultDerivationPath = `m/44'/${hathorLib.constants.HATHOR_BIP44_CODE}'/0'/0/`; @@ -37,20 +36,24 @@ beforeEach(async () => { const now = getUnixTimestamp(); const versionData = { - timestamp: now, version: '0.38.4', network: process.env.NETWORK, - minWeight: 8, - minTxWeight: 8, - minTxWeightCoefficient: 0, - minTxWeightK: 0, - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: 300, - maxNumberInputs: 255, - maxNumberOutputs: 255, + min_weight: 8, + min_tx_weight: 8, + min_tx_weight_coefficient: 1.6, + min_tx_weight_k: 100, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 255, + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR'}, }; - await addToVersionDataTable(mysql, versionData); + await addToVersionDataTable(mysql, now, versionData); }); afterAll(async () => { @@ -203,19 +206,24 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY const now = getUnixTimestamp(); - await updateVersionData(mysql, { - timestamp: now, + await updateVersionData(mysql, now, { version: '0.38.4', network: process.env.NETWORK, - minWeight: 8, - minTxWeight: 8, - minTxWeightCoefficient: 0, - minTxWeightK: 0, - tokenDepositPercentage: 0.01, - rewardSpendMinBlocks: 300, - maxNumberInputs: 255, - maxNumberOutputs: 2, // mocking to force a failure + min_weight: 8, + min_tx_weight: 8, + min_tx_weight_coefficient: 1.8, + min_tx_weight_k: 90, + token_deposit_percentage: 0.01, + reward_spend_min_blocks: 300, + max_number_inputs: 255, + max_number_outputs: 2, // mocking to force a failure + decimal_places: 2, + genesis_block_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx1_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', + native_token: { name: 'Hathor', symbol: 'HTR'}, }); + jest.resetModules(); await addToWalletTable(mysql, [{ id: 'my-wallet', @@ -236,7 +244,7 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY const token1 = '004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50'; const token2 = '002f2bcc3261b4fb8510a458ed9df9f6ba2a413ee35901b3c5f81b0c085287e2'; - const utxos = [ + const utxos: [string, number, string, string, number, number, null, null, boolean][] = [ ['004d75c1edd4294379e7e5b7ab6c118c53c8b07a506728feb5688c8d26a97e50', 0, token1, ADDRESSES[0], 300, 0, null, null, false], ['0000001e39bc37fe8710c01cc1e8c0a937bf6f9337551fbbfddc222bfc28c197', 0, token1, ADDRESSES[0], 100, 0, null, null, false], ['00000060a25077e48926bcd9473d77259296e123ec6af1c1a16c1c381093ab90', 0, token2, ADDRESSES[0], 300, 0, null, null, false], @@ -748,12 +756,14 @@ test('PUT /txproposals/{proposalId} with an invalid txHex should fail and update // Create the spy to mock wallet-lib const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); spy.mockReturnValue({ + // @ts-ignore post: () => Promise.resolve({ data: { success: false, message: 'invalid txhex', }, }), + // @ts-ignore get: () => Promise.resolve({ data: { success: true, @@ -897,6 +907,7 @@ test('PUT /txproposals/{proposalId} should update tx_proposal to SEND_ERROR on f post: () => { throw new Error('Wallet lib error'); }, + // @ts-ignore get: () => Promise.resolve({ data: { success: true, @@ -1510,9 +1521,7 @@ test('POST /txproposals a tx create action on txHex', async () => { const name = 'Test token'; const symbol = 'TSTKN'; - const transaction = new CreateTokenTransaction(name, symbol, inputs, outputs, { - version: hathorLib.constants.CREATE_TOKEN_TX_VERSION, - }); + const transaction = new CreateTokenTransaction(name, symbol, inputs, outputs); const txHex = transaction.toHex(); const event = makeGatewayEventWithAuthorizer('my-wallet', null, JSON.stringify({ txHex })); @@ -1532,9 +1541,11 @@ test('PUT /txproposals/{proposalId} with txhex', async () => { // Create the spy to mock wallet-lib const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); spy.mockReturnValue({ + // @ts-ignore post: () => Promise.resolve({ data: { success: true }, }), + // @ts-ignore get: () => Promise.resolve({ data: { success: true, @@ -1672,9 +1683,11 @@ test('PUT /txproposals/{proposalId} with a different txhex than the one sent in // Create the spy to mock wallet-lib const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); spy.mockReturnValue({ + // @ts-ignore post: () => Promise.resolve({ data: { success: true }, }), + // @ts-ignore get: () => Promise.resolve({ data: { success: true, diff --git a/packages/wallet-service/tests/utils.test.ts b/packages/wallet-service/tests/utils.test.ts index c3568748..ce8a3516 100644 --- a/packages/wallet-service/tests/utils.test.ts +++ b/packages/wallet-service/tests/utils.test.ts @@ -1,31 +1,8 @@ -import { CustomStorage, arrayShuffle, sha256d, isTxVoided } from '@src/utils'; +import { arrayShuffle, sha256d, isTxVoided } from '@src/utils'; import hathorLib from '@hathor/wallet-lib'; import * as Fullnode from '@src/fullnode'; import { TEST_SEED, XPUBKEY, AUTH_XPUBKEY, ADDRESSES } from '@tests/utils'; -test('CustomStorage', () => { - expect.hasAssertions(); - - const store = new CustomStorage(); - // Should be initialized with hathor default server and server - expect(store.getItem('wallet:defaultServer')).toBe(hathorLib.constants.DEFAULT_SERVER); - expect(store.getItem('wallet:server')).toBe(hathorLib.constants.DEFAULT_SERVER); - - store.setItem('hathor', 'hathor'); - expect(store.getItem('hathor')).toBe('hathor'); - store.removeItem('hathor'); - - expect(store.getItem('hathor')).toBeUndefined(); - - store.setItem('hathor', 'hathor2'); - store.clear(); - expect(store.getItem('hathor')).toBeUndefined(); - - store.preStart(); - expect(store.getItem('wallet:defaultServer')).toBe(hathorLib.constants.DEFAULT_SERVER); - expect(store.getItem('wallet:server')).toBe(hathorLib.constants.DEFAULT_SERVER); -}); - test('sha256d', () => { expect.hasAssertions(); // sha256d(my-test-data) -> 4f1ba9a4204e97a293b16ead6caced38f6d91d95618b96e261c6332ed24f7894 @@ -67,6 +44,7 @@ test('isTxVoided', async () => { }; }); + // @ts-ignore spy.mockImplementation(mockImplementation); expect(await isTxVoided('0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7')).toStrictEqual([ @@ -89,6 +67,10 @@ test('XPUBKEY, AUTH_XPUBKEY and ADDRESSES should be derived from TEST_SEED', asy // Generate addresses in change derivation path 0 const derivedXpub = hathorLib.walletUtils.xpubDeriveChild(xpubkey, 0); - const addresses = Object.keys(hathorLib.walletUtils.getAddresses(derivedXpub, 0, 17)); + const addresses: string[] = []; + for (let index = 0; index < 17; index++) { + const addressInfo = hathorLib.addressUtils.deriveAddressFromXPubP2PKH(derivedXpub, index, 'mainnet'); + addresses.push(addressInfo.base58); + } expect(addresses).toStrictEqual(ADDRESSES); }); diff --git a/packages/wallet-service/tests/utils.ts b/packages/wallet-service/tests/utils.ts index b1cc0b47..8342ef85 100644 --- a/packages/wallet-service/tests/utils.ts +++ b/packages/wallet-service/tests/utils.ts @@ -1,18 +1,18 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; +import { APIGatewayProxyEvent, SNSEvent, SNSEventRecord } from 'aws-lambda'; import { ServerlessMysql } from 'serverless-mysql'; import { isEqual } from 'lodash'; import { DbSelectResult, TxOutputWithIndex, - FullNodeVersionData, WalletBalanceValue, StringMap, PushProvider, DbTxOutput, + FullNodeApiVersionResponse, } from '@src/types'; import { TxInput } from '@wallet-service/common/src/types'; import { getWalletId } from '@src/utils'; -import { walletUtils, Network, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; +import { addressUtils, walletUtils, Network, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; import { AddressTxHistoryTableEntry, AddressTableEntry, @@ -819,35 +819,50 @@ export const makeGatewayEventWithAuthorizer = ( resource: null, }); -export const addToVersionDataTable = async (mysql: ServerlessMysql, versionData: FullNodeVersionData): Promise => { - const payload = [[ - 1, - versionData.timestamp, - versionData.version, - versionData.network, - versionData.minWeight, - versionData.minTxWeight, - versionData.minTxWeightCoefficient, - versionData.minTxWeightK, - versionData.tokenDepositPercentage, - versionData.rewardSpendMinBlocks, - versionData.maxNumberInputs, - versionData.maxNumberOutputs, - ]]; +export function makeLoadWalletFailedSNSEvent(count: number, xpubkey: string, requestId?: string, errorMessage?: string): SNSEvent { + const event: SNSEventRecord = { + EventVersion: '', + EventSubscriptionArn: '', + EventSource: '', + Sns: { + SignatureVersion: '', + Timestamp: '', + Signature: '', + SigningCertUrl: '', + MessageId: '', + Message: JSON.stringify({ + source: '', + xpubkey, + maxGap: 20, + }), + MessageAttributes: { + RequestID: { Type: 'string', Value: requestId || 'request-id' }, + ErrorMessage: { Type: 'string', Value: errorMessage || 'error-message' }, + }, + Type: '', + UnsubscribeUrl: '', + TopicArn: '', + Subject: '', + Token: '', + }, + }; + + return { + Records: Array(count).fill(event), + }; +} + +export const addToVersionDataTable = async (mysql: ServerlessMysql, timestamp: number, versionData: FullNodeApiVersionResponse): Promise => { + const payload = [[ 1, timestamp, JSON.stringify(versionData) ]]; await mysql.query( - `INSERT INTO \`version_data\`(\`id\`, \`timestamp\`, - \`version\`, \`network\`, - \`min_weight\`, \`min_tx_weight\`, - \`min_tx_weight_coefficient\`, \`min_tx_weight_k\`, - \`token_deposit_percentage\`, \`reward_spend_min_blocks\`, - \`max_number_inputs\`, \`max_number_outputs\`) + `INSERT INTO \`version_data\`(\`id\`, \`timestamp\`, \`data\`) VALUES ?`, [payload], ); }; -export const checkVersionDataTable = async (mysql: ServerlessMysql, versionData: FullNodeVersionData): Promise> => { +export const checkVersionDataTable = async (mysql: ServerlessMysql, versionData: FullNodeApiVersionResponse): Promise> => { // first check the total number of rows in the table let results: DbSelectResult = await mysql.query('SELECT * FROM `version_data`'); @@ -875,19 +890,7 @@ export const checkVersionDataTable = async (mysql: ServerlessMysql, versionData: }; } - const dbVersionData: FullNodeVersionData = { - timestamp: results[0].timestamp as number, - version: results[0].version as string, - network: results[0].network as string, - minWeight: results[0].min_weight as number, - minTxWeight: results[0].min_tx_weight as number, - minTxWeightCoefficient: results[0].min_tx_weight_coefficient as number, - minTxWeightK: results[0].min_tx_weight_k as number, - tokenDepositPercentage: results[0].token_deposit_percentage as number, - rewardSpendMinBlocks: results[0].reward_spend_min_blocks as number, - maxNumberInputs: results[0].max_number_inputs as number, - maxNumberOutputs: results[0].max_number_outputs as number, - }; + const dbVersionData: FullNodeApiVersionResponse = JSON.parse(results[0].data as string); if (Object.entries(dbVersionData).toString() !== Object.entries(versionData).toString()) { return { @@ -920,7 +923,8 @@ export const redisCleanup = ( export const getAuthData = (now: number): any => { // get the first address const xpubChangeDerivation = walletUtils.xpubDeriveChild(XPUBKEY, 0); - const firstAddress = walletUtils.getAddressAtIndex(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddressData = addressUtils.deriveAddressFromXPubP2PKH(xpubChangeDerivation, 0, process.env.NETWORK); + const firstAddress = firstAddressData.base58; // we need signatures for both the account path and the purpose path: const walletId = getWalletId(XPUBKEY); diff --git a/packages/wallet-service/tests/utils/pushnotification.utils.test.ts b/packages/wallet-service/tests/utils/pushnotification.utils.test.ts index cdcfb8be..3a15d80e 100644 --- a/packages/wallet-service/tests/utils/pushnotification.utils.test.ts +++ b/packages/wallet-service/tests/utils/pushnotification.utils.test.ts @@ -4,7 +4,7 @@ import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; import { sendMulticastMock, messaging, initFirebaseAdminMock } from '@tests/utils/firebase-admin.mock'; import { logger } from '@tests/winston.mock'; -import { PushNotificationUtils, PushNotificationError, buildFunctionName, FunctionName } from '@src/utils/pushnotification.utils'; +import { PushNotificationUtils, PushNotificationError, FunctionName } from '@src/utils/pushnotification.utils'; import * as pushnotificationUtils from '@src/utils/pushnotification.utils'; import { SendNotificationToDevice } from '@src/types'; import { Severity } from '@wallet-service/common/src/types'; @@ -43,6 +43,7 @@ describe('PushNotificationUtils', () => { afterEach(() => { process.env = initEnv; + jest.resetModules(); }); // test firebase initialization error @@ -57,6 +58,7 @@ describe('PushNotificationUtils', () => { }); // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); const resultMessageOfLastCallToLoggerError = logger.error.mock.calls[0][0]; @@ -71,6 +73,7 @@ describe('PushNotificationUtils', () => { process.env.WALLET_SERVICE_LAMBDA_ENDPOINT = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -89,6 +92,7 @@ describe('PushNotificationUtils', () => { process.env.STAGE = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -107,6 +111,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_PROJECT_ID = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -125,6 +130,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_PRIVATE_KEY_ID = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -143,6 +149,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_PRIVATE_KEY = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -162,6 +169,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_CLIENT_EMAIL = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -181,6 +189,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_CLIENT_ID = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -200,6 +209,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_AUTH_URI = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -219,6 +229,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_TOKEN_URI = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -238,6 +249,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -257,6 +269,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_CLIENT_X509_CERT_URL = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(mockedAddAlert).toHaveBeenLastCalledWith( @@ -276,6 +289,7 @@ describe('PushNotificationUtils', () => { process.env.FIREBASE_PRIVATE_KEY = true as unknown as string; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(logger.error).toHaveBeenLastCalledWith('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); @@ -288,6 +302,7 @@ describe('PushNotificationUtils', () => { process.env.PUSH_ALLOWED_PROVIDERS = ''; // reload module + jest.resetModules(); await import('@src/utils/pushnotification.utils'); expect(logger.error).toHaveBeenLastCalledWith('[ALERT] env.PUSH_ALLOWED_PROVIDERS is empty.'); @@ -427,6 +442,7 @@ describe('PushNotificationUtils', () => { process.env.STAGE = fakeStage; // reload module + jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); const notification = { @@ -469,6 +485,7 @@ describe('PushNotificationUtils', () => { process.env.STAGE = fakeStage; // reload module + jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); const notification = { @@ -499,6 +516,7 @@ describe('PushNotificationUtils', () => { process.env.STAGE = fakeStage; // reload module + jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); const notification = { @@ -526,7 +544,8 @@ describe('PushNotificationUtils', () => { sendMock.mockReturnValueOnce({ StatusCode: 202, }); - const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); + jest.resetModules(); + const { PushNotificationUtils, buildFunctionName } = await import('@src/utils/pushnotification.utils'); const walletMap = buildWalletBalanceValueMap(); const result = await PushNotificationUtils.invokeOnTxPushNotificationRequestedLambda(walletMap); @@ -559,6 +578,7 @@ describe('PushNotificationUtils', () => { jest.clearAllMocks(); // reload module process.env.PUSH_NOTIFICATION_ENABLED = 'false'; + jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); const walletMap = buildWalletBalanceValueMap(); @@ -588,6 +608,7 @@ describe('PushNotificationUtils', () => { // reload module process.env.PUSH_NOTIFICATION_ENABLED = 'true'; + jest.resetModules(); const { PushNotificationUtils } = await import('@src/utils/pushnotification.utils'); const walletMap = buildWalletBalanceValueMap(); diff --git a/packages/wallet-service/tests/ws.utils.test.ts b/packages/wallet-service/tests/ws.utils.test.ts index 6972cde8..7c6e84bb 100644 --- a/packages/wallet-service/tests/ws.utils.test.ts +++ b/packages/wallet-service/tests/ws.utils.test.ts @@ -1,6 +1,6 @@ import { Logger } from 'winston'; import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; -import { connectionInfoFromEvent, sendMessageToClient } from '@src/ws/utils'; +import { sendMessageToClient } from '@src/ws/utils'; import { Severity } from '@wallet-service/common/src/types'; import { logger } from '@tests/winston.mock'; @@ -45,6 +45,9 @@ import { endWsConnection } from '@src/redis'; test('connectionInfoFromEvent', async () => { expect.hasAssertions(); + const oldValue = process.env.IS_OFFLINE; + process.env.IS_OFFLINE = 'false'; + jest.resetModules(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const event = { @@ -54,16 +57,27 @@ test('connectionInfoFromEvent', async () => { stage: 'test123', }, }; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const connInfo = connectionInfoFromEvent(event); - expect(connInfo).toStrictEqual({ id: 'abc123', url: `https://${process.env.WS_DOMAIN}` }); + try { + + const { connectionInfoFromEvent } = await import('@src/ws/utils'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const connInfo = connectionInfoFromEvent(event); + expect(connInfo).toStrictEqual({ id: 'abc123', url: `https://${process.env.WS_DOMAIN}` }); + } finally { + process.env.IS_OFFLINE = oldValue; + jest.resetModules(); + } }); -test('missing WS_DOMAIN should throw', () => { +test('missing WS_DOMAIN should throw', async () => { expect.hasAssertions(); + const oldOffline = process.env.IS_OFFLINE; + process.env.IS_OFFLINE = 'false'; + const oldDomain = process.env.WS_DOMAIN; delete process.env.WS_DOMAIN; + jest.resetModules(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const event = { @@ -74,16 +88,23 @@ test('missing WS_DOMAIN should throw', () => { }, }; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => connectionInfoFromEvent(event)).toThrow('Domain not on env variables'); - expect(mockedAddAlert).toHaveBeenCalledWith( - 'Erroed while fetching connection info', - 'Domain not on env variables', - Severity.MINOR, - null, - expect.any(Logger), - ); + try { + const { connectionInfoFromEvent } = await import('@src/ws/utils'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(() => connectionInfoFromEvent(event)).toThrow('Domain not on env variables'); + expect(mockedAddAlert).toHaveBeenCalledWith( + 'Erroed while fetching connection info', + 'Domain not on env variables', + Severity.MINOR, + null, + expect.anything(), + ); + } finally { + process.env.WS_DOMAIN = oldDomain; + process.env.IS_OFFLINE = oldOffline; + jest.resetModules(); + } }); describe('sendMessageToClient', () => { diff --git a/yarn.lock b/yarn.lock index 5dd283ab..ca032f7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1984,7 +1984,7 @@ __metadata: languageName: node linkType: hard -"@hapi/hoek@npm:^9.0.0": +"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" checksum: 10/ad83a223787749f3873bce42bd32a9a19673765bf3edece0a427e138859ff729469e68d5fdf9ff6bbee6fb0c8e21bab61415afa4584f527cfc40b59ea1957e70 @@ -2105,7 +2105,7 @@ __metadata: languageName: node linkType: hard -"@hapi/topo@npm:^5.0.0": +"@hapi/topo@npm:^5.0.0, @hapi/topo@npm:^5.1.0": version: 5.1.0 resolution: "@hapi/topo@npm:5.1.0" dependencies: @@ -2160,19 +2160,23 @@ __metadata: languageName: node linkType: hard -"@hathor/wallet-lib@npm:0.39.0": - version: 0.39.0 - resolution: "@hathor/wallet-lib@npm:0.39.0" +"@hathor/wallet-lib@npm:1.14.1": + version: 1.14.1 + resolution: "@hathor/wallet-lib@npm:1.14.1" dependencies: - axios: "npm:^0.18.0" - bitcore-lib: "npm:^8.25.10" - bitcore-mnemonic: "npm:^8.25.10" - crypto-js: "npm:^3.1.9-1" - isomorphic-ws: "npm:^4.0.1" - lodash: "npm:^4.17.11" - long: "npm:^4.0.0" - ws: "npm:^7.2.1" - checksum: 10/1a49bb3f335b4d9f2005df4459f11687a2ccf4595afa45282b43f9127e2b4360ee37d2df6fe551007d83dc7bcbc518f0228f36247a1b147d702f9bd1cae66705 + abstract-level: "npm:1.0.4" + axios: "npm:1.7.2" + bitcore-lib: "npm:8.25.10" + bitcore-mnemonic: "npm:8.25.10" + buffer: "npm:6.0.3" + crypto-js: "npm:4.2.0" + isomorphic-ws: "npm:5.0.0" + level: "npm:8.0.1" + lodash: "npm:4.17.21" + long: "npm:5.2.3" + queue-microtask: "npm:1.2.3" + ws: "npm:8.17.1" + checksum: 10/e95fafa78a0a4a24cb4b6ee49c8abfb4754d347c159078e78486f57720499a58619339652c041313025d10b440b6069aee8676857890de7c0ef69c9c3870296a languageName: node linkType: hard @@ -2867,7 +2871,7 @@ __metadata: languageName: node linkType: hard -"@sideway/address@npm:^4.1.0": +"@sideway/address@npm:^4.1.0, @sideway/address@npm:^4.1.5": version: 4.1.5 resolution: "@sideway/address@npm:4.1.5" dependencies: @@ -2876,7 +2880,7 @@ __metadata: languageName: node linkType: hard -"@sideway/formula@npm:^3.0.0": +"@sideway/formula@npm:^3.0.0, @sideway/formula@npm:^3.0.1": version: 3.0.1 resolution: "@sideway/formula@npm:3.0.1" checksum: 10/8d3ee7f80df4e5204b2cbe92a2a711ca89684965a5c9eb3b316b7051212d3522e332a65a0bb2a07cc708fcd1d0b27fcb30f43ff0bcd5089d7006c7160a89eefe @@ -4190,7 +4194,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.13, @types/jest@npm:^29.5.13": +"@types/jest@npm:29.5.13": version: 29.5.13 resolution: "@types/jest@npm:29.5.13" dependencies: @@ -4210,6 +4214,15 @@ __metadata: languageName: node linkType: hard +"@types/joi@npm:17.2.3": + version: 17.2.3 + resolution: "@types/joi@npm:17.2.3" + dependencies: + joi: "npm:*" + checksum: 10/f26d0132b4e16d667176a0dce52a013fce1557a9d7a7b68c67e1bdb1ca470a221e638f2ad4566ca8fcfad544ca0845f6e241b3e86cb387ffee07910bad1147bb + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.13 resolution: "@types/json-schema@npm:7.0.13" @@ -5000,7 +5013,7 @@ __metadata: typescript: "npm:5.4.3" winston: "npm:3.13.0" peerDependencies: - "@hathor/wallet-lib": 0.39.0 + "@hathor/wallet-lib": 1.14.1 languageName: unknown linkType: soft @@ -5185,6 +5198,21 @@ __metadata: languageName: node linkType: hard +"abstract-level@npm:1.0.4, abstract-level@npm:^1.0.2, abstract-level@npm:^1.0.4": + version: 1.0.4 + resolution: "abstract-level@npm:1.0.4" + dependencies: + buffer: "npm:^6.0.3" + catering: "npm:^2.1.0" + is-buffer: "npm:^2.0.5" + level-supports: "npm:^4.0.0" + level-transcoder: "npm:^1.0.1" + module-error: "npm:^1.0.1" + queue-microtask: "npm:^1.2.3" + checksum: 10/8edf4cf55b7b66b653296f53a643bcf1501074be099d8c44351595cd33f769b7b2aed216d5fffe1c99ebea4acf14f5ae093e98baa60ea1d236ea8a3387350ebb + languageName: node + linkType: hard + "acorn-import-assertions@npm:^1.9.0": version: 1.9.0 resolution: "acorn-import-assertions@npm:1.9.0" @@ -5753,13 +5781,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^0.18.0": - version: 0.18.1 - resolution: "axios@npm:0.18.1" +"axios@npm:1.7.2": + version: 1.7.2 + resolution: "axios@npm:1.7.2" dependencies: - follow-redirects: "npm:1.5.10" - is-buffer: "npm:^2.0.2" - checksum: 10/e89e662c4998a2617bd7c34b9444a5d0b2029e3df952e5aa8756c5882e3f1d5a7143d10b5d6435d9276a71a199a3494672d7f1cc72cc8e598224feaf99793ef7 + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/6ae80dda9736bb4762ce717f1a26ff997d94672d3a5799ad9941c24d4fb019c1dff45be8272f08d1975d7950bac281f3ba24aff5ecd49ef5a04d872ec428782f languageName: node linkType: hard @@ -6041,7 +6070,7 @@ __metadata: languageName: node linkType: hard -"bitcore-lib@npm:^8.25.10, bitcore-lib@npm:^8.25.47": +"bitcore-lib@npm:^8.25.10": version: 8.25.47 resolution: "bitcore-lib@npm:8.25.47" dependencies: @@ -6069,18 +6098,6 @@ __metadata: languageName: node linkType: hard -"bitcore-mnemonic@npm:^8.25.10": - version: 8.25.47 - resolution: "bitcore-mnemonic@npm:8.25.47" - dependencies: - bitcore-lib: "npm:^8.25.47" - unorm: "npm:^1.4.1" - peerDependencies: - bitcore-lib: ^8.20.1 - checksum: 10/fca2d1471b5b4ff504111331d2a350aa2c2439a06ae56242deb9c6fdf4dcab7f1b6017b855c1c48bd889d5e48aae8fabb0dea1c6401880abbaa3d23bb0ced2d7 - languageName: node - linkType: hard - "bl@npm:^1.0.0": version: 1.2.3 resolution: "bl@npm:1.2.3" @@ -6181,6 +6198,18 @@ __metadata: languageName: node linkType: hard +"browser-level@npm:^1.0.1": + version: 1.0.1 + resolution: "browser-level@npm:1.0.1" + dependencies: + abstract-level: "npm:^1.0.2" + catering: "npm:^2.1.1" + module-error: "npm:^1.0.2" + run-parallel-limit: "npm:^1.1.0" + checksum: 10/e712569111782da76853fecf648b43ff878ff2301c2830a9e7399685b646824a85f304dea5f023e02ee41a63a972f9aad734bd411069095adc9c79784fc649a5 + languageName: node + linkType: hard + "browserify-aes@npm:^1.0.6": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" @@ -6343,6 +6372,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:6.0.3, buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10/b6bc68237ebf29bdacae48ce60e5e28fc53ae886301f2ad9496618efac49427ed79096750033e7eab1897a4f26ae374ace49106a5758f38fb70c78c9fda2c3b1 + languageName: node + linkType: hard + "buffer@npm:^5.2.1, buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" @@ -6510,6 +6549,13 @@ __metadata: languageName: node linkType: hard +"catering@npm:^2.1.0, catering@npm:^2.1.1": + version: 2.1.1 + resolution: "catering@npm:2.1.1" + checksum: 10/4669c9fa5f3a73273535fb458a964d8aba12dc5102d8487049cf03623bef3cdff4b5d9f92ff04c00f1001057a7cc7df6e700752ac622c2a7baf7bcff34166683 + languageName: node + linkType: hard + "chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -6642,6 +6688,20 @@ __metadata: languageName: node linkType: hard +"classic-level@npm:^1.2.0": + version: 1.4.1 + resolution: "classic-level@npm:1.4.1" + dependencies: + abstract-level: "npm:^1.0.2" + catering: "npm:^2.1.0" + module-error: "npm:^1.0.1" + napi-macros: "npm:^2.2.2" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/11f9362301477cb5cf3b147e5846754e0e4296231e265145101403f4a5cb797a685b6a9b6b4c880a42b05772f846a222a5a7a563262ca15b5ca03e25e9a805db + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -7075,10 +7135,10 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^3.1.9-1": - version: 3.3.0 - resolution: "crypto-js@npm:3.3.0" - checksum: 10/d7e11f3a387fb143be834e1a25ecf57ead6f5765e90fbf3aed9cead680cc38b1d241718768b7bfec448a843f569374ea5b5870ac7a8165e4bfa1915f0b00c89c +"crypto-js@npm:4.2.0": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: 10/c7bcc56a6e01c3c397e95aa4a74e4241321f04677f9a618a8f48a63b5781617248afb9adb0629824792e7ec20ca0d4241a49b6b2938ae6f973ec4efc5c53c924 languageName: node linkType: hard @@ -7144,15 +7204,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:=3.1.0": - version: 3.1.0 - resolution: "debug@npm:3.1.0" - dependencies: - ms: "npm:2.0.0" - checksum: 10/f5fd4b1390dd3b03a78aa30133a4b4db62acc3e6cd86af49f114bf7f7bd57c41a5c5c2eced2ad2c8190d70c60309f2dd5782feeaa0704dbaa5697890e3c5ad07 - languageName: node - linkType: hard - "debug@npm:^2.2.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -8932,16 +8983,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:1.5.10": - version: 1.5.10 - resolution: "follow-redirects@npm:1.5.10" - dependencies: - debug: "npm:=3.1.0" - checksum: 10/7cf3bb10dbffabce317d1de3e3f85ea8c47fea388c4bc3da1052fd6bbb7b036772f48eed2f154d63ac39c49ab5fe4df0dda94b8e0c54eda8ca2edb001b2550b7 - languageName: node - linkType: hard - -"follow-redirects@npm:^1.10.0": +"follow-redirects@npm:^1.10.0, follow-redirects@npm:^1.15.6": version: 1.15.9 resolution: "follow-redirects@npm:1.15.9" peerDependenciesMeta: @@ -9684,7 +9726,7 @@ __metadata: "@aws-sdk/client-apigatewaymanagementapi": "npm:3.540.0" "@aws-sdk/client-lambda": "npm:3.540.0" "@aws-sdk/client-sqs": "npm:3.540.0" - "@hathor/wallet-lib": "npm:0.39.0" + "@hathor/wallet-lib": "npm:1.14.1" "@types/jest": "npm:29.5.13" "@typescript-eslint/eslint-plugin": "npm:^7.4.0" "@typescript-eslint/parser": "npm:^7.4.0" @@ -10086,7 +10128,7 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^2.0.2": +"is-buffer@npm:^2.0.5": version: 2.0.5 resolution: "is-buffer@npm:2.0.5" checksum: 10/3261a8b858edcc6c9566ba1694bf829e126faa88911d1c0a747ea658c5d81b14b6955e3a702d59dabadd58fdd440c01f321aa71d6547105fd21d03f94d0597e7 @@ -10465,6 +10507,15 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: 10/e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + "isomorphic-ws@npm:^4.0.1": version: 4.0.1 resolution: "isomorphic-ws@npm:4.0.1" @@ -11037,6 +11088,19 @@ __metadata: languageName: node linkType: hard +"joi@npm:*": + version: 17.13.3 + resolution: "joi@npm:17.13.3" + dependencies: + "@hapi/hoek": "npm:^9.3.0" + "@hapi/topo": "npm:^5.1.0" + "@sideway/address": "npm:^4.1.5" + "@sideway/formula": "npm:^3.0.1" + "@sideway/pinpoint": "npm:^2.0.0" + checksum: 10/4c150db0c820c3a52f4a55c82c1fc5e144a5b5f4da9ffebc7339a15469d1a447ebb427ced446efcb9709ab56bd71a06c4c67c9381bc1b9f9ae63fc7c89209bdf + languageName: node + linkType: hard + "joi@npm:17.4.0": version: 17.4.0 resolution: "joi@npm:17.4.0" @@ -11427,6 +11491,34 @@ __metadata: languageName: node linkType: hard +"level-supports@npm:^4.0.0": + version: 4.0.1 + resolution: "level-supports@npm:4.0.1" + checksum: 10/e2f177af813a25af29d15406a14240e2e10e5efb1c35b03643c885ac5931af760b9337826506b6395f98cf6b1e68ba294bfc345a248a1ae3f9c69e08e81824b2 + languageName: node + linkType: hard + +"level-transcoder@npm:^1.0.1": + version: 1.0.1 + resolution: "level-transcoder@npm:1.0.1" + dependencies: + buffer: "npm:^6.0.3" + module-error: "npm:^1.0.1" + checksum: 10/2fb41a1d8037fc279f851ead8cdc3852b738f1f935ac2895183cd606aae3e57008e085c7c2bd2b2d43cfd057333108cfaed604092e173ac2abdf5ab1b8333f9e + languageName: node + linkType: hard + +"level@npm:8.0.1": + version: 8.0.1 + resolution: "level@npm:8.0.1" + dependencies: + abstract-level: "npm:^1.0.4" + browser-level: "npm:^1.0.1" + classic-level: "npm:^1.2.0" + checksum: 10/a9c6d1fc50e30b2cc80b3c975b34de0eb12daab7fb4f8a546a28303705a45685340a904544fcd32e9a380fae7c62474ebd9cdb0108021ddbc7b88dd9c913f126 + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -11718,6 +11810,13 @@ __metadata: languageName: node linkType: hard +"long@npm:5.2.3, long@npm:^5.0.0, long@npm:^5.2.1": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 10/9167ec6947a825b827c30da169a7384eec6c0c9ec2f0b9c74da2e93d81159bbe39fb09c3f13dae9721d4b807ccfa09797a7dd1012f5d478e3e33ca3c78b608e6 + languageName: node + linkType: hard + "long@npm:^4.0.0": version: 4.0.0 resolution: "long@npm:4.0.0" @@ -11725,13 +11824,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^5.0.0, long@npm:^5.2.1": - version: 5.2.3 - resolution: "long@npm:5.2.3" - checksum: 10/9167ec6947a825b827c30da169a7384eec6c0c9ec2f0b9c74da2e93d81159bbe39fb09c3f13dae9721d4b807ccfa09797a7dd1012f5d478e3e33ca3c78b608e6 - languageName: node - linkType: hard - "lowercase-keys@npm:^2.0.0": version: 2.0.0 resolution: "lowercase-keys@npm:2.0.0" @@ -12193,6 +12285,13 @@ __metadata: languageName: node linkType: hard +"module-error@npm:^1.0.1, module-error@npm:^1.0.2": + version: 1.0.2 + resolution: "module-error@npm:1.0.2" + checksum: 10/5d653e35bd55b3e95f8aee2cdac108082ea892e71b8f651be92cde43e4ee86abee4fa8bd7fc3fe5e68b63926d42f63c54cd17b87a560c31f18739295575a3962 + languageName: node + linkType: hard + "moment-timezone@npm:^0.5.43": version: 0.5.43 resolution: "moment-timezone@npm:0.5.43" @@ -12322,6 +12421,13 @@ __metadata: languageName: node linkType: hard +"napi-macros@npm:^2.2.2": + version: 2.2.2 + resolution: "napi-macros@npm:2.2.2" + checksum: 10/2cdb9c40ad4b424b14fbe5e13c5329559e2b511665acf41cdcda172fd2270202dc747a2d288b687c72bc70f654c797bc24a93adb67631128d62461588d7cc070 + languageName: node + linkType: hard + "native-promise-only@npm:^0.8.1": version: 0.8.1 resolution: "native-promise-only@npm:0.8.1" @@ -13431,7 +13537,7 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.2.2": +"queue-microtask@npm:1.2.3, queue-microtask@npm:^1.2.2, queue-microtask@npm:^1.2.3": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" checksum: 10/72900df0616e473e824202113c3df6abae59150dfb73ed13273503127235320e9c8ca4aaaaccfd58cf417c6ca92a6e68ee9a5c3182886ae949a768639b388a7b @@ -15120,7 +15226,7 @@ __metadata: ws: "npm:8.13.0" xstate: "npm:4.38.2" peerDependencies: - "@hathor/wallet-lib": 0.39.0 + "@hathor/wallet-lib": 1.14.1 "@wallet-service/common": 1.5.0 languageName: unknown linkType: soft @@ -16142,7 +16248,8 @@ __metadata: "@middy/core": "npm:2.5.7" "@middy/http-cors": "npm:2.5.7" "@types/aws-lambda": "npm:8.10.95" - "@types/jest": "npm:^29.5.13" + "@types/jest": "npm:29.5.13" + "@types/joi": "npm:17.2.3" "@types/node": "npm:18.0.4" "@types/redis": "npm:2.8.28" "@typescript-eslint/eslint-plugin": "npm:6.7.4" @@ -16195,7 +16302,7 @@ __metadata: webpack-node-externals: "npm:3.0.0" winston: "npm:3.13.0" peerDependencies: - "@hathor/wallet-lib": 0.39.0 + "@hathor/wallet-lib": 1.14.1 "@wallet-service/common": 1.5.0 languageName: unknown linkType: soft @@ -16511,7 +16618,22 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.2.1, ws@npm:^7.5.3, ws@npm:^7.5.9": +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d + languageName: node + linkType: hard + +"ws@npm:^7.5.3, ws@npm:^7.5.9": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: