classes/MiniKeyConverter.js

'use strict';

require('../../core/setupAliases');
const logger = require('@core/logger');
const crypto = require('crypto');
const { sha256 } = require('@utils/hashUtils');
const { encodeBase58 } = require('@utils/base58');
const config = require('@config/config.json');

/**
 * Class for managing and converting Bitcoin MiniKeys.
 *
 * This class provides utilities for:
 * - Generating valid 30-character Bitcoin MiniKeys.
 * - Validating MiniKeys to ensure they conform to the correct format.
 * - Converting MiniKeys to private keys in HEX format.
 * - Converting private keys in HEX format to Wallet Import Format (WIF).
 *
 * **Important:** MiniKeys cannot directly generate WIF keys. They must first be converted
 * to HEX format using the `miniToHex` method, and only then can they be converted to WIF using `hexToWif`.
 *
 * @example
 * const converter = new MiniKeyConverter();
 *
 * // Generate a new MiniKey
 * const newMiniKey = converter.generateMiniKey();
 * console.log(`Generated MiniKey: ${newMiniKey}`);
 *
 * // Validate a MiniKey
 * const isValid = converter.check('S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy');
 * if (isValid) {
 *   console.log('Valid MiniKey!');
 * }
 *
 * // Convert MiniKey to HEX
 * const hexKey = converter.miniToHex('S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy');
 * console.log(`HEX Key: ${hexKey}`);
 *
 * // Convert HEX to WIF
 * const wifKey = converter.hexToWif(hexKey, false, false);
 * console.log(`WIF Key: ${wifKey}`);
 */
class MiniKeyConverter {
  /**
   * Generates a valid 30-character MiniKey.
   *
   * The MiniKey will always start with 'S' and will contain 29 additional Base58 characters.
   * It ensures the generated MiniKey passes validation checks before returning it.
   *
   * @returns {string} A valid 30-character MiniKey.
   * @example
   * const newMiniKey = converter.generateMiniKey();
   * console.log(`Generated MiniKey: ${newMiniKey}`);
   */
  generateMiniKey() {
    let minikey;
    do {
      minikey = 'S' + crypto.randomBytes(32).toString('base64')
        .replace(/[^1-9A-HJ-NP-Za-km-z]/g, '')
        .slice(0, 29);
    } while (!this.check(minikey, true));
    return minikey;
  }

  /**
   * Validates a MiniKey to ensure it conforms to the correct format.
   *
   * A valid MiniKey:
   * - Starts with 'S'.
   * - Contains only Base58 characters.
   * - Is either 22 or 30 characters long (or only 30 if strictMode is enabled).
   * - Has a SHA-256 hash (appended with '?') that starts with 0x00.
   *
   * @param {string} miniKey - The MiniKey to validate.
   * @param {boolean} [strictMode=false] - Enforce strict validation, allowing only 30-character MiniKeys.
   * @returns {boolean} True if the MiniKey is valid, false otherwise.
   * @example
   * const isValid = converter.check('S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy');
   * console.log(`Is valid: ${isValid}`);
   */
  check(miniKey, strictMode = false) {
    logger.debug({
      method: 'check',
      input: miniKey,
      strictMode,
      message: `Validating MiniKey`
    });

    if (!miniKey || typeof miniKey !== 'string') {
      logger.warn({
        method: 'check',
        message: 'Invalid MiniKey: Must be a non-empty string.'
      });
      return false;
    }

    const validLengths = strictMode ? [30] : [22, 30];
    if (!validLengths.includes(miniKey.length)) {
      logger.warn({
        method: 'check',
        message: `Invalid MiniKey length: ${miniKey.length}. Allowed lengths: ${validLengths.join(', ')}.`
      });
      return false;
    }

    if (!/^[S][1-9A-HJ-NP-Za-km-z]+$/.test(miniKey)) {
      logger.warn({
        method: 'check',
        message: 'Invalid MiniKey format: Must start with "S" and contain only Base58 characters.'
      });
      return false;
    }

    const isValid = /^00[0-9A-F]{62}$/.test(sha256(`${miniKey}?`).toString('hex').toUpperCase());
    logger.debug({
      method: 'check',
      input: miniKey,
      result: isValid,
      message: `MiniKey validation result`
    });

    return isValid;
  }

