mirror of
https://github.com/Permissionless-Software-Foundation/bch-js.git
synced 2026-09-23 01:22:01 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d37a2f8139 | ||
|
|
3b873b6064 | ||
|
|
687b5bca02 | ||
|
|
b80697500c | ||
|
|
626d27033e | ||
|
|
ce2ee3fc1b | ||
|
|
4768dcadc6 | ||
|
|
5fef09edbf | ||
|
|
37109cd7ef | ||
|
|
83d523260d |
@@ -3,6 +3,7 @@ node_modules/*
|
||||
wallet-info.txt
|
||||
wallet.json
|
||||
integration-test-sweet.sh
|
||||
integration-test-ss.sh
|
||||
|
||||
docs/
|
||||
coverage/
|
||||
|
||||
+2
-2
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user