From 21c2e8d980a72bcbf7af7c406e4a6ea65d84778b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Thu, 6 Mar 2025 12:27:28 -0300 Subject: [PATCH 01/19] feat: node config module --- packages/wallet-service/package.json | 1 + packages/wallet-service/src/config.ts | 67 +++++++++++++++++++++++ packages/wallet-service/src/db/index.ts | 7 +++ packages/wallet-service/src/fullnode.ts | 62 ++++++++++++++++++++- packages/wallet-service/src/nodeConfig.ts | 59 ++++++++++++++++++++ packages/wallet-service/src/types.ts | 3 + yarn.lock | 31 +++++++++-- 7 files changed, 224 insertions(+), 6 deletions(-) create mode 100644 packages/wallet-service/src/config.ts create mode 100644 packages/wallet-service/src/nodeConfig.ts diff --git a/packages/wallet-service/package.json b/packages/wallet-service/package.json index 264412c2..a1f296f5 100644 --- a/packages/wallet-service/package.json +++ b/packages/wallet-service/package.json @@ -46,6 +46,7 @@ "devDependencies": { "@types/aws-lambda": "8.10.95", "@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/config.ts b/packages/wallet-service/src/config.ts new file mode 100644 index 00000000..593ff788 --- /dev/null +++ b/packages/wallet-service/src/config.ts @@ -0,0 +1,67 @@ +/** + * 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. + */ + +interface EnvironmentConfig { + defaultServer: string; + stage: string; + network: string; + serviceName: string; + maxAddressGap: number; + voidedTxOffset: number; + blockRewardLock: number; + confirmFirstAddress: boolean; + wsDomain: string; + dbEndpoint: string; + dbName: string; + dbUser: string; + dbPass: string; + redisHost: string; + redisPort: number; + authSecret: string; + explorerServiceLambdaEndpoint: string; + walletServiceLambdaEndpoint: string; + pushNotification: boolean; + pushAllowedProviders: string; +} + +let ENVIRONMENT_CONFIG: EnvironmentConfig = null; + +function loadEnvConfig() { + ENVIRONMENT_CONFIG = { + 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), + blockRewardLock: Number.parseInt(process.env.BLOCK_REWARD_LOCK, 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, + dbPass: process.env.DB_PASS, + redisHost: process.env.REDIS_HOST, + redisPort: Number.parseInt(process.env.REDIS_PORT, 10), + authSecret: process.env.AUTH_SECRET, + explorerServiceLambdaEndpoint: process.env.EXPLORER_SERVICE_LAMBDA_ENDPOINT, + walletServiceLambdaEndpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, + pushNotification: process.env.PUSH_NOTIFICATION === 'true', + pushAllowedProviders: process.env.PUSH_ALLOWED_PROVIDERS, + }; +} + +const handler = { + get(target, prop, receiver) { + if (ENVIRONMENT_CONFIG === null) { + loadEnvConfig(); + } + return Reflect.get(target, prop, receiver); + }, +}; + +export default new Proxy(ENVIRONMENT_CONFIG, handler); diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index da913f5e..75e62672 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -1600,6 +1600,9 @@ export const updateVersionData = async (mysql: ServerlessMysql, data: FullNodeVe reward_spend_min_blocks: data.rewardSpendMinBlocks, max_number_inputs: data.maxNumberInputs, max_number_outputs: data.maxNumberOutputs, + decimal_places: data.decimalPlaces, + native_token_name: data.nativeTokenName, + native_token_symbol: data.nativeTokenSymbol, }; await mysql.query( @@ -1632,6 +1635,10 @@ export const getVersionData = async (mysql: ServerlessMysql): Promise({ + 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(), + }), +}) + /** * Creates a handler for requesting data from the fullnode * * @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 +131,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/nodeConfig.ts b/packages/wallet-service/src/nodeConfig.ts new file mode 100644 index 00000000..5b3d117e --- /dev/null +++ b/packages/wallet-service/src/nodeConfig.ts @@ -0,0 +1,59 @@ +/** + * 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 } from '@src/types'; +import fullnode from '@src/fullnode'; + +const VERSION_CHECK_MAX_DIFF = 60 * 60 * 1000; // 1 hour + +export class NodeConfig { + fullnodeVersion: FullNodeVersionData | null = null; + setFullnodeVersion(data: FullNodeVersionData) { + this.fullnodeVersion = data; + } + + async refreshFullnodeVersion() { + // Get data from fullnode version api + const data = await fullnode.version(); + + const now = getUnixTimestamp(); + const fullnodeVersion = { + timestamp: now, + 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, + }; + this.setFullnodeVersion(fullnodeVersion); + } + + async getFullnodeData(mysql: ServerlessMysql): Promise { + const lastVersionData = await getVersionData(mysql); + const now = getUnixTimestamp(); + + if (!lastVersionData || now - lastVersionData.timestamp > VERSION_CHECK_MAX_DIFF) { + await this.refreshFullnodeVersion(); + await updateVersionData(mysql, this.fullnodeVersion); + } else if (this.fullnodeVersion === null) { + this.fullnodeVersion = lastVersionData; + } + + return this.fullnodeVersion; + } +} diff --git a/packages/wallet-service/src/types.ts b/packages/wallet-service/src/types.ts index 1f911035..679f6220 100644 --- a/packages/wallet-service/src/types.ts +++ b/packages/wallet-service/src/types.ts @@ -51,6 +51,9 @@ export interface FullNodeVersionData { rewardSpendMinBlocks: number; maxNumberInputs: number; maxNumberOutputs: number; + decimalPlaces: number; + nativeTokenName: string; + nativeTokenSymbol: string; } export interface TxProposal { diff --git a/yarn.lock b/yarn.lock index f923db7c..27f9cebb 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: @@ -2871,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: @@ -2880,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 @@ -4214,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" @@ -11079,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" @@ -16227,6 +16249,7 @@ __metadata: "@middy/http-cors": "npm:2.5.7" "@types/aws-lambda": "npm:8.10.95" "@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" From 775719b5c1089b70838b8099b77a021c5d4fef1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Fri, 7 Mar 2025 13:51:28 -0300 Subject: [PATCH 02/19] feat: version_data and config migration --- .../20250306170811-node_version_proxy.js | 83 +++++++++++++++++ .../src/api/txProposalCreate.ts | 10 +-- packages/wallet-service/src/api/version.ts | 12 +-- packages/wallet-service/src/commons.ts | 43 --------- packages/wallet-service/src/config.ts | 69 +++++++-------- packages/wallet-service/src/db/index.ts | 47 +++------- packages/wallet-service/src/fullnode.ts | 48 +--------- packages/wallet-service/src/nodeConfig.ts | 78 ++++++++-------- packages/wallet-service/src/schemas.ts | 53 +++++++++++ packages/wallet-service/src/types.ts | 51 ++++++++++- packages/wallet-service/tests/commons.test.ts | 88 +++++++++---------- 11 files changed, 322 insertions(+), 260 deletions(-) create mode 100644 db/migrations/20250306170811-node_version_proxy.js create mode 100644 packages/wallet-service/src/schemas.ts diff --git a/db/migrations/20250306170811-node_version_proxy.js b/db/migrations/20250306170811-node_version_proxy.js new file mode 100644 index 00000000..14cba16a --- /dev/null +++ b/db/migrations/20250306170811-node_version_proxy.js @@ -0,0 +1,83 @@ +'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 addind 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 + await queryInterface.createTable('version_data', { + id: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + primaryKey: true, + defaultValue: 1, + }, + timestamp: { + type: Sequelize.BIGINT.UNSIGNED, + allowNull: false, + }, + version: { + type: Sequelize.STRING(11), + allowNull: false, + }, + network: { + type: Sequelize.STRING(8), + allowNull: false, + }, + min_weight: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + min_tx_weight: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + min_tx_weight_coefficient: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + min_tx_weight_k: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + token_deposit_percentage: { + type: Sequelize.FLOAT.UNSIGNED, + allowNull: false, + }, + reward_spend_min_blocks: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + max_number_inputs: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + max_number_outputs: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + }, + }); + } +}; diff --git a/packages/wallet-service/src/api/txProposalCreate.ts b/packages/wallet-service/src/api/txProposalCreate.ts index 2f6a9f14..c2ca3b6c 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, @@ -29,6 +29,7 @@ import { closeDbConnection, getDbConnection, getUnixTimestamp } from '@src/utils import middy from '@middy/core'; import cors from '@middy/http-cors'; import { constants, Network, Transaction, helpersUtils } from '@hathor/wallet-lib'; +import { getFullnodeData } from '@src/nodeConfig'; const mysql = getDbConnection(); @@ -42,7 +43,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 { @@ -69,8 +70,7 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { const body = value; const tx: Transaction = helpersUtils.createTxFromHex(body.txHex, new Network(process.env.NETWORK)); - // XXX: DEC-0001 - if (tx.outputs.length > constants.MAX_OUTPUTS) { + if (tx.outputs.length > versionData.maxNumberOutputs) { return closeDbAndGetError(mysql, ApiError.TOO_MANY_OUTPUTS, { outputs: tx.outputs.length }); } @@ -111,7 +111,7 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { return closeDbAndGetError(mysql, ApiError.INPUTS_ALREADY_USED); } - if (inputUtxos.length > constants.MAX_OUTPUTS) { + if (inputUtxos.length > versionData.maxNumberOutputs) { return closeDbAndGetError(mysql, ApiError.TOO_MANY_INPUTS, { inputs: inputUtxos.length }); } 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/commons.ts b/packages/wallet-service/src/commons.ts index b8ab2c7c..0506a12f 100644 --- a/packages/wallet-service/src/commons.ts +++ b/packages/wallet-service/src/commons.ts @@ -294,49 +294,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 - // XXX: DEC-0001 - const apiResponse = await hathorLib.versionApi.asyncGetVersion(); - // 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 { - // XXX: DEC-0001 - // 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 index 593ff788..dede3624 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -5,33 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -interface EnvironmentConfig { - defaultServer: string; - stage: string; - network: string; - serviceName: string; - maxAddressGap: number; - voidedTxOffset: number; - blockRewardLock: number; - confirmFirstAddress: boolean; - wsDomain: string; - dbEndpoint: string; - dbName: string; - dbUser: string; - dbPass: string; - redisHost: string; - redisPort: number; - authSecret: string; - explorerServiceLambdaEndpoint: string; - walletServiceLambdaEndpoint: string; - pushNotification: boolean; - pushAllowedProviders: string; -} - -let ENVIRONMENT_CONFIG: EnvironmentConfig = null; +import Joi from 'joi'; +import { EnvironmentConfig } from '@src/types'; +import { EnvironmentConfigSchema } from '@src/schemas'; -function loadEnvConfig() { - ENVIRONMENT_CONFIG = { +export function loadEnvConfig(): EnvironmentConfig { + const config = { defaultServer: process.env.DEFAULT_SERVER ?? 'https://node1.mainnet.hathor.network/v1a/', stage: process.env.STAGE, network: process.env.NETWORK, @@ -46,22 +25,40 @@ function loadEnvConfig() { dbUser: process.env.DB_USER, dbPass: process.env.DB_PASS, redisHost: process.env.REDIS_HOST, - redisPort: Number.parseInt(process.env.REDIS_PORT, 10), + redisPort: Number.parseInt(process.env.REDIS_PORT, 10), authSecret: process.env.AUTH_SECRET, explorerServiceLambdaEndpoint: process.env.EXPLORER_SERVICE_LAMBDA_ENDPOINT, walletServiceLambdaEndpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, pushNotification: process.env.PUSH_NOTIFICATION === 'true', pushAllowedProviders: process.env.PUSH_ALLOWED_PROVIDERS, }; -} -const handler = { - get(target, prop, receiver) { - if (ENVIRONMENT_CONFIG === null) { - loadEnvConfig(); - } - return Reflect.get(target, prop, receiver); - }, + const { value, error } = EnvironmentConfigSchema.validate(config); + if (error) { + throw error; + } + + return value; }; -export default new Proxy(ENVIRONMENT_CONFIG, handler); +/** + * Get a lazy loaded config. + */ +function getConfig(): EnvironmentConfig { + let loaded = false; + // @ts-ignore + let config: EnvironmentConfig = {}; + const handler = { + get(target, prop, receiver) { + if (!loaded) { + config = loadEnvConfig(); + loaded = true; + } + config[prop]; + }, + }; + + return new Proxy(config, handler); +} + +export default getConfig(); diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index 75e62672..03658319 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -39,6 +39,7 @@ import { PushDevice, TxByIdToken, PushDeviceSettings, + FullNodeApiVersionResponse, } from '@src/types'; import { getUnixTimestamp, @@ -57,6 +58,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(); @@ -1586,23 +1588,11 @@ export const getWalletUnlockedUtxos = async ( * @param mysql - Database connection * @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, - decimal_places: data.decimalPlaces, - native_token_name: data.nativeTokenName, - native_token_symbol: data.nativeTokenSymbol, + timestamp, + data: JSON.stringify(data), }; await mysql.query( @@ -1617,31 +1607,22 @@ 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, - // FIXME: update values after migration - decimalPlaces: 2, - nativeTokenName: 'Hathor', - nativeTokenSymbol: 'HTR', + data: entry as FullNodeApiVersionResponse, }; - - return entry; } return null; diff --git a/packages/wallet-service/src/fullnode.ts b/packages/wallet-service/src/fullnode.ts index 319f500f..3e024ed2 100644 --- a/packages/wallet-service/src/fullnode.ts +++ b/packages/wallet-service/src/fullnode.ts @@ -8,52 +8,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 = config.defaultServer; export const TIMEOUT = 10000; -/** - * Fullnode API response. - */ -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}; -} - -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(), - }), -}) - /** * Creates a handler for requesting data from the fullnode * @@ -66,7 +26,7 @@ export const create = (baseURL = BASE_URL) => { timeout: TIMEOUT, }); - const version = async (): Promise => { + const version = async (): Promise => { const response = await api.get('version', { data: null, headers: { 'content-type': 'application/json' }, @@ -76,7 +36,7 @@ export const create = (baseURL = BASE_URL) => { throw new Error(error.message); } - return value as FullnodeApiVersionResponse; + return value as FullNodeApiVersionResponse; }; const downloadTx = async (txId: string) => { diff --git a/packages/wallet-service/src/nodeConfig.ts b/packages/wallet-service/src/nodeConfig.ts index 5b3d117e..24f1ba8d 100644 --- a/packages/wallet-service/src/nodeConfig.ts +++ b/packages/wallet-service/src/nodeConfig.ts @@ -8,52 +8,50 @@ import { getUnixTimestamp } from '@src/utils'; import { ServerlessMysql } from 'serverless-mysql'; import { getVersionData, updateVersionData } from '@src/db'; -import { FullNodeVersionData } from '@src/types'; +import { FullNodeVersionData, FullNodeApiVersionResponse } from '@src/types'; import fullnode from '@src/fullnode'; const VERSION_CHECK_MAX_DIFF = 60 * 60 * 1000; // 1 hour -export class NodeConfig { - fullnodeVersion: FullNodeVersionData | null = null; - setFullnodeVersion(data: FullNodeVersionData) { - this.fullnodeVersion = data; +let RAW_FULLNODE_VERSION: FullNodeApiVersionResponse = null; + +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); + RAW_FULLNODE_VERSION = versionData; + } else if (RAW_FULLNODE_VERSION === null) { + RAW_FULLNODE_VERSION = lastVersionData; } - async refreshFullnodeVersion() { - // Get data from fullnode version api - const data = await fullnode.version(); - - const now = getUnixTimestamp(); - const fullnodeVersion = { - timestamp: now, - 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, - }; - this.setFullnodeVersion(fullnodeVersion); - } - - async getFullnodeData(mysql: ServerlessMysql): Promise { - const lastVersionData = await getVersionData(mysql); - const now = getUnixTimestamp(); + return RAW_FULLNODE_VERSION; +} - if (!lastVersionData || now - lastVersionData.timestamp > VERSION_CHECK_MAX_DIFF) { - await this.refreshFullnodeVersion(); - await updateVersionData(mysql, this.fullnodeVersion); - } else if (this.fullnodeVersion === null) { - this.fullnodeVersion = lastVersionData; - } +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, + }; +} - return this.fullnodeVersion; - } +export async function getFullnodeData(mysql: ServerlessMysql): Promise { + const data = await getRawFullnodeData(mysql); + return convertApiVersionData(data); } diff --git a/packages/wallet-service/src/schemas.ts b/packages/wallet-service/src/schemas.ts new file mode 100644 index 00000000..9b9e5bea --- /dev/null +++ b/packages/wallet-service/src/schemas.ts @@ -0,0 +1,53 @@ +/** + * 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 } 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(), + }), +}); + +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(), + blockRewardLock: 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(), + redisHost: Joi.string().required(), + redisPort: Joi.number().required(), + authSecret: Joi.string().required(), + explorerServiceLambdaEndpoint: Joi.string().required(), + walletServiceLambdaEndpoint: Joi.string().required(), + pushNotification: Joi.boolean().required(), + pushAllowedProviders: Joi.string().required(), +}); diff --git a/packages/wallet-service/src/types.ts b/packages/wallet-service/src/types.ts index 679f6220..b47e2608 100644 --- a/packages/wallet-service/src/types.ts +++ b/packages/wallet-service/src/types.ts @@ -39,8 +39,36 @@ export enum TxProposalStatus { CANCELLED = 'cancelled', } +/** + * wallet-service environment config. + */ +export interface EnvironmentConfig { + defaultServer: string; + stage: string; + network: string; + serviceName: string; + maxAddressGap: number; + voidedTxOffset: number; + blockRewardLock: number; + confirmFirstAddress: boolean; + wsDomain: string; + dbEndpoint: string; + dbName: string; + dbUser: string; + dbPass: string; + redisHost: string; + redisPort: number; + authSecret: string; + explorerServiceLambdaEndpoint: string; + walletServiceLambdaEndpoint: string; + pushNotification: boolean; + pushAllowedProviders: string; +}; + +/** + * Fullnode converted version data. + */ export interface FullNodeVersionData { - timestamp: number; version: string; network: string; minWeight: number; @@ -56,6 +84,27 @@ export interface FullNodeVersionData { 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 { id: string; walletId: string; diff --git a/packages/wallet-service/tests/commons.test.ts b/packages/wallet-service/tests/commons.test.ts index c4fed141..2eadac34 100644 --- a/packages/wallet-service/tests/commons.test.ts +++ b/packages/wallet-service/tests/commons.test.ts @@ -5,17 +5,16 @@ import { markLockedOutputs, unlockUtxos, unlockTimelockedUtxos, - maybeRefreshWalletConstants, searchForLatestValidBlock, getWalletBalancesForTx, } from '@src/commons'; import { - FullNodeVersionData, Authorities, Balance, TokenBalanceMap, DbTxOutput, Block, + FullNodeApiVersionResponse, } from '@src/types'; import { TxInput, @@ -50,6 +49,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,8 +510,7 @@ test('unlockTimelockedUtxos', async () => { await expect(checkWalletBalanceTable(mysql, 1, walletId, token, 5000, 0, null, 3, 0b10, 0)).resolves.toBe(true); }); -// XXX: DEC-0001 -test('maybeRefreshWalletConstants with an uninitialized version_data database should call hathorLib.versionApi.asyncGetVersion()', async () => { +test('getFullnodeData with an uninitialized version_data database should call the version api', async () => { expect.hasAssertions(); const spy = jest.spyOn(hathorLib.axios, 'createRequestInstance'); @@ -529,6 +528,8 @@ test('maybeRefreshWalletConstants with an uninitialized version_data database sh reward_spend_min_blocks: 300, max_number_inputs: 255, max_number_outputs: 255, + decimal_places: 2, + native_token: { name: 'Hathor', symbol: 'HTR'}, }, })); @@ -543,54 +544,45 @@ test('maybeRefreshWalletConstants with an uninitialized version_data database sh get: mockGet, }); - await maybeRefreshWalletConstants(mysql); + await getFullnodeData(mysql); expect(mockGet).toHaveBeenCalledTimes(1); }); -// XXX: DEC-0001 -// test('maybeRefreshWalletConstants 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: {} })); - -// 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 { -// txMinWeight, -// txWeightCoefficient, -// txMinWeightK, -// } = hathorLib.transaction.getTransactionWeightConstants(); - -// const maxNumberInputs = hathorLib.transaction.getMaxInputsConstant(); -// const maxNumberOutputs = hathorLib.transaction.getMaxOutputsConstant(); - -// 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); -// }); +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: 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'}, + }; + + await updateVersionData(mysql, new Date().getTime(), mockedVersionData); + + const data = await getFullnodeData(mysql); + + expect(mockGet).toHaveBeenCalledTimes(0); + expect(data).toEqual(convertApiVersionData(mockedVersionData)); +}); test('searchForLatestValidBlock should find the first voided block', async () => { expect.hasAssertions(); From a8d4c50808b3e1d547075ff89b8b9b0360a7fbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Fri, 7 Mar 2025 13:55:43 -0300 Subject: [PATCH 03/19] chore: add missing import --- packages/wallet-service/src/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet-service/src/schemas.ts b/packages/wallet-service/src/schemas.ts index 9b9e5bea..91713615 100644 --- a/packages/wallet-service/src/schemas.ts +++ b/packages/wallet-service/src/schemas.ts @@ -6,7 +6,7 @@ */ import Joi from 'joi'; -import { FullNodeApiVersionResponse } from '@src/types'; +import { FullNodeApiVersionResponse, EnvironmentConfig } from '@src/types'; export const FullnodeVersionSchema = Joi.object({ version: Joi.string().min(1).required(), From 2eafebe50eca1644b50fbfb2f72e2d039a4d1b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Fri, 7 Mar 2025 18:03:04 -0300 Subject: [PATCH 04/19] fix: version data test utils and diagnostics --- packages/wallet-service/src/commons.ts | 9 +-- packages/wallet-service/src/db/index.ts | 5 +- packages/wallet-service/tests/api.test.ts | 29 +++---- packages/wallet-service/tests/db.test.ts | 78 ++++++++++--------- .../wallet-service/tests/txProposal.test.ts | 50 +++++++----- packages/wallet-service/tests/utils.ts | 42 ++-------- 6 files changed, 97 insertions(+), 116 deletions(-) diff --git a/packages/wallet-service/src/commons.ts b/packages/wallet-service/src/commons.ts index 0506a12f..f660a8ce 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,15 +61,11 @@ 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'; -const VERSION_CHECK_MAX_DIFF = 60 * 60 * 1000; // 1 hour const WARN_MAX_REORG_SIZE = parseInt(process.env.WARN_MAX_REORG_SIZE || '100', 10); /** diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index 03658319..c0f38888 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, @@ -3081,7 +3080,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 @@ -3096,7 +3095,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/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index 7d306c90..9a5a4ff0 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -29,7 +29,7 @@ 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 { WalletStatus, FullNodeApiVersionResponse } from '@src/types'; import { walletUtils, addressUtils, constants, network, HathorWalletServiceWallet } from '@hathor/wallet-lib'; import bitcore from 'bitcore-lib'; import { @@ -55,7 +55,6 @@ import { } from '@tests/utils'; import fullnode from '@src/fullnode'; import { getHealthcheck } from '@src/api/healthcheck'; -import { ping } from "@src/redis"; // Monkey patch bitcore-lib @@ -1593,21 +1592,25 @@ 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); + await updateVersionData(mysql, 1614875031449, mockData); const event = makeGatewayEvent({}); const result = await getVersionDataGet(event, null, null) as APIGatewayProxyResult; diff --git a/packages/wallet-service/tests/db.test.ts b/packages/wallet-service/tests/db.test.ts index 7b32e0b7..cf781e19 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,37 @@ 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); + await updateVersionData(mysql, 1614875031449, mockData); + await updateVersionData(mysql, 1614875031449, mockData2); + await updateVersionData(mysql, 1614875031449, mockData3); await expect( checkVersionDataTable(mysql, mockData3), @@ -1622,25 +1626,29 @@ 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); + await updateVersionData(mysql, 1614875031449, 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 +3254,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 +3264,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 +3316,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 +3326,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 +3389,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 +3399,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/txProposal.test.ts b/packages/wallet-service/tests/txProposal.test.ts index 7d0299ab..bbf31121 100644 --- a/packages/wallet-service/tests/txProposal.test.ts +++ b/packages/wallet-service/tests/txProposal.test.ts @@ -36,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,24 +207,28 @@ test.skip('POST /txproposals with too many outputs should fail with ApiError.TOO 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: 0, + min_tx_weight_k: 0, + 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'}, }); await addToWalletTable(mysql, [{ id: 'my-wallet', xpubkey: 'xpubkey', - authXpubkey: 'auth_xpubkey', + authXpubkey: 'auth_xpubkey', status: 'ready', maxGap: 5, createdAt: 10000, diff --git a/packages/wallet-service/tests/utils.ts b/packages/wallet-service/tests/utils.ts index e07e08b7..3badfcc8 100644 --- a/packages/wallet-service/tests/utils.ts +++ b/packages/wallet-service/tests/utils.ts @@ -4,11 +4,11 @@ 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'; @@ -819,35 +819,17 @@ 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 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 +857,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 { From c3eabea71ebf80d6785093f72edffafcd543f9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Fri, 7 Mar 2025 18:51:18 -0300 Subject: [PATCH 05/19] tests: environment config tests --- .github/workflows/main.yml | 2 ++ packages/wallet-service/tests/env.test.ts | 25 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 packages/wallet-service/tests/env.test.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1cac9ac3..bc79c69e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,6 +114,8 @@ jobs: ALERT_MANAGER_REGION: us-east-1 ALERT_MANAGER_TOPIC: alert-topic PUSH_ALLOWED_PROVIDERS: "" + REDIS_HOST: 127.0.0.1 + REDIS_PORT: 6379 - name: Run integration tests on the daemon run: | diff --git a/packages/wallet-service/tests/env.test.ts b/packages/wallet-service/tests/env.test.ts new file mode 100644 index 00000000..3b133dce --- /dev/null +++ b/packages/wallet-service/tests/env.test.ts @@ -0,0 +1,25 @@ +import config, { loadEnvConfig } from '@src/config'; + +test('Configuration should load correctly during tests', () => { + expect.hasAssertions(); + + const loadedConfig = loadEnvConfig(); + + console.log(loadedConfig); + console.log(config); + expect(loadedConfig).toStrictEqual(config); + + expect(config.confirmFirstAddress).toEqual(true); +}); + +test('loadEnvConfig should get the config from the env', () => { + expect.hasAssertions(); + + const oldNetwork = process.env.NETWORK; + process.env.NETWORK = 'unknown unexisting network'; + + const loadedConfig = loadEnvConfig(); + expect(loadedConfig.network).toEqual('unknown unexisting network'); + + process.env.NETWORK = oldNetwork; +}); From 59c07fd5141537ea1fad55e3fa088d60636d551c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 10 Mar 2025 15:50:03 -0300 Subject: [PATCH 06/19] feat(wallet-service): use config module where possible --- .github/workflows/main.yml | 7 +-- packages/wallet-service/README.md | 2 +- packages/wallet-service/src/api/auth.ts | 5 +- .../wallet-service/src/api/healthcheck.ts | 9 ++-- .../src/api/txProposalCreate.ts | 3 +- .../wallet-service/src/api/txProposalSend.ts | 3 +- packages/wallet-service/src/api/txhistory.ts | 3 +- packages/wallet-service/src/api/wallet.ts | 15 +++--- packages/wallet-service/src/commons.ts | 3 +- packages/wallet-service/src/config.ts | 45 +++++++++++++++-- packages/wallet-service/src/logger.ts | 3 +- packages/wallet-service/src/mempool.ts | 5 +- packages/wallet-service/src/metrics.ts | 5 +- packages/wallet-service/src/redis.ts | 5 +- packages/wallet-service/src/schemas.ts | 26 ++++++++-- packages/wallet-service/src/txProcessor.ts | 3 +- packages/wallet-service/src/types.ts | 28 +++++++++-- packages/wallet-service/src/utils.ts | 19 +++---- .../src/utils/pushnotification.utils.ts | 50 +++++++------------ packages/wallet-service/src/ws/utils.ts | 5 +- packages/wallet-service/tests/utils.test.ts | 1 + 21 files changed, 160 insertions(+), 85 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bc79c69e..ad15ffeb 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,8 +114,9 @@ jobs: ALERT_MANAGER_REGION: us-east-1 ALERT_MANAGER_TOPIC: alert-topic PUSH_ALLOWED_PROVIDERS: "" - REDIS_HOST: 127.0.0.1 - REDIS_PORT: 6379 + REDIS_URL: 127.0.0.1:6379 + REDIS_PASSWORD: "" + IS_OFFLINE: 'true' - name: Run integration tests on the daemon run: | 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/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/txProposalCreate.ts b/packages/wallet-service/src/api/txProposalCreate.ts index c2ca3b6c..8b40abeb 100644 --- a/packages/wallet-service/src/api/txProposalCreate.ts +++ b/packages/wallet-service/src/api/txProposalCreate.ts @@ -30,6 +30,7 @@ import middy from '@middy/core'; import cors from '@middy/http-cors'; import { constants, Network, Transaction, helpersUtils } from '@hathor/wallet-lib'; import { getFullnodeData } from '@src/nodeConfig'; +import config from '@src/config'; const mysql = getDbConnection(); @@ -68,7 +69,7 @@ export const create = middy(walletIdProxyHandler(async (walletId, event) => { } const body = value; - const tx: Transaction = helpersUtils.createTxFromHex(body.txHex, new Network(process.env.NETWORK)); + const tx: Transaction = helpersUtils.createTxFromHex(body.txHex, new Network(config.network)); if (tx.outputs.length > versionData.maxNumberOutputs) { return closeDbAndGetError(mysql, ApiError.TOO_MANY_OUTPUTS, { outputs: tx.outputs.length }); 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 1a5cc03c..ddeebae5 100644 --- a/packages/wallet-service/src/api/txhistory.ts +++ b/packages/wallet-service/src/api/txhistory.ts @@ -19,8 +19,9 @@ 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 MAX_COUNT = config.txHistoryMaxCount; const htrToken = hathorLib.constants.NATIVE_TOKEN_UID; const paramsSchema = Joi.object({ diff --git a/packages/wallet-service/src/api/wallet.ts b/packages/wallet-service/src/api/wallet.ts index d709e7d3..5fea41fc 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; diff --git a/packages/wallet-service/src/commons.ts b/packages/wallet-service/src/commons.ts index f660a8ce..341de854 100644 --- a/packages/wallet-service/src/commons.ts +++ b/packages/wallet-service/src/commons.ts @@ -65,8 +65,9 @@ import { isTxVoided } from '@src/utils'; import hathorLib from '@hathor/wallet-lib'; import { stringMapIterator, WalletBalanceMapConverter } from '@src/db/utils'; +import config from '@src/config'; -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. diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts index dede3624..a0d1ac4c 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -10,7 +10,7 @@ import { EnvironmentConfig } from '@src/types'; import { EnvironmentConfigSchema } from '@src/schemas'; export function loadEnvConfig(): EnvironmentConfig { - const config = { + const config: EnvironmentConfig = { defaultServer: process.env.DEFAULT_SERVER ?? 'https://node1.mainnet.hathor.network/v1a/', stage: process.env.STAGE, network: process.env.NETWORK, @@ -23,14 +23,51 @@ export function loadEnvConfig(): EnvironmentConfig { dbEndpoint: process.env.DB_ENDPOINT, dbName: process.env.DB_NAME, dbUser: process.env.DB_USER, + dbPort: parseInt(process.env.DB_PORT, 10), dbPass: process.env.DB_PASS, - redisHost: process.env.REDIS_HOST, - redisPort: Number.parseInt(process.env.REDIS_PORT, 10), + redisUrl: process.env.REDIS_URL, + redisPassword: process.env.REDIS_PASSWORD, authSecret: process.env.AUTH_SECRET, explorerServiceLambdaEndpoint: process.env.EXPLORER_SERVICE_LAMBDA_ENDPOINT, walletServiceLambdaEndpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, - pushNotification: process.env.PUSH_NOTIFICATION === 'true', + pushNotificationEnabled: process.env.PUSH_NOTIFICATION_ENABLED === 'true', pushAllowedProviders: process.env.PUSH_ALLOWED_PROVIDERS, + isOffline: process.env.IS_OFFLINE === 'true', + txHistoryMaxCount: parseInt(process.env.TX_HISTORY_MAX_COUNT || '50', 10), + healthCheckMaximumHeightDifference: Number(process.env.HEALTHCHECK_MAXIMUM_HEIGHT_DIFFERENCE ?? 5), + + 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: (() => { + try { + /** + * To fix the error 'Error: Invalid PEM formatted message.', + * when initializing the firebase admin app, we need to replace + * 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; + return privateKey + ? privateKey.replace(/\\n/gm, '\n') + : null; + } catch (error) { + console.error('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); + return null; + } + })(), + + maxLoadWalletRetries: parseInt(process.env.MAX_LOAD_WALLET_RETRIES || '5', 10), + logLevel: process.env.LOG_LEVEL || 'info', + createNftMaxRetries: parseInt(process.env.CREATE_NFT_MAX_RETRIES || '3', 10), + warnMaxReorgSize: parseInt(process.env.WARN_MAX_REORG_SIZE || '100', 10), }; const { value, error } = EnvironmentConfigSchema.validate(config); 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/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 index 91713615..5d01029b 100644 --- a/packages/wallet-service/src/schemas.ts +++ b/packages/wallet-service/src/schemas.ts @@ -43,11 +43,31 @@ export const EnvironmentConfigSchema = Joi.object({ dbName: Joi.string().required(), dbUser: Joi.string().required(), dbPass: Joi.string().required(), - redisHost: Joi.string().required(), - redisPort: Joi.number().required(), + dbPort: Joi.number().required(), + redisUrl: Joi.string().required(), + redisPassword: Joi.string(), authSecret: Joi.string().required(), explorerServiceLambdaEndpoint: Joi.string().required(), walletServiceLambdaEndpoint: Joi.string().required(), - pushNotification: Joi.boolean().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 b47e2608..6823b386 100644 --- a/packages/wallet-service/src/types.ts +++ b/packages/wallet-service/src/types.ts @@ -49,20 +49,38 @@ export interface EnvironmentConfig { serviceName: string; maxAddressGap: number; voidedTxOffset: number; - blockRewardLock: number; + blockRewardLock: number; // * confirmFirstAddress: boolean; wsDomain: string; dbEndpoint: string; dbName: string; dbUser: string; dbPass: string; - redisHost: string; - redisPort: number; + dbPort: number; + redisUrl: string; + redisPassword: string; authSecret: string; - explorerServiceLambdaEndpoint: string; + explorerServiceLambdaEndpoint: string; // * walletServiceLambdaEndpoint: string; - pushNotification: boolean; + 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; }; /** diff --git a/packages/wallet-service/src/utils.ts b/packages/wallet-service/src/utils.ts index 6a740a88..02883112 100644 --- a/packages/wallet-service/src/utils.ts +++ b/packages/wallet-service/src/utils.ts @@ -15,6 +15,7 @@ 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); @@ -53,13 +54,13 @@ export class CustomStorage { // XXX: DEC-0002 // DEFAULT_SERVER - 'https://node1.mainnet.hathor.network/v1a/' this.store = { - 'wallet:server': process.env.DEFAULT_SERVER || 'https://node1.mainnet.hathor.network/v1a/', - 'wallet:defaultServer': process.env.DEFAULT_SERVER || 'https://node1.mainnet.hathor.network/v1a/', + 'wallet:server': config.defaultServer, + 'wallet:defaultServer': config.defaultServer, }; } } -hathorLib.network.setNetwork(process.env.NETWORK); +hathorLib.network.setNetwork(config.network); const libNetwork = hathorLib.network.getNetwork(); const hathorNetwork = { @@ -119,19 +120,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..3af25871 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,39 +53,23 @@ 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_PRIVATE_KEY = (() => { - try { - /** - * To fix the error 'Error: Invalid PEM formatted message.', - * when initializing the firebase admin app, we need to replace - * 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; - return privateKey - ? privateKey.replace(/\\n/gm, '\n') - : null; - } catch (error) { - logger.error('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); - return null; - } -})(); +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 = config.firebasePrivateKey; /** 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 +86,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 +97,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/utils.test.ts b/packages/wallet-service/tests/utils.test.ts index eaa11379..cc433b83 100644 --- a/packages/wallet-service/tests/utils.test.ts +++ b/packages/wallet-service/tests/utils.test.ts @@ -68,6 +68,7 @@ test('isTxVoided', async () => { }; }); + // @ts-ignore spy.mockImplementation(mockImplementation); expect(await isTxVoided('0000000f1fbb4bd8a8e71735af832be210ac9a6c1e2081b21faeea3c0f5797f7')).toStrictEqual([ From 3fc81fc5eeaa2a1f429db6e76bfbf85c81179aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 10 Mar 2025 17:54:01 -0300 Subject: [PATCH 07/19] feat(wallet-service): skip config schema validation on tests --- packages/wallet-service/src/config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts index a0d1ac4c..ea5546c3 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -70,6 +70,10 @@ export function loadEnvConfig(): EnvironmentConfig { warnMaxReorgSize: 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) { throw error; From ee6df72dbe24f3454d84b61153688ae8228502ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 10 Mar 2025 19:07:03 -0300 Subject: [PATCH 08/19] feat(wallet-service): remove unused config --- packages/wallet-service/src/config.ts | 2 -- packages/wallet-service/src/schemas.ts | 1 - packages/wallet-service/src/types.ts | 1 - 3 files changed, 4 deletions(-) diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts index ea5546c3..6ee42e3a 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import Joi from 'joi'; import { EnvironmentConfig } from '@src/types'; import { EnvironmentConfigSchema } from '@src/schemas'; @@ -28,7 +27,6 @@ export function loadEnvConfig(): EnvironmentConfig { redisUrl: process.env.REDIS_URL, redisPassword: process.env.REDIS_PASSWORD, authSecret: process.env.AUTH_SECRET, - explorerServiceLambdaEndpoint: process.env.EXPLORER_SERVICE_LAMBDA_ENDPOINT, walletServiceLambdaEndpoint: process.env.WALLET_SERVICE_LAMBDA_ENDPOINT, pushNotificationEnabled: process.env.PUSH_NOTIFICATION_ENABLED === 'true', pushAllowedProviders: process.env.PUSH_ALLOWED_PROVIDERS, diff --git a/packages/wallet-service/src/schemas.ts b/packages/wallet-service/src/schemas.ts index 5d01029b..094ce652 100644 --- a/packages/wallet-service/src/schemas.ts +++ b/packages/wallet-service/src/schemas.ts @@ -47,7 +47,6 @@ export const EnvironmentConfigSchema = Joi.object({ redisUrl: Joi.string().required(), redisPassword: Joi.string(), authSecret: Joi.string().required(), - explorerServiceLambdaEndpoint: Joi.string().required(), walletServiceLambdaEndpoint: Joi.string().required(), pushNotificationEnabled: Joi.boolean().required(), pushAllowedProviders: Joi.string().required(), diff --git a/packages/wallet-service/src/types.ts b/packages/wallet-service/src/types.ts index 6823b386..aa0fe50e 100644 --- a/packages/wallet-service/src/types.ts +++ b/packages/wallet-service/src/types.ts @@ -60,7 +60,6 @@ export interface EnvironmentConfig { redisUrl: string; redisPassword: string; authSecret: string; - explorerServiceLambdaEndpoint: string; // * walletServiceLambdaEndpoint: string; pushNotificationEnabled: boolean; pushAllowedProviders: string; From 5cd72aa2745362ccafd0f3bb38a1ce051e4c1bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Mon, 10 Mar 2025 19:21:52 -0300 Subject: [PATCH 09/19] feat(wallet-service): cleanup after test --- packages/wallet-service/tests/env.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/wallet-service/tests/env.test.ts b/packages/wallet-service/tests/env.test.ts index 3b133dce..9c5c97c0 100644 --- a/packages/wallet-service/tests/env.test.ts +++ b/packages/wallet-service/tests/env.test.ts @@ -3,6 +3,8 @@ import config, { loadEnvConfig } from '@src/config'; test('Configuration should load correctly during tests', () => { expect.hasAssertions(); + // Access config so the proxy loads the config from the environment variables. + expect(config.stage).toBe('local'); const loadedConfig = loadEnvConfig(); console.log(loadedConfig); @@ -18,8 +20,11 @@ test('loadEnvConfig should get the config from the env', () => { const oldNetwork = process.env.NETWORK; process.env.NETWORK = 'unknown unexisting network'; - const loadedConfig = loadEnvConfig(); - expect(loadedConfig.network).toEqual('unknown unexisting network'); + try { + const loadedConfig = loadEnvConfig(); + expect(loadedConfig.network).toEqual('unknown unexisting network'); + } finally { + process.env.NETWORK = oldNetwork; + } - process.env.NETWORK = oldNetwork; }); From c1a39c84d68a6d64e80d4481b6f1869429ad1bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 11 Mar 2025 13:12:07 -0300 Subject: [PATCH 10/19] feat(wallet-service): getVersionData type consistency --- packages/wallet-service/src/config.ts | 22 +--------------------- packages/wallet-service/src/db/index.ts | 5 ++++- packages/wallet-service/tests/env.test.ts | 5 ----- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts index 6ee42e3a..38ff4a4d 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -80,24 +80,4 @@ export function loadEnvConfig(): EnvironmentConfig { return value; }; -/** - * Get a lazy loaded config. - */ -function getConfig(): EnvironmentConfig { - let loaded = false; - // @ts-ignore - let config: EnvironmentConfig = {}; - const handler = { - get(target, prop, receiver) { - if (!loaded) { - config = loadEnvConfig(); - loaded = true; - } - config[prop]; - }, - }; - - return new Proxy(config, handler); -} - -export default getConfig(); +export default loadEnvConfig(); diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index c0f38888..b90a8f1c 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -1624,7 +1624,10 @@ export const getVersionData = async (mysql: ServerlessMysql): Promise<{ timestam }; } - return null; + return { + timestamp: 0, + data: null, + }; }; /** diff --git a/packages/wallet-service/tests/env.test.ts b/packages/wallet-service/tests/env.test.ts index 9c5c97c0..076985d7 100644 --- a/packages/wallet-service/tests/env.test.ts +++ b/packages/wallet-service/tests/env.test.ts @@ -3,12 +3,7 @@ import config, { loadEnvConfig } from '@src/config'; test('Configuration should load correctly during tests', () => { expect.hasAssertions(); - // Access config so the proxy loads the config from the environment variables. - expect(config.stage).toBe('local'); const loadedConfig = loadEnvConfig(); - - console.log(loadedConfig); - console.log(config); expect(loadedConfig).toStrictEqual(config); expect(config.confirmFirstAddress).toEqual(true); From 21dc9bae8e90776ea6748c777add53cd101997ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 11 Mar 2025 13:39:16 -0300 Subject: [PATCH 11/19] feat(wallet-service): use current date when updating version_data --- packages/wallet-service/src/db/index.ts | 1 + packages/wallet-service/tests/api.test.ts | 3 ++- packages/wallet-service/tests/db.test.ts | 10 ++++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/wallet-service/src/db/index.ts b/packages/wallet-service/src/db/index.ts index b90a8f1c..5ba752ca 100644 --- a/packages/wallet-service/src/db/index.ts +++ b/packages/wallet-service/src/db/index.ts @@ -1585,6 +1585,7 @@ 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, timestamp: number, data: FullNodeApiVersionResponse): Promise => { diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index 9a5a4ff0..4c3a70a8 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -1610,7 +1610,8 @@ test('GET /version', async () => { native_token: { name: 'Hathor', symbol: 'HTR'}, }; - await updateVersionData(mysql, 1614875031449, 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/db.test.ts b/packages/wallet-service/tests/db.test.ts index cf781e19..857e85e3 100644 --- a/packages/wallet-service/tests/db.test.ts +++ b/packages/wallet-service/tests/db.test.ts @@ -1614,9 +1614,10 @@ test('updateVersionData', async () => { version: '0.39.2', }; - await updateVersionData(mysql, 1614875031449, mockData); - await updateVersionData(mysql, 1614875031449, mockData2); - await updateVersionData(mysql, 1614875031449, mockData3); + const ts = getUnixTimestamp(); + await updateVersionData(mysql, ts, mockData); + await updateVersionData(mysql, ts, mockData2); + await updateVersionData(mysql, ts, mockData3); await expect( checkVersionDataTable(mysql, mockData3), @@ -1644,7 +1645,8 @@ test('getVersionData', async () => { native_token: { name: 'Hathor', symbol: 'HTR'}, }; - await updateVersionData(mysql, 1614875031449, mockData); + const ts = getUnixTimestamp(); + await updateVersionData(mysql, ts, mockData); const { data } = await getVersionData(mysql); From 06d5497bdea85700c80499d457a56c98a245eda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 11 Mar 2025 14:34:53 -0300 Subject: [PATCH 12/19] feat(wallet-service): test with different const --- packages/wallet-service/src/nodeConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet-service/src/nodeConfig.ts b/packages/wallet-service/src/nodeConfig.ts index 24f1ba8d..0e5a58dd 100644 --- a/packages/wallet-service/src/nodeConfig.ts +++ b/packages/wallet-service/src/nodeConfig.ts @@ -11,7 +11,7 @@ import { getVersionData, updateVersionData } from '@src/db'; import { FullNodeVersionData, FullNodeApiVersionResponse } from '@src/types'; import fullnode from '@src/fullnode'; -const VERSION_CHECK_MAX_DIFF = 60 * 60 * 1000; // 1 hour +const VERSION_CHECK_MAX_DIFF = 60 * 60; // 1 hour let RAW_FULLNODE_VERSION: FullNodeApiVersionResponse = null; From 465efce0a460d6f170e0a009b18a7edeb359e286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 11 Mar 2025 15:29:05 -0300 Subject: [PATCH 13/19] feat(wallet-service): do not cache raw version data --- .github/workflows/main.yml | 2 +- packages/wallet-service/src/nodeConfig.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ad15ffeb..11385fd0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,7 +114,7 @@ jobs: ALERT_MANAGER_REGION: us-east-1 ALERT_MANAGER_TOPIC: alert-topic PUSH_ALLOWED_PROVIDERS: "" - REDIS_URL: 127.0.0.1:6379 + REDIS_URL: redis://127.0.0.1:6379 REDIS_PASSWORD: "" IS_OFFLINE: 'true' diff --git a/packages/wallet-service/src/nodeConfig.ts b/packages/wallet-service/src/nodeConfig.ts index 0e5a58dd..51543eb1 100644 --- a/packages/wallet-service/src/nodeConfig.ts +++ b/packages/wallet-service/src/nodeConfig.ts @@ -13,8 +13,6 @@ import fullnode from '@src/fullnode'; const VERSION_CHECK_MAX_DIFF = 60 * 60; // 1 hour -let RAW_FULLNODE_VERSION: FullNodeApiVersionResponse = null; - export async function getRawFullnodeData(mysql: ServerlessMysql): Promise { const { timestamp, @@ -25,12 +23,10 @@ export async function getRawFullnodeData(mysql: ServerlessMysql): Promise VERSION_CHECK_MAX_DIFF) { const versionData = await fullnode.version(); await updateVersionData(mysql, timestamp, versionData); - RAW_FULLNODE_VERSION = versionData; - } else if (RAW_FULLNODE_VERSION === null) { - RAW_FULLNODE_VERSION = lastVersionData; + return versionData; } - return RAW_FULLNODE_VERSION; + return lastVersionData; } export function convertApiVersionData(data: FullNodeApiVersionResponse): FullNodeVersionData { From 7080e28bf0532df0280e923730bc7617d3d22c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Tue, 11 Mar 2025 22:15:42 -0300 Subject: [PATCH 14/19] tests: change how we mock the config module --- packages/wallet-service/src/config.ts | 27 ++++----- packages/wallet-service/src/schemas.ts | 2 +- .../src/utils/pushnotification.utils.ts | 18 +++++- packages/wallet-service/tests/api.test.ts | 1 + packages/wallet-service/tests/commons.test.ts | 56 +++++++++---------- .../utils/pushnotification.utils.test.ts | 25 ++++++++- .../wallet-service/tests/ws.utils.test.ts | 53 ++++++++++++------ 7 files changed, 116 insertions(+), 66 deletions(-) diff --git a/packages/wallet-service/src/config.ts b/packages/wallet-service/src/config.ts index 38ff4a4d..48b2b4b7 100644 --- a/packages/wallet-service/src/config.ts +++ b/packages/wallet-service/src/config.ts @@ -7,6 +7,7 @@ import { EnvironmentConfig } from '@src/types'; import { EnvironmentConfigSchema } from '@src/schemas'; +import { Severity, addAlert } from '@wallet-service/common'; export function loadEnvConfig(): EnvironmentConfig { const config: EnvironmentConfig = { @@ -44,23 +45,7 @@ export function loadEnvConfig(): EnvironmentConfig { firebaseTokenUri: process.env.FIREBASE_TOKEN_URI, firebaseAuthProviderX509CertUrl: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, firebaseClientX509CertUrl: process.env.FIREBASE_CLIENT_X509_CERT_URL, - firebasePrivateKey: (() => { - try { - /** - * To fix the error 'Error: Invalid PEM formatted message.', - * when initializing the firebase admin app, we need to replace - * 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; - return privateKey - ? privateKey.replace(/\\n/gm, '\n') - : null; - } catch (error) { - console.error('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); - return null; - } - })(), + firebasePrivateKey: process.env.FIREBASE_PRIVATE_KEY, maxLoadWalletRetries: parseInt(process.env.MAX_LOAD_WALLET_RETRIES || '5', 10), logLevel: process.env.LOG_LEVEL || 'info', @@ -74,6 +59,14 @@ export function loadEnvConfig(): EnvironmentConfig { 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; } diff --git a/packages/wallet-service/src/schemas.ts b/packages/wallet-service/src/schemas.ts index 094ce652..be878377 100644 --- a/packages/wallet-service/src/schemas.ts +++ b/packages/wallet-service/src/schemas.ts @@ -27,7 +27,7 @@ export const FullnodeVersionSchema = 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(), diff --git a/packages/wallet-service/src/utils/pushnotification.utils.ts b/packages/wallet-service/src/utils/pushnotification.utils.ts index 3af25871..8e5ef5af 100644 --- a/packages/wallet-service/src/utils/pushnotification.utils.ts +++ b/packages/wallet-service/src/utils/pushnotification.utils.ts @@ -66,7 +66,23 @@ 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 = config.firebasePrivateKey; +const FIREBASE_PRIVATE_KEY = (() => { + try { + /** + * To fix the error 'Error: Invalid PEM formatted message.', + * when initializing the firebase admin app, we need to replace + * the escaped line break with an unescaped line break. + * https://github.com/gladly-team/next-firebase-auth/discussions/95#discussioncomment-2891225 + */ + const privateKey = config.firebasePrivateKey; + return privateKey + ? privateKey.replace(/\\n/gm, '\n') + : null; + } catch (error) { + logger.error('[ALERT] Error while parsing the env.FIREBASE_PRIVATE_KEY.'); + return null; + } +})(); /** Local feature toggle that disable the push notification by default */ const PUSH_NOTIFICATION_ENABLED = config.pushNotificationEnabled; diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index 4c3a70a8..53e7e1c9 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -1610,6 +1610,7 @@ test('GET /version', async () => { native_token: { name: 'Hathor', symbol: 'HTR'}, }; + console.log(`GET /version`); const ts = getUnixTimestamp() await updateVersionData(mysql, ts, mockData); diff --git a/packages/wallet-service/tests/commons.test.ts b/packages/wallet-service/tests/commons.test.ts index 2eadac34..9e551a59 100644 --- a/packages/wallet-service/tests/commons.test.ts +++ b/packages/wallet-service/tests/commons.test.ts @@ -16,6 +16,7 @@ import { Block, FullNodeApiVersionResponse, } from '@src/types'; +import fullnode from '@src/fullnode'; import { TxInput, TxOutput, @@ -513,40 +514,37 @@ test('unlockTimelockedUtxos', 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, - decimal_places: 2, - native_token: { name: 'Hathor', symbol: 'HTR'}, - }, + const spy = jest.spyOn(fullnode.api, 'get'); + spy.mockImplementation(() => Promise.resolve({ + status: 200, + data: mockData, })); - spy.mockReturnValue({ - // @ts-ignore - post: () => Promise.resolve({ - data: { - success: true, - }, - }), - // @ts-ignore - get: mockGet, - }); + await mysql.query('DELETE FROM`version_data`'); - await getFullnodeData(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('getFullnodeData with an initialized version_data database should query data from the database', async () => { 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', () => { From 1d0748b8c26a6cc400d51cabb6c54ef25710e753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 12 Mar 2025 15:21:47 -0300 Subject: [PATCH 15/19] tests: add tests to increase coverage --- packages/wallet-service/tests/api.test.ts | 64 +++++++++++++++++++++++ packages/wallet-service/tests/utils.ts | 35 ++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index 53e7e1c9..bc02dada 100644 --- a/packages/wallet-service/tests/api.test.ts +++ b/packages/wallet-service/tests/api.test.ts @@ -19,6 +19,7 @@ import { get as walletGet, load as walletLoad, loadWallet, + loadWalletFailed, changeAuthXpub, } from '@src/api/wallet'; import { @@ -52,9 +53,13 @@ import { makeGatewayEventWithAuthorizer, getAuthData, getXPrivKeyFromSeed, + makeLoadWalletFailedSNSEvent, } from '@tests/utils'; import fullnode from '@src/fullnode'; import { getHealthcheck } from '@src/api/healthcheck'; +import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; +import { Severity } from '@wallet-service/common'; +import { ErrorMessages } from '@hathor/wallet-lib/lib/errorMessages'; // Monkey patch bitcore-lib @@ -1153,6 +1158,47 @@ 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); + // 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, + }); + + // 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.details[0].message).toBe('Expected first address to be'); +}, 30000); + test('PUT /wallet/auth should change the auth_xpub only after validating both the xpub and the auth_xpubkey', async () => { expect.hasAssertions(); @@ -1326,6 +1372,24 @@ 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 = '{}'; + 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(); diff --git a/packages/wallet-service/tests/utils.ts b/packages/wallet-service/tests/utils.ts index 3badfcc8..20347619 100644 --- a/packages/wallet-service/tests/utils.ts +++ b/packages/wallet-service/tests/utils.ts @@ -1,4 +1,4 @@ -import { APIGatewayProxyEvent } from 'aws-lambda'; +import { APIGatewayProxyEvent, SNSEvent, SNSEventRecord } from 'aws-lambda'; import { ServerlessMysql } from 'serverless-mysql'; import { isEqual } from 'lodash'; import { @@ -819,6 +819,39 @@ export const makeGatewayEventWithAuthorizer = ( resource: null, }); +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) ]]; From 5d6586834f57d76e2fe2f972a81fc3e526ce8e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 12 Mar 2025 17:34:55 -0300 Subject: [PATCH 16/19] tests: create wallet before testing for first address --- packages/wallet-service/tests/api.test.ts | 17 +++++++++++++---- packages/wallet-service/tests/utils.ts | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/wallet-service/tests/api.test.ts b/packages/wallet-service/tests/api.test.ts index bc02dada..7ea2c8bd 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'; @@ -24,6 +25,7 @@ import { } from '@src/api/wallet'; import { updateVersionData, + createWallet, } from '@src/db'; import * as Wallet from '@src/api/wallet'; import * as Db from '@src/db'; @@ -57,7 +59,6 @@ import { } from '@tests/utils'; import fullnode from '@src/fullnode'; import { getHealthcheck } from '@src/api/healthcheck'; -import { mockedAddAlert } from '@tests/utils/alerting.utils.mock'; import { Severity } from '@wallet-service/common'; import { ErrorMessages } from '@hathor/wallet-lib/lib/errorMessages'; @@ -1163,6 +1164,9 @@ test('PUT /wallet/auth should fail if we cannot confirm the firstAddress', async // 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; @@ -1175,6 +1179,8 @@ test('PUT /wallet/auth should fail if we cannot confirm the firstAddress', async networkName: process.env.NETWORK, }); + const wallet = await createWallet(mysql, walletId, XPUBKEY, AUTH_XPUBKEY, 20); + // account path const accountDerivationIndex = '0\''; @@ -1196,7 +1202,8 @@ test('PUT /wallet/auth should fail if we cannot confirm the firstAddress', async expect(result.statusCode).toBe(400); expect(returnBody.success).toBe(false); - expect(returnBody.details[0].message).toBe('Expected first address to be'); + 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 () => { @@ -1377,13 +1384,16 @@ test('loadWalletFailed should create alert if xpubkey is missing', async () => { 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', + RequestID: 'a-req-01', ErrorMessage: 'an-error-01' }, expect.anything(), @@ -1674,7 +1684,6 @@ test('GET /version', async () => { native_token: { name: 'Hathor', symbol: 'HTR'}, }; - console.log(`GET /version`); const ts = getUnixTimestamp() await updateVersionData(mysql, ts, mockData); diff --git a/packages/wallet-service/tests/utils.ts b/packages/wallet-service/tests/utils.ts index 20347619..8342ef85 100644 --- a/packages/wallet-service/tests/utils.ts +++ b/packages/wallet-service/tests/utils.ts @@ -836,7 +836,7 @@ export function makeLoadWalletFailedSNSEvent(count: number, xpubkey: string, req maxGap: 20, }), MessageAttributes: { - RequestId: { Type: 'string', Value: requestId || 'request-id' }, + RequestID: { Type: 'string', Value: requestId || 'request-id' }, ErrorMessage: { Type: 'string', Value: errorMessage || 'error-message' }, }, Type: '', From 855491f8bfcd6d3591393095ade693e1c09f5c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 12 Mar 2025 17:57:34 -0300 Subject: [PATCH 17/19] chore: resolving decision issues --- packages/wallet-service/src/api/wallet.ts | 1 + packages/wallet-service/src/utils.ts | 41 ------------------- .../wallet-service/tests/txProposal.test.ts | 3 +- packages/wallet-service/tests/utils.test.ts | 24 ----------- 4 files changed, 2 insertions(+), 67 deletions(-) diff --git a/packages/wallet-service/src/api/wallet.ts b/packages/wallet-service/src/api/wallet.ts index 5fea41fc..96cdc938 100644 --- a/packages/wallet-service/src/api/wallet.ts +++ b/packages/wallet-service/src/api/wallet.ts @@ -443,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/utils.ts b/packages/wallet-service/src/utils.ts index 02883112..06809ab1 100644 --- a/packages/wallet-service/src/utils.ts +++ b/packages/wallet-service/src/utils.ts @@ -19,47 +19,6 @@ import config from '@src/config'; const bip32 = BIP32Factory(ecc); -// XXX - Check effects of this storage -/* 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 { - // XXX: DEC-0002 - // DEFAULT_SERVER - 'https://node1.mainnet.hathor.network/v1a/' - this.store = { - 'wallet:server': config.defaultServer, - 'wallet:defaultServer': config.defaultServer, - }; - } -} - hathorLib.network.setNetwork(config.network); const libNetwork = hathorLib.network.getNetwork(); diff --git a/packages/wallet-service/tests/txProposal.test.ts b/packages/wallet-service/tests/txProposal.test.ts index bbf31121..8fe6f097 100644 --- a/packages/wallet-service/tests/txProposal.test.ts +++ b/packages/wallet-service/tests/txProposal.test.ts @@ -201,8 +201,7 @@ test('POST /txproposals with utxos that are already used on another txproposal s expect(usedInputsReturnBody.error).toBe(ApiError.INPUTS_ALREADY_USED); }); -// XXX: DEC-0001 -test.skip('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY_OUTPUTS', async () => { +test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY_OUTPUTS', async () => { expect.hasAssertions(); const now = getUnixTimestamp(); diff --git a/packages/wallet-service/tests/utils.test.ts b/packages/wallet-service/tests/utils.test.ts index cc433b83..09b53623 100644 --- a/packages/wallet-service/tests/utils.test.ts +++ b/packages/wallet-service/tests/utils.test.ts @@ -3,30 +3,6 @@ import hathorLib from '@hathor/wallet-lib'; import * as Fullnode from '@src/fullnode'; import { TEST_SEED, XPUBKEY, AUTH_XPUBKEY, ADDRESSES } from '@tests/utils'; -// XXX: DEC-0002 -test('CustomStorage', () => { - expect.hasAssertions(); - - const store = new CustomStorage(); - // Should be initialized with hathor default server and server - expect(store.getItem('wallet:defaultServer')).toBe('https://node1.mainnet.hathor.network/v1a/'); - expect(store.getItem('wallet:server')).toBe('https://node1.mainnet.hathor.network/v1a/'); - - 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('https://node1.mainnet.hathor.network/v1a/'); - expect(store.getItem('wallet:server')).toBe('https://node1.mainnet.hathor.network/v1a/'); -}); - test('sha256d', () => { expect.hasAssertions(); // sha256d(my-test-data) -> 4f1ba9a4204e97a293b16ead6caced38f6d91d95618b96e261c6332ed24f7894 From 812defec698b61b494360c3d1ba1071e68cafbf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 12 Mar 2025 21:20:01 -0300 Subject: [PATCH 18/19] tests: remove broken import --- packages/wallet-service/tests/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet-service/tests/utils.test.ts b/packages/wallet-service/tests/utils.test.ts index 09b53623..ce8a3516 100644 --- a/packages/wallet-service/tests/utils.test.ts +++ b/packages/wallet-service/tests/utils.test.ts @@ -1,4 +1,4 @@ -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'; From ac2b3b6e0dab845f152b65a586db1e7ed1aa7942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Carneiro?= Date: Wed, 12 Mar 2025 21:51:54 -0300 Subject: [PATCH 19/19] tests: change coef value --- packages/wallet-service/tests/txProposal.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/wallet-service/tests/txProposal.test.ts b/packages/wallet-service/tests/txProposal.test.ts index 8fe6f097..a6d86e22 100644 --- a/packages/wallet-service/tests/txProposal.test.ts +++ b/packages/wallet-service/tests/txProposal.test.ts @@ -211,8 +211,8 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY network: process.env.NETWORK, min_weight: 8, min_tx_weight: 8, - min_tx_weight_coefficient: 0, - min_tx_weight_k: 0, + 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, @@ -223,6 +223,7 @@ test('POST /txproposals with too many outputs should fail with ApiError.TOO_MANY genesis_tx2_hash: 'cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe', native_token: { name: 'Hathor', symbol: 'HTR'}, }); + jest.resetModules(); await addToWalletTable(mysql, [{ id: 'my-wallet',