mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be2fd18f5c | ||
|
|
cfcbdb0429 | ||
|
|
9463afcbc3 | ||
|
|
13a1e1b1db | ||
|
|
6e19738d1e | ||
|
|
2a3baf39b9 | ||
|
|
d58c98030d | ||
|
|
5c4f6802b3 | ||
|
|
fd51f82dbc | ||
|
|
a77cb3c249 | ||
|
|
015c33faff | ||
|
|
738c643c1b | ||
|
|
69ce039882 | ||
|
|
1642603b78 | ||
|
|
85a170073d | ||
|
|
a8882bd618 | ||
|
|
a6b139e0c4 | ||
|
|
5880f14e07 | ||
|
|
9bf271b4c2 | ||
|
|
82af58e8b3 | ||
|
|
6d00681d6e | ||
|
|
59043bc154 | ||
|
|
ffb63125e1 | ||
|
|
a3cd832c63 | ||
|
|
12d88e8846 | ||
|
|
17fe219773 | ||
|
|
d76c663f25 | ||
|
|
b1655e843e | ||
|
|
af5d44c278 |
@@ -30,6 +30,7 @@ start-fullstack-abc.sh
|
||||
start-fullstack-bchn.sh
|
||||
start-ss-main.sh
|
||||
start-decatur-bchn.sh
|
||||
shell-scripts/
|
||||
|
||||
coverage
|
||||
start-my-infra
|
||||
|
||||
@@ -82,6 +82,20 @@ installation.
|
||||
|
||||
`docker-compose up`
|
||||
|
||||
## Rate Limits
|
||||
The rate limits for [api.fullstack.cash](https://api.fullstack.cash) are controlled by a JWT token. You can increase your rate limits by [purchasing a JWT token](https://https://fullstack.cash). If you're using bch-js, [check the readme for instructions on increasing rate limits](https://github.com/Permissionless-Software-Foundation/bch-js#api-key). For interacting with bch-api directly, you can then include the JWT token in the HTTP header like this:
|
||||
|
||||
- `Authorization: Token <JWT token>`
|
||||
|
||||
### cURL example
|
||||
Here is an example using curl for using the JWT token when making a call to the REST API:
|
||||
|
||||
```
|
||||
export BCHJSTOKEN=eyJhbGciOiJ....
|
||||
|
||||
curl "https://bchn.fullstack.cash/v4/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -X GET -H "Content-Type: application/json" -H "Authorization: Token ${BCHJSTOKEN}"
|
||||
```
|
||||
|
||||
## Support
|
||||
Have questions? Need help? Join our community support
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
@@ -16,6 +16,9 @@ const config = {
|
||||
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
||||
? Number(process.env.WHITELIST_RATE_LIMIT)
|
||||
: 10,
|
||||
internalRateLimit: process.env.INTERNAL_RATE_LIMIT
|
||||
? Number(process.env.INTERNAL_RATE_LIMIT)
|
||||
: 10,
|
||||
pointsPerMinute: process.env.POINTS_PER_MINUTE
|
||||
? Number(process.env.POINTS_PER_MINUTE)
|
||||
: 10000,
|
||||
|
||||
Generated
+1078
-2580
File diff suppressed because it is too large
Load Diff
+6
-5
@@ -16,20 +16,21 @@
|
||||
"dev": "nodemon ./dist/app.js",
|
||||
"test": "npm run lint && npm run test-v4",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v4/",
|
||||
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v4/",
|
||||
"test:integration": "mocha test/v4/integration",
|
||||
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js",
|
||||
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v4/integration/nft.js",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs",
|
||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#hydrateUtxos-' test/v4/integration/",
|
||||
"test:temp2": "mocha test/v4/rate-limit2-unit.js"
|
||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#getNftChildren' test/v4/integration/",
|
||||
"test:temp2": "mocha -g '#getNftGroup' --exit --timeout 30000 test/v4/"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "^4.17.6",
|
||||
"@psf/bch-js": "^4.18.0",
|
||||
"apidoc": "^0.26.0",
|
||||
"axios": "^0.21.1",
|
||||
"bitcore-lib-cash": "^8.23.1",
|
||||
@@ -70,7 +71,7 @@
|
||||
"nock": "^13.0.5",
|
||||
"nyc": "^15.0.0",
|
||||
"prettier": "^2.0.0",
|
||||
"semantic-release": "^17.3.9",
|
||||
"semantic-release": "^17.4.2",
|
||||
"sinon": "^9.0.0",
|
||||
"standard": "^14.3.1"
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ const ANON_LIMITS = config.anonRateLimit
|
||||
const WHITELIST_DOMAINS = config.whitelistDomains
|
||||
const WHITELIST_POINTS_TO_CONSUME = config.whitelistRateLimit
|
||||
const POINTS_PER_MINUTE = config.pointsPerMinute
|
||||
const INTERNAL_POINTS_TO_CONSUME = 10
|
||||
const INTERNAL_POINTS_TO_CONSUME = config.internalRateLimit
|
||||
|
||||
class RateLimits {
|
||||
constructor () {
|
||||
@@ -154,6 +154,7 @@ class RateLimits {
|
||||
res,
|
||||
defaultJwt
|
||||
)
|
||||
// console.log(`hasExceededRateLimit: `, hasExceededRateLimit)
|
||||
|
||||
if (!hasExceededRateLimit) {
|
||||
// Rate limits have not been exceeded. Processing can continue.
|
||||
@@ -195,6 +196,7 @@ class RateLimits {
|
||||
res,
|
||||
req.locals.jwtToken
|
||||
)
|
||||
// console.log('hasExceededRateLimit: ', hasExceededRateLimit)
|
||||
|
||||
if (!hasExceededRateLimit) {
|
||||
// Rate limits have not been exceeded. Processing can continue.
|
||||
@@ -220,8 +222,12 @@ class RateLimits {
|
||||
async trackRateLimits (req, res, jwtToken) {
|
||||
// Anonymous rate limits are used by default.
|
||||
let pointsToConsume = ANON_LIMITS
|
||||
// console.log('pointsToConsume: ', pointsToConsume)
|
||||
|
||||
let key = req.ip // Use the IP address as the key, by default.
|
||||
|
||||
// console.log('jwtToken: ', jwtToken)
|
||||
|
||||
try {
|
||||
// Decode the JWT token if it exists
|
||||
if (jwtToken) {
|
||||
@@ -239,6 +245,10 @@ class RateLimits {
|
||||
// The 429 error response is handled by the catch().
|
||||
await _this.rateLimiter.consume(key, pointsToConsume)
|
||||
|
||||
// Debugging
|
||||
// const rateLimitData = await _this.rateLimiter.consume(key, pointsToConsume)
|
||||
// console.log(`rateLimitData: `, rateLimitData)
|
||||
|
||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
||||
|
||||
// Signal that the user has not exceeded their rate limits.
|
||||
|
||||
@@ -140,7 +140,7 @@ class RawTransactions {
|
||||
const options = _this.routeUtils.getAxiosOptions()
|
||||
|
||||
// Loop through each height and creates an array of requests to call in parallel
|
||||
const promises = hexes.map(async hex => {
|
||||
const promises = hexes.map(async (hex) => {
|
||||
options.data.id = 'decoderawtransaction'
|
||||
options.data.method = 'decoderawtransaction'
|
||||
options.data.params = [hex]
|
||||
@@ -152,7 +152,7 @@ class RawTransactions {
|
||||
const axiosResult = await _this.axios.all(promises)
|
||||
|
||||
// Retrieve the data part of the result.
|
||||
const result = axiosResult.map(x => x.data.result)
|
||||
const result = axiosResult.map((x) => x.data.result)
|
||||
|
||||
res.status(200)
|
||||
return res.json(result)
|
||||
@@ -254,7 +254,7 @@ class RawTransactions {
|
||||
const options = _this.routeUtils.getAxiosOptions()
|
||||
|
||||
// Loop through each hex and create an array of promises
|
||||
const promises = hexes.map(async hex => {
|
||||
const promises = hexes.map(async (hex) => {
|
||||
options.data.id = 'decodescript'
|
||||
options.data.method = 'decodescript'
|
||||
options.data.params = [hex]
|
||||
@@ -267,7 +267,7 @@ class RawTransactions {
|
||||
const resolved = await Promise.all(promises)
|
||||
|
||||
// Retrieve the data from each resolved promise.
|
||||
const result = resolved.map(x => x.data.result)
|
||||
const result = resolved.map((x) => x.data.result)
|
||||
|
||||
res.status(200)
|
||||
return res.json(result)
|
||||
@@ -348,7 +348,7 @@ class RawTransactions {
|
||||
}
|
||||
|
||||
// Loop through each txid and create an array of promises
|
||||
const promises = txids.map(async txid =>
|
||||
const promises = txids.map(async (txid) =>
|
||||
_this.getRawTransactionsFromNode(txid, verbose)
|
||||
)
|
||||
|
||||
@@ -436,7 +436,8 @@ class RawTransactions {
|
||||
return res.json({ error: 'hex must be an array' })
|
||||
}
|
||||
|
||||
const options = _this.routeUtils.getAxiosOptions()
|
||||
let options = _this.routeUtils.getAxiosOptions()
|
||||
options = _this.sendTxOptions(options)
|
||||
|
||||
// Enforce array size rate limits
|
||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||
@@ -536,7 +537,8 @@ class RawTransactions {
|
||||
})
|
||||
}
|
||||
|
||||
const options = _this.routeUtils.getAxiosOptions()
|
||||
let options = _this.routeUtils.getAxiosOptions()
|
||||
options = _this.sendTxOptions(options)
|
||||
|
||||
// RPC call
|
||||
options.data.id = 'sendrawtransaction'
|
||||
@@ -557,6 +559,31 @@ class RawTransactions {
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
// This method modifies the default axios options. It attempts to inject
|
||||
// a specific full node to use when broadcasting transactions. This is useful
|
||||
// because it leverages the built-in protections that a full node has against
|
||||
// accidental double spends. It mitigates a corner-case when rapidly spending
|
||||
// TXs on load balanced nodes. By piping all TX sends through a single node,
|
||||
// accidental double spends can be reduced.
|
||||
sendTxOptions (options) {
|
||||
try {
|
||||
const sendUrl = process.env.RPC_SENDURL
|
||||
|
||||
if (sendUrl !== 'undefined' && sendUrl !== undefined) {
|
||||
// console.log(`original options: ${JSON.stringify(options, null, 2)}`)
|
||||
|
||||
options.baseURL = process.env.RPC_SENDURL
|
||||
|
||||
// console.log(`modified options: ${JSON.stringify(options, null, 2)}`)
|
||||
}
|
||||
|
||||
return options
|
||||
} catch (err) {
|
||||
wlogger.error('Error in rawtransactions.js/sendTxOptions()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RawTransactions
|
||||
|
||||
+139
-4
@@ -50,7 +50,9 @@ const rawTransactions = new RawTransactions()
|
||||
|
||||
// Setup REST and TREST URLs used by slpjs
|
||||
// Dev note: this allows for unit tests to mock the URL.
|
||||
if (!process.env.REST_URL) { process.env.REST_URL = 'https://bchn.fullstack.cash/v4/' }
|
||||
if (!process.env.REST_URL) {
|
||||
process.env.REST_URL = 'https://bchn.fullstack.cash/v4/'
|
||||
}
|
||||
if (!process.env.TREST_URL) {
|
||||
process.env.TREST_URL = 'https://testnet.fullstack.cash/v4/'
|
||||
}
|
||||
@@ -100,6 +102,8 @@ class Slp {
|
||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
_this.router.post('/hydrateUtxosWL', _this.hydrateUtxosWL)
|
||||
_this.router.get('/status', _this.getStatus)
|
||||
_this.router.get('/nftChildren/:tokenId', _this.getNftChildren)
|
||||
_this.router.get('/nftGroup/:tokenId', _this.getNftGroup)
|
||||
}
|
||||
|
||||
// DRY error handler.
|
||||
@@ -133,6 +137,10 @@ class Slp {
|
||||
delete token.tokenDetails.batonVout
|
||||
delete token.tokenDetails.sendOutputs
|
||||
|
||||
if (token.tokenDetails.versionType === 65 && token.nftParentId) {
|
||||
token.tokenDetails.nftParentId = token.nftParentId
|
||||
}
|
||||
|
||||
token.tokenDetails.blockCreated = token.tokenStats.block_created
|
||||
token.tokenDetails.blockLastActiveSend =
|
||||
token.tokenStats.block_last_active_send
|
||||
@@ -296,7 +304,7 @@ class Slp {
|
||||
'tokenDetails.tokenIdHex': tokenId
|
||||
}
|
||||
},
|
||||
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
|
||||
project: { tokenDetails: 1, tokenStats: 1, nftParentId: 1, _id: 0 },
|
||||
limit: 1000
|
||||
}
|
||||
}
|
||||
@@ -1780,8 +1788,6 @@ class Slp {
|
||||
|
||||
// Format the response from SLPDB into an object.
|
||||
async formatToRestObject (slpDBFormat) {
|
||||
_this.BigNumber.set({ DECIMAL_PLACES: 8 })
|
||||
|
||||
// console.log(`slpDBFormat.data: ${JSON.stringify(slpDBFormat.data, null, 2)}`)
|
||||
|
||||
const transaction = slpDBFormat.data.u.length
|
||||
@@ -2224,6 +2230,135 @@ class Slp {
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /slp/nftChildren/{tokenId} Get all NFT children for a given NFT group
|
||||
* @apiName Get all NFT children for a given NFT group
|
||||
* @apiGroup SLP
|
||||
* @apiDescription Get all NFT children for a given NFT group
|
||||
*
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v4/slp/nftChildren/68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a" -H "accept:application/json"
|
||||
*
|
||||
*/
|
||||
async getNftChildren (req, res, next) {
|
||||
try {
|
||||
// Validate the input data.
|
||||
const tokenId = req.params.tokenId
|
||||
|
||||
if (!tokenId || tokenId === '') {
|
||||
res.status(400)
|
||||
return res.json({ error: 'tokenId can not be empty' })
|
||||
}
|
||||
|
||||
const token = await _this.lookupToken(tokenId)
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
|
||||
if (!token || token.id === 'not found' || token.versionType !== 129) {
|
||||
res.status(400)
|
||||
return res.json({ error: 'NFT group does not exists' })
|
||||
}
|
||||
|
||||
const query = {
|
||||
v: 3,
|
||||
q: {
|
||||
db: ['t'],
|
||||
aggregate: [
|
||||
{ $match: { nftParentId: tokenId } },
|
||||
{ $skip: 0 }, // TODO: pass start point
|
||||
{ $limit: 100 } // TODO: pass count limit
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const s = JSON.stringify(query)
|
||||
const b64 = Buffer.from(s).toString('base64')
|
||||
const url = `${process.env.SLPDB_URL}q/${b64}`
|
||||
// Request options
|
||||
const opt = {
|
||||
method: 'get',
|
||||
baseURL: url
|
||||
}
|
||||
const childrenIds = []
|
||||
const childrenRes = await _this.axios.request(opt)
|
||||
// console.log(`childrenRes.data: ${JSON.stringify(childrenRes.data, null, 2)}`)
|
||||
if (!childrenRes || !childrenRes.data || !childrenRes.data.t) {
|
||||
return { error: 'No children data in the group' }
|
||||
}
|
||||
|
||||
childrenRes.data.t.forEach(function (token) {
|
||||
// console.log(`info: ${JSON.stringify(token, null, 2)}`)
|
||||
if (
|
||||
token.tokenDetails.versionType === 65 &&
|
||||
token.tokenDetails.transactionType === 'GENESIS'
|
||||
) {
|
||||
childrenIds.push(token.tokenDetails.tokenIdHex)
|
||||
}
|
||||
})
|
||||
|
||||
res.status(200)
|
||||
return res.json({ nftChildren: childrenIds })
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
wlogger.error('Error in slp.js/getNftChildren().', err)
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /slp/nftGroup/{tokenId} Get the NFT group for a given NFT child token
|
||||
* @apiName Get the NFT group for a given NFT child token
|
||||
* @apiGroup SLP
|
||||
* @apiDescription Get the NFT group for a given NFT child token
|
||||
*
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X GET "https://api.fullstack.cash/v4/slp/nftGroup/45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9" -H "accept:application/json"
|
||||
*
|
||||
*/
|
||||
async getNftGroup (req, res, next) {
|
||||
try {
|
||||
// Validate the input data.
|
||||
const tokenId = req.params.tokenId
|
||||
|
||||
if (!tokenId || tokenId === '') {
|
||||
res.status(400)
|
||||
return res.json({ error: 'tokenId can not be empty' })
|
||||
}
|
||||
|
||||
const token = await _this.lookupToken(tokenId)
|
||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
||||
|
||||
if (
|
||||
!token ||
|
||||
token.id === 'not found' ||
|
||||
token.versionType !== 65 ||
|
||||
!token.nftParentId
|
||||
) {
|
||||
res.status(400)
|
||||
return res.json({ error: 'NFT child does not exists' })
|
||||
}
|
||||
|
||||
const parentToken = await _this.lookupToken(token.nftParentId)
|
||||
// console.log(`parentToken: ${JSON.stringify(token, null, 2)}`)
|
||||
if (
|
||||
!parentToken ||
|
||||
parentToken.id === 'not found' ||
|
||||
parentToken.versionType !== 129
|
||||
) {
|
||||
res.status(400)
|
||||
return res.json({ error: 'NFT group does not exists' })
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json({ nftGroup: parentToken })
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
wlogger.error('Error in slp.js/getNftGroup().', err)
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Slp
|
||||
|
||||
@@ -154,16 +154,23 @@ class RouteUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 429 errors thrown by nginx
|
||||
// Handle 429 errors
|
||||
if (err.error) {
|
||||
// console.log('decodeError: err: ', err)
|
||||
console.log('decodeError: err: ', err)
|
||||
|
||||
// Error thrown by nginx (usually the SLPDB load balancer.)
|
||||
if (err.error.includes('429 Too Many Requests')) {
|
||||
const internalMsg =
|
||||
'429 error thrown by nginx caught by route-utils.js/decodeError()'
|
||||
console.error(internalMsg)
|
||||
wlogger.error(internalMsg)
|
||||
|
||||
return {
|
||||
msg: '429 Too Many Requests',
|
||||
status: 429
|
||||
}
|
||||
} else if (err.error.includes('Too many requests')) {
|
||||
// Error is being thrown by bch-api rate limit middleware.
|
||||
return {
|
||||
msg: '429 Too Many Requests',
|
||||
status: 429
|
||||
|
||||
@@ -15,7 +15,11 @@ var NETWORK = process.env.NETWORK
|
||||
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
|
||||
|
||||
// Default 5 days.
|
||||
var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
|
||||
// This was causing a problem with popularity. Creating over a gigabyte of files.
|
||||
// var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
|
||||
|
||||
// 250 files @ 1Meg each = 250 megs
|
||||
var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '250'
|
||||
|
||||
// Configure daily-rotation transport.
|
||||
var transport = new winston.transports.DailyRotateFile({
|
||||
|
||||
@@ -15,6 +15,11 @@ export RPC_BASEURL=http://$RPC_IP/
|
||||
export RPC_USERNAME=bitcoin
|
||||
export RPC_PASSWORD=password
|
||||
|
||||
# (optional) If load-balancing multiple full nodes, it's best to pick a single
|
||||
# one for broadcasting transactions to the network. Prevents accidental
|
||||
# double spends.
|
||||
#export RPC_SENDURL=http://$RPC_IP/
|
||||
|
||||
# SLPDB
|
||||
export SLPDB_PASS_GP=somelongpassword
|
||||
export SLPDB_URL=http://<SLPDB IP>:12300/
|
||||
@@ -48,12 +53,12 @@ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
||||
# that originate froma domain on the whitelist.
|
||||
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
||||
|
||||
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON.
|
||||
# Rate Limits. Numbers are divided into 1000. e.g. 10000 / 500 = 20 RPM for ANON.
|
||||
# Requests use the ANON rate limit if they fail to pass in a JWT token.
|
||||
# ANON = 20 requests per minute (RPM)
|
||||
export ANON_RATE_LIMIT=50
|
||||
# 10 = 100 RPM
|
||||
export WHITELIST_RATE_LIMIT=10
|
||||
export ANON_RATE_LIMIT=500
|
||||
# 10 = 1000 RPM
|
||||
export WHITELIST_RATE_LIMIT=100
|
||||
|
||||
# Set logging parameters
|
||||
#1m means no more than 1 megabyte
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
These integration tests need to be run against a live SLPDB. They query
|
||||
against a live SLPDB and test the results against known token stats.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
const assert = chai.assert
|
||||
// const axios = require('axios')
|
||||
|
||||
// Used for debugging.
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
// Exit if SLPDB URL is not defined.
|
||||
if (!process.env.SLPDB_URL) {
|
||||
throw new Error(
|
||||
'SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.'
|
||||
)
|
||||
}
|
||||
|
||||
const SLP = require('../../../src/routes/v4/slp')
|
||||
const slp = new SLP()
|
||||
|
||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
||||
|
||||
describe('#nft', () => {
|
||||
let req, res
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the req and res objects used by Express routes.
|
||||
req = mockReq
|
||||
res = mockRes
|
||||
})
|
||||
|
||||
describe('#getNftChildren', () => {
|
||||
it('should get NFT children list', async () => {
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
||||
|
||||
const result = await slp.getNftChildren(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.isArray(result.nftChildren)
|
||||
assert.equal(result.nftChildren.length, 2)
|
||||
assert.equal(
|
||||
result.nftChildren[0],
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
)
|
||||
assert.equal(
|
||||
result.nftChildren[1],
|
||||
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getNftGroup', () => {
|
||||
it('should get NFT group token info', async () => {
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
// req.params.tokenId = '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
|
||||
const result = await slp.getNftGroup(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
assert.property(result, 'nftGroup')
|
||||
assert.property(result.nftGroup, 'id')
|
||||
assert.property(result.nftGroup, 'versionType')
|
||||
assert.property(result.nftGroup, 'symbol')
|
||||
assert.equal(
|
||||
result.nftGroup.id,
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
||||
)
|
||||
assert.equal(result.nftGroup.versionType, 129)
|
||||
assert.equal(result.nftGroup.symbol, 'PSF.TEST.GROUP')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -66,6 +66,69 @@ const mockSingleToken = {
|
||||
]
|
||||
}
|
||||
|
||||
const mockNftGroup = {
|
||||
decimals: 0,
|
||||
timestamp: '2021-05-03 10:36:01',
|
||||
timestamp_unix: 1620038161,
|
||||
versionType: 129,
|
||||
documentUri: 'psfoundation.cash',
|
||||
symbol: 'PSF.TEST.GROUP',
|
||||
name: 'PSF Test NFT Group',
|
||||
containsBaton: true,
|
||||
id: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
|
||||
documentHash: null,
|
||||
initialTokenQty: 1000000,
|
||||
blockCreated: 686117,
|
||||
totalMinted: null,
|
||||
totalBurned: null,
|
||||
circulatingSupply: null
|
||||
}
|
||||
|
||||
const mockNftChildren = [
|
||||
{
|
||||
decimals: 0,
|
||||
timestamp: '2021-05-03 11:59:30',
|
||||
timestamp_unix: 1620043170,
|
||||
versionType: 65,
|
||||
documentUri: 'psfoundation.cash',
|
||||
symbol: 'PSF.TEST.CHILD.1',
|
||||
name: 'PSF Test NFT Child #1',
|
||||
containsBaton: false,
|
||||
id: '45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9',
|
||||
documentHash: null,
|
||||
initialTokenQty: 1,
|
||||
nftParentId: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
|
||||
blockCreated: 686130,
|
||||
totalMinted: null,
|
||||
totalBurned: null,
|
||||
circulatingSupply: null,
|
||||
// only in axios.data.t
|
||||
transactionType: 'GENESIS',
|
||||
tokenIdHex: '45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
},
|
||||
{
|
||||
decimals: 0,
|
||||
timestamp: '2021-05-03 11:59:30',
|
||||
timestamp_unix: 1620043170,
|
||||
versionType: 65,
|
||||
documentUri: 'psfoundation.cash',
|
||||
symbol: 'PSF.TEST.CHILD.2',
|
||||
name: 'PSF Test NFT Child #2',
|
||||
containsBaton: false,
|
||||
id: '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55',
|
||||
documentHash: null,
|
||||
initialTokenQty: 1,
|
||||
nftParentId: '68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a',
|
||||
blockCreated: 686130,
|
||||
totalMinted: null,
|
||||
totalBurned: null,
|
||||
circulatingSupply: null,
|
||||
// only in axios.data.t
|
||||
transactionType: 'GENESIS',
|
||||
tokenIdHex: '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
|
||||
}
|
||||
]
|
||||
|
||||
const mockSingleTokenError = {
|
||||
t: []
|
||||
}
|
||||
@@ -1038,5 +1101,7 @@ module.exports = {
|
||||
mockPsfToken,
|
||||
mockValidateBulk,
|
||||
mockValidate3Bulk,
|
||||
mockStatus
|
||||
mockStatus,
|
||||
mockNftGroup,
|
||||
mockNftChildren
|
||||
}
|
||||
|
||||
@@ -250,6 +250,7 @@ describe('#Raw-Transactions', () => {
|
||||
'Proper error message'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service stalls', async () => {
|
||||
req.body.hexes = [
|
||||
'0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000'
|
||||
@@ -1046,10 +1047,12 @@ describe('#Raw-Transactions', () => {
|
||||
// Save the existing RPC URL.
|
||||
const savedUrl = process.env.BITCOINCOM_BASEURL
|
||||
const savedUrl2 = process.env.RPC_BASEURL
|
||||
const savedUrl3 = process.env.RPC_SENDURL
|
||||
|
||||
// Manipulate the URL to cause a 500 network error.
|
||||
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
|
||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
||||
process.env.RPC_SENDURL = 'http://fakeurl/api/'
|
||||
|
||||
req.params.hex =
|
||||
'020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900'
|
||||
@@ -1059,6 +1062,7 @@ describe('#Raw-Transactions', () => {
|
||||
// Restore the saved URL.
|
||||
process.env.BITCOINCOM_BASEURL = savedUrl
|
||||
process.env.RPC_BASEURL = savedUrl2
|
||||
process.env.RPC_SENDURL = savedUrl3
|
||||
|
||||
assert.isAbove(
|
||||
res.statusCode,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Unit tests for the route-utils.js library.
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
|
||||
const RouteUtils = require('../../src/util/route-utils.js')
|
||||
|
||||
describe('#route-utils', () => {
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
uut = new RouteUtils()
|
||||
})
|
||||
|
||||
describe('#decodeError', () => {
|
||||
it('should decode a 429 error from nginx', () => {
|
||||
const err = {
|
||||
error: '<html>\r\n<head><title>429 Too Many Requests</title></head>\r\n<body>\r\n<center><h1>429 Too Many Requests</h1></center>\r\n<hr><center>nginx/1.18.0 (Ubuntu)</center>\r\n</body>\r\n</html>\r\n',
|
||||
level: 'error',
|
||||
message: 'Error in slp.js/hydrateUtxos().',
|
||||
timestamp: '2021-03-31T04:13:36.662Z'
|
||||
}
|
||||
|
||||
const result = uut.decodeError(err)
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'msg')
|
||||
assert.equal(result.msg, '429 Too Many Requests')
|
||||
assert.property(result, 'status')
|
||||
assert.equal(result.status, 429)
|
||||
})
|
||||
})
|
||||
})
|
||||
+271
@@ -1760,6 +1760,277 @@ describe('#SLP', () => {
|
||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getNftChildren', () => {
|
||||
it('should throw 400 if tokenID is empty', async () => {
|
||||
req.params.tokenId = ''
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'tokenId can not be empty')
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service stalls', async () => {
|
||||
// Mock the timeout error.
|
||||
sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
||||
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||
assert.include(
|
||||
result.error,
|
||||
'Could not communicate with full node',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service is down', async () => {
|
||||
// Mock the timeout error.
|
||||
sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
||||
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
||||
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||
assert.include(
|
||||
result.error,
|
||||
'Could not communicate with full node',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error on non-existing NFT group token', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(slpRoute, 'lookupToken').resolves({ id: 'not found' })
|
||||
}
|
||||
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
|
||||
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.include(
|
||||
result.error,
|
||||
'NFT group does not exists',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error on non-group NFT token', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox
|
||||
.stub(slpRoute, 'lookupToken')
|
||||
.resolves(mockData.mockNftChildren[0])
|
||||
}
|
||||
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
|
||||
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.include(
|
||||
result.error,
|
||||
'NFT group does not exists',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
if (process.env.TEST === 'unit') {
|
||||
it('should return error on invalid NFT group data', async () => {
|
||||
sandbox.stub(slpRoute, 'lookupToken').resolves(mockData.mockNftGroup)
|
||||
sandbox.stub(slpRoute.axios, 'request').resolves({
|
||||
data: { u: 'invalid' }
|
||||
})
|
||||
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
||||
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.include(
|
||||
result.error,
|
||||
'No children data in the group',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (process.env.ISBCHN) {
|
||||
it('should get NFT children IDs in given NFT group', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(slpRoute, 'lookupToken').resolves(mockData.mockNftGroup)
|
||||
sandbox.stub(slpRoute.axios, 'request').resolves({
|
||||
data: {
|
||||
t: [
|
||||
{
|
||||
tokenDetails: mockData.mockNftChildren[0],
|
||||
nftParentId: mockData.mockNftGroup.id
|
||||
},
|
||||
{
|
||||
tokenDetails: mockData.mockNftChildren[1],
|
||||
nftParentId: mockData.mockNftGroup.id
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
req.params.tokenId =
|
||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
||||
|
||||
const result = await slpRoute.getNftChildren(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.isArray(result.nftChildren)
|
||||
assert.equal(result.nftChildren.length, 2)
|
||||
assert.equal(
|
||||
result.nftChildren[0],
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
)
|
||||
assert.equal(
|
||||
result.nftChildren[1],
|
||||
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('#getNftGroup', () => {
|
||||
it('should throw 400 if tokenID is empty', async () => {
|
||||
req.params.tokenId = ''
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'tokenId can not be empty')
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service stalls', async () => {
|
||||
// Mock the timeout error.
|
||||
sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||
assert.include(
|
||||
result.error,
|
||||
'Could not communicate with full node',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service is down', async () => {
|
||||
// Mock the timeout error.
|
||||
sandbox.stub(slpRoute.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
||||
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||
assert.include(
|
||||
result.error,
|
||||
'Could not communicate with full node',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error on non-existing NFT child token', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(slpRoute, 'lookupToken').resolves({ id: 'not found' })
|
||||
}
|
||||
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
|
||||
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.include(
|
||||
result.error,
|
||||
'NFT child does not exists',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return error on invalid NFT child token', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(slpRoute, 'lookupToken').resolves(mockData.mockNftGroup)
|
||||
}
|
||||
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
|
||||
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.include(
|
||||
result.error,
|
||||
'NFT child does not exists',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
if (process.env.TEST === 'unit') {
|
||||
it('should return error on invalid parent', async () => {
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
|
||||
const callback = sandbox.stub(slpRoute, 'lookupToken')
|
||||
callback
|
||||
.withArgs(req.params.tokenId)
|
||||
.resolves(mockData.mockNftChildren[0])
|
||||
// parent is non-valid NFT group (type != 129)
|
||||
callback
|
||||
.withArgs(mockData.mockNftGroup.id)
|
||||
.resolves(mockData.mockNftChildren[0])
|
||||
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.include(
|
||||
result.error,
|
||||
'NFT group does not exists',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (process.env.ISBCHN) {
|
||||
it('should get NFT group information for tokenId', async () => {
|
||||
req.params.tokenId =
|
||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||
|
||||
if (process.env.TEST === 'unit') {
|
||||
const callback = sandbox.stub(slpRoute, 'lookupToken')
|
||||
callback
|
||||
.withArgs(req.params.tokenId)
|
||||
.resolves(mockData.mockNftChildren[0])
|
||||
callback
|
||||
.withArgs(mockData.mockNftGroup.id)
|
||||
.resolves(mockData.mockNftGroup)
|
||||
}
|
||||
const result = await slpRoute.getNftGroup(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
assert.property(result, 'nftGroup')
|
||||
assert.property(result.nftGroup, 'id')
|
||||
assert.equal(result.nftGroup.id, mockData.mockNftGroup.id)
|
||||
assert.property(result.nftGroup, 'versionType')
|
||||
assert.equal(result.nftGroup.versionType, 129)
|
||||
assert.property(result.nftGroup, 'symbol')
|
||||
assert.property(result.nftGroup, 'initialTokenQty')
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user