diff --git a/.env.example b/.env.example index 2178d94..8265cba 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,5 @@ DESTINATION_ADDR= NETWORK= DERIVATION_PATH= N= -BLOCKCYPHER_API_KEY= \ No newline at end of file +BLOCKCYPHER_API_KEY= +ASSERT_UNSPENT_TRANSACTION_HASH_CONFIRMATION_IS_VALID=False \ No newline at end of file diff --git a/index.js b/index.js index 894ffa4..1f9f615 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,9 @@ const privkey2 = process.env.PRIVATE_KEY2; const toAddr = process.env.DESTINATION_ADDR; const network = process.env.NETWORK; const derivationPath = process.env.DERIVATION_PATH; +const ASSERT_UNSPENT_TRANSACTION_HASH_CONFIRMATION_IS_VALID = + `${process.env.ASSERT_UNSPENT_TRANSACTION_HASH_CONFIRMATION_IS_VALID}`.toUpperCase() == + "TRUE"; console.log({ n }); @@ -19,15 +22,28 @@ if (!n || !bip32 || !privkey2 || !toAddr || !network || !derivationPath) { let sweep; -if (network == constants.NETWORKS.BTC || network == constants.NETWORKS.BTCTEST) { - sweep = new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath); +if ( + network == constants.NETWORKS.BTC || + network == constants.NETWORKS.BTCTEST +) { + sweep = new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath, { + revalidate_txhash_status: + ASSERT_UNSPENT_TRANSACTION_HASH_CONFIRMATION_IS_VALID, + }); } else { /** ASSERTS THAT BLOCKCYPHER API KEY/TOKEN IS AVAILABLE */ if (!process.env.BLOCKCYPHER_API_KEY) { - console.error("Consider adding 'BLOCKCYPHER_API_KEY' to your .env file. visit: https://accounts.blockcypher.com/signup"); + console.error( + "Consider adding 'BLOCKCYPHER_API_KEY' to your .env file. visit: https://accounts.blockcypher.com/signup" + ); process.exit(0); } - sweep = new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath, { provider: constants.PROVIDERS.BLOCKCYPHER, key: process.env.BLOCKCYPHER_API_KEY }); + sweep = new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath, { + provider: constants.PROVIDERS.BLOCKCYPHER, + key: process.env.BLOCKCYPHER_API_KEY, + revalidate_txhash_status: + ASSERT_UNSPENT_TRANSACTION_HASH_CONFIRMATION_IS_VALID, + }); } Sweep(); diff --git a/src/services/ProviderService.js b/src/services/ProviderService.js index 445f556..8f97582 100644 --- a/src/services/ProviderService.js +++ b/src/services/ProviderService.js @@ -2,7 +2,12 @@ const constants = require("../constants"); const fetch = require("node-fetch"); const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -const ProviderService = function (provider, network, apiKey = null) { +const ProviderService = function ( + provider, + network, + apiKey = null, + revalidate_txhash_status = false +) { const providerIndex = Object.values(constants.PROVIDERS).indexOf(provider); if (providerIndex < 0) { throw new Error("Blockchain provider not supported"); @@ -14,13 +19,19 @@ const ProviderService = function (provider, network, apiKey = null) { this.network = network; this.provider = provider; this.apiKey = apiKey; + this.revalidate_txhash_status = revalidate_txhash_status; }; ProviderService.prototype.getTxHex = async function (txId) { try { switch (this.provider) { case constants.PROVIDERS.SOCHAIN: { - const apiUrl = [constants.PROVIDER_URLS.SOCHAIN.URL, "get_tx", this.network, txId].join("/"); + const apiUrl = [ + constants.PROVIDER_URLS.SOCHAIN.URL, + "get_tx", + this.network, + txId, + ].join("/"); const res = await fetchUrl(apiUrl); const json = await res.json(); if (json.status === "fail") { @@ -29,8 +40,15 @@ ProviderService.prototype.getTxHex = async function (txId) { return json.data.tx_hex; } case constants.PROVIDERS.MEMPOOLSPACE: { - const networkType = this.network === constants.NETWORKS.BTC ? "api" : "testnet/api"; - const apiUrl = [constants.PROVIDER_URLS.MEMPOOLSPACE.URL, networkType, "tx", txId, "hex"].join("/"); + const networkType = + this.network === constants.NETWORKS.BTC ? "api" : "testnet/api"; + const apiUrl = [ + constants.PROVIDER_URLS.MEMPOOLSPACE.URL, + networkType, + "tx", + txId, + "hex", + ].join("/"); const res = await fetchUrl(apiUrl); const hex = await res.text(); if (res.status !== 200) { @@ -39,7 +57,12 @@ ProviderService.prototype.getTxHex = async function (txId) { return hex; } case constants.PROVIDERS.BLOCKCHAINCOM: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, "rawtx", txId, "?format=hex"].join("/"); + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, + "rawtx", + txId, + "?format=hex", + ].join("/"); const res = await fetchUrl(apiUrl); const hex = await res.text(); if (res.status !== 200) { @@ -49,7 +72,13 @@ ProviderService.prototype.getTxHex = async function (txId) { } case constants.PROVIDERS.BLOCKCYPHER: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCYPHER.URL, this.network.toLowerCase(), "main/txs", txId, `?includeHex=true&token=${this.apiKey}`].join("/"); + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCYPHER.URL, + this.network.toLowerCase(), + "main/txs", + txId, + `?includeHex=true&token=${this.apiKey}`, + ].join("/"); const res = await fetchUrl(apiUrl); const json = await res.json(); if (!json.hex) { @@ -71,7 +100,12 @@ ProviderService.prototype.getUtxo = async function (addr) { try { switch (this.provider) { case constants.PROVIDERS.SOCHAIN: { - const apiUrl = [constants.PROVIDER_URLS.SOCHAIN.URL, "get_tx_unspent", this.network, addr].join("/"); + const apiUrl = [ + constants.PROVIDER_URLS.SOCHAIN.URL, + "get_tx_unspent", + this.network, + addr, + ].join("/"); const res = await fetchUrl(apiUrl); const json = await res.json(); if (json.status === "fail") { @@ -80,7 +114,10 @@ ProviderService.prototype.getUtxo = async function (addr) { return json.data.txs; } case constants.PROVIDERS.BLOCKCHAINCOM: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, "unspent?active=" + addr].join("/"); + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, + "unspent?active=" + addr, + ].join("/"); const res = await fetchUrl(apiUrl); const json = await res.json(); if (json.error) { @@ -89,9 +126,12 @@ ProviderService.prototype.getUtxo = async function (addr) { return json.unspent_outputs; } case constants.PROVIDERS.BLOCKCYPHER: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCYPHER.URL, this.network.toLowerCase(), "main/addrs", addr + `?unspentOnly=true&includeScript=true&token=${this.apiKey}`].join( - "/" - ); + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCYPHER.URL, + this.network.toLowerCase(), + "main/addrs", + addr + `?unspentOnly=true&includeScript=true&token=${this.apiKey}`, + ].join("/"); const res = await fetchUrl(apiUrl); const json = await res.json(); if (json.error) { @@ -108,19 +148,100 @@ ProviderService.prototype.getUtxo = async function (addr) { } }; +ProviderService.prototype.getTransactionComfirmationStatus = async function ( + txHash +) { + const MINIMUM_CONFIRMATIONS_REQUIRED = 3; + try { + switch (this.provider) { + case constants.PROVIDERS.BLOCKCHAINCOM: { + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, + "rawtx", + txHash, + ].join("/"); + const res = await fetchUrl(apiUrl); + const json = await res.json(); + if (json.error) { + throw new Error(json.message); + } + return json.block_height && json.block_index + ? { confirmations: MINIMUM_CONFIRMATIONS_REQUIRED } + : { confirmations: 0 }; + } + + case constants.PROVIDERS.MEMPOOLSPACE: { + const apiUrl = [ + constants.PROVIDER_URLS.MEMPOOLSPACE.URL, + "api/tx", + txHash, + ].join("/"); + const res = await fetchUrl(apiUrl); + const json = await res.json(); + if (json.error) { + throw new Error(json.message); + } + return json?.status.confirmed && + json?.status.block_height && + json?.status.block_hash + ? { confirmations: MINIMUM_CONFIRMATIONS_REQUIRED } + : { confirmations: 0 }; + } + case constants.PROVIDERS.BLOCKCYPHER: { + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCYPHER.URL, + this.network.toLowerCase(), + "main/txs", + txHash + `?token=${this.apiKey}`, + ].join("/"); + const res = await fetchUrl(apiUrl); + const json = await res.json(); + if (json.error) { + throw new Error(json.message); + } + return json.confirmations + ? { confirmations: json.confirmations } + : { confirmations: 0 }; + } + default: { + throw new Error( + "Error getting transaction status with provider: " + this.provider + ); + } + } + } catch (err) { + throw new Error(err); + } +}; + ProviderService.prototype.sendTx = async function (txHex) { try { switch (this.provider) { case constants.PROVIDERS.SOCHAIN: { - const apiUrl = [constants.PROVIDER_URLS.SOCHAIN.URL, "send_tx", this.network].join("/"); + const apiUrl = [ + constants.PROVIDER_URLS.SOCHAIN.URL, + "send_tx", + this.network, + ].join("/"); await broadcastTx(apiUrl, this.network.toLowerCase(), txHex); return; } case constants.PROVIDERS.BLOCKCHAINCOM: case constants.PROVIDERS.BLOCKCYPHER: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCYPHER.URL, this.network.toLowerCase(), "main", "txs", `push?token=${this.apiKey}`].join("/"); - await broadcastTx(apiUrl, this.network.toLowerCase(), txHex, this.apiKey); + const apiUrl = [ + constants.PROVIDER_URLS.BLOCKCYPHER.URL, + this.network.toLowerCase(), + "main", + "txs", + `push?token=${this.apiKey}`, + ].join("/"); + await broadcastTx( + apiUrl, + this.network.toLowerCase(), + txHex, + this.apiKey + ); return; } default: { @@ -140,7 +261,9 @@ async function fetchUrl(url) { if (response.ok) { return response; } else { - console.log(" -- retrying in 10 seconds due to status = " + response.status); + console.log( + " -- retrying in 10 seconds due to status = " + response.status + ); await delay(10000); return await fetchUrl(url); } @@ -152,7 +275,16 @@ async function fetchUrl(url) { async function broadcastTx(apiUrl, network, txHex, apiKey = null) { try { let res; - if (apiUrl == [constants.PROVIDER_URLS.BLOCKCYPHER.URL, network, "main", "txs", `push?token=${apiKey}`].join("/")) { + if ( + apiUrl == + [ + constants.PROVIDER_URLS.BLOCKCYPHER.URL, + network, + "main", + "txs", + `push?token=${apiKey}`, + ].join("/") + ) { res = await fetch(apiUrl, { method: "POST", body: JSON.stringify({ tx: txHex }), diff --git a/src/sweeper.js b/src/sweeper.js index 660ead8..fc37a91 100644 --- a/src/sweeper.js +++ b/src/sweeper.js @@ -25,7 +25,15 @@ let initialstring = JSON.stringify({ fs.writeFile(jsonPath, initialstring, "utf8", () => console.log()); /** */ -function BlockIoSweep(network, bip32_private_key_1, private_key_2, destination_address, n, derivation_path, options) { +function BlockIoSweep( + network, + bip32_private_key_1, + private_key_2, + destination_address, + n, + derivation_path, + options +) { // TODO perform error checking on all these inputs this.network = network; this.networkObj = networks[network]; @@ -36,25 +44,37 @@ function BlockIoSweep(network, bip32_private_key_1, private_key_2, destination_a this.n = n || BlockIoSweep.DEFAULT_N; if (options && typeof options === "object") { - this.provider = options.provider || BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER; + this.provider = + options.provider || BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER; this.feeRate = options.feeRate || BlockIoSweep.DEFAULT_FEE_RATE[network]; - this.maxTxInputs = options.maxTxInputs || BlockIoSweep.DEFAULT_MAX_TX_INPUTS; + this.maxTxInputs = + options.maxTxInputs || BlockIoSweep.DEFAULT_MAX_TX_INPUTS; this.apiKey = options?.key; + this.revalidate_txhash_status = options.revalidate_txhash_status; } else if (network == constants.NETWORKS.BTC) { this.provider = BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER; this.feeRate = BlockIoSweep.DEFAULT_FEE_RATE[network]; this.maxTxInputs = BlockIoSweep.DEFAULT_MAX_TX_INPUTS; this.apiKey = null; } else { - throw Error(`option must be set for ${network} network i.e new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath, option);`); + throw Error( + `option must be set for ${network} network i.e new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath, option);` + ); } - this.providerService = new ProviderService(this.provider, this.network, this.apiKey); + this.providerService = new ProviderService( + this.provider, + this.network, + this.apiKey, + this.revalidate_txhash_status + ); } // set defaults from constants BlockIoSweep.DEFAULT_N = parseInt(constants.N); -BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER = constants.BLOCKCHAIN_PROVIDER_DEFAULT; -BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER_API_URL = constants.BLOCKCHAIN_PROVIDER_URL_DEFAULT; +BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER = + constants.BLOCKCHAIN_PROVIDER_DEFAULT; +BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER_API_URL = + constants.BLOCKCHAIN_PROVIDER_URL_DEFAULT; BlockIoSweep.DEFAULT_FEE_RATE = constants.FEE_RATE; BlockIoSweep.DEFAULT_MAX_TX_INPUTS = constants.MAX_TX_INPUTS; @@ -74,7 +94,9 @@ BlockIoSweep.prototype.begin = async function () { this.network !== constants.NETWORKS.DOGE && this.network !== constants.NETWORKS.DOGETEST ) { - throw new Error("Must specify a valid network. Valid values are: BTC, LTC, DOGE, BTCTEST, LTCTEST, DOGETEST"); + throw new Error( + "Must specify a valid network. Valid values are: BTC, LTC, DOGE, BTCTEST, LTCTEST, DOGETEST" + ); } if (!this.bip32PrivKey || !this.privateKey2) { @@ -96,16 +118,29 @@ BlockIoSweep.prototype.begin = async function () { try { // get the public key from the user-specified private key - const publicKey2 = ecpair.ECPair.fromWIF(this.privateKey2, this.networkObj).publicKey.toString("hex"); + const publicKey2 = ecpair.ECPair.fromWIF( + this.privateKey2, + this.networkObj + ).publicKey.toString("hex"); // generate addresses for the N paths and initiate a utxo - const utxoMap = await createBalanceMap(this.n, this.bip32PrivKey, publicKey2, this.networkObj, this.network, this.derivationPath, this.providerService); + const utxoMap = await createBalanceMap( + this.n, + this.bip32PrivKey, + publicKey2, + this.networkObj, + this.network, + this.derivationPath, + this.providerService + ); const txs = []; let psbt = new bitcoin.Psbt({ network: this.networkObj }); - const root = bip32.default(ecc).fromBase58(this.bip32PrivKey, this.networkObj); + const root = bip32 + .default(ecc) + .fromBase58(this.bip32PrivKey, this.networkObj); let ecKeys = {}; let balToSweep = 0; @@ -134,18 +169,33 @@ BlockIoSweep.prototype.begin = async function () { psbt.addInput(input); ecKeys[inputNum++] = key; - if (psbt.txInputs.length === this.maxTxInputs || (addrIte === addressCount && i === addrTxCount)) { + if ( + psbt.txInputs.length === this.maxTxInputs || + (addrIte === addressCount && i === addrTxCount) + ) { if (balToSweep <= constants.DUST[this.network]) { throw new Error("Amount less than dust being sent, tx aborted"); } // create the transaction without network fees const tempPsbt = psbt.clone(); - createAndFinalizeTx(tempPsbt, this.toAddr, balToSweep, 0, ecKeys, this.privateKey2, this.networkObj); + createAndFinalizeTx( + tempPsbt, + this.toAddr, + balToSweep, + 0, + ecKeys, + this.privateKey2, + this.networkObj + ); // we know the size of the transaction now, // calculate the network fee, and recreate the appropriate transaction - const networkFee = getNetworkFee(this.network, tempPsbt, this.feeRate); + const networkFee = getNetworkFee( + this.network, + tempPsbt, + this.feeRate + ); // I noticed that developers may want prefer to want to increase transaction fee for higher priority. const NETWORK_FEE_MULTIPLIER_EFFECT = 1; // Could be 1.2, 1.3, 1.5 etc. @@ -154,10 +204,20 @@ BlockIoSweep.prototype.begin = async function () { console.log({ "Transferrable balance": balToSweep }); if (NETWORK_FEE_MULTIPLIER_EFFECT > 1) { - console.warn({ message: `Please note you would be paying ${NETWORK_FEE_MULTIPLIER_EFFECT} above the calculated average` }); + console.warn({ + message: `Please note you would be paying ${NETWORK_FEE_MULTIPLIER_EFFECT} above the calculated average`, + }); } - createAndFinalizeTx(psbt, this.toAddr, balToSweep, networkFee * NETWORK_FEE_MULTIPLIER_EFFECT, ecKeys, this.privateKey2, this.networkObj); + createAndFinalizeTx( + psbt, + this.toAddr, + balToSweep, + networkFee * NETWORK_FEE_MULTIPLIER_EFFECT, + ecKeys, + this.privateKey2, + this.networkObj + ); if (psbt.getFee() > constants.NETWORK_FEE_MAX[this.network]) { throw new Error( @@ -173,7 +233,12 @@ BlockIoSweep.prototype.begin = async function () { // we'll show the network fee, the network fee rate, and the transaction hex for the user to independently verify before broadcast // we don't ask bitcoinjs to enforce the max fee rate here, we've already done it above ourselves - txs.push({ network_fee: psbt.getFee(), network_fee_rate: psbt.getFeeRate(), tx_hex: extracted_tx.toHex(), tx_size: extracted_tx.virtualSize() }); + txs.push({ + network_fee: psbt.getFee(), + network_fee_rate: psbt.getFeeRate(), + tx_hex: extracted_tx.toHex(), + tx_size: extracted_tx.virtualSize(), + }); psbt = new bitcoin.Psbt({ network: this.networkObj }); balToSweep = 0; @@ -185,7 +250,9 @@ BlockIoSweep.prototype.begin = async function () { } if (!txs.length) { - throw new Error("No transaction created, do your addresses have balance?"); + throw new Error( + "No transaction created, do your addresses have balance?" + ); } for (let i = 0; i < txs.length; i++) { @@ -196,7 +263,14 @@ BlockIoSweep.prototype.begin = async function () { console.log("Transaction Hex:", tx.tx_hex); console.log("Network Fee Rate:", tx.network_fee_rate, "sats/byte"); console.log("Transaction VSize:", tx.tx_size, "bytes"); - console.log("Network Fee:", tx.network_fee, "sats", "(max allowed:", constants.NETWORK_FEE_MAX[this.network], "sats)"); + console.log( + "Network Fee:", + tx.network_fee, + "sats", + "(max allowed:", + constants.NETWORK_FEE_MAX[this.network], + "sats)" + ); const ans = await promptConfirmation( "\n\n*** YOU MUST INDEPENDENTLY VERIFY THE NETWORK FEE IS APPROPRIATE AND THE TRANSACTION IS PROPERLY CONSTRUCTED. ***\n*** ONCE A TRANSACTION IS BROADCAST TO THE NETWORK, IT IS CONSIDERED IRREVERSIBLE ***\n\nIf you approve of this transaction and have verified its accuracy, type '" + @@ -219,7 +293,15 @@ BlockIoSweep.prototype.begin = async function () { module.exports = BlockIoSweep; -function createAndFinalizeTx(psbt, toAddr, balance, networkFee, ecKeys, privKey2, network) { +function createAndFinalizeTx( + psbt, + toAddr, + balance, + networkFee, + ecKeys, + privKey2, + network +) { // balance and network fee are in COIN const val = balance - networkFee; @@ -255,7 +337,9 @@ function getCoinValue(floatAsString, provider) { s[1] = "0"; } - const r = parseInt("" + s[0] + s[1] + constants.COIN.substr(1, 8 - s[1].length)); + const r = parseInt( + "" + s[0] + s[1] + constants.COIN.substr(1, 8 - s[1].length) + ); console.log(`${floatAsString} becomes ${r}`); @@ -270,7 +354,15 @@ function getCoinValue(floatAsString, provider) { } } -async function createBalanceMap(n, bip32Priv, pubKey, networkObj, network, derivationPath, providerService) { +async function createBalanceMap( + n, + bip32Priv, + pubKey, + networkObj, + network, + derivationPath, + providerService +) { // generates addresses for the N paths and retrieves their unspent outputs // returns balanceMap with all the appropriate data for creating and signing transactions @@ -279,71 +371,130 @@ async function createBalanceMap(n, bip32Priv, pubKey, networkObj, network, deriv for (let i = 0; i <= n; i++) { console.log("Evaluating addresses at i=" + i); - if (network !== constants.NETWORKS.DOGE && network !== constants.NETWORKS.DOGETEST) { + if ( + network !== constants.NETWORKS.DOGE && + network !== constants.NETWORKS.DOGETEST + ) { // Dogecoin only has P2SH addresses, so populate balanceMap with data for P2WSH-over-P2SH and P2WSH (Witness V0) addresses here - await addAddrToMap(balanceMap, constants.P2WSH_P2SH, i, bip32Priv, pubKey, networkObj, derivationPath, providerService); - await addAddrToMap(balanceMap, constants.P2WSH, i, bip32Priv, pubKey, networkObj, derivationPath, providerService); + await addAddrToMap( + balanceMap, + constants.P2WSH_P2SH, + i, + bip32Priv, + pubKey, + networkObj, + derivationPath, + providerService + ); + await addAddrToMap( + balanceMap, + constants.P2WSH, + i, + bip32Priv, + pubKey, + networkObj, + derivationPath, + providerService + ); } // populate balanceMap with data for P2SH address for any network - await addAddrToMap(balanceMap, constants.P2SH, i, bip32Priv, pubKey, networkObj, derivationPath, providerService); + await addAddrToMap( + balanceMap, + constants.P2SH, + i, + bip32Priv, + pubKey, + networkObj, + derivationPath, + providerService + ); } return balanceMap; } -async function addAddrToMap(balanceMap, addrType, i, bip32Priv, pubKey, networkObj, derivationPath, providerService) { - // generates addresses at i and returns the appropriate data (including utxo) - - const addresses = AddressService.generateAddresses(addrType, bip32Priv, pubKey, networkObj, i, derivationPath); +async function addAddrToMap( + balanceMap, + addrType, + i, + bip32Priv, + pubKey, + networkObj, + derivationPath, + providerService +) { + // Generates addresses at i and returns the appropriate data (including UTXO) + + const addresses = AddressService.generateAddresses( + addrType, + bip32Priv, + pubKey, + networkObj, + i, + derivationPath + ); for (let addressData of addresses) { const payment = addressData.payment; + console.log(`type=${addrType} address=${payment.address}`); - console.log("type=" + addrType + " address=" + payment.address); - - // prepare the object in balanceMap - balanceMap[payment.address] = {}; - balanceMap[payment.address].address_type = addrType; - balanceMap[payment.address].i = i; - balanceMap[payment.address].primaryKey = addressData.primaryKey; - balanceMap[payment.address].tx = []; + // Prepare the object in balanceMap + balanceMap[payment.address] = { + address_type: addrType, + i: i, + primaryKey: addressData.primaryKey, + tx: [], + }; - // get the unspent transactions for the derived address + // Get the unspent transactions for the derived address const addrUtxo = await providerService.getUtxo(payment.address); - let x; - - for (x of addrUtxo) { - const unspentObj = {}; - unspentObj.hash = x.tx_hash_big_endian || x.tx_hash; - unspentObj.index = x.tx_output_n; - unspentObj.value = x.value; - - // Keeps a Copy of all addresses with balance greater than 0 for recording purposes - - fs.readFile(jsonPath, "utf-8", function (err, data) { - if (err) { - console.log(err); - } else { - let obj = JSON.parse(data); - obj.table.push({ - address: payment.address, - balance: x.value, - }); - - let json = JSON.stringify(obj); - - fs.writeFile(jsonPath, json, "utf8", () => console.log("")); + for (let x of addrUtxo) { + const unspentObj = { + hash: x.tx_hash_big_endian || x.tx_hash, + index: x.tx_output_n, + value: x.value, + }; + + if (providerService.revalidate_txhash_status) { + // Verify the referenced transaction status + const referencedTxStatus = + await providerService.getTransactionComfirmationStatus( + unspentObj.hash + ); + if (!referencedTxStatus || referencedTxStatus.confirmations === 0) { + console.error( + `Transaction ${unspentObj.hash} is orphaned or missing.` + ); + // Log txHash && Input addresses for debugging + const ans = await promptConfirmation( + "\n\n*** INPUT 'continue' to skip, or 'break' to exit process: " + ); + //continue; + if (String(ans).toLocaleLowerCase() == "continue") { + continue; + } else { + console.log("\nTRANSACTION ABORTED\n"); + process.exit(); + } } - }); + } - switch (addrType) { - // handle different scripts for different address types here + // Keeps a copy of all addresses with balance greater than 0 for recording purposes + try { + const data = await fs.promises.readFile(jsonPath, "utf-8"); + let obj = JSON.parse(data); + obj.table.push({ address: payment.address, balance: x.value }); + const json = JSON.stringify(obj); + await fs.promises.writeFile(jsonPath, json, "utf8"); + } catch (err) { + console.error(err); + } + switch (addrType) { case constants.P2WSH_P2SH: // P2WSH-over-P2SH unspentObj.witnessUtxo = { - //script: Buffer.from(x.script_hex, "hex"), script: Buffer.from(x.script, "hex"), value: getCoinValue(x.value, providerService.provider), }; @@ -353,7 +504,6 @@ async function addAddrToMap(balanceMap, addrType, i, bip32Priv, pubKey, networkO case constants.P2WSH: // Native Segwit (v0) or Witness v0 unspentObj.witnessUtxo = { - //script: Buffer.from(x.script_hex, "hex"), script: Buffer.from(x.script, "hex"), value: getCoinValue(x.value, providerService.provider), }; @@ -361,7 +511,10 @@ async function addAddrToMap(balanceMap, addrType, i, bip32Priv, pubKey, networkO break; case constants.P2SH: // Legacy P2SH - unspentObj.nonWitnessUtxo = Buffer.from(await providerService.getTxHex(x.txid || x.hash), "hex"); + unspentObj.nonWitnessUtxo = Buffer.from( + await providerService.getTxHex(x.txid || x.hash), + "hex" + ); unspentObj.redeemScript = payment.redeem.output; break; } @@ -370,12 +523,12 @@ async function addAddrToMap(balanceMap, addrType, i, bip32Priv, pubKey, networkO } if (!balanceMap[payment.address].tx.length) { - // no unspent transactions found, so just discard this address + // No unspent transactions found, so just discard this address delete balanceMap[payment.address]; } - } - return true; + return true; + } } function promptConfirmation(query) {