feat(tokenUtxoDetailsWL): tokenUtxoDetails that use whitelist SLPDB

This commit is contained in:
Chris Troutner
2021-02-09 07:22:01 -08:00
parent 95c88b5f29
commit 36432af7d5
4 changed files with 274 additions and 39 deletions
+2 -2
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 '#waterfallValidateTxid' test/unit/",
"test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#tokenUtxoDetailsWL' test/integration/",
"test:temp2": "mocha --timeout=30000 -g '#tokenUtxoDetailsWL' 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",
+89
View File
@@ -1120,6 +1120,95 @@ class Utils {
}
}
/**
* @api SLP.Utils.tokenUtxoDetailsWL() tokenUtxoDetailsWL()
* @apiName tokenUtxoDetailsWL
* @apiGroup SLP Utils
* @apiDescription
*
* Same as tokenUtxoDetails(), but it only uses the whitelist SLPDB to
* validate UTXOs. This will result in a lot of `isValid: null` values,
* but much more performant handling of SLP tokens. Some wallet apps prefer
* the scaling performance over the breadth of supported tokens.
*
*/
async tokenUtxoDetailsWL (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]
// Ensure the UTXO has a txid or tx_hash property.
if (!utxo.txid) {
// If Electrumx, convert the tx_hash property to txid.
if (utxo.tx_hash) {
utxo.txid = utxo.tx_hash
} else {
// If there is neither a txid or tx_hash property, throw an error.
throw new Error(
`utxo ${i} does not have a txid or tx_hash property.`
)
}
}
// Ensure the UTXO has a vout or tx_pos 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.`
)
}
}
}
// Hydrate each UTXO with data from SLP OP_REUTRNs.
const outAry = await this._hydrateUtxo(utxos)
// console.log(`outAry: ${JSON.stringify(outAry, null, 2)}`)
// *After* each UTXO has been hydrated with SLP data,
// validate the TXID with SLPDB.
for (let i = 0; i < outAry.length; i++) {
const utxo = outAry[i]
// *After* the UTXO has been hydrated with SLP data,
// validate the TXID with SLPDB.
if (utxo.tokenType) {
// Only execute this block if the current UTXO has a 'tokenType'
// property. i.e. it has been successfully hydrated with SLP
// information.
// Validate against the whitelist SLPDB.
const whitelistResult = await this.validateTxid3(utxo.txid)
// console.log(
// `whitelist-SLPDB for ${txid}: ${JSON.stringify(
// whitelistResult,
// null,
// 2
// )}`
// )
let isValid = null
// Safely retrieve the returned value.
if (whitelistResult[0] !== null) isValid = whitelistResult[0].valid
utxo.isValid = isValid
}
}
return outAry
} catch (error) {
if (error.response && error.response.data) throw error.response.data
throw error
}
}
// This is a private function that is called by tokenUtxoDetails().
// It loops through an array of UTXOs and tries to hydrate them with SLP
// token information from the OP_RETURN data.
+114 -2
View File
@@ -32,7 +32,8 @@ describe('#SLP', () => {
describe('#util', () => {
describe('#list', () => {
it('should get information on the Spice token', async () => {
const tokenId = '4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf'
const tokenId =
'4de69e374a8ed21cbddd47f2338cc0f479dc58daa2bbe11cd604ca488eca0ddf'
const result = await bchjs.SLP.Utils.list(tokenId)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
@@ -490,6 +491,116 @@ describe('#SLP', () => {
})
})
describe('#tokenUtxoDetailsWL', () => {
it('should return details for a simple SEND SLP token utxo', async () => {
const utxos = [
{
height: 660554,
tx_hash:
'89b3f0c84efe8b01b24e2d7ac08636de5781f31dbb84478e3de868ca0a7ed93a',
tx_pos: 1,
value: 546
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.property(data[0], 'txid')
assert.property(data[0], 'vout')
assert.property(data[0], 'height')
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 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.tokenUtxoDetailsWL(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.isArray(data)
assert.equal(data[0].isValid, false)
assert.equal(data[1].isValid, false)
})
it('should handle a dust attack', async () => {
// it("#dustattack", async () => {
const utxos = [
{
height: 655965,
tx_hash:
'a675af87dcd8d39be782737aa52e0076b52eb2f5ce355ffcb5567a64dd96b77e',
tx_pos: 151,
value: 547,
satoshis: 547,
txid:
'a675af87dcd8d39be782737aa52e0076b52eb2f5ce355ffcb5567a64dd96b77e',
vout: 151,
address: 'bitcoincash:qq4dw3sm8qvglspy6w2qg0u2ugsy9zcfcqrpeflwww',
hdIndex: 11
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.equal(data[0].isValid, false)
})
it('should handle null SLPDB validations', async () => {
const utxos = [
{
height: 665577,
tx_hash:
'4b89405c54d1c0bde8aa476a47561a42a6e7a5e927daa2ec69d428810eae3419',
tx_pos: 1,
value: 546
},
{
height: 665577,
tx_hash:
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
tx_pos: 1,
value: 546
}
]
const data = await bchjs.SLP.Utils.tokenUtxoDetailsWL(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.isArray(data)
// assert.equal(data[0].isValid, null)
})
})
describe('#balancesForAddress', () => {
it('should fetch all balances for address: simpleledger:qzv3zz2trz0xgp6a96lu4m6vp2nkwag0kvyucjzqt9', async () => {
const balances = await bchjs.SLP.Utils.balancesForAddress(
@@ -738,7 +849,8 @@ describe('#SLP', () => {
describe('#waterfallValidateTxid', () => {
it('should validate known good txid not in whitelist', async () => {
const txid = '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488'
const txid =
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488'
const result = await bchjs.SLP.Utils.waterfallValidateTxid(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
+69 -35
View File
@@ -497,41 +497,6 @@ describe('#SLP Utils', () => {
}
})
// CT 1/9/21: This test can be removed after 2/1/21
// 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 uut.Utils.tokenUtxoDetails(utxos)
//
// assert.equal(true, false, 'Unexpected result.')
// } catch (err) {
// assert.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 = [
@@ -1596,6 +1561,75 @@ describe('#SLP Utils', () => {
})
})
describe('#tokenUtxoDetailsWL', () => {
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(uut.Utils, 'decodeOpReturn')
.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(uut.Utils, 'validateTxid3').resolves([
{
txid:
'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb',
valid: true
}
])
const utxos = [
{
txid:
'fde117b1f176b231e2fa9a6cb022e0f7c31c288221df6bcb05f8b7d040ca87cb',
vout: 1,
amount: 0.00000546,
satoshis: 546,
height: 596089,
confirmations: 748
}
]
const data = await uut.Utils.tokenUtxoDetailsWL(utxos)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
assert.property(data[0], 'txid')
assert.property(data[0], 'vout')
assert.property(data[0], 'height')
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)
})
})
describe('#txDetails', () => {
it('should throw an error if txid is not included', async () => {
try {