Compare commits

...
10 Commits
8 changed files with 352 additions and 7 deletions
+1
View File
@@ -3,6 +3,7 @@ node_modules/*
wallet-info.txt
wallet.json
integration-test-sweet.sh
integration-test-ss.sh
docs/
coverage/
+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 '#rawtransaction' test/integration/",
"test:temp2": "mocha --timeout=30000 -g '#getTxData' test/unit/",
"test:temp": "export RESTURL=https://bchn.fullstack.cash/v4/ && mocha --timeout 30000 -g '#waterfallValidateTxid' test/integration/",
"test:temp2": "mocha --timeout=30000 -g '#waterfallValidateTxid' 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",
+1 -1
View File
@@ -14,7 +14,7 @@ class IPFS {
constructor (config) {
this.IPFS_API = process.env.IPFS_API
? process.env.IPFS_API
: 'https://ipfs-api.fullstack.cash'
: 'https://ipfs-file-upload.fullstackcash.nl'
// : `http://localhost:5001`
// Default options when calling axios.
+83
View File
@@ -1,5 +1,7 @@
const axios = require('axios')
const SlpUtils = require('./slp/utils')
let _this
class RawTransactions {
@@ -27,6 +29,9 @@ class RawTransactions {
// Encapsulate dependencies
this.axios = axios
// Dependencies
this.slpUtils = new SlpUtils(config)
_this = this
}
@@ -385,6 +390,84 @@ class RawTransactions {
}
}
// Wraps getTxData(), but also appends SLP token information to each input
// and output of the transaction. This is a very API-heavy call.
// Returns false if the txid is not an SLP transaction.
//
// Warning! This is a prototype function and the output can change at any time
// without reflecting a change in the semantic version. DO NOT USE IN PRODUCTION.
async getTxDataSlp (txid) {
try {
if (typeof txid !== 'string') {
throw new Error('Input must be a string or array of strings.')
}
const txDetails = await this.getTxData(txid)
// First get the token information for the output. If that fails, then
// this is not an SLP transaction, and this method can return false.
let outTokenData
try {
outTokenData = await this.slpUtils.decodeOpReturn(txid)
// console.log(`outTokenData: ${JSON.stringify(outTokenData, null, 2)}`)
// Add token information to the tx details object.
txDetails.tokenTxType = outTokenData.txType
txDetails.tokenId = outTokenData.tokenId
// Add the token quantity to each output.
for (let i = 0; i < outTokenData.amounts.length; i++) {
txDetails.vout[i + 1].tokenQty = outTokenData.amounts[i]
}
// Loop through each input and retrieve the token data.
for (let i = 0; i < txDetails.vin.length; i++) {
const thisVin = txDetails.vin[i]
try {
// If decodeOpReturn() throws an error, then this input is not
// from an SLP transaction and can be ignored.
const inTokenData = await this.slpUtils.decodeOpReturn(thisVin.txid)
// console.log(
// `vin[${i}] tokenData: ${JSON.stringify(inTokenData, null, 2)}`
// )
// Get the appropriate vout token amount. This may throw an error,
// which means this Vin is not actually a token UTXO, it was just
// associated with a previous token TX.
const tokenQty = inTokenData.amounts[thisVin.vout - 1]
// console.log(`tokenQty: ${JSON.stringify(tokenQty, null, 2)}`)
if (tokenQty) {
thisVin.tokenQty = tokenQty
// txDetails.vin[i].tokenQty = tokenQty
} else {
thisVin.tokenQty = null
}
} catch (err) {
// console.log('catch 2: ', err)
// If decodeOpReturn() throws an error, then this input is not
// from an SLP transaction and can be ignored.
// thisVin.tokenQty = null
thisVin.tokenQty = null
continue
}
// console.log(
// `2: txDetails.vin[i]: ${JSON.stringify(txDetails.vin[i], null, 2)}`
// )
}
} catch (err) {
// console.log('catch 1: ', err)
return false
}
return txDetails
} catch (error) {
if (error.response && error.response.data) throw error.response.data
else throw error
}
}
/**
* @api RawTransactions.sendRawTransaction() sendRawTransaction()
* @apiName sendRawTransaction
+134
View File
@@ -1426,6 +1426,140 @@ class Utils {
}
}
/**
* @api SLP.Utils.waterfallValidateTxid() waterfallValidateTxid()
* @apiName waterfallValidateTxid
* @apiGroup SLP Utils
* @apiDescription Use multiple validators to validate an SLP TXID.
*
* This function aggregates all the available SLP token validation sources.
* It starts with the fastest, most-efficient source first, and continues
* to other validation sources until the txid is validated (true or false).
* If the txid goes through all sources and can't be validated, it will
* return null.
*
* Validation sources from most efficient to least efficient:
* - SLPDB with whitelist filter
* - SLPDB general purpose
* - slp-api
*
* Currently only supports a single txid at a time.
*
* @apiExample Example usage:
*
* // validate single SLP txid
* (async () => {
* try {
* let validated = await bchjs.SLP.Utils.waterfallValidateTxid(
* "df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb"
* );
* console.log(validated);
* } catch (error) {
* console.error(error);
* }
* })();
*
* // returns
* true
*/
async waterfallValidateTxid (txid) {
try {
const cachedTxValidation = {}
// If the value has been cached, use the cached version first.
let isValid = cachedTxValidation[txid]
if (!isValid) {
isValid = null
} else {
return isValid
}
// There are two possible responses from SLPDB. If SLPDB is functioning
// correctly, then validateTxid() will return this:
// isValid: [
// {
// "txid": "ff0c0354f8d3ddb34fa36f73494eb58ea24f8b8da6904aa8ed43b7a74886c583",
// "valid": true
// }
// ]
//
// If SLPDB has fallen behind real-time processing, it will return this:
// isValid: [
// null
// ]
//
// Note: validateTxid3() has the same output as validateTxid().
// validateTxid2() uses slp-validate, which has a different output format.
// Validate against the whitelist SLPDB first.
const whitelistResult = await this.validateTxid3(txid)
// console.log(
// `whitelist-SLPDB for ${txid}: ${JSON.stringify(
// whitelistResult,
// null,
// 2
// )}`
// )
// Safely retrieve the returned value.
if (whitelistResult[0] !== null) isValid = whitelistResult[0].valid
// Exit if isValid is not null.
if (isValid !== null) {
// Save to the cache.
cachedTxValidation[txid] = isValid
return isValid
}
// Try the general SLPDB, if the whitelist returned null.
const generalResult = await this.validateTxid(txid)
// console.log(
// `validateTxid() isValid: ${JSON.stringify(generalResult, null, 2)}`
// )
// Safely retrieve the returned value.
if (generalResult[0] !== null) isValid = generalResult[0].valid
// Exit if isValid is not null.
if (isValid !== null) {
// Save to the cache.
cachedTxValidation[txid] = isValid
return isValid
}
// If still null, as a last resort, check it against slp-validate
let slpValidateResult = null
try {
slpValidateResult = await this.validateTxid2(txid)
} catch (err) {
/* exit quietly */
}
// console.log(
// `slpValidateResult: ${JSON.stringify(slpValidateResult, null, 2)}`
// )
// Exit if isValid is not null.
if (slpValidateResult !== null) {
isValid = slpValidateResult.isValid
// Save to the cache.
cachedTxValidation[txid] = isValid
return isValid
}
// If isValid is still null, return that value, signaling that the txid
// could not be validated.
return isValid
} catch (error) {
if (error.response && error.response.data) throw error.response.data
throw error
}
}
/**
* @api SLP.Utils.hydrateUtxos() hydrateUtxos()
* @apiName hydrateUtxos
+26 -2
View File
@@ -238,9 +238,9 @@ describe('#rawtransaction', () => {
const data = []
for (let i = 0; i < 25; i++) data.push(dataMock)
const result = await bchjs.RawTransactions.sendRawTransaction(data)
await bchjs.RawTransactions.sendRawTransaction(data)
console.log(`result: ${util.inspect(result)}`)
// console.log(`result: ${util.inspect(result)}`)
assert.equal(true, false, 'Unexpected result!')
} catch (err) {
assert.hasAnyKeys(err, ['error'])
@@ -276,6 +276,30 @@ describe('#rawtransaction', () => {
assert.property(result.vin[0], 'address')
})
})
describe('#getTxDataSlp', () => {
it('should return tx data with SLP information', async () => {
const txid = 'b438855cfcab64516b44097d7212df9cdb99226c8d7c7ab504d35fcfd834cb5b'
const result = await bchjs.RawTransactions.getTxDataSlp(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.property(result.vin[0], 'address')
assert.property(result.vin[0], 'tokenQty')
assert.equal(result.vin[0].tokenQty, null)
assert.equal(result.vin[1].tokenQty, '100000000000')
})
it('should handle non-slp tx', async () => {
const txid = '04a6aca328af2445327015895b8da9766093b1989b52e477559759eb8072fc0a'
const result = await bchjs.RawTransactions.getTxDataSlp(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, false)
})
})
})
function sleep (ms) {
+21
View File
@@ -735,6 +735,27 @@ describe('#SLP', () => {
assert.property(result, 'slpProcessedBlockHeight')
})
})
describe('#waterfallValidateTxid', () => {
it('should validate known good txid not in whitelist', async () => {
const txid = '3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488'
const result = await bchjs.SLP.Utils.waterfallValidateTxid(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, true)
})
it('should invalidate a known invalid TXID', async () => {
const txid =
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a'
const result = await bchjs.SLP.Utils.waterfallValidateTxid(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, false)
})
})
})
describe('#tokentype1', () => {
+84 -2
View File
@@ -1855,7 +1855,8 @@ describe('#SLP Utils', () => {
.throws({ error: 'TXID not found' })
}
const txid = 'd284e71227ec89f714b964d8eda595be6392bebd2fac46082bc5a9ce6fb7b33e'
const txid =
'd284e71227ec89f714b964d8eda595be6392bebd2fac46082bc5a9ce6fb7b33e'
await uut.Utils.txDetails(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
@@ -1875,7 +1876,8 @@ describe('#SLP Utils', () => {
.resolves({ data: mockData.mockTxDetails })
}
const txid = '9dbaaafc48c49a21beabada8de632009288a2cd52eecefd0c00edcffca9955d0'
const txid =
'9dbaaafc48c49a21beabada8de632009288a2cd52eecefd0c00edcffca9955d0'
const result = await uut.Utils.txDetails(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
@@ -2055,6 +2057,86 @@ describe('#SLP Utils', () => {
*/
})
describe('#waterfallValidateTxid', () => {
// This test ensures that the whitelist SLPDB is called before the
// general purpose SLPDB.
it('should call validateTxid3() first', async () => {
const txid = 'fakeTxid'
const retVal = [{ txid, valid: true }]
// Use stubs to force the desired code path.
sandbox.stub(uut.Utils, 'validateTxid3').resolves(retVal)
sandbox
.stub(uut.Utils, 'validateTxid')
.rejects(new Error('Unexpected code path'))
sandbox
.stub(uut.Utils, 'validateTxid2')
.rejects(new Error('Unexpected code path'))
const result = await uut.Utils.waterfallValidateTxid(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, true)
})
// This test ensures that the general purpose SLPDB is called after the
// whitelist SLPDB, but before slp-api.
it('should call validateTxid() second', async () => {
const txid = 'fakeTxid'
const retVal1 = [{ txid, valid: null }]
const retVal2 = [{ txid, valid: true }]
// Use stubs to force the desired code path.
sandbox.stub(uut.Utils, 'validateTxid3').resolves(retVal1)
sandbox.stub(uut.Utils, 'validateTxid').resolves(retVal2)
sandbox
.stub(uut.Utils, 'validateTxid2')
.rejects(new Error('Unexpected code path'))
const result = await uut.Utils.waterfallValidateTxid(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, true)
})
// This test ensures that slp-api is called if both SLPDBs return null.
it('should call slp-api last', async () => {
const txid = 'fakeTxid'
const retVal1 = [{ txid, valid: null }]
// Use stubs to force the desired code path.
sandbox.stub(uut.Utils, 'validateTxid3').resolves(retVal1)
sandbox.stub(uut.Utils, 'validateTxid').resolves(retVal1)
sandbox.stub(uut.Utils, 'validateTxid2').resolves({ isValid: true })
const result = await uut.Utils.waterfallValidateTxid(txid)
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
assert.equal(result, true)
})
it('catches and throws an error', async () => {
try {
const txid = 'fakeTxid'
// Force an error
sandbox
.stub(uut.Utils, 'validateTxid3')
.rejects(new Error('fake error'))
await await uut.Utils.waterfallValidateTxid(txid)
assert.fail('Unexpected result')
} catch (err) {
// console.log(`err.message: ${err.message}`)
assert.include(err.message, 'fake error')
}
})
})
describe('#getWhitelist', () => {
it('should return the list', async () => {
sandbox