mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8fe6f0bfd | ||
|
|
96b5e0e408 | ||
|
|
6ca014e160 | ||
|
|
233e210131 | ||
|
|
e55d421f13 | ||
|
|
1b1bf36a89 | ||
|
|
8de5399818 | ||
|
|
4f4984a982 | ||
|
|
d9fc84f136 | ||
|
|
d444666dec | ||
|
|
c8f3649004 | ||
|
|
ee876ce8e4 | ||
|
|
fbd02802f0 | ||
|
|
6e72879103 | ||
|
|
2817300ad6 | ||
|
|
4aa7cad552 | ||
|
|
5a9401aeb1 | ||
|
|
0ae02790a2 | ||
|
|
4d3adab9e0 | ||
|
|
91f06b6311 | ||
|
|
2fc175d818 | ||
|
|
731861cb1d | ||
|
|
9a01f5ffe6 | ||
|
|
c62c515474 | ||
|
|
ab198b958e | ||
|
|
2618008247 | ||
|
|
df27ba2063 | ||
|
|
f19e70ae40 | ||
|
|
63fa97c998 | ||
|
|
61b1b8ebdf | ||
|
|
de6c19e877 | ||
|
|
66efb76f94 | ||
|
|
1250a40ff0 | ||
|
|
6786ca9ee5 | ||
|
|
e96b9e7a44 | ||
|
|
d29f99965f | ||
|
|
9825ec1bab | ||
|
|
fb61cfeca3 | ||
|
|
5f9a635ab5 | ||
|
|
41c5504846 | ||
|
|
e5f9455fbb | ||
|
|
20cb48caff | ||
|
|
f241aed5b2 | ||
|
|
87c2faa6af | ||
|
|
10a840f48d | ||
|
|
61f9531046 | ||
|
|
1ab408a757 | ||
|
|
84df2be3b8 | ||
|
|
17376d9dba | ||
|
|
4289cf4f7c | ||
|
|
f04449be72 | ||
|
|
a0f9d065f9 | ||
|
|
20f2e20bcb | ||
|
|
b410b501a1 | ||
|
|
31d430e30e | ||
|
|
82d8e39ca4 | ||
|
|
e60f4ff144 | ||
|
|
156ea0ef30 | ||
|
|
7ec6435f37 | ||
|
|
6da385c90e | ||
|
|
7eca556322 | ||
|
|
99a6f88a92 | ||
|
|
ef4a3299ed | ||
|
|
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 | ||
|
|
0d9caca8b0 | ||
|
|
0cdb9572f1 | ||
|
|
8507abd5db | ||
|
|
fae9a75f6a | ||
|
|
fc6da588ee | ||
|
|
6f8c00bab9 | ||
|
|
a2ab88adab | ||
|
|
60528dbd6e | ||
|
|
cebf1ea1d6 | ||
|
|
11a62af39b | ||
|
|
83aeabd913 | ||
|
|
7acb778470 | ||
|
|
766925c9a8 | ||
|
|
51c37e1371 | ||
|
|
244acfb26b | ||
|
|
d03303728c | ||
|
|
9fa16aea78 | ||
|
|
29864bcf66 | ||
|
|
03a5d8076e | ||
|
|
9d8f214098 | ||
|
|
4b68a75a41 | ||
|
|
901189ae42 | ||
|
|
62bba1d79f | ||
|
|
9c735e3c5a | ||
|
|
62600fdd62 | ||
|
|
27e5fb8728 | ||
|
|
93811ca331 | ||
|
|
e37858fd19 | ||
|
|
3ddbb728b4 | ||
|
|
ef49b85495 | ||
|
|
be6550686f | ||
|
|
8de2e9e10b | ||
|
|
5650afb22f | ||
|
|
6540171263 | ||
|
|
fa1f87c5c8 | ||
|
|
13580d1081 | ||
|
|
0aa4d0ecd4 | ||
|
|
a475aca8bb | ||
|
|
f792c8d90f | ||
|
|
ca9dd314dc | ||
|
|
00a36ec509 | ||
|
|
2fa911c04e | ||
|
|
8345311284 | ||
|
|
137cacd1da | ||
|
|
cf86e390fb | ||
|
|
ff52ca9418 | ||
|
|
154229d5f5 | ||
|
|
f7125be3c8 | ||
|
|
d38746dcbd | ||
|
|
a1ce8bf742 | ||
|
|
57a69eee5b | ||
|
|
9de3436a61 | ||
|
|
ddd379b3d5 | ||
|
|
2e1c5a0d61 | ||
|
|
8383818c51 | ||
|
|
8a28652c4d | ||
|
|
72ba3c86c7 | ||
|
|
e6d4bb5443 | ||
|
|
bcfe503aa6 | ||
|
|
bb3d020646 | ||
|
|
ff5554ab68 | ||
|
|
7cafb11c0d | ||
|
|
dce665b174 | ||
|
|
43c1ee9b69 | ||
|
|
4b95fa127c | ||
|
|
65314439d7 | ||
|
|
36a84726af | ||
|
|
78f462c9ec | ||
|
|
b97d94edaf | ||
|
|
b4d29a5856 | ||
|
|
64e59f607a | ||
|
|
fca0b50a2a | ||
|
|
47ddc6f186 | ||
|
|
1f1e3c781f | ||
|
|
09f0cb6b6e | ||
|
|
fb43b3f390 | ||
|
|
f76f00d235 |
@@ -29,6 +29,8 @@ test-fullstack-bchn.sh
|
|||||||
start-fullstack-abc.sh
|
start-fullstack-abc.sh
|
||||||
start-fullstack-bchn.sh
|
start-fullstack-bchn.sh
|
||||||
start-ss-main.sh
|
start-ss-main.sh
|
||||||
|
start-decatur-bchn.sh
|
||||||
|
shell-scripts/
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
start-my-infra
|
start-my-infra
|
||||||
|
|||||||
@@ -82,6 +82,20 @@ installation.
|
|||||||
|
|
||||||
`docker-compose up`
|
`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
|
## Support
|
||||||
Have questions? Need help? Join our community support
|
Have questions? Need help? Join our community support
|
||||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"sampleUrl": null
|
||||||
|
|
||||||
|
}
|
||||||
+10
-2
@@ -10,13 +10,21 @@ const config = {
|
|||||||
: 'secret-jwt-token',
|
: 'secret-jwt-token',
|
||||||
|
|
||||||
// Rate Limits
|
// Rate Limits
|
||||||
anonRateLimit: process.env.ANON_RATE_LIMIT ? Number(process.env.ANON_RATE_LIMIT) : 50,
|
anonRateLimit: process.env.ANON_RATE_LIMIT
|
||||||
|
? Number(process.env.ANON_RATE_LIMIT)
|
||||||
|
: 500,
|
||||||
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
||||||
? Number(process.env.WHITELIST_RATE_LIMIT)
|
? Number(process.env.WHITELIST_RATE_LIMIT)
|
||||||
: 10,
|
: 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,
|
||||||
whitelistDomains: process.env.WHITELIST_DOMAINS
|
whitelistDomains: process.env.WHITELIST_DOMAINS
|
||||||
? process.env.WHITELIST_DOMAINS.split(',')
|
? process.env.WHITELIST_DOMAINS.split(',')
|
||||||
: ['fullstack.cash', 'psfoundation.cash']
|
: ['fullstack.cash', 'psfoundation.cash', '10.0.']
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config
|
module.exports = config
|
||||||
|
|||||||
@@ -1,19 +1,48 @@
|
|||||||
FROM christroutner/ct-base-ubuntu
|
FROM ubuntu:20.04
|
||||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||||
|
|
||||||
RUN apt-get update -y
|
#Update the OS and install any OS packages needed.
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||||
|
|
||||||
|
#Install Node and NPM
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
|
||||||
|
RUN bash nodesource_setup.sh
|
||||||
|
RUN apt-get install -y nodejs build-essential
|
||||||
|
|
||||||
|
#Create the user 'safeuser' and add them to the sudo group.
|
||||||
|
RUN useradd -ms /bin/bash safeuser
|
||||||
|
RUN adduser safeuser sudo
|
||||||
|
|
||||||
|
#Set password to 'abcd8765' change value below if you want a different password
|
||||||
|
RUN echo safeuser:abcd8765 | chpasswd
|
||||||
|
|
||||||
|
#Set the working directory to be the users home directory
|
||||||
|
WORKDIR /home/safeuser
|
||||||
|
|
||||||
|
#Setup NPM for non-root global install (like on a mac)
|
||||||
|
RUN mkdir /home/safeuser/.npm-global
|
||||||
|
RUN chown -R safeuser .npm-global
|
||||||
|
RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
||||||
|
RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||||
|
|
||||||
|
# Update to the latest version of npm.
|
||||||
|
# Working with npm@7.21.1
|
||||||
|
RUN npm install -g npm
|
||||||
|
|
||||||
|
#FROM christroutner/ct-base-ubuntu
|
||||||
|
#MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||||
|
|
||||||
|
#RUN apt-get update -y
|
||||||
|
|
||||||
#Set the working directory to be the home directory
|
#Set the working directory to be the home directory
|
||||||
WORKDIR /home/safeuser
|
#WORKDIR /home/safeuser
|
||||||
|
|
||||||
# Switch to user account.
|
# Switch to user account.
|
||||||
USER safeuser
|
USER safeuser
|
||||||
# Prep 'sudo' commands.
|
# Prep 'sudo' commands.
|
||||||
#RUN echo 'abcd8765' | sudo -S pwd
|
#RUN echo 'abcd8765' | sudo -S pwd
|
||||||
|
|
||||||
# Install Redis. TODO: Make this a separate container.
|
|
||||||
|
|
||||||
|
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
WORKDIR /home/safeuser
|
WORKDIR /home/safeuser
|
||||||
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
|
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
|
||||||
|
|||||||
Generated
+3995
-9134
File diff suppressed because it is too large
Load Diff
+11
-8
@@ -14,22 +14,24 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./src/app.js",
|
"start": "node ./src/app.js",
|
||||||
"dev": "nodemon ./dist/app.js",
|
"dev": "nodemon ./dist/app.js",
|
||||||
"test": "npm run lint && npm run test-v4",
|
"test": "npm run lint && npm run test-v5",
|
||||||
"lint": "standard --env mocha --fix",
|
"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-v5": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v5/",
|
||||||
"test:integration": "mocha test/v4/integration",
|
"test:integration": "mocha test/v4/integration",
|
||||||
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js",
|
"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": "nyc report --reporter=text-lcov | coveralls",
|
||||||
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
||||||
"docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs",
|
"docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs",
|
||||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#hydrateUtxosWL' test/v4/integration/",
|
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/",
|
||||||
"test:temp2": "mocha test/v4/rate-limits.js"
|
"test:temp2": "export NETWORK=mainnet && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.15.1"
|
"node": ">=10.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@psf/bch-js": "^4.15.8",
|
"@psf/bch-js": "^4.20.7",
|
||||||
"apidoc": "^0.26.0",
|
"apidoc": "^0.26.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bitcore-lib-cash": "^8.23.1",
|
"bitcore-lib-cash": "^8.23.1",
|
||||||
@@ -66,10 +68,11 @@
|
|||||||
"eslint-plugin-prettier": "^3.1.0",
|
"eslint-plugin-prettier": "^3.1.0",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"nock": "^13.0.5",
|
"nock": "^13.0.5",
|
||||||
"nyc": "^15.0.0",
|
"nyc": "^15.0.0",
|
||||||
"prettier": "^2.0.0",
|
"prettier": "^2.0.0",
|
||||||
"semantic-release": "^17.3.9",
|
"semantic-release": "^17.4.2",
|
||||||
"sinon": "^9.0.0",
|
"sinon": "^9.0.0",
|
||||||
"standard": "^14.3.1"
|
"standard": "^14.3.1"
|
||||||
},
|
},
|
||||||
@@ -83,6 +86,6 @@
|
|||||||
},
|
},
|
||||||
"apidoc": {
|
"apidoc": {
|
||||||
"title": "bch-api",
|
"title": "bch-api",
|
||||||
"url": "https://api.fullstack.cash/v4"
|
"url": "https://api.fullstack.cash/v5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-71
@@ -3,7 +3,6 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
// const { routeRateLimit } = require("./middleware/route-ratelimit")
|
|
||||||
const RateLimits = require('./middleware/route-ratelimit')
|
const RateLimits = require('./middleware/route-ratelimit')
|
||||||
const rateLimits = new RateLimits()
|
const rateLimits = new RateLimits()
|
||||||
|
|
||||||
@@ -25,22 +24,6 @@ const jwtAuth = require('./middleware/jwt-auth')
|
|||||||
// Logging for API requests.
|
// Logging for API requests.
|
||||||
const logReqInfo = require('./middleware/req-logging')
|
const logReqInfo = require('./middleware/req-logging')
|
||||||
|
|
||||||
// v3
|
|
||||||
const healthCheckV3 = require('./routes/v3/health-check')
|
|
||||||
const BlockchainV3 = require('./routes/v3/full-node/blockchain')
|
|
||||||
const ControlV3 = require('./routes/v3/full-node/control')
|
|
||||||
const MiningV3 = require('./routes/v3/full-node/mining')
|
|
||||||
const networkV3 = require('./routes/v3/full-node/network')
|
|
||||||
const RawtransactionsV3 = require('./routes/v3/full-node/rawtransactions')
|
|
||||||
const utilV3 = require('./routes/v3/util')
|
|
||||||
const SlpV3 = require('./routes/v3/slp')
|
|
||||||
const xpubV3 = require('./routes/v3/xpub')
|
|
||||||
const BlockbookV3 = require('./routes/v3/blockbook')
|
|
||||||
const Ninsight = require('./routes/v3/ninsight')
|
|
||||||
const ElectrumXV3 = require('./routes/v3/electrumx')
|
|
||||||
const EncryptionV3 = require('./routes/v3/encryption')
|
|
||||||
const PriceV3 = require('./routes/v3/price')
|
|
||||||
|
|
||||||
// v4
|
// v4
|
||||||
const healthCheckV4 = require('./routes/v4/health-check')
|
const healthCheckV4 = require('./routes/v4/health-check')
|
||||||
const BlockchainV4 = require('./routes/v4/full-node/blockchain')
|
const BlockchainV4 = require('./routes/v4/full-node/blockchain')
|
||||||
@@ -48,40 +31,59 @@ const ControlV4 = require('./routes/v4/full-node/control')
|
|||||||
const MiningV4 = require('./routes/v4/full-node/mining')
|
const MiningV4 = require('./routes/v4/full-node/mining')
|
||||||
const networkV4 = require('./routes/v4/full-node/network')
|
const networkV4 = require('./routes/v4/full-node/network')
|
||||||
const RawtransactionsV4 = require('./routes/v4/full-node/rawtransactions')
|
const RawtransactionsV4 = require('./routes/v4/full-node/rawtransactions')
|
||||||
const utilV4 = require('./routes/v4/util')
|
const UtilV4 = require('./routes/v4/util')
|
||||||
const SlpV4 = require('./routes/v4/slp')
|
const SlpV4 = require('./routes/v4/slp')
|
||||||
const xpubV4 = require('./routes/v4/xpub')
|
const xpubV4 = require('./routes/v4/xpub')
|
||||||
const BlockbookV4 = require('./routes/v4/blockbook')
|
|
||||||
const ElectrumXV4 = require('./routes/v4/electrumx')
|
const ElectrumXV4 = require('./routes/v4/electrumx')
|
||||||
const EncryptionV4 = require('./routes/v4/encryption')
|
const EncryptionV4 = require('./routes/v4/encryption')
|
||||||
const PriceV4 = require('./routes/v4/price')
|
const PriceV4 = require('./routes/v4/price')
|
||||||
|
const Ninsight = require('./routes/v4/ninsight')
|
||||||
|
|
||||||
|
// v5
|
||||||
|
const healthCheckV5 = require('./routes/v5/health-check')
|
||||||
|
const BlockchainV5 = require('./routes/v5/full-node/blockchain')
|
||||||
|
const ControlV5 = require('./routes/v5/full-node/control')
|
||||||
|
const MiningV5 = require('./routes/v5/full-node/mining')
|
||||||
|
const networkV5 = require('./routes/v5/full-node/network')
|
||||||
|
const RawtransactionsV5 = require('./routes/v5/full-node/rawtransactions')
|
||||||
|
const DSProofV5 = require('./routes/v5/full-node/dsproof')
|
||||||
|
const UtilV5 = require('./routes/v5/util')
|
||||||
|
const SlpV5 = require('./routes/v5/slp')
|
||||||
|
const xpubV5 = require('./routes/v5/xpub')
|
||||||
|
const ElectrumXV5 = require('./routes/v5/electrumx')
|
||||||
|
const EncryptionV5 = require('./routes/v5/encryption')
|
||||||
|
const PriceV5 = require('./routes/v5/price')
|
||||||
|
const JWTV5 = require('./routes/v5/jwt')
|
||||||
|
const BcashSLP = require('./routes/v5/bcash/slp')
|
||||||
|
// const Ninsight = require('./routes/v5/ninsight')
|
||||||
|
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
// Instantiate v3 route libraries.
|
|
||||||
const blockchainV3 = new BlockchainV3()
|
|
||||||
const controlV3 = new ControlV3()
|
|
||||||
const miningV3 = new MiningV3()
|
|
||||||
const rawtransactionsV3 = new RawtransactionsV3()
|
|
||||||
const slpV3 = new SlpV3()
|
|
||||||
const blockbookV3 = new BlockbookV3()
|
|
||||||
const electrumxv3 = new ElectrumXV3()
|
|
||||||
electrumxv3.connect()
|
|
||||||
const encryptionv3 = new EncryptionV3()
|
|
||||||
const pricev3 = new PriceV3()
|
|
||||||
|
|
||||||
// Instantiate v4 route libraries.
|
// Instantiate v4 route libraries.
|
||||||
const blockchainV4 = new BlockchainV4()
|
const blockchainV4 = new BlockchainV4()
|
||||||
const controlV4 = new ControlV4()
|
const controlV4 = new ControlV4()
|
||||||
const miningV4 = new MiningV4()
|
const miningV4 = new MiningV4()
|
||||||
const rawtransactionsV4 = new RawtransactionsV4()
|
const rawtransactionsV4 = new RawtransactionsV4()
|
||||||
const slpV4 = new SlpV4()
|
const slpV4 = new SlpV4()
|
||||||
const blockbookV4 = new BlockbookV4()
|
|
||||||
const electrumxv4 = new ElectrumXV4()
|
const electrumxv4 = new ElectrumXV4()
|
||||||
electrumxv4.connect()
|
electrumxv4.connect()
|
||||||
const encryptionv4 = new EncryptionV4()
|
const encryptionv4 = new EncryptionV4()
|
||||||
const pricev4 = new PriceV4()
|
const pricev4 = new PriceV4()
|
||||||
|
const utilV4 = new UtilV4({ electrumx: electrumxv4 })
|
||||||
|
|
||||||
|
// Instantiate v5 route libraries.
|
||||||
|
const blockchainV5 = new BlockchainV5()
|
||||||
|
const controlV5 = new ControlV5()
|
||||||
|
const miningV5 = new MiningV5()
|
||||||
|
const rawtransactionsV5 = new RawtransactionsV5()
|
||||||
|
const slpV5 = new SlpV5()
|
||||||
|
const electrumxv5 = new ElectrumXV5()
|
||||||
|
const encryptionv5 = new EncryptionV5()
|
||||||
|
const pricev5 = new PriceV5()
|
||||||
|
const utilV5 = new UtilV5({ electrumx: electrumxv5 })
|
||||||
|
const dsproofV5 = new DSProofV5()
|
||||||
|
const jwtV5 = new JWTV5()
|
||||||
|
const bcashSLP = new BcashSLP()
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.locals.env = process.env
|
app.locals.env = process.env
|
||||||
@@ -122,43 +124,43 @@ app.use(express.static(path.join(__dirname, 'public')))
|
|||||||
// Log requests for later analysis.
|
// Log requests for later analysis.
|
||||||
app.use('/', logReqInfo)
|
app.use('/', logReqInfo)
|
||||||
|
|
||||||
// const v2prefix = "v2"
|
|
||||||
const v3prefix = 'v3'
|
|
||||||
const v4prefix = 'v4'
|
const v4prefix = 'v4'
|
||||||
|
const v5prefix = 'v5'
|
||||||
|
|
||||||
// Inspect the header for a JWT token.
|
// START Rate Limits
|
||||||
app.use(`/${v3prefix}/`, jwtAuth.getTokenFromHeaders)
|
|
||||||
app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders)
|
|
||||||
|
|
||||||
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
|
|
||||||
// Handles Anonymous and Basic Authorization schemes used by passport.js
|
|
||||||
const auth = new AuthMW()
|
const auth = new AuthMW()
|
||||||
app.use(`/${v3prefix}/`, auth.mw())
|
|
||||||
app.use(`/${v4prefix}/`, auth.mw())
|
|
||||||
|
|
||||||
// Rate limit on all v3 routes
|
// Ensure req.locals and res.locals objects exist.
|
||||||
// Establish and enforce rate limits.
|
app.use(`/${v4prefix}/`, rateLimits.populateLocals)
|
||||||
// app.use(`/${v3prefix}/`, rateLimits.routeRateLimit)
|
app.use(`/${v5prefix}/`, rateLimits.populateLocals)
|
||||||
app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource)
|
|
||||||
|
|
||||||
// Rate limit on all v4 routes
|
// Allow users to turn off rate limits with an environment variable.
|
||||||
// Establish and enforce rate limits.
|
const DO_NOT_USE_RATE_LIMITS = process.env.DO_NOT_USE_RATE_LIMITS || false
|
||||||
app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource)
|
|
||||||
|
|
||||||
// Connect v3 routes
|
console.log(`DO_NOT_USE_RATE_LIMITS: ${DO_NOT_USE_RATE_LIMITS}`)
|
||||||
app.use(`/${v3prefix}/` + 'health-check', healthCheckV3)
|
|
||||||
app.use(`/${v3prefix}/` + 'blockchain', blockchainV3.router)
|
if (!DO_NOT_USE_RATE_LIMITS) {
|
||||||
app.use(`/${v3prefix}/` + 'control', controlV3.router)
|
console.log('Rate limits are being used')
|
||||||
app.use(`/${v3prefix}/` + 'mining', miningV3.router)
|
// Inspect the header for a JWT token.
|
||||||
app.use(`/${v3prefix}/` + 'network', networkV3)
|
app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders)
|
||||||
app.use(`/${v3prefix}/` + 'rawtransactions', rawtransactionsV3.router)
|
app.use(`/${v5prefix}/`, jwtAuth.getTokenFromHeaders)
|
||||||
app.use(`/${v3prefix}/` + 'util', utilV3.router)
|
|
||||||
app.use(`/${v3prefix}/` + 'slp', slpV3.router)
|
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
|
||||||
app.use(`/${v3prefix}/` + 'xpub', xpubV3.router)
|
// Handles Anonymous and Basic Authorization schemes used by passport.js
|
||||||
app.use(`/${v3prefix}/` + 'blockbook', blockbookV3.router)
|
app.use(`/${v4prefix}/`, auth.mw())
|
||||||
app.use(`/${v3prefix}/` + 'electrumx', electrumxv3.router)
|
app.use(`/${v5prefix}/`, auth.mw())
|
||||||
app.use(`/${v3prefix}/` + 'encryption', encryptionv3.router)
|
|
||||||
app.use(`/${v3prefix}/` + 'price', pricev3.router)
|
// Experimental rate limits
|
||||||
|
app.use(`/${v4prefix}/`, rateLimits.applyRateLimits)
|
||||||
|
app.use(`/${v5prefix}/`, rateLimits.applyRateLimits)
|
||||||
|
|
||||||
|
// Rate limit on all v4 routes
|
||||||
|
// Establish and enforce rate limits.
|
||||||
|
// app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource)
|
||||||
|
} else {
|
||||||
|
console.log('Rate limits are NOT being used')
|
||||||
|
}
|
||||||
|
// END Rate Limits
|
||||||
|
|
||||||
// Connect v4 routes
|
// Connect v4 routes
|
||||||
app.use(`/${v4prefix}/` + 'health-check', healthCheckV4)
|
app.use(`/${v4prefix}/` + 'health-check', healthCheckV4)
|
||||||
@@ -167,18 +169,37 @@ app.use(`/${v4prefix}/` + 'control', controlV4.router)
|
|||||||
app.use(`/${v4prefix}/` + 'mining', miningV4.router)
|
app.use(`/${v4prefix}/` + 'mining', miningV4.router)
|
||||||
app.use(`/${v4prefix}/` + 'network', networkV4)
|
app.use(`/${v4prefix}/` + 'network', networkV4)
|
||||||
app.use(`/${v4prefix}/` + 'rawtransactions', rawtransactionsV4.router)
|
app.use(`/${v4prefix}/` + 'rawtransactions', rawtransactionsV4.router)
|
||||||
app.use(`/${v4prefix}/` + 'util', utilV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'slp', slpV4.router)
|
app.use(`/${v4prefix}/` + 'slp', slpV4.router)
|
||||||
app.use(`/${v4prefix}/` + 'xpub', xpubV4.router)
|
app.use(`/${v4prefix}/` + 'xpub', xpubV4.router)
|
||||||
app.use(`/${v4prefix}/` + 'blockbook', blockbookV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'electrumx', electrumxv4.router)
|
app.use(`/${v4prefix}/` + 'electrumx', electrumxv4.router)
|
||||||
app.use(`/${v4prefix}/` + 'encryption', encryptionv4.router)
|
app.use(`/${v4prefix}/` + 'encryption', encryptionv4.router)
|
||||||
app.use(`/${v4prefix}/` + 'price', pricev4.router)
|
app.use(`/${v4prefix}/` + 'price', pricev4.router)
|
||||||
|
app.use(`/${v4prefix}/` + 'util', utilV4.router)
|
||||||
|
|
||||||
const ninsight = new Ninsight()
|
const ninsight = new Ninsight()
|
||||||
app.use(`/${v3prefix}/` + 'ninsight', ninsight.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'ninsight', ninsight.router)
|
app.use(`/${v4prefix}/` + 'ninsight', ninsight.router)
|
||||||
|
|
||||||
|
// Connect v5 routes
|
||||||
|
app.use(`/${v5prefix}/` + 'health-check', healthCheckV5)
|
||||||
|
app.use(`/${v5prefix}/` + 'blockchain', blockchainV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'control', controlV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'mining', miningV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'network', networkV5)
|
||||||
|
app.use(`/${v5prefix}/` + 'rawtransactions', rawtransactionsV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'slp', slpV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'xpub', xpubV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'electrumx', electrumxv5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'encryption', encryptionv5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'price', pricev5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'util', utilV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'dsproof', dsproofV5.router)
|
||||||
|
app.use(`/${v5prefix}/` + 'jwt', jwtV5.router)
|
||||||
|
|
||||||
|
// const ninsight = new Ninsight()
|
||||||
|
app.use(`/${v5prefix}/` + 'ninsight', ninsight.router)
|
||||||
|
|
||||||
|
app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router)
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const err = {
|
const err = {
|
||||||
@@ -225,9 +246,11 @@ server.listen(port)
|
|||||||
server.on('error', onError)
|
server.on('error', onError)
|
||||||
server.on('listening', onListening)
|
server.on('listening', onListening)
|
||||||
|
|
||||||
// Set the time before a timeout error is generated. This impacts testing and
|
// Set the time before a timeout error is generated.
|
||||||
// the handling of timeout errors. Is 10 seconds too agressive?
|
// 10 seconds is way too agressive. 30 Seconds was used for a while, but with
|
||||||
server.setTimeout(30 * 1000)
|
// being able to set a timeout between UTXOs for tokenUtxoDetails, the timeout
|
||||||
|
// needed to be extended.
|
||||||
|
server.setTimeout(1000 * 60 * 5) // 5 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a port into a number, string, or false.
|
* Normalize a port into a number, string, or false.
|
||||||
@@ -262,11 +285,11 @@ function onError (error) {
|
|||||||
case 'EACCES':
|
case 'EACCES':
|
||||||
console.error(`${bind} requires elevated privileges`)
|
console.error(`${bind} requires elevated privileges`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
// break
|
// break
|
||||||
case 'EADDRINUSE':
|
case 'EADDRINUSE':
|
||||||
console.error(`${bind} is already in use`)
|
console.error(`${bind} is already in use`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
// break
|
// break
|
||||||
default:
|
default:
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -1,22 +1,25 @@
|
|||||||
/*
|
/*
|
||||||
CT 2/4/20 Note: This library handles anonymous and Basic auth. This library
|
This library handles anonymous and Basic Authentication.
|
||||||
can be phased out with the chage to JWT tokens and the new rate-limit library.
|
|
||||||
|
|
||||||
Handle authorization for bypassing rate limits.
|
|
||||||
|
|
||||||
1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by
|
1) Default is 'Anonymous Authentication', which unlocks the freemimum tier by
|
||||||
default.
|
default.
|
||||||
|
|
||||||
2) Hard-coded 'Basic Authentication' is a token that does not expire and is
|
2) Hard-coded 'Basic Authentication' is a token that does not expire and is
|
||||||
provided to buisiness partners.
|
provided for clients who run their own isolated infrastructure without rate
|
||||||
|
limits, but still need a way from preventing the random public from using
|
||||||
|
their API.
|
||||||
|
|
||||||
3) JWT-based 'Local Authentication' is used for normal users that pay to
|
3) JWT-based 'Local Authentication' is used for normal users that pay to
|
||||||
access the premium pro-tier services.
|
access the premium pro-tier services.
|
||||||
|
|
||||||
This file uses the passport npm library to check the header of each REST API
|
This file uses the passport npm library to check the header of each REST API
|
||||||
call for the prescence of a Basic authorization header:
|
call for the prescence of a Basic Authentication header:
|
||||||
https://en.wikipedia.org/wiki/Basic_access_authentication
|
https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||||
|
|
||||||
If the header is found and validated, the req.locals.proLimit Boolean value
|
If the header is found and validated, the req.locals.proLimit Boolean value
|
||||||
is set and passed to the route-ratelimits.ts middleware.
|
is set and passed to the route-ratelimit.js middleware. route-ratelimit.js
|
||||||
|
is for fine-grain JWT-based rate limits. If req.locals.proLimit is set to
|
||||||
|
true, then those rate limits will be skipped.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
@@ -76,8 +79,9 @@ class AuthMW {
|
|||||||
req.locals.proLimit = false
|
req.locals.proLimit = false
|
||||||
|
|
||||||
// Evaluate the username and password and set the rate limit accordingly.
|
// Evaluate the username and password and set the rate limit accordingly.
|
||||||
// if (username === "BITBOX" && password === PRO_PASS) {
|
|
||||||
if (username === 'fullstackcash') {
|
if (username === 'fullstackcash') {
|
||||||
|
// Can set several different passwords in the environment variable.
|
||||||
|
// Loop through each one to see if one matches.
|
||||||
for (let i = 0; i < PRO_PASS.length; i++) {
|
for (let i = 0; i < PRO_PASS.length; i++) {
|
||||||
const thisPass = PRO_PASS[i]
|
const thisPass = PRO_PASS[i]
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
// req.locals.jwtToken property.
|
// req.locals.jwtToken property.
|
||||||
const getTokenFromHeaders = (req, res, next) => {
|
const getTokenFromHeaders = (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
// console.log('req.headers: ', req.headers)
|
||||||
|
|
||||||
// Only executes if the authorization header exists.
|
// Only executes if the authorization header exists.
|
||||||
if (req.headers.authorization) {
|
if (req.headers.authorization) {
|
||||||
// Retrieve the auth string from the header object.
|
// Retrieve the auth string from the header object.
|
||||||
|
|||||||
+353
-230
@@ -1,49 +1,62 @@
|
|||||||
/*
|
/*
|
||||||
Sets the rate limits for the anonymous and paid tiers. Current rate limits:
|
This file will replace the original rate-limit.js file.
|
||||||
- 1000 points in 60 seconds
|
|
||||||
- 10 points per call for paid tier (100 RPM)
|
|
||||||
- 50 points per call for anonymous tier (20 RPM)
|
|
||||||
|
|
||||||
Background:
|
Sets the rate limits for the anonymous and paid tiers. Current rate limits:
|
||||||
The rate limits below were originially coded with the idea of charging on a
|
- 10000 points in 60 seconds
|
||||||
per-resource basis. However, that was confusing to end users trying to purchase
|
- 500 points per call for anonymous tier (20 RPM)
|
||||||
a subscription. So everything was simplied to two tiers: paid and anonymous
|
- 100 points per call for tier 40 (100 RPM)
|
||||||
|
- 40 points per call for tier 50 (250 RPM)
|
||||||
|
- 16 points per call for tier 60 (625 RPM)
|
||||||
|
|
||||||
|
The rate limit handling is designed for these four use cases:
|
||||||
|
- Users who want to buy a JWT token for 24 hour access.
|
||||||
|
- Users who want to buy different RPM tiers: 100, 250, 600
|
||||||
|
- Basic Authentication which should not have any rate limits applied.
|
||||||
|
- Local installations that do not want any authentication or rate limits at all.
|
||||||
|
|
||||||
|
The Basic Auth use cases is considered when determining internal rate limits.
|
||||||
|
The internal rate limits should not be applied to calls from those users.
|
||||||
|
|
||||||
|
A lot of attention has been paid to passing rate-limit information for the user
|
||||||
|
when they trigger an endpoint that makes a lot of internal API calls. Examples
|
||||||
|
are hydrateUtxos() and getPublicKey(). These keeps things fair by charging the
|
||||||
|
same for 'light' API calls and 'heavy' API calls.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- Add code for applying rate limits to whitelist domains.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
// Public npm libraries.
|
// Public npm libraries.
|
||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('jsonwebtoken')
|
||||||
|
const Redis = require('ioredis')
|
||||||
|
const { RateLimiterRedis } = require('rate-limiter-flexible')
|
||||||
|
|
||||||
// local libraries.
|
// local libraries.
|
||||||
const wlogger = require('../util/winston-logging')
|
const wlogger = require('../util/winston-logging')
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
|
||||||
const ANON_LIMITS = config.anonRateLimit
|
let _this // Global pointer to instance of class, when 'this' context is lost.
|
||||||
const WHITELIST_RATE_LIMIT = config.whitelistRateLimit
|
|
||||||
const WHITELIST_DOMAINS = config.whitelistDomains
|
|
||||||
const INTERNAL_RATE_LIMIT = 1
|
|
||||||
|
|
||||||
// Redis
|
// Setup Redis to track rate limits for each user.
|
||||||
const redisOptions = {
|
const redisOptions = {
|
||||||
enableOfflineQueue: false,
|
enableOfflineQueue: false,
|
||||||
port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379,
|
port: process.env.REDIS_PORT ? process.env.REDIS_PORT : 6379,
|
||||||
host: process.env.REDIS_HOST ? process.env.REDIS_HOST : '127.0.0.1'
|
host: process.env.REDIS_HOST ? process.env.REDIS_HOST : '127.0.0.1'
|
||||||
}
|
}
|
||||||
console.log(`redisOptions: ${JSON.stringify(redisOptions, null, 2)}`)
|
|
||||||
|
|
||||||
const Redis = require('ioredis')
|
|
||||||
const redisClient = new Redis(redisOptions)
|
const redisClient = new Redis(redisOptions)
|
||||||
|
|
||||||
// Rate limiter middleware lib.
|
|
||||||
const { RateLimiterRedis } = require('rate-limiter-flexible')
|
|
||||||
const rateLimitOptions = {
|
const rateLimitOptions = {
|
||||||
storeClient: redisClient,
|
storeClient: redisClient,
|
||||||
points: 1000, // Number of points
|
points: config.pointsPerMinute, // Number of points
|
||||||
duration: 60 // Per minute (per 60 seconds)
|
duration: 60 // Per minute (per 60 seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
let _this
|
// Constants
|
||||||
|
const ANON_LIMITS = config.anonRateLimit
|
||||||
|
// const WHITELIST_RATE_LIMIT = config.whitelistRateLimit
|
||||||
|
const WHITELIST_DOMAINS = config.whitelistDomains
|
||||||
|
const WHITELIST_POINTS_TO_CONSUME = config.whitelistRateLimit
|
||||||
|
const POINTS_PER_MINUTE = config.pointsPerMinute
|
||||||
|
const INTERNAL_POINTS_TO_CONSUME = config.internalRateLimit
|
||||||
|
|
||||||
class RateLimits {
|
class RateLimits {
|
||||||
constructor () {
|
constructor () {
|
||||||
@@ -54,248 +67,273 @@ class RateLimits {
|
|||||||
this.config = config
|
this.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to disconnect from the Redis DB.
|
// This is the main middleware funciton of this library. All other functions
|
||||||
// Called by unit tests so that node.js thread doesn't live forever.
|
// support this function.
|
||||||
closeRedis () {
|
async applyRateLimits (req, res, next) {
|
||||||
redisClient.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async wipeRedis () {
|
|
||||||
await redisClient.flushdb()
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the new rate limit function that uses the rate-limiter-flexible npm
|
|
||||||
// library. It uses fine-grain rate limiting based on the resources being
|
|
||||||
// consumed.
|
|
||||||
async rateLimitByResource (req, res, next) {
|
|
||||||
try {
|
try {
|
||||||
let userId
|
// Exit if the user has already authenticated with Basic Authentication.
|
||||||
let decoded = {}
|
if (req.locals.proLimit) {
|
||||||
|
console.log('External call, basic auth, skipping rate limits.')
|
||||||
// Create a req.locals object if not passed in.
|
wlogger.debug(
|
||||||
if (!req.locals) {
|
'req.locals.proLimit = true; Using Basic Authentication instead of rate limits'
|
||||||
req.locals = {
|
)
|
||||||
// default values
|
return next()
|
||||||
jwtToken: '',
|
|
||||||
proLimit: false,
|
|
||||||
apiLevel: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a res.locals object if it does not exist. This is used for
|
// Determine if the call is an external or internal API call.
|
||||||
// debugging.
|
const isInternal = _this.checkInternalIp(req)
|
||||||
if (!res.locals) {
|
// console.log(`isInternal: ${isInternal}`)
|
||||||
res.locals = {
|
|
||||||
rateLimitTriggered: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode the JWT token if one exists.
|
// Determine if the call originates from another computer on the intranet.
|
||||||
if (req.locals.jwtToken) {
|
const isWhitelistOrigin = _this.isInWhitelist(req)
|
||||||
try {
|
// console.log('isWhitelistOrigin: ', isWhitelistOrigin)
|
||||||
decoded = _this.jwt.verify(
|
|
||||||
req.locals.jwtToken,
|
|
||||||
_this.config.apiTokenSecret
|
|
||||||
)
|
|
||||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
|
||||||
|
|
||||||
userId = decoded.id
|
// Handle the use case of internally-generated requests.
|
||||||
} catch (err) {
|
if (isInternal) {
|
||||||
// This handler will be triggered if the JWT token does not match the
|
// Internal API calls should pass the authentication data in through the
|
||||||
// token secret.
|
// the usrObj in the body.
|
||||||
wlogger.error(
|
if (req.body && req.body.usrObj) {
|
||||||
`Last three letters of token secret: ${_this.config.apiTokenSecret.slice(
|
if (req.body.usrObj.proLimit) {
|
||||||
-3
|
// console.log('Internal call, basic auth, skipping rate limits.')
|
||||||
)}`
|
|
||||||
)
|
// If this is an internal call that originated from a user using
|
||||||
wlogger.error(
|
// Basic Authentication, then skip rate-limits.
|
||||||
'Error trying to decode JWT token in route-ratelimit.js/newRateLimit(): ',
|
return next()
|
||||||
err
|
} else {
|
||||||
|
// console.log(
|
||||||
|
// 'Internal call, applying rate limits. Using JWT if available.'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// Determine if user has exceeded their rate limits. Pass in the
|
||||||
|
// JWT token if one exists.
|
||||||
|
const hasExceededRateLimit = await _this.trackRateLimits(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
req.body.usrObj.jwtToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasExceededRateLimit) {
|
||||||
|
// Rate limits have not been exceeded. Processing can continue.
|
||||||
|
return next()
|
||||||
|
} else {
|
||||||
|
// trackRateLimits() returns the 'res' object with an error message
|
||||||
|
// and status code.
|
||||||
|
return hasExceededRateLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This should be a corner case. Calls should not be going into this
|
||||||
|
// code path, so the system should throw up big warning signs when they
|
||||||
|
// do.
|
||||||
|
// This code path happens when an internal call is made but does not
|
||||||
|
// pass the usrObj. Legacy code needs to be refactored to use the usrObj
|
||||||
|
// and avoid this code path. This code path is 'pooled': all users
|
||||||
|
// share the same rate limits. Even at 1000 RPM, this pool will get
|
||||||
|
// exhausted easily.
|
||||||
|
// const warnMsg =
|
||||||
|
// 'Internal call. req.body.usrObj does not exist. Applying high-speed internal rate limits.'
|
||||||
|
// console.log(warnMsg)
|
||||||
|
// wlogger.info(warnMsg)
|
||||||
|
|
||||||
|
const defaultPayload = {
|
||||||
|
id: '98.76.54.32',
|
||||||
|
email: 'internal@bchtest.net',
|
||||||
|
apiLevel: 40,
|
||||||
|
rateLimit: 100,
|
||||||
|
pointsToConsume: INTERNAL_POINTS_TO_CONSUME,
|
||||||
|
duration: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values, in case there is an error.
|
||||||
|
const defaultJwt = _this.generateJwtToken(defaultPayload)
|
||||||
|
|
||||||
|
// Track the rate limit for this user. Pass in the JWT token, if one
|
||||||
|
// is available.
|
||||||
|
const hasExceededRateLimit = await _this.trackRateLimits(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
defaultJwt
|
||||||
)
|
)
|
||||||
|
// console.log(`hasExceededRateLimit: `, hasExceededRateLimit)
|
||||||
|
|
||||||
|
if (!hasExceededRateLimit) {
|
||||||
|
// Rate limits have not been exceeded. Processing can continue.
|
||||||
|
return next()
|
||||||
|
} else {
|
||||||
|
// trackRateLimits() returns the 'res' object with an error message
|
||||||
|
// and status code.
|
||||||
|
return hasExceededRateLimit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
} else if (req.body && req.body.usrObj) {
|
//
|
||||||
// Same as above, but this code path is activated from internal calls to
|
|
||||||
// bch-js, like hydrateUtxo(), which passes the user object from the
|
|
||||||
// original API call.
|
|
||||||
|
|
||||||
try {
|
|
||||||
decoded = _this.jwt.verify(
|
|
||||||
req.body.usrObj.jwtToken,
|
|
||||||
_this.config.apiTokenSecret
|
|
||||||
)
|
|
||||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
|
||||||
|
|
||||||
userId = decoded.id
|
|
||||||
} catch (err) {
|
|
||||||
// This handler will be triggered if the JWT token does not match the
|
|
||||||
// token secret.
|
|
||||||
wlogger.error(
|
|
||||||
'Error in route-ratelimit.js trying to decode JWT token in usrObj'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
wlogger.debug('No JWT token found!')
|
// Handle the normal use-case of external requests
|
||||||
}
|
// console.log(
|
||||||
|
// 'External call, applying rate limits. Using JWT if available.'
|
||||||
|
// )
|
||||||
|
|
||||||
// Default value is 50 points per request = 20 RPM
|
// For calls originating from a whitelist domain, apply a high-RPM
|
||||||
let rateLimit = ANON_LIMITS
|
// JWT token to the call.
|
||||||
|
if (isWhitelistOrigin) {
|
||||||
// Only evaluate the JWT token if the user is not using Basic Authentication.
|
const defaultPayload = {
|
||||||
if (!req.locals.proLimit) {
|
id: '77.77.77.77',
|
||||||
// Code here for the rate limiter is adapted from this example:
|
email: 'whitelist@bchtest.net',
|
||||||
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
|
apiLevel: 40,
|
||||||
try {
|
rateLimit: 100,
|
||||||
// The resource being consumed: full node, indexer, SLPDB, etc.
|
pointsToConsume: WHITELIST_POINTS_TO_CONSUME,
|
||||||
const resource = _this.getResource(req.url)
|
duration: 30
|
||||||
wlogger.debug(`resource: ${resource}`)
|
|
||||||
|
|
||||||
// Key will be the JWT ID if it exists, otherwise the IP address of the caller.
|
|
||||||
let key = userId || req.ip
|
|
||||||
res.locals.key = key // Feedback for tests.
|
|
||||||
// console.log(`key: ${key}`)
|
|
||||||
|
|
||||||
// const pointsToConsume = userId ? 1 : 30
|
|
||||||
decoded.resource = resource
|
|
||||||
let pointsToConsume = _this.calcPoints(decoded)
|
|
||||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
|
||||||
|
|
||||||
// Retrieve the origin.
|
|
||||||
let origin = req.get('origin')
|
|
||||||
|
|
||||||
// Handle calls coming from the intranet.
|
|
||||||
if (origin === undefined && key.indexOf('10.0.0.5') > -1) {
|
|
||||||
origin = 'slp-api'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wlogger.info(`origin: ${origin}`)
|
// Inject the high-RPM JWT token into the call.
|
||||||
|
req.locals.jwtToken = _this.generateJwtToken(defaultPayload)
|
||||||
|
}
|
||||||
|
|
||||||
// If the request originates from one of the approved wallet apps, then
|
// Track the rate limit for this user. Pass in the JWT token, if one
|
||||||
// apply paid-access rate limits.
|
// is available.
|
||||||
// console.log(`origin: ${JSON.stringify(origin, null, 2)}`)
|
const hasExceededRateLimit = await _this.trackRateLimits(
|
||||||
// console.log(`whitelist: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
|
req,
|
||||||
const isInWhitelist = _this.isInWhitelist(origin)
|
res,
|
||||||
if (isInWhitelist) {
|
req.locals.jwtToken
|
||||||
pointsToConsume = WHITELIST_RATE_LIMIT
|
)
|
||||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
// console.log('hasExceededRateLimit: ', hasExceededRateLimit)
|
||||||
}
|
|
||||||
|
|
||||||
// For internal calls, increase rate limits to as fast as possible.
|
if (!hasExceededRateLimit) {
|
||||||
if (
|
// Rate limits have not been exceeded. Processing can continue.
|
||||||
// Comment out the line below when running bch-js e2e rate limit tests.
|
return next()
|
||||||
key.toString().indexOf('::ffff:127.0.0.1') > -1 ||
|
} else {
|
||||||
// Do not comment out this line.
|
// trackRateLimits() returns the 'res' object with an error message
|
||||||
key.toString().indexOf('172.17.') > -1
|
// and status code.
|
||||||
) {
|
return hasExceededRateLimit
|
||||||
pointsToConsume = INTERNAL_RATE_LIMIT
|
|
||||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.info(
|
|
||||||
`User ${key} consuming ${pointsToConsume} point for resource ${resource}.`
|
|
||||||
)
|
|
||||||
|
|
||||||
rateLimit = Math.floor(1000 / pointsToConsume)
|
|
||||||
|
|
||||||
// Update the key so that rate limits track both the user and the resource.
|
|
||||||
key = `${key}-${resource}`
|
|
||||||
|
|
||||||
await _this.rateLimiter.consume(key, pointsToConsume)
|
|
||||||
} catch (err) {
|
|
||||||
// console.log('err: ', err)
|
|
||||||
|
|
||||||
// Used for returning data for tests.
|
|
||||||
res.locals.rateLimitTriggered = true
|
|
||||||
// console.log('res.locals: ', res.locals)
|
|
||||||
|
|
||||||
// Rate limited was triggered
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: `Too many requests. Your limits are currently ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash`
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error('Error in route-ratelimit.js/newRateLimit(): ', err)
|
wlogger.error('Error in route-ratelimit2.js/applyRateLimits(): ', err)
|
||||||
// throw err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// By default, move to the next middleware.
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculates the points consumed, based on the jwt information and the route
|
// A wrapper for Redis-based rate limiter.
|
||||||
// requested.
|
// Will return false if the user has not exceeded the rate limit. Otherwise
|
||||||
calcPoints (jwtInfo) {
|
// it will return the 'res' object with an error status and message, which
|
||||||
let retVal = ANON_LIMITS // By default, use anonymous tier.
|
// should be returned by the middleware.
|
||||||
|
async trackRateLimits (req, res, jwtToken) {
|
||||||
try {
|
const debugInfo = {
|
||||||
// console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`)
|
jwtToken,
|
||||||
|
userObj: req.body.usrObj,
|
||||||
const apiLevel = jwtInfo.apiLevel
|
locals: req.locals
|
||||||
const resource = jwtInfo.resource
|
|
||||||
|
|
||||||
const level30Routes = ['insight', 'bitcore', 'blockbook', 'electrumx']
|
|
||||||
const level40Routes = ['slp']
|
|
||||||
|
|
||||||
wlogger.debug(`apiLevel: ${apiLevel}`)
|
|
||||||
|
|
||||||
// Only evaluate if user is using a JWT token.
|
|
||||||
if (jwtInfo.id) {
|
|
||||||
// SLP indexer routes
|
|
||||||
if (level40Routes.includes(resource)) {
|
|
||||||
if (apiLevel >= 40) retVal = 10
|
|
||||||
// else if (apiLevel >= 10) retVal = 10
|
|
||||||
else retVal = ANON_LIMITS
|
|
||||||
|
|
||||||
// Normal indexer routes
|
|
||||||
} else if (level30Routes.includes(resource)) {
|
|
||||||
if (apiLevel >= 30) retVal = 10
|
|
||||||
else retVal = ANON_LIMITS
|
|
||||||
|
|
||||||
// Full node tier
|
|
||||||
} else if (apiLevel >= 20) {
|
|
||||||
retVal = 10
|
|
||||||
|
|
||||||
// Free tier, full node only.
|
|
||||||
} else {
|
|
||||||
retVal = ANON_LIMITS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retVal
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in route-ratelimit.js/calcPoints()')
|
|
||||||
// throw err
|
|
||||||
retVal = ANON_LIMITS
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return retVal
|
// 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.
|
||||||
|
debugInfo.ip = req.ip
|
||||||
|
|
||||||
|
// console.log('jwtToken: ', jwtToken)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Decode the JWT token if it exists
|
||||||
|
if (jwtToken) {
|
||||||
|
const decoded = _this.decodeJwtToken(jwtToken)
|
||||||
|
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||||
|
|
||||||
|
// Preferentially use the decoded ID in the JWT payload, as the key.
|
||||||
|
key = decoded.id
|
||||||
|
debugInfo.id = key
|
||||||
|
|
||||||
|
pointsToConsume = decoded.pointsToConsume
|
||||||
|
debugInfo.pointsToConsume = pointsToConsume
|
||||||
|
}
|
||||||
|
// console.log(`rate limit key: ${key}`)
|
||||||
|
|
||||||
|
// This function will throw an error if the user exceeds the rate limit.
|
||||||
|
// 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.
|
||||||
|
return false
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('err: ', err)
|
||||||
|
|
||||||
|
const rateLimit = Math.floor(POINTS_PER_MINUTE / pointsToConsume)
|
||||||
|
|
||||||
|
res.locals.rateLimitTriggered = true
|
||||||
|
// console.log('res.locals: ', res.locals)
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// `rate limit debug info: ${JSON.stringify(debugInfo, null, 2)}`
|
||||||
|
// )
|
||||||
|
|
||||||
|
// Rate limited was triggered
|
||||||
|
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
|
return res.json({
|
||||||
|
error: `Too many requests. Your limits are currently ${rateLimit} requests per minute. Increase rate limits at https://fullstack.cash`
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function parses the req.url property to identify what resource
|
// Attempts to decode a JWT token. Returns default values if it fails.
|
||||||
// the user is requesting.
|
decodeJwtToken (jwtToken) {
|
||||||
// This was created as a function so that it can be unit tested. Not sure
|
const defaultPayload = {
|
||||||
// what kind of variations will be seen in production.
|
id: '123.456.789.10',
|
||||||
getResource (url) {
|
email: 'test@bchtest.net',
|
||||||
|
apiLevel: 10,
|
||||||
|
rateLimit: 3,
|
||||||
|
pointsToConsume: ANON_LIMITS,
|
||||||
|
duration: 30
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`)
|
// Default values, in case there is an error.
|
||||||
|
const defaultJwt = _this.generateJwtToken(defaultPayload)
|
||||||
|
|
||||||
const splitUrl = url.split('/')
|
// Generate a default payload to use, if the decoding of the user-provided
|
||||||
const resource = splitUrl[1]
|
// jwt fails.
|
||||||
|
let decoded = _this.jwt.verify(defaultJwt, _this.config.apiTokenSecret)
|
||||||
|
|
||||||
return resource
|
try {
|
||||||
|
decoded = _this.jwt.verify(jwtToken, _this.config.apiTokenSecret)
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in route-ratelimit2.js/decodeJwtTokens(): ', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error('Error in getResource().')
|
wlogger.error(
|
||||||
throw err
|
'Unhandled error in route-ratelimit2.js/deocdeJwtToken: ',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
|
||||||
|
// Making sure there is an exp property. Not sure if this will cause an
|
||||||
|
// issue, using a hard-coded value.
|
||||||
|
defaultPayload.exp = 1574269450
|
||||||
|
|
||||||
|
return defaultPayload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a boolean if the origin of the request matches a domain in the
|
// Returns a boolean if the origin of the request matches a domain in the
|
||||||
// whitelist.
|
// whitelist.
|
||||||
isInWhitelist (origin) {
|
isInWhitelist (req) {
|
||||||
try {
|
try {
|
||||||
const retVal = false // Default value.
|
const retVal = false // Default value.
|
||||||
|
|
||||||
|
// Retrieve the origin.
|
||||||
|
const origin = req.get('origin')
|
||||||
|
|
||||||
|
if (!process.env.TEST) console.log('origin:', origin)
|
||||||
|
|
||||||
|
// If the origin is not determinable, return false.
|
||||||
if (!origin) return false
|
if (!origin) return false
|
||||||
|
|
||||||
// console.log(`WHITELIST_DOMAINS: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
|
// console.log(`WHITELIST_DOMAINS: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
|
||||||
@@ -303,9 +341,7 @@ class RateLimits {
|
|||||||
for (let i = 0; i < WHITELIST_DOMAINS.length; i++) {
|
for (let i = 0; i < WHITELIST_DOMAINS.length; i++) {
|
||||||
const thisDomain = WHITELIST_DOMAINS[i]
|
const thisDomain = WHITELIST_DOMAINS[i]
|
||||||
|
|
||||||
if (origin.toString().indexOf(thisDomain) > -1) {
|
if (origin.includes(thisDomain)) return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return retVal
|
return retVal
|
||||||
@@ -316,6 +352,93 @@ class RateLimits {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks the request object to see if it's IP address matches an internal
|
||||||
|
// IP address. That means the call is an internal API call and should be
|
||||||
|
// treated differently than an external API call.
|
||||||
|
checkInternalIp (req) {
|
||||||
|
try {
|
||||||
|
// Default value
|
||||||
|
let isInternal = false
|
||||||
|
|
||||||
|
const ip = req.ip
|
||||||
|
|
||||||
|
if (ip.includes('127.0.0.1')) isInternal = true
|
||||||
|
|
||||||
|
if (ip.includes('172.17.')) isInternal = true
|
||||||
|
|
||||||
|
// TODO: Add 192.168.
|
||||||
|
|
||||||
|
return isInternal
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error(
|
||||||
|
'Error in checkInternalIp(). Returning false be default. Err: ',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to disconnect from the Redis DB.
|
||||||
|
// Called by unit tests so that node.js thread doesn't live forever.
|
||||||
|
closeRedis () {
|
||||||
|
redisClient.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the redis database. Used by unit tests.
|
||||||
|
async wipeRedis () {
|
||||||
|
await redisClient.flushdb()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a JWT token for testing purposes. This is not used in production.
|
||||||
|
// This function mirrors the kind of JWT token that would be generated by
|
||||||
|
// jwt-bch-api.
|
||||||
|
generateJwtToken (payload) {
|
||||||
|
try {
|
||||||
|
const jwtOptions = {
|
||||||
|
expiresIn: '30 days'
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = _this.jwt.sign(
|
||||||
|
payload,
|
||||||
|
_this.config.apiTokenSecret,
|
||||||
|
jwtOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
return token
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in generateJwtToken()')
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when rate limits are not used.
|
||||||
|
populateLocals (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Create a re*Q*.locals object if not passed in.
|
||||||
|
// req.locals.proLimit will be true if the user is using Basic Authentication.
|
||||||
|
if (!req.locals) {
|
||||||
|
req.locals = {
|
||||||
|
// default values
|
||||||
|
jwtToken: '',
|
||||||
|
proLimit: false,
|
||||||
|
apiLevel: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a re*S*.locals object if it does not exist.
|
||||||
|
if (!res.locals) {
|
||||||
|
res.locals = {
|
||||||
|
rateLimitTriggered: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in populateLocals(): ', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = RateLimits
|
module.exports = RateLimits
|
||||||
|
|||||||
@@ -1,521 +0,0 @@
|
|||||||
/*
|
|
||||||
Blockbook API route
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
// Library for easily switching the API paths to use different instances of
|
|
||||||
// Blockbook.
|
|
||||||
const BlockbookPath = require('../../util/blockbook-path')
|
|
||||||
const BLOCKBOOKPATH = new BlockbookPath()
|
|
||||||
// BLOCKBOOKPATH.toOpenBazaar()
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
// Used for processing error messages before sending them to the user.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Blockbook {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
_this.axios = axios
|
|
||||||
_this.routeUtils = routeUtils
|
|
||||||
_this.bchjs = bchjs
|
|
||||||
_this.BLOCKBOOKPATH = BLOCKBOOKPATH
|
|
||||||
|
|
||||||
_this.router = router
|
|
||||||
_this.router.get('/', _this.root)
|
|
||||||
_this.router.get('/balance/:address', _this.balanceSingle)
|
|
||||||
_this.router.post('/balance', _this.balanceBulk)
|
|
||||||
_this.router.get('/utxos/:address', _this.utxosSingle)
|
|
||||||
_this.router.post('/utxos', _this.utxosBulk)
|
|
||||||
_this.router.get('/tx/:txid', _this.txSingle)
|
|
||||||
_this.router.post('/tx', _this.txBulk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root API endpoint. Simply acknowledges that it exists.
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'address' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API for a balance on a single BCH address.
|
|
||||||
// Returns a Promise.
|
|
||||||
async balanceFromBlockbook (thisAddress) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
// Convert the address to a cashaddr without a prefix.
|
|
||||||
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retData = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
return retData
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/balanceFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async balanceSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'address can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook/balanceSingle with this address: ',
|
|
||||||
address
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
|
|
||||||
_this.bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const retData = await _this.balanceFromBlockbook(address)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/balanceSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on address details
|
|
||||||
async balanceBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let addresses = req.body.addresses
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook.js/balanceBulk with these addresses: ',
|
|
||||||
addresses
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const thisAddress = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
_this.bchjs.Address.toLegacyAddress(thisAddress)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
addresses = addresses.map(async (address, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.balanceFromBlockbook(address)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(addresses)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/balanceBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook API for utxos associated with a BCH address.
|
|
||||||
// Returns a Promise.
|
|
||||||
async utxosFromBlockbook (thisAddress) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
// Convert the address to a cashaddr without a prefix.
|
|
||||||
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook API.
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retData = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
// Add the satoshis property to each UTXO.
|
|
||||||
for (let i = 0; i < retData.length; i++) {
|
|
||||||
retData[i].satoshis = Number(retData[i].value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return retData
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/utxosFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler for single balance
|
|
||||||
async utxosSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'address can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook/utxosSingle with this address: ',
|
|
||||||
address
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
|
|
||||||
_this.bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook API.
|
|
||||||
const retData = await _this.utxosFromBlockbook(address)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/utxosSingle().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on address utxos
|
|
||||||
async utxosBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let addresses = req.body.addresses
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook.js/utxosBulk with these addresses: ',
|
|
||||||
addresses
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const thisAddress = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
_this.bchjs.Address.toLegacyAddress(thisAddress)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
addresses = addresses.map(async (address, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.utxosFromBlockbook(address)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(addresses)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/utxosBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API for transactions on a single TXID.
|
|
||||||
// Returns a Promise.
|
|
||||||
async transactionsFromBlockbook (txid) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.txPath}${txid}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retPromise = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
return retPromise
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler for single transaction details.
|
|
||||||
async txSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const txid = req.params.txid
|
|
||||||
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(txid)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txid can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add regex comparison of txid to ensure it's valid.
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `txid must be of length 64 (not ${txid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing blockbook/txSingle with this txid: ', txid)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const retData = await _this.transactionsFromBlockbook(txid)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/txSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on tx details
|
|
||||||
async txBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let txids = req.body.txids
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if txids is not an array.
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txids need to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids)
|
|
||||||
|
|
||||||
// Validate each element in the txids array.
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const thisTxid = txids[i]
|
|
||||||
|
|
||||||
if (!thisTxid || thisTxid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add regex comparison of txid to ensure it's valid.
|
|
||||||
if (thisTxid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `txid must be of length 64 (not ${thisTxid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
txids = txids.map(async (txid, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.transactionsFromBlockbook(txid)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(txids)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/txBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Blockbook
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
A private library of utility functions used by several different routes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateNetwork, // Prevents a common user error
|
|
||||||
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
|
|
||||||
decodeError, // Extract and interpret error messages.
|
|
||||||
validateArraySize, // Ensure the passed array meets rate limiting requirements.
|
|
||||||
getAxiosOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function expects the Request Express.js object and an array as input.
|
|
||||||
// The array is then validated against freemium and pro-tier rate limiting
|
|
||||||
// requirements. A boolean is returned to indicate if the array size if valid
|
|
||||||
// or not.
|
|
||||||
function validateArraySize (req, array) {
|
|
||||||
const FREEMIUM_INPUT_SIZE = 20
|
|
||||||
const PRO_INPUT_SIZE = 20
|
|
||||||
|
|
||||||
if (req.locals && req.locals.proLimit) {
|
|
||||||
if (array.length <= PRO_INPUT_SIZE) return true
|
|
||||||
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if user-provided cash address matches the correct network,
|
|
||||||
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
|
|
||||||
// This prevent a common user-error issue that is easy to make: passing a
|
|
||||||
// testnet address into rest.bitcoin.com or passing a mainnet address into
|
|
||||||
// trest.bitcoin.com.
|
|
||||||
function validateNetwork (addr) {
|
|
||||||
try {
|
|
||||||
const network = process.env.NETWORK
|
|
||||||
|
|
||||||
// Return false if NETWORK is not defined.
|
|
||||||
if (!network || network === '') {
|
|
||||||
console.log('Warning: NETWORK environment variable is not defined!')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the user-provided address to a cashaddress, for easy detection
|
|
||||||
// of the intended network.
|
|
||||||
const cashAddr = bchjs.Address.toCashAddress(addr)
|
|
||||||
|
|
||||||
// Return true if the network and address both match testnet
|
|
||||||
const addrIsTest = bchjs.Address.isTestnetAddress(cashAddr)
|
|
||||||
if (network === 'testnet' && addrIsTest) return true
|
|
||||||
|
|
||||||
// Return true if the network and address both match mainnet
|
|
||||||
const addrIsMain = bchjs.Address.isMainnetAddress(cashAddr)
|
|
||||||
if (network === 'mainnet' && addrIsMain) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in validateNetwork()')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamically set these based on env vars. Allows unit testing.
|
|
||||||
function setEnvVars () {
|
|
||||||
const BitboxHTTP = axios.create({
|
|
||||||
baseURL: process.env.RPC_BASEURL,
|
|
||||||
timeout: 15000
|
|
||||||
})
|
|
||||||
const username = process.env.RPC_USERNAME
|
|
||||||
const password = process.env.RPC_PASSWORD
|
|
||||||
|
|
||||||
const requestConfig = {
|
|
||||||
method: 'post',
|
|
||||||
auth: {
|
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
jsonrpc: '1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { BitboxHTTP, username, password, requestConfig }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options used when calling axios.post() to talk with a full node.
|
|
||||||
function getAxiosOptions () {
|
|
||||||
return {
|
|
||||||
method: 'post',
|
|
||||||
baseURL: process.env.RPC_BASEURL,
|
|
||||||
timeout: 15000,
|
|
||||||
auth: {
|
|
||||||
username: process.env.RPC_USERNAME,
|
|
||||||
password: process.env.RPC_PASSWORD
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
jsonrpc: '1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error messages returned by a full node can be burried pretty deep inside the
|
|
||||||
// error object returned by Axios. This function attempts to extract and interpret
|
|
||||||
// error messages.
|
|
||||||
// Returns an object. If successful, obj.msg is a string.
|
|
||||||
// If there is a failure, obj.msg is false.
|
|
||||||
function decodeError (err) {
|
|
||||||
try {
|
|
||||||
// Attempt to extract the full node error message.
|
|
||||||
if (
|
|
||||||
err.response &&
|
|
||||||
err.response.data &&
|
|
||||||
err.response.data.error &&
|
|
||||||
err.response.data.error.message
|
|
||||||
) {
|
|
||||||
return { msg: err.response.data.error.message, status: 400 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to extract the Insight error message
|
|
||||||
if (err.response && err.response.data) {
|
|
||||||
return { msg: err.response.data, status: err.response.status }
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`err.message: ${err.message}`)
|
|
||||||
// console.log(`err: `, err)
|
|
||||||
|
|
||||||
// Attempt to detect a network connection error.
|
|
||||||
if (err.message && err.message.indexOf('ENOTFOUND') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kind of network error
|
|
||||||
if (err.message && err.message.indexOf('ENETUNREACH') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kind of network error
|
|
||||||
if (err.message && err.message.indexOf('EAI_AGAIN') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios timeout (aborted) error, or service is down (connection refused).
|
|
||||||
if (
|
|
||||||
err.code &&
|
|
||||||
(err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { msg: false, status: 500 }
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
|
|
||||||
return { msg: false, status: 500 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,604 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const routeUtils = require('./route-utils')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
const Blockbook = require('./blockbook')
|
|
||||||
const blockbook = new Blockbook()
|
|
||||||
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v3/'
|
|
||||||
|
|
||||||
// const bchjsHTTP = axios.create({
|
|
||||||
// baseURL: process.env.RPC_BASEURL
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const username = process.env.RPC_USERNAME
|
|
||||||
// const password = process.env.RPC_PASSWORD
|
|
||||||
|
|
||||||
// const requestConfig = {
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: '1.0'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class UtilRoute {
|
|
||||||
constructor () {
|
|
||||||
this.bchjs = bchjs
|
|
||||||
this.blockbook = blockbook
|
|
||||||
|
|
||||||
_this = this
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'util' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /util/validateAddress/{address} Get information about single bitcoin cash address.
|
|
||||||
* @apiName Information about single bitcoin cash address
|
|
||||||
* @apiGroup Util
|
|
||||||
* @apiDescription Returns information about single bitcoin cash address.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async validateAddressSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
BitboxHTTP,
|
|
||||||
// username,
|
|
||||||
// password,
|
|
||||||
requestConfig
|
|
||||||
} = routeUtils.setEnvVars()
|
|
||||||
|
|
||||||
requestConfig.data.id = 'validateaddress'
|
|
||||||
requestConfig.data.method = 'validateaddress'
|
|
||||||
requestConfig.data.params = [address]
|
|
||||||
|
|
||||||
const response = await BitboxHTTP(requestConfig)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Error in util.ts/validateAddressSingle().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /util/validateAddress Get information about bulk bitcoin cash addresses..
|
|
||||||
* @apiName Information about bulk bitcoin cash addresses.
|
|
||||||
* @apiGroup Util
|
|
||||||
* @apiDescription Returns information about bulk bitcoin cash addresses..
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"]}'
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async validateAddressBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const addresses = req.body.addresses
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each element in the array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const address = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing util/validate with these addresses: ', addresses)
|
|
||||||
|
|
||||||
const {
|
|
||||||
BitboxHTTP,
|
|
||||||
// username,
|
|
||||||
// password,
|
|
||||||
requestConfig
|
|
||||||
} = routeUtils.setEnvVars()
|
|
||||||
|
|
||||||
// Loop through each address and creates an array of requests to call in parallel
|
|
||||||
const promises = addresses.map(async address => {
|
|
||||||
requestConfig.data.id = 'validateaddress'
|
|
||||||
requestConfig.data.method = 'validateaddress'
|
|
||||||
requestConfig.data.params = [address]
|
|
||||||
|
|
||||||
return BitboxHTTP(requestConfig)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const axiosResult = await axios.all(promises)
|
|
||||||
|
|
||||||
// Retrieve the data part of the result.
|
|
||||||
const result = axiosResult.map(x => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Error in util.ts/validateAddressSingle().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /util/sweep Sweep BCH and tokens
|
|
||||||
* @apiName Sweep BCH and tokens from a paper wallet
|
|
||||||
* @apiGroup Util
|
|
||||||
* @apiDescription This function can be used to check the BCH balance of a
|
|
||||||
* paper wallet. It can also be used to sweep BCH and tokens from a paper
|
|
||||||
* wallet and send them to a destination address.
|
|
||||||
*
|
|
||||||
* Note: It does not yet support multiple token classes on the same paper wallet.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "balanceOnly": true}'
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "toAddr": "bitcoincash:qpt8m4kqu963geedyrur6pdggqmv5kxwnq0rn322qu"}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
async sweepWif (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input
|
|
||||||
const wif = req.body.wif
|
|
||||||
const toAddr = req.body.toAddr
|
|
||||||
const balanceOnly = req.body.balanceOnly
|
|
||||||
|
|
||||||
if (typeof wif !== 'string' || wif.length !== 52) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'WIF needs to a proper compressed WIF starting with K or L'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!balanceOnly) {
|
|
||||||
// Only throw error if balanceOnly is false or undefined.
|
|
||||||
if (!toAddr || toAddr === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing util/sweepWif with this address: ', toAddr)
|
|
||||||
|
|
||||||
// Generate a private and public key pair from the WIF.
|
|
||||||
const ecPair = bchjs.ECPair.fromWIF(wif)
|
|
||||||
const fromAddr = bchjs.ECPair.toCashAddress(ecPair)
|
|
||||||
|
|
||||||
// Get a balance on the public address
|
|
||||||
const balances = await _this.blockbook.balanceFromBlockbook(fromAddr)
|
|
||||||
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
|
||||||
|
|
||||||
// Total balance is the sum of the confirmed and unconfirmed balance.
|
|
||||||
const totalBalance =
|
|
||||||
Number(balances.balance) + Number(balances.unconfirmedBalance)
|
|
||||||
|
|
||||||
// Exit if balance is zero.
|
|
||||||
if (isNaN(totalBalance) || totalBalance === 0) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({ error: 'No balance found at BCH address.' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit if this is a balance-only call.
|
|
||||||
if (balanceOnly) {
|
|
||||||
res.status(200)
|
|
||||||
return res.json(totalBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all UTXOs help by the address.
|
|
||||||
const utxos = await _this.blockbook.utxosFromBlockbook(fromAddr)
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
const tokenUtxos = []
|
|
||||||
const bchUtxos = []
|
|
||||||
|
|
||||||
// Exit if there are no UTXOs.
|
|
||||||
if (utxos.length === 0) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({ error: 'No utxos found.' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out which UTXOs are associated with SLP tokens.
|
|
||||||
const isTokenUtxo = await _this.bchjs.SLP.Utils.tokenUtxoDetails(utxos)
|
|
||||||
// console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`)
|
|
||||||
|
|
||||||
// Separate the bch and token UTXOs.
|
|
||||||
for (let i = 0; i < utxos.length; i++) {
|
|
||||||
// Filter based on isTokenUtxo.
|
|
||||||
if (!isTokenUtxo[i]) bchUtxos.push(utxos[i])
|
|
||||||
else tokenUtxos.push(isTokenUtxo[i])
|
|
||||||
}
|
|
||||||
// console.log(
|
|
||||||
// `bchUtxos.length: ${bchUtxos.length}, tokenUtxos.length: ${tokenUtxos.length}`
|
|
||||||
// )
|
|
||||||
|
|
||||||
// Throw error if no BCH to move tokens.
|
|
||||||
if (bchUtxos.length === 0 && tokenUtxos.length > 0) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
ecPair,
|
|
||||||
utxos,
|
|
||||||
fromAddr,
|
|
||||||
toAddr,
|
|
||||||
bchUtxos,
|
|
||||||
tokenUtxos
|
|
||||||
}
|
|
||||||
|
|
||||||
let hex
|
|
||||||
|
|
||||||
// Choose the sweeping algorithm, based on if there are tokens or not.
|
|
||||||
if (tokenUtxos.length === 0) hex = await _this._sweepBCH(options)
|
|
||||||
else hex = await _this._sweepTokens(options, bchUtxos, tokenUtxos)
|
|
||||||
// console.log(`hex: ${hex}`)
|
|
||||||
|
|
||||||
// Throw error if there is more than one token class.
|
|
||||||
|
|
||||||
// Generate a transaction to move tokens and BCH.
|
|
||||||
|
|
||||||
// Broadcast the transaction.
|
|
||||||
const txid = _this.bchjs.RawTransactions.sendRawTransaction([hex])
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(txid)
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch the specific case of multiple tokens.
|
|
||||||
if (
|
|
||||||
err.message &&
|
|
||||||
err.message.indexOf('Multiple token classes detected') > -1
|
|
||||||
) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({ error: err.message })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Error in util.js/sweepWif().', err)
|
|
||||||
console.error('Error in util.js/sweepWif().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: err.message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sweep BCH only from a private WIF.
|
|
||||||
async _sweepBCH (options) {
|
|
||||||
try {
|
|
||||||
// const wif = flags.wif
|
|
||||||
// const toAddr = flags.address
|
|
||||||
|
|
||||||
const ecPair = options.ecPair
|
|
||||||
const toAddr = options.toAddr
|
|
||||||
|
|
||||||
// const fromAddr = this.BITBOX.ECPair.toCashAddress(ecPair)
|
|
||||||
//
|
|
||||||
// // Get the UTXOs for that address.
|
|
||||||
// let utxos = await this.BITBOX.Blockbook.utxo(fromAddr)
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
let utxos = options.utxos
|
|
||||||
|
|
||||||
// Ensure all utxos have the satoshis property.
|
|
||||||
utxos = utxos.map(x => {
|
|
||||||
x.satoshis = Number(x.value)
|
|
||||||
return x
|
|
||||||
})
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
// instance of transaction builder
|
|
||||||
let transactionBuilder
|
|
||||||
if (options.testnet) {
|
|
||||||
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
|
|
||||||
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
|
|
||||||
|
|
||||||
let originalAmount = 0
|
|
||||||
|
|
||||||
// Loop through all UTXOs.
|
|
||||||
for (let i = 0; i < utxos.length; i++) {
|
|
||||||
const utxo = utxos[i]
|
|
||||||
|
|
||||||
originalAmount = originalAmount + utxo.satoshis
|
|
||||||
|
|
||||||
transactionBuilder.addInput(utxo.txid, utxo.vout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalAmount < 546) {
|
|
||||||
throw new Error(
|
|
||||||
'Original amount less than the dust limit. Not enough BCH to send.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get byte count to calculate fee. paying 1 sat/byte
|
|
||||||
const byteCount = _this.bchjs.BitcoinCash.getByteCount(
|
|
||||||
{ P2PKH: utxos.length },
|
|
||||||
{ P2PKH: 1 }
|
|
||||||
)
|
|
||||||
const fee = Math.ceil(1.1 * byteCount)
|
|
||||||
|
|
||||||
// amount to send to receiver. It's the original amount - 1 sat/byte for tx size
|
|
||||||
const sendAmount = originalAmount - fee
|
|
||||||
|
|
||||||
// add output w/ address and amount to send
|
|
||||||
transactionBuilder.addOutput(
|
|
||||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
|
||||||
sendAmount
|
|
||||||
)
|
|
||||||
|
|
||||||
// Loop through each input and sign
|
|
||||||
let redeemScript
|
|
||||||
for (var i = 0; i < utxos.length; i++) {
|
|
||||||
const utxo = utxos[i]
|
|
||||||
|
|
||||||
transactionBuilder.sign(
|
|
||||||
i,
|
|
||||||
ecPair,
|
|
||||||
redeemScript,
|
|
||||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
|
||||||
utxo.satoshis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build tx
|
|
||||||
const tx = transactionBuilder.build()
|
|
||||||
|
|
||||||
// output rawhex
|
|
||||||
const hex = tx.toHex()
|
|
||||||
return hex
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in util.js/sweepBCH().')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sweep BCH and tokens from a WIF.
|
|
||||||
async _sweepTokens (options) {
|
|
||||||
try {
|
|
||||||
// const { ecPair, utxos, fromAddr, toAddr, bchUtxos, tokenUtxos } = options
|
|
||||||
const { ecPair, utxos, toAddr, bchUtxos, tokenUtxos } = options
|
|
||||||
|
|
||||||
// Input validation
|
|
||||||
if (!Array.isArray(bchUtxos) || bchUtxos.length === 0) {
|
|
||||||
throw new Error('bchUtxos need to be an array with one UTXO.')
|
|
||||||
}
|
|
||||||
if (!Array.isArray(tokenUtxos) || tokenUtxos.length === 0) {
|
|
||||||
throw new Error('tokenUtxos need to be an array with one UTXO.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (flags.testnet)
|
|
||||||
// this.BITBOX = new config.BCHLIB({ restURL: config.TESTNET_REST })
|
|
||||||
|
|
||||||
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
|
|
||||||
|
|
||||||
// Ensure there is only one class of token in the wallet. Throw an error if
|
|
||||||
// there is more than one.
|
|
||||||
const tokenId = tokenUtxos[0].tokenId
|
|
||||||
const otherTokens = tokenUtxos.filter(x => x.tokenId !== tokenId)
|
|
||||||
if (otherTokens.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
'Multiple token classes detected. This function only supports a single class of token.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// instance of transaction builder
|
|
||||||
let transactionBuilder
|
|
||||||
if (options.testnet) {
|
|
||||||
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
|
|
||||||
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
|
|
||||||
|
|
||||||
// Combine all the UTXOs into a single array.
|
|
||||||
const allUtxos = utxos
|
|
||||||
// console.log(`allUtxos: ${JSON.stringify(allUtxos, null, 2)}`)
|
|
||||||
|
|
||||||
// Loop through all UTXOs.
|
|
||||||
let originalAmount = 0
|
|
||||||
for (let i = 0; i < allUtxos.length; i++) {
|
|
||||||
const utxo = allUtxos[i]
|
|
||||||
|
|
||||||
originalAmount = originalAmount + utxo.satoshis
|
|
||||||
|
|
||||||
transactionBuilder.addInput(utxo.txid, utxo.vout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalAmount < 300) {
|
|
||||||
throw new Error(
|
|
||||||
'Not enough BCH to send. Send more BCH to the wallet to pay miner fees.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get byte count to calculate fee. paying 1 sat
|
|
||||||
// Note: This may not be totally accurate. Just guessing on the byteCount size.
|
|
||||||
// const byteCount = this.BITBOX.BitcoinCash.getByteCount(
|
|
||||||
// { P2PKH: 3 },
|
|
||||||
// { P2PKH: 5 }
|
|
||||||
// )
|
|
||||||
// //console.log(`byteCount: ${byteCount}`)
|
|
||||||
// const satoshisPerByte = 1.1
|
|
||||||
// const txFee = Math.floor(satoshisPerByte * byteCount)
|
|
||||||
// console.log(`txFee: ${txFee} satoshis\n`)
|
|
||||||
const txFee = 500
|
|
||||||
|
|
||||||
// amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size
|
|
||||||
const remainder = originalAmount - txFee - 546
|
|
||||||
if (remainder < 1) {
|
|
||||||
throw new Error('Selected UTXO does not have enough satoshis')
|
|
||||||
}
|
|
||||||
// console.log(`remainder: ${remainder}`)
|
|
||||||
|
|
||||||
// Tally up the quantity of tokens
|
|
||||||
let tokenQty = 0
|
|
||||||
for (let i = 0; i < tokenUtxos.length; i++) {
|
|
||||||
tokenQty += tokenUtxos[i].tokenQty
|
|
||||||
}
|
|
||||||
// console.log(`tokenQty: ${tokenQty}`)
|
|
||||||
|
|
||||||
// Generate the OP_RETURN entry for an SLP SEND transaction.
|
|
||||||
// console.log(`Generating op-return.`)
|
|
||||||
const {
|
|
||||||
script,
|
|
||||||
outputs
|
|
||||||
} = _this.bchjs.SLP.TokenType1.generateSendOpReturn(tokenUtxos, tokenQty)
|
|
||||||
// console.log(`token outputs: ${outputs}`)
|
|
||||||
|
|
||||||
// Since we are sweeping all tokens from the WIF, there generateOpReturn()
|
|
||||||
// function should only compute 1 token output. If it returns 2, then there
|
|
||||||
// is something unexpected happening.
|
|
||||||
if (outputs > 1) {
|
|
||||||
throw new Error(
|
|
||||||
'More than one class of token detected. Sweep feature not supported.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add OP_RETURN as first output.
|
|
||||||
const data = _this.bchjs.Script.encode(script)
|
|
||||||
transactionBuilder.addOutput(data, 0)
|
|
||||||
|
|
||||||
// Send dust transaction representing tokens being sent.
|
|
||||||
transactionBuilder.addOutput(
|
|
||||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
|
||||||
546
|
|
||||||
)
|
|
||||||
|
|
||||||
// Last output: send remaining BCH
|
|
||||||
transactionBuilder.addOutput(
|
|
||||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
|
||||||
remainder
|
|
||||||
)
|
|
||||||
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
|
|
||||||
|
|
||||||
// Sign each UTXO being consumed.
|
|
||||||
let redeemScript
|
|
||||||
for (let i = 0; i < allUtxos.length; i++) {
|
|
||||||
const thisUtxo = allUtxos[i]
|
|
||||||
// console.log(`thisUtxo: ${JSON.stringify(thisUtxo, null, 2)}`)
|
|
||||||
|
|
||||||
transactionBuilder.sign(
|
|
||||||
i,
|
|
||||||
ecPair,
|
|
||||||
redeemScript,
|
|
||||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
|
||||||
thisUtxo.satoshis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build tx
|
|
||||||
const tx = transactionBuilder.build()
|
|
||||||
|
|
||||||
// output rawhex
|
|
||||||
const hex = tx.toHex()
|
|
||||||
// console.log(`Transaction raw hex: `)
|
|
||||||
// console.log(hex)
|
|
||||||
|
|
||||||
return hex
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in util.js/sweepBCH().')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const utilRoute = new UtilRoute()
|
|
||||||
|
|
||||||
router.get('/', utilRoute.root)
|
|
||||||
router.get('/validateAddress/:address', utilRoute.validateAddressSingle)
|
|
||||||
router.post('/validateAddress', utilRoute.validateAddressBulk)
|
|
||||||
router.post('/sweep', utilRoute.sweepWif)
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
router,
|
|
||||||
// testableComponents: {
|
|
||||||
// root,
|
|
||||||
// validateAddressSingle,
|
|
||||||
// validateAddressBulk,
|
|
||||||
// sweepWif
|
|
||||||
// }
|
|
||||||
UtilRoute
|
|
||||||
}
|
|
||||||
@@ -1,521 +0,0 @@
|
|||||||
/*
|
|
||||||
Blockbook API route
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
// Library for easily switching the API paths to use different instances of
|
|
||||||
// Blockbook.
|
|
||||||
const BlockbookPath = require('../../util/blockbook-path')
|
|
||||||
const BLOCKBOOKPATH = new BlockbookPath()
|
|
||||||
// BLOCKBOOKPATH.toOpenBazaar()
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
// Used for processing error messages before sending them to the user.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Blockbook {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
_this.axios = axios
|
|
||||||
_this.routeUtils = routeUtils
|
|
||||||
_this.bchjs = bchjs
|
|
||||||
_this.BLOCKBOOKPATH = BLOCKBOOKPATH
|
|
||||||
|
|
||||||
_this.router = router
|
|
||||||
_this.router.get('/', _this.root)
|
|
||||||
_this.router.get('/balance/:address', _this.balanceSingle)
|
|
||||||
_this.router.post('/balance', _this.balanceBulk)
|
|
||||||
_this.router.get('/utxos/:address', _this.utxosSingle)
|
|
||||||
_this.router.post('/utxos', _this.utxosBulk)
|
|
||||||
_this.router.get('/tx/:txid', _this.txSingle)
|
|
||||||
_this.router.post('/tx', _this.txBulk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root API endpoint. Simply acknowledges that it exists.
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'address' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API for a balance on a single BCH address.
|
|
||||||
// Returns a Promise.
|
|
||||||
async balanceFromBlockbook (thisAddress) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
// Convert the address to a cashaddr without a prefix.
|
|
||||||
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.addrPath}${addr}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retData = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
return retData
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/balanceFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async balanceSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'address can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook/balanceSingle with this address: ',
|
|
||||||
address
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
|
|
||||||
_this.bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const retData = await _this.balanceFromBlockbook(address)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/balanceSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on address details
|
|
||||||
async balanceBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let addresses = req.body.addresses
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook.js/balanceBulk with these addresses: ',
|
|
||||||
addresses
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const thisAddress = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
_this.bchjs.Address.toLegacyAddress(thisAddress)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
addresses = addresses.map(async (address, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.balanceFromBlockbook(address)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(addresses)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/balanceBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook API for utxos associated with a BCH address.
|
|
||||||
// Returns a Promise.
|
|
||||||
async utxosFromBlockbook (thisAddress) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
// Convert the address to a cashaddr without a prefix.
|
|
||||||
const addr = _this.bchjs.Address.toCashAddress(thisAddress)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.utxoPath}${addr}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook API.
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retData = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
// Add the satoshis property to each UTXO.
|
|
||||||
for (let i = 0; i < retData.length; i++) {
|
|
||||||
retData[i].satoshis = Number(retData[i].value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return retData
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/utxosFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler for single balance
|
|
||||||
async utxosSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'address can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook/utxosSingle with this address: ',
|
|
||||||
address
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
// const legacyAddr = bchjs.Address.toLegacyAddress(address)
|
|
||||||
_this.bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook API.
|
|
||||||
const retData = await _this.utxosFromBlockbook(address)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/utxosSingle().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on address utxos
|
|
||||||
async utxosBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let addresses = req.body.addresses
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockbook.js/utxosBulk with these addresses: ',
|
|
||||||
addresses
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const thisAddress = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
_this.bchjs.Address.toLegacyAddress(thisAddress)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${thisAddress}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(thisAddress)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid network for address ${thisAddress}. Trying to use a testnet address on mainnet, or vice versa.`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
addresses = addresses.map(async (address, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.utxosFromBlockbook(address)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(addresses)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/utxosBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query the Blockbook Node API for transactions on a single TXID.
|
|
||||||
// Returns a Promise.
|
|
||||||
async transactionsFromBlockbook (txid) {
|
|
||||||
try {
|
|
||||||
// console.log(`BLOCKBOOK_URL: ${BLOCKBOOK_URL}`)
|
|
||||||
|
|
||||||
const path = `${_this.BLOCKBOOKPATH.txPath}${txid}`
|
|
||||||
// console.log(`path: ${path}`)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const options = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: path
|
|
||||||
}
|
|
||||||
const axiosResponse = await _this.axios.request(options)
|
|
||||||
const retPromise = axiosResponse.data
|
|
||||||
// console.log(`retData: ${util.inspect(retData)}`)
|
|
||||||
|
|
||||||
return retPromise
|
|
||||||
} catch (err) {
|
|
||||||
// Dev Note: Do not log error messages here. Throw them instead and let the
|
|
||||||
// parent function handle it.
|
|
||||||
wlogger.debug('Error in blockbook.js/transactionsFromBlockbook()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET handler for single transaction details.
|
|
||||||
async txSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const txid = req.params.txid
|
|
||||||
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(txid)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txid can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add regex comparison of txid to ensure it's valid.
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `txid must be of length 64 (not ${txid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing blockbook/txSingle with this txid: ', txid)
|
|
||||||
|
|
||||||
// Query the Blockbook Node API.
|
|
||||||
const retData = await _this.transactionsFromBlockbook(txid)
|
|
||||||
|
|
||||||
// Return the retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(retData)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockbook.js/txSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST handler for bulk queries on tx details
|
|
||||||
async txBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let txids = req.body.txids
|
|
||||||
// const currentPage = req.body.page ? parseInt(req.body.page, 10) : 0
|
|
||||||
|
|
||||||
// Reject if txids is not an array.
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txids need to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing blockbook.js/txBulk with these txids: ', txids)
|
|
||||||
|
|
||||||
// Validate each element in the txids array.
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const thisTxid = txids[i]
|
|
||||||
|
|
||||||
if (!thisTxid || thisTxid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add regex comparison of txid to ensure it's valid.
|
|
||||||
if (thisTxid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `txid must be of length 64 (not ${thisTxid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loops through each address and creates an array of Promises, querying
|
|
||||||
// Insight API in parallel.
|
|
||||||
txids = txids.map(async (txid, index) =>
|
|
||||||
// console.log(`address: ${address}`)
|
|
||||||
_this.transactionsFromBlockbook(txid)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const result = await _this.axios.all(txids)
|
|
||||||
|
|
||||||
// Return the array of retrieved address information.
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in blockbook.js/txBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Blockbook
|
|
||||||
@@ -284,7 +284,7 @@ class Electrum {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -470,7 +470,7 @@ class Electrum {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
@@ -726,7 +726,7 @@ class Electrum {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, heights)) {
|
if (!_this.routeUtils.validateArraySize(req, heights)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
@@ -895,7 +895,7 @@ class Electrum {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -1088,7 +1088,7 @@ class Electrum {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -1281,7 +1281,7 @@ class Electrum {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -114,16 +114,17 @@ class Encryption {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(
|
||||||
wlogger.debug(
|
wlogger.debug(
|
||||||
'Executing encryption/getPublicKey with this address: ',
|
'Executing encryption/getPublicKey with this address: ',
|
||||||
cashAddr
|
cashAddr
|
||||||
)
|
)
|
||||||
|
|
||||||
const rawTxData = await _this.bchjs.Electrumx.transactions(cashAddr, usrObj)
|
const rawTxData = await _this.bchjs.Electrumx.transactions([cashAddr], usrObj)
|
||||||
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
|
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
|
||||||
|
|
||||||
// Extract just the TXIDs
|
// Extract just the TXIDs
|
||||||
const txids = rawTxData.transactions.map((elem) => elem.tx_hash)
|
const txids = rawTxData.transactions[0].transactions.map((elem) => elem.tx_hash)
|
||||||
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
|
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
|
||||||
|
|
||||||
// throw error if there is no transaction history.
|
// throw error if there is no transaction history.
|
||||||
@@ -135,16 +136,14 @@ class Encryption {
|
|||||||
for (let i = 0; i < txids.length; i++) {
|
for (let i = 0; i < txids.length; i++) {
|
||||||
const thisTx = txids[i]
|
const thisTx = txids[i]
|
||||||
|
|
||||||
// CT 2/24/21: I might want to convert this to the POST call, to take
|
|
||||||
// advantage of the usrObj. It does not get passed in a GET call.
|
|
||||||
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
|
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
|
||||||
thisTx,
|
[thisTx],
|
||||||
true,
|
true,
|
||||||
usrObj
|
usrObj
|
||||||
)
|
)
|
||||||
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
|
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
|
||||||
|
|
||||||
const vin = txDetails.vin
|
const vin = txDetails[0].vin
|
||||||
|
|
||||||
// Loop through each input.
|
// Loop through each input.
|
||||||
for (let j = 0; j < vin.length; j++) {
|
for (let j = 0; j < vin.length; j++) {
|
||||||
@@ -182,6 +181,7 @@ class Encryption {
|
|||||||
publicKey: 'not found'
|
publicKey: 'not found'
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// console.log('Error in encryption.js/getPublicKey().', err)
|
||||||
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class Blockchain {
|
|||||||
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
||||||
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
||||||
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
||||||
|
this.router.post('/getBlock', this.getBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
root (req, res, next) {
|
root (req, res, next) {
|
||||||
@@ -284,7 +285,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, hashes)) {
|
if (!routeUtils.validateArraySize(req, hashes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -457,7 +458,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
if (!routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -794,7 +795,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
if (!routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -888,7 +889,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, proofs)) {
|
if (!routeUtils.validateArraySize(req, proofs)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -937,6 +938,52 @@ class Blockchain {
|
|||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /blockchain/getBlock/ Get block details
|
||||||
|
* @apiName getBlock
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns block details
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl "https://api.fullstack.cash/v4/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }'
|
||||||
|
*
|
||||||
|
* @apiParam {String} blockhash Block hash (required)
|
||||||
|
* @apiParam {Number} verbosity Default 1 (optional)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getBlock (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Validate input parameter
|
||||||
|
const blockhash = req.body.blockhash
|
||||||
|
let verbosity = req.body.verbosity
|
||||||
|
|
||||||
|
// Default to a value of 1 if another verbosity level is not defined.
|
||||||
|
if (!verbosity && verbosity !== 0) verbosity = 1
|
||||||
|
|
||||||
|
if (!blockhash || blockhash === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ error: 'blockhash can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axios options
|
||||||
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
|
options.data.id = 'getblock'
|
||||||
|
options.data.method = 'getblock'
|
||||||
|
options.data.params = [blockhash, verbosity]
|
||||||
|
|
||||||
|
const response = await _this.axios.request(options)
|
||||||
|
|
||||||
|
return res.json(response.data.result)
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
||||||
|
wlogger.error('Error in blockchain.js/getBlock()', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Blockchain
|
module.exports = Blockchain
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -140,7 +140,7 @@ class RawTransactions {
|
|||||||
const options = _this.routeUtils.getAxiosOptions()
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
// Loop through each height and creates an array of requests to call in parallel
|
// 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.id = 'decoderawtransaction'
|
||||||
options.data.method = 'decoderawtransaction'
|
options.data.method = 'decoderawtransaction'
|
||||||
options.data.params = [hex]
|
options.data.params = [hex]
|
||||||
@@ -152,7 +152,7 @@ class RawTransactions {
|
|||||||
const axiosResult = await _this.axios.all(promises)
|
const axiosResult = await _this.axios.all(promises)
|
||||||
|
|
||||||
// Retrieve the data part of the result.
|
// 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)
|
res.status(200)
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
@@ -234,7 +234,7 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -254,7 +254,7 @@ class RawTransactions {
|
|||||||
const options = _this.routeUtils.getAxiosOptions()
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
// Loop through each hex and create an array of promises
|
// 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.id = 'decodescript'
|
||||||
options.data.method = 'decodescript'
|
options.data.method = 'decodescript'
|
||||||
options.data.params = [hex]
|
options.data.params = [hex]
|
||||||
@@ -267,7 +267,7 @@ class RawTransactions {
|
|||||||
const resolved = await Promise.all(promises)
|
const resolved = await Promise.all(promises)
|
||||||
|
|
||||||
// Retrieve the data from each resolved promise.
|
// 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)
|
res.status(200)
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
@@ -324,20 +324,12 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// stub response object
|
|
||||||
// const returnResponse = {
|
|
||||||
// status: 100,
|
|
||||||
// json: {
|
|
||||||
// error: ''
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Validate each txid in the array.
|
// Validate each txid in the array.
|
||||||
for (let i = 0; i < txids.length; i++) {
|
for (let i = 0; i < txids.length; i++) {
|
||||||
const txid = txids[i]
|
const txid = txids[i]
|
||||||
@@ -356,7 +348,7 @@ class RawTransactions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loop through each txid and create an array of promises
|
// 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)
|
_this.getRawTransactionsFromNode(txid, verbose)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -444,11 +436,12 @@ class RawTransactions {
|
|||||||
return res.json({ error: 'hex must be an array' })
|
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
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -544,7 +537,8 @@ class RawTransactions {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
let options = _this.routeUtils.getAxiosOptions()
|
||||||
|
options = _this.sendTxOptions(options)
|
||||||
|
|
||||||
// RPC call
|
// RPC call
|
||||||
options.data.id = 'sendrawtransaction'
|
options.data.id = 'sendrawtransaction'
|
||||||
@@ -565,6 +559,31 @@ class RawTransactions {
|
|||||||
return _this.errorHandler(err, res)
|
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
|
module.exports = RawTransactions
|
||||||
|
|||||||
+39
-1
@@ -25,13 +25,17 @@ class Price {
|
|||||||
|
|
||||||
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
|
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
|
||||||
this.coinexPriceUrl =
|
this.coinexPriceUrl =
|
||||||
'https://api.coinex.com/v1/market/ticker?market=bchausdt'
|
'https://api.coinex.com/v1/market/ticker?market=xecusdt'
|
||||||
|
|
||||||
|
this.bchCoinexPriceUrl =
|
||||||
|
'https://api.coinex.com/v1/market/ticker?market=bchusdt'
|
||||||
|
|
||||||
this.router = express.Router()
|
this.router = express.Router()
|
||||||
this.router.get('/', _this.root)
|
this.router.get('/', _this.root)
|
||||||
this.router.get('/usd', _this.getUSD)
|
this.router.get('/usd', _this.getUSD)
|
||||||
this.router.get('/rates', _this.getBCHRate)
|
this.router.get('/rates', _this.getBCHRate)
|
||||||
this.router.get('/bchausd', _this.getBCHAUSD)
|
this.router.get('/bchausd', _this.getBCHAUSD)
|
||||||
|
this.router.get('/bchusd', _this.getBCHUSD)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DRY error handler.
|
// DRY error handler.
|
||||||
@@ -150,6 +154,40 @@ class Price {
|
|||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /price/bchusd Get the USD price of BCH
|
||||||
|
* @apiName Get the USD price of BCH
|
||||||
|
* @apiGroup Price
|
||||||
|
* @apiDescription Get the USD price of BCH from Coinex.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v4/price/bchusd" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getBCHUSD (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Request options
|
||||||
|
const opt = {
|
||||||
|
method: 'get',
|
||||||
|
baseURL: _this.bchCoinexPriceUrl,
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.request(opt)
|
||||||
|
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
||||||
|
|
||||||
|
const price = Number(response.data.data.ticker.last)
|
||||||
|
|
||||||
|
return res.json({ usd: price })
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
wlogger.error('Error in price.js/getBCHUSD().', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Price
|
module.exports = Price
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
/*
|
|
||||||
A private library of utility functions used by several different routes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateNetwork, // Prevents a common user error
|
|
||||||
setEnvVars, // Allows RPC variables to be set dynamically based on changing env vars.
|
|
||||||
decodeError, // Extract and interpret error messages.
|
|
||||||
validateArraySize, // Ensure the passed array meets rate limiting requirements.
|
|
||||||
getAxiosOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function expects the Request Express.js object and an array as input.
|
|
||||||
// The array is then validated against freemium and pro-tier rate limiting
|
|
||||||
// requirements. A boolean is returned to indicate if the array size if valid
|
|
||||||
// or not.
|
|
||||||
function validateArraySize (req, array) {
|
|
||||||
const FREEMIUM_INPUT_SIZE = 20
|
|
||||||
const PRO_INPUT_SIZE = 20
|
|
||||||
|
|
||||||
if (req.locals && req.locals.proLimit) {
|
|
||||||
if (array.length <= PRO_INPUT_SIZE) return true
|
|
||||||
} else if (array.length <= FREEMIUM_INPUT_SIZE) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if user-provided cash address matches the correct network,
|
|
||||||
// mainnet or testnet. If NETWORK env var is not defined, it returns false.
|
|
||||||
// This prevent a common user-error issue that is easy to make: passing a
|
|
||||||
// testnet address into rest.bitcoin.com or passing a mainnet address into
|
|
||||||
// trest.bitcoin.com.
|
|
||||||
function validateNetwork (addr) {
|
|
||||||
try {
|
|
||||||
const network = process.env.NETWORK
|
|
||||||
|
|
||||||
// Return false if NETWORK is not defined.
|
|
||||||
if (!network || network === '') {
|
|
||||||
console.log('Warning: NETWORK environment variable is not defined!')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the user-provided address to a cashaddress, for easy detection
|
|
||||||
// of the intended network.
|
|
||||||
const cashAddr = bchjs.Address.toCashAddress(addr)
|
|
||||||
|
|
||||||
// Return true if the network and address both match testnet
|
|
||||||
const addrIsTest = bchjs.Address.isTestnetAddress(cashAddr)
|
|
||||||
if (network === 'testnet' && addrIsTest) return true
|
|
||||||
|
|
||||||
// Return true if the network and address both match mainnet
|
|
||||||
const addrIsMain = bchjs.Address.isMainnetAddress(cashAddr)
|
|
||||||
if (network === 'mainnet' && addrIsMain) return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in validateNetwork()')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamically set these based on env vars. Allows unit testing.
|
|
||||||
function setEnvVars () {
|
|
||||||
const BitboxHTTP = axios.create({
|
|
||||||
baseURL: process.env.RPC_BASEURL,
|
|
||||||
timeout: 15000
|
|
||||||
})
|
|
||||||
const username = process.env.RPC_USERNAME
|
|
||||||
const password = process.env.RPC_PASSWORD
|
|
||||||
|
|
||||||
const requestConfig = {
|
|
||||||
method: 'post',
|
|
||||||
auth: {
|
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
jsonrpc: '1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { BitboxHTTP, username, password, requestConfig }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options used when calling axios.post() to talk with a full node.
|
|
||||||
function getAxiosOptions () {
|
|
||||||
return {
|
|
||||||
method: 'post',
|
|
||||||
baseURL: process.env.RPC_BASEURL,
|
|
||||||
timeout: 15000,
|
|
||||||
auth: {
|
|
||||||
username: process.env.RPC_USERNAME,
|
|
||||||
password: process.env.RPC_PASSWORD
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
jsonrpc: '1.0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error messages returned by a full node can be burried pretty deep inside the
|
|
||||||
// error object returned by Axios. This function attempts to extract and interpret
|
|
||||||
// error messages.
|
|
||||||
// Returns an object. If successful, obj.msg is a string.
|
|
||||||
// If there is a failure, obj.msg is false.
|
|
||||||
function decodeError (err) {
|
|
||||||
try {
|
|
||||||
// Attempt to extract the full node error message.
|
|
||||||
if (
|
|
||||||
err.response &&
|
|
||||||
err.response.data &&
|
|
||||||
err.response.data.error &&
|
|
||||||
err.response.data.error.message
|
|
||||||
) {
|
|
||||||
return { msg: err.response.data.error.message, status: 400 }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to extract the Insight error message
|
|
||||||
if (err.response && err.response.data) {
|
|
||||||
return { msg: err.response.data, status: err.response.status }
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`err.message: ${err.message}`)
|
|
||||||
// console.log(`err: `, err)
|
|
||||||
|
|
||||||
// Attempt to detect a network connection error.
|
|
||||||
if (err.message && err.message.indexOf('ENOTFOUND') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kind of network error
|
|
||||||
if (err.message && err.message.indexOf('ENETUNREACH') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different kind of network error
|
|
||||||
if (err.message && err.message.indexOf('EAI_AGAIN') > -1) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios timeout (aborted) error, or service is down (connection refused).
|
|
||||||
if (
|
|
||||||
err.code &&
|
|
||||||
(err.code === 'ECONNABORTED' || err.code === 'ECONNREFUSED')
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
msg:
|
|
||||||
'Network error: Could not communicate with full node or other external service.',
|
|
||||||
status: 503
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle general Error objects.
|
|
||||||
if (err.message) {
|
|
||||||
return {
|
|
||||||
message: err.message,
|
|
||||||
status: 422
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { msg: false, status: 500 }
|
|
||||||
} catch (err) {
|
|
||||||
console.error('unhandled error in route-utils.js/decodeError(): ', err)
|
|
||||||
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
|
|
||||||
return { msg: false, status: 500 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+196
-18
@@ -36,9 +36,13 @@ util.inspect.defaultOptions = { depth: 5 }
|
|||||||
// Determine the Access password for a private instance of SLPDB.
|
// Determine the Access password for a private instance of SLPDB.
|
||||||
// https://gist.github.com/christroutner/fc717ca704dec3dded8b52fae387eab2
|
// https://gist.github.com/christroutner/fc717ca704dec3dded8b52fae387eab2
|
||||||
// Password for General Purpose (GP) SLPDB.
|
// Password for General Purpose (GP) SLPDB.
|
||||||
const SLPDB_PASS_GP = process.env.SLPDB_PASS_GP ? process.env.SLPDB_PASS_GP : 'BITBOX'
|
const SLPDB_PASS_GP = process.env.SLPDB_PASS_GP
|
||||||
|
? process.env.SLPDB_PASS_GP
|
||||||
|
: 'BITBOX'
|
||||||
// Password for Whitelist (WL) SLPDB.
|
// Password for Whitelist (WL) SLPDB.
|
||||||
const SLPDB_PASS_WL = process.env.SLPDB_PASS_WL ? process.env.SLPDB_PASS_WL : 'BITBOX'
|
const SLPDB_PASS_WL = process.env.SLPDB_PASS_WL
|
||||||
|
? process.env.SLPDB_PASS_WL
|
||||||
|
: 'BITBOX'
|
||||||
|
|
||||||
// const rawtransactions = require('./full-node/rawtransactions')
|
// const rawtransactions = require('./full-node/rawtransactions')
|
||||||
const RawTransactions = require('./full-node/rawtransactions')
|
const RawTransactions = require('./full-node/rawtransactions')
|
||||||
@@ -46,7 +50,9 @@ const rawTransactions = new RawTransactions()
|
|||||||
|
|
||||||
// Setup REST and TREST URLs used by slpjs
|
// Setup REST and TREST URLs used by slpjs
|
||||||
// Dev note: this allows for unit tests to mock the URL.
|
// 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) {
|
if (!process.env.TREST_URL) {
|
||||||
process.env.TREST_URL = 'https://testnet.fullstack.cash/v4/'
|
process.env.TREST_URL = 'https://testnet.fullstack.cash/v4/'
|
||||||
}
|
}
|
||||||
@@ -96,12 +102,19 @@ class Slp {
|
|||||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||||
_this.router.post('/hydrateUtxosWL', _this.hydrateUtxosWL)
|
_this.router.post('/hydrateUtxosWL', _this.hydrateUtxosWL)
|
||||||
_this.router.get('/status', _this.getStatus)
|
_this.router.get('/status', _this.getStatus)
|
||||||
|
_this.router.get('/nftChildren/:tokenId', _this.getNftChildren)
|
||||||
|
_this.router.get('/nftGroup/:tokenId', _this.getNftGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DRY error handler.
|
// DRY error handler.
|
||||||
errorHandler (err, res) {
|
errorHandler (err, res) {
|
||||||
|
// console.error('Entering slp.js/errorHandler(). err: ', err)
|
||||||
|
|
||||||
// Attempt to decode the error message.
|
// Attempt to decode the error message.
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
console.log('slp.js/errorHandler msg from decodeError: ', msg)
|
||||||
|
console.log('slp.js/errorHandler status from decodeError: ', status)
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
res.status(status)
|
res.status(status)
|
||||||
return res.json({ error: msg })
|
return res.json({ error: msg })
|
||||||
@@ -124,6 +137,10 @@ class Slp {
|
|||||||
delete token.tokenDetails.batonVout
|
delete token.tokenDetails.batonVout
|
||||||
delete token.tokenDetails.sendOutputs
|
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.blockCreated = token.tokenStats.block_created
|
||||||
token.tokenDetails.blockLastActiveSend =
|
token.tokenDetails.blockLastActiveSend =
|
||||||
token.tokenStats.block_last_active_send
|
token.tokenStats.block_last_active_send
|
||||||
@@ -215,7 +232,7 @@ class Slp {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, tokenIds)) {
|
if (!_this.routeUtils.validateArraySize(req, tokenIds)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -287,7 +304,7 @@ class Slp {
|
|||||||
'tokenDetails.tokenIdHex': tokenId
|
'tokenDetails.tokenIdHex': tokenId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
|
project: { tokenDetails: 1, tokenStats: 1, nftParentId: 1, _id: 0 },
|
||||||
limit: 1000
|
limit: 1000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,7 +562,7 @@ class Slp {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -914,7 +931,7 @@ class Slp {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
if (!_this.routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -975,7 +992,7 @@ class Slp {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -1403,7 +1420,7 @@ class Slp {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -1771,8 +1788,6 @@ class Slp {
|
|||||||
|
|
||||||
// Format the response from SLPDB into an object.
|
// Format the response from SLPDB into an object.
|
||||||
async formatToRestObject (slpDBFormat) {
|
async formatToRestObject (slpDBFormat) {
|
||||||
_this.BigNumber.set({ DECIMAL_PLACES: 8 })
|
|
||||||
|
|
||||||
// console.log(`slpDBFormat.data: ${JSON.stringify(slpDBFormat.data, null, 2)}`)
|
// console.log(`slpDBFormat.data: ${JSON.stringify(slpDBFormat.data, null, 2)}`)
|
||||||
|
|
||||||
const transaction = slpDBFormat.data.u.length
|
const transaction = slpDBFormat.data.u.length
|
||||||
@@ -1979,6 +1994,13 @@ class Slp {
|
|||||||
try {
|
try {
|
||||||
const utxos = req.body.utxos
|
const utxos = req.body.utxos
|
||||||
|
|
||||||
|
// Extract a delay value if the user passed it in.
|
||||||
|
const usrObjIn = req.body.usrObj
|
||||||
|
let utxoDelay = 0
|
||||||
|
if (usrObjIn && usrObjIn.utxoDelay) {
|
||||||
|
utxoDelay = usrObjIn.utxoDelay
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('req: ', req)
|
// console.log('req: ', req)
|
||||||
// console.log(`req._remoteAddress: ${req._remoteAddress}`)
|
// console.log(`req._remoteAddress: ${req._remoteAddress}`)
|
||||||
|
|
||||||
@@ -1988,7 +2010,8 @@ class Slp {
|
|||||||
ip: req._remoteAddress,
|
ip: req._remoteAddress,
|
||||||
jwtToken: req.locals.jwtToken,
|
jwtToken: req.locals.jwtToken,
|
||||||
proLimit: req.locals.proLimit,
|
proLimit: req.locals.proLimit,
|
||||||
apiLevel: req.locals.apiLevel
|
apiLevel: req.locals.apiLevel,
|
||||||
|
utxoDelay
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
@@ -2025,7 +2048,10 @@ class Slp {
|
|||||||
const theseUtxos = utxos[i].utxos
|
const theseUtxos = utxos[i].utxos
|
||||||
|
|
||||||
// Get SLP token details.
|
// Get SLP token details.
|
||||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(theseUtxos, usrObj)
|
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(
|
||||||
|
theseUtxos,
|
||||||
|
usrObj
|
||||||
|
)
|
||||||
// console.log('details: ', details)
|
// console.log('details: ', details)
|
||||||
|
|
||||||
// Replace the original UTXO data with the hydrated data.
|
// Replace the original UTXO data with the hydrated data.
|
||||||
@@ -2036,12 +2062,13 @@ class Slp {
|
|||||||
return res.json({ slpUtxos: utxos })
|
return res.json({ slpUtxos: utxos })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
wlogger.error('Error in slp.js/hydrateUtxos().', err)
|
||||||
console.error('Error in slp.js/hydrateUtxos().', err)
|
// console.error('Error in slp.js/hydrateUtxos().', err)
|
||||||
|
|
||||||
// Decode the error message.
|
// Decode the error message.
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
const { msg, status } = routeUtils.decodeError(err)
|
||||||
console.log('msg: ', msg)
|
// console.log('msg: ', msg)
|
||||||
console.log('status: ', status)
|
// console.log('status: ', status)
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
res.status(status)
|
res.status(status)
|
||||||
return res.json({ error: msg, message: msg, success: false })
|
return res.json({ error: msg, message: msg, success: false })
|
||||||
@@ -2049,7 +2076,8 @@ class Slp {
|
|||||||
|
|
||||||
res.status(500)
|
res.status(500)
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Error in hydrateUtxos()'
|
error: 'Undetermined error in hydrateUtxos()',
|
||||||
|
message: err.message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2075,6 +2103,23 @@ class Slp {
|
|||||||
try {
|
try {
|
||||||
const utxos = req.body.utxos
|
const utxos = req.body.utxos
|
||||||
|
|
||||||
|
// Extract a delay value if the user passed it in.
|
||||||
|
const usrObjIn = req.body.usrObj
|
||||||
|
let utxoDelay = 0
|
||||||
|
if (usrObjIn && usrObjIn.utxoDelay) {
|
||||||
|
utxoDelay = usrObjIn.utxoDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a user object that can be passed along with internal calls
|
||||||
|
// from bch-js.
|
||||||
|
const usrObj = {
|
||||||
|
ip: req._remoteAddress,
|
||||||
|
jwtToken: req.locals.jwtToken,
|
||||||
|
proLimit: req.locals.proLimit,
|
||||||
|
apiLevel: req.locals.apiLevel,
|
||||||
|
utxoDelay
|
||||||
|
}
|
||||||
|
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if (!Array.isArray(utxos)) {
|
if (!Array.isArray(utxos)) {
|
||||||
res.status(422)
|
res.status(422)
|
||||||
@@ -2110,7 +2155,10 @@ class Slp {
|
|||||||
// console.log(`theseUtxos: ${JSON.stringify(theseUtxos, null, 2)}`)
|
// console.log(`theseUtxos: ${JSON.stringify(theseUtxos, null, 2)}`)
|
||||||
|
|
||||||
// Get SLP token details.
|
// Get SLP token details.
|
||||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetailsWL(theseUtxos)
|
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetailsWL(
|
||||||
|
theseUtxos,
|
||||||
|
usrObj
|
||||||
|
)
|
||||||
// console.log('details : ', details)
|
// console.log('details : ', details)
|
||||||
|
|
||||||
// Replace the original UTXO data with the hydrated data.
|
// Replace the original UTXO data with the hydrated data.
|
||||||
@@ -2182,6 +2230,136 @@ class Slp {
|
|||||||
return _this.errorHandler(err, res)
|
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) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ 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
|
module.exports = Slp
|
||||||
|
|||||||
+39
-37
@@ -4,10 +4,10 @@ const express = require('express')
|
|||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
|
|
||||||
const routeUtils = require('./route-utils')
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
const wlogger = require('../../util/winston-logging')
|
||||||
const Blockbook = require('./blockbook')
|
|
||||||
const blockbook = new Blockbook()
|
|
||||||
|
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
@@ -37,9 +37,28 @@ const bchjs = new BCHJS()
|
|||||||
let _this
|
let _this
|
||||||
|
|
||||||
class UtilRoute {
|
class UtilRoute {
|
||||||
constructor () {
|
constructor (utilConfig) {
|
||||||
this.bchjs = bchjs
|
this.bchjs = bchjs
|
||||||
this.blockbook = blockbook
|
// this.blockbook = blockbook
|
||||||
|
|
||||||
|
if (!utilConfig) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass a config object when instantiating the Util library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!utilConfig.electrumx) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass an instance of Electrumx when instantiating the Util library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.electrumx = utilConfig.electrumx
|
||||||
|
|
||||||
|
this.router = router
|
||||||
|
this.router.get('/', this.root)
|
||||||
|
this.router.get('/validateAddress/:address', this.validateAddressSingle)
|
||||||
|
this.router.post('/validateAddress', this.validateAddressBulk)
|
||||||
|
this.router.post('/sweep', this.sweepWif)
|
||||||
|
|
||||||
_this = this
|
_this = this
|
||||||
}
|
}
|
||||||
@@ -124,7 +143,7 @@ class UtilRoute {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, addresses)) {
|
if (!routeUtils.validateArraySize(req, addresses)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -165,7 +184,7 @@ class UtilRoute {
|
|||||||
} = routeUtils.setEnvVars()
|
} = routeUtils.setEnvVars()
|
||||||
|
|
||||||
// Loop through each address and creates an array of requests to call in parallel
|
// Loop through each address and creates an array of requests to call in parallel
|
||||||
const promises = addresses.map(async address => {
|
const promises = addresses.map(async (address) => {
|
||||||
requestConfig.data.id = 'validateaddress'
|
requestConfig.data.id = 'validateaddress'
|
||||||
requestConfig.data.method = 'validateaddress'
|
requestConfig.data.method = 'validateaddress'
|
||||||
requestConfig.data.params = [address]
|
requestConfig.data.params = [address]
|
||||||
@@ -177,7 +196,7 @@ class UtilRoute {
|
|||||||
const axiosResult = await axios.all(promises)
|
const axiosResult = await axios.all(promises)
|
||||||
|
|
||||||
// Retrieve the data part of the result.
|
// 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)
|
res.status(200)
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
@@ -243,12 +262,11 @@ class UtilRoute {
|
|||||||
const fromAddr = bchjs.ECPair.toCashAddress(ecPair)
|
const fromAddr = bchjs.ECPair.toCashAddress(ecPair)
|
||||||
|
|
||||||
// Get a balance on the public address
|
// Get a balance on the public address
|
||||||
const balances = await _this.blockbook.balanceFromBlockbook(fromAddr)
|
const balances = await _this.electrumx._balanceFromElectrumx(fromAddr)
|
||||||
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
||||||
|
|
||||||
// Total balance is the sum of the confirmed and unconfirmed balance.
|
// Total balance is the sum of the confirmed and unconfirmed balance.
|
||||||
const totalBalance =
|
const totalBalance = balances.confirmed + balances.unconfirmed
|
||||||
Number(balances.balance) + Number(balances.unconfirmedBalance)
|
|
||||||
|
|
||||||
// Exit if balance is zero.
|
// Exit if balance is zero.
|
||||||
if (isNaN(totalBalance) || totalBalance === 0) {
|
if (isNaN(totalBalance) || totalBalance === 0) {
|
||||||
@@ -263,7 +281,7 @@ class UtilRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all UTXOs help by the address.
|
// Get all UTXOs help by the address.
|
||||||
const utxos = await _this.blockbook.utxosFromBlockbook(fromAddr)
|
const utxos = await _this.electrumx._utxosFromElectrumx(fromAddr)
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||||
|
|
||||||
const tokenUtxos = []
|
const tokenUtxos = []
|
||||||
@@ -368,7 +386,7 @@ class UtilRoute {
|
|||||||
let utxos = options.utxos
|
let utxos = options.utxos
|
||||||
|
|
||||||
// Ensure all utxos have the satoshis property.
|
// Ensure all utxos have the satoshis property.
|
||||||
utxos = utxos.map(x => {
|
utxos = utxos.map((x) => {
|
||||||
x.satoshis = Number(x.value)
|
x.satoshis = Number(x.value)
|
||||||
return x
|
return x
|
||||||
})
|
})
|
||||||
@@ -386,9 +404,9 @@ class UtilRoute {
|
|||||||
for (let i = 0; i < utxos.length; i++) {
|
for (let i = 0; i < utxos.length; i++) {
|
||||||
const utxo = utxos[i]
|
const utxo = utxos[i]
|
||||||
|
|
||||||
originalAmount = originalAmount + utxo.satoshis
|
originalAmount = originalAmount + utxo.value
|
||||||
|
|
||||||
transactionBuilder.addInput(utxo.txid, utxo.vout)
|
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originalAmount < 546) {
|
if (originalAmount < 546) {
|
||||||
@@ -423,7 +441,7 @@ class UtilRoute {
|
|||||||
ecPair,
|
ecPair,
|
||||||
redeemScript,
|
redeemScript,
|
||||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
transactionBuilder.hashTypes.SIGHASH_ALL,
|
||||||
utxo.satoshis
|
utxo.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,7 +479,7 @@ class UtilRoute {
|
|||||||
// Ensure there is only one class of token in the wallet. Throw an error if
|
// Ensure there is only one class of token in the wallet. Throw an error if
|
||||||
// there is more than one.
|
// there is more than one.
|
||||||
const tokenId = tokenUtxos[0].tokenId
|
const tokenId = tokenUtxos[0].tokenId
|
||||||
const otherTokens = tokenUtxos.filter(x => x.tokenId !== tokenId)
|
const otherTokens = tokenUtxos.filter((x) => x.tokenId !== tokenId)
|
||||||
if (otherTokens.length > 0) {
|
if (otherTokens.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Multiple token classes detected. This function only supports a single class of token.'
|
'Multiple token classes detected. This function only supports a single class of token.'
|
||||||
@@ -483,9 +501,9 @@ class UtilRoute {
|
|||||||
for (let i = 0; i < allUtxos.length; i++) {
|
for (let i = 0; i < allUtxos.length; i++) {
|
||||||
const utxo = allUtxos[i]
|
const utxo = allUtxos[i]
|
||||||
|
|
||||||
originalAmount = originalAmount + utxo.satoshis
|
originalAmount = originalAmount + utxo.value
|
||||||
|
|
||||||
transactionBuilder.addInput(utxo.txid, utxo.vout)
|
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (originalAmount < 300) {
|
if (originalAmount < 300) {
|
||||||
@@ -565,7 +583,7 @@ class UtilRoute {
|
|||||||
ecPair,
|
ecPair,
|
||||||
redeemScript,
|
redeemScript,
|
||||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
transactionBuilder.hashTypes.SIGHASH_ALL,
|
||||||
thisUtxo.satoshis
|
thisUtxo.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,20 +603,4 @@ class UtilRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const utilRoute = new UtilRoute()
|
module.exports = UtilRoute
|
||||||
|
|
||||||
router.get('/', utilRoute.root)
|
|
||||||
router.get('/validateAddress/:address', utilRoute.validateAddressSingle)
|
|
||||||
router.post('/validateAddress', utilRoute.validateAddressBulk)
|
|
||||||
router.post('/sweep', utilRoute.sweepWif)
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
router,
|
|
||||||
// testableComponents: {
|
|
||||||
// root,
|
|
||||||
// validateAddressSingle,
|
|
||||||
// validateAddressBulk,
|
|
||||||
// sweepWif
|
|
||||||
// }
|
|
||||||
UtilRoute
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
// const axios = require('axios')
|
|
||||||
const routeUtils = require('./route-utils')
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
const wlogger = require('../../util/winston-logging')
|
||||||
|
|
||||||
// const router = express.Router()
|
// const router = express.Router()
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
Electrum API route
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
const axios = require('axios')
|
||||||
|
const util = require('util')
|
||||||
|
// const bitcore = require('bitcore-lib-cash')
|
||||||
|
|
||||||
|
// const ElectrumCash = require('electrum-cash').ElectrumClient
|
||||||
|
// const ElectrumCash = require('/home/trout/work/personal/electrum-cash/electrum.js').Client // eslint-disable-line
|
||||||
|
|
||||||
|
const wlogger = require('../../../util/winston-logging')
|
||||||
|
const config = require('../../../../config')
|
||||||
|
|
||||||
|
const RouteUtils = require('../../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
|
const BCHJS = require('@psf/bch-js')
|
||||||
|
const bchjs = new BCHJS()
|
||||||
|
|
||||||
|
let _this
|
||||||
|
|
||||||
|
class BcashSlp {
|
||||||
|
constructor () {
|
||||||
|
_this = this
|
||||||
|
this.config = config
|
||||||
|
this.axios = axios
|
||||||
|
this.routeUtils = routeUtils
|
||||||
|
this.bchjs = bchjs
|
||||||
|
// _this.bitcore = bitcore
|
||||||
|
|
||||||
|
this.bcashServer = process.env.BCASH_SERVER
|
||||||
|
if (!this.bcashServer) {
|
||||||
|
// console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
|
||||||
|
throw new Error(
|
||||||
|
'BCASH_SERVER env var not set. Can not connect to bcash full node.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router = router
|
||||||
|
this.router.get('/', this.root)
|
||||||
|
this.router.get('/utxos/:address', this.getUtxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /bcash/utxos/{addr} Get utxos for a single address.
|
||||||
|
* @apiName UTXOs for a single address
|
||||||
|
* @apiGroup bcash
|
||||||
|
* @apiDescription Returns an object with UTXOs associated with an address.
|
||||||
|
* This UTXOs will be hydrated with token information.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/bcash/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
// GET handler for single balance
|
||||||
|
async getUtxos (req, res, next) {
|
||||||
|
try {
|
||||||
|
const address = req.params.address
|
||||||
|
|
||||||
|
// Reject if address is an array.
|
||||||
|
if (Array.isArray(address)) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'address can not be an array. Use POST for bulk upload.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cashAddr = _this.bchjs.Address.toCashAddress(address)
|
||||||
|
|
||||||
|
// Prevent a common user error. Ensure they are using the correct network address.
|
||||||
|
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
|
||||||
|
if (!networkIsValid) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Customize this function below here for bcash, based on this gist:
|
||||||
|
// https://gist.github.com/christroutner/dcb25a895900e557d7b43341ee85fa79
|
||||||
|
|
||||||
|
wlogger.debug(
|
||||||
|
'Executing bcash/slp.js/getUtxos(). with this address: ',
|
||||||
|
cashAddr
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get data from ElectrumX server.
|
||||||
|
const response = await _this.axios.get(
|
||||||
|
`${_this.bcashServer}coin/address/${cashAddr}?slp=true`
|
||||||
|
)
|
||||||
|
// Get address UTXOs
|
||||||
|
const utxos = response.data
|
||||||
|
// Hydrate UTXOs
|
||||||
|
const hydratedUtxos = await _this.hydrateUTXOS(utxos)
|
||||||
|
res.status(200)
|
||||||
|
return res.json(hydratedUtxos)
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
wlogger.error('Error in bcash/slp.js/getUtxos().', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps and filter the SLP UTXOs of an UTXOs array
|
||||||
|
// get information about the SLP UTXOs
|
||||||
|
async hydrateUTXOS (UTXOS) {
|
||||||
|
try {
|
||||||
|
if (!Array.isArray(UTXOS)) {
|
||||||
|
throw new Error('UTXOs must be an array of slp utxos')
|
||||||
|
}
|
||||||
|
const slpUtxos = UTXOS.filter((val) => val.slp)
|
||||||
|
const hydrated = []
|
||||||
|
|
||||||
|
// Find information of each token
|
||||||
|
for (let i = 0; i < slpUtxos.length; i++) {
|
||||||
|
const slp = slpUtxos[i].slp
|
||||||
|
const info = await _this.getTokenInfo(slp.tokenId)
|
||||||
|
const obj = Object.assign(slp, info)
|
||||||
|
|
||||||
|
slpUtxos[i].slp = obj
|
||||||
|
hydrated.push(slpUtxos[i])
|
||||||
|
}
|
||||||
|
return hydrated
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get information of a token
|
||||||
|
async getTokenInfo (tokenId) {
|
||||||
|
try {
|
||||||
|
if (!tokenId || typeof tokenId !== 'string') {
|
||||||
|
throw new Error('tokenId must be string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await _this.axios.get(
|
||||||
|
`${_this.bcashServer}token/${tokenId}`
|
||||||
|
)
|
||||||
|
const tokenInfo = result.data
|
||||||
|
// console.log('token info', tokenInfo)
|
||||||
|
return tokenInfo
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler.
|
||||||
|
errorHandler (err, res) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
// console.log('errorHandler msg: ', msg)
|
||||||
|
// console.log('errorHandler status: ', status)
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ success: false, error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error patterns specific to this route.
|
||||||
|
if (err.message) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ success: false, error: err.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If error can be handled, return the stack trace
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root API endpoint. Simply acknowledges that it exists.
|
||||||
|
root (req, res, next) {
|
||||||
|
return res.json({ status: 'bcash-slp' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BcashSlp
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ const routeUtils = new RouteUtils()
|
|||||||
const BCHJS = require('@psf/bch-js')
|
const BCHJS = require('@psf/bch-js')
|
||||||
const restURL = process.env.LOCAL_RESTURL
|
const restURL = process.env.LOCAL_RESTURL
|
||||||
? process.env.LOCAL_RESTURL
|
? process.env.LOCAL_RESTURL
|
||||||
: 'https://api.fullstack.cash/v3/'
|
: 'https://api.fullstack.cash/v5/'
|
||||||
const bchjs = new BCHJS({ restURL })
|
const bchjs = new BCHJS({ restURL })
|
||||||
|
|
||||||
let _this
|
let _this
|
||||||
@@ -45,7 +45,7 @@ class Encryption {
|
|||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
if (msg) {
|
if (msg) {
|
||||||
res.status(status)
|
res.status(status)
|
||||||
return res.json({ error: msg })
|
return res.json({ success: false, error: msg })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle error patterns specific to this route.
|
// Handle error patterns specific to this route.
|
||||||
@@ -76,7 +76,7 @@ class Encryption {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getPublicKey (req, res, next) {
|
async getPublicKey (req, res, next) {
|
||||||
@@ -92,6 +92,15 @@ class Encryption {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a user object that can be passed along with internal calls
|
||||||
|
// from bch-js.
|
||||||
|
const usrObj = {
|
||||||
|
ip: req._remoteAddress,
|
||||||
|
jwtToken: req.locals.jwtToken,
|
||||||
|
proLimit: req.locals.proLimit,
|
||||||
|
apiLevel: req.locals.apiLevel
|
||||||
|
}
|
||||||
|
|
||||||
const cashAddr = _this.bchjs.Address.toCashAddress(address)
|
const cashAddr = _this.bchjs.Address.toCashAddress(address)
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
// Prevent a common user error. Ensure they are using the correct network address.
|
||||||
@@ -105,16 +114,17 @@ class Encryption {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(
|
||||||
wlogger.debug(
|
wlogger.debug(
|
||||||
'Executing encryption/getPublicKey with this address: ',
|
'Executing encryption/getPublicKey with this address: ',
|
||||||
cashAddr
|
cashAddr
|
||||||
)
|
)
|
||||||
|
|
||||||
const rawTxData = await _this.bchjs.Electrumx.transactions(cashAddr)
|
const rawTxData = await _this.bchjs.Electrumx.transactions([cashAddr], usrObj)
|
||||||
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
|
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
|
||||||
|
|
||||||
// Extract just the TXIDs
|
// Extract just the TXIDs
|
||||||
const txids = rawTxData.transactions.map((elem) => elem.tx_hash)
|
const txids = rawTxData.transactions[0].transactions.map((elem) => elem.tx_hash)
|
||||||
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
|
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
|
||||||
|
|
||||||
// throw error if there is no transaction history.
|
// throw error if there is no transaction history.
|
||||||
@@ -127,12 +137,13 @@ class Encryption {
|
|||||||
const thisTx = txids[i]
|
const thisTx = txids[i]
|
||||||
|
|
||||||
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
|
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
|
||||||
thisTx,
|
[thisTx],
|
||||||
true
|
true,
|
||||||
|
usrObj
|
||||||
)
|
)
|
||||||
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
|
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
|
||||||
|
|
||||||
const vin = txDetails.vin
|
const vin = txDetails[0].vin
|
||||||
|
|
||||||
// Loop through each input.
|
// Loop through each input.
|
||||||
for (let j = 0; j < vin.length; j++) {
|
for (let j = 0; j < vin.length; j++) {
|
||||||
@@ -170,6 +181,7 @@ class Encryption {
|
|||||||
publicKey: 'not found'
|
publicKey: 'not found'
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// console.log('Error in encryption.js/getPublicKey().', err)
|
||||||
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
@@ -53,6 +53,8 @@ class Blockchain {
|
|||||||
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
||||||
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
||||||
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
||||||
|
this.router.post('/getBlock', this.getBlock)
|
||||||
|
this.router.get('/getBlockHash/:height', this.getBlockHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
root (req, res, next) {
|
root (req, res, next) {
|
||||||
@@ -80,7 +82,7 @@ class Blockchain {
|
|||||||
* block chain.
|
* block chain.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBestBlockHash" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBestBlockHash" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
|
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
|
||||||
*/
|
*/
|
||||||
@@ -111,7 +113,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns an object containing various state info regarding blockchain processing.
|
* @apiDescription Returns an object containing various state info regarding blockchain processing.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockchainInfo" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockchainInfo" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object} object Object containing data
|
* @apiSuccess {Object} object Object containing data
|
||||||
* @apiSuccess {String} object.chain "main"
|
* @apiSuccess {String} object.chain "main"
|
||||||
@@ -155,7 +157,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns the number of blocks in the longest blockchain.
|
* @apiDescription Returns the number of blocks in the longest blockchain.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockCount" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockCount" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} bestBlockCount 587665
|
* @apiSuccess {Number} bestBlockCount 587665
|
||||||
*/
|
*/
|
||||||
@@ -188,7 +190,7 @@ class Blockchain {
|
|||||||
* returns an Object with information about blockheader hash.
|
* returns an Object with information about blockheader hash.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiParam {String} hash block hash
|
* @apiParam {String} hash block hash
|
||||||
* @apiParam {Boolean} verbose Return verbose data
|
* @apiParam {Boolean} verbose Return verbose data
|
||||||
@@ -249,7 +251,7 @@ class Blockchain {
|
|||||||
* returns an Object with information about blockheader hash.
|
* returns an Object with information about blockheader hash.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
|
* curl -X POST "https://api.fullstack.cash/v5/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
|
||||||
*
|
*
|
||||||
* @apiParam {String} hash block hash
|
* @apiParam {String} hash block hash
|
||||||
* @apiParam {Boolean} verbose Return verbose data
|
* @apiParam {Boolean} verbose Return verbose data
|
||||||
@@ -284,7 +286,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, hashes)) {
|
if (!routeUtils.validateArraySize(req, hashes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -341,7 +343,7 @@ class Blockchain {
|
|||||||
* including the main chain as well as orphaned branches.
|
* including the main chain as well as orphaned branches.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getChainTips" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getChainTips" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getChainTips (req, res, next) {
|
async getChainTips (req, res, next) {
|
||||||
@@ -372,7 +374,7 @@ class Blockchain {
|
|||||||
* power on the network.
|
* power on the network.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getDifficulty" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getDifficulty" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getDifficulty (req, res, next) {
|
async getDifficulty (req, res, next) {
|
||||||
@@ -404,7 +406,7 @@ class Blockchain {
|
|||||||
* mempool (unconfirmed)
|
* mempool (unconfirmed)
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getMempoolEntrySingle (req, res, next) {
|
async getMempoolEntrySingle (req, res, next) {
|
||||||
@@ -442,7 +444,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns mempool data for multiple transactions
|
* @apiDescription Returns mempool data for multiple transactions
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X POST https://api.fullstack.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}"
|
* curl -X POST https://api.fullstack.cash/v5/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}"
|
||||||
*/
|
*/
|
||||||
async getMempoolEntryBulk (req, res, next) {
|
async getMempoolEntryBulk (req, res, next) {
|
||||||
try {
|
try {
|
||||||
@@ -457,7 +459,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
if (!routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -515,7 +517,7 @@ class Blockchain {
|
|||||||
* against the 25 ancestor chain-limit.
|
* against the 25 ancestor chain-limit.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getMempoolAncestorsSingle (req, res, next) {
|
async getMempoolAncestorsSingle (req, res, next) {
|
||||||
@@ -557,7 +559,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns details on the active state of the TX memory pool.
|
* @apiDescription Returns details on the active state of the TX memory pool.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET https://api.fullstack.cash/v3/getMempoolInfo -H "accept: application/json"
|
* curl -X GET https://api.fullstack.cash/v5/getMempoolInfo -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getMempoolInfo (req, res, next) {
|
async getMempoolInfo (req, res, next) {
|
||||||
@@ -587,7 +589,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns details on the active state of the TX memory pool.
|
* @apiDescription Returns details on the active state of the TX memory pool.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET https://api.fullstack.cash/v3/getMempoolInfo -H "accept: application/json"
|
* curl -X GET https://api.fullstack.cash/v5/getMempoolInfo -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -599,7 +601,7 @@ class Blockchain {
|
|||||||
* of string transaction ids.
|
* of string transaction ids.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/getRawMempool/?verbose=true" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiParam {Boolean} verbose Return verbose data
|
* @apiParam {Boolean} verbose Return verbose data
|
||||||
*
|
*
|
||||||
@@ -635,7 +637,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns details about an unspent transaction output.
|
* @apiDescription Returns details about an unspent transaction output.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiParam {String} txid Transaction id (required)
|
* @apiParam {String} txid Transaction id (required)
|
||||||
* @apiParam {Number} n Output number (required)
|
* @apiParam {Number} n Output number (required)
|
||||||
@@ -692,7 +694,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns details about an unspent transaction output (UTXO).
|
* @apiDescription Returns details about an unspent transaction output (UTXO).
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl "https://api.fullstack.cash/v3/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}'
|
* curl "https://api.fullstack.cash/v5/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}'
|
||||||
*
|
*
|
||||||
* @apiParam {String} txid Transaction id (required)
|
* @apiParam {String} txid Transaction id (required)
|
||||||
* @apiParam {Number} vout of transaction (required)
|
* @apiParam {Number} vout of transaction (required)
|
||||||
@@ -746,7 +748,7 @@ class Blockchain {
|
|||||||
* @apiDescription Returns a hex-encoded proof that 'txid' was included in a block.
|
* @apiDescription Returns a hex-encoded proof that 'txid' was included in a block.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
* @apiParam {String} txid Transaction id (required)
|
* @apiParam {String} txid Transaction id (required)
|
||||||
*
|
*
|
||||||
@@ -794,7 +796,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
if (!routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -888,7 +890,7 @@ class Blockchain {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!routeUtils.validateArraySize(req, proofs)) {
|
if (!routeUtils.validateArraySize(req, proofs)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -937,6 +939,93 @@ class Blockchain {
|
|||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /blockchain/getBlock/ Get block details
|
||||||
|
* @apiName getBlock
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns block details
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl "https://api.fullstack.cash/v5/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }'
|
||||||
|
*
|
||||||
|
* @apiParam {String} blockhash Block hash (required)
|
||||||
|
* @apiParam {Number} verbosity Default 1 (optional)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getBlock (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Validate input parameter
|
||||||
|
const blockhash = req.body.blockhash
|
||||||
|
let verbosity = req.body.verbosity
|
||||||
|
|
||||||
|
// Default to a value of 1 if another verbosity level is not defined.
|
||||||
|
if (!verbosity && verbosity !== 0) verbosity = 1
|
||||||
|
|
||||||
|
if (!blockhash || blockhash === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ error: 'blockhash can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axios options
|
||||||
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
|
options.data.id = 'getblock'
|
||||||
|
options.data.method = 'getblock'
|
||||||
|
options.data.params = [blockhash, verbosity]
|
||||||
|
|
||||||
|
const response = await _this.axios.request(options)
|
||||||
|
|
||||||
|
return res.json(response.data.result)
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
||||||
|
wlogger.error('Error in blockchain.js/getBlock()', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /blockchain/getBlockHash/ Get block hash
|
||||||
|
* @apiName getBlockHash
|
||||||
|
* @apiGroup Blockchain
|
||||||
|
* @apiDescription Returns the hash of a block, given its block height.
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/blockchain/getBlockHash/544444" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
* @apiParam {String} height Block height (required)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getBlockHash (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Validate input parameter
|
||||||
|
const height = req.params.height
|
||||||
|
if (!height || height === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ error: 'height can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axios options
|
||||||
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
|
options.data.id = 'getblockhash'
|
||||||
|
options.data.method = 'getblockhash'
|
||||||
|
options.data.params = [parseInt(height)]
|
||||||
|
|
||||||
|
const response = await _this.axios.request(options)
|
||||||
|
|
||||||
|
return res.json(response.data.result)
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
||||||
|
wlogger.error('Error in blockchain.js/getBlockHash()', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Blockchain
|
module.exports = Blockchain
|
||||||
@@ -53,7 +53,7 @@ class Control {
|
|||||||
* @apiDescription RPC call which gets basic full node information.
|
* @apiDescription RPC call which gets basic full node information.
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/control/getnetworkinfo" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/control/getnetworkinfo" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getNetworkInfo (req, res, next) {
|
async getNetworkInfo (req, res, next) {
|
||||||
@@ -75,42 +75,6 @@ class Control {
|
|||||||
return _this.errorHandler(error, res)
|
return _this.errorHandler(error, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// router.get('/getMemoryInfo', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getmemoryinfo",
|
|
||||||
// method: "getmemoryinfo"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/help', (req, res, next) => {
|
|
||||||
// BITBOX.Control.help()
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.post('/stop', (req, res, next) => {
|
|
||||||
// BITBOX.Control.stop()
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Control
|
module.exports = Control
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const axios = require('axios')
|
||||||
|
const wlogger = require('../../../util/winston-logging')
|
||||||
|
|
||||||
|
const RouteUtils = require('../../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
|
// Used to convert error messages to strings, to safely pass to users.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
let _this
|
||||||
|
class DSProof {
|
||||||
|
constructor () {
|
||||||
|
_this = this
|
||||||
|
_this.axios = axios
|
||||||
|
_this.routeUtils = routeUtils
|
||||||
|
|
||||||
|
_this.router = router
|
||||||
|
_this.router.get('/', this.root)
|
||||||
|
_this.router.get('/getdsproof/:txid', _this.getDSProof)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler.
|
||||||
|
errorHandler (err, res) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
root (req, res, next) {
|
||||||
|
return res.json({ status: 'dsproof' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /dsproof/getdsproof/:txid Get Double-Spend Proof.
|
||||||
|
* @apiName DS Proof.
|
||||||
|
* @apiGroup DSProof
|
||||||
|
* @apiDescription Get information for a double-spend proof.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/dsproof/getdsproof/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getDSProof (req, res, next) {
|
||||||
|
try {
|
||||||
|
const txid = req.params.txid
|
||||||
|
|
||||||
|
let verbose = 2 // default
|
||||||
|
if (req.query.verbose === 'true') verbose = 3
|
||||||
|
|
||||||
|
if (!txid || txid === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'txid can not be empty'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txid.length !== 64) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: `txid must be of length 64 (not ${txid.length})`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
options.data.id = 'getdsproof'
|
||||||
|
options.data.method = 'getdsproof'
|
||||||
|
options.data.params = [txid, verbose]
|
||||||
|
|
||||||
|
const response = await _this.axios.request(options)
|
||||||
|
return res.json(response.data.result)
|
||||||
|
} catch (err) {
|
||||||
|
wlogger.error('Error in dsproof.js/getDSProof().', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DSProof
|
||||||
@@ -77,7 +77,7 @@ class Mining {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/mining/getMiningInfo" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/mining/getMiningInfo" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -107,7 +107,7 @@ class Mining {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
+46
-27
@@ -65,7 +65,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json"
|
||||||
*/
|
*/
|
||||||
async decodeRawTransactionSingle (req, res, next) {
|
async decodeRawTransactionSingle (req, res, next) {
|
||||||
try {
|
try {
|
||||||
@@ -103,7 +103,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
* curl -X POST "https://api.fullstack.cash/v5/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +118,7 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -140,7 +140,7 @@ class RawTransactions {
|
|||||||
const options = _this.routeUtils.getAxiosOptions()
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
// Loop through each height and creates an array of requests to call in parallel
|
// 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.id = 'decoderawtransaction'
|
||||||
options.data.method = 'decoderawtransaction'
|
options.data.method = 'decoderawtransaction'
|
||||||
options.data.params = [hex]
|
options.data.params = [hex]
|
||||||
@@ -152,7 +152,7 @@ class RawTransactions {
|
|||||||
const axiosResult = await _this.axios.all(promises)
|
const axiosResult = await _this.axios.all(promises)
|
||||||
|
|
||||||
// Retrieve the data part of the result.
|
// 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)
|
res.status(200)
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
@@ -177,7 +177,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -218,7 +218,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
*curl -X POST "https://api.fullstack.cash/v3/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
*curl -X POST "https://api.fullstack.cash/v5/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -234,7 +234,7 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -254,7 +254,7 @@ class RawTransactions {
|
|||||||
const options = _this.routeUtils.getAxiosOptions()
|
const options = _this.routeUtils.getAxiosOptions()
|
||||||
|
|
||||||
// Loop through each hex and create an array of promises
|
// 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.id = 'decodescript'
|
||||||
options.data.method = 'decodescript'
|
options.data.method = 'decodescript'
|
||||||
options.data.params = [hex]
|
options.data.params = [hex]
|
||||||
@@ -267,7 +267,7 @@ class RawTransactions {
|
|||||||
const resolved = await Promise.all(promises)
|
const resolved = await Promise.all(promises)
|
||||||
|
|
||||||
// Retrieve the data from each resolved promise.
|
// 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)
|
res.status(200)
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
@@ -308,7 +308,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}'
|
* curl -X POST "https://api.fullstack.cash/v5/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}'
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getRawTransactionBulk (req, res, next) {
|
async getRawTransactionBulk (req, res, next) {
|
||||||
@@ -324,20 +324,12 @@ class RawTransactions {
|
|||||||
|
|
||||||
// Enforce array size rate limits
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// stub response object
|
|
||||||
// const returnResponse = {
|
|
||||||
// status: 100,
|
|
||||||
// json: {
|
|
||||||
// error: ''
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Validate each txid in the array.
|
// Validate each txid in the array.
|
||||||
for (let i = 0; i < txids.length; i++) {
|
for (let i = 0; i < txids.length; i++) {
|
||||||
const txid = txids[i]
|
const txid = txids[i]
|
||||||
@@ -356,7 +348,7 @@ class RawTransactions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loop through each txid and create an array of promises
|
// 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)
|
_this.getRawTransactionsFromNode(txid, verbose)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -383,7 +375,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -429,7 +421,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X POST "https://api.fullstack.cash/v3/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
* curl -X POST "https://api.fullstack.cash/v5/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -444,11 +436,12 @@ class RawTransactions {
|
|||||||
return res.json({ error: 'hex must be an array' })
|
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
|
// Enforce array size rate limits
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
error: 'Array too large.'
|
error: 'Array too large.'
|
||||||
})
|
})
|
||||||
@@ -522,7 +515,7 @@ class RawTransactions {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
|
* curl -X GET "https://api.fullstack.cash/v5/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -544,7 +537,8 @@ class RawTransactions {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
let options = _this.routeUtils.getAxiosOptions()
|
||||||
|
options = _this.sendTxOptions(options)
|
||||||
|
|
||||||
// RPC call
|
// RPC call
|
||||||
options.data.id = 'sendrawtransaction'
|
options.data.id = 'sendrawtransaction'
|
||||||
@@ -565,6 +559,31 @@ class RawTransactions {
|
|||||||
return _this.errorHandler(err, res)
|
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
|
module.exports = RawTransactions
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Utility functions for troubleshooting a JWT token.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// Public npm libraries.
|
||||||
|
const jwt = require('jsonwebtoken')
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const config = require('../../../config')
|
||||||
|
const wlogger = require('../../util/winston-logging')
|
||||||
|
|
||||||
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
|
// Used for processing error messages before sending them to the user.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
let _this
|
||||||
|
|
||||||
|
class JWT {
|
||||||
|
constructor () {
|
||||||
|
_this = this
|
||||||
|
|
||||||
|
_this.axios = axios
|
||||||
|
_this.routeUtils = routeUtils
|
||||||
|
_this.jwt = jwt
|
||||||
|
_this.config = config
|
||||||
|
|
||||||
|
_this.router = router
|
||||||
|
_this.router.get('/', _this.root)
|
||||||
|
_this.router.post('/info', _this.jwtInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
root (req, res, next) {
|
||||||
|
return res.json({ status: 'jwt' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler.
|
||||||
|
errorHandler (err, res) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /jwt/info Get JWT Info
|
||||||
|
* @apiName GetJWTInfo
|
||||||
|
* @apiGroup JWT
|
||||||
|
* @apiDescription
|
||||||
|
* Get info on your JWT token. Useful for debugging rate limit issues with your
|
||||||
|
* JWT token.
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X POST "https://api.fullstack.cash/v5/jwt/info" -H "accept: application/json" -H "Content-Type: application/json" -d '{"jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZGRmZmI2NzRhM2Q2MDAxOTY3NjE1NCIsImVtYWlsIjoiY2hyaXNAYmNodGVzdC5uZXQiLCJhcGlMZXZlbCI6NjAsInJhdGVMaW1pdCI6MywicG9pbnRzVG9Db25zdW1lIjoxNiwiZHVyYXRpb24iOjMwLCJpYXQiOjE2MjU0MzQ2MzYsImv5cCI6MTYyODAyNjYzNn0.1WLugTkQKVG0yMXD1h5nxfho3gRzvSvs8NMa9obVhPM"}'
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async jwtInfo (req, res, next) {
|
||||||
|
try {
|
||||||
|
const jwtIn = req.body.jwt
|
||||||
|
// console.log('jwt: ', jwtIn)
|
||||||
|
// console.log('_this.config.apiTokenSecret: ', _this.config.apiTokenSecret)
|
||||||
|
|
||||||
|
const decoded = _this.jwt.verify(jwtIn, _this.config.apiTokenSecret)
|
||||||
|
// console.log('decoded: ', decoded)
|
||||||
|
|
||||||
|
const expiration = new Date(decoded.exp * 1000)
|
||||||
|
decoded.expiration = expiration.toISOString()
|
||||||
|
|
||||||
|
const createdAt = new Date(decoded.iat * 1000)
|
||||||
|
decoded.createdAt = createdAt.toISOString()
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
return res.json(decoded)
|
||||||
|
} catch (error) {
|
||||||
|
// console.log('Error in jwt.js/jwtInfo().', error)
|
||||||
|
wlogger.error('Error in jwt.js/jwtInfo().', error)
|
||||||
|
|
||||||
|
return _this.errorHandler(error, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = JWT
|
||||||
@@ -25,13 +25,17 @@ class Price {
|
|||||||
|
|
||||||
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
|
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
|
||||||
this.coinexPriceUrl =
|
this.coinexPriceUrl =
|
||||||
'https://api.coinex.com/v1/market/ticker?market=bchausdt'
|
'https://api.coinex.com/v1/market/ticker?market=xecusdt'
|
||||||
|
|
||||||
|
this.bchCoinexPriceUrl =
|
||||||
|
'https://api.coinex.com/v1/market/ticker?market=bchusdt'
|
||||||
|
|
||||||
this.router = express.Router()
|
this.router = express.Router()
|
||||||
this.router.get('/', _this.root)
|
this.router.get('/', _this.root)
|
||||||
this.router.get('/usd', _this.getUSD)
|
this.router.get('/usd', _this.getUSD)
|
||||||
this.router.get('/rates', _this.getBCHRate)
|
this.router.get('/rates', _this.getBCHRate)
|
||||||
this.router.get('/bchausd', _this.getBCHAUSD)
|
this.router.get('/bchausd', _this.getBCHAUSD)
|
||||||
|
this.router.get('/bchusd', _this.getBCHUSD)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DRY error handler.
|
// DRY error handler.
|
||||||
@@ -60,7 +64,7 @@ class Price {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/price/usd" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/price/usd" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getUSD (req, res, next) {
|
async getUSD (req, res, next) {
|
||||||
@@ -92,7 +96,7 @@ class Price {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/price/rates" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/price/rates" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
// Get rates for several different currencies
|
// Get rates for several different currencies
|
||||||
@@ -125,7 +129,7 @@ class Price {
|
|||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @apiExample Example usage:
|
* @apiExample Example usage:
|
||||||
* curl -X GET "https://api.fullstack.cash/v3/price/bchausd" -H "accept: application/json"
|
* curl -X GET "https://api.fullstack.cash/v5/price/bchausd" -H "accept: application/json"
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async getBCHAUSD (req, res, next) {
|
async getBCHAUSD (req, res, next) {
|
||||||
@@ -150,6 +154,40 @@ class Price {
|
|||||||
return _this.errorHandler(err, res)
|
return _this.errorHandler(err, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /price/bchusd Get the USD price of BCH
|
||||||
|
* @apiName Get the USD price of BCH
|
||||||
|
* @apiGroup Price
|
||||||
|
* @apiDescription Get the USD price of BCH from Coinex.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/price/bchusd" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getBCHUSD (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Request options
|
||||||
|
const opt = {
|
||||||
|
method: 'get',
|
||||||
|
baseURL: _this.bchCoinexPriceUrl,
|
||||||
|
timeout: 15000
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.request(opt)
|
||||||
|
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
||||||
|
|
||||||
|
const price = Number(response.data.data.ticker.last)
|
||||||
|
|
||||||
|
return res.json({ usd: price })
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
wlogger.error('Error in price.js/getBCHUSD().', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Price
|
module.exports = Price
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
|
const wlogger = require('../../util/winston-logging')
|
||||||
|
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
const BCHJS = require('@psf/bch-js')
|
||||||
|
const bchjs = new BCHJS()
|
||||||
|
|
||||||
|
// let _this
|
||||||
|
|
||||||
|
class UtilRoute {
|
||||||
|
constructor (utilConfig) {
|
||||||
|
this.bchjs = bchjs
|
||||||
|
// this.blockbook = blockbook
|
||||||
|
|
||||||
|
if (!utilConfig) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass a config object when instantiating the Util library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!utilConfig.electrumx) {
|
||||||
|
throw new Error(
|
||||||
|
'Must pass an instance of Electrumx when instantiating the Util library.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.electrumx = utilConfig.electrumx
|
||||||
|
|
||||||
|
this.router = router
|
||||||
|
this.router.get('/', this.root)
|
||||||
|
this.router.get('/validateAddress/:address', this.validateAddressSingle)
|
||||||
|
this.router.post('/validateAddress', this.validateAddressBulk)
|
||||||
|
// this.router.post('/sweep', this.sweepWif)
|
||||||
|
|
||||||
|
// _this = this
|
||||||
|
}
|
||||||
|
|
||||||
|
root (req, res, next) {
|
||||||
|
return res.json({ status: 'util' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /util/validateAddress/{address} Get information about single bitcoin cash address.
|
||||||
|
* @apiName Information about single bitcoin cash address
|
||||||
|
* @apiGroup Util
|
||||||
|
* @apiDescription Returns information about single bitcoin cash address.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async validateAddressSingle (req, res, next) {
|
||||||
|
try {
|
||||||
|
const address = req.params.address
|
||||||
|
if (!address || address === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ error: 'address can not be empty' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
BitboxHTTP,
|
||||||
|
// username,
|
||||||
|
// password,
|
||||||
|
requestConfig
|
||||||
|
} = routeUtils.setEnvVars()
|
||||||
|
|
||||||
|
requestConfig.data.id = 'validateaddress'
|
||||||
|
requestConfig.data.method = 'validateaddress'
|
||||||
|
requestConfig.data.params = [address]
|
||||||
|
|
||||||
|
const response = await BitboxHTTP(requestConfig)
|
||||||
|
|
||||||
|
return res.json(response.data.result)
|
||||||
|
} catch (err) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = routeUtils.decodeError(err)
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
wlogger.error('Error in util.ts/validateAddressSingle().', err)
|
||||||
|
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /util/validateAddress Get information about bulk bitcoin cash addresses..
|
||||||
|
* @apiName Information about bulk bitcoin cash addresses.
|
||||||
|
* @apiGroup Util
|
||||||
|
* @apiDescription Returns information about bulk bitcoin cash addresses..
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X POST "https://api.fullstack.cash/v5/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"]}'
|
||||||
|
* curl -X POST "https://api.fullstack.cash/v5/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}'
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async validateAddressBulk (req, res, next) {
|
||||||
|
try {
|
||||||
|
const addresses = req.body.addresses
|
||||||
|
|
||||||
|
// Reject if addresses is not an array.
|
||||||
|
if (!Array.isArray(addresses)) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
error: 'addresses needs to be an array. Use GET for single address.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce array size rate limits
|
||||||
|
if (!routeUtils.validateArraySize(req, addresses)) {
|
||||||
|
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
|
return res.json({
|
||||||
|
error: 'Array too large.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each element in the array.
|
||||||
|
for (let i = 0; i < addresses.length; i++) {
|
||||||
|
const address = addresses[i]
|
||||||
|
|
||||||
|
// Ensure the input is a valid BCH address.
|
||||||
|
try {
|
||||||
|
bchjs.Address.toLegacyAddress(address)
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent a common user error. Ensure they are using the correct network address.
|
||||||
|
const networkIsValid = routeUtils.validateNetwork(address)
|
||||||
|
if (!networkIsValid) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
error:
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wlogger.debug('Executing util/validate with these addresses: ', addresses)
|
||||||
|
|
||||||
|
const {
|
||||||
|
BitboxHTTP,
|
||||||
|
// username,
|
||||||
|
// password,
|
||||||
|
requestConfig
|
||||||
|
} = routeUtils.setEnvVars()
|
||||||
|
|
||||||
|
// Loop through each address and creates an array of requests to call in parallel
|
||||||
|
const promises = addresses.map(async (address) => {
|
||||||
|
requestConfig.data.id = 'validateaddress'
|
||||||
|
requestConfig.data.method = 'validateaddress'
|
||||||
|
requestConfig.data.params = [address]
|
||||||
|
|
||||||
|
return BitboxHTTP(requestConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for all parallel Insight requests to return.
|
||||||
|
const axiosResult = await axios.all(promises)
|
||||||
|
|
||||||
|
// Retrieve the data part of the result.
|
||||||
|
const result = axiosResult.map((x) => x.data.result)
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
return res.json(result)
|
||||||
|
} catch (err) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = routeUtils.decodeError(err)
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
wlogger.error('Error in util.ts/validateAddressSingle().', err)
|
||||||
|
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UtilRoute
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
// const axios = require('axios')
|
|
||||||
const routeUtils = require('./route-utils')
|
const RouteUtils = require('../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
const wlogger = require('../../util/winston-logging')
|
||||||
|
|
||||||
// const router = express.Router()
|
// const router = express.Router()
|
||||||
@@ -154,6 +154,35 @@ class RouteUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle 429 errors
|
||||||
|
if (err.error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} else if (err.error.includes('Network error:')) {
|
||||||
|
return {
|
||||||
|
msg: err.error,
|
||||||
|
status: 503
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle general Error objects.
|
// Handle general Error objects.
|
||||||
if (err.message) {
|
if (err.message) {
|
||||||
return {
|
return {
|
||||||
@@ -169,6 +198,29 @@ class RouteUtils {
|
|||||||
return { msg: false, status: 500 }
|
return { msg: false, status: 500 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dynamically set these based on env vars. Allows unit testing.
|
||||||
|
setEnvVars () {
|
||||||
|
const BitboxHTTP = axios.create({
|
||||||
|
baseURL: process.env.RPC_BASEURL,
|
||||||
|
timeout: 15000
|
||||||
|
})
|
||||||
|
const username = process.env.RPC_USERNAME
|
||||||
|
const password = process.env.RPC_PASSWORD
|
||||||
|
|
||||||
|
const requestConfig = {
|
||||||
|
method: 'post',
|
||||||
|
auth: {
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
jsonrpc: '1.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { BitboxHTTP, username, password, requestConfig }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = RouteUtils
|
module.exports = RouteUtils
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ var NETWORK = process.env.NETWORK
|
|||||||
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
|
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
|
||||||
|
|
||||||
// Default 5 days.
|
// 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.
|
// Configure daily-rotation transport.
|
||||||
var transport = new winston.transports.DailyRotateFile({
|
var transport = new winston.transports.DailyRotateFile({
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export RPC_BASEURL=http://$RPC_IP/
|
|||||||
export RPC_USERNAME=bitcoin
|
export RPC_USERNAME=bitcoin
|
||||||
export RPC_PASSWORD=password
|
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
|
# SLPDB
|
||||||
export SLPDB_PASS_GP=somelongpassword
|
export SLPDB_PASS_GP=somelongpassword
|
||||||
export SLPDB_URL=http://<SLPDB IP>:12300/
|
export SLPDB_URL=http://<SLPDB IP>:12300/
|
||||||
@@ -48,12 +53,12 @@ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
|||||||
# that originate froma domain on the whitelist.
|
# that originate froma domain on the whitelist.
|
||||||
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
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.
|
# Requests use the ANON rate limit if they fail to pass in a JWT token.
|
||||||
# ANON = 20 requests per minute (RPM)
|
# ANON = 20 requests per minute (RPM)
|
||||||
export ANON_RATE_LIMIT=50
|
export ANON_RATE_LIMIT=500
|
||||||
# 10 = 100 RPM
|
# 10 = 1000 RPM
|
||||||
export WHITELIST_RATE_LIMIT=10
|
export WHITELIST_RATE_LIMIT=100
|
||||||
|
|
||||||
# Set logging parameters
|
# Set logging parameters
|
||||||
#1m means no more than 1 megabyte
|
#1m means no more than 1 megabyte
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
Mocking data for electrumx unit tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const utxos = [
|
|
||||||
{
|
|
||||||
height: 604392,
|
|
||||||
tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
|
|
||||||
tx_pos: 0,
|
|
||||||
value: 1000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 630834,
|
|
||||||
tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
|
|
||||||
tx_pos: 0,
|
|
||||||
value: 6000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const balance = {
|
|
||||||
confirmed: 7000,
|
|
||||||
unconfirmed: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const txHistory = [
|
|
||||||
{
|
|
||||||
height: 601861,
|
|
||||||
tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mempool = [
|
|
||||||
{
|
|
||||||
tx_hash: '45381031132c57b2ff1cbe8d8d3920cf9ed25efd9a0beb764bdb2f24c7d1c7e3',
|
|
||||||
height: 0,
|
|
||||||
fee: 24310
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const txDetails = {
|
|
||||||
blockhash: '0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc',
|
|
||||||
blocktime: 1578327094,
|
|
||||||
confirmations: 31861,
|
|
||||||
hash: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
|
|
||||||
hex: '020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000',
|
|
||||||
locktime: 0,
|
|
||||||
size: 392,
|
|
||||||
time: 1578327094,
|
|
||||||
txid: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
|
|
||||||
version: 2,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
scriptSig: {
|
|
||||||
asm: 'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309',
|
|
||||||
hex: '41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309'
|
|
||||||
},
|
|
||||||
sequence: 4294967295,
|
|
||||||
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
|
|
||||||
vout: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scriptSig: {
|
|
||||||
asm: '347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954',
|
|
||||||
hex: '41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954'
|
|
||||||
},
|
|
||||||
sequence: 4294967295,
|
|
||||||
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
|
|
||||||
vout: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
addresses: ['bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl'],
|
|
||||||
asm: 'OP_HASH160 189ce02e332548f4804bac65cba68202c9dbf822 OP_EQUAL',
|
|
||||||
hex: 'a914189ce02e332548f4804bac65cba68202c9dbf82287',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'scripthash'
|
|
||||||
},
|
|
||||||
value: 0.0562057
|
|
||||||
},
|
|
||||||
{
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
addresses: ['bitcoincash: qq59hv6s3qdjrtyfwfxxldkuj9xsjmx48vrz882knz'],
|
|
||||||
asm: 'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a914285bb350881b21ac89724c6fb6dc914d096cd53b88ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
value: 0.00589197
|
|
||||||
},
|
|
||||||
{
|
|
||||||
n: 2,
|
|
||||||
scriptPubKey: {
|
|
||||||
addresses: ['bitcoincash: qpzlruwy4xu5rxjs3z37nsj29y7h59gwvsu4ddp0u4'],
|
|
||||||
asm: 'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
value: 0.03272697
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockHeaders = [
|
|
||||||
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
|
|
||||||
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
|
|
||||||
]
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
utxos,
|
|
||||||
balance,
|
|
||||||
txHistory,
|
|
||||||
mempool,
|
|
||||||
txDetails,
|
|
||||||
blockHeaders
|
|
||||||
}
|
|
||||||
@@ -1,545 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockList = {
|
|
||||||
t: [
|
|
||||||
{
|
|
||||||
tokenDetails: {
|
|
||||||
tokenIdHex:
|
|
||||||
'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
|
|
||||||
documentUri: '',
|
|
||||||
documentSha256: '',
|
|
||||||
symbol: 'NAKAMOTO',
|
|
||||||
name: 'NAKAMOTO',
|
|
||||||
decimals: 8
|
|
||||||
},
|
|
||||||
tokenStats: {
|
|
||||||
qty_valid_txns_since_genesis: 241,
|
|
||||||
qty_valid_token_utxos: 151,
|
|
||||||
qty_valid_token_addresses: 113,
|
|
||||||
qty_token_circulating_supply: '20995990',
|
|
||||||
qty_token_burned: '4010',
|
|
||||||
qty_token_minted: '21000000',
|
|
||||||
qty_satoshis_locked_up: 81900
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockSingleToken = {
|
|
||||||
t: [
|
|
||||||
{
|
|
||||||
tokenDetails: {
|
|
||||||
decimals: 0,
|
|
||||||
tokenIdHex:
|
|
||||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
|
||||||
timestamp: '2019-06-14 06:31:37',
|
|
||||||
timestamp_unix: 1560493897,
|
|
||||||
transactionType: 'GENESIS',
|
|
||||||
versionType: 1,
|
|
||||||
documentUri: 'ot@ot.com',
|
|
||||||
documentSha256Hex: null,
|
|
||||||
symbol: 'OT',
|
|
||||||
name: 'Test First SLP oasis Token',
|
|
||||||
batonVout: 2,
|
|
||||||
containsBaton: true,
|
|
||||||
genesisOrMintQuantity: '10000',
|
|
||||||
sendOutputs: null
|
|
||||||
},
|
|
||||||
tokenStats: {
|
|
||||||
block_created: 1308634,
|
|
||||||
block_last_active_send: 1308655,
|
|
||||||
block_last_active_mint: 1308636,
|
|
||||||
qty_valid_txns_since_genesis: 11,
|
|
||||||
qty_valid_token_utxos: 9,
|
|
||||||
qty_valid_token_addresses: 2,
|
|
||||||
qty_token_minted: '20000',
|
|
||||||
qty_token_burned: '0',
|
|
||||||
qty_token_circulating_supply: '20000',
|
|
||||||
qty_satoshis_locked_up: 4914,
|
|
||||||
minting_baton_status: 'ALIVE'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockSingleTokenError = {
|
|
||||||
t: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockSingleAddress = {
|
|
||||||
g: [
|
|
||||||
{
|
|
||||||
_id: 'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f',
|
|
||||||
balanceString: '0.002382',
|
|
||||||
slpAddress: 'simpleledger:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5me5fdrdn'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
t: [
|
|
||||||
{
|
|
||||||
tokenDetails: {
|
|
||||||
decimals: 6,
|
|
||||||
tokenIdHex:
|
|
||||||
'f05faf13a29c7f5e54ab921750aafb6afaa953db863bd2cf432e918661d4132f'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTx = {
|
|
||||||
txid: '57b3082a2bf269b3d6f40fee7fb9c664e8256a88ca5ee2697c05b9457822d446',
|
|
||||||
version: 2,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: '61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49',
|
|
||||||
vout: 2,
|
|
||||||
sequence: 4294967295,
|
|
||||||
n: 0,
|
|
||||||
scriptSig: {
|
|
||||||
hex:
|
|
||||||
'4730440220409e79fec552f01203f41d3d621ae3db89c720af261c8268ce5f0453de009f5d022001e7ffefeba7b0716d32ea55cb6ace267b6ee9cbcc8a017bb9c3b6acf7889418412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585',
|
|
||||||
asm:
|
|
||||||
'30440220409e79fec552f01203f41d3d621ae3db89c720af261c8268ce5f0453de009f5d022001e7ffefeba7b0716d32ea55cb6ace267b6ee9cbcc8a017bb9c3b6acf7889418[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585'
|
|
||||||
},
|
|
||||||
value: 546,
|
|
||||||
legacyAddress: 'mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL',
|
|
||||||
cashAddress: 'bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
txid: '61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49',
|
|
||||||
vout: 1,
|
|
||||||
sequence: 4294967295,
|
|
||||||
n: 1,
|
|
||||||
scriptSig: {
|
|
||||||
hex:
|
|
||||||
'483045022100a743bee56c99bd103be48a78fa4c7342100815d9d2448dbe6e1d338c3a13b241022066728b5279fc22eef5cd019582ff34771e29175835fc98aa3168a1548fd78ac8412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585',
|
|
||||||
asm:
|
|
||||||
'3045022100a743bee56c99bd103be48a78fa4c7342100815d9d2448dbe6e1d338c3a13b241022066728b5279fc22eef5cd019582ff34771e29175835fc98aa3168a1548fd78ac8[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585'
|
|
||||||
},
|
|
||||||
value: 546,
|
|
||||||
legacyAddress: 'mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL',
|
|
||||||
cashAddress: 'bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
txid: '61e71554a3dc18158f30d9e8f5c9b6641a789690b32302899f81cbea9fe3bb49',
|
|
||||||
vout: 3,
|
|
||||||
sequence: 4294967295,
|
|
||||||
n: 2,
|
|
||||||
scriptSig: {
|
|
||||||
hex:
|
|
||||||
'483045022100821473902eec5f1ce7d43b1ba7f9ec453bfe8b8dfc3de3e0723c883ab109922f02206162960e80618531fab2c16aee260fddd7979bab62471c8686af7de75f8732ec412103c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585',
|
|
||||||
asm:
|
|
||||||
'3045022100821473902eec5f1ce7d43b1ba7f9ec453bfe8b8dfc3de3e0723c883ab109922f02206162960e80618531fab2c16aee260fddd7979bab62471c8686af7de75f8732ec[ALL|FORKID] 03c87f0ec048a0771bdb60533d45cac88c6974afeb055a65edd663c2f947335585'
|
|
||||||
},
|
|
||||||
value: 9997521,
|
|
||||||
legacyAddress: 'mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL',
|
|
||||||
cashAddress: 'bchtest:qz4qnxcxwvmacgye8wlakhz0835x0w3vtvaga95c09'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: '0.00000000',
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
hex:
|
|
||||||
'6a04534c500001010453454e44207ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796080000000049504f80080000001c71e64280',
|
|
||||||
asm:
|
|
||||||
'OP_RETURN 5262419 1 1145980243 7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796 0000000049504f80 0000001c71e64280'
|
|
||||||
},
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '0.00000546',
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
hex: '76a914396b8e57ad0cb58d30e2992f22047b3c20377aa688ac',
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 396b8e57ad0cb58d30e2992f22047b3c20377aa6 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
addresses: ['mkkZf7T3fU3vHSzNPy51HBmM46ghN1gnN9'],
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '0.00000546',
|
|
||||||
n: 2,
|
|
||||||
scriptPubKey: {
|
|
||||||
hex: '76a914aa099b067337dc20993bbfdb5c4f3c6867ba2c5b88ac',
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 aa099b067337dc20993bbfdb5c4f3c6867ba2c5b OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
addresses: ['mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL'],
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '0.09996891',
|
|
||||||
n: 3,
|
|
||||||
scriptPubKey: {
|
|
||||||
hex: '76a914aa099b067337dc20993bbfdb5c4f3c6867ba2c5b88ac',
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 aa099b067337dc20993bbfdb5c4f3c6867ba2c5b OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
addresses: ['mw22g57T9YA7MZQQu5eBDKj3PKTdt99oDL'],
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
blockhash: '000000000000ce978accc64a6bb567acf0c653c202309a0f8e220149bf0c6968',
|
|
||||||
blockheight: 1287490,
|
|
||||||
confirmations: 746,
|
|
||||||
time: 1550855104,
|
|
||||||
blocktime: 1550855104,
|
|
||||||
valueOut: 0.09997983,
|
|
||||||
size: 628,
|
|
||||||
valueIn: 0.09998613,
|
|
||||||
fees: 0.0000063,
|
|
||||||
tokenInfo: {
|
|
||||||
versionType: 1,
|
|
||||||
transactionType: 'SEND',
|
|
||||||
tokenIdHex:
|
|
||||||
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796',
|
|
||||||
sendOutputs: ['0', '1230000000', '122170000000']
|
|
||||||
},
|
|
||||||
tokenIsValid: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockConvert = {
|
|
||||||
slpAddress: 'slptest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2shlcycvd5',
|
|
||||||
cashAddress: 'bchtest:qz35h5mfa8w2pqma2jq06lp7dnv5fxkp2svtllzmlf',
|
|
||||||
legacyAddress: 'mvQPGnzRT6gMWASZBMg7NcT3vmvsSKSQtf'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTokenDetails = {
|
|
||||||
tokenIdHex:
|
|
||||||
'df808a41672a0a0ae6475b44f272a107bc9961b90f29dc918d71301f24fe92fb',
|
|
||||||
documentUri: '',
|
|
||||||
symbol: 'NAKAMOTO',
|
|
||||||
name: 'NAKAMOTO',
|
|
||||||
decimals: 8,
|
|
||||||
timestamp: '',
|
|
||||||
containsBaton: true,
|
|
||||||
versionType: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTokenStats = {
|
|
||||||
qty_valid_txns_since_genesis: 241,
|
|
||||||
qty_valid_token_utxos: 151,
|
|
||||||
qty_valid_token_addresses: 113,
|
|
||||||
qty_token_circulating_supply: '20995990',
|
|
||||||
qty_token_burned: '4010',
|
|
||||||
qty_token_minted: '21000000',
|
|
||||||
qty_satoshis_locked_up: 81900
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockBalance = {
|
|
||||||
_id: 'simpleledger:qp9d8mn8ypryfvea2mev0ggc3wg6plpn4suuaeuss3',
|
|
||||||
token_balance: '1000'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTransactions = [
|
|
||||||
{
|
|
||||||
txid: 'a302f045be8efa1cd982833a7f187ff4fac8baac36da0c887eb2787d8b45e2af',
|
|
||||||
tokenDetails: {
|
|
||||||
valid: true,
|
|
||||||
detail: {
|
|
||||||
decimals: null,
|
|
||||||
tokenIdHex:
|
|
||||||
'495322b37d6b2eae81f045eda612b95870a0c2b6069c58f70cf8ef4e6a9fd43a',
|
|
||||||
timestamp: null,
|
|
||||||
transactionType: 'MINT',
|
|
||||||
versionType: 1,
|
|
||||||
documentUri: null,
|
|
||||||
documentSha256Hex: null,
|
|
||||||
symbol: null,
|
|
||||||
name: null,
|
|
||||||
batonVout: 2,
|
|
||||||
containsBaton: true,
|
|
||||||
genesisOrMintQuantity: {
|
|
||||||
$numberDecimal: '1000'
|
|
||||||
},
|
|
||||||
sendOutputs: null
|
|
||||||
},
|
|
||||||
invalidReason: null,
|
|
||||||
schema_version: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockFoobar = {
|
|
||||||
c: [],
|
|
||||||
u: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockSingleValidTxid = {
|
|
||||||
c: [
|
|
||||||
{
|
|
||||||
_id: '5d965fc27f1cf2184ca2fe73',
|
|
||||||
tx: {
|
|
||||||
h: '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d'
|
|
||||||
},
|
|
||||||
slp: {
|
|
||||||
valid: true,
|
|
||||||
invalidReason: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
u: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTwoValidTxid = {
|
|
||||||
c: [
|
|
||||||
{
|
|
||||||
_id: '5d965fc27f1cf2184ca2fe73',
|
|
||||||
tx: {
|
|
||||||
h: '77872738b6bddee6c0cbdb9509603de20b15d4f6b26602f629417aec2f5d5e8d'
|
|
||||||
},
|
|
||||||
slp: {
|
|
||||||
valid: true,
|
|
||||||
invalidReason: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: '5d71e69758380a002c492a90',
|
|
||||||
tx: {
|
|
||||||
h: '552112f9e458dc7d1d8b328b0a6685e8af74a64b60b6846e7c86407f27f47e42'
|
|
||||||
},
|
|
||||||
slp: {
|
|
||||||
valid: true,
|
|
||||||
invalidReason: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
u: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTwoRedundentTxid = {
|
|
||||||
c: [
|
|
||||||
{
|
|
||||||
_id: '5db99c72a391ae2afd604bde',
|
|
||||||
tx: {
|
|
||||||
h: 'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56'
|
|
||||||
},
|
|
||||||
slp: {
|
|
||||||
valid: true,
|
|
||||||
invalidReason: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
u: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTxHistory = [
|
|
||||||
{
|
|
||||||
tx: {
|
|
||||||
h: '3a7646b3976a8745928c7192c8dde989bfa275fa6d7bce950180ab8002cf6cef'
|
|
||||||
},
|
|
||||||
in: [
|
|
||||||
{
|
|
||||||
i: 0,
|
|
||||||
e: {
|
|
||||||
h: '7bdd586ebd1e5f3dfd5295ab6e896c48b25855ab77a72c035ce1e7aacc965c2d',
|
|
||||||
i: 1,
|
|
||||||
s:
|
|
||||||
'RzBEAiAsu5o9EnxgZChjxKygxhGLhlfxJJbQfd4PP/vp2od3fAIgPpMqg+Sp4YpiInQZ6weJwELq8LEIrHXJpueBz7yF5gRBIQLsyL3809mRphc/+tPAcTQO5bi0kqZRFKpKIHXE3qN8Rw==',
|
|
||||||
a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
i: 1,
|
|
||||||
e: {
|
|
||||||
h: 'df49feff24dc34a10a44a0cbd7c908d964801ec7218512c84574fbce698535f0',
|
|
||||||
i: 3,
|
|
||||||
s:
|
|
||||||
'SDBFAiEAhW3zbKTlPrOXD2E2oEcNof6vCMPGYIg8vOVSE9c8IakCIFLg/gxydG9eL8HMAzkScGElKWJRnfhVMpMHU3evNcrZQSEDRS7F+pSC8OxSldsT4FctJLZBU7f2+FDiE05ae1xqtN0=',
|
|
||||||
a: 'simpleledger:qpkpeqfslejw5pptzcy25h2jxhsc9k0vts43n26up0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
out: [
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 0,
|
|
||||||
i: 0,
|
|
||||||
s:
|
|
||||||
'agRTTFAAAQEEU0VORCA46XxdfTWFosvz+VgMgsozmF+csIRdTcziIMtwn5U4sAgAAAAAAJiWgAgAAAAAJHfU+g=='
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 546,
|
|
||||||
i: 1,
|
|
||||||
s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==',
|
|
||||||
a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 546,
|
|
||||||
i: 2,
|
|
||||||
s: 'dqkUgYSfxf90wVkERCiOMyh16iB4d92IrA==',
|
|
||||||
a: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 21849,
|
|
||||||
i: 3,
|
|
||||||
s: 'dqkUxjCDbbIQGbn4E5D5IxJviX4qkiWIrA==',
|
|
||||||
a: 'simpleledger:qrrrpqmdkggpnw0czwg0jgcjd7yhu25jy5zxh2gqdq'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
slp: {
|
|
||||||
detail: {
|
|
||||||
decimals: 8,
|
|
||||||
tokenIdHex:
|
|
||||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
|
||||||
transactionType: 'SEND',
|
|
||||||
versionType: 1,
|
|
||||||
documentUri: 'psfoundation.cash',
|
|
||||||
documentSha256Hex: null,
|
|
||||||
symbol: 'PSF',
|
|
||||||
name: 'Permissionless Software Foundation',
|
|
||||||
txnBatonVout: null,
|
|
||||||
txnContainsBaton: false,
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza',
|
|
||||||
amount: '0.1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
address: 'simpleledger:qzqcf879la6vzkgygs5guvegwh4zq7rhm5nu0pljjl',
|
|
||||||
amount: '6.11833082'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
blk: {
|
|
||||||
h: '000000000000000001a47bf337ca211c1e7218234d0d70117c64d6f213f5dacb',
|
|
||||||
i: 634833,
|
|
||||||
t: 1589300214
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tx: {
|
|
||||||
h: '3b11b48cab1e7c8384facf482b3f6bfe659a58245ab4aa4147b42b5cb2a5fac5'
|
|
||||||
},
|
|
||||||
in: [
|
|
||||||
{
|
|
||||||
i: 0,
|
|
||||||
e: {
|
|
||||||
h: 'e4e1e1f6b502cbd42f69b919634cea3e37fc8595e6ca800e0a7d8f6dcdfb249e',
|
|
||||||
i: 3,
|
|
||||||
s:
|
|
||||||
'SDBFAiEAjCGRmU28x24LMTwg5XqC+fLf3zGhTYCsSWpmF8Eshq0CIAJhxAKMbwKWC4ASmjWzplpno2ch+hGNUD6HNWOdbd4SQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=',
|
|
||||||
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
i: 1,
|
|
||||||
e: {
|
|
||||||
h: '438420345dd8b7fb4ea74aaf2e3090f44899bf3c71662243946a441de6dae720',
|
|
||||||
i: 2,
|
|
||||||
s:
|
|
||||||
'SDBFAiEAhmLYL4QY3mvZo3/i0XG9PJAYMruw3MaGOLJ3Z4si7hwCIAaaGwRnj/cZK3L/KKFBoMtItpGRZ10GowNN//vgn6PKQSECeRsZo5Fl29g0A9bfJo1E/WIdowWBsLblyxWnEB7ViFE=',
|
|
||||||
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
out: [
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 0,
|
|
||||||
i: 0,
|
|
||||||
s:
|
|
||||||
'agRTTFAAAQEEU0VORCCk+1wtoaoGTiUBikP5FlBABx2emEuhkMIip/WQU6+EsggAAAAAAA9CQAgAAAAXQnHEwA=='
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 546,
|
|
||||||
i: 1,
|
|
||||||
s: 'dqkUqo4lVohqOK6rDd7nIIkFQ6Uf41aIrA==',
|
|
||||||
a: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 546,
|
|
||||||
i: 2,
|
|
||||||
s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==',
|
|
||||||
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
e: {
|
|
||||||
v: 43074,
|
|
||||||
i: 3,
|
|
||||||
s: 'dqkUWQQVny9pv6Y+76cSYzoNltwufoiIrA==',
|
|
||||||
a: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
slp: {
|
|
||||||
detail: {
|
|
||||||
decimals: 2,
|
|
||||||
tokenIdHex:
|
|
||||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
|
||||||
transactionType: 'SEND',
|
|
||||||
versionType: 1,
|
|
||||||
documentUri: 'troutsblog.com',
|
|
||||||
documentSha256Hex: null,
|
|
||||||
symbol: 'TROUT',
|
|
||||||
name: "Trout's test token",
|
|
||||||
txnBatonVout: null,
|
|
||||||
txnContainsBaton: false,
|
|
||||||
outputs: [
|
|
||||||
{
|
|
||||||
address: 'simpleledger:qz4guf2k3p4r3t4tph0wwgyfq4p628lr2c0cvqplza',
|
|
||||||
amount: '10000'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
address: 'simpleledger:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qznkmw3s7',
|
|
||||||
amount: '998990000'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
blk: {
|
|
||||||
h: '000000000000000002a4ee2ed6fe8a764ffef0b2556f545ca0b3ef39ecd2b7c4',
|
|
||||||
i: 634832,
|
|
||||||
t: 1589297145
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockList,
|
|
||||||
mockSingleToken,
|
|
||||||
mockConvert,
|
|
||||||
mockTokenDetails,
|
|
||||||
mockTokenStats,
|
|
||||||
mockTx,
|
|
||||||
mockBalance,
|
|
||||||
mockTransactions,
|
|
||||||
mockSingleTokenError,
|
|
||||||
mockSingleAddress,
|
|
||||||
mockFoobar,
|
|
||||||
mockSingleValidTxid,
|
|
||||||
mockTwoValidTxid,
|
|
||||||
mockTwoRedundentTxid,
|
|
||||||
mockTxHistory
|
|
||||||
}
|
|
||||||
@@ -1,460 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
// const nock = require('nock') // HTTP mocking
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes, mockNext } = require('./mocks/express-mocks')
|
|
||||||
|
|
||||||
// Libraries under test
|
|
||||||
const RateLimits = require('../../src/middleware/route-ratelimit')
|
|
||||||
let rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
// const controlRoute = require('../../src/routes/v3/full-node/control')
|
|
||||||
const jwtAuth = require('../../src/middleware/jwt-auth')
|
|
||||||
|
|
||||||
let req, res, next
|
|
||||||
// let originalEnvVars // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// JWT token used in tests.
|
|
||||||
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ'
|
|
||||||
|
|
||||||
describe('#route-ratelimits & jwt-auth', () => {
|
|
||||||
let sandbox
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
// Save existing environment variables.
|
|
||||||
// originalEnvVars = {
|
|
||||||
// BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
|
||||||
// RPC_BASEURL: process.env.RPC_BASEURL,
|
|
||||||
// RPC_USERNAME: process.env.RPC_USERNAME,
|
|
||||||
// RPC_PASSWORD: process.env.RPC_PASSWORD
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!process.env.JWT_AUTH_SERVER) { process.env.JWT_AUTH_SERVER = 'http://fakeurl.com/' }
|
|
||||||
|
|
||||||
// Wipe the Redis DB, which prevents false negatives when running integration
|
|
||||||
// tests back-to-back.
|
|
||||||
await rateLimits.wipeRedis()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = Object.assign({}, mockReq)
|
|
||||||
res = Object.assign({}, mockRes)
|
|
||||||
next = mockNext
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
req.locals = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
rateLimits.closeRedis()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#jwt-auth.js', () => {
|
|
||||||
describe('#getTokenFromHeaders', () => {
|
|
||||||
it('should populate the req.locals object correctly', () => {
|
|
||||||
// Initialize req.locals
|
|
||||||
req.locals = {
|
|
||||||
proLimit: false,
|
|
||||||
apiLevel: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = `Token ${jwt}`
|
|
||||||
req.headers.authorization = header
|
|
||||||
|
|
||||||
jwtAuth.getTokenFromHeaders(req, res, next)
|
|
||||||
|
|
||||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
|
||||||
|
|
||||||
assert.property(req.locals, 'proLimit')
|
|
||||||
assert.property(req.locals, 'apiLevel')
|
|
||||||
assert.property(req.locals, 'jwtToken')
|
|
||||||
assert.equal(req.locals.jwtToken, jwt)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getResource', () => {
|
|
||||||
it('should decode a blockchain request', () => {
|
|
||||||
const url =
|
|
||||||
'/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true'
|
|
||||||
|
|
||||||
const result = rateLimits.getResource(url)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(result, 'blockchain')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#calcPoints', () => {
|
|
||||||
it('should return 50 points for anonymous user', () => {
|
|
||||||
const result = rateLimits.calcPoints()
|
|
||||||
// console.log(`result: ${result}`)
|
|
||||||
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for free tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for free tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for free tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for full node tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for full-node tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for full node tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 point for indexer tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 30,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for indexer tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 30,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for indexer tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 30,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 point for SLP tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 40,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for SLP tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 40,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for SLP tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 40,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#rateLimitByResource', () => {
|
|
||||||
// NOTE: this test will fail if you run multiple integration tests in a
|
|
||||||
// short period. Because it talks to the Redis DB.
|
|
||||||
it('should pass through rate-limit middleware', async () => {
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
// Call the route twice to trigger the rate handling.
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
|
|
||||||
// next() will be called if rate-limit is not triggered
|
|
||||||
assert.equal(next.called, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => {
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
false,
|
|
||||||
'next should not be called if rate limit was triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should NOT trigger rate-limit for free-tier at 5 RPM', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
id: '5e3a0415eb29a962da2708b1'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
true,
|
|
||||||
'next should be called if rate limit was not triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should trigger rate-limit for free tier after 20 RPM', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
id: '5e3a0415eb29a962da2708b2'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 22; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
false,
|
|
||||||
'next should not be called if rate limit was triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should NOT trigger rate-limit handler for indexer-tier at 25 RPM', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
id: '5e3a0415eb29a962da2708b3'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
true,
|
|
||||||
'next should be called if rate limit was not triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should still rate-limit at a higher RPM for pro-tier', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
id: '5e3a0415eb29a962da2708b5'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 150; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
false,
|
|
||||||
'next should NOT be called if rate limit was triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle misconfigured token secret', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v3'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
|
|
||||||
// Issues with token secret should treat incoming requests as anonymous
|
|
||||||
// calls with 30 points or 3 RPM.
|
|
||||||
assert.equal(res.locals.pointsToConsume, 50)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generates a Basic authorization header.
|
|
||||||
// function generateAuthHeader (pass) {
|
|
||||||
// // https://en.wikipedia.org/wiki/Basic_access_authentication
|
|
||||||
// const username = 'BITBOX'
|
|
||||||
// const combined = `${username}:${pass}`
|
|
||||||
//
|
|
||||||
// var base64Credential = Buffer.from(combined).toString('base64')
|
|
||||||
// var readyCredential = `Basic ${base64Credential}`
|
|
||||||
//
|
|
||||||
// return readyCredential
|
|
||||||
// }
|
|
||||||
@@ -594,7 +594,7 @@ describe('#Electrumx', () => {
|
|||||||
assert.isArray(result.transactions)
|
assert.isArray(result.transactions)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw 429 error if txid array is too large', async () => {
|
it('should throw 400 error if txid array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (var i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
@@ -603,7 +603,7 @@ describe('#Electrumx', () => {
|
|||||||
const result = await electrumxRoute.transactionDetailsBulk(req, res)
|
const result = await electrumxRoute.transactionDetailsBulk(req, res)
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
expectRouteError(res, result, 'Array too large', 429)
|
expectRouteError(res, result, 'Array too large', 400)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get details for a single txid', async () => {
|
it('should get details for a single txid', async () => {
|
||||||
@@ -841,7 +841,7 @@ describe('#Electrumx', () => {
|
|||||||
assert.isArray(result.headers)
|
assert.isArray(result.headers)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw 429 error if heights array is too large', async () => {
|
it('should throw 400 error if heights array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (var i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
@@ -849,7 +849,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
const result = await electrumxRoute.blockHeadersBulk(req, res)
|
const result = await electrumxRoute.blockHeadersBulk(req, res)
|
||||||
|
|
||||||
expectRouteError(res, result, 'Array too large', 429)
|
expectRouteError(res, result, 'Array too large', 400)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should get details for a single height', async () => {
|
it('should get details for a single height', async () => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1823,4 +1823,204 @@ describe('#BlockchainRouter', () => {
|
|||||||
assert.equal(result.length, 2)
|
assert.equal(result.length, 2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#getBlock()', () => {
|
||||||
|
it('returns proper error when downstream service stalls', async () => {
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700'
|
||||||
|
|
||||||
|
const result = await uut.getBlock(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(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700'
|
||||||
|
const result = await uut.getBlock(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 throw 400 if blockhash is empty', async () => {
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error'])
|
||||||
|
assert.include(result.error, 'blockhash can not be empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 0', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity0 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 0
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.isString(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 1', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 1
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 2', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 2
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info without verbosity especified', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe('#Encryption Router', () => {
|
|||||||
.resolves(mockData.mockFulcrumTxHistory)
|
.resolves(mockData.mockFulcrumTxHistory)
|
||||||
sandbox
|
sandbox
|
||||||
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
||||||
.resolves(mockData.mockTxDetails2)
|
.resolves([mockData.mockTxDetails2])
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
const result = await encryptionRoute.getPublicKey(req, res)
|
||||||
@@ -110,7 +110,7 @@ describe('#Encryption Router', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
const result = await encryptionRoute.getPublicKey(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.property(result, 'success')
|
assert.property(result, 'success')
|
||||||
assert.equal(result.success, false)
|
assert.equal(result.success, false)
|
||||||
@@ -130,7 +130,7 @@ describe('#Encryption Router', () => {
|
|||||||
.resolves(mockData.mockFulcrumNoSendBalance)
|
.resolves(mockData.mockFulcrumNoSendBalance)
|
||||||
sandbox
|
sandbox
|
||||||
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
||||||
.resolves(mockData.mockNoSendTx)
|
.resolves([mockData.mockNoSendTx])
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
const result = await encryptionRoute.getPublicKey(req, res)
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
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 return error on non-existing NFT group token', async () => {
|
||||||
|
req.params.tokenId =
|
||||||
|
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
|
||||||
|
const result = await slp.getNftChildren(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'NFT group does not exists',
|
||||||
|
'Error message expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should return error on non-group NFT token', async () => {
|
||||||
|
req.params.tokenId =
|
||||||
|
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||||
|
|
||||||
|
const result = await slp.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 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 return error on non-existing NFT child token', async () => {
|
||||||
|
req.params.tokenId =
|
||||||
|
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
|
||||||
|
const result = await slp.getNftGroup(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'NFT child does not exists',
|
||||||
|
'Error message expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -39,6 +39,14 @@ describe('#price', () => {
|
|||||||
const result = await price.getBCHAUSD(req, res)
|
const result = await price.getBCHAUSD(req, res)
|
||||||
console.log(`result: ${util.inspect(result)}`)
|
console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.isNumber(result.usd)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('#getBCHUSD', () => {
|
||||||
|
it('should get the USD price of BCH', async () => {
|
||||||
|
const result = await price.getBCHUSD(req, res)
|
||||||
|
console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
assert.isNumber(result.usd)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -285,7 +285,86 @@ const mockAncestors = [
|
|||||||
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
||||||
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
||||||
]
|
]
|
||||||
|
const mockBlockInfo = {
|
||||||
|
verbosity0: '20000000',
|
||||||
|
verbosity1: {
|
||||||
|
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
confirmations: 1,
|
||||||
|
size: 3725,
|
||||||
|
strippedsize: 3725,
|
||||||
|
weight: 3725,
|
||||||
|
height: 6725,
|
||||||
|
version: 1,
|
||||||
|
versionHex: '00000000',
|
||||||
|
merkleroot: 'xxxx',
|
||||||
|
tx: [
|
||||||
|
'2afb8264508e2bf3e5288ccad01ed2ab766745b6b9747666b519d59212012c01',
|
||||||
|
'18f40b1ae56bba3fa1934b737fbe46ed8d5ca40fa9aed95073eeb5a119530cd3',
|
||||||
|
'349720d878547752607a69eb19e330592fee271fb5376cdfd811bee423558ed8',
|
||||||
|
'35571c80e7d0e9247b467454ef147d1d5833c775bc2d4164b1bebd4c1f69164f',
|
||||||
|
'480937e8efacdafeeb97d401ff0dd9ea8e8ddb27244cefa67a03621315bdb0e6',
|
||||||
|
'523327469d0b90c0de9a905c2fe6e227278fc5b55b9d9911ca151e1c26647065',
|
||||||
|
'61de4af971d94dbc741762f21dcef08b74b62863a56a1cb3496becdd8d47a858',
|
||||||
|
'69f70a288403b5bba23030ccd05d2e5cb00394620fdae3b050cff9432cc590ce',
|
||||||
|
'94472a90fbdba2eb2cba68308415181faedf3585c66b89efcbdb927a0c10ba23',
|
||||||
|
'bc6f781f9e2f2df460f89995c5e1c7224e48ccbe1aa2a575961f3f5330259864',
|
||||||
|
'f2d945a79bec5454a9ab4d570a150d81124daa308f1c81106a977d6413476944'
|
||||||
|
],
|
||||||
|
time: 111,
|
||||||
|
mediantime: 111,
|
||||||
|
nonce: 111,
|
||||||
|
bits: '1d00ffff',
|
||||||
|
difficulty: 99.999,
|
||||||
|
chainwork: 'xxxx',
|
||||||
|
nTx: 1,
|
||||||
|
previousblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
nextblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
||||||
|
},
|
||||||
|
verbosity2: {
|
||||||
|
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
confirmations: 1,
|
||||||
|
size: 3725,
|
||||||
|
strippedsize: 3725,
|
||||||
|
weight: 3725,
|
||||||
|
height: 6725,
|
||||||
|
version: 1,
|
||||||
|
versionHex: '00000000',
|
||||||
|
merkleroot: 'xxxx',
|
||||||
|
tx: [
|
||||||
|
{
|
||||||
|
hex:
|
||||||
|
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000',
|
||||||
|
txid:
|
||||||
|
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
||||||
|
hash:
|
||||||
|
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
||||||
|
size: 134,
|
||||||
|
version: 1,
|
||||||
|
locktime: 0,
|
||||||
|
vin: [],
|
||||||
|
vout: [],
|
||||||
|
blockhash:
|
||||||
|
'00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048',
|
||||||
|
confirmations: 581882,
|
||||||
|
time: 1231469665,
|
||||||
|
blocktime: 1231469665
|
||||||
|
}
|
||||||
|
],
|
||||||
|
time: 111,
|
||||||
|
mediantime: 111,
|
||||||
|
nonce: 111,
|
||||||
|
bits: '1d00ffff',
|
||||||
|
difficulty: 99.999,
|
||||||
|
chainwork: 'xxxx',
|
||||||
|
nTx: 1,
|
||||||
|
previousblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
nextblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
||||||
|
}
|
||||||
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mockBlockHash,
|
mockBlockHash,
|
||||||
mockBlockchainInfo,
|
mockBlockchainInfo,
|
||||||
@@ -296,5 +375,6 @@ module.exports = {
|
|||||||
mockBlockHeader,
|
mockBlockHeader,
|
||||||
mockTxOut,
|
mockTxOut,
|
||||||
mockTxOutProof,
|
mockTxOutProof,
|
||||||
mockAncestors
|
mockAncestors,
|
||||||
|
mockBlockInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,64 +55,68 @@ const mockFulcrumTxHistory = {
|
|||||||
success: true,
|
success: true,
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
height: 511463,
|
transactions: [
|
||||||
tx_hash:
|
{
|
||||||
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
|
height: 511463,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
|
||||||
height: 511464,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
|
height: 511464,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
|
||||||
height: 513373,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
|
height: 513373,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
|
||||||
height: 513373,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
|
height: 513373,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
|
||||||
height: 560481,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
|
height: 560481,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
|
||||||
height: 560615,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
|
height: 560615,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
|
||||||
height: 561568,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
|
height: 561568,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
|
||||||
height: 561569,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
|
height: 561569,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
|
||||||
height: 561572,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
|
height: 561572,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
|
||||||
height: 561582,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
|
height: 561582,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
|
||||||
height: 562106,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
|
height: 562106,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
|
||||||
height: 562106,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
|
height: 562106,
|
||||||
|
tx_hash:
|
||||||
|
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -159,16 +163,25 @@ const mockTxDetails2 = {
|
|||||||
|
|
||||||
const mockFulcrumNoTxHistory = {
|
const mockFulcrumNoTxHistory = {
|
||||||
success: true,
|
success: true,
|
||||||
transactions: []
|
transactions: [
|
||||||
|
{
|
||||||
|
transactions: [],
|
||||||
|
address: 'bitcoincash:qrgqqkky28jdkv3w0ctrah0mz3jcsnsklc34gtukrh'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockFulcrumNoSendBalance = {
|
const mockFulcrumNoSendBalance = {
|
||||||
success: true,
|
success: true,
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
height: 633578,
|
transactions: [
|
||||||
tx_hash:
|
{
|
||||||
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
|
height: 633578,
|
||||||
|
tx_hash:
|
||||||
|
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
const mockSingleTokenError = {
|
||||||
t: []
|
t: []
|
||||||
}
|
}
|
||||||
@@ -1038,5 +1101,7 @@ module.exports = {
|
|||||||
mockPsfToken,
|
mockPsfToken,
|
||||||
mockValidateBulk,
|
mockValidateBulk,
|
||||||
mockValidate3Bulk,
|
mockValidate3Bulk,
|
||||||
mockStatus
|
mockStatus,
|
||||||
|
mockNftGroup,
|
||||||
|
mockNftChildren
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-47
@@ -13,75 +13,70 @@ const mockAddress = {
|
|||||||
isscript: false
|
isscript: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const mockBalance = {
|
||||||
|
// page: 1,
|
||||||
|
// totalPages: 1,
|
||||||
|
// itemsOnPage: 1000,
|
||||||
|
// address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv43u3mdxyc77j3u6k',
|
||||||
|
// balance: '2546',
|
||||||
|
// totalReceived: '2546',
|
||||||
|
// totalSent: '0',
|
||||||
|
// unconfirmedBalance: '0',
|
||||||
|
// unconfirmedTxs: 0,
|
||||||
|
// txs: 2,
|
||||||
|
// txids: [
|
||||||
|
// 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
||||||
|
// '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6'
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
|
||||||
const mockBalance = {
|
const mockBalance = {
|
||||||
page: 1,
|
confirmed: 2546,
|
||||||
totalPages: 1,
|
unconfirmed: 0
|
||||||
itemsOnPage: 1000,
|
|
||||||
address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv43u3mdxyc77j3u6k',
|
|
||||||
balance: '2546',
|
|
||||||
totalReceived: '2546',
|
|
||||||
totalSent: '0',
|
|
||||||
unconfirmedBalance: '0',
|
|
||||||
unconfirmedTxs: 0,
|
|
||||||
txs: 2,
|
|
||||||
txids: [
|
|
||||||
'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
|
||||||
'44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockUtxos = [
|
const mockUtxos = [
|
||||||
{
|
{
|
||||||
txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
||||||
vout: 0,
|
tx_pos: 0,
|
||||||
value: '2000',
|
value: 2000,
|
||||||
height: 605873,
|
height: 605873
|
||||||
confirmations: 298,
|
|
||||||
satoshis: 2000
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
||||||
vout: 1,
|
tx_pos: 1,
|
||||||
value: '546',
|
value: 546,
|
||||||
height: 605873,
|
height: 605873
|
||||||
confirmations: 298,
|
|
||||||
satoshis: 546
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const mockThreeUtxos = [
|
const mockThreeUtxos = [
|
||||||
{
|
{
|
||||||
txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
||||||
vout: 0,
|
tx_pos: 0,
|
||||||
value: '2000',
|
value: 2000,
|
||||||
height: 605873,
|
height: 605873
|
||||||
confirmations: 298,
|
|
||||||
satoshis: 2000
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
||||||
vout: 1,
|
tx_pos: 1,
|
||||||
value: '546',
|
value: 546,
|
||||||
height: 605873,
|
height: 605873
|
||||||
confirmations: 298,
|
|
||||||
satoshis: 546
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
||||||
vout: 1,
|
tx_pos: 1,
|
||||||
value: '546',
|
value: 546,
|
||||||
height: 605873,
|
height: 605873
|
||||||
confirmations: 298,
|
|
||||||
satoshis: 546
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const mockIsTokenUtxos = [
|
const mockIsTokenUtxos = [
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
||||||
vout: 1,
|
tx_out: 1,
|
||||||
value: '546',
|
value: 546,
|
||||||
height: 605873,
|
height: 605873,
|
||||||
confirmations: 298,
|
confirmations: 298,
|
||||||
satoshis: 546,
|
satoshis: 546,
|
||||||
|
|||||||
@@ -277,4 +277,70 @@ describe('#PriceRouter', () => {
|
|||||||
assert.isNumber(result.usd)
|
assert.isNumber(result.usd)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#getBCHUSD', () => {
|
||||||
|
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
|
||||||
|
|
||||||
|
it('should throw 500 when network issues', async () => {
|
||||||
|
uut.bchCoinexPriceUrl = 'http://fakeurl/api/'
|
||||||
|
|
||||||
|
await uut.getBCHUSD(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
res.statusCode,
|
||||||
|
499,
|
||||||
|
'HTTP status code 500 or greater expected.'
|
||||||
|
)
|
||||||
|
// console.log(res)
|
||||||
|
assert.include(
|
||||||
|
res.output.error,
|
||||||
|
'Network error: Could not communicate with full node or other external service'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns proper error when downstream service stalls', async () => {
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
const result = await uut.getBCHUSD(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(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
const result = await uut.getBCHUSD(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 get the USD price of BCH', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: mockData.mockCoinexFeed })
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await uut.getBCHUSD(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.isNumber(result.usd)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,506 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for the route-ratelimit2.js middleware.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
// Public npm libraries.
|
||||||
|
const assert = require('chai').assert
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const cloneDeep = require('lodash.clonedeep')
|
||||||
|
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
|
// Mocking data.
|
||||||
|
const { mockReq, mockRes, mockNext } = require('./mocks/express-mocks')
|
||||||
|
|
||||||
|
// Libraries under test
|
||||||
|
const RateLimits = require('../../src/middleware/route-ratelimit')
|
||||||
|
let uut = new RateLimits()
|
||||||
|
|
||||||
|
let req, res, next
|
||||||
|
|
||||||
|
describe('#rate-routelimit', () => {
|
||||||
|
let sandbox
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
if (!process.env.JWT_AUTH_SERVER) {
|
||||||
|
process.env.JWT_AUTH_SERVER = 'http://fakeurl.com/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wipe the Redis DB, which prevents false negatives when running integration
|
||||||
|
// tests back-to-back.
|
||||||
|
await uut.wipeRedis()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup the mocks before each test.
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the req and res objects used by Express routes.
|
||||||
|
req = cloneDeep(mockReq)
|
||||||
|
res = cloneDeep(mockRes)
|
||||||
|
next = mockNext
|
||||||
|
|
||||||
|
// Explicitly reset the parmas and body.
|
||||||
|
req.params = {}
|
||||||
|
req.body = {}
|
||||||
|
req.query = {}
|
||||||
|
req.locals = {}
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
|
||||||
|
uut = new RateLimits()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
uut.closeRedis()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#checkInternalIp', () => {
|
||||||
|
it('should return true for a request from localhost', () => {
|
||||||
|
req.ip = '::ffff:127.0.0.1'
|
||||||
|
|
||||||
|
const result = uut.checkInternalIp(req)
|
||||||
|
|
||||||
|
assert.equal(result, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for a request from a Docker container', () => {
|
||||||
|
req.ip = '172.17.0.3'
|
||||||
|
|
||||||
|
const result = uut.checkInternalIp(req)
|
||||||
|
|
||||||
|
assert.equal(result, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for a random ip address', () => {
|
||||||
|
req.ip = '123.456.7.8'
|
||||||
|
|
||||||
|
const result = uut.checkInternalIp(req)
|
||||||
|
|
||||||
|
assert.equal(result, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when an error is encountered', () => {
|
||||||
|
req.ip = 4
|
||||||
|
|
||||||
|
const result = uut.checkInternalIp(req)
|
||||||
|
|
||||||
|
assert.equal(result, false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#isInWhitelist', () => {
|
||||||
|
it('should return false when no argument is passed in', () => {
|
||||||
|
const result = uut.isInWhitelist()
|
||||||
|
|
||||||
|
assert.equal(result, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false when origin is not in the whitelist', () => {
|
||||||
|
req.origin = 'blah.com'
|
||||||
|
req.get = sandbox.stub().returns(req.origin)
|
||||||
|
|
||||||
|
const result = uut.isInWhitelist(req)
|
||||||
|
|
||||||
|
assert.equal(result, false)
|
||||||
|
|
||||||
|
// Used to appease linter. Remove these.
|
||||||
|
res.blah = 4
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true when origin is in the whitelist', () => {
|
||||||
|
req.origin = 'message.fullstack.cash'
|
||||||
|
req.get = sandbox.stub().returns(req.origin)
|
||||||
|
|
||||||
|
const result = uut.isInWhitelist(req)
|
||||||
|
|
||||||
|
assert.equal(result, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#decodeJwtToken', () => {
|
||||||
|
it('should return the default JWT payload if decoding fails', () => {
|
||||||
|
const jwt =
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYxNTE1NzA4NywiZXhwIjoxNjE3NzQ5MDg3fQ.RLNGuYAa-CcLdhTGD27tDeaxT6-GIdeR8T4JWZZLDZA'
|
||||||
|
|
||||||
|
const result = uut.decodeJwtToken(jwt)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.property(result, 'id')
|
||||||
|
// assert.equal(result.id, '123.456.789.10')
|
||||||
|
assert.property(result, 'email')
|
||||||
|
// assert.equal(result.email, 'test@bchtest.net')
|
||||||
|
// assert.property(result, 'pointsToConsume')
|
||||||
|
// assert.equal(result.pointsToConsume, config.anonRateLimit)
|
||||||
|
// assert.property(result, 'duration')
|
||||||
|
// assert.equal(result.duration, 30)
|
||||||
|
assert.property(result, 'exp')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the default JWT payload if no input is given', () => {
|
||||||
|
const result = uut.decodeJwtToken()
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.property(result, 'id')
|
||||||
|
assert.equal(result.id, '123.456.789.10')
|
||||||
|
assert.property(result, 'email')
|
||||||
|
assert.equal(result.email, 'test@bchtest.net')
|
||||||
|
assert.property(result, 'pointsToConsume')
|
||||||
|
assert.equal(result.pointsToConsume, config.anonRateLimit)
|
||||||
|
assert.property(result, 'duration')
|
||||||
|
assert.equal(result.duration, 30)
|
||||||
|
assert.property(result, 'exp')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should correctly decode a JWT token', () => {
|
||||||
|
// Generate a new JWT token for the test.
|
||||||
|
const jwtPayload = {
|
||||||
|
id: '5dade3f5739e6c0ff034b9a1',
|
||||||
|
pointsToConsume: 10,
|
||||||
|
email: 'gooduser@test.com',
|
||||||
|
apiLevel: 40,
|
||||||
|
rateLimit: 100,
|
||||||
|
duration: 30
|
||||||
|
}
|
||||||
|
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||||
|
|
||||||
|
const result = uut.decodeJwtToken(jwtToken)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.property(result, 'id')
|
||||||
|
assert.equal(result.id, jwtPayload.id)
|
||||||
|
assert.property(result, 'email')
|
||||||
|
assert.equal(result.email, jwtPayload.email)
|
||||||
|
assert.property(result, 'pointsToConsume')
|
||||||
|
assert.equal(result.pointsToConsume, jwtPayload.pointsToConsume)
|
||||||
|
assert.property(result, 'duration')
|
||||||
|
assert.equal(result.duration, jwtPayload.duration)
|
||||||
|
assert.property(result, 'exp')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the default payload if there is an unhandled error', () => {
|
||||||
|
// Force an error.
|
||||||
|
sandbox.stub(uut, 'generateJwtToken').throws(new Error('test error'))
|
||||||
|
|
||||||
|
const result = uut.decodeJwtToken()
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.property(result, 'id')
|
||||||
|
assert.equal(result.id, '123.456.789.10')
|
||||||
|
assert.property(result, 'email')
|
||||||
|
assert.equal(result.email, 'test@bchtest.net')
|
||||||
|
assert.property(result, 'pointsToConsume')
|
||||||
|
assert.equal(result.pointsToConsume, config.anonRateLimit)
|
||||||
|
assert.property(result, 'duration')
|
||||||
|
assert.equal(result.duration, 30)
|
||||||
|
assert.property(result, 'exp')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#trackRateLimits', () => {
|
||||||
|
it('should apply anonymous rate limits if no JWT token is provided', async () => {
|
||||||
|
req.ip = '127.0.0.1'
|
||||||
|
|
||||||
|
const result = await uut.trackRateLimits(req, res)
|
||||||
|
// console.log(`result: `, result)
|
||||||
|
|
||||||
|
// console.log('res.locals.pointsToConsume: ', res.locals.pointsToConsume)
|
||||||
|
|
||||||
|
assert.equal(result, false, 'Rate limits not exceeded')
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
config.anonRateLimit,
|
||||||
|
'Anonymous rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply 100 RPM rate limits when JWT token is provided', async () => {
|
||||||
|
// Generate a new JWT token for the test.
|
||||||
|
const jwtPayload = {
|
||||||
|
id: '5dade3f5739e6c0ff034b9a1',
|
||||||
|
pointsToConsume: 10
|
||||||
|
}
|
||||||
|
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||||
|
|
||||||
|
const result = await uut.trackRateLimits(req, res, jwtToken)
|
||||||
|
// console.log(`result: `, result)
|
||||||
|
|
||||||
|
// console.log('res.locals.pointsToConsume: ', res.locals.pointsToConsume)
|
||||||
|
|
||||||
|
assert.equal(result, false, 'Rate limits not exceeded')
|
||||||
|
assert.equal(res.locals.pointsToConsume, 10, '100 RPM limits applied')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#applyRateLimits', () => {
|
||||||
|
it('should skip rate limits if basic auth token is used', async () => {
|
||||||
|
req.locals.proLimit = true
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip rate limits if internal call passes basic auth token', async () => {
|
||||||
|
req.ip = '127.0.0.1'
|
||||||
|
req.body.usrObj = {
|
||||||
|
proLimit: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply rate limits to anonymous users', async () => {
|
||||||
|
req.ip = '123.456.7.8'
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
config.anonRateLimit,
|
||||||
|
'Anonymous rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 429 error when anonymous users exceed rate limit', async () => {
|
||||||
|
req.ip = '123.456.7.8'
|
||||||
|
|
||||||
|
// force req.locals.jwtToken to be empty.
|
||||||
|
req.locals.jwtToken = undefined
|
||||||
|
|
||||||
|
let val
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
// console.log('req.locals: ', req.locals)
|
||||||
|
val = await uut.applyRateLimits(req, res, next)
|
||||||
|
}
|
||||||
|
// console.log('val: ', val)
|
||||||
|
|
||||||
|
assert.property(val, 'error')
|
||||||
|
assert.include(
|
||||||
|
val.error,
|
||||||
|
'Too many requests. Your limits are currently 20 requests per minute.'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
config.anonRateLimit,
|
||||||
|
'Anonymous rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply rate limits when JWT token is provided', async () => {
|
||||||
|
// Generate a new JWT token for the test.
|
||||||
|
const jwtPayload = {
|
||||||
|
id: '5dade3f5739e6c0ff034b9a1',
|
||||||
|
pointsToConsume: 10
|
||||||
|
}
|
||||||
|
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||||
|
|
||||||
|
req.ip = '123.456.7.8'
|
||||||
|
req.locals.jwtToken = jwtToken
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
10,
|
||||||
|
'Anonymous rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply internal rate limits to internal calls', async () => {
|
||||||
|
req.ip = '127.0.0.1'
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
10,
|
||||||
|
'Internal rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 429 error when internal calls exceed interal rate limit', async () => {
|
||||||
|
req.ip = '127.0.0.1'
|
||||||
|
|
||||||
|
let val
|
||||||
|
for (let i = 0; i < 1025; i++) {
|
||||||
|
val = await uut.applyRateLimits(req, res, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.property(val, 'error')
|
||||||
|
assert.include(
|
||||||
|
val.error,
|
||||||
|
'Too many requests. Your limits are currently 1000 requests per minute.'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
10,
|
||||||
|
'Internal rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply JWT rate limits to internal calls when JWT passes through', async () => {
|
||||||
|
// Generate a new JWT token for the test.
|
||||||
|
const jwtPayload = {
|
||||||
|
id: '5dade3f5739e6c0ff034b9a1',
|
||||||
|
pointsToConsume: 10
|
||||||
|
}
|
||||||
|
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||||
|
|
||||||
|
req.ip = '127.0.0.1'
|
||||||
|
req.body.usrObj = {
|
||||||
|
jwtToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
10,
|
||||||
|
'User JWT rate limits applied'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 429 error when internal calls using JWT pass-through exceeds rate limit', async () => {
|
||||||
|
// Generate a new JWT token for the test.
|
||||||
|
const jwtPayload = {
|
||||||
|
id: '5dade3f5739e6c0ff034b9a1',
|
||||||
|
pointsToConsume: 100
|
||||||
|
}
|
||||||
|
const jwtToken = uut.generateJwtToken(jwtPayload)
|
||||||
|
|
||||||
|
req.ip = '127.0.0.1'
|
||||||
|
req.body.usrObj = {
|
||||||
|
jwtToken
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let val
|
||||||
|
for (let i = 0; i < 120; i++) {
|
||||||
|
val = await uut.applyRateLimits(req, res, next)
|
||||||
|
}
|
||||||
|
// console.log('val: ', val)
|
||||||
|
|
||||||
|
assert.property(val, 'error')
|
||||||
|
assert.include(
|
||||||
|
val.error,
|
||||||
|
'Too many requests. Your limits are currently 100 requests per minute.'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
res.locals.pointsToConsume,
|
||||||
|
100,
|
||||||
|
'User JWT rate limits applied'
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('err: ', err)
|
||||||
|
assert.fail('Unexpected result')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should move to the next middleware when encountering an unexpected internal error', async () => {
|
||||||
|
// Force the creation of the res and req locals property. Covers an
|
||||||
|
// otherwise untested code path.
|
||||||
|
req.locals = undefined
|
||||||
|
res.locals = undefined
|
||||||
|
|
||||||
|
// Force an error
|
||||||
|
sandbox.stub(uut, 'checkInternalIp').throws(new Error('test error'))
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const startCallCount = next.callCount
|
||||||
|
|
||||||
|
await uut.applyRateLimits(req, res, next)
|
||||||
|
|
||||||
|
// console.log('next.callCount: ', next.callCount)
|
||||||
|
const endCallCount = next.callCount
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
endCallCount,
|
||||||
|
startCallCount,
|
||||||
|
'Expecting next() to be called'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,488 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
// const nock = require('nock') // HTTP mocking
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes, mockNext } = require('./mocks/express-mocks')
|
|
||||||
|
|
||||||
// Libraries under test
|
|
||||||
const RateLimits = require('../../src/middleware/route-ratelimit')
|
|
||||||
let rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
// const controlRoute = require('../../src/routes/v4/full-node/control')
|
|
||||||
const jwtAuth = require('../../src/middleware/jwt-auth')
|
|
||||||
|
|
||||||
let req, res, next
|
|
||||||
// let originalEnvVars // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// JWT token used in tests.
|
|
||||||
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVkYWRlM2Y1NzM5ZTZjMGZmMDM0YjlhMSIsImlhdCI6MTU3MTY3NzQ1MCwiZXhwIjoxNTc0MjY5NDUwfQ.SSz7F7ETyBB3eoNG2VKCzPOhddtB-vrtmEoj7PxicrQ'
|
|
||||||
|
|
||||||
describe('#route-ratelimits & jwt-auth', () => {
|
|
||||||
let sandbox
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
// Save existing environment variables.
|
|
||||||
// originalEnvVars = {
|
|
||||||
// BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
|
||||||
// RPC_BASEURL: process.env.RPC_BASEURL,
|
|
||||||
// RPC_USERNAME: process.env.RPC_USERNAME,
|
|
||||||
// RPC_PASSWORD: process.env.RPC_PASSWORD
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!process.env.JWT_AUTH_SERVER) { process.env.JWT_AUTH_SERVER = 'http://fakeurl.com/' }
|
|
||||||
|
|
||||||
// Wipe the Redis DB, which prevents false negatives when running integration
|
|
||||||
// tests back-to-back.
|
|
||||||
await rateLimits.wipeRedis()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = Object.assign({}, mockReq)
|
|
||||||
res = Object.assign({}, mockRes)
|
|
||||||
next = mockNext
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
req.locals = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
rateLimits.closeRedis()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#jwt-auth.js', () => {
|
|
||||||
describe('#getTokenFromHeaders', () => {
|
|
||||||
it('should populate the req.locals object correctly', () => {
|
|
||||||
// Initialize req.locals
|
|
||||||
req.locals = {
|
|
||||||
proLimit: false,
|
|
||||||
apiLevel: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = `Token ${jwt}`
|
|
||||||
req.headers.authorization = header
|
|
||||||
|
|
||||||
jwtAuth.getTokenFromHeaders(req, res, next)
|
|
||||||
|
|
||||||
// console.log(`req.locals: ${JSON.stringify(req.locals, null, 2)}`)
|
|
||||||
|
|
||||||
assert.property(req.locals, 'proLimit')
|
|
||||||
assert.property(req.locals, 'apiLevel')
|
|
||||||
assert.property(req.locals, 'jwtToken')
|
|
||||||
assert.equal(req.locals.jwtToken, jwt)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getResource', () => {
|
|
||||||
it('should decode a blockchain request', () => {
|
|
||||||
const url =
|
|
||||||
'/blockchain/getTxOut/62a3ea958a463a372bc0caf2c374a7f60be9c624be63a0db8db78f05809df6d8/0?include_mempool=true'
|
|
||||||
|
|
||||||
const result = rateLimits.getResource(url)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(result, 'blockchain')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#calcPoints', () => {
|
|
||||||
it('should return 50 points for anonymous user', () => {
|
|
||||||
const result = rateLimits.calcPoints()
|
|
||||||
// console.log(`result: ${result}`)
|
|
||||||
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for free tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for free tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for free tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for full node tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for full-node tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for full node tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 point for indexer tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 30,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for indexer tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 30,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 50 points for indexer tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 30,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 point for SLP tier requesting full node access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 40,
|
|
||||||
resource: 'blockchain',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for SLP tier requesting indexer access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 40,
|
|
||||||
resource: 'blockbook',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 10 points for SLP tier requesting SLPDB access', () => {
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 40,
|
|
||||||
resource: 'slp',
|
|
||||||
id: '5e3a0415eb29a962da2708b4'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = rateLimits.calcPoints(jwtInfo)
|
|
||||||
assert.equal(result, 10)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#rateLimitByResource', () => {
|
|
||||||
// NOTE: this test will fail if you run multiple integration tests in a
|
|
||||||
// short period. Because it talks to the Redis DB.
|
|
||||||
it('should pass through rate-limit middleware', async () => {
|
|
||||||
req.baseUrl = '/v4'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
// Call the route twice to trigger the rate handling.
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
|
|
||||||
// next() will be called if rate-limit is not triggered
|
|
||||||
assert.equal(next.called, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => {
|
|
||||||
req.baseUrl = '/v4'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
false,
|
|
||||||
'next should not be called if rate limit was triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should NOT trigger rate-limit for free-tier at 5 RPM', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v4'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
id: '5e3a0415eb29a962da2708b1'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
true,
|
|
||||||
'next should be called if rate limit was not triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should trigger rate-limit for free tier after 20 RPM', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v4'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 10,
|
|
||||||
id: '5e3a0415eb29a962da2708b2'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 22; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
false,
|
|
||||||
'next should not be called if rate limit was triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should NOT trigger rate-limit handler for indexer-tier at 25 RPM', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v4'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
id: '5e3a0415eb29a962da2708b3'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
true,
|
|
||||||
'next should be called if rate limit was not triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should still rate-limit at a higher RPM for pro-tier', async () => {
|
|
||||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
rateLimits = new RateLimits()
|
|
||||||
|
|
||||||
req.baseUrl = '/v4'
|
|
||||||
req.path = '/control/getNetworkInfo'
|
|
||||||
req.url = req.path
|
|
||||||
req.method = 'GET'
|
|
||||||
|
|
||||||
req.locals.jwtToken = 'some-token'
|
|
||||||
|
|
||||||
const jwtInfo = {
|
|
||||||
apiLevel: 20,
|
|
||||||
id: '5e3a0415eb29a962da2708b5'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the call to the jwt library.
|
|
||||||
sandbox.stub(rateLimits.jwt, 'verify').returns(jwtInfo)
|
|
||||||
|
|
||||||
for (let i = 0; i < 150; i++) {
|
|
||||||
next.reset() // reset the stubbed next() function.
|
|
||||||
|
|
||||||
await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
// console.log(`next() called: ${next.called}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`req.locals after test: ${util.inspect(req.locals)}`)
|
|
||||||
|
|
||||||
// Note: next() will be called unless the rate-limit kicks in.
|
|
||||||
assert.equal(
|
|
||||||
next.called,
|
|
||||||
false,
|
|
||||||
'next should NOT be called if rate limit was triggered.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// CT 2/24/21 This test may have been invalidated by the interal IP address
|
|
||||||
// passing that I implemented to get hydrateUtxos() working properly.
|
|
||||||
// I'm commenting this out until I can study the side effects of this change,
|
|
||||||
// and why exactly this test is breaking.
|
|
||||||
// it('should handle misconfigured token secret', async () => {
|
|
||||||
// // Create a new instance of the rate limit so we start with zeroed tracking.
|
|
||||||
// rateLimits = new RateLimits()
|
|
||||||
//
|
|
||||||
// req.baseUrl = '/v4'
|
|
||||||
// req.path = '/control/getNetworkInfo'
|
|
||||||
// req.url = req.path
|
|
||||||
// req.method = 'GET'
|
|
||||||
//
|
|
||||||
// req.locals.jwtToken = 'some-token'
|
|
||||||
//
|
|
||||||
// next.reset() // reset the stubbed next() function.
|
|
||||||
//
|
|
||||||
// await rateLimits.rateLimitByResource(req, res, next)
|
|
||||||
//
|
|
||||||
// // Issues with token secret should treat incoming requests as anonymous
|
|
||||||
// // calls with 50 points, or 20 RPM.
|
|
||||||
// assert.equal(res.locals.pointsToConsume, 50)
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#isInWhitelist', () => {
|
|
||||||
it('should return false when no argument is passed in', () => {
|
|
||||||
const result = rateLimits.isInWhitelist()
|
|
||||||
|
|
||||||
assert.equal(result, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return false when origin is not in the whitelist', () => {
|
|
||||||
const origin = 'blah.com'
|
|
||||||
|
|
||||||
const result = rateLimits.isInWhitelist(origin)
|
|
||||||
|
|
||||||
assert.equal(result, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return true when origin is in the whitelist', () => {
|
|
||||||
const origin = 'message.fullstack.cash'
|
|
||||||
|
|
||||||
const result = rateLimits.isInWhitelist(origin)
|
|
||||||
|
|
||||||
assert.equal(result, true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generates a Basic authorization header.
|
|
||||||
// function generateAuthHeader (pass) {
|
|
||||||
// // https://en.wikipedia.org/wiki/Basic_access_authentication
|
|
||||||
// const username = 'BITBOX'
|
|
||||||
// const combined = `${username}:${pass}`
|
|
||||||
//
|
|
||||||
// var base64Credential = Buffer.from(combined).toString('base64')
|
|
||||||
// var readyCredential = `Basic ${base64Credential}`
|
|
||||||
//
|
|
||||||
// return readyCredential
|
|
||||||
// }
|
|
||||||
@@ -250,6 +250,7 @@ describe('#Raw-Transactions', () => {
|
|||||||
'Proper error message'
|
'Proper error message'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
it('returns proper error when downstream service stalls', async () => {
|
||||||
req.body.hexes = [
|
req.body.hexes = [
|
||||||
'0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000'
|
'0200000001b9b598d7d6d72fc486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac00000000'
|
||||||
@@ -1046,10 +1047,12 @@ describe('#Raw-Transactions', () => {
|
|||||||
// Save the existing RPC URL.
|
// Save the existing RPC URL.
|
||||||
const savedUrl = process.env.BITCOINCOM_BASEURL
|
const savedUrl = process.env.BITCOINCOM_BASEURL
|
||||||
const savedUrl2 = process.env.RPC_BASEURL
|
const savedUrl2 = process.env.RPC_BASEURL
|
||||||
|
const savedUrl3 = process.env.RPC_SENDURL
|
||||||
|
|
||||||
// Manipulate the URL to cause a 500 network error.
|
// Manipulate the URL to cause a 500 network error.
|
||||||
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
|
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
||||||
|
process.env.RPC_SENDURL = 'http://fakeurl/api/'
|
||||||
|
|
||||||
req.params.hex =
|
req.params.hex =
|
||||||
'020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900'
|
'020000000136697692fed77bc4f5b6885295d0c56d1d0280fb578f445ce42be4eb6db381f2010000006a4730440220473adba0e7da14f0abf4817bbd591741ecb8da6544b998f10341f6704f5f05280220405221c626cb7edcf333367ebd469aff3f5a2169e37ee58eebb811ffc2fbc9e0412102202ff86325c5d903171fa5a2895c4efb3765105115460dc96f113048ddb69b47feffffff027a621b00000000001976a914e0a8ffc3b91e35f46618d6db90f66397989abf0588ac38041300000000001976a914a741f282af390bc7ea8c4375a3a56401d668564288ac2c330900'
|
||||||
@@ -1059,6 +1062,7 @@ describe('#Raw-Transactions', () => {
|
|||||||
// Restore the saved URL.
|
// Restore the saved URL.
|
||||||
process.env.BITCOINCOM_BASEURL = savedUrl
|
process.env.BITCOINCOM_BASEURL = savedUrl
|
||||||
process.env.RPC_BASEURL = savedUrl2
|
process.env.RPC_BASEURL = savedUrl2
|
||||||
|
process.env.RPC_SENDURL = savedUrl3
|
||||||
|
|
||||||
assert.isAbove(
|
assert.isAbove(
|
||||||
res.statusCode,
|
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.')
|
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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+46
-33
@@ -4,16 +4,24 @@
|
|||||||
This test file uses the environment variable TEST to switch between unit
|
This test file uses the environment variable TEST to switch between unit
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
to 'integration' to run the tests against BCH mainnet.
|
||||||
|
|
||||||
|
These tests use this private key:
|
||||||
|
L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq
|
||||||
|
Which corresponds to this address:
|
||||||
|
bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const chai = require('chai')
|
// Public npm libraries
|
||||||
const assert = chai.assert
|
const assert = require('chai').assert
|
||||||
const utilRoute = require('../../src/routes/v4/util')
|
|
||||||
const nock = require('nock') // HTTP mocking
|
const nock = require('nock') // HTTP mocking
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
// Local libraries
|
||||||
|
const Electrumx = require('../../src/routes/v4/electrumx')
|
||||||
|
const UtilRoute = require('../../src/routes/v4/util')
|
||||||
|
|
||||||
let originalEnvVars // Used during transition from integration to unit tests.
|
let originalEnvVars // Used during transition from integration to unit tests.
|
||||||
|
|
||||||
// Mocking data.
|
// Mocking data.
|
||||||
@@ -24,13 +32,16 @@ const util = require('util')
|
|||||||
util.inspect.defaultOptions = { depth: 1 }
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
// const UtilRoute = utilRoute.UtilRoute
|
// const UtilRoute = utilRoute.UtilRoute
|
||||||
const utilRouteInst = new utilRoute.UtilRoute()
|
// const electrumx = new Electrumx()
|
||||||
|
const utilRouteInst = new UtilRoute({ electrumx: {} })
|
||||||
|
|
||||||
describe('#Util', () => {
|
describe('#Util', () => {
|
||||||
let req, res
|
let req, res
|
||||||
let sandbox
|
let sandbox
|
||||||
|
let electrumx
|
||||||
|
let utilRoute
|
||||||
|
|
||||||
before(() => {
|
before(async () => {
|
||||||
// Save existing environment variables.
|
// Save existing environment variables.
|
||||||
originalEnvVars = {
|
originalEnvVars = {
|
||||||
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
||||||
@@ -47,6 +58,9 @@ describe('#Util', () => {
|
|||||||
process.env.RPC_USERNAME = 'fakeusername'
|
process.env.RPC_USERNAME = 'fakeusername'
|
||||||
process.env.RPC_PASSWORD = 'fakepassword'
|
process.env.RPC_PASSWORD = 'fakepassword'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
electrumx = new Electrumx()
|
||||||
|
// await electrumx.connect()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
// Setup the mocks before each test.
|
||||||
@@ -64,6 +78,8 @@ describe('#Util', () => {
|
|||||||
if (!nock.isActive()) nock.activate()
|
if (!nock.isActive()) nock.activate()
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
sandbox = sinon.createSandbox()
|
||||||
|
|
||||||
|
utilRoute = new UtilRoute({ electrumx })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -357,29 +373,29 @@ describe('#Util', () => {
|
|||||||
// Mock the RPC call for unit tests.
|
// Mock the RPC call for unit tests.
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
||||||
.resolves(mockData.mockBalance)
|
.resolves(mockData.mockBalance)
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
||||||
.resolves(mockData.mockUtxos)
|
.resolves(mockData.mockUtxos)
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
||||||
.resolves(mockData.mockIsTokenUtxos)
|
.resolves(mockData.mockIsTokenUtxos)
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||||
// to the network.
|
// to the network.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
||||||
.resolves('test-txid')
|
.resolves('test-txid')
|
||||||
|
|
||||||
req.body = {
|
req.body = {
|
||||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
wif: 'L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq',
|
||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
toAddr: 'bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r'
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
const result = await utilRoute.sweepWif(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.equal(result, 'test-txid')
|
assert.equal(result, 'test-txid')
|
||||||
@@ -389,13 +405,13 @@ describe('#Util', () => {
|
|||||||
// Mock the RPC call for unit tests.
|
// Mock the RPC call for unit tests.
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
||||||
.resolves(mockData.mockBalance)
|
.resolves(mockData.mockBalance)
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||||
// to the network.
|
// to the network.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
||||||
.resolves('test-txid')
|
.resolves('test-txid')
|
||||||
|
|
||||||
req.body = {
|
req.body = {
|
||||||
@@ -403,33 +419,30 @@ describe('#Util', () => {
|
|||||||
balanceOnly: true
|
balanceOnly: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
const result = await utilRoute.sweepWif(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.isNumber(result)
|
assert.isNumber(result)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// Unit tests only
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
it('should generate transaction for valid BCH-only sweep', async () => {
|
it('should generate transaction for valid BCH-only sweep', async () => {
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
||||||
.resolves(mockData.mockBalance)
|
.resolves(mockData.mockBalance)
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
||||||
.resolves(mockData.mockUtxos)
|
.resolves(mockData.mockUtxos)
|
||||||
|
|
||||||
// Force token utxo to appear as regular BCH utxo.
|
// Force token utxo to appear as regular BCH utxo.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
||||||
.resolves([false, false])
|
.resolves([false, false])
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||||
// to the network.
|
// to the network.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
||||||
.resolves('test-txid')
|
.resolves('test-txid')
|
||||||
|
|
||||||
req.body = {
|
req.body = {
|
||||||
@@ -437,7 +450,7 @@ describe('#Util', () => {
|
|||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
const result = await utilRoute.sweepWif(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.equal(result, 'test-txid')
|
assert.equal(result, 'test-txid')
|
||||||
@@ -445,22 +458,22 @@ describe('#Util', () => {
|
|||||||
|
|
||||||
it('should throw 422 error if no non-token UTXOs', async () => {
|
it('should throw 422 error if no non-token UTXOs', async () => {
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
||||||
.resolves(mockData.mockBalance)
|
.resolves(mockData.mockBalance)
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
||||||
.resolves(mockData.mockUtxos)
|
.resolves(mockData.mockUtxos)
|
||||||
|
|
||||||
// Force token utxo to appear as regular BCH utxo.
|
// Force token utxo to appear as regular BCH utxo.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
||||||
.resolves(mockData.tokensOnly)
|
.resolves(mockData.tokensOnly)
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||||
// to the network.
|
// to the network.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
||||||
.resolves('test-txid')
|
.resolves('test-txid')
|
||||||
|
|
||||||
req.body = {
|
req.body = {
|
||||||
@@ -468,7 +481,7 @@ describe('#Util', () => {
|
|||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
const result = await utilRoute.sweepWif(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.equal(res.statusCode, 422)
|
assert.equal(res.statusCode, 422)
|
||||||
@@ -481,22 +494,22 @@ describe('#Util', () => {
|
|||||||
|
|
||||||
it('should detect and throw error for multiple token classes', async () => {
|
it('should detect and throw error for multiple token classes', async () => {
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
|
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
||||||
.resolves(mockData.mockBalance)
|
.resolves(mockData.mockBalance)
|
||||||
|
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
|
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
||||||
.resolves(mockData.mockThreeUtxos)
|
.resolves(mockData.mockThreeUtxos)
|
||||||
|
|
||||||
// Force token utxo to appear as regular BCH utxo.
|
// Force token utxo to appear as regular BCH utxo.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
||||||
.resolves(mockData.multipleTokens)
|
.resolves(mockData.multipleTokens)
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
||||||
// to the network.
|
// to the network.
|
||||||
sandbox
|
sandbox
|
||||||
.stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
|
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
||||||
.resolves('test-txid')
|
.resolves('test-txid')
|
||||||
|
|
||||||
req.body = {
|
req.body = {
|
||||||
@@ -504,7 +517,7 @@ describe('#Util', () => {
|
|||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
const result = await utilRoute.sweepWif(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.equal(res.statusCode, 422)
|
assert.equal(res.statusCode, 422)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
TESTS FOR THE bcash/slp.js LIBRARY
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const assert = chai.assert
|
||||||
|
|
||||||
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
const BcashSlp = require('../../src/routes/v5/bcash/slp')
|
||||||
|
|
||||||
|
// Mocking data.
|
||||||
|
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
||||||
|
const mockData = require('./mocks/bcash-slp-mock')
|
||||||
|
|
||||||
|
// Used for debugging.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
if (!process.env.BCASH_SERVER) process.env.BCASH_SERVER = 'http://localhost'
|
||||||
|
|
||||||
|
describe('#bcash-slp', () => {
|
||||||
|
let req, res
|
||||||
|
let sandbox
|
||||||
|
const bcashSlp = new BcashSlp()
|
||||||
|
// let electrumxRoute
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
if (!process.env.TEST) {
|
||||||
|
process.env.TEST = 'unit'
|
||||||
|
}
|
||||||
|
console.log(`Testing type is: ${process.env.TEST}`)
|
||||||
|
|
||||||
|
if (!process.env.NETWORK) process.env.NETWORK = 'testnet'
|
||||||
|
|
||||||
|
// Connect to electrumx servers if this is an integration test.
|
||||||
|
// if (process.env.TEST === 'integration') {
|
||||||
|
// await electrumxRoute.connect()
|
||||||
|
// console.log('Connected to ElectrumX server')
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
// console.log(`electrumxRoute.electrumx: `, electrumxRoute.electrumx)
|
||||||
|
// Disconnect from the electrumx server if this is an integration test.
|
||||||
|
// if (process.env.TEST === 'integration') {
|
||||||
|
// await electrumxRoute.disconnect()
|
||||||
|
// console.log('Disconnected from ElectrumX server')
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup the mocks before each test.
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the req and res objects used by Express routes.
|
||||||
|
req = mockReq
|
||||||
|
res = mockRes
|
||||||
|
|
||||||
|
// Explicitly reset the parmas and body.
|
||||||
|
req.params = {}
|
||||||
|
req.body = {}
|
||||||
|
req.query = {}
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
|
||||||
|
// electrumxRoute = new ElecrumxRoute()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#root', () => {
|
||||||
|
// root route handler.
|
||||||
|
const root = bcashSlp.root
|
||||||
|
|
||||||
|
it('should respond to GET for base route', async () => {
|
||||||
|
const result = root(req, res)
|
||||||
|
|
||||||
|
assert.equal(result.status, 'bcash-slp', 'Returns static string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getUtxos', () => {
|
||||||
|
it('should return UTXOs for an address, hydrated with SLP info', async () => {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.utxos })
|
||||||
|
sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo)
|
||||||
|
req.params.address =
|
||||||
|
'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
|
||||||
|
const utxos = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.isArray(utxos)
|
||||||
|
const utxo = utxos[0]
|
||||||
|
|
||||||
|
assert.property(utxo, 'version')
|
||||||
|
assert.property(utxo, 'height')
|
||||||
|
assert.property(utxo, 'value')
|
||||||
|
assert.property(utxo, 'script')
|
||||||
|
assert.property(utxo, 'address')
|
||||||
|
assert.property(utxo, 'coinbase')
|
||||||
|
assert.property(utxo, 'hash')
|
||||||
|
assert.property(utxo, 'index')
|
||||||
|
assert.property(utxo, 'slp')
|
||||||
|
|
||||||
|
assert.property(utxo.slp, 'tokenId')
|
||||||
|
assert.property(utxo.slp, 'ticker')
|
||||||
|
assert.property(utxo.slp, 'name')
|
||||||
|
assert.property(utxo.slp, 'uri')
|
||||||
|
assert.property(utxo.slp, 'hash')
|
||||||
|
assert.property(utxo.slp, 'decimals')
|
||||||
|
assert.property(utxo.slp, 'vout')
|
||||||
|
assert.property(utxo.slp, 'value')
|
||||||
|
assert.property(utxo.slp, 'type')
|
||||||
|
})
|
||||||
|
it('should handle error if address is array', async () => {
|
||||||
|
req.params.address = [
|
||||||
|
'bchtest:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'address can not be an array. Use POST for bulk upload.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should throw error if address has invalid format', async () => {
|
||||||
|
sandbox.stub(bcashSlp.routeUtils, 'validateNetwork').returns(false)
|
||||||
|
|
||||||
|
req.params.address = 'qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
|
||||||
|
const result = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should catch axios error', async () => {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error'))
|
||||||
|
req.params.address =
|
||||||
|
'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
const result = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.include(result.error, 'test error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#hydrateUTXOS', () => {
|
||||||
|
it('should hydrate slp utxos', async () => {
|
||||||
|
sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo)
|
||||||
|
|
||||||
|
const hydratedUtxos = await bcashSlp.hydrateUTXOS(mockData.utxos)
|
||||||
|
assert.isArray(hydratedUtxos)
|
||||||
|
const hydratedUtxo = hydratedUtxos[0]
|
||||||
|
assert.property(hydratedUtxo, 'version')
|
||||||
|
assert.property(hydratedUtxo, 'height')
|
||||||
|
assert.property(hydratedUtxo, 'value')
|
||||||
|
assert.property(hydratedUtxo, 'script')
|
||||||
|
assert.property(hydratedUtxo, 'address')
|
||||||
|
assert.property(hydratedUtxo, 'coinbase')
|
||||||
|
assert.property(hydratedUtxo, 'hash')
|
||||||
|
assert.property(hydratedUtxo, 'index')
|
||||||
|
assert.property(hydratedUtxo, 'slp')
|
||||||
|
|
||||||
|
assert.property(hydratedUtxo.slp, 'tokenId')
|
||||||
|
assert.property(hydratedUtxo.slp, 'ticker')
|
||||||
|
assert.property(hydratedUtxo.slp, 'name')
|
||||||
|
assert.property(hydratedUtxo.slp, 'uri')
|
||||||
|
assert.property(hydratedUtxo.slp, 'hash')
|
||||||
|
assert.property(hydratedUtxo.slp, 'decimals')
|
||||||
|
assert.property(hydratedUtxo.slp, 'vout')
|
||||||
|
assert.property(hydratedUtxo.slp, 'value')
|
||||||
|
assert.property(hydratedUtxo.slp, 'type')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if utxos is not provided', async () => {
|
||||||
|
try {
|
||||||
|
await bcashSlp.hydrateUTXOS()
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'UTXOs must be an array of slp utxos')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should handle error', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(bcashSlp, 'getTokenInfo').throws(new Error('test error'))
|
||||||
|
|
||||||
|
await bcashSlp.getTokenInfo(mockData.utxos)
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTokenInfo', () => {
|
||||||
|
it('should return SLP info for an token id', async () => {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.tokenInfo })
|
||||||
|
|
||||||
|
const tokenId =
|
||||||
|
'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900'
|
||||||
|
const slpInfo = await bcashSlp.getTokenInfo(tokenId)
|
||||||
|
assert.isObject(slpInfo)
|
||||||
|
assert.property(slpInfo, 'tokenId')
|
||||||
|
assert.property(slpInfo, 'ticker')
|
||||||
|
assert.property(slpInfo, 'name')
|
||||||
|
assert.property(slpInfo, 'uri')
|
||||||
|
assert.property(slpInfo, 'hash')
|
||||||
|
assert.property(slpInfo, 'decimals')
|
||||||
|
})
|
||||||
|
it('should throw error if token id is not provided', async () => {
|
||||||
|
try {
|
||||||
|
await bcashSlp.getTokenInfo()
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'tokenId must be string')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should catch axios error', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error'))
|
||||||
|
const tokenId =
|
||||||
|
'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900'
|
||||||
|
await bcashSlp.getTokenInfo(tokenId)
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('#errorHandler', () => {
|
||||||
|
it('should handle unexpected errors', () => {
|
||||||
|
sandbox.stub(bcashSlp.routeUtils, 'decodeError').returns({ msg: false })
|
||||||
|
|
||||||
|
const result = bcashSlp.errorHandler(new Error('test error'), res)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
||||||
|
assert.property(result, 'error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,7 +12,7 @@ const chai = require('chai')
|
|||||||
const assert = chai.assert
|
const assert = chai.assert
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
|
||||||
const Blockchain = require('../../src/routes/v3/full-node/blockchain')
|
const Blockchain = require('../../src/routes/v5/full-node/blockchain')
|
||||||
const uut = new Blockchain()
|
const uut = new Blockchain()
|
||||||
|
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
@@ -1823,4 +1823,204 @@ describe('#BlockchainRouter', () => {
|
|||||||
assert.equal(result.length, 2)
|
assert.equal(result.length, 2)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#getBlock()', () => {
|
||||||
|
it('returns proper error when downstream service stalls', async () => {
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700'
|
||||||
|
|
||||||
|
const result = await uut.getBlock(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(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c1e000000067139f230701a2819a76795564bd2f67ded7eeae68596f368eddb3dd5bc54e59320e896f71d61cfc3ae3d4a90fca08b1aa35ba91256d8939d2cad11e638c0081f66724abdef55cf7b8b9fed064bce0369171434f8b289c1330ccef765f8e97a2c0d794d81aafb535855f7daa6bb51e40f77c6b59d7af7f62d0eb726a4fc4df82353d56fcbda7c7ea6bd935d61af8fb3b295637e6f323b10231135b3f10a034cfb238f635830c0595e52c6c31247cf677b555f7a287076e20cd0e1d3cc9af7260f02b700'
|
||||||
|
const result = await uut.getBlock(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 throw 400 if blockhash is empty', async () => {
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error'])
|
||||||
|
assert.include(result.error, 'blockhash can not be empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 0', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity0 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 0
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.isString(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 1', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 1
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info with verbosity 2', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
req.body.verbosity = 2
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return block info without verbosity especified', async () => {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockBlockInfo.verbosity1 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.blockhash =
|
||||||
|
'0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536'
|
||||||
|
|
||||||
|
const result = await uut.getBlock(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'hash', 'hash property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'confirmations',
|
||||||
|
'confirmations property expected'
|
||||||
|
)
|
||||||
|
assert.property(result, 'size', 'size property expected')
|
||||||
|
assert.property(result, 'height', 'height property expected')
|
||||||
|
assert.property(result, 'version', 'version property expected')
|
||||||
|
assert.property(result, 'versionHex', 'versionHex property expected')
|
||||||
|
assert.property(result, 'merkleroot', 'merkleroot property expected')
|
||||||
|
assert.property(result, 'tx', 'tx property expected')
|
||||||
|
assert.property(result, 'time', 'time property expected')
|
||||||
|
assert.property(result, 'mediantime', 'mediantime property expected')
|
||||||
|
assert.property(result, 'nonce', 'nonce property expected')
|
||||||
|
assert.property(result, 'bits', 'bits property expected')
|
||||||
|
assert.property(result, 'difficulty', 'difficulty property expected')
|
||||||
|
assert.property(result, 'chainwork', 'chainwork property expected')
|
||||||
|
assert.property(result, 'nTx', 'nTx property expected')
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'previousblockhash',
|
||||||
|
'previousblockhash property expected'
|
||||||
|
)
|
||||||
|
assert.property(
|
||||||
|
result,
|
||||||
|
'nextblockhash',
|
||||||
|
'nextblockhash property expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const assert = chai.assert
|
const assert = chai.assert
|
||||||
const ControlRoute = require('../../src/routes/v3/full-node/control')
|
const ControlRoute = require('../../src/routes/v5/full-node/control')
|
||||||
const uut = new ControlRoute()
|
const uut = new ControlRoute()
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
|
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
TESTS FOR THE DSPROOF.JS LIBRARY
|
||||||
|
|
||||||
|
This test file uses the environment variable TEST to switch between unit
|
||||||
|
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
||||||
|
to 'integration' to run the tests against BCH mainnet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const assert = chai.assert
|
||||||
|
const DSProofRoute = require('../../src/routes/v5/full-node/dsproof')
|
||||||
|
const uut = new DSProofRoute()
|
||||||
|
|
||||||
|
// const nock = require('nock') // HTTP mocking
|
||||||
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
let originalEnvVars // Used during transition from integration to unit tests.
|
||||||
|
|
||||||
|
// Mocking data.
|
||||||
|
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
||||||
|
const mockData = require('./mocks/dsproof-mocks')
|
||||||
|
|
||||||
|
// Used for debugging.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
describe('#DSProof', () => {
|
||||||
|
let req, res
|
||||||
|
let sandbox
|
||||||
|
before(() => {
|
||||||
|
// Save existing environment variables.
|
||||||
|
originalEnvVars = {
|
||||||
|
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
||||||
|
RPC_BASEURL: process.env.RPC_BASEURL,
|
||||||
|
RPC_USERNAME: process.env.RPC_USERNAME,
|
||||||
|
RPC_PASSWORD: process.env.RPC_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default environment variables for unit tests.
|
||||||
|
if (!process.env.TEST) process.env.TEST = 'unit'
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
|
||||||
|
process.env.RPC_BASEURL = 'http://fakeurl/api'
|
||||||
|
process.env.RPC_USERNAME = 'fakeusername'
|
||||||
|
process.env.RPC_PASSWORD = 'fakepassword'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup the mocks before each test.
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the req and res objects used by Express routes.
|
||||||
|
req = mockReq
|
||||||
|
res = mockRes
|
||||||
|
|
||||||
|
// Explicitly reset the parmas and body.
|
||||||
|
req.params = {}
|
||||||
|
req.body = {}
|
||||||
|
req.query = {}
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore Sandbox
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
// Restore any pre-existing environment variables.
|
||||||
|
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
|
||||||
|
process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL
|
||||||
|
process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME
|
||||||
|
process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#root', async () => {
|
||||||
|
// root route handler.
|
||||||
|
it('should respond to GET for base route', async () => {
|
||||||
|
const result = uut.root(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.equal(result.status, 'dsproof', 'Returns static string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getDSProof', async () => {
|
||||||
|
it('should throw 400 error if txid is missing', async () => {
|
||||||
|
const result = await uut.getDSProof(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
assert.include(result.error, 'txid can not be empty')
|
||||||
|
})
|
||||||
|
it('should throw 400 error if txid is invalid', async () => {
|
||||||
|
req.params.txid = 'abc123'
|
||||||
|
|
||||||
|
const result = await uut.getDSProof(req, res)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
|
||||||
|
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
||||||
|
assert.include(result.error, 'txid must be of length 64')
|
||||||
|
})
|
||||||
|
it('should throw 503 when network issues', async () => {
|
||||||
|
req.params.txid =
|
||||||
|
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
|
||||||
|
|
||||||
|
// Save the existing RPC URL.
|
||||||
|
const savedUrl2 = process.env.RPC_BASEURL
|
||||||
|
|
||||||
|
// Manipulate the URL to cause a 500 network error.
|
||||||
|
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
||||||
|
|
||||||
|
await uut.getDSProof(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
// Restore the saved URL.
|
||||||
|
process.env.RPC_BASEURL = savedUrl2
|
||||||
|
|
||||||
|
assert.isAbove(
|
||||||
|
res.statusCode,
|
||||||
|
499,
|
||||||
|
'HTTP status code 500 or greater expected.'
|
||||||
|
)
|
||||||
|
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
|
||||||
|
})
|
||||||
|
it('returns proper error when downstream service stalls', async () => {
|
||||||
|
req.params.txid =
|
||||||
|
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
const result = await uut.getDSProof(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 () => {
|
||||||
|
req.params.txid =
|
||||||
|
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
const result = await uut.getDSProof(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 GET double-spend proof', async function () {
|
||||||
|
req.params.txid =
|
||||||
|
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox
|
||||||
|
.stub(uut.axios, 'request')
|
||||||
|
.resolves({ data: { result: mockData.mockGetDsProof } })
|
||||||
|
} else {
|
||||||
|
return this.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await uut.getDSProof(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'dspid')
|
||||||
|
assert.property(result, 'txid')
|
||||||
|
assert.property(result, 'outpoint')
|
||||||
|
assert.property(result, 'path')
|
||||||
|
assert.property(result, 'descendants')
|
||||||
|
|
||||||
|
assert.property(result.outpoint, 'txid')
|
||||||
|
assert.property(result.outpoint, 'vout')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,7 +19,7 @@ const sinon = require('sinon')
|
|||||||
if (!process.env.TEST) process.env.TEST = 'unit'
|
if (!process.env.TEST) process.env.TEST = 'unit'
|
||||||
|
|
||||||
// Only load blockbook library after setting BLOCKBOOK_URL env var.
|
// Only load blockbook library after setting BLOCKBOOK_URL env var.
|
||||||
const EncryptionRoute = require('../../src/routes/v3/encryption')
|
const EncryptionRoute = require('../../src/routes/v5/encryption')
|
||||||
const encryptionRoute = new EncryptionRoute()
|
const encryptionRoute = new EncryptionRoute()
|
||||||
|
|
||||||
// Mocking data.
|
// Mocking data.
|
||||||
@@ -82,7 +82,7 @@ describe('#Encryption Router', () => {
|
|||||||
.resolves(mockData.mockFulcrumTxHistory)
|
.resolves(mockData.mockFulcrumTxHistory)
|
||||||
sandbox
|
sandbox
|
||||||
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
||||||
.resolves(mockData.mockTxDetails2)
|
.resolves([mockData.mockTxDetails2])
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
const result = await encryptionRoute.getPublicKey(req, res)
|
||||||
@@ -110,7 +110,7 @@ describe('#Encryption Router', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
const result = await encryptionRoute.getPublicKey(req, res)
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
assert.property(result, 'success')
|
assert.property(result, 'success')
|
||||||
assert.equal(result.success, false)
|
assert.equal(result.success, false)
|
||||||
@@ -130,7 +130,7 @@ describe('#Encryption Router', () => {
|
|||||||
.resolves(mockData.mockFulcrumNoSendBalance)
|
.resolves(mockData.mockFulcrumNoSendBalance)
|
||||||
sandbox
|
sandbox
|
||||||
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
||||||
.resolves(mockData.mockNoSendTx)
|
.resolves([mockData.mockNoSendTx])
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
const result = await encryptionRoute.getPublicKey(req, res)
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
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 return error on non-existing NFT group token', async () => {
|
||||||
|
req.params.tokenId =
|
||||||
|
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
|
||||||
|
const result = await slp.getNftChildren(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'NFT group does not exists',
|
||||||
|
'Error message expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should return error on non-group NFT token', async () => {
|
||||||
|
req.params.tokenId =
|
||||||
|
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
||||||
|
|
||||||
|
const result = await slp.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 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 return error on non-existing NFT child token', async () => {
|
||||||
|
req.params.tokenId =
|
||||||
|
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
|
||||||
|
const result = await slp.getNftGroup(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'NFT child does not exists',
|
||||||
|
'Error message expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,7 +11,7 @@ const assert = require('chai').assert
|
|||||||
const util = require('util')
|
const util = require('util')
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
const Price = require('../../../src/routes/v3/price')
|
const Price = require('../../../src/routes/v4/price')
|
||||||
const price = new Price()
|
const price = new Price()
|
||||||
|
|
||||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
||||||
@@ -39,6 +39,14 @@ describe('#price', () => {
|
|||||||
const result = await price.getBCHAUSD(req, res)
|
const result = await price.getBCHAUSD(req, res)
|
||||||
console.log(`result: ${util.inspect(result)}`)
|
console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.isNumber(result.usd)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('#getBCHUSD', () => {
|
||||||
|
it('should get the USD price of BCH', async () => {
|
||||||
|
const result = await price.getBCHUSD(req, res)
|
||||||
|
console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
assert.isNumber(result.usd)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -13,9 +13,9 @@ const axios = require('axios')
|
|||||||
const util = require('util')
|
const util = require('util')
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
// const SERVER = `http://192.168.0.36:12400/v3/`
|
// const SERVER = `http://192.168.0.36:12400/v4/`
|
||||||
const SERVER = 'http://localhost:3000/v3/'
|
const SERVER = 'http://localhost:3000/v4/'
|
||||||
// const SERVER = 'https://api.fullstack.cash/v3/'
|
// const SERVER = 'https://api.fullstack.cash/v4/'
|
||||||
//
|
//
|
||||||
// const TEST_JWT =
|
// const TEST_JWT =
|
||||||
// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk'
|
// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk'
|
||||||
@@ -7,21 +7,22 @@
|
|||||||
TODO:
|
TODO:
|
||||||
-/rawtransactions/sendRawTransaction is more appropropriate for an e2e test,
|
-/rawtransactions/sendRawTransaction is more appropropriate for an e2e test,
|
||||||
so it is omitted here.
|
so it is omitted here.
|
||||||
|
- Replace request-promise with axios.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const chai = require('chai')
|
// const chai = require('chai')
|
||||||
const assert = chai.assert
|
// const assert = chai.assert
|
||||||
// const rawtransactions = require('../../../dist/routes/v2/rawtransactions')
|
// const rawtransactions = require('../../../dist/routes/v2/rawtransactions')
|
||||||
const rp = require('request-promise')
|
// const rp = require('request-promise')
|
||||||
|
|
||||||
// Used for debugging.
|
// Used for debugging.
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
// const mockData = require('../mocks/raw-transactions-mocks')
|
// const mockData = require('../mocks/raw-transactions-mocks')
|
||||||
|
/*
|
||||||
describe('#Raw-Transactions', () => {
|
describe('#Raw-Transactions', () => {
|
||||||
describe('#root', () => {
|
describe('#root', () => {
|
||||||
it('should return root', async () => {
|
it('should return root', async () => {
|
||||||
@@ -189,3 +190,4 @@ describe('#Raw-Transactions', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
@@ -19,7 +19,7 @@ if (!process.env.SLPDB_URL) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SLP = require('../../../src/routes/v3/slp')
|
const SLP = require('../../../src/routes/v4/slp')
|
||||||
const slp = new SLP()
|
const slp = new SLP()
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
const BCHJS = require('@psf/bch-js')
|
||||||
@@ -110,8 +110,8 @@ describe('#slp', () => {
|
|||||||
req.body.tokenUtxos = [
|
req.body.tokenUtxos = [
|
||||||
{
|
{
|
||||||
tokenId:
|
tokenId:
|
||||||
'0a321bff9761f28e06a268b14711274bb77617410a16807bd0437ef234a072b1',
|
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
||||||
decimals: 0,
|
decimals: 8,
|
||||||
tokenQty: 2
|
tokenQty: 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -214,6 +214,39 @@ describe('#slp', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#hydrateUtxosWL', () => {
|
||||||
|
it('should return utxo details', async () => {
|
||||||
|
const utxos = [
|
||||||
|
{
|
||||||
|
utxos: [
|
||||||
|
{
|
||||||
|
tx_hash:
|
||||||
|
'89b3f0c84efe8b01b24e2d7ac08636de5781f31dbb84478e3de868ca0a7ed93a',
|
||||||
|
tx_pos: 1,
|
||||||
|
value: 546
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
req.body.utxos = utxos
|
||||||
|
const result = await slp.hydrateUtxosWL(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
// Test the general structure of the output.
|
||||||
|
assert.isArray(result.slpUtxos)
|
||||||
|
assert.equal(result.slpUtxos.length, 1)
|
||||||
|
assert.equal(result.slpUtxos[0].utxos.length, 1)
|
||||||
|
|
||||||
|
// Test the non-slp UTXO.
|
||||||
|
assert.property(result.slpUtxos[0].utxos[0], 'tx_hash')
|
||||||
|
assert.property(result.slpUtxos[0].utxos[0], 'tx_pos')
|
||||||
|
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||||
|
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||||
|
assert.equal(result.slpUtxos[0].utxos[0].isValid, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('#validate2Single', () => {
|
describe('#validate2Single', () => {
|
||||||
it('should invalidate a known invalid TXID', async () => {
|
it('should invalidate a known invalid TXID', async () => {
|
||||||
const txid =
|
const txid =
|
||||||
@@ -258,4 +291,171 @@ describe('#slp', () => {
|
|||||||
// )
|
// )
|
||||||
// })
|
// })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#validateBulk', () => {
|
||||||
|
it('should handle a mix of valid, invalid, and non-SLP txs', async () => {
|
||||||
|
const txids = [
|
||||||
|
// Malformed SLP tx
|
||||||
|
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
|
||||||
|
// Normal TX (non-SLP)
|
||||||
|
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
|
||||||
|
// Valid PSF SLP tx
|
||||||
|
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
|
||||||
|
// Valid SLP token not in whitelist
|
||||||
|
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
|
||||||
|
// Token send on BCHN network.
|
||||||
|
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019',
|
||||||
|
// Token send on ABC network.
|
||||||
|
'336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d',
|
||||||
|
// Known invalid SLP token send of PSF tokens.
|
||||||
|
'2bf691ad3679d928fef880b8a45b93b233f8fa0d0a92cf792313dbe77b1deb74'
|
||||||
|
]
|
||||||
|
|
||||||
|
req.body.txids = txids
|
||||||
|
const result = await slp.validateBulk(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
// BCHN expected results
|
||||||
|
if (process.env.ISBCHN) {
|
||||||
|
assert.equal(result[0].txid, txids[0])
|
||||||
|
assert.equal(result[0].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[1].txid, txids[1])
|
||||||
|
assert.equal(result[1].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[2].txid, txids[2])
|
||||||
|
assert.equal(result[2].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[3].txid, txids[3])
|
||||||
|
assert.equal(result[3].valid, true)
|
||||||
|
|
||||||
|
// Note: This should change from null to true once SLPDB finishes indexing.
|
||||||
|
assert.equal(result[4].txid, txids[4])
|
||||||
|
assert.equal(result[4].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[5].txid, txids[5])
|
||||||
|
assert.equal(result[5].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[6].txid, txids[6])
|
||||||
|
assert.equal(result[6].valid, false)
|
||||||
|
assert.include(
|
||||||
|
result[6].invalidReason,
|
||||||
|
'Token outputs are greater than valid token inputs'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert.equal(result[0].txid, txids[0])
|
||||||
|
assert.equal(result[0].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[1].txid, txids[1])
|
||||||
|
assert.equal(result[1].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[2].txid, txids[2])
|
||||||
|
assert.equal(result[2].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[3].txid, txids[3])
|
||||||
|
assert.equal(result[3].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[4].txid, txids[4])
|
||||||
|
assert.equal(result[4].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[5].txid, txids[5])
|
||||||
|
assert.equal(result[5].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[6].txid, txids[6])
|
||||||
|
assert.equal(result[6].valid, false)
|
||||||
|
assert.include(
|
||||||
|
result[6].invalidReason,
|
||||||
|
'Token outputs are greater than valid token inputs'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#validate3Bulk', () => {
|
||||||
|
it('should handle a mix of valid, invalid, and non-SLP txs', async () => {
|
||||||
|
const txids = [
|
||||||
|
// Malformed SLP tx
|
||||||
|
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
|
||||||
|
// Normal TX (non-SLP)
|
||||||
|
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
|
||||||
|
// Valid PSF SLP tx
|
||||||
|
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
|
||||||
|
// Valid SLP token not in whitelist
|
||||||
|
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
|
||||||
|
// Token send on BCHN network.
|
||||||
|
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019',
|
||||||
|
// Token send on ABC network.
|
||||||
|
'336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d',
|
||||||
|
// Known invalid SLP token send of PSF tokens.
|
||||||
|
'2bf691ad3679d928fef880b8a45b93b233f8fa0d0a92cf792313dbe77b1deb74'
|
||||||
|
]
|
||||||
|
|
||||||
|
req.body.txids = txids
|
||||||
|
const result = await slp.validate3Bulk(req, res)
|
||||||
|
console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
// BCHN expected results
|
||||||
|
if (process.env.ISBCHN) {
|
||||||
|
assert.equal(result[0].txid, txids[0])
|
||||||
|
assert.equal(result[0].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[1].txid, txids[1])
|
||||||
|
assert.equal(result[1].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[2].txid, txids[2])
|
||||||
|
assert.equal(result[2].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[3].txid, txids[3])
|
||||||
|
assert.equal(result[3].valid, null)
|
||||||
|
|
||||||
|
// Note: This should change from null to true once SLPDB finishes indexing.
|
||||||
|
assert.equal(result[4].txid, txids[4])
|
||||||
|
assert.equal(result[4].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[5].txid, txids[5])
|
||||||
|
assert.equal(result[5].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[6].txid, txids[6])
|
||||||
|
assert.equal(result[6].valid, false)
|
||||||
|
assert.include(
|
||||||
|
result[6].invalidReason,
|
||||||
|
'Token outputs are greater than valid token inputs'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert.equal(result[0].txid, txids[0])
|
||||||
|
assert.equal(result[0].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[1].txid, txids[1])
|
||||||
|
assert.equal(result[1].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[2].txid, txids[2])
|
||||||
|
assert.equal(result[2].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[3].txid, txids[3])
|
||||||
|
assert.equal(result[3].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[4].txid, txids[4])
|
||||||
|
assert.equal(result[4].valid, null)
|
||||||
|
|
||||||
|
assert.equal(result[5].txid, txids[5])
|
||||||
|
assert.equal(result[5].valid, true)
|
||||||
|
|
||||||
|
assert.equal(result[6].txid, txids[6])
|
||||||
|
assert.equal(result[6].valid, false)
|
||||||
|
assert.include(
|
||||||
|
result[6].invalidReason,
|
||||||
|
'Token outputs are greater than valid token inputs'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getStatus', () => {
|
||||||
|
it('should get the SLPDB status', async () => {
|
||||||
|
const result = await slp.getStatus(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'bchBlockHeight')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -18,7 +18,7 @@ if (!process.env.SLPDB_URL) {
|
|||||||
throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.')
|
throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const SLPDB = require('../../../src/routes/v3/services/slpdb')
|
const SLPDB = require('../../../src/routes/v4/services/slpdb')
|
||||||
const slpdb = new SLPDB()
|
const slpdb = new SLPDB()
|
||||||
|
|
||||||
describe('#slpdb', () => {
|
describe('#slpdb', () => {
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
TESTS FOR THE JWT.JS LIBRARY
|
||||||
|
|
||||||
|
This test file uses the environment variable TEST to switch between unit
|
||||||
|
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
||||||
|
to 'integration' to run the tests against BCH mainnet.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const assert = chai.assert
|
||||||
|
const JWTRoute = require('../../src/routes/v5/jwt')
|
||||||
|
const uut = new JWTRoute()
|
||||||
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
// Mocking data.
|
||||||
|
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
||||||
|
// const mockData = require('./mocks/control-mock')
|
||||||
|
|
||||||
|
// Used for debugging.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
describe('#JWTRouter', () => {
|
||||||
|
let req, res
|
||||||
|
let sandbox
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
// Set default environment variables for unit tests.
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup the mocks before each test.
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the req and res objects used by Express routes.
|
||||||
|
req = mockReq
|
||||||
|
res = mockRes
|
||||||
|
|
||||||
|
// Explicitly reset the parmas and body.
|
||||||
|
req.params = {}
|
||||||
|
req.body = {}
|
||||||
|
req.query = {}
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore Sandbox
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {})
|
||||||
|
|
||||||
|
describe('#root', async () => {
|
||||||
|
// root route handler.
|
||||||
|
// const root = controlRoute.testableComponents.root
|
||||||
|
|
||||||
|
it('should respond to GET for base route', async () => {
|
||||||
|
const result = uut.root(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.equal(result.status, 'jwt', 'Returns static string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#jwtInfo', () => {
|
||||||
|
it('should decode the JWT token', async () => {
|
||||||
|
const jwtToken =
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZWM3NTY1YTM4ZjkyMjdlOWVjNTM3MCIsImVtYWlsIjoidGVzdHVzZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjAsInJhdGVMaW1pdCI6MywicG9pbnRzVG9Db25zdW1lIjo1MDAsImR1cmF0aW9uIjozMCwiaWF0IjoxNjI2MTA5MzQ4LCJleHAiOjE2Mjg3MDEzNDh9.hF8BKU-1SvlvQBnTNbB65ErQTtNSl-pWlRANSJY-Zb4'
|
||||||
|
req.body.jwt = jwtToken
|
||||||
|
|
||||||
|
const result = await uut.jwtInfo(req, res)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.equal(result.error, 'jwt expired')
|
||||||
|
|
||||||
|
// assert.property(result, 'id')
|
||||||
|
// assert.property(result, 'email')
|
||||||
|
// assert.property(result, 'apiLevel')
|
||||||
|
// assert.property(result, 'rateLimit')
|
||||||
|
// assert.property(result, 'pointsToConsume')
|
||||||
|
// assert.property(result, 'duration')
|
||||||
|
// assert.property(result, 'iat')
|
||||||
|
// assert.property(result, 'exp')
|
||||||
|
// assert.property(result, 'expiration')
|
||||||
|
// assert.property(result, 'createdAt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return an error with malformed JWT token', async () => {
|
||||||
|
const jwtToken =
|
||||||
|
'eyJhbGciOiJIUzI1NiLsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwZWM3NTY1YTM4ZjkyMjdlOWVjNTM3MCIsImVtYWlsIjoidGVzdHVzZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjAsInJhdGVMaW1pdCI6MywicG9pbnRzVG9Db25zdW1lIjo1MDAsImR1cmF0aW9uIjozMCwiaWF0IjoxNjI2MTA5MzQ4LCJleHAiOjE2Mjg3MDEzNDh9.hF8BKU-1SvlvQBnTNbB65ErQTtNSl-pWlRANSJY-Zb4'
|
||||||
|
req.body.jwt = jwtToken
|
||||||
|
|
||||||
|
const result = await uut.jwtInfo(req, res)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
|
||||||
|
assert.equal(result.error, 'invalid token')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const assert = chai.assert
|
const assert = chai.assert
|
||||||
const MiningRoute = require('../../src/routes/v3/full-node/mining')
|
const MiningRoute = require('../../src/routes/v5/full-node/mining')
|
||||||
const uut = new MiningRoute()
|
const uut = new MiningRoute()
|
||||||
|
|
||||||
// const nock = require('nock') // HTTP mocking
|
// const nock = require('nock') // HTTP mocking
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
This library contains mocking data for running unit tests on the bcash route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const tokenInfo = {
|
||||||
|
tokenId: 'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84',
|
||||||
|
ticker: 'PSI',
|
||||||
|
name: 'Psidium',
|
||||||
|
uri: '',
|
||||||
|
hash: '',
|
||||||
|
decimals: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
const utxos = [
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
height: 707135,
|
||||||
|
value: 546,
|
||||||
|
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
|
||||||
|
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
|
||||||
|
coinbase: false,
|
||||||
|
hash: '77e514d59c0c866e791d08ddab73a1f7827be5f1ca15c1e05515dac9b271b596',
|
||||||
|
index: 2,
|
||||||
|
slp: {
|
||||||
|
vout: 2,
|
||||||
|
tokenId:
|
||||||
|
'5f2914840d0fd82689bdf0a0f8194ea363b9d0e2bd69b01608e18e1c6a4b9861',
|
||||||
|
value: '9999777700000',
|
||||||
|
type: 'SEND'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
height: 707135,
|
||||||
|
value: 546,
|
||||||
|
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
|
||||||
|
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
|
||||||
|
coinbase: false,
|
||||||
|
hash: '350aab06bf9523c062692f62622cb28b18cff9e4aa92be5476143336da217e98',
|
||||||
|
index: 1,
|
||||||
|
slp: {
|
||||||
|
vout: 1,
|
||||||
|
tokenId:
|
||||||
|
'2d860662801067828be3c4f504ce31b2a1a36c2cdbc6d92f6160920f66b0a9ee',
|
||||||
|
value: '1',
|
||||||
|
type: 'SEND'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
height: 687335,
|
||||||
|
value: 546,
|
||||||
|
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
|
||||||
|
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
|
||||||
|
coinbase: false,
|
||||||
|
hash: 'b7a0f52a344b1169292c757a36c62dbdaaa4ad684e96837eec732a9a2033dfd1',
|
||||||
|
index: 1,
|
||||||
|
slp: {
|
||||||
|
vout: 1,
|
||||||
|
tokenId:
|
||||||
|
'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84',
|
||||||
|
value: '100000000',
|
||||||
|
type: 'SEND'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tokenInfo,
|
||||||
|
utxos
|
||||||
|
}
|
||||||
@@ -285,7 +285,86 @@ const mockAncestors = [
|
|||||||
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
||||||
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
||||||
]
|
]
|
||||||
|
const mockBlockInfo = {
|
||||||
|
verbosity0: '20000000',
|
||||||
|
verbosity1: {
|
||||||
|
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
confirmations: 1,
|
||||||
|
size: 3725,
|
||||||
|
strippedsize: 3725,
|
||||||
|
weight: 3725,
|
||||||
|
height: 6725,
|
||||||
|
version: 1,
|
||||||
|
versionHex: '00000000',
|
||||||
|
merkleroot: 'xxxx',
|
||||||
|
tx: [
|
||||||
|
'2afb8264508e2bf3e5288ccad01ed2ab766745b6b9747666b519d59212012c01',
|
||||||
|
'18f40b1ae56bba3fa1934b737fbe46ed8d5ca40fa9aed95073eeb5a119530cd3',
|
||||||
|
'349720d878547752607a69eb19e330592fee271fb5376cdfd811bee423558ed8',
|
||||||
|
'35571c80e7d0e9247b467454ef147d1d5833c775bc2d4164b1bebd4c1f69164f',
|
||||||
|
'480937e8efacdafeeb97d401ff0dd9ea8e8ddb27244cefa67a03621315bdb0e6',
|
||||||
|
'523327469d0b90c0de9a905c2fe6e227278fc5b55b9d9911ca151e1c26647065',
|
||||||
|
'61de4af971d94dbc741762f21dcef08b74b62863a56a1cb3496becdd8d47a858',
|
||||||
|
'69f70a288403b5bba23030ccd05d2e5cb00394620fdae3b050cff9432cc590ce',
|
||||||
|
'94472a90fbdba2eb2cba68308415181faedf3585c66b89efcbdb927a0c10ba23',
|
||||||
|
'bc6f781f9e2f2df460f89995c5e1c7224e48ccbe1aa2a575961f3f5330259864',
|
||||||
|
'f2d945a79bec5454a9ab4d570a150d81124daa308f1c81106a977d6413476944'
|
||||||
|
],
|
||||||
|
time: 111,
|
||||||
|
mediantime: 111,
|
||||||
|
nonce: 111,
|
||||||
|
bits: '1d00ffff',
|
||||||
|
difficulty: 99.999,
|
||||||
|
chainwork: 'xxxx',
|
||||||
|
nTx: 1,
|
||||||
|
previousblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
nextblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
||||||
|
},
|
||||||
|
verbosity2: {
|
||||||
|
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
confirmations: 1,
|
||||||
|
size: 3725,
|
||||||
|
strippedsize: 3725,
|
||||||
|
weight: 3725,
|
||||||
|
height: 6725,
|
||||||
|
version: 1,
|
||||||
|
versionHex: '00000000',
|
||||||
|
merkleroot: 'xxxx',
|
||||||
|
tx: [
|
||||||
|
{
|
||||||
|
hex:
|
||||||
|
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000',
|
||||||
|
txid:
|
||||||
|
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
||||||
|
hash:
|
||||||
|
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
||||||
|
size: 134,
|
||||||
|
version: 1,
|
||||||
|
locktime: 0,
|
||||||
|
vin: [],
|
||||||
|
vout: [],
|
||||||
|
blockhash:
|
||||||
|
'00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048',
|
||||||
|
confirmations: 581882,
|
||||||
|
time: 1231469665,
|
||||||
|
blocktime: 1231469665
|
||||||
|
}
|
||||||
|
],
|
||||||
|
time: 111,
|
||||||
|
mediantime: 111,
|
||||||
|
nonce: 111,
|
||||||
|
bits: '1d00ffff',
|
||||||
|
difficulty: 99.999,
|
||||||
|
chainwork: 'xxxx',
|
||||||
|
nTx: 1,
|
||||||
|
previousblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
||||||
|
nextblockhash:
|
||||||
|
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
||||||
|
}
|
||||||
|
}
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mockBlockHash,
|
mockBlockHash,
|
||||||
mockBlockchainInfo,
|
mockBlockchainInfo,
|
||||||
@@ -296,5 +375,6 @@ module.exports = {
|
|||||||
mockBlockHeader,
|
mockBlockHeader,
|
||||||
mockTxOut,
|
mockTxOut,
|
||||||
mockTxOutProof,
|
mockTxOutProof,
|
||||||
mockAncestors
|
mockAncestors,
|
||||||
|
mockBlockInfo
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
This library contains mocking data for running unit tests on the dsproof route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const mockGetDsProof = {
|
||||||
|
dspid: '6234fff2a9dbcaa50e2c50d74e1d725a2bed1757d7a05a2db7a82df659554397',
|
||||||
|
txid: 'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e',
|
||||||
|
outpoint: {
|
||||||
|
txid: '17ebef34f7e9a0692317dd6fbb43ffccff45b7d688a2a754dbccfc5e6d93ce3e',
|
||||||
|
vout: 1
|
||||||
|
},
|
||||||
|
path: ['ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'],
|
||||||
|
descendants: [
|
||||||
|
'ee0df780b58f6f24467605b2589c44c3a50fc849fb8f91b89669a4ae0d86bc7e'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mockGetDsProof
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
Mocking data for electrumx unit tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const utxos = [
|
||||||
|
{
|
||||||
|
height: 604392,
|
||||||
|
tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
|
||||||
|
tx_pos: 0,
|
||||||
|
value: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
height: 630834,
|
||||||
|
tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
|
||||||
|
tx_pos: 0,
|
||||||
|
value: 6000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const utxosArray = {
|
||||||
|
success: true,
|
||||||
|
utxos: [
|
||||||
|
{
|
||||||
|
utxos: [
|
||||||
|
{
|
||||||
|
height: 601861,
|
||||||
|
tx_hash:
|
||||||
|
'6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d',
|
||||||
|
tx_pos: 0,
|
||||||
|
value: 1000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
utxos: [
|
||||||
|
{
|
||||||
|
height: 604392,
|
||||||
|
tx_hash:
|
||||||
|
'7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
|
||||||
|
tx_pos: 0,
|
||||||
|
value: 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
height: 630834,
|
||||||
|
tx_hash:
|
||||||
|
'4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
|
||||||
|
tx_pos: 0,
|
||||||
|
value: 6000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = {
|
||||||
|
success: true,
|
||||||
|
balance: {
|
||||||
|
confirmed: 7000,
|
||||||
|
unconfirmed: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const balances = {
|
||||||
|
success: true,
|
||||||
|
balances: [
|
||||||
|
{
|
||||||
|
balance: { confirmed: 7000, unconfirmed: 0 },
|
||||||
|
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
balance: { confirmed: 7000, unconfirmed: 0 },
|
||||||
|
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
const txHistory = [
|
||||||
|
{
|
||||||
|
height: 601861,
|
||||||
|
tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const mempool = [
|
||||||
|
{
|
||||||
|
tx_hash: '45381031132c57b2ff1cbe8d8d3920cf9ed25efd9a0beb764bdb2f24c7d1c7e3',
|
||||||
|
height: 0,
|
||||||
|
fee: 24310
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const txDetails = {
|
||||||
|
blockhash: '0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc',
|
||||||
|
blocktime: 1578327094,
|
||||||
|
confirmations: 31861,
|
||||||
|
hash: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
|
||||||
|
hex:
|
||||||
|
'020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000',
|
||||||
|
locktime: 0,
|
||||||
|
size: 392,
|
||||||
|
time: 1578327094,
|
||||||
|
txid: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
|
||||||
|
version: 2,
|
||||||
|
vin: [
|
||||||
|
{
|
||||||
|
scriptSig: {
|
||||||
|
asm:
|
||||||
|
'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309',
|
||||||
|
hex:
|
||||||
|
'41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309'
|
||||||
|
},
|
||||||
|
sequence: 4294967295,
|
||||||
|
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
|
||||||
|
vout: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scriptSig: {
|
||||||
|
asm:
|
||||||
|
'347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954',
|
||||||
|
hex:
|
||||||
|
'41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954'
|
||||||
|
},
|
||||||
|
sequence: 4294967295,
|
||||||
|
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
|
||||||
|
vout: 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
vout: [
|
||||||
|
{
|
||||||
|
n: 0,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl'],
|
||||||
|
asm: 'OP_HASH160 189ce02e332548f4804bac65cba68202c9dbf822 OP_EQUAL',
|
||||||
|
hex: 'a914189ce02e332548f4804bac65cba68202c9dbf82287',
|
||||||
|
reqSigs: 1,
|
||||||
|
type: 'scripthash'
|
||||||
|
},
|
||||||
|
value: 0.0562057
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 1,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash: qq59hv6s3qdjrtyfwfxxldkuj9xsjmx48vrz882knz'],
|
||||||
|
asm:
|
||||||
|
'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG',
|
||||||
|
hex: '76a914285bb350881b21ac89724c6fb6dc914d096cd53b88ac',
|
||||||
|
reqSigs: 1,
|
||||||
|
type: 'pubkeyhash'
|
||||||
|
},
|
||||||
|
value: 0.00589197
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 2,
|
||||||
|
scriptPubKey: {
|
||||||
|
addresses: ['bitcoincash: qpzlruwy4xu5rxjs3z37nsj29y7h59gwvsu4ddp0u4'],
|
||||||
|
asm:
|
||||||
|
'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG',
|
||||||
|
hex: '76a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac',
|
||||||
|
reqSigs: 1,
|
||||||
|
type: 'pubkeyhash'
|
||||||
|
},
|
||||||
|
value: 0.03272697
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const txDetailsBulk = {
|
||||||
|
success: true,
|
||||||
|
transactions: [{ details: txDetails }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockHeaders = [
|
||||||
|
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
|
||||||
|
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
|
||||||
|
]
|
||||||
|
|
||||||
|
const blockHeadersBulk = {
|
||||||
|
success: true,
|
||||||
|
headers: [{ headers: blockHeaders }, { headers: blockHeaders }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const transactions = [
|
||||||
|
{
|
||||||
|
height: 603416,
|
||||||
|
tx_hash: 'eef683d236d88e978bd406419f144057af3fe1b62ef59162941c1a9f05ded62c'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
height: 646894,
|
||||||
|
tx_hash: '4c695fae636f3e8e2edc571d11756b880ccaae744390f3950d798ce7b5e25754'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const transactionsBulk = {
|
||||||
|
success: true,
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
transactions,
|
||||||
|
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transactions,
|
||||||
|
address: 'bitcoincash:qp3sn6vlwz28ntmf3wmyra7jqttfx7z6zgtkygjhc7'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
module.exports = {
|
||||||
|
utxos,
|
||||||
|
utxosArray,
|
||||||
|
balance,
|
||||||
|
txHistory,
|
||||||
|
mempool,
|
||||||
|
txDetails,
|
||||||
|
txDetailsBulk,
|
||||||
|
blockHeaders,
|
||||||
|
blockHeadersBulk,
|
||||||
|
balances,
|
||||||
|
transactions,
|
||||||
|
transactionsBulk
|
||||||
|
}
|
||||||
@@ -55,64 +55,68 @@ const mockFulcrumTxHistory = {
|
|||||||
success: true,
|
success: true,
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
height: 511463,
|
transactions: [
|
||||||
tx_hash:
|
{
|
||||||
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
|
height: 511463,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
|
||||||
height: 511464,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
|
height: 511464,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
|
||||||
height: 513373,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
|
height: 513373,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
|
||||||
height: 513373,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
|
height: 513373,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
|
||||||
height: 560481,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
|
height: 560481,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
|
||||||
height: 560615,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
|
height: 560615,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
|
||||||
height: 561568,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
|
height: 561568,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
|
||||||
height: 561569,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
|
height: 561569,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
|
||||||
height: 561572,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
|
height: 561572,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
|
||||||
height: 561582,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
|
height: 561582,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
|
||||||
height: 562106,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
|
height: 562106,
|
||||||
},
|
tx_hash:
|
||||||
{
|
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
|
||||||
height: 562106,
|
},
|
||||||
tx_hash:
|
{
|
||||||
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
|
height: 562106,
|
||||||
|
tx_hash:
|
||||||
|
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -159,16 +163,25 @@ const mockTxDetails2 = {
|
|||||||
|
|
||||||
const mockFulcrumNoTxHistory = {
|
const mockFulcrumNoTxHistory = {
|
||||||
success: true,
|
success: true,
|
||||||
transactions: []
|
transactions: [
|
||||||
|
{
|
||||||
|
transactions: [],
|
||||||
|
address: 'bitcoincash:qrgqqkky28jdkv3w0ctrah0mz3jcsnsklc34gtukrh'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockFulcrumNoSendBalance = {
|
const mockFulcrumNoSendBalance = {
|
||||||
success: true,
|
success: true,
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
height: 633578,
|
transactions: [
|
||||||
tx_hash:
|
{
|
||||||
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
|
height: 633578,
|
||||||
|
tx_hash:
|
||||||
|
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user