Compare commits

...
Author SHA1 Message Date
Chris Troutner cbb0d0a8ca Merge pull request #33 from Permissionless-Software-Foundation/ct-unstable
validateTxid2
2020-10-11 16:51:40 -07:00
Chris Troutner 7ec8442540 fix(Electrumx.blockHeader): Mocking negative test-case unit tests 2020-10-11 15:39:17 -07:00
Chris Troutner be3094fc77 feat(validateTxid2): slp-validate based validation of SLP txids 2020-10-11 15:23:50 -07:00
Chris Troutner d35f0e5473 Merge pull request #30 from Permissionless-Software-Foundation/broacast-tx
Add Electrumx Broadcast TX method
2020-10-09 11:35:12 -07:00
Chris Troutner 79088fc68d linting 2020-10-09 11:30:07 -07:00
Chris Troutner fe84729737 Merge branch 'master' into broacast-tx 2020-10-09 11:21:29 -07:00
Chris Troutner 2ec72e73c1 Merge pull request #32 from Permissionless-Software-Foundation/ct-unstable
fix(bchn): Adding bchn free tier to integration tests
2020-10-09 11:21:05 -07:00
Chris Troutner a2c121bf3b fix(bchn): Adding bchn free tier to integration tests 2020-10-09 11:17:40 -07:00
Chris Troutner fd6f58fd45 Merge pull request #31 from Permissionless-Software-Foundation/ct-unstable
fix(decodeOpReturn): Added additional comments
2020-10-07 10:53:01 -07:00
Chris Troutner e8b2f88ff5 fix(decodeOpReturn): Added additional comments 2020-10-07 08:07:06 -07:00
Chris Troutner 4a11db7c45 Merge pull request #29 from kaellis/master
add internal caching for slp.tokenUtxoDetails to reduce duplicate calls
2020-10-07 08:02:08 -07:00
Stoyan Zhekov af118f8a62 Add Electrumx Broadcast TX method 2020-10-07 17:14:30 +09:00
Ken Ellis 1f41b67e3b add internal caching for slp.tokenUtxoDetails to reduce duplicate calls 2020-10-01 09:33:06 -04:00
Chris Troutner 054fb9c14e Merge pull request #28 from Permissionless-Software-Foundation/ct-unstable
Improvements to hydrateUtxos()
2020-09-28 19:33:20 -07:00
Chris Troutner 1fd4095dcb fix(hydateUtxos): Refined integration test 2020-09-28 19:30:06 -07:00
Chris Troutner 0eb1f74ba1 fix(hydrateUtxos): fixing documentation for the function 2020-09-28 19:18:27 -07:00
Chris Troutner 1a13c41c22 Merge pull request #27 from Permissionless-Software-Foundation/ct-unstable
fix(tokenUtxoDetails): returning isValid:null when 429 error encounte…
2020-09-28 18:00:27 -07:00
Chris Troutner 8b6952b1d4 Updating docs 2020-09-28 17:52:37 -07:00
Chris Troutner 73dd2c010d fix(tokenUtxoDetails): returning isValid:null when 429 error encountered with decodeOpReturn() 2020-09-28 17:39:26 -07:00
Chris Troutner 70743876d2 Merge pull request #25 from Permissionless-Software-Foundation/ct-unstable
fix(hydrateUtxos): Adapted to accept Electrumx.utxo() output
2020-09-27 17:56:36 -07:00
Chris Troutner ab51afb2a8 fix(hydrateUtxos): Adapted to accept Electrumx.utxo() output 2020-09-27 17:52:31 -07:00
Chris Troutner 8a59374831 Merge pull request #24 from Permissionless-Software-Foundation/ct-unstable
feat(SLP.Utils.hydrateUtxos): Added hydrateUtxos to match new bch-api…
2020-09-27 01:51:53 -07:00
Chris Troutner 32b8efa7a2 feat(SLP.Utils.hydrateUtxos): Added hydrateUtxos to match new bch-api endpoint 2020-09-27 01:47:46 -07:00
Chris Troutner b0bdc50193 Merge pull request #22 from Permissionless-Software-Foundation/add-electrumx-raw-tx-data
Add ElectrumX Raw Transaction Details method and tests
2020-09-12 06:58:33 -07:00
10 changed files with 726 additions and 28 deletions
+2 -1
View File
@@ -14,8 +14,9 @@
"test:integration:local": "RESTURL=http://localhost:3000/v3/ mocha --timeout 30000 test/integration",
"test:integration:testnet:local": "RESTURL=http://localhost:4000/v3/ mocha --timeout 30000 test/integration/testnet",
"test:integration:free": "bash test/integration/test-free-tier.sh",
"test:integration:free:bchn": "bash test/integration/test-free-tier-bchn.sh",
"test:integration:free:testnet": "bash test/integration/testnet/test-free-tier.sh",
"test:temp": "mocha --timeout 30000 test/integration/testnet/ninsight.js",
"test:temp": "mocha --timeout 30000 -g '#blockHeader' test/unit/electrumx.js",
"test:integration:api": "RESTURL=https://api.fullstack.cash/v3/ mocha --timeout 30000 test/integration",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"coverage:report": "nyc --reporter=html mocha --timeout 25000 test/unit/",
+49 -3
View File
@@ -441,8 +441,14 @@ class ElectrumX {
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw 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
}
}
}
@@ -515,7 +521,7 @@ class ElectrumX {
* }
* ]
* }
*/
*/
async txData(txid) {
try {
// Handle single transaction.
@@ -543,6 +549,46 @@ class ElectrumX {
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
}
}
}
module.exports = ElectrumX
+273 -17
View File
@@ -1,10 +1,10 @@
const axios = require("axios")
const slpParser = require("slp-parser")
const Script = require("../script")
const scriptLib = new Script()
// const Script = require("../script")
// const scriptLib = new Script()
const BigNumber = require("bignumber.js")
// const BigNumber = require("bignumber.js")
let _this
@@ -455,6 +455,66 @@ class Utils {
}
}
/**
* @api SLP.Utils.validateTxid2() validateTxid2()
* @apiName validateTxid2
* @apiGroup SLP Utils
* @apiDescription Validate that txid is an SLP transaction.
*
* This second validatoin version uses the slp-validate slp library. It is
* much slower and less efficient than SLPDB and is prone to time-outs for
* tokens with large DAGs. However, it operates independently of SLPDB and
* is a great second validation option, particularly when SLPDB returns 'null'
* values.
*
* Due to the inefficiency of this call, only a single TXID can be input at a
* time. This call will throw an error if the input is an array.
*
* @apiExample Example usage:
*
* // validate single SLP txid
* (async () => {
* try {
* let validated = await bchjs.SLP.Utils.validateTxid(
* "df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb"
* );
* console.log(validated);
* } catch (error) {
* console.error(error);
* }
* })();
*
* // returns
* [ { txid:
* 'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
* valid: true } ]
*/
async validateTxid2(txid) {
try {
// console.log(`txid: ${JSON.stringify(txid, null, 2)}`)
if (
!txid ||
txid === "" ||
typeof txid !== "string" ||
txid.length !== 64
)
throw new Error(`txid must be 64 character string.`)
const path = `${this.restURL}slp/validateTxid2/${txid}`
const response = await axios.get(path, _this.axiosOptions)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
if (error.error && error.error.indexOf("Network error") > -1)
throw new Error("slp-validate timed out")
throw error
}
}
/**
* @api SLP.Utils.tokenStats() tokenStats()
* @apiName tokenStats
@@ -662,6 +722,9 @@ class Utils {
* Similar to decodeOpReturn(), except decodeOpReturn2() uses the slp-parser
* library maintained by JT Freeman. Outputs have slightly different format.
*
* If optional associative array parameter cache is used, will cache and
* reuse responses for the same input.
*
* Throws an error if given a non-SLP txid.
*
* In a future version of bch-js, this method will replace the origonal
@@ -694,7 +757,21 @@ class Utils {
* }
*/
// Reimplementation of decodeOpReturn() using slp-parser.
async decodeOpReturn(txid) {
async decodeOpReturn(txid, cache = null) {
// The cache object is an in-memory cache (JS Object) that can be passed
// into this function. It helps if multiple vouts from the same TXID are
// being evaluated. In that case, it can significantly reduce the number
// of API calls.
// To use: add the output of this function to the cache object:
// cache[txid] = returnValue
// Then pass that cache object back into this function every time its called.
if (cache) {
if (!(cache instanceof Object))
throw new Error(`decodeOpReturn cache parameter must be Object`)
const cachedVal = cache[txid]
if (cachedVal) return cachedVal
}
try {
// Validate the txid input.
if (!txid || txid === "" || typeof txid !== "string")
@@ -746,8 +823,11 @@ class Utils {
}
// console.log(`tokenData: ${JSON.stringify(tokenData, null, 2)}`)
if (cache) cache[txid] = tokenData
return tokenData
} catch (error) {
// console.log('decodeOpReturn error: ', error)
if (error.response && error.response.data) throw error.response.data
throw error
}
@@ -766,18 +846,20 @@ class Utils {
* set to false.
* If the UTXO is part of an SLP transaction, it will return the UTXO object
* with additional SLP information attached. An `isValid` property will be included.
* If its value is true, the UTXO is a valid SLP UTXO. If the value is null,
* then SLPDB has not yet processed that txid and validity has not been confirmed.
* If its value is true, the UTXO is a valid SLP UTXO.
* If the isValid value is null,
* then SLPDB has not yet processed that txid and validity has not been confirmed,
* or a 429 rate-limit error was enountered during the processing of the request.
*
* @apiExample Example usage:
*
* (async () => {
* try {
* const utxos = await bchjs.Blockbook.utxos(`bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05`)
* const utxos = await bchjs.Electrumx.utxo(`bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05`)
*
* const utxoInfo = await bchjs.SLP.Utils.tokenUtxoDetails(utxos)
*
* console.log(`utxoInfo: ${JSON.stringify(isSLPUtxo,null,2)}`)
* console.log(`utxoInfo: ${JSON.stringify(utxoInfo, null, 2)}`)
* } catch (error) {
* console.error(error)
* }
@@ -810,6 +892,10 @@ class Utils {
// CT 5/31/20: Refactored to use slp-parse library.
async tokenUtxoDetails(utxos) {
try {
// utxo list may have duplicate tx_hash, varying tx_pos
// only need to call decodeOpReturn once for those
const decodeOpReturnCache = {}
const cachedTxValidation = {}
// Throw error if input is not an array.
if (!Array.isArray(utxos)) throw new Error(`Input must be an array.`)
@@ -867,15 +953,32 @@ class Utils {
// If there is no OP_RETURN, mark the UTXO as false.
let slpData = false
try {
slpData = await this.decodeOpReturn(utxo.txid)
slpData = await this.decodeOpReturn(utxo.txid, decodeOpReturnCache)
// console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
} catch (err) {
// console.log(`error: `, err)
// console.log(`error from decodeOpReturn(${utxo.txid}): `, err)
// An error will be thrown if the txid is not SLP.
// Mark as false and continue the loop.
// outAry.push(false)
utxo.isValid = false
outAry.push(utxo)
// If error is for some other reason, like a 429 error, mark utxo as 'null'
// to display the unknown state.
if (
!err.message ||
err.message.indexOf("scriptpubkey not op_return") === -1
) {
// console.log(`error from decodeOpReturn(${utxo.txid}): `, err)
utxo.isValid = null
outAry.push(utxo)
// If error is thrown because there is no OP_RETURN, then it's not
// an SLP UTXO.
// Mark as false and continue the loop.
} else {
utxo.isValid = false
outAry.push(utxo)
}
// Halt the execution of the loop and increase to the next index.
continue
}
@@ -935,7 +1038,10 @@ class Utils {
// If UTXO passes validation, then return formatted token data.
else {
const genesisData = await this.decodeOpReturn(slpData.tokenId)
const genesisData = await this.decodeOpReturn(
slpData.tokenId,
decodeOpReturnCache
)
// console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`)
// Minting Baton
@@ -983,7 +1089,10 @@ class Utils {
// If UTXO passes validation, then return formatted token data.
else {
const genesisData = await this.decodeOpReturn(slpData.tokenId)
const genesisData = await this.decodeOpReturn(
slpData.tokenId,
decodeOpReturnCache
)
// console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`)
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
@@ -1010,7 +1119,11 @@ class Utils {
// Finally, validate the SLP txid with SLPDB.
if (outAry[i].tokenType) {
const isValid = await this.validateTxid(utxo.txid)
var isValid = cachedTxValidation[utxo.txid]
if (isValid == null) {
isValid = await this.validateTxid(utxo.txid)
cachedTxValidation[utxo.txid] = isValid
}
// console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`)
outAry[i].isValid = isValid[0].valid
@@ -1023,6 +1136,149 @@ class Utils {
throw error
}
}
/**
* @api SLP.Utils.hydrateUtxos() hydrateUtxos()
* @apiName hydrateUtxos
* @apiGroup SLP Utils
* @apiDescription Hydrate a UTXO with SLP token metadata.
*
* The same as tokenUtxoDetails(), but uses bch-api to do the heavy lifting,
* which greatly reduces the number of API calls.
*
* Expects an array of UTXO objects as input. Returns an array of equal size.
* Returns UTXO data hydrated with token information.
* If the
* UTXO does not belong to a SLP transaction, it will return an `isValid` property
* set to false.
* If the UTXO is part of an SLP transaction, it will return the UTXO object
* with additional SLP information attached. An `isValid` property will be included.
* If its value is true, the UTXO is a valid SLP UTXO. \
* If the isValid value is null,
* then SLPDB has not yet processed that txid and validity has not been confirmed,
* or a 429 rate-limit error was enountered during the processing of the request.
*
* @apiExample Example usage:
*
* (async () => {
* try {
* const utxos = await bchjs.Electrumx.utxo([
* "bitcoincash:qq6mvsm7l92d77zpymmltvaw09p5uzghyuyx7spygg",
* "bitcoincash:qpjdrs8qruzh8xvusdfmutjx62awcepnhyperm3g89",
* "bitcoincash:qzygn28zpgeemnptkn26xzyuzzfu9l8f9vfvq7kptk"
* ])
*
* const utxoInfo = await bchjs.SLP.Utils.hydrateUtxos(utxos.utxos)
*
* console.log(`${JSON.stringify(utxoInfo, null, 2)}`)
* } catch (error) {
* console.error(error)
* }
* })()
*
* // returns
* {
* "slpUtxos": [
* {
* "utxos": [
* {
* "height": 654522,
* "tx_hash": "516e763932061f9e868652d727045b714db1ecac459e84cd52b5b4cb39572ecc",
* "tx_pos": 0,
* "value": 6000,
* "satoshis": 6000,
* "txid": "516e763932061f9e868652d727045b714db1ecac459e84cd52b5b4cb39572ecc",
* "vout": 0,
* "isValid": false
* }
* ],
* "address": "bitcoincash:qq6mvsm7l92d77zpymmltvaw09p5uzghyuyx7spygg"
* },
* {
* "utxos": [
* {
* "height": 654522,
* "tx_hash": "8ec01d851d9df9fb4b4331275e2ff680257c224100d0081cec6fbeedf982f738",
* "tx_pos": 1,
* "value": 546,
* "satoshis": 546,
* "txid": "8ec01d851d9df9fb4b4331275e2ff680257c224100d0081cec6fbeedf982f738",
* "vout": 1,
* "utxoType": "token",
* "transactionType": "send",
* "tokenId": "a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2",
* "tokenTicker": "TROUT",
* "tokenName": "Trout's test token",
* "tokenDocumentUrl": "troutsblog.com",
* "tokenDocumentHash": "",
* "decimals": 2,
* "tokenType": 1,
* "tokenQty": 10,
* "isValid": true
* }
* ],
* "address": "bitcoincash:qpjdrs8qruzh8xvusdfmutjx62awcepnhyperm3g89"
* },
* {
* "utxos": [
* {
* "height": 654522,
* "tx_hash": "072a1e2c2d5f1309bf4eef7f88684e4ecd544a903b386b07f3e04b91b13d8af1",
* "tx_pos": 0,
* "value": 6999,
* "satoshis": 6999,
* "txid": "072a1e2c2d5f1309bf4eef7f88684e4ecd544a903b386b07f3e04b91b13d8af1",
* "vout": 0,
* "isValid": false
* },
* {
* "height": 654522,
* "tx_hash": "a72db6a0883ecb8e379f317231b2571e41e041b7b1107e3e54c2e0b3386ac6ca",
* "tx_pos": 1,
* "value": 546,
* "satoshis": 546,
* "txid": "a72db6a0883ecb8e379f317231b2571e41e041b7b1107e3e54c2e0b3386ac6ca",
* "vout": 1,
* "utxoType": "token",
* "transactionType": "send",
* "tokenId": "6201f3efe486c577433622817b99645e1d473cd3882378f9a0efc128ab839a82",
* "tokenTicker": "VALENTINE",
* "tokenName": "Valentine day token",
* "tokenDocumentUrl": "fullstack.cash",
* "tokenDocumentHash": "",
* "decimals": 2,
* "tokenType": 1,
* "tokenQty": 5,
* "isValid": true
* }
* ],
* "address": "bitcoincash:qzygn28zpgeemnptkn26xzyuzzfu9l8f9vfvq7kptk"
* }
* ]
* }
*
*/
// Same as tokenUtxoDetails(), but reduces API calls by having bch-api server
// do the heavy lifting.
async hydrateUtxos(utxos) {
try {
// Throw error if input is not an array.
if (!Array.isArray(utxos)) throw new Error(`Input must be an array.`)
const response = await axios.post(
`${this.restURL}slp/hydrateUtxos`,
{
utxos: utxos
},
_this.axiosOptions
)
return response.data
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
}
}
}
module.exports = Utils
+22 -3
View File
@@ -271,7 +271,8 @@ describe(`#ElectrumX`, () => {
describe(`#txData`, () => {
it(`should GET details for a single transaction`, async () => {
const txid = "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251"
const txid =
"4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251"
const result = await bchjs.Electrumx.txData(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
@@ -318,8 +319,11 @@ describe(`#ElectrumX`, () => {
it(`should throw error on array size rate limit`, async () => {
try {
const txids = []
for (let i = 0; i < 25; i++)
txids.push("4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251")
for (let i = 0; i < 25; i++) {
txids.push(
"4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251"
)
}
await bchjs.Electrumx.txData(txids)
@@ -331,4 +335,19 @@ describe(`#ElectrumX`, () => {
}
})
})
describe(`#broadcast`, () => {
it(`should broadcast a single transaction`, async () => {
const txHex =
"020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000"
try {
await bchjs.Electrumx.broadcast(txHex)
} catch (err) {
assert.property(err, "success")
assert.equal(err.success, false)
assert.include(err.error, "Missing inputs")
}
})
})
})
+145
View File
@@ -492,6 +492,151 @@ describe(`#SLP`, () => {
])
})
})
describe("#hydrateUtxos", () => {
it("should throw an error if input is not an array", async () => {
try {
const utxos = 1234
await bchjs.SLP.Utils.hydrateUtxos(utxos)
assert.equal(true, false, "Uh oh. Code path should not end here.")
} catch (err) {
// console.log(`Error: `, err)
assert.include(err.message, `Input must be an array.`)
}
})
it("should hydrate UTXOs", async () => {
const utxos = [
{
utxos: [
{
txid:
"d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56",
vout: 3,
value: "6816",
height: 606848,
confirmations: 13,
satoshis: 6816
},
{
txid:
"d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56",
vout: 2,
value: "546",
height: 606848,
confirmations: 13,
satoshis: 546
}
]
}
]
const result = await bchjs.SLP.Utils.hydrateUtxos(utxos)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
// Test the general structure of the output.
assert.isArray(result.slpUtxos)
assert.equal(result.slpUtxos.length, 1)
assert.equal(result.slpUtxos[0].utxos.length, 2)
// Test the non-slp UTXO.
assert.property(result.slpUtxos[0].utxos[0], "txid")
assert.property(result.slpUtxos[0].utxos[0], "vout")
assert.property(result.slpUtxos[0].utxos[0], "value")
assert.property(result.slpUtxos[0].utxos[0], "height")
assert.property(result.slpUtxos[0].utxos[0], "confirmations")
assert.property(result.slpUtxos[0].utxos[0], "satoshis")
assert.property(result.slpUtxos[0].utxos[0], "isValid")
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
// Test the slp UTXO.
assert.property(result.slpUtxos[0].utxos[1], "txid")
assert.property(result.slpUtxos[0].utxos[1], "vout")
assert.property(result.slpUtxos[0].utxos[1], "value")
assert.property(result.slpUtxos[0].utxos[1], "height")
assert.property(result.slpUtxos[0].utxos[1], "confirmations")
assert.property(result.slpUtxos[0].utxos[1], "satoshis")
assert.property(result.slpUtxos[0].utxos[1], "isValid")
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
assert.property(result.slpUtxos[0].utxos[1], "transactionType")
assert.property(result.slpUtxos[0].utxos[1], "tokenId")
assert.property(result.slpUtxos[0].utxos[1], "tokenTicker")
assert.property(result.slpUtxos[0].utxos[1], "tokenName")
assert.property(result.slpUtxos[0].utxos[1], "tokenDocumentUrl")
assert.property(result.slpUtxos[0].utxos[1], "tokenDocumentHash")
assert.property(result.slpUtxos[0].utxos[1], "decimals")
assert.property(result.slpUtxos[0].utxos[1], "tokenType")
assert.property(result.slpUtxos[0].utxos[1], "tokenQty")
})
it("should process data directly from Electrumx", async () => {
const addrs = [
"bitcoincash:qq6mvsm7l92d77zpymmltvaw09p5uzghyuyx7spygg",
"bitcoincash:qpjdrs8qruzh8xvusdfmutjx62awcepnhyperm3g89",
"bitcoincash:qzygn28zpgeemnptkn26xzyuzzfu9l8f9vfvq7kptk"
]
const utxos = await bchjs.Electrumx.utxo(addrs)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
const result = await bchjs.SLP.Utils.hydrateUtxos(utxos.utxos)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
// Test the general structure of the output.
assert.isArray(result.slpUtxos)
assert.equal(result.slpUtxos.length, 3)
assert.equal(result.slpUtxos[0].utxos.length, 1)
assert.equal(result.slpUtxos[1].utxos.length, 1)
assert.equal(result.slpUtxos[2].utxos.length, 2)
try {
// Test the expected values.
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
assert.equal(result.slpUtxos[1].utxos[0].isValid, true)
assert.equal(result.slpUtxos[1].utxos[0].tokenTicker, "TROUT")
assert.equal(result.slpUtxos[2].utxos[0].isValid, false)
assert.equal(result.slpUtxos[2].utxos[1].isValid, true)
assert.equal(result.slpUtxos[2].utxos[1].tokenTicker, "VALENTINE")
} catch (err) {
console.error(
'The hydrateUtxos call may hitting rate limits, or SLPDB may be having issues if "isValid" results are "null"'
)
console.log("Error: ", err)
}
})
})
describe("#validateTxid2", () => {
it("should invalidate a known invalid TXID", async () => {
const txid =
"f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a"
const result = await bchjs.SLP.Utils.validateTxid2(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, "txid")
assert.equal(result.txid, txid)
assert.property(result, "isValid")
assert.equal(result.isValid, false)
})
it("should validate a known valid TXID", async () => {
const txid =
"3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488"
const result = await bchjs.SLP.Utils.validateTxid2(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, "txid")
assert.equal(result.txid, txid)
assert.property(result, "isValid")
assert.equal(result.isValid, true)
})
})
})
describe("#tokentype1", () => {
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
export RESTURL=https://bchn-free-main.fullstack.cash/v3/
#export RESTURL=http://localhost:3000/v3/
cd test/integration/
mocha --timeout 30000 blockchain.js control.js electrumx.js ninsight.js openbazaar.js price.js rawtransaction.js slp.js util.js
+14
View File
@@ -335,4 +335,18 @@ describe(`#ElectrumX`, () => {
}
})
})
describe(`#broadcast`, () => {
it(`should broadcast a single transaction`, async () => {
const txHex = "020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000"
try {
await bchjs.Electrumx.broadcast(txHex)
} catch (err) {
assert.property(err, "success")
assert.equal(err.success, false)
assert.include(err.error, "Missing inputs")
}
})
})
})
+53 -3
View File
@@ -264,17 +264,38 @@ describe(`#ElectrumX`, () => {
describe(`#blockHeader`, () => {
it(`should throw an error for improper height input`, async () => {
try {
// Mock network calls.
sandbox.stub(axios, "get").rejects({
response: {
data: {
success: false,
error: "height must be a positive number"
}
}
})
const height = -10
await bchjs.Electrumx.blockHeader(height)
assert.equal(true, false, "Unexpected result!")
} catch (err) {
// console.log(`err: `, err)
assert.include(err.error, `height must be a positive number`)
assert.include(err.message, `height must be a positive number`)
}
})
it(`should throw an error for improper count input`, async () => {
try {
// Mock network calls.
sandbox.stub(axios, "get").rejects({
response: {
data: {
success: false,
error: "count must be a positive number"
}
}
})
const height = 42
const count = -10
@@ -282,7 +303,7 @@ describe(`#ElectrumX`, () => {
assert.equal(true, false, "Unexpected result!")
} catch (err) {
// console.log(`err: `, err)
assert.include(err.error, `count must be a positive number`)
assert.include(err.message, `count must be a positive number`)
}
})
@@ -319,7 +340,8 @@ describe(`#ElectrumX`, () => {
// Stub the network call.
sandbox.stub(axios, "get").resolves({ data: mockData.details })
const txid = "4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251"
const txid =
"4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251"
const result = await bchjs.Electrumx.txData(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
@@ -368,4 +390,32 @@ 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 {
const txHex = 12345
await bchjs.Electrumx.broadcast(txHex)
assert.equal(true, false, "Unexpected result!")
} catch (err) {
// console.log(`err: `, err)
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 })
const tx = mockData.details
const txid = tx.details.txid
const result = await bchjs.Electrumx.broadcast(tx.details.hex)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result, "success")
assert.equal(result.success, true)
assert.property(result, "txid")
assert.equal(result.txid, txid)
})
})
})
+7 -1
View File
@@ -250,6 +250,11 @@ const detailsArray = {
]
}
const broadcast = {
success: true,
txid: txDetails.txid
}
module.exports = {
utxo,
utxos,
@@ -261,5 +266,6 @@ module.exports = {
unconfirmedArray,
blockHeaders,
details,
detailsArray
detailsArray,
broadcast
}
+154
View File
@@ -1695,6 +1695,46 @@ describe("#SLP Utils", () => {
assert2.equal(data[2].isValid, false)
})
it("should return null value when 429 recieved", async () => {
const utxos = [
{
height: 654522,
tx_hash:
"072a1e2c2d5f1309bf4eef7f88684e4ecd544a903b386b07f3e04b91b13d8af1",
tx_pos: 0,
value: 6999,
satoshis: 6999,
txid:
"072a1e2c2d5f1309bf4eef7f88684e4ecd544a903b386b07f3e04b91b13d8af1",
vout: 0
},
{
height: 654522,
tx_hash:
"a72db6a0883ecb8e379f317231b2571e41e041b7b1107e3e54c2e0b3386ac6ca",
tx_pos: 1,
value: 546,
satoshis: 546,
txid:
"a72db6a0883ecb8e379f317231b2571e41e041b7b1107e3e54c2e0b3386ac6ca",
vout: 1
}
]
sandbox.stub(slp.Utils, "decodeOpReturn").rejects({
error:
"Too many requests. Your limits are currently 3 requests per minute. Increase rate limits at https://fullstack.cash"
})
const data = await slp.Utils.tokenUtxoDetails(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
// Values should be 'null' to signal that a determination could not be made
// due to a throttling issue.
assert.equal(data[0].isValid, null)
assert.equal(data[1].isValid, null)
})
})
describe("#txDetails", () => {
@@ -1760,4 +1800,118 @@ describe("#SLP Utils", () => {
])
})
})
describe("#hydrateUtxos", () => {
it("should throw an error if input is not an array", async () => {
try {
const utxos = 1234
await slp.Utils.hydrateUtxos(utxos)
assert2.equal(true, false, "Uh oh. Code path should not end here.")
} catch (err) {
// console.log(`Error: `, err)
assert2.include(err.message, `Input must be an array.`)
}
})
})
describe("#validateTxid2", () => {
it("should throw an error if the input is an array", async () => {
try {
const txid = [
"f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a"
]
await slp.Utils.validateTxid2(txid)
assert2.equal(true, false, "Unexpected result")
} catch (err) {
// console.log("err: ", err)
assert2.include(err.message, "txid must be 64 character string")
}
})
it("should throw an error for a malformed txid", async () => {
try {
const txid =
"f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783"
await slp.Utils.validateTxid2(txid)
assert2.equal(true, false, "Unexpected result")
} catch (err) {
// console.log("err: ", err)
assert2.include(err.message, "txid must be 64 character string")
}
})
it("should invalidate a known invalid TXID", async () => {
const txid =
"f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a"
// Mock live network calls.
sandbox.stub(axios, "get").resolves({
data: {
txid: txid,
isValid: false,
msg: ""
}
})
const result = await slp.Utils.validateTxid2(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert2.property(result, "txid")
assert2.equal(result.txid, txid)
assert2.property(result, "isValid")
assert2.equal(result.isValid, false)
})
it("should validate a known valid TXID", async () => {
const txid =
"3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488"
// Mock live network calls.
sandbox.stub(axios, "get").resolves({
data: {
txid: txid,
isValid: true,
msg: ""
}
})
const result = await slp.Utils.validateTxid2(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert2.property(result, "txid")
assert2.equal(result.txid, txid)
assert2.property(result, "isValid")
assert2.equal(result.isValid, true)
})
// slp-validate can take a long time. bch-api cuts it off if it fails to
// return is less than 10 seconds. This test case handle this situation.
it("should handle timeout errors", async () => {
try {
const txid =
"eacb1085dfa296fef6d4ae2c0f4529a1bef096dd2325bdcc6dcb5241b3bdb579"
// Mock live network calls.
sandbox.stub(axios, "get").rejects({
error:
"Network error: Could not communicate with full node or other external service."
})
await slp.Utils.validateTxid2(txid)
assert2.equal(true, false, "Unexpected result")
} catch (err) {
// console.log("err: ", err)
assert2.include(err.message, "slp-validate timed out")
}
})
})
})