Files
bch-js/src/electrumx.js
T

722 lines
21 KiB
JavaScript

/*
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