Merge pull request #28 from christroutner/unstable

Refactoring SLP endpoints
This commit is contained in:
Chris Troutner
2020-05-31 09:36:20 -07:00
committed by GitHub
8 changed files with 1444 additions and 162 deletions
+65
View File
@@ -5,6 +5,8 @@ const Address = require("./address")
const Script = require("../script")
const BigNumber = require("bignumber.js")
const slpMdm = require("slp-mdm")
// const addy = new Address()
let addy
const TransactionBuilder = require("../transaction-builder")
@@ -105,6 +107,69 @@ class TokenType1 {
}
}
// New version using slp-mdm
generateSendOpReturn2(tokenUtxos, sendQty) {
try {
const tokenId = tokenUtxos[0].tokenId
const decimals = tokenUtxos[0].decimals
// Calculate the total amount of tokens owned by the wallet.
let totalTokens = 0
for (let i = 0; i < tokenUtxos.length; i++)
totalTokens += tokenUtxos[i].tokenQty
const change = totalTokens - sendQty
// console.log(`change: ${change}`)
let script
let outputs = 1
// The normal case, when there is token change to return to sender.
if (change > 0) {
outputs = 2
// Convert the send quantity to the format expected by slp-mdm.
let baseQty = new BigNumber(sendQty).times(10 ** decimals)
baseQty = baseQty.absoluteValue()
baseQty = Math.floor(baseQty)
baseQty = baseQty.toString()
console.log(`baseQty: `, baseQty)
// Convert the change quantity to the format expected by slp-mdm.
let baseChange = new BigNumber(change).times(10 ** decimals)
baseChange = baseChange.absoluteValue()
baseChange = Math.floor(baseChange)
baseChange = baseChange.toString()
console.log(`baseChange: `, baseChange)
// Generate the OP_RETURN as a Buffer.
script = slpMdm.TokenType1.send(tokenId, [
new slpMdm.BN(baseQty),
new slpMdm.BN(baseChange)
])
//
// Corner case, when there is no token change to send back.
} else {
let baseQty = new BigNumber(sendQty).times(10 ** decimals)
baseQty = baseQty.absoluteValue()
baseQty = Math.floor(baseQty)
baseQty = baseQty.toString()
console.log(`baseQty: `, baseQty)
// console.log(`baseQty: ${baseQty.toString()}`)
// Generate the OP_RETURN as a Buffer.
script = slpMdm.TokenType1.send(tokenId, [new slpMdm.BN(baseQty)])
}
return { script, outputs }
} catch (err) {
console.log(`Error in generateSendOpReturn2()`)
throw err
}
}
/**
* @api SLP.TokenType1.generateBurnOpReturn() generateBurnOpReturn() - OP_RETURN code for SLP Send tx
* @apiName generateBurnOpReturn
+329 -67
View File
@@ -1050,38 +1050,54 @@ class Utils {
* @apiGroup SLP
* @apiDescription Determine if UTXO belongs to an SLP transaction.
*
* Expects an array of UTXO objects as input. Returns an array of Boolean
* values indicating if a UTXO is associated with SLP tokens (true) or not
* (false).
* Note: There is no way to validate SLP UTXOs without inspecting the OP_RETURN.
* If a UTXO returns false when its txid is passed to validateTxid(), then the
* UTXO is not associated with SLP. This is a fast and quick check.
* If a UTXO returns true though, the OP_RETURN has to be inspected to determine
* for sure that it *is* associated with an SLP transaction not just change.
* Expects an array of UTXO objects as input. Returns the same array with
* additional properties for each UTXO, including an isSlp property which
* will be true or false indicating if the UTXO belongs to a valid SLP
* transaction.
*
* @apiExample Example usage:
*
* (async () => {
* try {
* const u = await bchjs.Insight.utxos(`bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05`)
* const utxos = u.utxos
* const addr = 'bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4'
* const utxos = await bchjs.Blockbook.utxo(addr)
*
* const isSLPUtxo = await bchjs.SLP.Utils.isTokenUtxo(utxos)
*
* console.log(`isSLPUtxo: ${JSON.stringify(isSLPUtxo,null,2)}`)
* console.log(`${JSON.stringify(isSLPUtxo,null,2)}`)
* } catch (error) {
* console.error(error)
* }
* })()
*
* // returns
* {
* transactionId: 'c7078a6c7400518a513a0bde1f4158cf740d08d3b5bfb19aa7b6657e2f4160de',
* inputTotal: 100000100,
* outputTotal: 100000000,
* burnTotal: 100
* }
* [
* {
* "txid": "99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730",
* "vout": 1,
* "value": "546",
* "height": 636885,
* "confirmations": 123,
* "satoshis": 546,
* "isSlp": true
* },
* {
* "txid": "2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7",
* "vout": 0,
* "value": "600",
* "height": 636885,
* "confirmations": 123,
* "satoshis": 600,
* "isSlp": false
* }
* ]
*/
// Note: There is no way to validate SLP UTXOs without inspecting the OP_RETURN.
// If a UTXO returns false when its txid is passed to validateTxid(), then the
// UTXO is not associated with SLP. This is a fast and quick check.
// If a UTXO returns true though, the OP_RETURN has to be inspected to determine
// for sure that it *is* associated with an SLP transaction not just change.
async isTokenUtxo(utxos) {
try {
// Throw error if input is not an array.
@@ -1092,59 +1108,109 @@ class Utils {
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
// Throw error if utxo does not have a satoshis property.
if (!utxo.satoshis)
throw new Error(`utxo ${i} does not have a satoshis property.`)
// Throw error if utxo does not have a txid property.
if (!utxo.txid)
throw new Error(`utxo ${i} does not have a txid property.`)
}
// Create an array of txid strings to feed to validateTxid
const txids = utxos.map(x => x.txid)
// Validate the array of txids.
let validations = await this.validateTxid(txids)
//console.log(`validations: ${JSON.stringify(validations, null, 2)}`)
// Extract the boolean result
validations = validations.map(x => x.valid)
// Loop through each element in the array and validate the input before
// further processing.
for (let i = 0; i < utxos.length; i++) {
const thisValidation = validations[i]
const utxo = utxos[i]
// Only need to worry about validations that are still true.
if (thisValidation) {
const slpData = await this.decodeOpReturn(utxo.txid)
//console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
// Handle Genesis and Mint SLP transactions.
if (
slpData.transactionType === "genesis" ||
slpData.transactionType === "mint"
) {
if (
utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
if (!utxo.satoshis) {
// If Electrumx, convert the value to satoshis.
if (utxo.value) {
utxo.satoshis = utxo.value
}
// If there is neither a satoshis or value property, throw an error.
else {
throw new Error(
`utxo ${i} does not have a satoshis or value property.`
)
// Can safely be marked as false.
validations[i] = false
} else if (slpData.transactionType === "send") {
// Filter out any vouts that match.
const voutMatch = slpData.spendData.filter(
x => utxo.vout === x.vout
)
//console.log(`voutMatch: ${JSON.stringify(voutMatch, null, 2)}`)
// If there are no vout matches, the UTXO can safely be marked as false.
if (voutMatch.length === 0) validations[i] = false
}
}
if (!utxo.txid) {
// If Electrumx, convert the tx_hash property to txid.
if (utxo.tx_hash) {
utxo.txid = utxo.tx_hash
}
// If there is neither a txid or tx_hash property, throw an error.
else {
throw new Error(
`utxo ${i} does not have a txid or tx_hash property.`
)
}
}
if (!Number.isInteger(utxo.vout)) {
if (Number.isInteger(utxo.tx_pos)) {
utxo.vout = utxo.tx_pos
} else {
throw new Error(
`utxo ${i} does not have a vout or tx_pos property.`
)
}
}
}
const validatedSlpUtxos = []
// Loop through each utxo.
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
// console.log(`thisValidation: `, thisValidation)
// console.log(`utxo: `, utxo)
// Validate the txid for the UTXO using SLPDB.
const isValid = await _this.validateTxid(utxo.txid)
// console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`)
// If SLPDB says the UTXOs TXID is invalid, mark the UTXO and move on.
if (!isValid) {
utxo.isSlp = false
validatedSlpUtxos.push(utxo)
continue
}
// Decode the OP_RETURN.
let slpData = {}
try {
slpData = await _this.decodeOpReturn2(utxo.txid)
// console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
} catch (err) {
// console.log(`decodeOpReturn2 error: `, err)
// If that function throws an error, then mark the UTXO as false.
utxo.isSlp = false
validatedSlpUtxos.push(utxo)
continue
}
const txType = slpData.txType.toLowerCase()
// Handle Genesis and Mint SLP transactions.
if (txType === "genesis" || txType === "mint") {
if (
utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
) {
// Can safely be marked as false.
utxo.isSlp = false
validatedSlpUtxos.push(utxo)
continue
}
//
// Handle Send transactions.
} else if (txType === "send") {
const slpOutputs = slpData.amounts.length
// If the UTXO vout value does not line up with an SLP output in the
// OP_RETURN, then it is not an SLP UTXO.
if (utxo.vout > slpOutputs) {
utxo.isSlp = false
validatedSlpUtxos.push(utxo)
continue
}
}
// If code path gets to this point and it hasn't been marked false,
// then it can safely be marked as true.
utxo.isSlp = true
validatedSlpUtxos.push(utxo)
// Below is deprecated, but kept for posterity:
// Invalidate the utxo if it contains more than dust, since SLP token
// UTXOs only contain dust values. <--- NOT TRUE
@@ -1153,7 +1219,7 @@ class Utils {
//if (utxo.satoshis > 546) validations[i] = false
}
return validations
return validatedSlpUtxos
} catch (error) {
if (error.response && error.response.data) throw error.response.data
throw error
@@ -1377,6 +1443,202 @@ class Utils {
throw error
}
}
async tokenUtxoDetails2(utxos) {
try {
// Throw error if input is not an array.
if (!Array.isArray(utxos)) throw new Error(`Input must be an array.`)
// Loop through each element in the array and validate the input before
// further processing.
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
if (!utxo.satoshis) {
// If Electrumx, convert the value to satoshis.
if (utxo.value) {
utxo.satoshis = utxo.value
}
// If there is neither a satoshis or value property, throw an error.
else {
throw new Error(
`utxo ${i} does not have a satoshis or value property.`
)
}
}
if (!utxo.txid) {
// If Electrumx, convert the tx_hash property to txid.
if (utxo.tx_hash) {
utxo.txid = utxo.tx_hash
}
// If there is neither a txid or tx_hash property, throw an error.
else {
throw new Error(
`utxo ${i} does not have a txid or tx_hash property.`
)
}
}
if (!Number.isInteger(utxo.vout)) {
if (Number.isInteger(utxo.tx_pos)) {
utxo.vout = utxo.tx_pos
} else {
throw new Error(
`utxo ${i} does not have a vout or tx_pos property.`
)
}
}
}
// Output Array
const outAry = []
// Loop through each utxo
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i]
// Get raw transaction data from the full node and attempt to decode
// the OP_RETURN data.
// If there is no OP_RETURN, mark the UTXO as false.
let slpData = false
try {
slpData = await this.decodeOpReturn2(utxo.txid)
// console.log(`slpData: ${JSON.stringify(slpData, null, 2)}`)
} catch (err) {
// console.log(`error: `, err)
// An error will be thrown if the txid is not SLP.
// Mark as false and continue the loop.
outAry.push(false)
continue
}
const txType = slpData.txType.toLowerCase()
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
// If there is an OP_RETURN, attempt to decode it.
// Handle Genesis SLP transactions.
if (txType === "genesis") {
if (
utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
) {
// Can safely be marked as false.
outAry[i] = false
}
// If this is a valid SLP UTXO, then return the decoded OP_RETURN data.
else {
utxo.tokenType = "minting-baton"
utxo.tokenId = utxo.txid
utxo.tokenTicker = slpData.ticker
utxo.tokenName = slpData.name
utxo.tokenDocumentUrl = slpData.documentUri
utxo.tokenDocumentHash = slpData.documentHash
utxo.decimals = slpData.decimals
// something
outAry[i] = utxo
}
}
// Handle Mint SLP transactions.
if (txType === "mint") {
if (
utxo.vout !== slpData.mintBatonVout && // UTXO is not a mint baton output.
utxo.vout !== 1 // UTXO is not the reciever of the genesis or mint tokens.
) {
// Can safely be marked as false.
outAry[i] = false
}
// If UTXO passes validation, then return formatted token data.
else {
const genesisData = await this.decodeOpReturn2(slpData.tokenId)
// console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`)
// Hydrate the UTXO object with information about the SLP token.
utxo.utxoType = "token"
utxo.transactionType = "mint"
utxo.tokenId = slpData.tokenId
utxo.tokenTicker = genesisData.ticker
utxo.tokenName = genesisData.name
utxo.tokenDocumentUrl = genesisData.documentUri
utxo.tokenDocumentHash = genesisData.documentHash
utxo.decimals = genesisData.decimals
utxo.mintBatonVout = slpData.mintBatonVout
// Calculate the real token quantity.
utxo.tokenQty =
Number(slpData.qty) / Math.pow(10, genesisData.decimals)
outAry[i] = utxo
}
}
// Handle Send SLP transactions.
if (txType === "send") {
// Filter out any vouts that match.
// const voutMatch = slpData.spendData.filter(x => utxo.vout === x.vout)
// console.log(`voutMatch: ${JSON.stringify(voutMatch, null, 2)}`)
// If there are no vout matches, the UTXO can safely be marked as false.
// if (voutMatch.length === 0) {
// outAry[i] = false
// }
// Figure out what token quantity is represented by this utxo.
const tokenQty = slpData.amounts[utxo.vout - 1]
// console.log(`tokenQty: `, tokenQty)
if (!tokenQty) {
outAry[i] = false
}
// If UTXO passes validation, then return formatted token data.
else {
const genesisData = await this.decodeOpReturn2(slpData.tokenId)
// console.log(`genesisData: ${JSON.stringify(genesisData, null, 2)}`)
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
// Hydrate the UTXO object with information about the SLP token.
utxo.utxoType = "token"
utxo.transactionType = "send"
utxo.tokenId = slpData.tokenId
utxo.tokenTicker = genesisData.ticker
utxo.tokenName = genesisData.name
utxo.tokenDocumentUrl = genesisData.documentUri
utxo.tokenDocumentHash = genesisData.documentHash
utxo.decimals = genesisData.decimals
// Calculate the real token quantity.
utxo.tokenQty = tokenQty / Math.pow(10, genesisData.decimals)
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
outAry[i] = utxo
}
}
// Finally, validate the SLP txid with SLPDB.
if (outAry[i]) {
const isValid = await this.validateTxid(utxo.txid)
// console.log(`isValid: ${JSON.stringify(isValid, null, 2)}`)
outAry[i].isValid = isValid[0].valid
}
}
return outAry
} catch (error) {
if (error.response && error.response.data) throw error.response.data
throw error
}
}
}
module.exports = Utils
@@ -8,7 +8,7 @@
const assert = require("chai").assert
const RESTURL = `https://api.bchjs.cash/v3/`
const RESTURL = `https://api.fullstack.cash/v3/`
// const RESTURL = `http://localhost:3000/v3/`
const BCHJS = require("../../../src/bch-js")
+37 -10
View File
@@ -9,13 +9,12 @@
const assert = require("chai").assert
// const JWT_TOKEN = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmM5ZTI3YWIwMDI4M2E1MDhkYzQ4OSIsImlhdCI6MTU3MjY0MjM1NCwiZXhwIjoxNTc1MjM0MzU0fQ.6dzUh10UXoQXjJujRZ0AMSBhz0ElM1Cc-rCyb50PDqI`
const JWT_TOKEN =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmM5ZTI3YWIwMDI4M2E1MDhkYzQ4OSIsImlhdCI6MTU3MjgzMjAzNywiZXhwIjoxNTc1NDI0MDM3fQ.FygzeHYGH5vsXurFlI7ZFTmS2eyLDt3iglhFWgIZRxY"
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2YyMDIyMWMxMDAxMmFkOTQwMSIsImVtYWlsIjoiZGVtb0BkZW1vLmNvbSIsImFwaUxldmVsIjoxMCwicmF0ZUxpbWl0IjozLCJpYXQiOjE1OTA1Mjk2NzgsImV4cCI6MTU5MzEyMTY3OH0.4q5MFDCcVx0FusivJPiEIj3w8nhFG7ZV5IhJS7faUlc"
const BCHJS = require("../../../src/bch-js")
const bchjs = new BCHJS({
restURL: `https://api.bchjs.cash/v3/`,
restURL: `https://api.fullstack.cash/v3/`,
// restURL: `http://localhost:3000/v3/`,
apiToken: JWT_TOKEN
})
@@ -29,18 +28,42 @@ describe("#free rate limits", () => {
assert.property(result, "balance")
}).timeout(5000)
it("should throw error when rate limit exceeded 3 RPM for indexer endpoints", async () => {
const addr = "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"
// it("should throw error when rate limit exceeded 3 RPM for indexer endpoints", async () => {
// const addr = "bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"
//
// try {
// for (let i = 0; i < 5; i++) await bchjs.Blockbook.balance(addr)
//
// assert.equal(true, false, "unexpected result")
// } catch (err) {
// // console.log(`err: `, err)
// assert.include(err.error, "Too many requests")
// }
// }).timeout(10000)
it("should allow up to 10 RPM to indexer, then throw error", async () => {
try {
for (let i = 0; i < 5; i++) await bchjs.Blockbook.balance(addr)
const addr = "bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"
assert.equal(true, false, "unexpected result")
for (let i = 0; i < 15; i++) {
const result = await await bchjs.Blockbook.balance(addr)
if (i === 5) {
// console.log(`validating 5th call: ${i}`)
assert.property(result, "address", "more than 3 calls allowed")
}
}
} catch (err) {
// console.log(`err: `, err)
assert.include(err.error, "Too many requests")
assert.include(
err.error,
"currently 10 requests",
"more than 10 not allowed"
)
assert.include(err.error, 10)
assert.notInclude(err.error, 3)
}
}).timeout(10000)
}).timeout(20000)
it("should allow up to 10 RPM to full node, then throw error", async () => {
try {
@@ -55,7 +78,11 @@ describe("#free rate limits", () => {
} catch (err) {
// console.log(`validating after 10th call`)
// console.log(`err: `, err)
assert.include(err.error, "Too many requests", "more than 10 not allowed")
assert.include(
err.error,
"currently 10 requests",
"more than 10 not allowed"
)
assert.include(err.error, 10)
assert.notInclude(err.error, 3)
}
+2 -2
View File
@@ -10,11 +10,11 @@
const assert = require("chai").assert
const JWT_TOKEN =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYmY4MjA1YTYwODliMjliYTlhZjc1OSIsImlhdCI6MTU3MjgzMjEwNywiZXhwIjoxNTc1NDI0MTA3fQ.UD-36TKwN65-zePwNzujGmHeQ60fKqYPunawGxktwws"
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTU5MDQzMjIxNywiZXhwIjoxNTkzMDI0MjE3fQ.R0Euhutd5FHCxLlSzjq1TYsGjI6iua_AguWIeLK2xCU"
const BCHJS = require("../../../src/bch-js")
const bchjs = new BCHJS({
restURL: `https://api.bchjs.cash/v3/`,
restURL: `https://api.fullstack.cash/v3/`,
// restURL: `http://localhost:3000/v3/`,
apiToken: JWT_TOKEN
})
+307 -14
View File
@@ -227,6 +227,101 @@ describe(`#SLP`, () => {
})
})
describe("#isTokenUtxo", () => {
it("should accurately analyze UTXOs from Blockbook", async () => {
const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4"
const utxos = await bchjs.Blockbook.utxo(addr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos)
// console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`)
// Filter out the result that is associated with the known non-slp tx.
const nonSlp = isTokenUtxos.filter(
x =>
x.txid ===
"2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7"
)
const isSlp = isTokenUtxos.filter(
x =>
x.txid ===
"99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730"
)
assert.isArray(isTokenUtxos)
assert.equal(nonSlp[0].isSlp, false)
assert.equal(isSlp[0].isSlp, true)
})
it("should accurately analyze UTXOs from Electrumx", async () => {
try {
const addr = "bitcoincash:qqkpnx2rff4q5jranf3wk9jsprhae9unxckq07gph4"
const utxos = await bchjs.Electrumx.utxo(addr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
const isTokenUtxos = await bchjs.SLP.Utils.isTokenUtxo(utxos.utxos)
// console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`)
// Filter out the result that is associated with the known non-slp tx.
const nonSlp = isTokenUtxos.filter(
x =>
x.tx_hash ===
"2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7"
)
const isSlp = isTokenUtxos.filter(
x =>
x.tx_hash ===
"99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730"
)
assert.isArray(isTokenUtxos)
assert.equal(nonSlp[0].isSlp, false)
assert.equal(isSlp[0].isSlp, true)
} catch (err) {
console.log(`Error: `, err)
}
})
// This captures an important corner-case. When an SLP token is created, the
// change UTXO will contain the same SLP txid, but it is not an SLP UTXO.
it("should return false for change in an SLP token creation transaction", async () => {
const utxos = [
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 3,
amount: 0.00002015,
satoshis: 2015,
height: 594892,
confirmations: 5
},
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 2,
amount: 0.00000546,
satoshis: 546,
height: 594892,
confirmations: 5
}
]
const data = await bchjs.SLP.Utils.isTokenUtxo(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.equal(
data[0].isSlp,
false,
"Change should not be identified as SLP utxo."
)
assert.equal(data[1].isSlp, true, "SLP UTXO correctly identified.")
})
})
describe("#tokenUtxoDetails", () => {
it("should return token details on a valid UTXO", async () => {
const utxos = [
@@ -540,6 +635,218 @@ describe(`#SLP`, () => {
})
})
describe("#tokenUtxoDetails2", () => {
// // This captures an important corner-case. When an SLP token is created, the
// // change UTXO will contain the same SLP txid, but it is not an SLP UTXO.
it("should return details on minting baton from genesis transaction", async () => {
const utxos = [
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 3,
amount: 0.00002015,
satoshis: 2015,
height: 594892,
confirmations: 5
},
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 2,
amount: 0.00000546,
satoshis: 546,
height: 594892,
confirmations: 5
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.equal(data[0], false, "Change UTXO marked as false.")
assert.property(data[1], "txid")
assert.property(data[1], "vout")
assert.property(data[1], "amount")
assert.property(data[1], "satoshis")
assert.property(data[1], "height")
assert.property(data[1], "confirmations")
assert.property(data[1], "tokenType")
assert.property(data[1], "tokenId")
assert.property(data[1], "tokenTicker")
assert.property(data[1], "tokenName")
assert.property(data[1], "tokenDocumentUrl")
assert.property(data[1], "tokenDocumentHash")
assert.property(data[1], "decimals")
assert.property(data[1], "isValid")
assert.equal(data[1].isValid, true)
})
it("should return details for a MINT token utxo", async () => {
// Mock the call to REST API
const utxos = [
{
txid:
"cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 600297,
confirmations: 76
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.property(data[0], "txid")
assert.property(data[0], "vout")
assert.property(data[0], "amount")
assert.property(data[0], "satoshis")
assert.property(data[0], "height")
assert.property(data[0], "confirmations")
assert.property(data[0], "utxoType")
assert.property(data[0], "transactionType")
assert.property(data[0], "tokenId")
assert.property(data[0], "tokenTicker")
assert.property(data[0], "tokenName")
assert.property(data[0], "tokenDocumentUrl")
assert.property(data[0], "tokenDocumentHash")
assert.property(data[0], "decimals")
assert.property(data[0], "mintBatonVout")
assert.property(data[0], "tokenQty")
assert.property(data[0], "isValid")
assert.equal(data[0].isValid, true)
})
it("should return details for a simple SEND SLP token utxo", async () => {
const utxos = [
{
txid:
"fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 596089,
confirmations: 748
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.property(data[0], "txid")
assert.property(data[0], "vout")
assert.property(data[0], "amount")
assert.property(data[0], "satoshis")
assert.property(data[0], "height")
assert.property(data[0], "confirmations")
assert.property(data[0], "utxoType")
assert.property(data[0], "tokenId")
assert.property(data[0], "tokenTicker")
assert.property(data[0], "tokenName")
assert.property(data[0], "tokenDocumentUrl")
assert.property(data[0], "tokenDocumentHash")
assert.property(data[0], "decimals")
assert.property(data[0], "tokenQty")
assert.property(data[0], "isValid")
assert.equal(data[0].isValid, true)
})
it("should handle BCH and SLP utxos in the same TX", async () => {
const 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.tokenUtxoDetails2(utxos)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.equal(result.length, 2)
assert.equal(result[0], false)
assert.equal(result[1].isValid, true)
})
it("should handle problematic utxos", async () => {
const utxos = [
{
txid:
"0e3a217fc22612002031d317b4cecd9b692b66b52951a67b23c43041aefa3959",
vout: 0,
amount: 0.00018362,
satoshis: 18362,
height: 613483,
confirmations: 124
},
{
txid:
"67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 612075,
confirmations: 1532
}
]
const result = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.isArray(result)
assert.equal(result.length, 2)
assert.equal(result[0], false)
assert.equal(result[1].isValid, true)
})
it("should return false for BCH-only UTXOs", async () => {
const utxos = [
{
txid:
"a937f792c7c9eb23b4f344ce5c233d1ac0909217d0a504d71e6b1e4efb864a3b",
vout: 0,
amount: 0.00001,
satoshis: 1000,
confirmations: 0,
ts: 1578424704
},
{
txid:
"53fd141c2e999e080a5860887441a2c45e9cbe262027e2bd2ac998fc76e43c44",
vout: 0,
amount: 0.00001,
satoshis: 1000,
confirmations: 0,
ts: 1578424634
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.isArray(data)
assert.equal(false, data[0])
assert.equal(false, data[1])
})
})
describe("#balancesForAddress", () => {
it(`should throw an error if input is not a string or array of strings`, async () => {
try {
@@ -558,13 +865,6 @@ describe(`#SLP`, () => {
})
it(`should fetch all balances for address: simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9`, async () => {
// Mock the call to rest.bitcoin.com
if (process.env.TEST === "unit") {
sandbox
.stub(axios, "get")
.resolves({ data: mockData.balancesForAddress })
}
const balances = await bchjs.SLP.Utils.balancesForAddress(
"simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9"
)
@@ -586,13 +886,6 @@ describe(`#SLP`, () => {
"simpleledger:qqss4zp80hn6szsa4jg2s9fupe7g5tcg5ucdyl3r57"
]
// Mock the call to rest.bitcoin.com
if (process.env.TEST === "unit") {
sandbox
.stub(axios, "post")
.resolves({ data: mockData.balancesForAddresses })
}
const balances = await bchjs.SLP.Utils.balancesForAddress(addresses)
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
+36
View File
@@ -66,6 +66,7 @@ describe("#SLP TokenType1", () => {
tokenUtxos,
1
)
console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.hasAllKeys(result, ["script", "outputs"])
assert.isNumber(result.outputs)
@@ -153,6 +154,41 @@ describe("#SLP TokenType1", () => {
})
})
describe("#generateSendOpReturn2", () => {
it("should generate send OP_RETURN code", async () => {
// Mock UTXO.
const tokenUtxos = [
{
txid:
"a8eb788b8ddda6faea00e6e2756624b8feb97655363d0400dd66839ea619d36e",
vout: 2,
value: "546",
confirmations: 0,
satoshis: 546,
utxoType: "token",
transactionType: "send",
tokenId:
"497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
tokenTicker: "TOK-CH",
tokenName: "TokyoCash",
tokenDocumentUrl: "",
tokenDocumentHash: "",
decimals: 8,
tokenQty: 7
}
]
const result = await bchjs.SLP.TokenType1.generateSendOpReturn2(
tokenUtxos,
1
)
console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.hasAllKeys(result, ["script", "outputs"])
assert.isNumber(result.outputs)
})
})
describe("#generateBurnOpReturn", () => {
it("should generate burn OP_RETURN code", async () => {
// Mock UTXO.
+667 -68
View File
@@ -721,7 +721,7 @@ describe("#SLP Utils", () => {
}
})
it("should throw error if utxo does not have satoshis property.", async () => {
it("should throw error if utxo does not have satoshis or value property.", async () => {
try {
const utxos = [
{
@@ -749,13 +749,13 @@ describe("#SLP Utils", () => {
} catch (err) {
assert2.include(
err.message,
`utxo 1 does not have a satoshis property`,
`utxo 1 does not have a satoshis or value property`,
"Expected error message."
)
}
})
it("should throw error if utxo does not have txid property.", async () => {
it("should throw error if utxo does not have txid or tx_hash property.", async () => {
try {
const utxos = [
{
@@ -782,7 +782,7 @@ describe("#SLP Utils", () => {
} catch (err) {
assert2.include(
err.message,
`utxo 1 does not have a txid property`,
`utxo 1 does not have a txid or tx_hash property`,
"Expected error message."
)
}
@@ -790,39 +790,96 @@ describe("#SLP Utils", () => {
// This captures an important corner-case. When an SLP token is created, the
// change UTXO will contain the same SLP txid, but it is not an SLP UTXO.
// it("should return false for change in an SLP token creation transaction", async () => {
// // Mock the call to the REST API
// if (process.env.TEST === "unit") {
// // Stub the call to validateTxid
// sandbox.stub(slp.Utils, "validateTxid").resolves([
// {
// txid:
// "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
// valid: true
// },
// {
// txid:
// "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
// valid: true
// }
// ])
//
// // Stub the calls to decodeOpReturn.
// sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({
// tokenType: 1,
// transactionType: "genesis",
// ticker: "SLPSDK",
// name: "SLP SDK example using BITBOX",
// documentUrl: "developer.bitcoin.com",
// documentHash: "",
// decimals: 8,
// mintBatonVout: 2,
// initialQty: 507,
// tokensSentTo:
// "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05",
// batonHolder: "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05"
// })
// }
//
// const utxos = [
// {
// txid:
// "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
// vout: 3,
// amount: 0.00002015,
// satoshis: 2015,
// height: 594892,
// confirmations: 5
// },
// {
// txid:
// "bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
// vout: 2,
// amount: 0.00000546,
// satoshis: 546,
// height: 594892,
// confirmations: 5
// }
// ]
//
// const data = await slp.Utils.isTokenUtxo(utxos)
// //console.log(`data: ${JSON.stringify(data, null, 2)}`)
//
// assert.equal(
// data[0],
// false,
// "Change should not be identified as SLP utxo."
// )
// assert.equal(data[1], true, "SLP UTXO correctly identified.")
// })
it("should return false for change in an SLP token creation transaction", async () => {
// Mock the call to the REST API
if (process.env.TEST === "unit") {
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
valid: true
},
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
valid: true
}
])
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
valid: true
}
])
// Stub the calls to decodeOpReturn.
sandbox.stub(slp.Utils, "decodeOpReturn").resolves({
tokenType: 1,
transactionType: "genesis",
ticker: "SLPSDK",
name: "SLP SDK example using BITBOX",
documentUrl: "developer.bitcoin.com",
documentHash: "",
decimals: 8,
mintBatonVout: 2,
initialQty: 507,
tokensSentTo:
"bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05",
batonHolder: "bitcoincash:qpcqs0n5xap26un2828n55gan2ylj7wavvzeuwdx05"
})
}
// Stub the calls to decodeOpReturn2.
sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({
tokenType: 1,
txType: "GENESIS",
ticker: "SLPSDK",
name: "SLP SDK example using BITBOX",
tokenId:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
documentUri: "developer.bitcoin.com",
documentHash: "",
decimals: 8,
mintBatonVout: 2,
qty: "50700000000"
})
const utxos = [
{
@@ -846,50 +903,38 @@ describe("#SLP Utils", () => {
]
const data = await slp.Utils.isTokenUtxo(utxos)
//console.log(`data: ${JSON.stringify(data, null, 2)}`)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.equal(
data[0],
data[0].isSlp,
false,
"Change should not be identified as SLP utxo."
)
assert.equal(data[1], true, "SLP UTXO correctly identified.")
assert.equal(data[1].isSlp, true, "SLP UTXO correctly identified.")
})
it("should return true for a simple SEND SLP token utxo", async () => {
// Mock the call to the REST API
if (process.env.TEST === "unit") {
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb",
valid: true
}
])
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb",
valid: true
}
])
// Stub the calls to decodeOpReturn.
sandbox.stub(slp.Utils, "decodeOpReturn").resolves({
tokenType: 1,
transactionType: "send",
tokenId:
"497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
spendData: [
{
quantity: "200000000",
sentTo: "bitcoincash:qqll3st8xl0k8cgv8dgrrrkntv6hqdn8huv3xm2ztf",
vout: 1
},
{
quantity: "99887500000000",
sentTo: "bitcoincash:qzv7t2pzn2d0pklnetdjt65crh6fe8vnhuwvhsk2nn",
vout: 2
}
]
})
}
// Stub the calls to decodeOpReturn.
sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({
tokenType: 1,
txType: "SEND",
tokenId:
"497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
amounts: ["200000000", "99887500000000"]
})
// Input object.
const utxos = [
{
txid:
@@ -903,9 +948,90 @@ describe("#SLP Utils", () => {
]
const data = await slp.Utils.isTokenUtxo(utxos)
//console.log(`data: ${JSON.stringify(data, null, 2)}`)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.equal(data[0], true, "Simple send UTXO correctly identified")
assert.equal(data[0].isSlp, true, "Simple send UTXO correctly identified")
})
it("should accurately analyze non-slp UTXO from Electrumx", async () => {
try {
// Mock the call to the REST API
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7",
valid: false
}
])
// Simulate the utxos array returned by Electrumx.
const utxos = [
{
height: 636885,
tx_hash:
"2069e99a90499693e42cd1db82147e3e0acfe5e7315c6cc2f0252432f45300d7",
tx_pos: 0,
value: 600
}
]
const isTokenUtxos = await slp.Utils.isTokenUtxo(utxos)
// console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`)
assert.equal(
isTokenUtxos[0].isSlp,
false,
"Non-slp UTXO correctly identified"
)
} catch (err) {
console.log(`Error: `, err)
}
})
it("should accurately analyze slp UTXO from Electrumx", async () => {
try {
// Mock the call to the REST API
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730",
valid: true
}
])
// Stub the calls to decodeOpReturn.
sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({
tokenType: 1,
txType: "SEND",
tokenId:
"a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2",
amounts: ["100", "198999900"]
})
// Simulate the utxos array returned by Electrumx.
const utxos = [
{
height: 636885,
tx_hash:
"99093e8a19e0a649bf943dbc33d926feb09c02e61258c1bdaf2caffa7183c730",
tx_pos: 1,
value: 546
}
]
const isTokenUtxos = await slp.Utils.isTokenUtxo(utxos)
// console.log(`isTokenUtxos: ${JSON.stringify(isTokenUtxos, null, 2)}`)
assert.equal(
isTokenUtxos[0].isSlp,
true,
"Slp UTXO correctly identified"
)
} catch (err) {
console.log(`Error: `, err)
}
})
})
@@ -1395,6 +1521,479 @@ describe("#SLP Utils", () => {
})
})
describe("#tokenUtxoDetails2", () => {
it("should throw error if input is not an array.", async () => {
try {
await slp.Utils.tokenUtxoDetails2("test")
assert2.equal(true, false, "Unexpected result.")
} catch (err) {
assert2.include(
err.message,
`Input must be an array`,
"Expected error message."
)
}
})
it("should throw error if utxo does not have satoshis or value property.", async () => {
try {
const utxos = [
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 3,
amount: 0.00002015,
satoshis: 2015,
height: 594892,
confirmations: 5
},
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 2,
amount: 0.00000546,
height: 594892,
confirmations: 5
}
]
await slp.Utils.tokenUtxoDetails2(utxos)
assert2.equal(true, false, "Unexpected result.")
} catch (err) {
assert2.include(
err.message,
`utxo 1 does not have a satoshis or value property`,
"Expected error message."
)
}
})
it("should throw error if utxo does not have txid or tx_hash property.", async () => {
try {
const utxos = [
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 3,
amount: 0.00002015,
satoshis: 2015,
height: 594892,
confirmations: 5
},
{
vout: 2,
amount: 0.00000546,
satoshis: 546,
height: 594892,
confirmations: 5
}
]
await slp.Utils.tokenUtxoDetails2(utxos)
assert2.equal(true, false, "Unexpected result.")
} catch (err) {
assert2.include(
err.message,
`utxo 1 does not have a txid or tx_hash property`,
"Expected error message."
)
}
})
// // This captures an important corner-case. When an SLP token is created, the
// // change UTXO will contain the same SLP txid, but it is not an SLP UTXO.
it("should return details on minting baton from genesis transaction", async () => {
// Mock the call to REST API
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
valid: true
}
])
// Stub the calls to decodeOpReturn.
sandbox.stub(slp.Utils, "decodeOpReturn2").resolves({
tokenType: 1,
txType: "GENESIS",
ticker: "SLPSDK",
name: "SLP SDK example using BITBOX",
tokenId:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
documentUri: "developer.bitcoin.com",
documentHash: "",
decimals: 8,
mintBatonVout: 2,
qty: "50700000000"
})
const utxos = [
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 3,
amount: 0.00002015,
satoshis: 2015,
height: 594892,
confirmations: 5
},
{
txid:
"bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90",
vout: 2,
amount: 0.00000546,
satoshis: 546,
height: 594892,
confirmations: 5
}
]
const data = await slp.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert2.equal(data[0], false, "Change UTXO marked as false.")
assert2.property(data[1], "txid")
assert2.property(data[1], "vout")
assert2.property(data[1], "amount")
assert2.property(data[1], "satoshis")
assert2.property(data[1], "height")
assert2.property(data[1], "confirmations")
assert2.property(data[1], "tokenType")
assert2.property(data[1], "tokenId")
assert2.property(data[1], "tokenTicker")
assert2.property(data[1], "tokenName")
assert2.property(data[1], "tokenDocumentUrl")
assert2.property(data[1], "tokenDocumentHash")
assert2.property(data[1], "decimals")
assert2.property(data[1], "isValid")
assert2.equal(data[1].isValid, true)
})
it("should return details for a MINT token utxo", async () => {
// Mock the call to REST API
// Stub the calls to decodeOpReturn.
sandbox
.stub(slp.Utils, "decodeOpReturn2")
.onCall(0)
.resolves({
tokenType: 1,
txType: "MINT",
tokenId:
"38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0",
mintBatonVout: 2,
qty: "1000000000000"
})
.onCall(1)
.resolves({
tokenType: 1,
txType: "GENESIS",
ticker: "PSF",
name: "Permissionless Software Foundation",
tokenId:
"38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0",
documentUri: "psfoundation.cash",
documentHash: "",
decimals: 8,
mintBatonVout: 2,
qty: "1988209163133"
})
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76",
valid: true
}
])
const utxos = [
{
txid:
"cf4b922d1e1aa56b52d752d4206e1448ea76c3ebe69b3b97d8f8f65413bd5c76",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 600297,
confirmations: 76
}
]
const data = await slp.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert2.property(data[0], "txid")
assert2.property(data[0], "vout")
assert2.property(data[0], "amount")
assert2.property(data[0], "satoshis")
assert2.property(data[0], "height")
assert2.property(data[0], "confirmations")
assert2.property(data[0], "utxoType")
assert2.property(data[0], "transactionType")
assert2.property(data[0], "tokenId")
assert2.property(data[0], "tokenTicker")
assert2.property(data[0], "tokenName")
assert2.property(data[0], "tokenDocumentUrl")
assert2.property(data[0], "tokenDocumentHash")
assert2.property(data[0], "decimals")
assert2.property(data[0], "mintBatonVout")
assert2.property(data[0], "tokenQty")
assert2.property(data[0], "isValid")
assert.equal(data[0].isValid, true)
})
it("should return details for a simple SEND SLP token utxo", async () => {
// Mock the call to REST API
// Stub the calls to decodeOpReturn.
sandbox
.stub(slp.Utils, "decodeOpReturn2")
.onCall(0)
.resolves({
tokenType: 1,
txType: "SEND",
tokenId:
"497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
amounts: ["200000000", "99887500000000"]
})
.onCall(1)
.resolves({
tokenType: 1,
txType: "GENESIS",
ticker: "TOK-CH",
name: "TokyoCash",
tokenId:
"497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7",
documentUri: "",
documentHash: "",
decimals: 8,
mintBatonVout: 0,
qty: "2100000000000000"
})
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb",
valid: true
}
])
const utxos = [
{
txid:
"fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 596089,
confirmations: 748
}
]
const data = await slp.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert2.property(data[0], "txid")
assert2.property(data[0], "vout")
assert2.property(data[0], "amount")
assert2.property(data[0], "satoshis")
assert2.property(data[0], "height")
assert2.property(data[0], "confirmations")
assert2.property(data[0], "utxoType")
assert2.property(data[0], "tokenId")
assert2.property(data[0], "tokenTicker")
assert2.property(data[0], "tokenName")
assert2.property(data[0], "tokenDocumentUrl")
assert2.property(data[0], "tokenDocumentHash")
assert2.property(data[0], "decimals")
assert2.property(data[0], "tokenQty")
assert2.property(data[0], "isValid")
assert.equal(data[0].isValid, true)
})
it("should handle BCH and SLP utxos in the same TX", async () => {
// Mock external dependencies.
sandbox
.stub(slp.Utils, "validateTxid")
.resolves(mockData.mockDualValidation)
sandbox
.stub(slp.Utils, "decodeOpReturn2")
.onCall(0)
.resolves({
tokenType: 1,
txType: "SEND",
tokenId:
"dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d",
amounts: ["1", "5"]
})
.onCall(1)
.resolves({
tokenType: 1,
txType: "SEND",
tokenId:
"dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d",
amounts: ["1", "5"]
})
.onCall(2)
.resolves({
tokenType: 1,
txType: "GENESIS",
ticker: "TAP",
name: "Thoughts and Prayers",
tokenId:
"dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d",
documentUri: "",
documentHash: "",
decimals: 0,
mintBatonVout: 2,
qty: "1000000"
})
const 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 slp.Utils.tokenUtxoDetails2(utxos)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert2.isArray(result)
assert2.equal(result.length, 2)
assert2.equal(result[0], false)
assert.equal(result[1].isValid, true)
})
it("should handle problematic utxos", async () => {
// Mock external dependencies.
// Stub the calls to decodeOpReturn.
sandbox
.stub(slp.Utils, "decodeOpReturn2")
.onCall(0)
.throws({ message: "scriptpubkey not op_return" })
.onCall(1)
.resolves({
tokenType: 1,
txType: "SEND",
tokenId:
"f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f",
amounts: ["5000000", "395010942"]
})
.onCall(2)
.resolves({
tokenType: 1,
txType: "GENESIS",
ticker: "AUDC",
name: "AUD Coin",
tokenId:
"f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f",
documentUri: "audcoino@gmail.com",
documentHash: "",
decimals: 6,
mintBatonVout: 0,
qty: "2000000000000000000"
})
// Stub the call to validateTxid
sandbox.stub(slp.Utils, "validateTxid").resolves([
{
txid:
"67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19",
valid: true
}
])
const utxos = [
{
txid:
"0e3a217fc22612002031d317b4cecd9b692b66b52951a67b23c43041aefa3959",
vout: 0,
amount: 0.00018362,
satoshis: 18362,
height: 613483,
confirmations: 124
},
{
txid:
"67fd3c7c3a6eb0fea9ab311b91039545086220f7eeeefa367fa28e6e43009f19",
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 612075,
confirmations: 1532
}
]
const result = await slp.Utils.tokenUtxoDetails2(utxos)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert2.isArray(result)
assert2.equal(result.length, 2)
assert2.equal(result[0], false)
assert2.equal(result[1].isValid, true)
})
it("should return false for BCH-only UTXOs", async () => {
// Mock live network calls
if (process.env.TEST === "unit") {
sandbox
.stub(slp.Utils, "decodeOpReturn2")
.throws(new Error("scriptpubkey not op_return"))
}
const utxos = [
{
txid:
"a937f792c7c9eb23b4f344ce5c233d1ac0909217d0a504d71e6b1e4efb864a3b",
vout: 0,
amount: 0.00001,
satoshis: 1000,
confirmations: 0,
ts: 1578424704
},
{
txid:
"53fd141c2e999e080a5860887441a2c45e9cbe262027e2bd2ac998fc76e43c44",
vout: 0,
amount: 0.00001,
satoshis: 1000,
confirmations: 0,
ts: 1578424634
}
]
const data = await slp.Utils.tokenUtxoDetails2(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert2.isArray(data)
assert2.equal(false, data[0])
assert2.equal(false, data[1])
})
})
describe("#txDetails", () => {
it("should throw an error if txid is not included", async () => {
try {