mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac96860c27 | ||
|
|
c51290acf5 | ||
|
|
2f520dcaec | ||
|
|
e0be1d308a | ||
|
|
9cd06630ea | ||
|
|
6b281e2842 | ||
|
|
2e143d5a9e | ||
|
|
eb817da044 | ||
|
|
1c88aa7d72 | ||
|
|
a9e976cc24 | ||
|
|
5644b53cf8 | ||
|
|
236637f0e0 | ||
|
|
fb1bb0b7ed | ||
|
|
8a91f3ae3a | ||
|
|
80dc296dee | ||
|
|
f01c82631b | ||
|
|
ef2a681edb | ||
|
|
44ce5f2aa9 | ||
|
|
f52a04a337 | ||
|
|
9aa3e5b52a | ||
|
|
71452f4834 | ||
|
|
96f1cb6ad4 | ||
|
|
36dd64db22 | ||
|
|
05de04a569 | ||
|
|
c661799095 | ||
|
|
4f416d6423 | ||
|
|
58dc4da92c | ||
|
|
7b8710d3da | ||
|
|
7976909dbb | ||
|
|
24b85e410d | ||
|
|
ae13d2c42e | ||
|
|
bf0fc2437a | ||
|
|
88d3dd12a1 | ||
|
|
cb8b4a5586 | ||
|
|
ec0ef24508 | ||
|
|
5a16e69719 | ||
|
|
154dbe8151 | ||
|
|
785d3059c3 | ||
|
|
21b19b59c9 | ||
|
|
588feff5db | ||
|
|
9fafc6fba3 | ||
|
|
8412da15db | ||
|
|
54c299e7fe | ||
|
|
a06399cfda | ||
|
|
e49c488e06 | ||
|
|
be07ffab33 | ||
|
|
41a5b24faf | ||
|
|
2172d0feff | ||
|
|
c4664ca173 | ||
|
|
11a7915cb8 | ||
|
|
06b944f36a | ||
|
|
9a8886121d | ||
|
|
89c6d48dfd |
@@ -28,6 +28,7 @@ test-fullstack-abc.sh
|
||||
test-fullstack-bchn.sh
|
||||
start-fullstack-abc.sh
|
||||
start-fullstack-bchn.sh
|
||||
start-ss-main.sh
|
||||
|
||||
coverage
|
||||
start-my-infra
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Chris Troutner
|
||||
Copyright (c) 2021 Permissionless Software Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# bch-api
|
||||
|
||||
This is a node.js JavaScript REST API server using Express.js. 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/Permissionless-Software-Foundation/bch-js) JavaScript library.
|
||||
[](https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/LICENSE.md)
|
||||
[](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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -20,7 +23,7 @@ The following features set this repository apart from rest.bitcoin.com:
|
||||
- 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.
|
||||
- Typescript removed and ES8 JavaScript used instead.
|
||||
- npm audit run on all dependencies.
|
||||
- `npm audit fix` frequently run to fix dependencies.
|
||||
|
||||
## Live Demo
|
||||
You can test a live demo of the REST API by running the
|
||||
@@ -31,17 +34,17 @@ You can bootstrap your own REST API server by downloading and installing the inf
|
||||
### Configure bch-js
|
||||
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:
|
||||
|
||||
- BCHN Mainnet REST API server: https://bchn.fullstack.cash/v3/
|
||||
- ABC Mainnet REST API server: https://abc.fullstack.cash/v3/
|
||||
- Testnet3 REST API server: https://testnet3.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
|
||||
|
||||
## Installation
|
||||
There are two installation paths, depending if you want a *development* or
|
||||
*production* environment. You'll also need to set up the underlying infrastructure
|
||||
described on [this page](https://troutsblog.com/research/bitcoin-cash/how-to-bch-full-stack-developer).
|
||||
described on [this page](https://psfoundation.cash/blog/cash-stack).
|
||||
|
||||
This code targets the Ubuntu 18.04 LTS Linux OS.
|
||||
This code targets the Ubuntu 18.04 LTS Linux OS or higher.
|
||||
|
||||
### Development
|
||||
This is a standard node.js project. The installation is as follows:
|
||||
@@ -86,7 +89,8 @@ Have questions? Need help? Join our community support
|
||||
## IPFS
|
||||
Copies of this repository will occasionally be uploaded and hosted on [IPFS](https://ipfs.io):
|
||||
|
||||
- v1.15.4: QmSAerFmDJm6KQp2HC8jsRR5MdL5X39hoUsJYvWNDvrD8m
|
||||
- v1.25.1: [QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX](https://ipfs.io/ipfs/QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX)
|
||||
|
||||
## License
|
||||
[MIT](./LICENSE.md)
|
||||
a
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@ const config = {
|
||||
: 'secret-jwt-token',
|
||||
|
||||
// Rate Limits
|
||||
anonRateLimit: process.env.ANON_RATE_LIMIT ? process.env.ANON_RATE_LIMIT : 50,
|
||||
anonRateLimit: process.env.ANON_RATE_LIMIT ? Number(process.env.ANON_RATE_LIMIT) : 50,
|
||||
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
||||
? process.env.WHITELIST_RATE_LIMIT
|
||||
? Number(process.env.WHITELIST_RATE_LIMIT)
|
||||
: 10,
|
||||
whitelistDomains: process.env.WHITELIST_DOMAINS
|
||||
? process.env.WHITELIST_DOMAINS.split(',')
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
# test
|
||||
# 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
|
||||
export RPC_BASEURL=http://172.17.0.1:8332/
|
||||
export RPC_IP=172.17.0.1:8332
|
||||
export RPC_BASEURL=http://$RPC_IP/
|
||||
export RPC_USERNAME=bitcoin
|
||||
export RPC_PASSWORD=password
|
||||
export NETWORK=mainnet
|
||||
|
||||
# SLPDB
|
||||
export SLPDB_PASS_GP=somelongpassword
|
||||
export SLPDB_URL=http://172.17.0.1:12300/
|
||||
export SLPDB_PASS=somelongpassword
|
||||
|
||||
# Blockbook Indexer
|
||||
export BLOCKBOOK_URL=https://172.17.0.1:9131/
|
||||
# Allow node.js to make network calls to https using self-signed certificate.
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
# Use the same address as SLPDB_URL if you don't have a separate whitelist server.
|
||||
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
|
||||
export FULCRUM_URL=172.17.0.1
|
||||
@@ -26,12 +34,31 @@ 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=somelongsecretvalue
|
||||
|
||||
# So that bch-api can call bch-js locally.
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v3/
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
|
||||
|
||||
# slp-api alternative SLP validator.
|
||||
export SLP_API_URL=http://10.0.0.5:5001/
|
||||
# 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
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
#!/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
|
||||
export RPC_BASEURL=http://172.17.0.1:18332/
|
||||
export RPC_IP=172.17.0.1:18332
|
||||
export RPC_BASEURL=http://$RPC_IP/
|
||||
export RPC_USERNAME=bitcoin
|
||||
export RPC_PASSWORD=password
|
||||
export NETWORK=testnet
|
||||
|
||||
# SLPDB
|
||||
export SLPDB_URL=http://172.17.0.1:13300/
|
||||
|
||||
# Blockbook Indexer
|
||||
export BLOCKBOOK_URL=https://172.17.0.1:19131/
|
||||
# Allow node.js to make network calls to https using self-signed certificate.
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
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
|
||||
export FULCRUM_URL=172.17.0.1
|
||||
export FULCRUM_PORT=50002
|
||||
export FULCRUM_PORT=60002
|
||||
|
||||
# Redis DB
|
||||
export REDIS_PORT=6380
|
||||
@@ -26,9 +36,20 @@ export REDIS_HOST=172.17.0.1
|
||||
export TOKENSECRET=somelongsecretvalue
|
||||
|
||||
# So that bch-api can call bch-js locally.
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v3/
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
|
||||
|
||||
# slp-api alternative SLP validator.
|
||||
export SLP_API_URL=http://10.0.0.5:5001/
|
||||
# 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
|
||||
|
||||
Generated
+19352
-1116
File diff suppressed because it is too large
Load Diff
+6
-14
@@ -22,15 +22,15 @@
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/routes/v4 -o docs",
|
||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --timeout 25000 -g '#generateSendOpReturn()' test/v4/integration/",
|
||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha --exit --timeout 25000 -g '#hydrateUtxosWL' test/v4/integration/",
|
||||
"test:temp2": "mocha test/v4/rate-limits.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "^4.5.2",
|
||||
"apidoc": "^0.23.0",
|
||||
"@psf/bch-js": "^4.15.8",
|
||||
"apidoc": "^0.26.0",
|
||||
"axios": "^0.21.1",
|
||||
"bitcore-lib-cash": "^8.23.1",
|
||||
"body-parser": "^1.18.3",
|
||||
@@ -45,7 +45,6 @@
|
||||
"helmet": "^3.21.2",
|
||||
"ioredis": "^4.14.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"level": "^6.0.0",
|
||||
"mkdirp": "^1.0.0",
|
||||
"mocha": "^7.1.1",
|
||||
"morgan": "^1.9.1",
|
||||
@@ -53,8 +52,6 @@
|
||||
"passport": "^0.4.0",
|
||||
"passport-anonymous": "^1.0.1",
|
||||
"passport-http": "^0.3.0",
|
||||
"pg": "^8.0.0",
|
||||
"pg-hstore": "^2.3.2",
|
||||
"rate-limiter-flexible": "^2.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^4.0.0"
|
||||
@@ -69,17 +66,12 @@
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"nock": "^12.0.0",
|
||||
"node-mocks-http": "^1.7.0",
|
||||
"nock": "^13.0.5",
|
||||
"nyc": "^15.0.0",
|
||||
"prettier": "^2.0.0",
|
||||
"proxyquire": "^2.1.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"semantic-release": "^17.3.0",
|
||||
"semantic-release": "^17.3.9",
|
||||
"sinon": "^9.0.0",
|
||||
"standard": "^14.3.1",
|
||||
"typescript": "^3.1.4"
|
||||
"standard": "^14.3.1"
|
||||
},
|
||||
"release": {
|
||||
"publish": [
|
||||
|
||||
@@ -113,6 +113,27 @@ class RateLimits {
|
||||
err
|
||||
)
|
||||
}
|
||||
//
|
||||
} else if (req.body && req.body.usrObj) {
|
||||
// Same as above, but this code path is activated from internal calls to
|
||||
// bch-js, like hydrateUtxo(), which passes the user object from the
|
||||
// original API call.
|
||||
|
||||
try {
|
||||
decoded = _this.jwt.verify(
|
||||
req.body.usrObj.jwtToken,
|
||||
_this.config.apiTokenSecret
|
||||
)
|
||||
// console.log(`decoded: ${JSON.stringify(decoded, null, 2)}`)
|
||||
|
||||
userId = decoded.id
|
||||
} catch (err) {
|
||||
// This handler will be triggered if the JWT token does not match the
|
||||
// token secret.
|
||||
wlogger.error(
|
||||
'Error in route-ratelimit.js trying to decode JWT token in usrObj'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
wlogger.debug('No JWT token found!')
|
||||
}
|
||||
@@ -129,8 +150,10 @@ class RateLimits {
|
||||
const resource = _this.getResource(req.url)
|
||||
wlogger.debug(`resource: ${resource}`)
|
||||
|
||||
// Key will be the JWT ID if it exists, otherwise the IP address of the caller.
|
||||
let key = userId || req.ip
|
||||
res.locals.key = key // Feedback for tests.
|
||||
// console.log(`key: ${key}`)
|
||||
|
||||
// const pointsToConsume = userId ? 1 : 30
|
||||
decoded.resource = resource
|
||||
@@ -287,7 +310,9 @@ class RateLimits {
|
||||
|
||||
return retVal
|
||||
} catch (err) {
|
||||
wlogger.error('Error in route-ratelimit.js/isInWhitelist(). Returning false by default.')
|
||||
wlogger.error(
|
||||
'Error in route-ratelimit.js/isInWhitelist(). Returning false by default.'
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class Slp {
|
||||
_this.txsByAddressSingle
|
||||
)
|
||||
_this.router.post('/generateSendOpReturn', _this.generateSendOpReturn)
|
||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
// _this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
}
|
||||
|
||||
// DRY error handler.
|
||||
@@ -1621,6 +1621,7 @@ class Slp {
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
async hydrateUtxos (req, res, next) {
|
||||
try {
|
||||
const utxos = req.body.utxos
|
||||
|
||||
@@ -135,7 +135,7 @@ class Electrum {
|
||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
return res.json({ success: false, error: msg })
|
||||
}
|
||||
|
||||
// Handle error patterns specific to this route.
|
||||
|
||||
@@ -45,7 +45,7 @@ class Encryption {
|
||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
return res.json({ success: false, error: msg })
|
||||
}
|
||||
|
||||
// Handle error patterns specific to this route.
|
||||
@@ -92,6 +92,15 @@ class Encryption {
|
||||
})
|
||||
}
|
||||
|
||||
// Generate a user object that can be passed along with internal calls
|
||||
// from bch-js.
|
||||
const usrObj = {
|
||||
ip: req._remoteAddress,
|
||||
jwtToken: req.locals.jwtToken,
|
||||
proLimit: req.locals.proLimit,
|
||||
apiLevel: req.locals.apiLevel
|
||||
}
|
||||
|
||||
const cashAddr = _this.bchjs.Address.toCashAddress(address)
|
||||
|
||||
// Prevent a common user error. Ensure they are using the correct network address.
|
||||
@@ -110,7 +119,7 @@ class Encryption {
|
||||
cashAddr
|
||||
)
|
||||
|
||||
const rawTxData = await _this.bchjs.Electrumx.transactions(cashAddr)
|
||||
const rawTxData = await _this.bchjs.Electrumx.transactions(cashAddr, usrObj)
|
||||
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
|
||||
|
||||
// Extract just the TXIDs
|
||||
@@ -126,9 +135,12 @@ class Encryption {
|
||||
for (let i = 0; i < txids.length; i++) {
|
||||
const thisTx = txids[i]
|
||||
|
||||
// CT 2/24/21: I might want to convert this to the POST call, to take
|
||||
// advantage of the usrObj. It does not get passed in a GET call.
|
||||
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
|
||||
thisTx,
|
||||
true
|
||||
true,
|
||||
usrObj
|
||||
)
|
||||
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
|
||||
|
||||
|
||||
@@ -175,8 +175,17 @@ function decodeError (err) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle general Error objects.
|
||||
if (err.message) {
|
||||
return {
|
||||
message: err.message,
|
||||
status: 422
|
||||
}
|
||||
}
|
||||
|
||||
return { msg: false, status: 500 }
|
||||
} catch (err) {
|
||||
console.error('unhandled error in route-utils.js/decodeError(): ', err)
|
||||
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
|
||||
return { msg: false, status: 500 }
|
||||
}
|
||||
|
||||
+145
-16
@@ -17,7 +17,9 @@ const wlogger = require('../../util/winston-logging')
|
||||
const LOCAL_RESTURL = process.env.LOCAL_RESTURL
|
||||
? process.env.LOCAL_RESTURL
|
||||
: 'https://api.fullstack.cash/v4/'
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
// const BCHJS = require('../../../../bch-js')
|
||||
const bchjs = new BCHJS({ restURL: LOCAL_RESTURL })
|
||||
|
||||
// Used to convert error messages to strings, to safely pass to users.
|
||||
@@ -33,7 +35,10 @@ util.inspect.defaultOptions = { depth: 5 }
|
||||
|
||||
// Determine the Access password for a private instance of SLPDB.
|
||||
// https://gist.github.com/christroutner/fc717ca704dec3dded8b52fae387eab2
|
||||
const SLPDB_PASS = process.env.SLPDB_PASS ? process.env.SLPDB_PASS : 'BITBOX'
|
||||
// Password for General Purpose (GP) SLPDB.
|
||||
const SLPDB_PASS_GP = process.env.SLPDB_PASS_GP ? process.env.SLPDB_PASS_GP : 'BITBOX'
|
||||
// Password for Whitelist (WL) SLPDB.
|
||||
const SLPDB_PASS_WL = process.env.SLPDB_PASS_WL ? process.env.SLPDB_PASS_WL : 'BITBOX'
|
||||
|
||||
// const rawtransactions = require('./full-node/rawtransactions')
|
||||
const RawTransactions = require('./full-node/rawtransactions')
|
||||
@@ -41,9 +46,9 @@ const rawTransactions = new RawTransactions()
|
||||
|
||||
// Setup REST and TREST URLs used by slpjs
|
||||
// Dev note: this allows for unit tests to mock the URL.
|
||||
if (!process.env.REST_URL) process.env.REST_URL = 'https://rest.bitcoin.com/v2/'
|
||||
if (!process.env.REST_URL) process.env.REST_URL = 'https://bchn.fullstack.cash/v4/'
|
||||
if (!process.env.TREST_URL) {
|
||||
process.env.TREST_URL = 'https://trest.bitcoin.com/v2/'
|
||||
process.env.TREST_URL = 'https://testnet.fullstack.cash/v4/'
|
||||
}
|
||||
|
||||
let _this
|
||||
@@ -89,6 +94,7 @@ class Slp {
|
||||
)
|
||||
_this.router.post('/generateSendOpReturn', _this.generateSendOpReturn)
|
||||
_this.router.post('/hydrateUtxos', _this.hydrateUtxos)
|
||||
_this.router.post('/hydrateUtxosWL', _this.hydrateUtxosWL)
|
||||
_this.router.get('/status', _this.getStatus)
|
||||
}
|
||||
|
||||
@@ -431,7 +437,7 @@ class Slp {
|
||||
const b64 = Buffer.from(s).toString('base64')
|
||||
const url = `${process.env.SLPDB_URL}q/${b64}`
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsGP()
|
||||
// Request options
|
||||
const opt = {
|
||||
method: 'get',
|
||||
@@ -582,7 +588,7 @@ class Slp {
|
||||
}
|
||||
}
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsGP()
|
||||
|
||||
// Collect an array of promises, one for each request to slpserve.
|
||||
// This is a nested array of promises.
|
||||
@@ -804,7 +810,7 @@ class Slp {
|
||||
const b64 = Buffer.from(s).toString('base64')
|
||||
const url = `${process.env.SLPDB_URL}q/${b64}`
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsGP()
|
||||
const opt = {
|
||||
method: 'get',
|
||||
baseURL: url,
|
||||
@@ -993,7 +999,7 @@ class Slp {
|
||||
const url = `${process.env.SLPDB_URL}q/${b64}`
|
||||
// console.log('url: ', url)
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsGP()
|
||||
|
||||
// Get data from SLPDB.
|
||||
const opt = {
|
||||
@@ -1127,7 +1133,7 @@ class Slp {
|
||||
}
|
||||
}
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsGP()
|
||||
|
||||
const s = JSON.stringify(query)
|
||||
const b64 = Buffer.from(s).toString('base64')
|
||||
@@ -1305,6 +1311,7 @@ class Slp {
|
||||
async validate3Single (req, res, next) {
|
||||
try {
|
||||
const txid = req.params.txid
|
||||
// console.log('validate3Single txid: ', txid)
|
||||
|
||||
// Validate input
|
||||
if (!txid || txid === '') {
|
||||
@@ -1326,7 +1333,7 @@ class Slp {
|
||||
}
|
||||
}
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsWL()
|
||||
|
||||
const s = JSON.stringify(query)
|
||||
const b64 = Buffer.from(s).toString('base64')
|
||||
@@ -1386,6 +1393,7 @@ class Slp {
|
||||
async validate3Bulk (req, res, next) {
|
||||
try {
|
||||
const txids = req.body.txids
|
||||
// console.log(`validate3Bulk txids: `, txids)
|
||||
|
||||
// Reject if txids is not an array.
|
||||
if (!Array.isArray(txids)) {
|
||||
@@ -1419,7 +1427,7 @@ class Slp {
|
||||
const url = `${process.env.SLPDB_WHITELIST_URL}q/${b64}`
|
||||
// console.log('url: ', url)
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsWL()
|
||||
|
||||
// Get data from SLPDB.
|
||||
const opt = {
|
||||
@@ -1562,7 +1570,7 @@ class Slp {
|
||||
const b64 = Buffer.from(s).toString('base64')
|
||||
const url = `${process.env.SLPDB_URL}q/${b64}`
|
||||
|
||||
const options = _this.generateCredentials()
|
||||
const options = _this.generateCredentialsGP()
|
||||
const opt = {
|
||||
method: 'get',
|
||||
baseURL: url,
|
||||
@@ -1722,11 +1730,32 @@ class Slp {
|
||||
}
|
||||
|
||||
// Generates a Basic Authorization header for slpserve.
|
||||
generateCredentials () {
|
||||
generateCredentialsGP () {
|
||||
// Generate the Basic Authentication header for a private instance of SLPDB.
|
||||
const username = 'BITBOX'
|
||||
const password = SLPDB_PASS
|
||||
const password = SLPDB_PASS_GP
|
||||
const combined = `${username}:${password}`
|
||||
// console.log(`combined: ${combined}`)
|
||||
var base64Credential = Buffer.from(combined).toString('base64')
|
||||
var readyCredential = `Basic ${base64Credential}`
|
||||
|
||||
const options = {
|
||||
headers: {
|
||||
authorization: readyCredential
|
||||
},
|
||||
timeout: 15000
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Generates a Basic Authorization header for slpserve.
|
||||
generateCredentialsWL () {
|
||||
// Generate the Basic Authentication header for a private instance of SLPDB.
|
||||
const username = 'BITBOX'
|
||||
const password = SLPDB_PASS_WL
|
||||
const combined = `${username}:${password}`
|
||||
// console.log(`combined: ${combined}`)
|
||||
var base64Credential = Buffer.from(combined).toString('base64')
|
||||
var readyCredential = `Basic ${base64Credential}`
|
||||
|
||||
@@ -1950,6 +1979,18 @@ class Slp {
|
||||
try {
|
||||
const utxos = req.body.utxos
|
||||
|
||||
// console.log('req: ', req)
|
||||
// console.log(`req._remoteAddress: ${req._remoteAddress}`)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (!Array.isArray(utxos)) {
|
||||
res.status(422)
|
||||
@@ -1984,8 +2025,8 @@ class Slp {
|
||||
const theseUtxos = utxos[i].utxos
|
||||
|
||||
// Get SLP token details.
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(theseUtxos)
|
||||
// console.log('details : ', details)
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetails(theseUtxos, usrObj)
|
||||
// console.log('details: ', details)
|
||||
|
||||
// Replace the original UTXO data with the hydrated data.
|
||||
utxos[i].utxos = details
|
||||
@@ -1999,9 +2040,11 @@ class Slp {
|
||||
|
||||
// Decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
console.log('msg: ', msg)
|
||||
console.log('status: ', status)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg })
|
||||
return res.json({ error: msg, message: msg, success: false })
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
@@ -2011,6 +2054,92 @@ class Slp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /slp/hydrateUtxosWL/ hydrateUtxosWL
|
||||
* @apiName SLP hydrateUtxosWL
|
||||
* @apiGroup SLP
|
||||
* @apiDescription Hydrate UTXO data with SLP information, using only the whitelist SLPDB.
|
||||
*
|
||||
* This call is identical to `hydrateUtxos`, except it will only use the
|
||||
* filtered SLPDB with a whitelist. This results in faster performance, more
|
||||
* reliable uptime, but more frequent `isValid: null` values. Some use-cases
|
||||
* prioritize the speed and reliability over acceptance of a wide range of
|
||||
* SLP tokens.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -X POST "https://api.fullstack.cash/v4/slp/hydrateUtxosWL" -H "accept:application/json" -H "Content-Type: application/json" -d '{"utxos":[{"utxos":[{"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 3, "value": "6816", "height": 606848, "confirmations": 13, "satoshis": 6816}, {"txid": "d56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56","vout": 2, "value": "546", "height": 606848, "confirmations": 13, "satoshis": 546}]}]}'
|
||||
*
|
||||
*
|
||||
*/
|
||||
async hydrateUtxosWL (req, res, next) {
|
||||
try {
|
||||
const utxos = req.body.utxos
|
||||
|
||||
// Validate inputs
|
||||
if (!Array.isArray(utxos)) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Input must be an array.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!utxos.length) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array should not be empty'
|
||||
})
|
||||
}
|
||||
|
||||
if (utxos.length > 20) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Array too long, max length is 20'
|
||||
})
|
||||
}
|
||||
|
||||
if (!utxos[0].utxos) {
|
||||
res.status(422)
|
||||
return res.json({
|
||||
error: 'Each element in array should have a utxos property'
|
||||
})
|
||||
}
|
||||
|
||||
// Loop through each address and query the UTXOs for that element.
|
||||
for (let i = 0; i < utxos.length; i++) {
|
||||
const theseUtxos = utxos[i].utxos
|
||||
// console.log(`theseUtxos: ${JSON.stringify(theseUtxos, null, 2)}`)
|
||||
|
||||
// Get SLP token details.
|
||||
const details = await _this.bchjs.SLP.Utils.tokenUtxoDetailsWL(theseUtxos)
|
||||
// console.log('details : ', details)
|
||||
|
||||
// Replace the original UTXO data with the hydrated data.
|
||||
utxos[i].utxos = details
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json({ slpUtxos: utxos })
|
||||
} catch (err) {
|
||||
wlogger.error('Error in slp.js/hydrateUtxosWL().', err)
|
||||
console.error('Error in slp.js/hydrateUtxosWL().', err)
|
||||
|
||||
// Decode the error message.
|
||||
const { msg, status } = routeUtils.decodeError(err)
|
||||
console.log('msg: ', msg)
|
||||
console.log('status: ', status)
|
||||
if (msg) {
|
||||
res.status(status)
|
||||
return res.json({ error: msg, message: msg, success: false })
|
||||
}
|
||||
|
||||
res.status(500)
|
||||
return res.json({
|
||||
error: 'Error in hydrateUtxosWL()',
|
||||
message: 'Error in hydrateUtxosWL()'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /slp/status Get the health status of SLPDB
|
||||
* @apiName Get the health status of SLPDB
|
||||
|
||||
@@ -154,8 +154,17 @@ class RouteUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle general Error objects.
|
||||
if (err.message) {
|
||||
return {
|
||||
msg: err.message,
|
||||
status: 422
|
||||
}
|
||||
}
|
||||
|
||||
return { msg: false, status: 500 }
|
||||
} catch (err) {
|
||||
console.error('unhandled error in route-utils.js/decodeError(): ', err)
|
||||
wlogger.error('unhandled error in route-utils.js/decodeError(): ', err)
|
||||
return { msg: false, status: 500 }
|
||||
}
|
||||
|
||||
@@ -11,13 +11,19 @@ require('winston-daily-rotate-file')
|
||||
|
||||
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.
|
||||
var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
|
||||
|
||||
// Configure daily-rotation transport.
|
||||
var transport = new winston.transports.DailyRotateFile({
|
||||
filename: `${__dirname}/../../logs/rest-${NETWORK}-%DATE%.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxSize: '1m',
|
||||
maxFiles: '5d',
|
||||
maxSize: LOG_MAX_SIZE,
|
||||
maxFiles: LOG_MAX_FILES,
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
|
||||
+40
-13
@@ -1,37 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Full node
|
||||
export RPC_BASEURL=http://<full node ip>:8332/
|
||||
export RPC_USERNAME=<RPC username>
|
||||
export RPC_PASSWORD=<RPC password>
|
||||
# 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
|
||||
export RPC_IP=<ip>:8332
|
||||
export RPC_BASEURL=http://$RPC_IP/
|
||||
export RPC_USERNAME=bitcoin
|
||||
export RPC_PASSWORD=password
|
||||
|
||||
# SLPDB
|
||||
export SLPDB_PASS_GP=somelongpassword
|
||||
export SLPDB_URL=http://<SLPDB IP>:12300/
|
||||
export SLPDB_PASS=somelongpassword
|
||||
|
||||
# Use the same address as SLPDB_URL if you don't have a separate whitelist server.
|
||||
export SLPDB_PASS_WL=somelongpassword
|
||||
export SLPDB_WHITELIST_URL=http://<SLPDB IP>: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/
|
||||
|
||||
# Blockbook
|
||||
export BLOCKBOOK_URL=https://<Blockbook IP>:9131/
|
||||
# Allow node.js to make network calls to https using self-signed certificate.
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
# Mainnet Fulcrum / ElectrumX
|
||||
export FULCRUM_URL=192.168.0.6
|
||||
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
|
||||
|
||||
# So that bch-api can call bch-js locally.
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v3/
|
||||
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
|
||||
|
||||
@@ -199,11 +199,11 @@ describe('#Electrumx', () => {
|
||||
})
|
||||
|
||||
describe('#getUtxos', () => {
|
||||
it('should throw 400 if address is empty', async () => {
|
||||
it('should throw 422 if address is empty', async () => {
|
||||
const result = await electrumxRoute.getUtxos(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -233,7 +233,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getUtxos(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -1063,7 +1063,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getBalance(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -1093,7 +1093,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getBalance(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -1350,7 +1350,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getTransactions(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -1380,7 +1380,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getTransactions(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -1645,7 +1645,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getMempool(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
@@ -1675,7 +1675,7 @@ describe('#Electrumx', () => {
|
||||
const result = await electrumxRoute.getMempool(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
||||
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Unsupported address format')
|
||||
|
||||
@@ -7,21 +7,22 @@
|
||||
TODO:
|
||||
-/rawtransactions/sendRawTransaction is more appropropriate for an e2e test,
|
||||
so it is omitted here.
|
||||
- Replace request-promise with axios.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const chai = require('chai')
|
||||
const assert = chai.assert
|
||||
// const chai = require('chai')
|
||||
// const assert = chai.assert
|
||||
// const rawtransactions = require('../../../dist/routes/v2/rawtransactions')
|
||||
const rp = require('request-promise')
|
||||
// const rp = require('request-promise')
|
||||
|
||||
// Used for debugging.
|
||||
const util = require('util')
|
||||
util.inspect.defaultOptions = { depth: 1 }
|
||||
|
||||
// const mockData = require('../mocks/raw-transactions-mocks')
|
||||
|
||||
/*
|
||||
describe('#Raw-Transactions', () => {
|
||||
describe('#root', () => {
|
||||
it('should return root', async () => {
|
||||
@@ -189,3 +190,4 @@ describe('#Raw-Transactions', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
@@ -214,6 +214,39 @@ describe('#slp', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxosWL', () => {
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
tx_hash:
|
||||
'89b3f0c84efe8b01b24e2d7ac08636de5781f31dbb84478e3de868ca0a7ed93a',
|
||||
tx_pos: 1,
|
||||
value: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slp.hydrateUtxosWL(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
// Test the general structure of the output.
|
||||
assert.isArray(result.slpUtxos)
|
||||
assert.equal(result.slpUtxos.length, 1)
|
||||
assert.equal(result.slpUtxos[0].utxos.length, 1)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'tx_hash')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'tx_pos')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#validate2Single', () => {
|
||||
it('should invalidate a known invalid TXID', async () => {
|
||||
const txid =
|
||||
|
||||
+23
-19
@@ -425,25 +425,29 @@ describe('#route-ratelimits & jwt-auth', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle misconfigured token secret', async () => {
|
||||
// Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
rateLimits = new RateLimits()
|
||||
|
||||
req.baseUrl = '/v4'
|
||||
req.path = '/control/getNetworkInfo'
|
||||
req.url = req.path
|
||||
req.method = 'GET'
|
||||
|
||||
req.locals.jwtToken = 'some-token'
|
||||
|
||||
next.reset() // reset the stubbed next() function.
|
||||
|
||||
await rateLimits.rateLimitByResource(req, res, next)
|
||||
|
||||
// Issues with token secret should treat incoming requests as anonymous
|
||||
// calls with 30 points or 3 RPM.
|
||||
assert.equal(res.locals.pointsToConsume, 50)
|
||||
})
|
||||
// CT 2/24/21 This test may have been invalidated by the interal IP address
|
||||
// passing that I implemented to get hydrateUtxos() working properly.
|
||||
// I'm commenting this out until I can study the side effects of this change,
|
||||
// and why exactly this test is breaking.
|
||||
// it('should handle misconfigured token secret', async () => {
|
||||
// // Create a new instance of the rate limit so we start with zeroed tracking.
|
||||
// rateLimits = new RateLimits()
|
||||
//
|
||||
// req.baseUrl = '/v4'
|
||||
// req.path = '/control/getNetworkInfo'
|
||||
// req.url = req.path
|
||||
// req.method = 'GET'
|
||||
//
|
||||
// req.locals.jwtToken = 'some-token'
|
||||
//
|
||||
// next.reset() // reset the stubbed next() function.
|
||||
//
|
||||
// await rateLimits.rateLimitByResource(req, res, next)
|
||||
//
|
||||
// // Issues with token secret should treat incoming requests as anonymous
|
||||
// // calls with 50 points, or 20 RPM.
|
||||
// assert.equal(res.locals.pointsToConsume, 50)
|
||||
// })
|
||||
})
|
||||
|
||||
describe('#isInWhitelist', () => {
|
||||
|
||||
+175
@@ -1559,6 +1559,181 @@ describe('#SLP', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hydrateUtxosWL', () => {
|
||||
it('should throw error if input is not an array.', async () => {
|
||||
req.body.utxos = 'test'
|
||||
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
// console.log(`result: `, result)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Input must be an array')
|
||||
})
|
||||
|
||||
it('should throw error if Array is empty', async () => {
|
||||
req.body.utxos = []
|
||||
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array should not be empty')
|
||||
})
|
||||
|
||||
it('should throw error if Array is too long', async () => {
|
||||
const utxo = {
|
||||
txid:
|
||||
'bd158c564dd4ef54305b14f44f8e94c44b649f246dab14bcb42fb0d0078b8a90',
|
||||
vout: 3,
|
||||
amount: 0.00002015,
|
||||
satoshis: 2015,
|
||||
height: 594892,
|
||||
confirmations: 5
|
||||
}
|
||||
|
||||
const utxos = []
|
||||
|
||||
// Populate array with 21 utxos
|
||||
for (let i = 0; i < 21; i++) {
|
||||
utxos.push(utxo)
|
||||
}
|
||||
|
||||
req.body.utxos = utxos
|
||||
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(result.error, 'Array too long, max length is 20')
|
||||
})
|
||||
|
||||
it('should return utxo details', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
utxos: [
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
// Mock the external network call.
|
||||
sandbox.stub(slpRoute.bchjs.SLP.Utils, 'tokenUtxoDetailsWL').resolves([
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 3,
|
||||
value: '6816',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 6816,
|
||||
isValid: false
|
||||
},
|
||||
{
|
||||
txid:
|
||||
'd56a2b446d8149c39ca7e06163fe8097168c3604915f631bc58777d669135a56',
|
||||
vout: 2,
|
||||
value: '546',
|
||||
height: 606848,
|
||||
confirmations: 13,
|
||||
satoshis: 546,
|
||||
utxoType: 'token',
|
||||
transactionType: 'send',
|
||||
tokenId:
|
||||
'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d',
|
||||
tokenTicker: 'TAP',
|
||||
tokenName: 'Thoughts and Prayers',
|
||||
tokenDocumentUrl: '',
|
||||
tokenDocumentHash: '',
|
||||
decimals: 0,
|
||||
tokenType: 1,
|
||||
tokenQty: 5,
|
||||
isValid: true
|
||||
}
|
||||
])
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slpRoute.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, 2)
|
||||
|
||||
// Test the non-slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[0], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[0].isValid, false)
|
||||
|
||||
// Test the slp UTXO.
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'txid')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'vout')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'value')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'height')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'confirmations')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'satoshis')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'isValid')
|
||||
assert.equal(result.slpUtxos[0].utxos[1].isValid, true)
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'transactionType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenId')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenTicker')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenName')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentUrl')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenDocumentHash')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'decimals')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenType')
|
||||
assert.property(result.slpUtxos[0].utxos[1], 'tokenQty')
|
||||
})
|
||||
|
||||
it('should throw error for missing properties', async () => {
|
||||
const utxos = [
|
||||
{
|
||||
height: 639443,
|
||||
tx_hash:
|
||||
'30707fffb9b295a06a68d217f49c198e9e1dbe1edc3874a0928ca1905f1709df',
|
||||
tx_pos: 0,
|
||||
value: 6000
|
||||
},
|
||||
{
|
||||
height: 639443,
|
||||
tx_hash:
|
||||
'8962566e413501224d178a02effc89be5ac0d8e4195f617415d443dc4c38fe50',
|
||||
tx_pos: 1,
|
||||
value: 546
|
||||
}
|
||||
]
|
||||
|
||||
req.body.utxos = utxos
|
||||
const result = await slpRoute.hydrateUtxosWL(req, res)
|
||||
|
||||
assert.hasAllKeys(result, ['error'])
|
||||
assert.include(
|
||||
result.error,
|
||||
'Each element in array should have a utxos property'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getStatus', () => {
|
||||
it('should get the SLPDB status', async () => {
|
||||
if (process.env.TEST === 'unit') {
|
||||
|
||||
Reference in New Issue
Block a user