Compare commits

..
2 Commits
Author SHA1 Message Date
Chris Troutner bddff1f9bd Merge branch 'master' into bchn 2020-11-08 07:31:09 -08:00
Chris Troutner c927763849 Customizing for bchn 2020-11-07 20:21:27 -08:00
74 changed files with 13748 additions and 9967 deletions
-6
View File
@@ -16,7 +16,6 @@ start-local-testnet.sh
test-fullstack-main.sh test-fullstack-main.sh
start-fullstack-main.sh start-fullstack-main.sh
start-fullstack-test.sh start-fullstack-test.sh
start-fullstack-testnet.sh
start-free-main.sh start-free-main.sh
start-local-mainnet.sh.save start-local-mainnet.sh.save
start-local-testnet.sh.save start-local-testnet.sh.save
@@ -26,11 +25,6 @@ start-sweet-main.sh
test-sweet-main.sh test-sweet-main.sh
test-fullstack-abc.sh test-fullstack-abc.sh
test-fullstack-bchn.sh test-fullstack-bchn.sh
start-fullstack-abc.sh
start-fullstack-bchn.sh
start-ss-main.sh
start-decatur-bchn.sh
shell-scripts/
coverage coverage
start-my-infra start-my-infra
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 Permissionless Software Foundation Copyright (c) 2020 Chris Troutner
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+26 -39
View File
@@ -1,15 +1,19 @@
# bch-api # bch-api
[![License](https://img.shields.io/npm/l/@psf/bch-js)](https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/LICENSE.md)
[![js-standard-style](https://img.shields.io/badge/javascript-standard%20code%20style-green.svg?style=flat-square)](https://github.com/feross/standard)
This is a REST API server, written in node.js JavaScript, using [Express.js](https://expressjs.com/) framework. The purpose of this code is to create a REST API server that provides a common interface for working with a Bitcoin Cash full node and various indexers. See [this article](https://psfoundation.cash/blog/cash-stack) to learn about the 'Cash Stack'. Visit [FullStack.cash](https://fullstack.cash), sign up for a free account, and use this REST API right away with the [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) JavaScript library. [![Build Status](https://travis-ci.org/christroutner/bch-api.svg?branch=master)](https://travis-ci.org/christroutner/bch-api)
This repository is intended to be paired with [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js), an npm JavaScript library for building Bitcoin Cash apps. This is a fork and alternative implementation of
the [rest.bitcoin.com](https://github.com/Bitcoin-com/rest.bitcoin.com) repository.
The purpose of this code is to create a REST API server that provides a common
interface for working with a Bitcoin Cash full node and various indexers. See [this article](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer) to learn about the 'Cash Stack'. Visit [FullStack.cash](https://fullstack.cash), sign up for a free account, and use this REST API right away with the [bch-js](https://github.com/christroutner/bch-js) JavaScript library.
![Cash Stack Network Diagram](./generic-network-diagram.png) This repository is intended to be paired with [bch-js](https://github.com/christroutner/bch-js),
an npm JavaScript library, and an alternative implementation
of [BITBOX SDK](https://github.com/Bitcoin-com/bitbox-sdk).
Both bch-api and bch-js are part of the 'Cash Stack' [full stack of BCH software](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer). Both bch-api and bch-js are part of the
[full stack of BCH software](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer).
- [API Documentation](https://fullstack.cash/documentation) - [API Documentation](https://fullstack.cash/documentation)
- [Example Code](https://fullstack.cash/examples) - [Example Code](https://fullstack.cash/examples)
@@ -20,38 +24,41 @@ Have questions? Need help? Join our community support:
## Features ## Features
The following features set this repository apart from rest.bitcoin.com: The following features set this repository apart from rest.bitcoin.com:
- Address balance and UTXO queries use the [Blockbook](https://github.com/trezor/blockbook)
indexer instead of Insight.
- Fine grain access is controlled with a JWT token using - Fine grain access is controlled with a JWT token using
[this back end auth server](https://github.com/Permissionless-Software-Foundation/jwt-bch-api) and [this front end](https://github.com/Permissionless-Software-Foundation/jwt-bch-frontend). Can be used to monetize access to the REST API. [this back end auth server](https://github.com/Permissionless-Software-Foundation/jwt-bch-api) and [this front end](https://github.com/Permissionless-Software-Foundation/jwt-bch-frontend).
- Default rate limits are set to 3 RPM for anonymous connections, 10 RPM for [free accounts](https://fullstack.cash/pricing), up to 100 RPM if a full-access JWT token is used.
- Typescript removed and ES8 JavaScript used instead. - Typescript removed and ES8 JavaScript used instead.
- `npm audit fix` frequently run to fix dependencies. - npm audit run on all dependencies.
- [Greenkeeper](https://greenkeeper.io/) implemented for automatic dependency management
and security updates.
## Live Demo ## Live Demo
You can test a live demo of the REST API by running the You can test a live demo of the REST API by running the
[bch-js examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples). [bch-js examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples).
Rate limits are 20 requests per minute, but you can increase them to 100 with a [paid account](https://fullstack.cash/pricing). Rate limits are 3 requests per minute, but you can increase them to 10 with a [free account](https://fullstack.cash/pricing).
You can bootstrap your own REST API server by downloading and installing the infrastructure listed on the [CashStrap](https://fullstack.cash/cashstrap) page. This is fast enough to try out the examples
but these servers are not intended as a freemium service. You can run your own
REST server by purchasing the hard drive at [bchjs.cash](https://bchjs.cash).
### Configure bch-js - Mainnet REST API server: https://api.fullstack.cash/v3/
The live servers can be used by [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) by settings the `restURL` config property to one of these servers: - Testnet REST API server: https://tapi.fullstack.cash/v3/
- BCHN Mainnet REST API server: https://bchn.fullstack.cash/v4/
- ABC Mainnet REST API server: https://abc.fullstack.cash/v4/
- Testnet3 REST API server: https://testnet3.fullstack.cash/v4/
- Check server status: https://metrics.fullstack.cash - Check server status: https://metrics.fullstack.cash
## Installation ## Installation
There are two installation paths, depending if you want a *development* or There are two installation paths, depending if you want a *development* or
*production* environment. You'll also need to set up the underlying infrastructure *production* environment. You'll also need to set up the underlying infrastructure
described on [this page](https://psfoundation.cash/blog/cash-stack). described [this page](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer).
This code targets the Ubuntu 18.04 LTS Linux OS or higher. This code targets the Ubuntu 18.04 LTS Linux OS.
### Development ### Development
This is a standard node.js project. The installation is as follows: This is a standard node.js project. The installation is as follows:
- Clone this repository: - Clone this repository:
`git clone https://github.com/Permissionless-Software-Foundation/bch-api && cd bch-api` `git clone https://github.com/christroutner/bch-api && cd bch-api`
- Install dependencies: - Install dependencies:
@@ -82,29 +89,9 @@ 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)
## IPFS
Copies of this repository will occasionally be uploaded and hosted on [IPFS](https://ipfs.io):
- v1.25.1: [QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX](https://ipfs.io/ipfs/QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX)
## License ## License
[MIT](./LICENSE.md) [MIT](./LICENSE.md)
a
-4
View File
@@ -1,4 +0,0 @@
{
"sampleUrl": null
}
+1 -20
View File
@@ -1,30 +1,11 @@
/* /*
Common configuration settings. Common configuration settings.
Default settings in this file can be overridden by an environment variable.
*/ */
const config = { const config = {
// This is the same secret used by jwt-bch-api
apiTokenSecret: process.env.TOKENSECRET apiTokenSecret: process.env.TOKENSECRET
? process.env.TOKENSECRET ? process.env.TOKENSECRET
: 'secret-jwt-token', : 'secret-jwt-token'
// Rate Limits
anonRateLimit: process.env.ANON_RATE_LIMIT
? Number(process.env.ANON_RATE_LIMIT)
: 500,
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
? Number(process.env.WHITELIST_RATE_LIMIT)
: 10,
internalRateLimit: process.env.INTERNAL_RATE_LIMIT
? Number(process.env.INTERNAL_RATE_LIMIT)
: 10,
pointsPerMinute: process.env.POINTS_PER_MINUTE
? Number(process.env.POINTS_PER_MINUTE)
: 10000,
whitelistDomains: process.env.WHITELIST_DOMAINS
? process.env.WHITELIST_DOMAINS.split(',')
: ['fullstack.cash', 'psfoundation.cash', '10.0.']
} }
module.exports = config module.exports = config
+2 -3
View File
@@ -16,9 +16,8 @@ if [[ $GIT_BRANCH =~ "master" ]]
curl -X POST http://fullstack.cash:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA curl -X POST http://fullstack.cash:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA
curl -X POST http://fullstack.cash:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA curl -X POST http://fullstack.cash:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA
#curl -X POST http://$FREE_SERVER:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA curl -X POST http://$FREE_SERVER:9000/hooks/bch-api-testnet -H "Content-Type: application/json" -d $DATA
#curl -X POST http://$FREE_SERVER:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA curl -X POST http://$FREE_SERVER:9000/hooks/bch-api-mainnet -H "Content-Type: application/json" -d $DATA
curl -X POST http://$BCHN_BCHAPI:9000/hooks/bch-api-bchn -H "Content-Type: application/json" -d $DATA
echo "...Finished deploying to production." echo "...Finished deploying to production."
+9 -46
View File
@@ -1,64 +1,27 @@
#!/bin/bash #!/bin/bash
# Docker containers for the below infrastructure is described here: # test
# https://psfoundation.cash/blog/cash-stack
# Pre-synced databases can be downloaded on the CashStrap page:
# https://fullstack.cash/cashstrap
# Which network are you using?
export NETWORK=mainnet
#export NETWORK=testnet
# Full node # Full node
export RPC_IP=172.17.0.1:8332 export RPC_BASEURL=http://172.17.0.1:8332/
export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password export RPC_PASSWORD=password
export NETWORK=mainnet
# SLPDB # SLPDB
export SLPDB_PASS_GP=somelongpassword
export SLPDB_URL=http://172.17.0.1:12300/ export SLPDB_URL=http://172.17.0.1:12300/
# Use the same address as SLPDB_URL if you don't have a separate whitelist server. export SLPDB_PASS=somelongpassword
export SLPDB_PASS_WL=somelongpassword
export SLPDB_WHITELIST_URL=http://172.17.0.1:12300/
# slp-api alternative SLP validator using slp-validate:
# https://github.com/Permissionless-Software-Foundation/slp-api
export SLP_API_URL=http://10.0.0.5:5001/
# Mainnet Fulcrum / ElectrumX # Blockbook Indexer
export FULCRUM_URL=172.17.0.1 export BLOCKBOOK_URL=https://172.17.0.1:9131/
export FULCRUM_PORT=50002 # Allow node.js to make network calls to https using self-signed certificate.
export NODE_TLS_REJECT_UNAUTHORIZED=0
# Redis DB - Used for rate limiting # Redis DB
export REDIS_PORT=6379 export REDIS_PORT=6379
export REDIS_HOST=172.17.0.1 export REDIS_HOST=172.17.0.1
# JWT Token Secret # JWT Token Secret
# This is used to verify JWT tokens generated with jwt-bch-api:
# https://github.com/Permissionless-Software-Foundation/jwt-bch-api
export TOKENSECRET=somelongsecretvalue export TOKENSECRET=somelongsecretvalue
# So that bch-api can call bch-js locally.
export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
# Basic Authentication password
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON.
# Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=50
# 10 = 100 RPM
export WHITELIST_RATE_LIMIT=10
# Set logging parameters
#1m means no more than 1 megabyte
export LOG_MAX_SIZE=1m
#5d means store no more than 5 days
export LOG_MAX_FILES=5d
npm start npm start
+6 -37
View File
@@ -1,32 +1,18 @@
#!/bin/bash #!/bin/bash
# Docker containers for the below infrastructure is described here:
# https://psfoundation.cash/blog/cash-stack
# Pre-synced databases can be downloaded on the CashStrap page:
# https://fullstack.cash/cashstrap
# Which network are you using?
#export NETWORK=mainnet
export NETWORK=testnet
# Full node # Full node
export RPC_IP=172.17.0.1:18332 export RPC_BASEURL=http://172.17.0.1:18332/
export RPC_BASEURL=http://$RPC_IP/
export RPC_USERNAME=bitcoin export RPC_USERNAME=bitcoin
export RPC_PASSWORD=password export RPC_PASSWORD=password
export NETWORK=testnet
# SLPDB # SLPDB
export SLPDB_URL=http://172.17.0.1:13300/ export SLPDB_URL=http://172.17.0.1:13300/
export SLPDB_PASS=somelongpassword
# Use the same address as SLPDB_URL if you don't have a separate whitelist server.
export SLPDB_WHITELIST_URL=http://172.17.0.1:13300/
# slp-api alternative SLP validator using slp-validate:
# https://github.com/Permissionless-Software-Foundation/slp-api
export SLP_API_URL=http://10.0.0.5:5001/
# Mainnet Fulcrum / ElectrumX # Blockbook Indexer
export FULCRUM_URL=172.17.0.1 export BLOCKBOOK_URL=https://172.17.0.1:19131/
export FULCRUM_PORT=60002 # Allow node.js to make network calls to https using self-signed certificate.
export NODE_TLS_REJECT_UNAUTHORIZED=0
# Redis DB # Redis DB
export REDIS_PORT=6380 export REDIS_PORT=6380
@@ -35,21 +21,4 @@ export REDIS_HOST=172.17.0.1
# JWT Token Secret # JWT Token Secret
export TOKENSECRET=somelongsecretvalue export TOKENSECRET=somelongsecretvalue
# So that bch-api can call bch-js locally.
export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
# Basic Authentication password (optional)
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON.
# Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=50
# 10 = 100 RPM
export WHITELIST_RATE_LIMIT=10
npm start npm start
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

+9870 -4387
View File
File diff suppressed because it is too large Load Diff
+23 -18
View File
@@ -14,26 +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-v3",
"lint": "standard --env mocha --fix", "lint": "standard --env mocha --fix",
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v4/", "test-v3": "export NETWORK=mainnet && nyc --reporter=text mocha --timeout 60000 test/v3/",
"test:integration": "mocha test/v4/integration", "test:temp": "export NETWORK=mainnet && mocha --timeout 25000 test/v3/slp.js",
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js", "test:integration": "mocha test/v3/integration",
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v4/integration/nft.js", "test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v3/integration/slp*.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/v3/",
"docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs", "docs": "./node_modules/.bin/apidoc -i src/routes/v3 -o docs"
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#getNftChildren' test/v4/integration/",
"test:temp2": "mocha -g '#getNftGroup' --exit --timeout 30000 test/v4/"
}, },
"engines": { "engines": {
"node": ">=10.15.1" "node": ">=10.15.1"
}, },
"dependencies": { "dependencies": {
"@psf/bch-js": "^4.18.0", "@psf/bch-js": "^3.7.1",
"apidoc": "^0.26.0", "apidoc": "^0.23.0",
"axios": "^0.21.1", "axios": "^0.19.0",
"bitcore-lib-cash": "^8.23.1", "bitcore-lib-cash": "^8.20.3",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"cors": "^2.8.3", "cors": "^2.8.3",
@@ -46,6 +44,7 @@
"helmet": "^3.21.2", "helmet": "^3.21.2",
"ioredis": "^4.14.1", "ioredis": "^4.14.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"level": "^6.0.0",
"mkdirp": "^1.0.0", "mkdirp": "^1.0.0",
"mocha": "^7.1.1", "mocha": "^7.1.1",
"morgan": "^1.9.1", "morgan": "^1.9.1",
@@ -53,6 +52,8 @@
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-anonymous": "^1.0.1", "passport-anonymous": "^1.0.1",
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"pg": "^8.0.0",
"pg-hstore": "^2.3.2",
"rate-limiter-flexible": "^2.0.0", "rate-limiter-flexible": "^2.0.0",
"winston": "^3.2.1", "winston": "^3.2.1",
"winston-daily-rotate-file": "^4.0.0" "winston-daily-rotate-file": "^4.0.0"
@@ -67,13 +68,17 @@
"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": "^12.0.0",
"nock": "^13.0.5", "node-mocks-http": "^1.7.0",
"nyc": "^15.0.0", "nyc": "^15.0.0",
"prettier": "^2.0.0", "prettier": "^2.0.0",
"semantic-release": "^17.4.2", "proxyquire": "^2.1.0",
"request": "^2.88.0",
"request-promise": "^4.2.2",
"semantic-release": "^17.0.0",
"sinon": "^9.0.0", "sinon": "^9.0.0",
"standard": "^14.3.1" "standard": "^14.3.1",
"typescript": "^3.1.4"
}, },
"release": { "release": {
"publish": [ "publish": [
@@ -85,6 +90,6 @@
}, },
"apidoc": { "apidoc": {
"title": "bch-api", "title": "bch-api",
"url": "https://api.fullstack.cash/v4" "url": "https://api.fullstack.cash/v3"
} }
} }
+58 -75
View File
@@ -3,6 +3,7 @@
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()
@@ -24,34 +25,35 @@ 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')
// v4 // v3
const healthCheckV4 = require('./routes/v4/health-check') const healthCheckV3 = require('./routes/v3/health-check')
const BlockchainV4 = require('./routes/v4/full-node/blockchain') const BlockchainV3 = require('./routes/v3/full-node/blockchain')
const ControlV4 = require('./routes/v4/full-node/control') const ControlV3 = require('./routes/v3/full-node/control')
const MiningV4 = require('./routes/v4/full-node/mining') const MiningV3 = require('./routes/v3/full-node/mining')
const networkV4 = require('./routes/v4/full-node/network') const networkV3 = require('./routes/v3/full-node/network')
const RawtransactionsV4 = require('./routes/v4/full-node/rawtransactions') const RawtransactionsV3 = require('./routes/v3/full-node/rawtransactions')
const UtilV4 = require('./routes/v4/util') const utilV3 = require('./routes/v3/util')
const SlpV4 = require('./routes/v4/slp') const SlpV3 = require('./routes/v3/slp')
const xpubV4 = require('./routes/v4/xpub') const xpubV3 = require('./routes/v3/xpub')
const ElectrumXV4 = require('./routes/v4/electrumx') const BlockbookV3 = require('./routes/v3/blockbook')
const EncryptionV4 = require('./routes/v4/encryption') const Ninsight = require('./routes/v3/ninsight')
const PriceV4 = require('./routes/v4/price') const ElectrumXV3 = require('./routes/v3/electrumx')
const Ninsight = require('./routes/v4/ninsight') const EncryptionV3 = require('./routes/v3/encryption')
const PriceV3 = require('./routes/v3/price')
require('dotenv').config() require('dotenv').config()
// Instantiate v4 route libraries. // Instantiate route libraries.
const blockchainV4 = new BlockchainV4() const blockchainV3 = new BlockchainV3()
const controlV4 = new ControlV4() const controlV3 = new ControlV3()
const miningV4 = new MiningV4() const miningV3 = new MiningV3()
const rawtransactionsV4 = new RawtransactionsV4() const rawtransactionsV3 = new RawtransactionsV3()
const slpV4 = new SlpV4() const slpV3 = new SlpV3()
const electrumxv4 = new ElectrumXV4() const blockbookV3 = new BlockbookV3()
electrumxv4.connect() const electrumxv3 = new ElectrumXV3()
const encryptionv4 = new EncryptionV4() electrumxv3.connect()
const pricev4 = new PriceV4() const encryptionv3 = new EncryptionV3()
const utilV4 = new UtilV4({ electrumx: electrumxv4 }) const pricev3 = new PriceV3()
const app = express() const app = express()
@@ -93,55 +95,38 @@ 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 v4prefix = 'v4' // const v2prefix = "v2"
const v3prefix = 'v3'
// START Rate Limits // Inspect the header for a JWT token.
app.use(`/${v3prefix}/`, 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())
// Ensure req.locals and res.locals objects exist. // Rate limit on all v3 routes
app.use(`/${v4prefix}/`, rateLimits.populateLocals) // Establish and enforce rate limits.
// app.use(`/${v3prefix}/`, rateLimits.routeRateLimit)
app.use(`/${v3prefix}/`, rateLimits.rateLimitByResource)
// Allow users to turn off rate limits with an environment variable. app.use(`/${v3prefix}/` + 'health-check', healthCheckV3)
const DO_NOT_USE_RATE_LIMITS = process.env.DO_NOT_USE_RATE_LIMITS || false app.use(`/${v3prefix}/` + 'blockchain', blockchainV3.router)
app.use(`/${v3prefix}/` + 'control', controlV3.router)
console.log(`DO_NOT_USE_RATE_LIMITS: ${DO_NOT_USE_RATE_LIMITS}`) app.use(`/${v3prefix}/` + 'mining', miningV3.router)
app.use(`/${v3prefix}/` + 'network', networkV3)
if (!DO_NOT_USE_RATE_LIMITS) { app.use(`/${v3prefix}/` + 'rawtransactions', rawtransactionsV3.router)
console.log('Rate limits are being used') app.use(`/${v3prefix}/` + 'util', utilV3.router)
// Inspect the header for a JWT token. app.use(`/${v3prefix}/` + 'slp', slpV3.router)
app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders) app.use(`/${v3prefix}/` + 'xpub', xpubV3.router)
app.use(`/${v3prefix}/` + 'blockbook', blockbookV3.router)
// Instantiate the authorization middleware, used to implement pro-tier rate limiting. app.use(`/${v3prefix}/` + 'electrumx', electrumxv3.router)
// Handles Anonymous and Basic Authorization schemes used by passport.js app.use(`/${v3prefix}/` + 'encryption', encryptionv3.router)
app.use(`/${v4prefix}/`, auth.mw()) app.use(`/${v3prefix}/` + 'price', pricev3.router)
// Experimental rate limits
app.use(`/${v4prefix}/`, 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
app.use(`/${v4prefix}/` + 'health-check', healthCheckV4)
app.use(`/${v4prefix}/` + 'blockchain', blockchainV4.router)
app.use(`/${v4prefix}/` + 'control', controlV4.router)
app.use(`/${v4prefix}/` + 'mining', miningV4.router)
app.use(`/${v4prefix}/` + 'network', networkV4)
app.use(`/${v4prefix}/` + 'rawtransactions', rawtransactionsV4.router)
app.use(`/${v4prefix}/` + 'slp', slpV4.router)
app.use(`/${v4prefix}/` + 'xpub', xpubV4.router)
app.use(`/${v4prefix}/` + 'electrumx', electrumxv4.router)
app.use(`/${v4prefix}/` + 'encryption', encryptionv4.router)
app.use(`/${v4prefix}/` + 'price', pricev4.router)
app.use(`/${v4prefix}/` + 'util', utilV4.router)
const ninsight = new Ninsight() const ninsight = new Ninsight()
app.use(`/${v4prefix}/` + 'ninsight', ninsight.router) app.use(`/${v3prefix}/` + 'ninsight', ninsight.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) => {
@@ -189,11 +174,9 @@ 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. // Set the time before a timeout error is generated. This impacts testing and
// 10 seconds is way too agressive. 30 Seconds was used for a while, but with // the handling of timeout errors. Is 10 seconds too agressive?
// being able to set a timeout between UTXOs for tokenUtxoDetails, the timeout server.setTimeout(30 * 1000)
// 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.
@@ -228,11 +211,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
} }
+56 -66
View File
@@ -1,33 +1,30 @@
/* /*
This library handles anonymous and Basic Authentication. CT 2/4/20 Note: This library handles anonymous and Basic auth. This library
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 for clients who run their own isolated infrastructure without rate provided to buisiness partners.
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 Authentication header: call for the prescence of a Basic authorization 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-ratelimit.js middleware. route-ratelimit.js is set and passed to the route-ratelimits.ts middleware.
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'
const passport = require('passport') const passport = require('passport')
const BasicStrategy = require('passport-http').BasicStrategy // const BasicStrategy = require('passport-http').BasicStrategy
const AnonymousStrategy = require('passport-anonymous') const AnonymousStrategy = require('passport-anonymous')
const wlogger = require('../util/winston-logging') // const wlogger = require('../util/winston-logging')
// Used for debugging and iterrogating JS objects. // Used for debugging and iterrogating JS objects.
const util = require('util') const util = require('util')
@@ -36,11 +33,9 @@ util.inspect.defaultOptions = { depth: 1 }
// let _this // let _this
// Set default rate limit value for testing // Set default rate limit value for testing
const PRO_PASSES = process.env.PRO_PASS // const PRO_PASSES = process.env.PRO_PASS ? process.env.PRO_PASS : 'BITBOX'
? process.env.PRO_PASS
: 'testpassword'
// Convert the pro-tier password string into an array split by ':'. // Convert the pro-tier password string into an array split by ':'.
const PRO_PASS = PRO_PASSES.split(':') // const PRO_PASS = PRO_PASSES.split(':')
// wlogger.verbose(`PRO_PASS set to: ${PRO_PASS}`) // wlogger.verbose(`PRO_PASS set to: ${PRO_PASS}`)
@@ -52,61 +47,56 @@ class AuthMW {
// Initialize passport for 'anonymous' authentication. // Initialize passport for 'anonymous' authentication.
passport.use(new AnonymousStrategy()) passport.use(new AnonymousStrategy())
console.log(`PRO_PASS: ${JSON.stringify(PRO_PASS, null, 2)}`)
// Initialize passport for 'basic' authentication. // Initialize passport for 'basic' authentication.
passport.use( // passport.use(
new BasicStrategy({ passReqToCallback: true }, function ( // new BasicStrategy({ passReqToCallback: true }, function (
req, // req,
username, // username,
password, // password,
done // done
) { // ) {
// console.log(`req: ${util.inspect(req)}`) // // console.log(`req: ${util.inspect(req)}`)
// console.log(`username: ${username}`) // // console.log(`username: ${username}`)
// console.log(`password: ${password}`) // // console.log(`password: ${password}`)
//
// Create the req.locals property if it does not yet exist. // // Create the req.locals property if it does not yet exist.
if (!req.locals) { // if (!req.locals) {
req.locals = { // req.locals = {
// default values // // default values
proLimit: false, // proLimit: false,
apiLevel: 0 // apiLevel: 0
} // }
} // }
//
// Set pro-tier rate limit to flag to false by default. // // Set pro-tier rate limit to flag to false by default.
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 === 'fullstackcash') { // // if (username === "BITBOX" && password === PRO_PASS) {
// Can set several different passwords in the environment variable. // if (username === 'BITBOX') {
// 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] //
// if (password === thisPass) {
if (password === thisPass) { // wlogger.verbose(`${req.url} called by ${password.slice(0, 6)}`)
wlogger.verbose(`${req.url} called by ${password.slice(0, 6)}`) //
// // Success
// Success // req.locals.proLimit = true
req.locals.proLimit = true // break
console.log(`User ${req.ip} authenticated using Basic Auth`) // }
break // }
} // }
} //
} // // console.log(`req.locals: ${util.inspect(req.locals)}`)
//
// console.log(`req.locals: ${util.inspect(req.locals)}`) // return done(null, true)
// })
return done(null, true) // )
})
)
} }
// Middleware called by the route. // Middleware called by the route.
mw () { mw () {
console.log('Initializing passport') return passport.authenticate(['anonymous'], {
return passport.authenticate(['basic', 'anonymous'], {
session: false session: false
}) })
} }
+186 -371
View File
@@ -1,62 +1,30 @@
/* 'use strict'
This file will replace the original rate-limit.js file.
Sets the rate limits for the anonymous and paid tiers. Current rate limits:
- 10000 points in 60 seconds
- 500 points per call for anonymous tier (20 RPM)
- 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.
*/
// Public npm libraries.
const jwt = require('jsonwebtoken')
const Redis = require('ioredis')
const { RateLimiterRedis } = require('rate-limiter-flexible')
// local libraries.
const wlogger = require('../util/winston-logging') const wlogger = require('../util/winston-logging')
const config = require('../../config') const config = require('../../config')
let _this // Global pointer to instance of class, when 'this' context is lost. const jwt = require('jsonwebtoken')
// Setup Redis to track rate limits for each user. // Redis
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: config.pointsPerMinute, // Number of points points: 1000, // Number of points
duration: 60 // Per minute (per 60 seconds) duration: 60 // Per minute (per 60 seconds)
} }
// Constants let _this
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 () {
@@ -67,343 +35,25 @@ class RateLimits {
this.config = config this.config = config
} }
// This is the main middleware funciton of this library. All other functions
// support this function.
async applyRateLimits (req, res, next) {
try {
// Exit if the user has already authenticated with Basic Authentication.
if (req.locals.proLimit) {
console.log('External call, basic auth, skipping rate limits.')
wlogger.debug(
'req.locals.proLimit = true; Using Basic Authentication instead of rate limits'
)
return next()
}
// Determine if the call is an external or internal API call.
const isInternal = _this.checkInternalIp(req)
// console.log(`isInternal: ${isInternal}`)
// Determine if the call originates from another computer on the intranet.
const isWhitelistOrigin = _this.isInWhitelist(req)
// console.log('isWhitelistOrigin: ', isWhitelistOrigin)
// Handle the use case of internally-generated requests.
if (isInternal) {
// Internal API calls should pass the authentication data in through the
// the usrObj in the body.
if (req.body && req.body.usrObj) {
if (req.body.usrObj.proLimit) {
// console.log('Internal call, basic auth, skipping rate limits.')
// If this is an internal call that originated from a user using
// Basic Authentication, then skip rate-limits.
return next()
} 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 {
// Handle the normal use-case of external requests
// console.log(
// 'External call, applying rate limits. Using JWT if available.'
// )
// For calls originating from a whitelist domain, apply a high-RPM
// JWT token to the call.
if (isWhitelistOrigin) {
const defaultPayload = {
id: '77.77.77.77',
email: 'whitelist@bchtest.net',
apiLevel: 40,
rateLimit: 100,
pointsToConsume: WHITELIST_POINTS_TO_CONSUME,
duration: 30
}
// Inject the high-RPM JWT token into the call.
req.locals.jwtToken = _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,
req.locals.jwtToken
)
// 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
}
}
} catch (err) {
wlogger.error('Error in route-ratelimit2.js/applyRateLimits(): ', err)
}
// By default, move to the next middleware.
next()
}
// A wrapper for Redis-based rate limiter.
// Will return false if the user has not exceeded the rate limit. Otherwise
// it will return the 'res' object with an error status and message, which
// should be returned by the middleware.
async trackRateLimits (req, res, jwtToken) {
// Anonymous rate limits are used by default.
let pointsToConsume = ANON_LIMITS
// console.log('pointsToConsume: ', pointsToConsume)
let key = req.ip // Use the IP address as the key, by default.
// console.log('jwtToken: ', jwtToken)
try {
// Decode the JWT token if it exists
if (jwtToken) {
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
pointsToConsume = decoded.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)
// 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`
})
}
}
// Attempts to decode a JWT token. Returns default values if it fails.
decodeJwtToken (jwtToken) {
const defaultPayload = {
id: '123.456.789.10',
email: 'test@bchtest.net',
apiLevel: 10,
rateLimit: 3,
pointsToConsume: ANON_LIMITS,
duration: 30
}
try {
// Default values, in case there is an error.
const defaultJwt = _this.generateJwtToken(defaultPayload)
// Generate a default payload to use, if the decoding of the user-provided
// jwt fails.
let decoded = _this.jwt.verify(defaultJwt, _this.config.apiTokenSecret)
try {
decoded = _this.jwt.verify(jwtToken, _this.config.apiTokenSecret)
} catch (err) {
wlogger.error('Error in route-ratelimit2.js/decodeJwtTokens(): ', err)
}
return decoded
} catch (err) {
wlogger.error(
'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
// whitelist.
isInWhitelist (req) {
try {
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
// console.log(`WHITELIST_DOMAINS: ${JSON.stringify(WHITELIST_DOMAINS, null, 2)}`)
for (let i = 0; i < WHITELIST_DOMAINS.length; i++) {
const thisDomain = WHITELIST_DOMAINS[i]
if (origin.includes(thisDomain)) return true
}
return retVal
} catch (err) {
wlogger.error(
'Error in route-ratelimit.js/isInWhitelist(). Returning false by default.'
)
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. // Used to disconnect from the Redis DB.
// Called by unit tests so that node.js thread doesn't live forever. // Called by unit tests so that node.js thread doesn't live forever.
closeRedis () { closeRedis () {
redisClient.disconnect() redisClient.disconnect()
} }
// Clear the redis database. Used by unit tests.
async wipeRedis () { async wipeRedis () {
await redisClient.flushdb() await redisClient.flushdb()
} }
// Generates a JWT token for testing purposes. This is not used in production. // This is the new rate limit function that uses the rate-limiter-flexible npm
// This function mirrors the kind of JWT token that would be generated by // library. It uses fine-grain rate limiting based on the resources being
// jwt-bch-api. // consumed.
generateJwtToken (payload) { async rateLimitByResource (req, res, next) {
try { try {
const jwtOptions = { let userId
expiresIn: '30 days' let decoded = {}
}
const token = _this.jwt.sign( // Create a req.locals object if not passed in.
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) { if (!req.locals) {
req.locals = { req.locals = {
// default values // default values
@@ -413,16 +63,181 @@ class RateLimits {
} }
} }
// Create a re*S*.locals object if it does not exist. // Create a res.locals object if it does not exist. This is used for
// debugging.
if (!res.locals) { if (!res.locals) {
res.locals = { res.locals = {
rateLimitTriggered: false rateLimitTriggered: false
} }
} }
next() // Decode the JWT token if one exists.
if (req.locals.jwtToken) {
try {
decoded = _this.jwt.verify(
req.locals.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(
`Last three letters of token secret: ${_this.config.apiTokenSecret.slice(
-3
)}`
)
wlogger.error(
'Error trying to decode JWT token in route-ratelimit.js/newRateLimit(): ',
err
)
}
} else {
wlogger.debug('No JWT token found!')
}
// Used for displaying error message. Default value is 30.
let rateLimit = 30
// Code here for the rate limiter is adapted from this example:
// https://github.com/animir/node-rate-limiter-flexible/wiki/Overall-example#authorized-and-not-authorized-users
try {
// The resource being consumed: full node, indexer, SLPDB, etc.
const resource = _this.getResource(req.url)
wlogger.debug(`resource: ${resource}`)
let key = userId || req.ip
res.locals.key = key // Feedback for tests.
// 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')
if (origin === undefined && key.indexOf('10.0.0.5') > -1) {
origin = 'slp-api'
}
wlogger.info(`origin: ${origin}`)
// If the request originates from one of the approved wallet apps, then
// apply paid-access rate limits.
if (
origin &&
(origin.toString().indexOf('wallet.fullstack.cash') > -1 ||
origin.toString().indexOf('sandbox.fullstack.cash') > -1 ||
origin === 'slp-api')
) {
pointsToConsume = 10
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
}
// Apply paid-access rate limits based on key/IP
if (
key.toString().indexOf('172.17.') > -1 ||
key.toString().indexOf('::ffff:127.0.0.1') > -1
) {
pointsToConsume = 1
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) {
console.error('Error in populateLocals(): ', err) wlogger.error('Error in route-ratelimit.js/newRateLimit(): ', err)
// throw err
}
next()
}
// Calculates the points consumed, based on the jwt information and the route
// requested.
calcPoints (jwtInfo) {
let retVal = 300 // By default, use anonymous tier.
try {
// console.log(`jwtInfo: ${JSON.stringify(jwtInfo, null, 2)}`)
const apiLevel = jwtInfo.apiLevel
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 = 100
// Normal indexer routes
} else if (level30Routes.includes(resource)) {
if (apiLevel >= 30) retVal = 10
else retVal = 100
// Full node tier
} else if (apiLevel >= 20) {
retVal = 10
// Free tier, full node only.
} else {
retVal = 100
}
}
return retVal
} catch (err) {
wlogger.error('Error in route-ratelimit.js/calcPoints()')
// throw err
retVal = 300
}
return retVal
}
// This function parses the req.url property to identify what resource
// the user is requesting.
// This was created as a function so that it can be unit tested. Not sure
// what kind of variations will be seen in production.
getResource (url) {
try {
wlogger.debug(`url: ${JSON.stringify(url, null, 2)}`)
const splitUrl = url.split('/')
const resource = splitUrl[1]
return resource
} catch (err) {
wlogger.error('Error in getResource().')
throw err throw err
} }
} }
+591
View File
@@ -0,0 +1,591 @@
/*
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
}
}
/**
* @api {get} /blockbook/balance/{addr} Get balance for a single address.
* @apiName Balance for a single address
* @apiGroup Blockbook
* @apiDescription Returns an object with balance and details about an address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockbook/balance/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
*
*/
// GET handler for single balance
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)
}
}
/**
* @api {post} /blockbook/balance Get balance for an array of addresses.
* @apiName Balance for an array of addresses
* @apiGroup Blockbook
* @apiDescription Return balances and details for an array of addresses.
* Limited to 20 items per request.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/blockbook/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}'
*
*
*/
// 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
}
}
/**
* @api {get} /blockbook/utxos/{addr} Get utxos for a single address.
* @apiName UTXOs for a single address
* @apiGroup Blockbook
* @apiDescription Returns an object with UTXOs associated with an address.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockbook/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
*
*/
// 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)
}
}
/**
* @api {post} /blockbook/utxos Get UTXOs for an array of addresses.
* @apiName UTXOs for an array of addresses
* @apiGroup Blockbook
* @apiDescription Return UTXOs associate with an array of addresses.
* Limited to 20 items per request.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/blockbook/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3","bitcoincash:qzy8wnj0dz927eu6kvh8v2pqsr5w8jh33ys757tdtq"]}'
*
*
*/
// 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
}
}
/**
* @api {get} /blockbook/tx/{txid} Get details for a single transaction.
* @apiName Details for a single transaction
* @apiGroup Blockbook
* @apiDescription Returns an object with details for a single transaction
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v3/blockbook/tx/6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d" -H "accept: application/json"
*
*/
// 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)
}
}
/**
* @api {post} /blockbook/tx Get details for an array of transactions.
* @apiName Details for an array of transactions
* @apiGroup Blockbook
* @apiDescription Return details for an array of transactions.
* Limited to 20 items per request.
*
* @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v3/blockbook/tx" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d","6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d"]}'
*
*
*/
// 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
@@ -79,21 +79,6 @@ class Electrum {
// Set the connection flag. // Set the connection flag.
_this.isReady = true _this.isReady = true
// Periodically check the connection. If it's not connected, attempt to
// reconnect.
_this.reconnectIntervalHandle = setInterval(async function () {
const status = _this.electrumx.connection.status
// console.log(`Electrumx status: ${status}`)
// 1 = connected. If we're not connected, attemp to reconnect.
if (status !== 1) {
wlogger.info(`Electrumx not connectes. Status: ${status}`)
wlogger.info('Attempting to reconnect...')
await _this.electrumx.connect()
wlogger.info('...reconnected.')
}
}, 30000)
console.log('...Successfully connected to ElectrumX server.') console.log('...Successfully connected to ElectrumX server.')
// console.log(`_this.isReady: ${_this.isReady}`) // console.log(`_this.isReady: ${_this.isReady}`)
@@ -111,9 +96,6 @@ class Electrum {
// Return immediately if the isReady flag is false. // Return immediately if the isReady flag is false.
if (!_this.isReady) return true if (!_this.isReady) return true
// Disable the reconnect timer.
clearInterval(_this.reconnectIntervalHandle)
// Disconnect from the server. // Disconnect from the server.
await _this.electrumx.disconnect() await _this.electrumx.disconnect()
@@ -135,7 +117,7 @@ class Electrum {
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({ success: false, error: msg }) return res.json({ error: msg })
} }
// Handle error patterns specific to this route. // Handle error patterns specific to this route.
@@ -195,7 +177,7 @@ class Electrum {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/electrumx/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/electrumx/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
* *
*/ */
// GET handler for single balance // GET handler for single balance
@@ -264,7 +246,7 @@ class Electrum {
* Limited to 20 items per request. * Limited to 20 items per request.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/utxos" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}'
* *
* *
*/ */
@@ -284,7 +266,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -389,7 +371,7 @@ class Electrum {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/electrumx/tx/data/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/electrumx/tx/data/a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d" -H "accept: application/json"
* *
*/ */
// GET handler for single transaction // GET handler for single transaction
@@ -449,7 +431,7 @@ class Electrum {
* Limited to 20 items per request. * Limited to 20 items per request.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/tx/data" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d","a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"], "verbose":false}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/tx/data" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d","a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"], "verbose":false}'
* *
* *
*/ */
@@ -470,7 +452,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // 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.'
@@ -550,7 +532,7 @@ class Electrum {
* @apiDescription Broadcast a raw transaction and return the transaction ID on success or error on failure. * @apiDescription Broadcast a raw transaction and return the transaction ID on success or error on failure.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/tx/broadcast" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txHex":"020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000"}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/tx/broadcast" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txHex":"020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000"}'
* *
*/ */
// POST handler for broadcasting a single transaction // POST handler for broadcasting a single transaction
@@ -641,7 +623,7 @@ class Electrum {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/electrumx/block/header/42?count=2" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/electrumx/block/header/42?count=2" -H "accept: application/json"
* *
*/ */
// GET handler for single block headers // GET handler for single block headers
@@ -707,7 +689,7 @@ class Electrum {
* Limited to 20 items per request. * Limited to 20 items per request.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/block/headers" -H "accept: application/json" -H "Content-Type: application/json" -d '{"heights":[{ "height": 42, count: 2 }, { "height": 100, count: 5 }]}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/block/headers" -H "accept: application/json" -H "Content-Type: application/json" -d '{"heights":[{ "height": 42, count: 2 }, { "height": 100, count: 5 }]}'
* *
*/ */
// POST handler for bulk queries on block headers // POST handler for bulk queries on block headers
@@ -726,7 +708,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // 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.'
@@ -806,7 +788,7 @@ class Electrum {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
* *
*/ */
// GET handler for single balance // GET handler for single balance
@@ -876,7 +858,7 @@ class Electrum {
* Limited to 20 items per request. * Limited to 20 items per request.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/balance" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}'
* *
* *
*/ */
@@ -895,7 +877,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -999,7 +981,7 @@ class Electrum {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/electrumx/transactions/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/electrumx/transactions/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
* *
*/ */
// GET handler for single balance // GET handler for single balance
@@ -1069,7 +1051,7 @@ class Electrum {
* Limited to 20 items per request. * Limited to 20 items per request.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/transactions" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/transactions" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}'
* *
* *
*/ */
@@ -1088,7 +1070,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -1192,7 +1174,7 @@ class Electrum {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/electrumx/unconfirmed/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/electrumx/unconfirmed/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
* *
*/ */
// GET handler for single balance // GET handler for single balance
@@ -1262,7 +1244,7 @@ class Electrum {
* Limited to 20 items per request. * Limited to 20 items per request.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/electrumx/unconfirmed" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}' * curl -X POST "https://api.fullstack.cash/v3/electrumx/unconfirmed" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf","bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf"]}'
* *
* *
*/ */
@@ -1281,7 +1263,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -16,10 +16,15 @@ const RouteUtils = require('../../util/route-utils')
const routeUtils = new RouteUtils() const routeUtils = new RouteUtils()
const BCHJS = require('@psf/bch-js') const BCHJS = require('@psf/bch-js')
const restURL = process.env.LOCAL_RESTURL const bchjs = new BCHJS()
? process.env.LOCAL_RESTURL
: 'https://api.fullstack.cash/v4/' // Use Blockbook for getting indexer data.
const bchjs = new BCHJS({ restURL }) const Blockbook = require('./blockbook')
const blockbook = new Blockbook()
// Use RawTransactions library for getting raw blockchain data.
const RawTransactions = require('./full-node/rawtransactions')
const rawTransactions = new RawTransactions()
let _this let _this
@@ -31,8 +36,8 @@ class Encryption {
_this.axios = axios _this.axios = axios
_this.routeUtils = routeUtils _this.routeUtils = routeUtils
_this.bchjs = bchjs _this.bchjs = bchjs
// _this.blockbook = blockbook _this.blockbook = blockbook
// _this.rawTransactions = rawTransactions _this.rawTransactions = rawTransactions
_this.router = router _this.router = router
_this.router.get('/', _this.root) _this.router.get('/', _this.root)
@@ -45,7 +50,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({ success: false, error: msg }) return res.json({ error: msg })
} }
// Handle error patterns specific to this route. // Handle error patterns specific to this route.
@@ -76,7 +81,7 @@ class Encryption {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
* *
*/ */
async getPublicKey (req, res, next) { async getPublicKey (req, res, next) {
@@ -92,15 +97,6 @@ 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.
@@ -114,36 +110,34 @@ 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) // Retrieve the transaction history for this address.
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`) const balance = await _this.blockbook.balanceFromBlockbook(cashAddr)
// console.log(`balance: ${JSON.stringify(balance, null, 2)}`)
// Extract just the TXIDs const txHistory = balance.txids
const txids = rawTxData.transactions[0].transactions.map((elem) => elem.tx_hash) // console.log(`txHistory: ${JSON.stringify(txHistory, 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.
if (!txids || txids.length === 0) { if (!txHistory || txHistory.length === 0) {
throw new Error('No transaction history.') throw new Error('No transaction history.')
} }
// Loop through the transaction history and search for the public key. // Loop through the transaction history and search for the public key.
for (let i = 0; i < txids.length; i++) { for (let i = 0; i < txHistory.length; i++) {
const thisTx = txids[i] const thisTx = txHistory[i]
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction( const txDetails = await _this.rawTransactions.getRawTransactionsFromNode(
[thisTx], thisTx,
true, true
usrObj
) )
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`) // console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
const vin = txDetails[0].vin const vin = txDetails.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++) {
@@ -181,7 +175,6 @@ 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,7 +53,6 @@ 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) {
@@ -81,7 +80,7 @@ class Blockchain {
* block chain. * block chain.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBestBlockHash" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getBestBlockHash" -H "accept: application/json"
* *
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1 * @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
*/ */
@@ -112,7 +111,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/v4/blockchain/getBlockchainInfo" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/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"
@@ -156,7 +155,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/v4/blockchain/getBlockCount" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getBlockCount" -H "accept: application/json"
* *
* @apiSuccess {Number} bestBlockCount 587665 * @apiSuccess {Number} bestBlockCount 587665
*/ */
@@ -189,7 +188,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/v4/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/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
@@ -250,7 +249,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/v4/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}" * 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}"
* *
* @apiParam {String} hash block hash * @apiParam {String} hash block hash
* @apiParam {Boolean} verbose Return verbose data * @apiParam {Boolean} verbose Return verbose data
@@ -285,7 +284,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(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -342,7 +341,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/v4/blockchain/getChainTips" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getChainTips" -H "accept: application/json"
* *
*/ */
async getChainTips (req, res, next) { async getChainTips (req, res, next) {
@@ -373,7 +372,7 @@ class Blockchain {
* power on the network. * power on the network.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getDifficulty" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getDifficulty" -H "accept: application/json"
* *
*/ */
async getDifficulty (req, res, next) { async getDifficulty (req, res, next) {
@@ -405,7 +404,7 @@ class Blockchain {
* mempool (unconfirmed) * mempool (unconfirmed)
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
* *
*/ */
async getMempoolEntrySingle (req, res, next) { async getMempoolEntrySingle (req, res, next) {
@@ -443,7 +442,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/v4/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}" * curl -X POST https://api.fullstack.cash/v3/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}"
*/ */
async getMempoolEntryBulk (req, res, next) { async getMempoolEntryBulk (req, res, next) {
try { try {
@@ -458,7 +457,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(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -516,7 +515,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/v4/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
* *
*/ */
async getMempoolAncestorsSingle (req, res, next) { async getMempoolAncestorsSingle (req, res, next) {
@@ -558,7 +557,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/v4/getMempoolInfo -H "accept: application/json" * curl -X GET https://api.fullstack.cash/v3/getMempoolInfo -H "accept: application/json"
* *
*/ */
async getMempoolInfo (req, res, next) { async getMempoolInfo (req, res, next) {
@@ -588,7 +587,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/v4/getMempoolInfo -H "accept: application/json" * curl -X GET https://api.fullstack.cash/v3/getMempoolInfo -H "accept: application/json"
* *
*/ */
@@ -600,7 +599,7 @@ class Blockchain {
* of string transaction ids. * of string transaction ids.
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/getRawMempool/?verbose=true" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/getRawMempool/?verbose=true" -H "accept: application/json"
* *
* @apiParam {Boolean} verbose Return verbose data * @apiParam {Boolean} verbose Return verbose data
* *
@@ -636,7 +635,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/v4/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/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)
@@ -693,7 +692,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/v4/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}' * curl "https://api.fullstack.cash/v3/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)
@@ -747,7 +746,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/v4/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
* *
* @apiParam {String} txid Transaction id (required) * @apiParam {String} txid Transaction id (required)
* *
@@ -795,7 +794,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(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -889,7 +888,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(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330 res.status(429) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -938,52 +937,6 @@ 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
@@ -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/v4/control/getnetworkinfo" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/control/getnetworkinfo" -H "accept: application/json"
* *
*/ */
async getNetworkInfo (req, res, next) { async getNetworkInfo (req, res, next) {
@@ -77,7 +77,7 @@ class Mining {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/mining/getMiningInfo" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/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/v4/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json"
* *
* *
*/ */
@@ -65,7 +65,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/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/v4/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}' * curl -X POST "https://api.fullstack.cash/v3/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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // 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/v4/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/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/v4/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}' *curl -X POST "https://api.fullstack.cash/v3/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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // 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/v4/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}' * 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}'
* *
*/ */
async getRawTransactionBulk (req, res, next) { async getRawTransactionBulk (req, res, next) {
@@ -324,12 +324,20 @@ 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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // 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]
@@ -348,7 +356,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)
) )
@@ -375,7 +383,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
* *
* *
*/ */
@@ -421,7 +429,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}' * curl -X POST "https://api.fullstack.cash/v3/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
* *
* *
*/ */
@@ -436,12 +444,11 @@ class RawTransactions {
return res.json({ error: 'hex must be an array' }) return res.json({ error: 'hex must be an array' })
} }
let options = _this.routeUtils.getAxiosOptions() const 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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -515,7 +522,7 @@ class RawTransactions {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: " * curl -X GET "https://api.fullstack.cash/v3/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
* *
* *
*/ */
@@ -537,8 +544,7 @@ class RawTransactions {
}) })
} }
let options = _this.routeUtils.getAxiosOptions() const options = _this.routeUtils.getAxiosOptions()
options = _this.sendTxOptions(options)
// RPC call // RPC call
options.data.id = 'sendrawtransaction' options.data.id = 'sendrawtransaction'
@@ -559,31 +565,6 @@ 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
@@ -24,18 +24,11 @@ class Price {
this.routeUtils = routeUtils this.routeUtils = routeUtils
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH' this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
this.coinexPriceUrl =
'https://api.coinex.com/v1/market/ticker?market=bchausdt'
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('/bchusd', _this.getBCHUSD)
} }
// DRY error handler. // DRY error handler.
@@ -60,11 +53,11 @@ class Price {
* @api {get} /price/usd Get the USD price of BCH * @api {get} /price/usd Get the USD price of BCH
* @apiName Get the USD price of BCH * @apiName Get the USD price of BCH
* @apiGroup Price * @apiGroup Price
* @apiDescription Get the USD price of BCH from Coinbase. * @apiDescription Get the USD price of BCH
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/usd" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/price/usd" -H "accept: application/json"
* *
*/ */
async getUSD (req, res, next) { async getUSD (req, res, next) {
@@ -92,11 +85,11 @@ class Price {
* @api {get} /price/usd Get rates for several different currencies * @api {get} /price/usd Get rates for several different currencies
* @apiName Get rates for several different currencies * @apiName Get rates for several different currencies
* @apiGroup Price * @apiGroup Price
* @apiDescription Get rates for several different currencies from Coinbase. * @apiDescription Get rates for several different currencies
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/rates" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/price/rates" -H "accept: application/json"
* *
*/ */
// Get rates for several different currencies // Get rates for several different currencies
@@ -120,74 +113,6 @@ class Price {
return _this.errorHandler(err, res) return _this.errorHandler(err, res)
} }
} }
/**
* @api {get} /price/bchausd Get the USD price of BCHA
* @apiName Get the USD price of BCHA
* @apiGroup Price
* @apiDescription Get the USD price of BCHA from Coinex.
*
*
* @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/price/bchausd" -H "accept: application/json"
*
*/
async getBCHAUSD (req, res, next) {
try {
// Request options
const opt = {
method: 'get',
baseURL: _this.coinexPriceUrl,
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/getBCHAUSD().', err)
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
+183
View File
@@ -0,0 +1,183 @@
/*
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 }
}
}
File diff suppressed because it is too large Load Diff
+43 -45
View File
@@ -4,17 +4,17 @@ const express = require('express')
const router = express.Router() const router = express.Router()
const axios = require('axios') const axios = require('axios')
const RouteUtils = require('../../util/route-utils') const routeUtils = require('./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 }
const BCHJS = require('@psf/bch-js') const BCHJS = require('@psf/bch-js')
const bchjs = new BCHJS() const bchjs = new BCHJS()
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v4/' // const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v3/'
// const bchjsHTTP = axios.create({ // const bchjsHTTP = axios.create({
// baseURL: process.env.RPC_BASEURL // baseURL: process.env.RPC_BASEURL
@@ -37,28 +37,9 @@ const bchjs = new BCHJS()
let _this let _this
class UtilRoute { class UtilRoute {
constructor (utilConfig) { constructor () {
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
} }
@@ -75,7 +56,7 @@ class UtilRoute {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X GET "https://api.fullstack.cash/v4/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json" * curl -X GET "https://api.fullstack.cash/v3/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
* *
* *
*/ */
@@ -124,8 +105,8 @@ class UtilRoute {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/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"]}'
* curl -X POST "https://api.fullstack.cash/v4/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}' * 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}'
* *
* *
*/ */
@@ -143,7 +124,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(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330 res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
return res.json({ return res.json({
error: 'Array too large.' error: 'Array too large.'
}) })
@@ -184,7 +165,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]
@@ -196,7 +177,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)
@@ -227,8 +208,8 @@ class UtilRoute {
* *
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -X POST "https://api.fullstack.cash/v4/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", "balanceOnly": true}'
* curl -X POST "https://api.fullstack.cash/v4/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "toAddr": "bitcoincash:qpt8m4kqu963geedyrur6pdggqmv5kxwnq0rn322qu"}' * 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"}'
* *
* *
*/ */
@@ -262,11 +243,12 @@ 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.electrumx._balanceFromElectrumx(fromAddr) const balances = await _this.blockbook.balanceFromBlockbook(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 = balances.confirmed + balances.unconfirmed const totalBalance =
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) {
@@ -281,7 +263,7 @@ class UtilRoute {
} }
// Get all UTXOs help by the address. // Get all UTXOs help by the address.
const utxos = await _this.electrumx._utxosFromElectrumx(fromAddr) const utxos = await _this.blockbook.utxosFromBlockbook(fromAddr)
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`) // console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
const tokenUtxos = [] const tokenUtxos = []
@@ -386,7 +368,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
}) })
@@ -404,9 +386,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.value originalAmount = originalAmount + utxo.satoshis
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) transactionBuilder.addInput(utxo.txid, utxo.vout)
} }
if (originalAmount < 546) { if (originalAmount < 546) {
@@ -441,7 +423,7 @@ class UtilRoute {
ecPair, ecPair,
redeemScript, redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL, transactionBuilder.hashTypes.SIGHASH_ALL,
utxo.value utxo.satoshis
) )
} }
@@ -479,7 +461,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.'
@@ -501,9 +483,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.value originalAmount = originalAmount + utxo.satoshis
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos) transactionBuilder.addInput(utxo.txid, utxo.vout)
} }
if (originalAmount < 300) { if (originalAmount < 300) {
@@ -583,7 +565,7 @@ class UtilRoute {
ecPair, ecPair,
redeemScript, redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL, transactionBuilder.hashTypes.SIGHASH_ALL,
thisUtxo.value thisUtxo.satoshis
) )
} }
@@ -603,4 +585,20 @@ class UtilRoute {
} }
} }
module.exports = UtilRoute 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
}
@@ -5,10 +5,8 @@
'use strict' 'use strict'
const express = require('express') const express = require('express')
// const axios = require('axios')
const RouteUtils = require('../../util/route-utils') const routeUtils = require('./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()
-56
View File
@@ -154,68 +154,12 @@ 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
}
}
}
// Handle general Error objects.
if (err.message) {
return {
msg: err.message,
status: 422
}
}
return { msg: false, status: 500 } return { msg: false, status: 500 }
} catch (err) { } catch (err) {
console.error('unhandled error in route-utils.js/decodeError(): ', err)
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err) wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
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
+2 -12
View File
@@ -11,23 +11,13 @@ require('winston-daily-rotate-file')
var NETWORK = process.env.NETWORK var NETWORK = process.env.NETWORK
// Default 1 Megabyte
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
// Default 5 days.
// 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({
filename: `${__dirname}/../../logs/rest-${NETWORK}-%DATE%.log`, filename: `${__dirname}/../../logs/rest-${NETWORK}-%DATE%.log`,
datePattern: 'YYYY-MM-DD', datePattern: 'YYYY-MM-DD',
zippedArchive: false, zippedArchive: false,
maxSize: LOG_MAX_SIZE, maxSize: '1m',
maxFiles: LOG_MAX_FILES, maxFiles: '5d',
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
winston.format.json() winston.format.json()
+9 -55
View File
@@ -1,69 +1,23 @@
#!/bin/bash #!/bin/bash
# Docker containers for the below infrastructure is described here:
# https://psfoundation.cash/blog/cash-stack
# Pre-synced databases can be downloaded on the CashStrap page:
# https://fullstack.cash/cashstrap
# Which network are you using?
export NETWORK=mainnet
#export NETWORK=testnet
# Full node # Full node
export RPC_IP=<ip>:8332 export RPC_BASEURL=http://<full node ip>:8332/
export RPC_BASEURL=http://$RPC_IP/ export RPC_USERNAME=<RPC username>
export RPC_USERNAME=bitcoin export RPC_PASSWORD=<RPC password>
export RPC_PASSWORD=password export NETWORK=mainnet
# (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_URL=http://<SLPDB IP>:12300/ export SLPDB_URL=http://<SLPDB IP>:12300/
# Use the same address as SLPDB_URL if you don't have a separate whitelist server.
export SLPDB_PASS_WL=somelongpassword # Blockbook
export SLPDB_WHITELIST_URL=http://<SLPDB IP>:12300/ export BLOCKBOOK_URL=https://<Blockbook IP>:9131/
# slp-api alternative SLP validator using slp-validate: # Allow node.js to make network calls to https using self-signed certificate.
# https://github.com/Permissionless-Software-Foundation/slp-api export NODE_TLS_REJECT_UNAUTHORIZED=0
export SLP_API_URL=http://10.0.0.5:5001/
# Mainnet Fulcrum / ElectrumX # Mainnet Fulcrum / ElectrumX
export FULCRUM_URL=192.168.0.6 export FULCRUM_URL=192.168.0.6
export FULCRUM_PORT=50002 export FULCRUM_PORT=50002
# Redis DB - Used for rate limiting - customize to your own Redis installation.
export REDIS_PORT=6379
export REDIS_HOST=172.17.0.1
# JWT Token Secret
# This is used to verify JWT tokens generated with jwt-bch-api:
# https://github.com/Permissionless-Software-Foundation/jwt-bch-api
export TOKENSECRET=somelongpassword export TOKENSECRET=somelongpassword
# So that bch-api can call bch-js locally.
export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
# Basic Authentication password
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
# that originate froma domain on the whitelist.
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
# Rate Limits. Numbers are divided into 1000. e.g. 10000 / 500 = 20 RPM for ANON.
# Requests use the ANON rate limit if they fail to pass in a JWT token.
# ANON = 20 requests per minute (RPM)
export ANON_RATE_LIMIT=500
# 10 = 1000 RPM
export WHITELIST_RATE_LIMIT=100
# Set logging parameters
#1m means no more than 1 megabyte
export LOG_MAX_SIZE=1m
#5d means store no more than 5 days
export LOG_MAX_FILES=5d
npm start npm start
@@ -20,7 +20,7 @@ const assert = chai.assert
const sinon = require('sinon') const sinon = require('sinon')
const ElecrumxRoute = require('../../src/routes/v4/electrumx') const ElecrumxRoute = require('../../src/routes/v3/electrumx')
// Mocking data. // Mocking data.
const { mockReq, mockRes } = require('./mocks/express-mocks') const { mockReq, mockRes } = require('./mocks/express-mocks')
@@ -42,7 +42,7 @@ function expectRouteError (res, result, expectedError, code = 400) {
assert.equal(result.success, false) assert.equal(result.success, false)
} }
describe('#Electrumx', () => { describe('#ElectrumX Router', () => {
let req, res let req, res
let sandbox let sandbox
const electrumxRoute = new ElecrumxRoute() const electrumxRoute = new ElecrumxRoute()
@@ -199,11 +199,11 @@ describe('#Electrumx', () => {
}) })
describe('#getUtxos', () => { describe('#getUtxos', () => {
it('should throw 422 if address is empty', async () => { it('should throw 400 if address is empty', async () => {
const result = await electrumxRoute.getUtxos(req, res) const result = await electrumxRoute.getUtxos(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -233,7 +233,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getUtxos(req, res) const result = await electrumxRoute.getUtxos(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -594,7 +594,7 @@ describe('#Electrumx', () => {
assert.isArray(result.transactions) assert.isArray(result.transactions)
}) })
it('should throw 400 error if txid array is too large', async () => { it('should throw 429 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', 400) expectRouteError(res, result, 'Array too large', 429)
}) })
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 400 error if heights array is too large', async () => { it('should throw 429 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', 400) expectRouteError(res, result, 'Array too large', 429)
}) })
it('should get details for a single height', async () => { it('should get details for a single height', async () => {
@@ -1063,7 +1063,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getBalance(req, res) const result = await electrumxRoute.getBalance(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -1093,7 +1093,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getBalance(req, res) const result = await electrumxRoute.getBalance(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -1350,7 +1350,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getTransactions(req, res) const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -1380,7 +1380,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getTransactions(req, res) const result = await electrumxRoute.getTransactions(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -1645,7 +1645,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getMempool(req, res) const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
@@ -1675,7 +1675,7 @@ describe('#Electrumx', () => {
const result = await electrumxRoute.getMempool(req, res) const result = await electrumxRoute.getMempool(req, res)
// console.log(`result: ${util.inspect(result)}`) // console.log(`result: ${util.inspect(result)}`)
assert.equal(res.statusCode, 422, 'Expect 422 status code') assert.equal(res.statusCode, 400, 'Expect 400 status code')
assert.property(result, 'error') assert.property(result, 'error')
assert.include(result.error, 'Unsupported address format') assert.include(result.error, 'Unsupported address format')
+1122
View File
File diff suppressed because it is too large Load Diff
+2 -206
View File
@@ -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/v4/full-node/blockchain') const Blockchain = require('../../src/routes/v3/full-node/blockchain')
const uut = new Blockchain() const uut = new Blockchain()
const util = require('util') const util = require('util')
@@ -821,7 +821,6 @@ describe('#BlockchainRouter', () => {
]) ])
}) })
}) })
describe('getRawMempool()', () => { describe('getRawMempool()', () => {
it('should throw 503 when network issues', async () => { it('should throw 503 when network issues', async () => {
// Save the existing RPC URL. // Save the existing RPC URL.
@@ -843,7 +842,6 @@ describe('#BlockchainRouter', () => {
'Error message expected' 'Error message expected'
) )
}) })
it('returns proper error when downstream service stalls', async () => { it('returns proper error when downstream service stalls', async () => {
// Mock the timeout error. // Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' }) sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
@@ -858,7 +856,6 @@ describe('#BlockchainRouter', () => {
'Error message expected' 'Error message expected'
) )
}) })
it('returns proper error when downstream service is down', async () => { it('returns proper error when downstream service is down', async () => {
// Mock the timeout error. // Mock the timeout error.
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' }) sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
@@ -874,7 +871,7 @@ describe('#BlockchainRouter', () => {
) )
}) })
it('should GET /getRawMempool', async () => { it('should GET /getMempoolInfo', async () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
sandbox sandbox
@@ -889,7 +886,6 @@ describe('#BlockchainRouter', () => {
// Not sure what other assertions should be made here. // Not sure what other assertions should be made here.
}) })
}) })
describe('getMempoolEntrySingle()', () => { describe('getMempoolEntrySingle()', () => {
it('should throw 400 if txid is empty', async () => { it('should throw 400 if txid is empty', async () => {
const result = await uut.getMempoolEntrySingle(req, res) const result = await uut.getMempoolEntrySingle(req, res)
@@ -1823,204 +1819,4 @@ 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'
)
})
})
}) })
+1 -1
View File
@@ -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/v4/full-node/control') const ControlRoute = require('../../src/routes/v3/full-node/control')
const uut = new ControlRoute() const uut = new ControlRoute()
const sinon = require('sinon') const sinon = require('sinon')
+12 -12
View File
@@ -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/v4/encryption') const EncryptionRoute = require('../../src/routes/v3/encryption')
const encryptionRoute = new EncryptionRoute() const encryptionRoute = new EncryptionRoute()
// Mocking data. // Mocking data.
@@ -78,11 +78,11 @@ describe('#Encryption Router', () => {
// Mock the Insight URL for unit tests. // Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
sandbox sandbox
.stub(encryptionRoute.bchjs.Electrumx, 'transactions') .stub(encryptionRoute.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockFulcrumTxHistory) .resolves(mockData.mockBalance)
sandbox sandbox
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction') .stub(encryptionRoute.rawTransactions, 'getRawTransactionsFromNode')
.resolves([mockData.mockTxDetails2]) .resolves(mockData.mockTxDetails)
} }
const result = await encryptionRoute.getPublicKey(req, res) const result = await encryptionRoute.getPublicKey(req, res)
@@ -105,12 +105,12 @@ describe('#Encryption Router', () => {
// Mock the Insight URL for unit tests. // Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
sandbox sandbox
.stub(encryptionRoute.bchjs.Electrumx, 'transactions') .stub(encryptionRoute.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockFulcrumNoTxHistory) .resolves(mockData.mockNoTxHistory)
} }
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)
@@ -126,11 +126,11 @@ describe('#Encryption Router', () => {
// Mock the Insight URL for unit tests. // Mock the Insight URL for unit tests.
if (process.env.TEST === 'unit') { if (process.env.TEST === 'unit') {
sandbox sandbox
.stub(encryptionRoute.bchjs.Electrumx, 'transactions') .stub(encryptionRoute.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockFulcrumNoSendBalance) .resolves(mockData.mockNoSendBalance)
sandbox sandbox
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction') .stub(encryptionRoute.rawTransactions, 'getRawTransactionsFromNode')
.resolves([mockData.mockNoSendTx]) .resolves(mockData.mockNoSendTx)
} }
const result = await encryptionRoute.getPublicKey(req, res) const result = await encryptionRoute.getPublicKey(req, res)
@@ -4,14 +4,14 @@
'use strict' 'use strict'
const assert = require('chai').assert // const assert = require('chai').assert
// const axios = require('axios') // const axios = require('axios')
// 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 Price = require('../../../src/routes/v4/price') const Price = require('../../../src/routes/v3/price')
const price = new Price() const price = new Price()
const { mockReq, mockRes } = require('../mocks/express-mocks') const { mockReq, mockRes } = require('../mocks/express-mocks')
@@ -29,25 +29,6 @@ describe('#price', () => {
it('should get the USD price', async () => { it('should get the USD price', async () => {
const result = await price.getUSD(req, res) const result = await price.getUSD(req, res)
console.log(`result: ${util.inspect(result)}`) console.log(`result: ${util.inspect(result)}`)
assert.isNumber(result.usd)
})
})
describe('#getBCHAUSD', () => {
it('should get the USD price of BCHA', async () => {
const result = await price.getBCHAUSD(req, res)
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)
}) })
}) })
}) })
@@ -1,6 +1,7 @@
/* /*
These tests have been deprecated. To test bch-api rate limits, run the e2e This integration tests should be run against a live bch-api REST server. It
tests in the bch-js repository. tests to ensure the rate-limits are working as expected. Adjust the values
in the tests below to match the rate limit setting in your own installation.
*/ */
'use strict' 'use strict'
@@ -13,14 +14,14 @@ 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/v4/` // const SERVER = `http://192.168.0.36:12400/v3/`
const SERVER = 'http://localhost:3000/v4/' const SERVER = 'http://localhost:3000/v3/'
// const SERVER = 'https://api.fullstack.cash/v4/' // const SERVER = 'https://api.fullstack.cash/v3/'
//
// const TEST_JWT =
// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk'
describe('#JWT rate limits', () => { const TEST_JWT =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk'
describe('#rate limits', () => {
it('should get control/getNetworkInfo() with no auth', async () => { it('should get control/getNetworkInfo() with no auth', async () => {
const options = { const options = {
method: 'GET', method: 'GET',
@@ -35,24 +36,25 @@ describe('#JWT rate limits', () => {
assert.hasAnyKeys(result.data, ['version']) assert.hasAnyKeys(result.data, ['version'])
}) })
it('should trigger rate-limit handler if rate limits exceeds 20 request per minute', async () => { it('should trigger rate-limit handler if rate limits exceeds 5 request per minute', async () => {
try { try {
// Actual rate limit is 60 per minute X 4 nodes = 240 rpm.
const options = { const options = {
method: 'GET', method: 'GET',
url: `${SERVER}control/getNetworkInfo` url: `${SERVER}control/getNetworkInfo`
} }
const promises = [] const promises = []
for (let i = 0; i < 30; i++) { for (let i = 0; i < 5; i++) {
const promise = axios(options) const promise = axios(options)
promises.push(promise) promises.push(promise)
} }
await Promise.all(promises) await Promise.all(promises)
assert.fail('Unexpected result!') assert.equal(true, false, 'Unexpected result!')
} catch (err) { } catch (err) {
console.log('err: ', err) // console.log(`err.response: ${util.inspect(err.response)}`)
assert.equal(err.response.status, 429) assert.equal(err.response.status, 429)
assert.include(err.response.data.error, 'Too many requests') assert.include(err.response.data.error, 'Too many requests')
@@ -133,32 +135,32 @@ describe('#JWT rate limits', () => {
// } // }
// }) // })
// it('should unlock pro-tier for a valid JWT token', async () => { it('should unlock pro-tier for a valid JWT token', async () => {
// try { try {
// // Actual rate limit is 60 per minute X 4 nodes = 240 rpm. // Actual rate limit is 60 per minute X 4 nodes = 240 rpm.
// const options = { const options = {
// method: 'GET', method: 'GET',
// url: `${SERVER}control/`, url: `${SERVER}control/`,
// headers: { headers: {
// Authorization: `Token ${TEST_JWT}` Authorization: `Token ${TEST_JWT}`
// } }
// } }
//
// const promises = [] const promises = []
// for (let i = 0; i < 60; i++) { for (let i = 0; i < 60; i++) {
// const promise = axios(options) const promise = axios(options)
// promises.push(promise) promises.push(promise)
// } }
//
// await Promise.all(promises) await Promise.all(promises)
//
// // assert.equal(true, false, "Unexpected result!") // assert.equal(true, false, "Unexpected result!")
// assert.equal(true, true, 'Not throwing an error is a pass!') assert.equal(true, true, 'Not throwing an error is a pass!')
// } catch (err) { } catch (err) {
// console.log(`err.response: ${util.inspect(err.response)}`) console.log(`err.response: ${util.inspect(err.response)}`)
//
// assert.equal(true, false, 'Unexpected result!') assert.equal(true, false, 'Unexpected result!')
// } }
// // Override default timeout for this test. // Override default timeout for this test.
// }).timeout(20000) }).timeout(20000)
}) })
@@ -7,22 +7,21 @@
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 () => {
@@ -190,4 +189,3 @@ describe('#Raw-Transactions', () => {
}) })
}) })
}) })
*/
@@ -19,7 +19,7 @@ if (!process.env.SLPDB_URL) {
) )
} }
const SLP = require('../../../src/routes/v4/slp') const SLP = require('../../../src/routes/v3/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:
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0', '0a321bff9761f28e06a268b14711274bb77617410a16807bd0437ef234a072b1',
decimals: 8, decimals: 0,
tokenQty: 2 tokenQty: 2
} }
] ]
@@ -214,39 +214,6 @@ 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 =
@@ -291,171 +258,4 @@ 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/v4/services/slpdb') const SLPDB = require('../../../src/routes/v3/services/slpdb')
const slpdb = new SLPDB() const slpdb = new SLPDB()
describe('#slpdb', () => { describe('#slpdb', () => {
+1 -1
View File
@@ -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/v4/full-node/mining') const MiningRoute = require('../../src/routes/v3/full-node/mining')
const uut = new MiningRoute() const uut = new MiningRoute()
// const nock = require('nock') // HTTP mocking // const nock = require('nock') // HTTP mocking
@@ -285,86 +285,7 @@ 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,
@@ -375,6 +296,5 @@ module.exports = {
mockBlockHeader, mockBlockHeader,
mockTxOut, mockTxOut,
mockTxOutProof, mockTxOutProof,
mockAncestors, mockAncestors
mockBlockInfo
} }
+179
View File
@@ -0,0 +1,179 @@
/*
Mock data for the encryption test library
*/
const mockBalance = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0',
balance: '582434',
totalReceived: '213083250',
totalSent: '212500816',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 12,
txids: [
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a',
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb',
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f',
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d',
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c',
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262',
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d',
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368',
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0',
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975',
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
]
}
const mockTxDetails = {
txid: '1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb',
hash: '1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb',
version: 1,
size: 437,
locktime: 0,
vin: [
{
txid: 'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a',
vout: 0,
scriptSig: {
asm:
'30450221008052d3b067418d53585fb8f91e1b57cf3c040dc9c07a70f393ed663b3f7502c50220749aa8e09ac922e78cb474c8097873cfb2634108d7acaa7db32a73a35743da97[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
hex:
'4830450221008052d3b067418d53585fb8f91e1b57cf3c040dc9c07a70f393ed663b3f7502c50220749aa8e09ac922e78cb474c8097873cfb2634108d7acaa7db32a73a35743da974141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
},
sequence: 4294967295
},
{
txid: 'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f',
vout: 1,
scriptSig: {
asm:
'3044022050d7fe7cdcec81eefa0987b88ddb83274d8e9063d927090dc4c2d1db76c512d302207dc1eea439a627476265ed87f59cc9823fb572ffc2640f0218d7bddc9a621c6e[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
hex:
'473044022050d7fe7cdcec81eefa0987b88ddb83274d8e9063d927090dc4c2d1db76c512d302207dc1eea439a627476265ed87f59cc9823fb572ffc2640f0218d7bddc9a621c6e4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.47,
n: 0,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 7ab928d0b41194411a2e87a782b688c7cc69ba46 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9147ab928d0b41194411a2e87a782b688c7cc69ba4688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qpatj2xsksgegsg696r60q4k3rruc6d6gc3srp333v']
}
},
{
value: 0.00531373,
n: 1,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 f3707320bbb4a28759a78a5ad63a77a2f5d462ec OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a914f3707320bbb4a28759a78a5ad63a77a2f5d462ec88ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0']
}
}
],
hex:
'01000000029a1c67eea78c7b8830dd8ed079239994e0df10f19ea83e34eea2bad3168f2fc4000000008b4830450221008052d3b067418d53585fb8f91e1b57cf3c040dc9c07a70f393ed663b3f7502c50220749aa8e09ac922e78cb474c8097873cfb2634108d7acaa7db32a73a35743da974141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff0f501c1be6601b0ed29bd579ae5be8e54842d3a3a2172734fc423fff48aca0e4010000008a473044022050d7fe7cdcec81eefa0987b88ddb83274d8e9063d927090dc4c2d1db76c512d302207dc1eea439a627476265ed87f59cc9823fb572ffc2640f0218d7bddc9a621c6e4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff02c029cd02000000001976a9147ab928d0b41194411a2e87a782b688c7cc69ba4688acad1b0800000000001976a914f3707320bbb4a28759a78a5ad63a77a2f5d462ec88ac00000000',
blockhash: '0000000000000000045e5e52fb4f9746b3d15d3062855fd346aaef3debef4360',
confirmations: 71465,
time: 1545564654,
blocktime: 1545564654
}
const mockNoTxHistory = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qqwfpk04ecf69wuprj9yjys9rla5mk7rj5j8uthqel',
balance: '0',
totalReceived: '0',
totalSent: '0',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 0
}
const mockNoSendBalance = {
page: 1,
totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg',
balance: '0',
totalReceived: '0',
totalSent: '0',
unconfirmedBalance: '600',
unconfirmedTxs: 1,
txs: 0,
txids: [
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
]
}
const mockNoSendTx = {
txid: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
hash: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
version: 2,
size: 226,
locktime: 0,
vin: [
{
txid: '681cc1d392975a7e78cbd490b8a940d7eb029c554f2cee332e3a7119d77fb189',
vout: 1,
scriptSig: {
asm: '30450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f[ALL|FORKID] 02bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7',
hex: '4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.000006,
n: 0,
scriptPubKey: {
asm: 'OP_DUP OP_HASH160 3c79ba54317c4be9cbfa5aaf357f74b97f3d9696 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: [
'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg'
]
}
},
{
value: 0.00003664,
n: 1,
scriptPubKey: {
asm: 'OP_DUP OP_HASH160 58502a73888e0a386fc69b44850af4b2a86ac9a6 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: [
'bitcoincash:qpv9q2nn3z8q5wr0c6d5fpg27je2s6kf5cavp4ney0'
]
}
}
],
hex: '020000000189b17fd719713a2e33ee2c4f559c02ebd740a9b890d4cb787e5a9792d3c11c68010000006b4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7ffffffff0258020000000000001976a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac500e0000000000001976a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac00000000'
}
module.exports = {
mockBalance,
mockTxDetails,
mockNoTxHistory,
mockNoSendBalance,
mockNoSendTx
}
@@ -216,26 +216,6 @@ const mockCoinbaseFeed = {
} }
} }
const mockCoinexFeed = {
code: 0,
data: {
date: 1605649499848,
ticker: {
vol: '26717.39062407',
low: '10.4000',
open: '11.8000',
high: '19.0029',
last: '18.5000',
buy: '18.0100',
buy_amount: '200.00000000',
sell: '18.5000',
sell_amount: '47.89580474'
}
},
message: 'OK'
}
module.exports = { module.exports = {
mockCoinbaseFeed, mockCoinbaseFeed
mockCoinexFeed
} }
+545
View File
@@ -0,0 +1,545 @@
/*
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
}
@@ -13,70 +13,75 @@ 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 = {
confirmed: 2546, page: 1,
unconfirmed: 0 totalPages: 1,
itemsOnPage: 1000,
address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv33u3mdxyc77j3u6k',
balance: '2546',
totalReceived: '2546',
totalSent: '0',
unconfirmedBalance: '0',
unconfirmedTxs: 0,
txs: 2,
txids: [
'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
'44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6'
]
} }
const mockUtxos = [ const mockUtxos = [
{ {
tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
tx_pos: 0, vout: 0,
value: 2000, value: '2000',
height: 605873 height: 605873,
confirmations: 298,
satoshis: 2000
}, },
{ {
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
tx_pos: 1, vout: 1,
value: 546, value: '546',
height: 605873 height: 605873,
confirmations: 298,
satoshis: 546
} }
] ]
const mockThreeUtxos = [ const mockThreeUtxos = [
{ {
tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56', txid: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
tx_pos: 0, vout: 0,
value: 2000, value: '2000',
height: 605873 height: 605873,
confirmations: 298,
satoshis: 2000
}, },
{ {
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
tx_pos: 1, vout: 1,
value: 546, value: '546',
height: 605873 height: 605873,
confirmations: 298,
satoshis: 546
}, },
{ {
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
tx_pos: 1, vout: 1,
value: 546, value: '546',
height: 605873 height: 605873,
confirmations: 298,
satoshis: 546
} }
] ]
const mockIsTokenUtxos = [ const mockIsTokenUtxos = [
false, false,
{ {
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6', txid: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
tx_out: 1, vout: 1,
value: 546, value: '546',
height: 605873, height: 605873,
confirmations: 298, confirmations: 298,
satoshis: 546, satoshis: 546,
+1 -133
View File
@@ -13,7 +13,7 @@ const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const sinon = require('sinon') const sinon = require('sinon')
const Price = require('../../src/routes/v4/price') const Price = require('../../src/routes/v3/price')
let uut let uut
// Mocking data. // Mocking data.
@@ -211,136 +211,4 @@ describe('#PriceRouter', () => {
assert.property(result, 'error') assert.property(result, 'error')
}) })
}) })
describe('#getBCHAUSD', () => {
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
it('should throw 500 when network issues', async () => {
uut.coinexPriceUrl = 'http://fakeurl/api/'
await uut.getBCHAUSD(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.getBCHAUSD(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.getBCHAUSD(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.getBCHAUSD(req, res)
// console.log(`result: ${util.inspect(result)}`)
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)
})
})
}) })
+460
View File
@@ -0,0 +1,460 @@
'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 300 points for anonymous user', () => {
const result = rateLimits.calcPoints()
// console.log(`result: ${result}`)
assert.equal(result, 300)
})
it('should return 100 points for free tier requesting full node access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'blockchain',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 100)
})
it('should return 100 points for free tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 100)
})
it('should return 100 points for free tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 10,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 100)
})
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 100 points for full-node tier requesting indexer access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'blockbook',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 100)
})
it('should return 100 points for full node tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 20,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 100)
})
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 100 points for indexer tier requesting SLPDB access', () => {
const jwtInfo = {
apiLevel: 30,
resource: 'slp',
id: '5e3a0415eb29a962da2708b4'
}
const result = rateLimits.calcPoints(jwtInfo)
assert.equal(result, 100)
})
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 10 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 < 12; 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, 300)
})
})
})
// 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
// }
@@ -14,7 +14,7 @@
const chai = require('chai') const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const Rawtransactions = require('../../src/routes/v4/full-node/rawtransactions') const Rawtransactions = require('../../src/routes/v3/full-node/rawtransactions')
const uut = new Rawtransactions() const uut = new Rawtransactions()
// const nock = require('nock') // HTTP mocking // const nock = require('nock') // HTTP mocking
@@ -250,7 +250,6 @@ 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'
@@ -1005,13 +1004,8 @@ describe('#Raw-Transactions', () => {
// Integration test // Integration test
} else { } else {
if (process.env.ISBCHN) { assert.hasAllKeys(result, ['error'])
assert.hasAllKeys(result, ['error']) assert.include(result.error, 'Missing inputs')
assert.include(result.error, 'Missing inputs')
} else {
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'bad-txns-inputs-missingorspent')
}
} }
}) })
}) })
@@ -1047,12 +1041,10 @@ 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'
@@ -1062,7 +1054,6 @@ 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,
@@ -1168,13 +1159,8 @@ describe('#Raw-Transactions', () => {
// Integration test // Integration test
} else { } else {
if (process.env.ISBCHN) { assert.hasAllKeys(result, ['error'])
assert.hasAllKeys(result, ['error']) assert.include(result.error, 'Missing inputs')
assert.include(result.error, 'Missing inputs')
} else {
assert.hasAllKeys(result, ['error'])
assert.include(result.error, 'bad-txns-inputs-missingorspent')
}
} }
}) })
}) })
File diff suppressed because it is too large Load Diff
+33 -46
View File
@@ -4,24 +4,16 @@
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'
// Public npm libraries const chai = require('chai')
const assert = require('chai').assert const assert = chai.assert
const utilRoute = require('../../src/routes/v3/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.
@@ -32,16 +24,13 @@ const util = require('util')
util.inspect.defaultOptions = { depth: 1 } util.inspect.defaultOptions = { depth: 1 }
// const UtilRoute = utilRoute.UtilRoute // const UtilRoute = utilRoute.UtilRoute
// const electrumx = new Electrumx() const utilRouteInst = new utilRoute.UtilRoute()
const utilRouteInst = new UtilRoute({ electrumx: {} })
describe('#Util', () => { describe('#Util', () => {
let req, res let req, res
let sandbox let sandbox
let electrumx
let utilRoute
before(async () => { before(() => {
// Save existing environment variables. // Save existing environment variables.
originalEnvVars = { originalEnvVars = {
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL, BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
@@ -58,9 +47,6 @@ 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.
@@ -78,8 +64,6 @@ describe('#Util', () => {
if (!nock.isActive()) nock.activate() if (!nock.isActive()) nock.activate()
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
utilRoute = new UtilRoute({ electrumx })
}) })
afterEach(() => { afterEach(() => {
@@ -373,29 +357,29 @@ describe('#Util', () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
sandbox sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx') .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx') .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
.resolves(mockData.mockUtxos) .resolves(mockData.mockUtxos)
sandbox sandbox
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .stub(utilRouteInst.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(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid') .resolves('test-txid')
req.body = { req.body = {
wif: 'L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq', wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
toAddr: 'bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r' toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
} }
const result = await utilRoute.sweepWif(req, res) const result = await utilRouteInst.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')
@@ -405,13 +389,13 @@ describe('#Util', () => {
// Mock the RPC call for unit tests. // Mock the RPC call for unit tests.
sandbox sandbox
.stub(utilRoute.electrumx, '_balanceFromElectrumx') .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
.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(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid') .resolves('test-txid')
req.body = { req.body = {
@@ -419,30 +403,33 @@ describe('#Util', () => {
balanceOnly: true balanceOnly: true
} }
const result = await utilRoute.sweepWif(req, res) const result = await utilRouteInst.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(utilRoute.electrumx, '_balanceFromElectrumx') .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx') .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
.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(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .stub(utilRouteInst.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(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid') .resolves('test-txid')
req.body = { req.body = {
@@ -450,7 +437,7 @@ describe('#Util', () => {
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
} }
const result = await utilRoute.sweepWif(req, res) const result = await utilRouteInst.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')
@@ -458,22 +445,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(utilRoute.electrumx, '_balanceFromElectrumx') .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx') .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
.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(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .stub(utilRouteInst.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(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid') .resolves('test-txid')
req.body = { req.body = {
@@ -481,7 +468,7 @@ describe('#Util', () => {
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
} }
const result = await utilRoute.sweepWif(req, res) const result = await utilRouteInst.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)
@@ -494,22 +481,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(utilRoute.electrumx, '_balanceFromElectrumx') .stub(utilRouteInst.blockbook, 'balanceFromBlockbook')
.resolves(mockData.mockBalance) .resolves(mockData.mockBalance)
sandbox sandbox
.stub(utilRoute.electrumx, '_utxosFromElectrumx') .stub(utilRouteInst.blockbook, 'utxosFromBlockbook')
.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(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails') .stub(utilRouteInst.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(utilRoute.bchjs.RawTransactions, 'sendRawTransaction') .stub(utilRouteInst.bchjs.RawTransactions, 'sendRawTransaction')
.resolves('test-txid') .resolves('test-txid')
req.body = { req.body = {
@@ -517,7 +504,7 @@ describe('#Util', () => {
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p' toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
} }
const result = await utilRoute.sweepWif(req, res) const result = await utilRouteInst.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)
+1 -1
View File
@@ -10,7 +10,7 @@
const chai = require('chai') const chai = require('chai')
const assert = chai.assert const assert = chai.assert
const xpubRoute = require('../../src/routes/v4/xpub') const xpubRoute = require('../../src/routes/v3/xpub')
const nock = require('nock') // HTTP mocking const nock = require('nock') // HTTP mocking
// let originalUrl // Used during transition from integration to unit tests. // let originalUrl // Used during transition from integration to unit tests.
-77
View File
@@ -1,77 +0,0 @@
/*
These integration tests need to be run against a live SLPDB. They query
against a live SLPDB and test the results against known token stats.
*/
'use strict'
const chai = require('chai')
const assert = chai.assert
// const axios = require('axios')
// Used for debugging.
const util = require('util')
util.inspect.defaultOptions = { depth: 1 }
// Exit if SLPDB URL is not defined.
if (!process.env.SLPDB_URL) {
throw new Error(
'SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.'
)
}
const SLP = require('../../../src/routes/v4/slp')
const slp = new SLP()
const { mockReq, mockRes } = require('../mocks/express-mocks')
describe('#nft', () => {
let req, res
beforeEach(() => {
// Mock the req and res objects used by Express routes.
req = mockReq
res = mockRes
})
describe('#getNftChildren', () => {
it('should get NFT children list', async () => {
req.params.tokenId =
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
const result = await slp.getNftChildren(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.isArray(result.nftChildren)
assert.equal(result.nftChildren.length, 2)
assert.equal(
result.nftChildren[0],
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
)
assert.equal(
result.nftChildren[1],
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
)
})
})
describe('#getNftGroup', () => {
it('should get NFT group token info', async () => {
req.params.tokenId =
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
// req.params.tokenId = '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
const result = await slp.getNftGroup(req, res)
// console.log(`result: ${util.inspect(result)}`)
assert.property(result, 'nftGroup')
assert.property(result.nftGroup, 'id')
assert.property(result.nftGroup, 'versionType')
assert.property(result.nftGroup, 'symbol')
assert.equal(
result.nftGroup.id,
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
)
assert.equal(result.nftGroup.versionType, 129)
assert.equal(result.nftGroup.symbol, 'PSF.TEST.GROUP')
})
})
})
-195
View File
@@ -1,195 +0,0 @@
/*
Mock data for the encryption test library
*/
const mockNoSendTx = {
txid: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
hash: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
version: 2,
size: 226,
locktime: 0,
vin: [
{
txid: '681cc1d392975a7e78cbd490b8a940d7eb029c554f2cee332e3a7119d77fb189',
vout: 1,
scriptSig: {
asm:
'30450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f[ALL|FORKID] 02bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7',
hex:
'4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.000006,
n: 0,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 3c79ba54317c4be9cbfa5aaf357f74b97f3d9696 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg']
}
},
{
value: 0.00003664,
n: 1,
scriptPubKey: {
asm:
'OP_DUP OP_HASH160 58502a73888e0a386fc69b44850af4b2a86ac9a6 OP_EQUALVERIFY OP_CHECKSIG',
hex: '76a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac',
reqSigs: 1,
type: 'pubkeyhash',
addresses: ['bitcoincash:qpv9q2nn3z8q5wr0c6d5fpg27je2s6kf5cavp4ney0']
}
}
],
hex:
'020000000189b17fd719713a2e33ee2c4f559c02ebd740a9b890d4cb787e5a9792d3c11c68010000006b4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7ffffffff0258020000000000001976a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac500e0000000000001976a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac00000000'
}
const mockFulcrumTxHistory = {
success: true,
transactions: [
{
transactions: [
{
height: 511463,
tx_hash:
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
},
{
height: 511464,
tx_hash:
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
},
{
height: 513373,
tx_hash:
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
},
{
height: 513373,
tx_hash:
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
},
{
height: 560481,
tx_hash:
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
},
{
height: 560615,
tx_hash:
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
},
{
height: 561568,
tx_hash:
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
},
{
height: 561569,
tx_hash:
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
},
{
height: 561572,
tx_hash:
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
},
{
height: 561582,
tx_hash:
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
},
{
height: 562106,
tx_hash:
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
},
{
height: 562106,
tx_hash:
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
}
]
}
]
}
const mockTxDetails2 = {
txid: '7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
hash: '7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
version: 1,
size: 221,
locktime: 0,
vin: [
{
txid: 'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3',
vout: 0,
scriptSig: {
asm:
'30440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
hex:
'4730440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
},
sequence: 4294967295
}
],
vout: [
{
value: 0.01266023,
n: 0,
scriptPubKey: {
asm: 'OP_HASH160 91bacd6785e3f8c78d5f29b4020b94c753444130 OP_EQUAL',
hex: 'a91491bacd6785e3f8c78d5f29b4020b94c75344413087',
reqSigs: 1,
type: 'scripthash',
addresses: ['bitcoincash:pzgm4nt8sh3l33udtu5mgqstjnr4x3zpxqrrrndnw5']
}
}
],
hex:
'0100000001c3aecb09fdfb9aff9a1bc8424c4583579be13db15fc74342f47f4838950af0ef000000008a4730440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff01675113000000000017a91491bacd6785e3f8c78d5f29b4020b94c7534441308700000000',
blockhash: '000000000000000001aab6818b4ad0379a3f7a13940d9b3d4838b3dea1d0a083',
confirmations: 149222,
time: 1515088493,
blocktime: 1515088493
}
const mockFulcrumNoTxHistory = {
success: true,
transactions: [
{
transactions: [],
address: 'bitcoincash:qrgqqkky28jdkv3w0ctrah0mz3jcsnsklc34gtukrh'
}
]
}
const mockFulcrumNoSendBalance = {
success: true,
transactions: [
{
transactions: [
{
height: 633578,
tx_hash:
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
}
]
}
]
}
module.exports = {
mockNoSendTx,
mockFulcrumTxHistory,
mockTxDetails2,
mockFulcrumNoTxHistory,
mockFulcrumNoSendBalance
}
File diff suppressed because it is too large Load Diff
-506
View File
@@ -1,506 +0,0 @@
/*
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'
)
})
})
})
-34
View File
@@ -1,34 +0,0 @@
/*
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)
})
})
})