diff --git a/.env.dist b/.env.example similarity index 76% rename from .env.dist rename to .env.example index 2e1e160..7dfc15f 100644 --- a/.env.dist +++ b/.env.example @@ -1,6 +1,6 @@ PRIVATE_KEY1_BIP32= PRIVATE_KEY2= DESTINATION_ADDR= -N= NETWORK= -DERIVATION_PATH= \ No newline at end of file +DERIVATION_PATH= +N= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1605aa8..1fb42d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Address +addresses.json # Logs logs *.log diff --git a/README.md b/README.md index 672518e..9f3fa48 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,29 @@ -* **THIS IS BETA SOFTWARE. BE CAREFUL!** -* Reference sweep script for Basic (2-of-2) MultiSig wallets. NodeJS. -* Uses [SoChain's Free API](https://sochain.com/api) by default for blockchain data and for broadcasting transactions. You can implement your own backends yourself. -* NEVER SHARE YOUR PRIVATE KEYS. NEVER USE PRIVATE KEYS ON INSECURE SYSTEMS. +- Reference sweep script for Basic (2-of-2) MultiSig wallets. NodeJS. +- This repository was originally cloned from [BlockIo Sweep](https://github.com/BlockIo/blockio-basic-multisig-sweep) Using [SoChain API](https://sochain.com/api) by default. It became imperative to move on from their Sochain API after more customers started complaining about account getting frozen for no clear reason on BlockIo and the Sochain Became a paid service. + +- This project now leverages [Blockchain.com Free Service](https://blockchain.com) and [Blockcypher](https://blockcypher.com/) for data and for broadcasting transactions. You can certainly implement your own backends yourself. + +- Ensure you've backed up your Keys from BlockIo + +- NEVER SHARE YOUR PRIVATE KEYS. NEVER USE PRIVATE KEYS ON INSECURE SYSTEMS. + +#### HOW TO RUN -Command-line Usage: ``` $ git clone $ cd && npm install -$ N= PRIVATE_KEY1_BIP32= PRIVATE_KEY2= DESTINATION_ADDR= NETWORK= DERIVATION_PATH= node example.js + +``` + +Create your `.env` file in the root directory using the `.env.example` format. + +- N is the number of addresses you've generated on the given network +- PRIVATE_KEY1_BIP32 is the BIP32 extended private key you backed up +- PRIVATE_KEY2 is the second private key you backed up +- DESTINATION_ADDR is where you want the swept coins to go +- NETWORK is the network for which you're sweeping coins +- DERIVATION_PATH is the derivation path shown when you back up your private keys + +``` +$ node index.js ``` -* N is the number of addresses you've generated on the given network -* PRIVATE_KEY1_BIP32 is the BIP32 extended private key you backed up -* PRIVATE_KEY2 is the second private key you backed up -* DESTINATION_ADDR is where you want the swept coins to go -* NETWORK is the network for which you're sweeping coins -* DERIVATION_PATH is the derivation path shown when you back up your private keys diff --git a/example.js b/example.js deleted file mode 100644 index b71eb15..0000000 --- a/example.js +++ /dev/null @@ -1,25 +0,0 @@ -const Sweeper = require('./src/sweeper') - -const n = process.env.N -const bip32 = process.env.PRIVATE_KEY1_BIP32 -const privkey2 = process.env.PRIVATE_KEY2 -const toAddr = process.env.DESTINATION_ADDR -const network = process.env.NETWORK -const derivationPath = process.env.DERIVATION_PATH - -if (!n || !bip32 || !privkey2 || !toAddr || !network || !derivationPath) { - console.log('One or more required arguments are missing') - process.exit(0) -} - -const sweep = new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath) - -Sweep() - -async function Sweep () { - try { - await sweep.begin() - } catch (err) { - console.log(err) - } -} diff --git a/index.js b/index.js new file mode 100644 index 0000000..568b014 --- /dev/null +++ b/index.js @@ -0,0 +1,28 @@ +const Sweeper = require("./src/sweeper"); + +require("dotenv").config({ debug: true }); +const n = process.env.N; +const bip32 = process.env.PRIVATE_KEY1_BIP32; +const privkey2 = process.env.PRIVATE_KEY2; +const toAddr = process.env.DESTINATION_ADDR; +const network = process.env.NETWORK; +const derivationPath = process.env.DERIVATION_PATH; + +console.log({ n }); + +if (!n || !bip32 || !privkey2 || !toAddr || !network || !derivationPath) { + console.log("One or more required arguments are missing"); + process.exit(0); +} + +const sweep = new Sweeper(network, bip32, privkey2, toAddr, n, derivationPath); + +Sweep(); + +async function Sweep() { + try { + await sweep.begin(); + } catch (err) { + console.log(err); + } +} diff --git a/package.json b/package.json index 1691d47..2e76044 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,14 @@ }, "homepage": "https://github.com/BlockIo/blockio-basic-multisig-sweep#readme", "dependencies": { - "bitcoinjs-lib": "^6.0.0", - "ecpair": "^1.0.1", "bip32": "^3.0.1", - "tiny-secp256k1": "^2.1.1", + "bitcoinjs-lib": "^6.0.0", "chai": "^4.3.4", + "dotenv": "^16.0.3", + "ecpair": "^1.0.1", "mocha": "^8.4.0", - "node-fetch": "^2.6.6" + "node-fetch": "^2.6.6", + "tiny-secp256k1": "^2.1.1" }, "engines": { "node": "^17 || ^16 || ^15 || ^14 || ^13 || ^12" diff --git a/src/constants.js b/src/constants.js index e1630d4..c89c8db 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,40 +1,45 @@ module.exports = { - P2WSH_P2SH: 'P2WSH-P2SH', - P2SH: 'P2SH', - P2WSH: 'WITNESS_V0', - COIN: '100000000', + P2WSH_P2SH: "P2WSH-P2SH", + P2SH: "P2SH", + P2WSH: "WITNESS_V0", + COIN: "100000000", N: 100, MAX_TX_INPUTS: 500, - BLOCKCHAIN_PROVIDER_DEFAULT: 'sochain', - BLOCKCHAIN_PROVIDER_URL_DEFAULT: 'https://sochain.com/api/v2/', + BLOCKCHAIN_PROVIDER_DEFAULT: "blockchaincom", + BLOCKCHAIN_PROVIDER_URL_DEFAULT: "https://blockchain.info", // the order of provider names matters in PROVIDERS and PROVIDER_URLS // the order needs to be the same PROVIDERS: { - SOCHAIN: 'sochain', - MEMPOOLSPACE: 'mempoolspace', - BLOCKCHAINCOM: 'blockchaincom' + MEMPOOLSPACE: "mempoolspace", + BLOCKCHAINCOM: "blockchaincom", + BLOCKCYPHER: "blockcypher", + SOCHAIN: "sochain", }, PROVIDER_URLS: { - SOCHAIN: { - URL: 'https://sochain.com/api/v2', - SUPPORT: ['BTC', 'LTC', 'DOGE', 'BTCTEST', 'DOGETEST', 'LTCTEST'] - }, MEMPOOLSPACE: { - URL: 'https://mempool.space', - SUPPORT: ['BTC', 'BTCTEST'] + URL: "https://mempool.space", + SUPPORT: ["BTC", "BTCTEST"], }, BLOCKCHAINCOM: { - URL: 'https://blockchain.info', - SUPPORT: ['BTC'] - } + URL: "https://blockchain.info", + SUPPORT: ["BTC"], + }, + BLOCKCYPHER: { + URL: "https://api.blockcypher.com/v1/btc/main", + SUPPORT: ["BTC"], + }, + SOCHAIN: { + URL: "https://sochain.com/api/v2", + SUPPORT: ["BTC", "LTC", "DOGE", "BTCTEST", "DOGETEST", "LTCTEST"], + }, }, NETWORKS: { - BTC: 'BTC', - BTCTEST: 'BTCTEST', - LTC: 'LTC', - LTCTEST: 'LTCTEST', - DOGE: 'DOGE', - DOGETEST: 'DOGETEST' + BTC: "BTC", + BTCTEST: "BTCTEST", + LTC: "LTC", + LTCTEST: "LTCTEST", + DOGE: "DOGE", + DOGETEST: "DOGETEST", }, FEE_RATE: { BTC: 20, @@ -42,7 +47,7 @@ module.exports = { DOGE: 2000, BTCTEST: 20, LTCTEST: 20, - DOGETEST: 2000 + DOGETEST: 2000, }, DUST: { BTC: 546, @@ -50,15 +55,15 @@ module.exports = { DOGE: 1000000, // https://github.com/dogecoin/dogecoin/blob/v1.14.5/doc/fee-recommendation.md BTCTEST: 546, LTCTEST: 1000, - DOGETEST: 1000000 // https://github.com/dogecoin/dogecoin/blob/v1.14.5/doc/fee-recommendation.md + DOGETEST: 1000000, // https://github.com/dogecoin/dogecoin/blob/v1.14.5/doc/fee-recommendation.md }, NETWORK_FEE_MAX: { - BTC: (250 * 100000), // 0.25 BTC - BTCTEST: (250 * 100000), // 0.25 BTCTEST - LTC: (50 * 100000), // 0.05 LTC - LTCTEST: (50 * 100000), // 0.05 LTCTEST - DOGE: (2 * 100000000), // 2.00 DOGE - DOGETEST: (2 * 100000000) // 2.00 DOGETEST + BTC: 250 * 100000, // 0.25 BTC + BTCTEST: 250 * 100000, // 0.25 BTCTEST + LTC: 50 * 100000, // 0.05 LTC + LTCTEST: 50 * 100000, // 0.05 LTCTEST + DOGE: 2 * 100000000, // 2.00 DOGE + DOGETEST: 2 * 100000000, // 2.00 DOGETEST }, - TX_BROADCAST_APPROVAL_TEXT: 'I have verified this transaction, and I want to broadcast it now' -} + TX_BROADCAST_APPROVAL_TEXT: "I have verified this transaction, and I want to broadcast it now", +}; diff --git a/src/services/ProviderService.js b/src/services/ProviderService.js index 432a617..16285f9 100644 --- a/src/services/ProviderService.js +++ b/src/services/ProviderService.js @@ -1,112 +1,119 @@ -const constants = require('../constants') -const fetch = require('node-fetch') -const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) +const constants = require("../constants"); +const fetch = require("node-fetch"); +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const ProviderService = function (provider, network) { - const providerIndex = Object.values(constants.PROVIDERS).indexOf(provider) + const providerIndex = Object.values(constants.PROVIDERS).indexOf(provider); if (providerIndex < 0) { - throw new Error('Blockchain provider not supported') + throw new Error("Blockchain provider not supported"); } - const providerKey = Object.keys(constants.PROVIDER_URLS)[providerIndex] + const providerKey = Object.keys(constants.PROVIDER_URLS)[providerIndex]; if (constants.PROVIDER_URLS[providerKey].SUPPORT.indexOf(network) < 0) { - throw new Error('Network not supported by provider') + throw new Error("Network not supported by provider"); } - this.network = network - this.provider = provider -} + this.network = network; + this.provider = provider; +}; 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 res = await fetchUrl(apiUrl) - const json = await res.json() - if (json.status === 'fail') { - throw new Error(JSON.stringify(json.data)) + 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") { + throw new Error(JSON.stringify(json.data)); } - return json.data.tx_hex + 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 res = await fetchUrl(apiUrl) - const hex = await res.text() + 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) { - throw new Error(hex) + throw new Error(hex); } - return hex + return hex; } case constants.PROVIDERS.BLOCKCHAINCOM: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, 'rawtx', txId, '?format=hex'].join('/') - const res = await fetchUrl(apiUrl) - const hex = await res.text() + 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) { - throw new Error(hex) + throw new Error(hex); } - return hex + return hex; } + default: { - throw new Error('Could not get hex with provider: ' + this.provider) + throw new Error("Could not get hex with provider: " + this.provider); } } } catch (err) { - throw new Error(err) + throw new Error(err); } -} +}; 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 res = await fetchUrl(apiUrl) - const json = await res.json() - if (json.status === 'fail') { - throw new Error(JSON.stringify(json.data)) + 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") { + throw new Error(JSON.stringify(json.data)); } - return json.data.txs + return json.data.txs; } case constants.PROVIDERS.BLOCKCHAINCOM: { - const apiUrl = [constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, 'unspent?active=' + addr].join('/') - const res = await fetchUrl(apiUrl) - const json = await res.json() + const apiUrl = [constants.PROVIDER_URLS.BLOCKCHAINCOM.URL, "unspent?active=" + addr].join("/"); + const res = await fetchUrl(apiUrl); + const json = await res.json(); if (json.error) { - throw new Error(json.message) + throw new Error(json.message); } - return json.unspent_outputs + return json.unspent_outputs; } default: { - throw new Error('Could not get utxo with provider: ' + this.provider) + throw new Error("Could not get utxo with provider: " + this.provider); } } } catch (err) { - throw new Error(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('/') - await broadcastTx(apiUrl, txHex) - return + const apiUrl = [constants.PROVIDER_URLS.SOCHAIN.URL, "send_tx", this.network].join("/"); + await broadcastTx(apiUrl, txHex); + return; + } + + case constants.PROVIDERS.BLOCKCHAINCOM: { + const apiUrl = [constants.PROVIDER_URLS.BLOCKCYPHER, "txs", "push"].join("/"); + await broadcastTx(apiUrl, txHex); + return; } default: { - throw new Error('Could not send tx with provider: ' + this.provider) + throw new Error("Could not send tx with provider: " + this.provider); } } } catch (err) { - throw new Error(err) + throw new Error(err); } -} +}; -module.exports = ProviderService +module.exports = ProviderService; -async function fetchUrl (url) { +async function fetchUrl(url) { try { - let response = await fetch(url) + let response = await fetch(url); if (response.ok) { return response; } else { @@ -115,26 +122,36 @@ async function fetchUrl (url) { return await fetchUrl(url); } } catch (err) { - throw new Error(err) + throw new Error(err); } } -async function broadcastTx (apiUrl, txHex) { +async function broadcastTx(apiUrl, txHex) { try { - let res = await fetch(apiUrl, { - method: 'POST', - body: JSON.stringify({ tx_hex: txHex }), - headers: { 'Content-Type': 'application/json' } - }) - res = await res.json() - if (res.status === 'success') { - console.log('Sweep Success!') - console.log('Tx_id:', res.data.txid) + let res; + if (apiUrl == [constants.PROVIDER_URLS.BLOCKCYPHER, "txs", "push"].join("/")) { + res = await fetch(apiUrl, { + method: "POST", + body: JSON.stringify({ tx: txHex }), + headers: { "Content-Type": "application/json" }, + }); + res = await res.json(); + } else { + res = await fetch(apiUrl, { + method: "POST", + body: JSON.stringify({ tx_hex: txHex }), + headers: { "Content-Type": "application/json" }, + }); + res = await res.json(); + } + if (res.status === "success") { + console.log("Sweep Success!"); + console.log("Tx_id:", res.data.txid); } else { - console.log('Sweep Failed:') - throw new Error(JSON.stringify(res.data)) + console.log("Sweep Failed:"); + throw new Error(JSON.stringify(res.data)); } } catch (err) { - throw new Error(err) + throw new Error(err); } } diff --git a/src/sweeper.js b/src/sweeper.js index e033a04..90304c5 100644 --- a/src/sweeper.js +++ b/src/sweeper.js @@ -1,321 +1,372 @@ -const readline = require('readline') -const constants = require('./constants') -const networks = require('./networks') -const AddressService = require('./services/AddressService') -const ProviderService = require('./services/ProviderService') -const bitcoin = require('bitcoinjs-lib') -const ecpair = require('ecpair'); -const bip32 = require('bip32'); -const ecc = require('tiny-secp256k1'); - -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] - this.bip32PrivKey = bip32_private_key_1 - this.privateKey2 = private_key_2 - this.toAddr = destination_address - this.derivationPath = derivation_path - this.n = n || BlockIoSweep.DEFAULT_N - - if (options && typeof (options) === 'object') { - 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 - } else { - this.provider = BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER - this.feeRate = BlockIoSweep.DEFAULT_FEE_RATE[network] - this.maxTxInputs = BlockIoSweep.DEFAULT_MAX_TX_INPUTS - } - this.providerService = new ProviderService(this.provider, this.network) +const readline = require("readline"); +const constants = require("./constants"); +const networks = require("./networks"); +const AddressService = require("./services/AddressService"); +const ProviderService = require("./services/ProviderService"); +const bitcoin = require("bitcoinjs-lib"); +const ecpair = require("ecpair"); +const bip32 = require("bip32"); +const ecc = require("tiny-secp256k1"); +var fs = require("fs"); +const path = require("path"); + +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]; + this.bip32PrivKey = bip32_private_key_1; + this.privateKey2 = private_key_2; + this.toAddr = destination_address; + this.derivationPath = derivation_path; + this.n = n || BlockIoSweep.DEFAULT_N; + + if (options && typeof options === "object") { + 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; + } else { + this.provider = BlockIoSweep.DEFAULT_BLOCKCHAIN_PROVIDER; + this.feeRate = BlockIoSweep.DEFAULT_FEE_RATE[network]; + this.maxTxInputs = BlockIoSweep.DEFAULT_MAX_TX_INPUTS; + } + this.providerService = new ProviderService(this.provider, this.network); } // 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_FEE_RATE = constants.FEE_RATE -BlockIoSweep.DEFAULT_MAX_TX_INPUTS = constants.MAX_TX_INPUTS +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_FEE_RATE = constants.FEE_RATE; +BlockIoSweep.DEFAULT_MAX_TX_INPUTS = constants.MAX_TX_INPUTS; BlockIoSweep.prototype.begin = async function () { - // the user calls this to begin sweep of addresses - // we look for the first N paths' addresses, - // we retrieve the unspent outputs for the addresses - // we construct transaction(s) and sign them - // we ask the user to validate the transaction meets their approval - // if approved, we broadcast the transaction to the network - - if (this.network !== constants.NETWORKS.BTC && this.network !== constants.NETWORKS.BTCTEST && - this.network !== constants.NETWORKS.LTC && this.network !== constants.NETWORKS.LTCTEST && - 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') - } - - if (!this.bip32PrivKey || !this.privateKey2) { - throw new Error('One or more private keys not provided') - } - - if (!this.toAddr) { - // TODO LTC and LTCTEST destination addresses must use legacy address version - throw new Error('Destination address not provided') - } + // the user calls this to begin sweep of addresses + console.log({ input }); + // we look for the first N paths' addresses, + // we retrieve the unspent outputs for the addresses + // we construct transaction(s) and sign them + // we ask the user to validate the transaction meets their approval + // if approved, we broadcast the transaction to the network + + if ( + this.network !== constants.NETWORKS.BTC && + this.network !== constants.NETWORKS.BTCTEST && + this.network !== constants.NETWORKS.LTC && + this.network !== constants.NETWORKS.LTCTEST && + 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"); + } + + if (!this.bip32PrivKey || !this.privateKey2) { + throw new Error("One or more private keys not provided"); + } + + if (!this.toAddr) { + // TODO LTC and LTCTEST destination addresses must use legacy address version + throw new Error("Destination address not provided"); + } + + if (!this.derivationPath) { + throw new Error("Must specify DERIVATION_PATH"); + } + + if (this.derivationPath != "m/i/0" && this.derivationPath != "m/0/i") { + throw new Error("Must specify DERIVATION_PATH. Can be: m/i/0 or m/0/i."); + } + + try { + // get the public key from the user-specified private key + 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 txs = []; + + let psbt = new bitcoin.Psbt({ network: this.networkObj }); + + const root = bip32.default(ecc).fromBase58(this.bip32PrivKey, this.networkObj); + let ecKeys = {}; + + let balToSweep = 0; + const addressCount = Object.keys(utxoMap).length - 1; + let addrIte = 0; + let inputNum = 0; + + // TODO test for multiple tx + + for (const address of Object.keys(utxoMap)) { + // for each address + + // the BIP32 derived key (ECPair) for this address + let key = utxoMap[address].primaryKey; + + const addrTxCount = utxoMap[address].tx.length - 1; + + for (let i = 0; i < utxoMap[address].tx.length; i++) { + const utxo = utxoMap[address].tx[i]; + balToSweep += getCoinValue(utxo.value); + delete utxo.value; + const input = { + ...utxo, + }; + + psbt.addInput(input); + ecKeys[inputNum++] = key; + + 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"); + } - if (!this.derivationPath) { - throw new Error('Must specify DERIVATION_PATH') + // create the transaction without network fees + const tempPsbt = psbt.clone(); + 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); + // 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. + + balToSweep = balToSweep - networkFee * NETWORK_FEE_MULTIPLIER_EFFECT; + + 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` }); + } + + 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( + " *** WARNING: max network fee exceeded. This transaction has a network fee of " + + psbt.getFee().toString() + + " sats, whereas the maximum network fee allowed is " + + constants.NETWORK_FEE_MAX[this.network].toString() + + " sats" + ); + } + + const extracted_tx = psbt.extractTransaction(); + + // 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() }); + + psbt = new bitcoin.Psbt({ network: this.networkObj }); + balToSweep = 0; + ecKeys = {}; + inputNum = 0; + } + } + addrIte++; } - if (this.derivationPath != 'm/i/0' && this.derivationPath != 'm/0/i') { - throw new Error('Must specify DERIVATION_PATH. Can be: m/i/0 or m/0/i.') + if (!txs.length) { + throw new Error("No transaction created, do your addresses have balance?"); } - - try { - - // get the public key from the user-specified private key - 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 txs = [] - - let psbt = new bitcoin.Psbt({ network: this.networkObj }) - - const root = bip32.default(ecc).fromBase58(this.bip32PrivKey, this.networkObj) - let ecKeys = {} - - let balToSweep = 0 - const addressCount = Object.keys(utxoMap).length - 1 - let addrIte = 0 - let inputNum = 0 - - // TODO test for multiple tx - - for (const address of Object.keys(utxoMap)) { - // for each address - - // the BIP32 derived key (ECPair) for this address - let key = utxoMap[address].primaryKey - - const addrTxCount = utxoMap[address].tx.length - 1 - - for (let i = 0; i < utxoMap[address].tx.length; i++) { - - const utxo = utxoMap[address].tx[i] - balToSweep += getCoinValue(utxo.value) - delete utxo.value - const input = { - ...utxo - } - - psbt.addInput(input) - ecKeys[inputNum++] = key - - 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) - - // 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) - createAndFinalizeTx(psbt, this.toAddr, balToSweep, networkFee, ecKeys, this.privateKey2, this.networkObj) - - if (psbt.getFee() > constants.NETWORK_FEE_MAX[this.network]) { - throw new Error(' *** WARNING: max network fee exceeded. This transaction has a network fee of ' + psbt.getFee().toString() + ' sats, whereas the maximum network fee allowed is ' + constants.NETWORK_FEE_MAX[this.network].toString() + ' sats') - } - - const extracted_tx = psbt.extractTransaction() - - // 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() }) - - psbt = new bitcoin.Psbt({ network: this.networkObj }) - balToSweep = 0 - ecKeys = {} - inputNum = 0 - } - } - addrIte++ - } - - if (!txs.length) { - throw new Error('No transaction created, do your addresses have balance?') - } - - for (let i = 0; i < txs.length; i++) { - const tx = txs[i] - - console.log('\n\nVERIFY THE FOLLOWING IS CORRECT INDEPENDENTLY:\n') - console.log('Network:', this.network) - 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)') - - 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 '" + constants.TX_BROADCAST_APPROVAL_TEXT + "', otherwise, press enter: ") - - if (ans !== constants.TX_BROADCAST_APPROVAL_TEXT) { - console.log('\nTRANSACTION ABORTED\n') - continue - } - - await this.providerService.sendTx(tx.tx_hex) - } - } catch (err) { - console.log(err.stack) - throw new Error(err) + + for (let i = 0; i < txs.length; i++) { + const tx = txs[i]; + + console.log("\n\nVERIFY THE FOLLOWING IS CORRECT INDEPENDENTLY:\n"); + console.log("Network:", this.network); + 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)"); + + 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 '" + + constants.TX_BROADCAST_APPROVAL_TEXT + + "', otherwise, press enter: " + ); + + if (ans !== constants.TX_BROADCAST_APPROVAL_TEXT) { + console.log("\nTRANSACTION ABORTED\n"); + continue; + } + + await this.providerService.sendTx(tx.tx_hex); } -} + } catch (err) { + console.log(err.stack); + throw new Error(err); + } +}; -module.exports = BlockIoSweep +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 + const val = balance - networkFee; psbt.addOutput({ address: toAddr, // destination address - value: val // value in sats - }) + value: val, // value in sats + }); for (let i = 0; i < psbt.txInputs.length; i++) { - psbt.signInput(i, ecKeys[i]) - psbt.signInput(i, ecpair.ECPair.fromWIF(privKey2, network)) + psbt.signInput(i, ecKeys[i]); + psbt.signInput(i, ecpair.ECPair.fromWIF(privKey2, network)); } - psbt.finalizeAllInputs() + psbt.finalizeAllInputs(); } -function getNetworkFee (network, psbt, feeRate) { - const tx = psbt.extractTransaction() - const vSize = tx.virtualSize() // in bytes +function getNetworkFee(network, psbt, feeRate) { + const tx = psbt.extractTransaction(); + const vSize = tx.virtualSize(); // in bytes - let f = feeRate * vSize + let f = feeRate * vSize; - return f + return f; } -function getCoinValue (floatAsString) { - const s = floatAsString.split('.') +function getCoinValue(floatAsString) { + floatAsString = String(floatAsString); + const s = floatAsString.split("."); - if (s[1] === undefined) { s[1] = '0' } + if (s[1] === undefined) { + 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)); - if (r > Number.MAX_SAFE_INTEGER) { throw new Error('Number exceeds MAX_SAFE_INTEGER') } + console.log(`${floatAsString} becomes ${r}`); - return r + if (r > Number.MAX_SAFE_INTEGER) { + throw new Error("Number exceeds MAX_SAFE_INTEGER"); + } + + return parseInt(floatAsString); } -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 - - const balanceMap = {} - - for (let i = 0; i <= n; i++) { - - console.log('Evaluating addresses at i=' + i) - - 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) - } - - // populate balanceMap with data for P2SH address for any network - await addAddrToMap(balanceMap, constants.P2SH, i, bip32Priv, pubKey, networkObj, 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 + + const balanceMap = {}; + for (let i = 0; i <= n; i++) { + console.log("Evaluating addresses at i=" + i); + + 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); } - return balanceMap + // populate balanceMap with data for P2SH address for any network + 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) - - for (let addressData of addresses) { - const payment = addressData.payment - - 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 = [] - - // 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.txid - unspentObj.index = x.output_no - unspentObj.value = x.value - - switch (addrType) { - // handle different scripts for different address types here - - case constants.P2WSH_P2SH: // P2WSH-over-P2SH - unspentObj.witnessUtxo = { - script: Buffer.from(x.script_hex, 'hex'), - value: getCoinValue(x.value) - } - unspentObj.redeemScript = payment.redeem.output - unspentObj.witnessScript = payment.redeem.redeem.output - break - - case constants.P2WSH: // Native Segwit (v0) or Witness v0 - unspentObj.witnessUtxo = { - script: Buffer.from(x.script_hex, 'hex'), - value: getCoinValue(x.value) - } - unspentObj.witnessScript = payment.redeem.output - break - - case constants.P2SH: // Legacy P2SH - unspentObj.nonWitnessUtxo = Buffer.from(await providerService.getTxHex(x.txid), 'hex') - unspentObj.redeemScript = payment.redeem.output - break - - } - - balanceMap[payment.address].tx.push(unspentObj) - - } - - if (!balanceMap[payment.address].tx.length) { - // no unspent transactions found, so just discard this address - delete balanceMap[payment.address] - } +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); + + // 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 = []; + + // get the unspent transactions for the derived address + const addrUtxo = await providerService.getUtxo(payment.address); + + const jsonPath = path.join(__dirname, "..", "addresses.json"); + + let x; + + for (x of addrUtxo) { + const unspentObj = {}; + unspentObj.hash = x.tx_hash_big_endian; + 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("")); + } + }); + + switch (addrType) { + // handle different scripts for different address types here + + 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), + }; + unspentObj.redeemScript = payment.redeem.output; + unspentObj.witnessScript = payment.redeem.redeem.output; + break; + + 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), + }; + unspentObj.witnessScript = payment.redeem.output; + break; + + case constants.P2SH: // Legacy P2SH + unspentObj.nonWitnessUtxo = Buffer.from(await providerService.getTxHex(x.txid), "hex"); + unspentObj.redeemScript = payment.redeem.output; + break; + } + + balanceMap[payment.address].tx.push(unspentObj); + } + if (!balanceMap[payment.address].tx.length) { + // no unspent transactions found, so just discard this address + delete balanceMap[payment.address]; } + } - return true + return true; } -function promptConfirmation (query) { +function promptConfirmation(query) { const rl = readline.createInterface({ input: process.stdin, - output: process.stdout - }) - - return new Promise(resolve => rl.question(query, ans => { - rl.close() - resolve(ans) - })) + output: process.stdout, + }); + + return new Promise((resolve) => + rl.question(query, (ans) => { + rl.close(); + resolve(ans); + }) + ); }