/* This library interacts with the ElectrumX bch-api REST API endpoints operated by FullStack.cash */ // Public npm libraries const axios = require('axios') // Local libraries. const Blockchain = require('./blockchain') // let _this class ElectrumX { constructor (config) { this.restURL = config.restURL this.apiToken = config.apiToken this.authToken = config.authToken if (this.authToken) { // Add Basic Authentication token to the authorization header. this.axiosOptions = { headers: { authorization: this.authToken } } } else { // Add JWT token to the authorization header. this.axiosOptions = { headers: { authorization: `Token ${this.apiToken}` } } } this.blockchain = new Blockchain(config) // _this = this } /** * @api Electrumx.utxo() utxo() * @apiName ElectrumX Utxo * @apiGroup ElectrumX * @apiDescription Return a list of uxtos for an address. * * @apiExample Example usage: * (async () => { * try { * let utxo = await bchjs.Electrumx.utxo('bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'); * console.log(utxo); * } catch(error) { * console.error(error) * } * })() * * utxo = { * "success": true, * "utxos": [ * { * "height": 602405, * "tx_hash": "2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7", * "tx_pos": 0, * "value": 1000 * } * ] * } * * (async () => { * try { * let utxo = await bchjs.Electrumx.utxo(['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf', 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v']); * console.log(utxo); * } catch(error) { * console.error(error) * } * })() * * utxos = { * "success": true, * "utxos": [ * { * "utxos": [ * { * "height": 604392, * "tx_hash": "7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41", * "tx_pos": 0, * "value": 1000 * }, * { * "height": 630834, * "tx_hash": "4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53", * "tx_pos": 0, * "value": 6000 * } * ], * "address": "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" * }, * { * "utxos": [], * "address": "bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v" * } * ] * } * */ async utxo (address) { try { // Handle single address. if (typeof address === 'string') { const response = await axios.get( `${this.restURL}electrumx/utxos/${address}`, this.axiosOptions ) return response.data // Handle array of addresses. } else if (Array.isArray(address)) { const response = await axios.post( `${this.restURL}electrumx/utxos`, { addresses: address }, this.axiosOptions ) return response.data } throw new Error('Input address must be a string or array of strings.') } catch (error) { if (error.response && error.response.data) throw error.response.data else throw error } } /** * @api Electrumx.balance() balance() * @apiName ElectrumX Balance * @apiGroup ElectrumX * @apiDescription Return a list of balances for an address. * * @apiExample Example usage: * (async () => { * try { * let balance = await bchjs.Electrumx.balance('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'); * console.log(balance); * } catch(error) { * console.error(error) * } * })() * * balance = { * "success": true, * "balance": { * "confirmed": 1000, * "unconfirmed": 0 * } * } * * (async () => { * try { * let balance = await bchjs.Electrumx.balance(['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf', 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v']); * console.log(balance); * } catch(error) { * console.error(error) * } * })() * * balance = { * "success": true, * "balances": [ * { * "balance": { * "confirmed": 7000, * "unconfirmed": 0 * }, * "address": "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" * }, * { * "balance": { * "confirmed": 0, * "unconfirmed": 0 * }, * "address": "bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v" * } * ] * } * */ async balance (address) { try { // Handle single address. if (typeof address === 'string') { const response = await axios.get( `${this.restURL}electrumx/balance/${address}`, this.axiosOptions ) return response.data // Handle array of addresses. } else if (Array.isArray(address)) { const response = await axios.post( `${this.restURL}electrumx/balance`, { addresses: address }, this.axiosOptions ) return response.data } throw new Error('Input address must be a string or array of strings.') } catch (error) { if (error.response && error.response.data) throw error.response.data else throw error } } /** * @api Electrumx.transactions() transactions() * @apiName ElectrumX Transactions * @apiGroup ElectrumX * @apiDescription Return a transaction history for an address. * * @apiExample Example usage: * (async () => { * try { * let transactions = await bchjs.Electrumx.transactions('bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'); * console.log(utxo); * } catch(error) { * console.error(error) * } * })() * * { * "success": true, * "transactions": [ * { * "height": 560430, * "tx_hash": "3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851" * }, * { * "height": 560534, * "tx_hash": "4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280" * } * ] * } * * (async () => { * try { * let transactions = await bchjs.Electrumx.transactions(['bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj', 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v']); * console.log(utxo); * } catch(error) { * console.error(error) * } * })() * * transactions = { * "success": true, * "transactions": [ * { * "transactions": [ * { * "height": 631219, * "tx_hash": "ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f" * } * ], * "address": "bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj" * }, * { * "transactions": [ * { * "height": 560430, * "tx_hash": "3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851" * }, * { * "height": 560534, * "tx_hash": "4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280" * } * ], * "address": "bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v" * } * ] * } * */ async transactions (address, usrObj = null) { try { // Handle single address. if (typeof address === 'string') { const response = await axios.get( `${this.restURL}electrumx/transactions/${address}`, this.axiosOptions ) return response.data // Handle array of addresses. } else if (Array.isArray(address)) { const response = await axios.post( `${this.restURL}electrumx/transactions`, { addresses: address, usrObj // pass user data when making an internal call. }, this.axiosOptions ) return response.data } throw new Error('Input address must be a string or array of strings.') } catch (error) { if (error.response && error.response.data) throw error.response.data else throw error } } /** * @api Electrumx.unconfirmed() unconfirmed() * @apiName ElectrumX Unconfirmed * @apiGroup ElectrumX * @apiDescription Return a list of unconfirmed uxtos (mempool) for an address. * * @apiExample Example usage: * (async () => { * try { * let mempool = await bchjs.Electrumx.unconfirmed('bitcoincash:qqh793x9au6ehvh7r2zflzguanlme760wuzehgzjh9'); * console.log(mempool); * } catch(error) { * console.error(error) * } * })() * * mempool = { * "success": true, * "utxos": [ * { * "height": 602405, * "tx_hash": "2b37bdb3b63dd0bca720437754a36671431a950e684b64c44ea910ea9d5297c7", * "fee": 24310 * } * ] * } * * (async () => { * try { * let mempool = await bchjs.Electrumx.unconfirmed(['bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf', 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v']); * console.log(mempool); * } catch(error) { * console.error(error) * } * })() * * mempool = { * "success": true, * "utxos": [ * { * "utxos": [ * { * "height": 604392, * "tx_hash": "7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41", * "fee": 24310 * }, * { * "height": 630834, * "tx_hash": "4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53", * "fee": 3000 * } * ], * "address": "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" * }, * { * "utxos": [], * "address": "bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v" * } * ] * } * */ async unconfirmed (address) { try { // Handle single address. if (typeof address === 'string') { const response = await axios.get( `${this.restURL}electrumx/unconfirmed/${address}`, this.axiosOptions ) return response.data // Handle array of addresses. } else if (Array.isArray(address)) { const response = await axios.post( `${this.restURL}electrumx/unconfirmed`, { addresses: address }, this.axiosOptions ) return response.data } throw new Error('Input address must be a string or array of strings.') } catch (error) { if (error.response && error.response.data) throw error.response.data else throw error } } /** * @api Electrumx.blockHeader() blockHeader() * @apiName ElectrumX Block headers * @apiGroup ElectrumX * @apiDescription Return block headers for a given height * * @apiExample Example usage: * (async () => { * try { * let headers = await bchjs.Electrumx.blockHeaders(42); * console.log(headers); * } catch(error) { * console.error(error) * } * })() * * headers = { * "success": true, * "headers": [ * "010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6", * "01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c" * ] * } * * (async () => { * try { * let headers = await bchjs.Electrumx.blockHeaders(42, 1); * console.log(headers); * } catch(error) { * console.error(error) * } * })() * * headers = { * "success": true, * "headers": [ * "010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6" * ] * } * */ async blockHeader (height, count = 1) { try { const response = await axios.get( `${this.restURL}electrumx/block/headers/${height}?count=${count}`, this.axiosOptions ) return response.data } catch (error) { // console.log("error: ", error) if (error.response && error.response.data) { if (error.response && error.response.data) { throw new Error(error.response.data.error) } else throw error.response.data } else { throw error } } } /** * @api Electrumx.txData() txData() * @apiName ElectrumX txData * @apiGroup ElectrumX * @apiDescription Returns an object with transaction details of the TXID * * @apiExample Example usage: * (async () => { * try { * let result = await bchjs.Electrumx.txData('4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251') * console.log(result); * } catch(error) { * console.error(error) * } * })() * * result = { * "success": true, * "details": { * "blockhash": "0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc", * "blocktime": 1578327094, * "confirmations": 31861, * "hash": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", * ... * "vin": [ * { * "scriptSig": { * ... * "vout": [ * { * "n": 0, * "scriptPubKey": { * "addresses": [ * "bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl" * ], * } * ... * } * * (async () => { * try { * let result = await bchjs.Electrumx.txData(['4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251', '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251']) * console.log(result); * } catch(error) { * console.error(error) * } * })() * * result = { * "transactions": [ * { * "txid": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", * "details": { * "blockhash": "0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc", * "blocktime": 1578327094, * "confirmations": 31861, * "hash": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", * ... * } * }, * { * "txid": "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251", * "details": { * "blockhash": "0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc", * "blocktime": 1578327094, * ... * } * ] * } */ async txData (txid) { try { // Handle single transaction. if (typeof txid === 'string') { const response = await axios.get( `${this.restURL}electrumx/tx/data/${txid}`, this.axiosOptions ) return response.data } else if (Array.isArray(txid)) { const response = await axios.post( `${this.restURL}electrumx/tx/data`, { txids: txid }, this.axiosOptions ) return response.data } throw new Error('Input txId must be a string or array of strings.') } catch (error) { if (error.response && error.response.data) throw error.response.data else throw error } } /** * @api Electrumx.broadcast() broadcast() * @apiName ElectrumX Broadcast * @apiGroup ElectrumX * @apiDescription Broadcast a raw transaction and return the transaction ID on success or error on failure. * * (async () => { * try { * const txHex = "020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000" * let result = await bchjs.Electrumx.broadcast(txHex) * console.log(result); * } catch(error) { * console.error(error) * } * })() * * result = { * "success": true, * "txid": "..." * } */ async broadcast (txHex) { try { if (typeof txHex === 'string') { const response = await axios.post( `${this.restURL}electrumx/tx/broadcast`, { txHex }, this.axiosOptions ) return response.data } throw new Error('Input txHex must be a string.') } catch (error) { if (error.response && error.response.data) throw error.response.data else throw error } } /** * @api Electrumx.sortConfTxs() sortConfTxs() * @apiName ElectrumX sortConfTxs * @apiGroup ElectrumX * @apiDescription Sort the output of Electrum.transactions() by block height. * * A simple sort function for the output of Electrum.transactions(). Ignores * unconfirmed transactions. * * Sorts in 'DESCENDING' order by default, or 'ASCENDING' can be specified. * Descending makes the first element the newest (largest block height). * * @apiExample Example usage: * (async () => { * const txs = await bchjs.Electrumx.transactions('bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v') * const sortedTxs = bchjs.Electrumx.sortConfTxs(txs.transactions, 'ASCENDING') * console.log(sortedTxs) * })() * * // [ * // { * // "height": 560430, * // "tx_hash": "3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851" * // }, * // { * // "height": 560534, * // "tx_hash": "4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280" * // } * // ] */ // Sort confirmed Transactions by the block height sortConfTxs (txs, sortingOrder = 'DESCENDING') { try { // console.log(`sortConfTxs txs: ${JSON.stringify(txs, null, 2)}`) // Filter out unconfirmed transactions, with a height of 0 or less. txs = txs.filter(elem => elem.height > 0) if (sortingOrder === 'DESCENDING') { // console.log('Sorting in descending order') return txs.sort((a, b) => { // console.log(`descending b.height: ${b.height}, a.height: ${a.height}`) return b.height - a.height }) } // console.log('Sorting in ascending order') return txs.sort((a, b) => { // console.log(`ascending b.height: ${b.height}, a.height: ${a.height}`) return a.height - b.height }) } catch (err) { console.log('Error in util.js/sortConfTxs()') throw err } } /** * @api Electrumx.sortAllTxs() sortAllTxs() * @apiName ElectrumX sortAllTxs * @apiGroup ElectrumX * @apiDescription Sort the output of Electrum.transactions() by block height. * * A simple sort function for the output of Electrum.transactions(). * Assumes that unconfirmed transactions will make it into the next block. Any * unconfirmed transactions have their block height with the height of the next * block. Returns a Promise. * * Sorts in 'ASCENDING' order by default, or 'DESCENDING' can be specified. * * @apiExample Example usage: * (async () => { * const txs = await bchjs.Electrumx.transactions('bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v') * const sortedTxs = await bchjs.Electrumx.sortAllTxs(txs.transactions, 'ASCENDING') * console.log(sortedTxs) * })() * * // [ * // { * // "height": 560430, * // "tx_hash": "3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851" * // }, * // { * // "height": 560534, * // "tx_hash": "4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280" * // } * // ] */ // Substitute zero-conf txs with the current block-height + 1 async sortAllTxs (txs, sortingOrder = 'DESCENDING') { try { // console.log(`sortingOrder: ${sortingOrder}`) // Calculate the height of the next block const nextBlock = (await this.blockchain.getBlockCount()) + 1 // Replace the height of any zero-conf transactions with the height of // the next block. const modifiedTxs = txs.map(elem => { if (elem.height <= 0) elem.height = nextBlock return elem }) // console.log(`modifiedTxs: ${JSON.stringify(modifiedTxs, null, 2)}`) // Sort the modified array of transactions. return this.sortConfTxs(modifiedTxs, sortingOrder) } catch (err) { console.log('Error in util.js/sort0ConfTxs') throw err } } } module.exports = ElectrumX