  /**
   * Converts a MiniKey into its corresponding private key in HEX format.
   *
   * @param {string} key - The MiniKey to convert.
   * @returns {string} The private key in HEX format.
   * @example
   * const hexKey = converter.miniToHex('S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy');
   * console.log(`HEX Key: ${hexKey}`);
   */
  miniToHex(key) {
    logger.debug({
      method: 'miniToHex',
      input: key,
      message: `Converting MiniKey to HEX`
    });
    const hexKey = sha256(key).toString('hex').toUpperCase();
    logger.debug({
      method: 'miniToHex',
      result: hexKey,
      message: `Converted HEX private key`
    });
    return hexKey;
  }

  /**
   * Converts a private key in HEX format to Wallet Import Format (WIF).
   *
   * **Note:** The input key must be in HEX format, typically obtained by converting a MiniKey
   * using the `miniToHex` method.
   *
   * @param {string} key - The private key in HEX format (64 characters).
   * @param {boolean} [testnet=false] - Whether the key is for the Bitcoin testnet.
   * @param {boolean} [compressed=false] - Whether the key is compressed.
   * @returns {string} The private key in Wallet Import Format (WIF).
   * @example
   * const wifKey = converter.hexToWif(hexKey, false, false);
   * console.log(`WIF Key: ${wifKey}`);
   */
  hexToWif(key, testnet = false, compressed = false) {
    logger.debug({
      method: 'hexToWif',
      input: key,
      testnet,
      compressed,
      message: `Converting HEX to WIF`
    });

    if (!/^[0-9A-Fa-f]{64}$/.test(key)) {
      logger.error({
        method: 'hexToWif',
        message: `Invalid HEX private key format: ${key}`
      });
      throw new Error('Invalid hexadecimal key format');
    }

    try {
      const prefix = testnet ? config.networks.testnet : config.networks.mainnet;
      let extendedKey = Buffer.from(`${prefix}${key}`, 'hex');
      if (compressed) {
        extendedKey = Buffer.concat([extendedKey, Buffer.from('01', 'hex')]);
      }

      const hash1 = sha256(extendedKey);
      const hash2 = sha256(hash1);
      const checksum = hash2.slice(0, 4);
      const fullKey = Buffer.concat([extendedKey, checksum]);
      const wif = encodeBase58(fullKey);

      logger.debug({
        method: 'hexToWif',
        result: wif,
        message: `Successfully converted to WIF`
      });
      return wif;
    } catch (error) {
      logger.error({
        method: 'hexToWif',
        message: `Error converting to WIF: ${error.message}`
      });
      throw error;
    }
  }

  /**
   * Verifies whether a WIF key matches a MiniKey.
   *
   * @param {string} miniKey - The Mini private key.
   * @param {string} wifKey - The WIF key to verify.
   * @param {boolean} [testnet=false] - Whether the WIF key is for testnet.
   * @param {boolean} [compressed=false] - Whether the WIF key is compressed.
   * @returns {boolean} - True if the WIF key matches the MiniKey, otherwise false.
   */
  verifyWifAgainstMiniKey(miniKey, wifKey, testnet = false, compressed = false) {
    try {
      // Convert MiniKey to HEX
      const hexKey = this.miniToHex(miniKey);

      // Generate the expected WIF key
      const expectedWifKey = this.hexToWif(hexKey, testnet, compressed);

      // Compare the expected WIF key with the provided one
      return expectedWifKey === wifKey;
    } catch (error) {
      logger.error(`Verification failed: ${error.message}`);
      return false;
    }
  }
}

module.exports = MiniKeyConverter;