Merge pull request #88 from Permissionless-Software-Foundation/ct-unstable

feat(tx sorting): added Electrumx.sort0ConfTxs()
This commit is contained in:
Chris Troutner
2021-01-26 20:28:40 -08:00
committed by GitHub
6 changed files with 443 additions and 765 deletions
+188 -748
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -16,8 +16,8 @@
"test:integration:local": "RESTURL=http://localhost:3000/v4/ mocha --timeout 30000 test/integration",
"test:integration:local:bchn": "RESTURL=http://localhost:3000/v4/ mocha --timeout 30000 test/integration/ && mocha --timeout 30000 test/integration/chains/bchn/",
"test:integration:local:testnet": "RESTURL=http://localhost:4000/v4/ mocha --timeout 30000 test/integration/testnet",
"test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#tokenUtxoDetails' test/integration/",
"test:temp2": "mocha --timeout 30000 -g '#generateSendOpReturn' test/unit/",
"test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#sort0ConfTxs' test/integration/",
"test:temp2": "mocha --timeout=30000 -g '#sortConfTxs' test/unit/",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
@@ -32,7 +32,6 @@
"dependencies": {
"@uppy/core": "^1.10.4",
"@uppy/tus": "^1.5.12",
"apidoc": "^0.25.0",
"assert": "^2.0.0",
"axios": "^0.21.1",
"bc-bip68": "^1.0.5",
@@ -76,6 +75,7 @@
"wif": "^2.0.6"
},
"devDependencies": {
"apidoc": "^0.17.7",
"chai": "^4.1.2",
"coveralls": "^3.0.2",
"eslint": "5.16.0",
+107 -2
View File
@@ -2,9 +2,12 @@
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 {
@@ -29,6 +32,8 @@ class ElectrumX {
}
}
this.blockchain = new Blockchain(config)
_this = this
}
@@ -452,7 +457,9 @@ class ElectrumX {
} 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
if (error.response && error.response.data) {
throw new Error(error.response.data.error)
} else throw error.response.data
} else {
throw error
}
@@ -596,6 +603,104 @@ class ElectrumX {
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 'ASCENDING' order by default, or 'DESCENDING' can be specified.
*
* @apiExample Example usage:
* (async () => {
* const txs = await bchjs.Electrumx.transactions('bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v')
* const sortedTxs = bchjs.Electrumx.sortConfTxs(txs.transactions, 'DESCENDING')
* console.log(sortedTxs)
* })()
*
* // [
* // {
* // "height": 560534,
* // "tx_hash": "4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280"
* // },
* // {
* // "height": 560430,
* // "tx_hash": "3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851"
* // }
* // ]
*/
// Sort confirmed Transactions by the block height
sortConfTxs (txs, sortingOrder = 'ASCENDING') {
try {
// Filter out unconfirmed transactions, with a height of 0 or less.
txs = txs.filter(elem => elem.height > 0)
if (sortingOrder === 'ASCENDING') {
return txs.sort((a, b) => a.height - b.height)
}
return txs.sort((a, b) => b.height - a.height)
} catch (err) {
console.log('Error in util.js/sortConfTxs()')
throw err
}
}
/**
* @api Electrumx.sort0ConfTxs() sort0ConfTxs()
* @apiName ElectrumX sort0ConfTxs
* @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.sort0ConfTxs(txs.transactions, 'DESCENDING')
* console.log(sortedTxs)
* })()
*
* // [
* // {
* // "height": 560534,
* // "tx_hash": "4ebbeaac51ce141e262964e3a0ce11b96ca72c0dffe9b4127ce80135f503a280"
* // },
* // {
* // "height": 560430,
* // "tx_hash": "3e1f3e882be9c03897eeb197224bf87f312be556a89f4308fabeeeabcf9bc851"
* // }
* // ]
*/
// Substitute zero-conf txs with the current block-height + 1
async sort0ConfTxs (txs, sortingOrder = 'ASCENDING') {
try {
// 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
+28 -4
View File
@@ -63,7 +63,9 @@ describe('#ElectrumX', () => {
it('should throw error on array size rate limit', async () => {
try {
const addr = []
for (let i = 0; i < 25; i++) { addr.push('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') }
for (let i = 0; i < 25; i++) {
addr.push('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
}
// const result = await bchjs.Electrumx.utxo(addr)
await bchjs.Electrumx.utxo(addr)
@@ -116,7 +118,9 @@ describe('#ElectrumX', () => {
it('should throw error on array size rate limit', async () => {
try {
const addr = []
for (let i = 0; i < 25; i++) { addr.push('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') }
for (let i = 0; i < 25; i++) {
addr.push('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
}
// const result = await bchjs.Electrumx.balance(addr)
// console.log(`result: ${util.inspect(result)}`)
@@ -172,7 +176,9 @@ describe('#ElectrumX', () => {
it('should throw error on array size rate limit', async () => {
try {
const addr = []
for (let i = 0; i < 25; i++) { addr.push('bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0') }
for (let i = 0; i < 25; i++) {
addr.push('bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0')
}
// const result = await bchjs.Electrumx.transactions(addr)
// console.log(`result: ${util.inspect(result)}`)
@@ -235,7 +241,9 @@ describe('#ElectrumX', () => {
it('should throw error on array size rate limit', async () => {
try {
const addr = []
for (let i = 0; i < 25; i++) { addr.push('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf') }
for (let i = 0; i < 25; i++) {
addr.push('bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf')
}
await bchjs.Electrumx.unconfirmed(addr)
@@ -360,6 +368,22 @@ describe('#ElectrumX', () => {
}
})
})
describe('#sort0ConfTxs', () => {
it('should GET transaction history for a single address', async () => {
const addr = 'bitcoincash:qpdh9s677ya8tnx7zdhfrn8qfyvy22wj4qa7nwqa5v'
const txs = await bchjs.Electrumx.transactions(addr)
const sortedTransactions = await bchjs.Electrumx.sort0ConfTxs(
txs.transactions
)
// console.log(
// `sortedTransactions: ${JSON.stringify(sortedTransactions, null, 2)}`
// )
assert.isBelow(sortedTransactions[0].height, sortedTransactions[1].height)
})
})
})
function sleep (ms) {
+73
View File
@@ -390,6 +390,7 @@ describe('#ElectrumX', () => {
assert.equal(result.transactions.length, 2, '2 outputs for 2 inputs')
})
})
describe('#broadcast', () => {
it('should throw an error for improper input', async () => {
try {
@@ -402,6 +403,7 @@ describe('#ElectrumX', () => {
assert.include(err.message, 'Input txHex must be a string.')
}
})
it('should broadcast a single transaction', async () => {
// Stub the network call.
sandbox.stub(axios, 'post').resolves({ data: mockData.broadcast })
@@ -418,4 +420,75 @@ describe('#ElectrumX', () => {
assert.equal(result.txid, txid)
})
})
describe('#sortConfTxs', () => {
it('should sort in ascending order by default', () => {
const result = bchjs.Electrumx.sortConfTxs(mockData.transaction.transactions)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isBelow(result[0].height, result[1].height)
})
it('should sort in descending order', () => {
const result = bchjs.Electrumx.sortConfTxs(mockData.transaction.transactions, 'DESCENDING')
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(result[0].height, result[1].height)
})
it('should ignore unconfirmed txs', () => {
const result = bchjs.Electrumx.sortConfTxs(mockData.txHistoryWithUnconfirmed.transactions)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(result[0].height, 0)
})
it('should handle errors', () => {
try {
bchjs.Electrumx.sortConfTxs('abc')
assert.fail('Unexpected result')
} catch (err) {
// console.log(err.message)
assert.equal(err.message, 'txs.filter is not a function')
}
})
})
// These tests use mocked data that contains unconfirmed transactions.
describe('#sort0ConfTxs', () => {
it('should sort in ascending order by default', async () => {
// Stub network calls
sandbox.stub(bchjs.Electrumx.blockchain, 'getBlockCount').resolves(672141)
const result = await bchjs.Electrumx.sort0ConfTxs(mockData.txHistoryWithUnconfirmed.transactions)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isBelow(result[0].height, result[1].height)
})
it('should sort in descending order', async () => {
// Stub network calls
sandbox.stub(bchjs.Electrumx.blockchain, 'getBlockCount').resolves(672141)
const result = await bchjs.Electrumx.sort0ConfTxs(mockData.txHistoryWithUnconfirmed.transactions, 'DESCENDING')
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isAbove(result[1].height, result[2].height)
})
it('should handle errors', async () => {
try {
// Stub network calls
sandbox.stub(bchjs.Electrumx.blockchain, 'getBlockCount').resolves(672141)
await bchjs.Electrumx.sort0ConfTxs('abc')
assert.fail('Unexpected result')
} catch (err) {
// console.log(err.message)
assert.equal(err.message, 'txs.map is not a function')
}
})
})
})
+44 -8
View File
@@ -168,7 +168,8 @@ const txDetails = {
blocktime: 1578327094,
confirmations: 31861,
hash: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
hex: '020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000',
hex:
'020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000',
locktime: 0,
size: 392,
time: 1578327094,
@@ -177,8 +178,10 @@ const txDetails = {
vin: [
{
scriptSig: {
asm: 'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309',
hex: '41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309'
asm:
'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309',
hex:
'41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309'
},
sequence: 4294967295,
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
@@ -186,8 +189,10 @@ const txDetails = {
},
{
scriptSig: {
asm: '347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954',
hex: '41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954'
asm:
'347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954',
hex:
'41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954'
},
sequence: 4294967295,
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
@@ -210,7 +215,8 @@ const txDetails = {
n: 1,
scriptPubKey: {
addresses: ['bitcoincash: qq59hv6s3qdjrtyfwfxxldkuj9xsjmx48vrz882knz'],
asm: 'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG',
asm:
'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914285bb350881b21ac89724c6fb6dc914d096cd53b88ac',
reqSigs: 1,
type: 'pubkeyhash'
@@ -221,7 +227,8 @@ const txDetails = {
n: 2,
scriptPubKey: {
addresses: ['bitcoincash: qpzlruwy4xu5rxjs3z37nsj29y7h59gwvsu4ddp0u4'],
asm: 'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG',
asm:
'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac',
reqSigs: 1,
type: 'pubkeyhash'
@@ -255,6 +262,34 @@ const broadcast = {
txid: txDetails.txid
}
const txHistoryWithUnconfirmed = {
success: true,
transactions: [
{
height: 668539,
tx_hash:
'fd9bffca3dd7a2d801c87cb36e8b43949d2e171bb8e25e3d92cf39ac5e2ba179'
},
{
height: 668544,
tx_hash:
'9d1b438aa55d75da8ebe9dcdfa4fa96714042ce0a19c8f3c10d86e1e3dcbccf5'
},
{
fee: 440,
height: 0,
tx_hash:
'30d3fa9bf78eab2162d4f1894223e417e7275fdf0d590fe5085381dd475ff077'
},
{
fee: 468,
height: -1,
tx_hash:
'4b93c26a4612b6819fde2bf5d1f6387bf92f49a3375c53a472001e7d76a80e59'
}
]
}
module.exports = {
utxo,
utxos,
@@ -267,5 +302,6 @@ module.exports = {
blockHeaders,
details,
detailsArray,
broadcast
broadcast,
txHistoryWithUnconfirmed
}