/**
* @fileoverview Wallet Service for Satoshi Showdown.
* Provides functionalities for managing cryptocurrency wallets, specifically focusing on HD SegWit Bitcoin wallets.
* It includes operations such as wallet creation, retrieval, balance update, transaction association, and generating new addresses.
* This service is essential for financial transactions within the platform, ensuring secure and efficient management of wallets.
*
* @module services/walletService
* @requires models/walletModel - Wallet data model for database interactions.
* @requires utils/keyUtil - Utility functions for generating and handling Bitcoin keys.
* @requires utils/errorUtil - Custom error classes and error handling utilities.
* @requires utils/logUtil - Logging utility for application-wide logging.
*/
const Wallet = require("../models/walletModel");
const UTXO = require("../models/utxoModel");
const { createUTXO } = require("../services/utxoService");
const {
generateHDSegWitWalletWithSeed,
generateChildAddress,
} = require("../utils/keyUtil");
const { decryptPrivateKey } = require("../utils/encryptionUtil");
const { NotFoundError } = require("../utils/errorUtil");
const log = require("../utils/logUtil");
const bitcoin = require("bitcoinjs-lib");
const network = bitcoin.networks.testnet;
const ecPairFactory = require("ecpair").default;
const { BIP32Factory } = require("bip32");
const ecc = require("tiny-secp256k1");
// Initialize bip32 with tiny-secp256k1
const bip32 = BIP32Factory(ecc);
// Initialize ECPair factory with tiny-secp256k1
const ecPair = ecPairFactory(ecc);
/**
* Creates a new HD SegWit wallet for an event, including the seed.
*
* @async
* @function createHDSegWitWalletForEvent
* @return {Promise<Object>} The created wallet object, including master public key, encrypted master private key, and encrypted seed.
* @throws {Error} If there's an issue in wallet generation or saving the wallet to the database.
*/
const createHDSegWitWalletForEvent = async () => {
const {
masterPublicKey,
encryptedMasterPrivateKey,
encryptedSeed,
derivationPath,
} = generateHDSegWitWalletWithSeed();
console.log("Master Public Key:", masterPublicKey);
console.log("Encrypted Master Private Key:", encryptedMasterPrivateKey);
console.log("Encrypted Seed:", encryptedSeed);
console.log("Derivation Path:", derivationPath);
const wallet = new Wallet({
walletType: "HD-SegWit",
masterPublicKey,
encryptedMasterPrivateKey,
encryptedSeed,
derivationPath,
});
// Generate the initial address and path for the wallet
const initialAddressData = await generateChildAddressForWallet(
masterPublicKey,
0,
);
// Add the initial address and path to the addresses array
wallet.addresses.push(initialAddressData);
try {
await wallet.save();
log.info(`HD Wallet created for event`);
return wallet;
} catch (err) {
log.error(`Error in createHDSegWitWalletForEvent: ${err.message}`);
throw err;
}
};
/**
* Generates a new child address from a given HD wallet's master public key.
*
* @async
* @function generateChildAddressForWallet
* @param {string} masterPublicKey - The master public key of the HD wallet.
* @param {string} path - The derivation path for the child address.
* @return {Promise<Object>} An object containing the child address and path.
* @throws {Error} If there's an issue in address generation.
*/
const generateChildAddressForWallet = async (masterPublicKey, path) => {
try {
const { address, path: derivedPath } = generateChildAddress(
masterPublicKey,
path,
);
log.info(`Child address generated at path ${derivedPath}`);
return { address, path: derivedPath };
} catch (err) {
log.error(`Error in generateChildAddressForWallet: ${err.message}`);
throw err;
}
};
/**
* Creates a raw Bitcoin transaction using UTXOs, target address, amount, change address, and a pre-calculated transaction fee.
* It signs the transaction inputs with the corresponding decrypted private keys from the wallets and includes the necessary witness scripts for SegWit transactions.
*
* @async
* @function createRawBitcoinTransaction
* @param {Array} selectedUTXOs - Array of selected UTXO objects for the transaction.
* @param {string} toAddress - The Bitcoin address to send the amount to.
* @param {number} amountToSend - The amount to send in satoshis.
* @param {string} changeAddress - The address where the change will be sent.
* @param {number} transactionFee - The pre-calculated transaction fee in satoshis.
* @return {Promise<string>} A promise that resolves to the raw transaction hex string.
* @throws {Error} Throws an error if transaction creation fails.
*/
const createRawBitcoinTransaction = async (
selectedUTXOs,
toAddress,
amountToSend,
changeAddress,
transactionFee,
) => {
try {
const psbt = new bitcoin.Psbt({ network: network });
// Add inputs with necessary UTXO details
for (const utxo of selectedUTXOs) {
console.log(utxo);
const wallet = await Wallet.findOne({
"addresses.address": utxo.address,
});
if (!wallet) {
throw new Error(`Wallet not found for address ${utxo.address}`);
}
const decryptedMasterPrivateKey = decryptPrivateKey(
wallet.encryptedMasterPrivateKey,
);
const node = bip32.fromBase58(decryptedMasterPrivateKey, network);
const child = node.derivePath(utxo.keyPath);
const pubkey = child.publicKey;
const p2wpkh = bitcoin.payments.p2wpkh({ pubkey, network });
psbt.addInput({
hash: utxo.transactionHash,
index: utxo.outputIndex,
witnessUtxo: {
script: Buffer.from(utxo.scriptPubKey, "hex"),
value: utxo.amount,
},
});
console.log(psbt.data.inputs);
}
// Add outputs for toAddress and changeAddress
psbt.addOutput({
address: toAddress,
value: amountToSend,
});
const totalAmount = selectedUTXOs.reduce(
(sum, utxo) => sum + utxo.amount,
0,
);
const changeAmount = totalAmount - amountToSend - transactionFee;
if (changeAmount > 0) {
psbt.addOutput({ address: changeAddress, value: changeAmount });
}
console.log("totalAmount:", totalAmount);
console.log("changeAmount:", changeAmount);
// Sign each input
for (const [index, utxo] of selectedUTXOs.entries()) {
const wallet = await Wallet.findOne({
"addresses.address": utxo.address,
});
const decryptedMasterPrivateKey = decryptPrivateKey(
wallet.encryptedMasterPrivateKey,
);
const node = bip32.fromBase58(decryptedMasterPrivateKey, network);
const child = node.derivePath(utxo.keyPath);
const keyPair = ecPair.fromPrivateKey(child.privateKey, {
network: network,
});
psbt.signInput(index, keyPair);
}
console.log(psbt.data.inputs);
console.log("OUTPUTS:", psbt.data.outputs);
// Finalize and extract transaction
psbt.finalizeAllInputs();
const transaction = psbt.extractTransaction();
return transaction.toHex();
} catch (error) {
throw new Error(
`Error in creating raw Bitcoin transaction: ${error.message}`,
);
}
};
/**
* Retrieves a wallet by its MongoDB ObjectId from the database.
* This function is essential for operations that require wallet details based on its unique ID.
*
* @async
* @function getWalletById
* @param {string} walletId - The MongoDB ObjectId of the wallet to retrieve.
* @return {Promise<Wallet>} A promise that resolves to the wallet object.
* @throws {NotFoundError} Thrown if no wallet is found with the given ID.
*/
const getWalletById = async (walletId) => {
try {
const wallet = await Wallet.findById(walletId);
if (!wallet) {
throw new NotFoundError(`Wallet with ID ${walletId} not found`);
}
return wallet;
} catch (err) {
log.error(`Error in getWalletById: ${err.message}`);
throw err;
}
};
/**
* Retrieves a wallet by its public address from the database.
* This function is vital for various operations such as validating wallet existence,
* conducting transactions, and querying wallet balance. It is a key component in ensuring
* that operations involving a particular wallet address are accurately processed.
*
* @async
* @function getWalletByAddress
* @param {string} address - The public address of the wallet to be retrieved.
* @return {Promise<Object>} The wallet object corresponding to the provided public address.
* @throws {NotFoundError} Thrown if no wallet is found with the given public address.
* @throws {Error} Thrown if there is an issue with the database query.
*/
const getWalletByAddress = async (address) => {
try {
const wallet = await Wallet.findOne({ publicAddress: address });
if (!wallet) {
throw new Error(`Wallet with address ${address} not found`);
}
return wallet;
} catch (err) {
log.error(`Error in getWalletByAddress: ${err.message}`);
throw err;
}
};
/**
* Updates the balance of an existing wallet record in the database by its MongoDB reference ID.
* This function focuses on updating only the confirmed and unconfirmed balances of the wallet.
* It retrieves the existing wallet record and applies balance updates as necessary.
*
* @async
* @function updateWalletBalanceById
* @param {string} walletId - The MongoDB reference ID of the wallet to update.
* @param {Object} updateData - An object containing the new balance data for the wallet.
* @return {Promise<Object>} A promise that resolves to the updated wallet object.
* @throws {NotFoundError} Thrown if the wallet with the specified ID is not found in the database.
* @throws {Error} Thrown if there is an error during the update process.
*/
const updateWalletBalanceById = async (walletId, updateData) => {
try {
const existingWallet = await Wallet.findById(walletId);
if (!existingWallet) {
throw new NotFoundError(`Wallet with ID ${walletId} not found`);
}
// Calculate potential new balances
const updatedConfirmedBalance =
existingWallet.confirmedBalance +
(updateData.confirmedIncrement || 0) -
(updateData.confirmedDecrement || 0);
const updatedUnconfirmedBalance =
existingWallet.unconfirmedBalance +
(updateData.unconfirmedIncrement || 0) -
(updateData.unconfirmedDecrement || 0);
// Check if there are actual changes to apply
if (
existingWallet.confirmedBalance !== updatedConfirmedBalance ||
existingWallet.unconfirmedBalance !== updatedUnconfirmedBalance
) {
const updatesToApply = {
confirmedBalance: updatedConfirmedBalance,
unconfirmedBalance: updatedUnconfirmedBalance,
};
const updatedWallet = await Wallet.findByIdAndUpdate(
walletId,
updatesToApply,
{ new: true },
);
log.info(`Wallet balance with ID ${walletId} updated`);
return updatedWallet;
} else {
log.info(`No changes to update for wallet balance with ID ${walletId}`);
return existingWallet;
}
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
throw new Error(`Error updating wallet balance: ${error.message}`);
}
};
/**
* Adds a UTXO reference to a wallet.
* This function is essential for updating the wallet's transaction history and balance.
*
* @async
* @function addUTXOToWallet
* @param {string} walletRef - The MongoDB ObjectId of the wallet.
* @param {string} utxoId - The ID of the UTXO to add to the wallet.
* @return {Promise<Object>} The updated wallet object with the new UTXO reference.
* @throws {NotFoundError} Thrown if the wallet or UTXO is not found.
* @throws {Error} Thrown if there is an issue updating the wallet.
*/
const addUTXOToWallet = async (walletRef, utxoData) => {
try {
// Retrieve the wallet by its MongoDB ObjectId
const wallet = await Wallet.findById(walletRef);
if (!wallet) {
throw new NotFoundError(`Wallet with ID ${walletRef} not found`);
}
// Find the address data within the wallet to get the keyPath
const addressData = wallet.addresses.find(
(a) => a.address === utxoData.address,
);
if (!addressData) {
throw new Error(`Address data not found for ${utxoData.address}`);
}
// Add keyPath to the UTXO data
utxoData.keyPath = addressData.path;
// Create UTXO with complete data
const utxo = await createUTXO(utxoData);
// Check if UTXO already exists in the wallet to avoid duplicates
if (!wallet.utxoRefs.includes(utxo._id)) {
// Add the UTXO reference to the wallet's UTXO references array
wallet.utxoRefs.push(utxo._id);
await wallet.save();
log.info(`UTXO with ID ${utxo._id} added to wallet with ID ${walletRef}`);
} else {
log.info(
`UTXO with ID ${utxo._id} already exists in wallet with ID ${walletRef}`,
);
}
return wallet;
} catch (err) {
log.error(`Error in addUTXOToWallet: ${err.message}`);
throw err;
}
};
/**
* Creates a raw Bitcoin transaction with an accurately calculated fee.
*
* @async
* @function createAccurateFeeBitcoinTransaction
* @param {Array} selectedUTXOs - Array of UTXOs to be used in the transaction.
* @param {string} toAddress - The Bitcoin address to send the amount to.
* @param {number} amountToSend - The amount to send in satoshis.
* @param {string} changeAddress - The address where the change will be sent.
* @param {number} feeRate - The fee rate in satoshis per byte.
* @return {Promise<string>} - A promise that resolves to the raw transaction hex string.
* @throws {Error} - Throws an error if transaction creation fails.
*/
const createAccurateFeeBitcoinTransaction = async (
selectedUTXOs,
toAddress,
amountToSend,
changeAddress,
feeRate,
) => {
try {
const psbt = new bitcoin.Psbt({ network: network });
// Add inputs
selectedUTXOs.forEach((utxo) => {
psbt.addInput({
hash: utxo.transactionHash,
index: utxo.outputIndex,
witnessUtxo: {
script: Buffer.from(utxo.scriptPubKey, "hex"),
value: utxo.amount,
},
});
});
// Add outputs (initially without considering the transaction fee)
psbt.addOutput({ address: toAddress, value: amountToSend });
// Total amount from all UTXOs
const totalInputAmount = selectedUTXOs.reduce(
(acc, utxo) => acc + utxo.amount,
0,
);
// Adding a dummy output for change to calculate the transaction size
psbt.addOutput({
address: changeAddress,
value: totalInputAmount - amountToSend,
});
// Calculate the size of the transaction to estimate the fee
const virtualSize = psbt.__CACHE.__TX.virtualSize();
const estimatedFee = Math.ceil(virtualSize * feeRate);
// Recalculate and update the change output considering the estimated fee
const changeValue = totalInputAmount - amountToSend - estimatedFee;
psbt.updateOutput(1, { address: changeAddress, value: changeValue });
// Sign each input
selectedUTXOs.forEach((utxo, index) => {
const keyPair = ecPair.fromWIF(utxo.privateKeyWIF, network);
psbt.signInput(index, keyPair);
});
// Finalize and extract the transaction
psbt.finalizeAllInputs();
const transaction = psbt.extractTransaction();
return transaction.toHex();
} catch (error) {
throw new Error(
`Error in creating accurate fee Bitcoin transaction: ${error.message}`,
);
}
};
module.exports = { createAccurateFeeBitcoinTransaction };
module.exports = {
createHDSegWitWalletForEvent,
createRawBitcoinTransaction,
getWalletById,
getWalletByAddress,
updateWalletBalanceById,
createHDSegWitWalletForEvent,
generateChildAddressForWallet,
addUTXOToWallet,
};