From 0b725038ec55ebb08936c28cb5dc68105a4c1755 Mon Sep 17 00:00:00 2001 From: Oleksandr Loiko Date: Thu, 5 Dec 2024 18:53:35 +0200 Subject: [PATCH] ALL-9923 Added report feature --- README.md | 36 +++++++++- package.json | 2 +- src/index.ts | 24 +++++-- src/interfaces.ts | 41 +++++++++++- src/management.ts | 165 +++++++++++++++++++++++++++++++++++----------- src/signatures.ts | 16 +++-- src/utils.ts | 32 +++++++++ 7 files changed, 266 insertions(+), 50 deletions(-) create mode 100644 src/utils.ts diff --git a/README.md b/README.md index 004b42a..e88b578 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ When KMS runs in [daemon mode](#run-kms-in-daemon-mode), use the following comma } } ``` -* `checkconfig` shows environment variables for Tatum KMS (for debugging). +* `checkconfig`(for debugging) shows environment variables for Tatum KMS. ``` bash:$ tatum-kms checkconfig @@ -385,6 +385,40 @@ When KMS runs in [daemon mode](#run-kms-in-daemon-mode), use the following comma Env file : .env TATUM_API_KEY : d2eb5c****************************** ... + ``` +* `report`(for debugging) shows report of system and requested wallets (+ warnings if they were found) + + ``` + bash:$ tatum-kms e3015fc0-2112-4c8a-b8bf-353b86f63ba5,11115fc0-2112-4c8a-b8bf-353b86f63111 + { + "system": { + "kmsVersion": "7.0.6", + "nodeVersion": "v18.18.2", + "store": { + "type": "LOCAL", + "exists": true + } + }, + "wallets": { + "e3015fc0-2112-4c8a-b8bf-353b86f63ba5": { + "type": "PRIVATE_KEY", + "chain": "BTC", + "testnet": true + }, + "11115fc0-2112-4c8a-b8bf-353b86f63111": { + "type": "MNEMONIC", + "chain": "ETH", + "testnet": true, + "warnings": [ + "No xpub found" + ] + } + }, + "apiKey": "t-6111***************************************222222", + "warnings": [ + "Wallets file was is not accessible" + ] + } ``` ## Common issues diff --git a/package.json b/package.json index 10b7898..1beaba0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tatumio/tatum-kms", - "version": "7.0.6", + "version": "7.0.7", "description": "Tatum KMS - Key Management System for Tatum-powered apps.", "main": "dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 2984e0a..350b398 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { getQuestion, getWallet, removeWallet, + report, setTatumKey, storePrivateKey, storeWallet, @@ -23,6 +24,7 @@ import HttpAgent from 'agentkeepalive' import { existsSync } from 'fs' import * as process from 'process' import { homedir } from 'os' +import { utils } from './utils' dotenv.config() @@ -44,7 +46,7 @@ const axiosInstance = axios.create({ const { input: command, flags, help } = meow( ` Usage - $ tatum-kms command + $ tatum-kms Commands daemon Run as a daemon, which periodically checks for a new transactions to sign. @@ -52,13 +54,17 @@ const { input: command, flags, help } = meow( generatemanagedwallet Generate wallet for a specific blockchain and add it to the managed wallets. storemanagedwallet Store mnemonic-based wallet for a specific blockchain and add it to the managed wallets. storemanagedprivatekey Store private key of a specific blockchain and add it to the managed wallets. - generatemanagedprivatekeybatch generate and store "cnt" number of private keys for a specific blockchain. This operation is usefull, if you wanna pregenerate bigger amount of managed private keys for later use. + generatemanagedprivatekeybatch Generate and store "cnt" number of private keys for a specific blockchain. This operation is usefull, if you wanna pregenerate bigger amount of managed private keys for later use. getprivatekey Obtain managed wallet from wallet store and generate private key for given derivation index. getaddress Obtain managed wallet from wallet store and generate address for given derivation index. getmanagedwallet Obtain managed wallet / private key from wallet store. removewallet Remove managed wallet from wallet store. export Export all managed wallets. + Debugging + report Shows report of system and requested wallets (+ warnings if they were found) + checkconfig Shows environment variables for Tatum KMS. + Options --api-key Tatum API Key to communicate with Tatum API. Daemon mode only. --testnet Indicates testnet version of blockchain. Mainnet by default. @@ -66,8 +72,8 @@ const { input: command, flags, help } = meow( --period Period in seconds to check for new transactions to sign, defaults to 5 seconds. Daemon mode only. --chain Blockchains to check, separated by comma. Daemon mode only. --env-file Path to .env file to set vars. - --aws Using AWS Secrets Manager (https://aws.amazon.com/secrets-manager/) as a secure storage of the password which unlocks the wallet file. - --vgs Using VGS (https://verygoodsecurity.com) as a secure storage of the password which unlocks the wallet file. + --aws Using AWS Secrets Manager (https://aws.amazon.com/secrets-manager/) as a secure storage of the password which unlocks the wallet file. + --vgs Using VGS (https://verygoodsecurity.com) as a secure storage of the password which unlocks the wallet file. --azure Using Azure Vault (https://azure.microsoft.com/en-us/services/key-vault/) as a secure storage of the password which unlocks the wallet file. --externalUrl Pass in external url to check valid transaction. This parameter is mandatory for mainnet (if testnet is false). Daemon mode only. --externalUrlMethod Determine what http method to use when calling the url passed in the --externalUrl option. Accepts GET or POST. Defaults to GET method. Daemon mode only. @@ -115,7 +121,7 @@ const { input: command, flags, help } = meow( runOnce: { type: 'boolean', default: false, - } + }, }, }, ) @@ -217,6 +223,14 @@ const startup = async () => { case 'checkconfig': checkConfig(getPasswordType(), envFilePath, flags.path) break + case 'report': + await report( + utils.csvToArray(command[1]), + getPasswordType(), + await getPassword(getPasswordType(), axiosInstance), + flags.path, + ) + break default: console.error('Unsupported command. Use tatum-kms --help for details.') process.exit(-1) diff --git a/src/interfaces.ts b/src/interfaces.ts index f61facf..c46b249 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -14,8 +14,11 @@ export interface Signature { export interface Wallet { mnemonic: string + xpub: string testnet: boolean privateKey: string + secret: string + chain: Currency } export interface WalletsValidationOptions { @@ -29,4 +32,40 @@ export interface StoreWalletValue { xpub?: string } -export type ExternalUrlMethod = 'GET' | 'POST'; +export type ExternalUrlMethod = 'GET' | 'POST' + +export type Report = { + system: { + kmsVersion: string + nodeVersion: string + store: { + type: string + exists: boolean + } + } + wallets: Record + apiKey: string + warnings?: string[] +} + +export type ReportWallet = { + type: WalletType + chain: Currency + testnet: boolean + warnings?: string[] +} + +export enum WalletType { + MNEMONIC = 'MNEMONIC', + PRIVATE_KEY = 'PRIVATE_KEY', + SECRET = 'SECRET', + OTHER = 'OTHER', +} + +export enum WalletStoreType { + LOCAL = 'LOCAL', + VGS = 'VGS', + AZURE = 'AZURE', + AWS = 'AWS', + NA = 'N/A', +} diff --git a/src/management.ts b/src/management.ts index 3c2b721..c52c26c 100644 --- a/src/management.ts +++ b/src/management.ts @@ -16,7 +16,19 @@ import { dirname } from 'path' import { question } from 'readline-sync' import { v4 as uuid } from 'uuid' import { Config, ConfigOption } from './config' -import { PasswordType, Signature, StoreWalletValue, WalletsValidationOptions } from './interfaces' +import { + PasswordType, + Report, + ReportWallet, + Signature, + StoreWalletValue, + Wallet, + WalletStoreType, + WalletsValidationOptions, + WalletType, +} from './interfaces' +import { utils } from './utils' +import semver from 'semver' const ensurePathExists = (path: string) => { const dir = dirname(path) @@ -395,57 +407,126 @@ export const removeWallet = async (id: string, pwd: string, path?: string) => { writeFileSync(pathToWallet, AES.encrypt(JSON.stringify(wallet), pwd).toString()) } -function parseWalletStoreName(pwdType: PasswordType): string { +function parseWalletStoreName(pwdType: PasswordType): WalletStoreType { if (pwdType === PasswordType.CMD_LINE) { - return 'LOCAL' + return WalletStoreType.LOCAL } else if (pwdType === PasswordType.VGS) { - return 'VGS' + return WalletStoreType.VGS } else if (pwdType === PasswordType.AZURE) { - return 'AZURE' + return WalletStoreType.AZURE } else if (pwdType === PasswordType.AWS) { - return 'AWS' + return WalletStoreType.AWS } - return 'N/A' -} - -function hidePassword(password: string | undefined, showSymbols = 6): string { - if (!password) { - return '' - } - if (password.length <= showSymbols) { - return '*'.repeat(password.length) - } - return password.slice(0, showSymbols) + '*'.repeat(password.length - showSymbols) -} - -function secretValue(secretValue: string | undefined): string { - if (!secretValue) { - return 'N/A' - } - return hidePassword(secretValue) + return WalletStoreType.NA } export const checkConfig = (pwdType: PasswordType, envFile?: string, path?: string) => { const pathToWallet = path || homedir() + '/.tatumrc/wallet.dat' - console.log(`Version : ${process.env.npm_package_version ?? 'N/A'}`) + console.log(`Version : ${getKmsVersion()}`) console.log(`Wallet file path : ${pathToWallet}`) console.log(`Wallet exists : ${existsSync(pathToWallet)}`) console.log(`Wallet store type : ${parseWalletStoreName(pwdType)}`) console.log(`Environment vars file : ${envFile ?? 'N/A'}`) - console.log(`TATUM_API_KEY : ${secretValue(process.env.TATUM_API_KEY)}`) - console.log(`TATUM_KMS_PASSWORD : ${secretValue(process.env.TATUM_KMS_PASSWORD)}`) - console.log(`TATUM_KMS_VGS_ALIAS : ${secretValue(process.env.TATUM_KMS_VGS_ALIAS)}`) - console.log(`TATUM_KMS_VGS_USERNAME : ${secretValue(process.env.TATUM_KMS_VGS_USERNAME)}`) - console.log(`TATUM_KMS_VGS_PASSWORD : ${secretValue(process.env.TATUM_KMS_VGS_PASSWORD)}`) - console.log(`TATUM_KMS_AZURE_SECRETVERSION : ${secretValue(process.env.TATUM_KMS_AZURE_SECRETVERSION)}`) - console.log(`TATUM_KMS_AZURE_SECRETNAME : ${secretValue(process.env.TATUM_KMS_AZURE_SECRETNAME)}`) - console.log(`TATUM_KMS_AZURE_VAULTURL : ${secretValue(process.env.TATUM_KMS_AZURE_VAULTURL)}`) + console.log(`TATUM_API_KEY : ${utils.hideApiKey(process.env.TATUM_API_KEY)}`) + console.log(`TATUM_KMS_PASSWORD : ${utils.hidePassword(process.env.TATUM_KMS_PASSWORD)}`) + console.log(`TATUM_KMS_VGS_ALIAS : ${utils.hidePassword(process.env.TATUM_KMS_VGS_ALIAS)}`) + console.log(`TATUM_KMS_VGS_USERNAME : ${utils.hidePassword(process.env.TATUM_KMS_VGS_USERNAME)}`) + console.log(`TATUM_KMS_VGS_PASSWORD : ${utils.hidePassword(process.env.TATUM_KMS_VGS_PASSWORD)}`) + console.log(`TATUM_KMS_AZURE_SECRETVERSION : ${utils.hidePassword(process.env.TATUM_KMS_AZURE_SECRETVERSION)}`) + console.log(`TATUM_KMS_AZURE_SECRETNAME : ${utils.hidePassword(process.env.TATUM_KMS_AZURE_SECRETNAME)}`) + console.log(`TATUM_KMS_AZURE_VAULTURL : ${utils.hidePassword(process.env.TATUM_KMS_AZURE_VAULTURL)}`) console.log(`TATUM_KMS_AWS_REGION : ${process.env.TATUM_KMS_AWS_REGION ?? 'N/A'}`) - console.log(`TATUM_KMS_AWS_ACCESS_KEY_ID : ${secretValue(process.env.TATUM_KMS_AWS_ACCESS_KEY_ID)}`) - console.log(`TATUM_KMS_AWS_SECRET_ACCESS_KEY : ${secretValue(process.env.TATUM_KMS_AWS_SECRET_ACCESS_KEY)}`) - console.log(`TATUM_KMS_AWS_SECRET_NAME : ${secretValue(process.env.TATUM_KMS_AWS_SECRET_NAME)}`) - console.log(`TATUM_KMS_AWS_SECRET_KEY : ${secretValue(process.env.TATUM_KMS_AWS_SECRET_KEY)}`) - console.log(`TATUM_KMS_DEBUG_MODE : ${process.env.TATUM_KMS_DEBUG_MODE ?? 'N/A'}`) + console.log(`TATUM_KMS_AWS_ACCESS_KEY_ID : ${utils.hidePassword(process.env.TATUM_KMS_AWS_ACCESS_KEY_ID)}`) + console.log(`TATUM_KMS_AWS_SECRET_ACCESS_KEY : ${utils.hidePassword(process.env.TATUM_KMS_AWS_SECRET_ACCESS_KEY)}`) + console.log(`TATUM_KMS_AWS_SECRET_NAME : ${utils.hidePassword(process.env.TATUM_KMS_AWS_SECRET_NAME)}`) + console.log(`TATUM_KMS_AWS_SECRET_KEY : ${utils.hidePassword(process.env.TATUM_KMS_AWS_SECRET_KEY)}`) + console.log(`TATUM_KMS_DEBUG_MODE : ${process.env.TATUM_KMS_DEBUG_MODE ?? 'N/A'}`) +} + +export const report = async (signatureIds: string[], passwordType: PasswordType, pwd: string, path?: string) => { + const systemWarnings: string[] = [] + const walletReports: Record = {} + for (const signatureId of signatureIds) { + const wallet: Wallet = await getWallet(signatureId, pwd, path, false) + if (!wallet) { + systemWarnings.push(`No wallet found for signatureId: ${signatureId}`) + continue + } + if (!_.isObject(wallet)) { + systemWarnings.push(`Wallet for signatureId: ${signatureId} is not an object. Its type is: ${typeof wallet}`) + continue + } + + const warnings: string[] = [] + const type: WalletType = validateWallet(wallet, warnings) + + walletReports[signatureId] = { + type: type, + chain: wallet.chain, + testnet: wallet.testnet, + warnings: warnings && warnings.length > 0 ? warnings : undefined, + } + } + const nodeVersion = validateNodeVersion(systemWarnings) + + const report: Report = { + system: { + kmsVersion: getKmsVersion(), + nodeVersion: nodeVersion, + store: { + type: parseWalletStoreName(passwordType), + exists: existsSync(getPathToWallet(path)), + }, + }, + wallets: walletReports, + apiKey: utils.hideApiKey(process.env.TATUM_API_KEY), + warnings: systemWarnings && systemWarnings.length > 0 ? systemWarnings : undefined, + } + + console.log(JSON.stringify(report, null, 2)) +} + +const validateWallet = (wallet: Wallet, warnings: string[]) => { + if (wallet.mnemonic) { + validateStringField(warnings, 'chain', wallet.chain) + validateStringField(warnings, 'mnemonic', wallet.mnemonic) + validateStringField(warnings, 'xpub', wallet.xpub) + validateBooleanField(warnings, 'testnet', wallet.testnet) + return WalletType.MNEMONIC + } else if (wallet.privateKey) { + validateStringField(warnings, 'chain', wallet.chain) + validateStringField(warnings, 'privateKey', wallet.privateKey) + validateBooleanField(warnings, 'testnet', wallet.testnet) + return WalletType.PRIVATE_KEY + } else if (wallet.secret) { + validateStringField(warnings, 'chain', wallet.chain) + validateStringField(warnings, 'secret', wallet.secret) + validateBooleanField(warnings, 'testnet', wallet.testnet) + return WalletType.SECRET + } else { + warnings.push('Wallet type is not recognized. Mnemonic, privateKey or secret are absent') + return WalletType.OTHER + } +} + +const validateNodeVersion = (systemWarnings: string[]) => { + const nodeVersion = process.version + if (semver.lt(nodeVersion, '18.0.0')) { + systemWarnings.push(`Node version is lower than v18.x.x. Current version is: ${nodeVersion}`) + } + return nodeVersion +} + +const validateStringField = (warnings: string[], fieldName: string, value: unknown) => { + if (!_.isString(value)) { + warnings.push(`Field '${fieldName}' is not string. Its type is: ${typeof value}`) + } +} + +const validateBooleanField = (warnings: string[], fieldName: string, value: unknown) => { + if (!_.isBoolean(value)) { + warnings.push(`Field '${fieldName}' is not boolean. Its type is: ${typeof value}`) + } } export const setTatumKey = (apiKey: string) => { @@ -462,3 +543,11 @@ export const getQuestion = (q: string, e?: string) => { hideEchoBack: true, }) } + +const getKmsVersion = (): string => { + return process.env.npm_package_version ?? 'N/A' +} + +const getPathToWallet = (path?: string) => { + return path || homedir() + '/.tatumrc/wallet.dat' +} diff --git a/src/signatures.ts b/src/signatures.ts index 61fe00c..883bf49 100644 --- a/src/signatures.ts +++ b/src/signatures.ts @@ -69,7 +69,12 @@ const getPrivateKeys = async (wallets: Wallet[]): Promise => { ) } - return [...new Set(keys)] + const result = [...new Set(keys)] + if (result.filter(key => !_.isString(key)).length > 0) { + console.error(`${new Date().toISOString()} - Some of private keys for transaction have incorrect format`) + } + + return result } function validatePrivateKeyWasFound(wallet: any, blockchainSignature: TransactionKMS, privateKey: string | undefined) { @@ -120,7 +125,10 @@ const processTransaction = async ( const wallets = [] for (const hash of blockchainSignature.hashes) { - wallets.push(await getWallet(hash, pwd, path, false)) + const wallet = await getWallet(hash, pwd, path, false) + if (wallet) { + wallets.push(wallet) + } } const signatures = blockchainSignature.signatures ?? [] @@ -224,8 +232,8 @@ const processTransaction = async ( if (blockchainSignature.withdrawalId) { txData = await signEthOffchainKMSTransaction(blockchainSignature, privateKey, testnet) } else { - const signKMSTransaction = await signEthKMSTransaction(blockchainSignature, privateKey); - const debugMode = Config.getValue(ConfigOption.TATUM_KMS_DEBUG_MODE) || 0; + const signKMSTransaction = await signEthKMSTransaction(blockchainSignature, privateKey) + const debugMode = Config.getValue(ConfigOption.TATUM_KMS_DEBUG_MODE) || 0 if (debugMode === 'true' || debugMode === '1') { console.log('signEthKMSTransaction data', signKMSTransaction, blockchainSignature.id) } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..2bff0d9 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,32 @@ +export const utils = { + csvToArray: (csv: string): string[] => { + if (!csv) return [] + return csv.split(',').map(value => value.trim()) + }, + hideValue: (value: string | undefined, showStart = 6, showEnd = 6): string => { + if (!value) { + return '' + } + const length = value.length + if (length <= showStart + showEnd) { + return '*'.repeat(length) + } + return ( + value.slice(0, showStart) + + '*'.repeat(length - showStart - showEnd) + + value.slice(length - 1 - showEnd, length - 1) + ) + }, + hidePassword: (password: string | undefined): string => { + if (!password) { + return 'N/A' + } + return utils.hideValue(password, 6, 0) + }, + hideApiKey: (secretValue: string | undefined): string => { + if (!secretValue) { + return 'N/A' + } + return utils.hideValue(secretValue) + }, +}