mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-21 16:52:04 -07:00
Merge branch 'master' into production
This commit is contained in:
+1
-1
@@ -42,6 +42,6 @@ logs/*
|
||||
docs/*
|
||||
docker/mainnet/start-local-mainnet.sh
|
||||
docker/testnet/start-local-testnet.sh
|
||||
|
||||
start-local-bchn.sh
|
||||
|
||||
!logs/README.md
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Permissionless Software Foundation
|
||||
Copyright (c) 2022 Permissionless Software Foundation developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -3,48 +3,41 @@
|
||||
[](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 is a REST API server, written in node.js JavaScript, using the [Express.js](https://expressjs.com/) framework. The purpose of this code is to create a REST API server that provides a common interface for developers to build blockchain-based business applications. This software works with the Bitcoin Cash (BCH) and eCash (XEC) blockchains.
|
||||
- High-level documentation is available on [CashStack.info](https://cashstack.info).
|
||||
|
||||
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.
|
||||
While developers are encouraged to run and manage their own infrastructure, cloud-based infrastructure is available via [FullStack.cash](https://fullstack.cash). Free and paid tiers are available. You can 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. [Code examples](https://github.com/Permissionless-Software-Foundation/bch-js-examples) for common use-cases are available, and the [minimal-slp-wallet](https://www.npmjs.com/package/minimal-slp-wallet) is a front end JavaScript library that incorporates bch-js and provides basic wallet functionality.
|
||||
|
||||
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).
|
||||

|
||||
|
||||
- [API Documentation](https://fullstack.cash/documentation)
|
||||
- [Example Code](https://fullstack.cash/examples)
|
||||
## Documentation
|
||||
All the software above is part of the [Cash Stack](https://cashstack.info).
|
||||
|
||||
- [CashStack](https://cashstack.info) high-level documentation.
|
||||
- [bch-api REST API reference](https://api.fullstack.cash/docs/)
|
||||
- [bch-js JavaScript API reference](https://bchjs.fullstack.cash/)
|
||||
- [bch-js example code](https://github.com/Permissionless-Software-Foundation/bch-js-examples)
|
||||
- [Additional documentation on FullStack.cash](https://fullstack.cash/documentation)
|
||||
- [Additional examples on FullStack.cash](https://fullstack.cash/examples)
|
||||
|
||||
Have questions? Need help? Join our community support:
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
## Features
|
||||
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 fix` frequently run to fix dependencies.
|
||||
|
||||
## Live Demo
|
||||
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).
|
||||
Rate limits are 20 requests per minute, but you can increase them to 100 with a [paid 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.
|
||||
- [Telegram channel](https://t.me/bch_js_toolkit)
|
||||
|
||||
### 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/v4/
|
||||
- ABC Mainnet REST API server: https://abc.fullstack.cash/v4/
|
||||
- Testnet3 REST API server: https://testnet3.fullstack.cash/v4/
|
||||
- BCH Mainnet REST API server: https://bchn.fullstack.cash/v5/
|
||||
- eCash Mainnet REST API server: https://abc.fullstack.cash/v5/
|
||||
- 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://psfoundation.cash/blog/cash-stack).
|
||||
described on [CashStack.info](https://cashstack.info).
|
||||
|
||||
This code targets the Ubuntu 18.04 LTS Linux OS or higher.
|
||||
This hardware target for this software is a PC running Ubuntu 18.04 LTS Linux OS or higher.
|
||||
|
||||
### Development
|
||||
This is a standard node.js project. The installation is as follows:
|
||||
@@ -69,7 +62,7 @@ For a production environment, a Docker container is provided in the
|
||||
containers target the Ubuntu 18.04 LTS Linux OS.
|
||||
|
||||
- Install Docker and Docker Compose by following the commands on
|
||||
[this Dev Ops page](https://troutsblog.com/research/dev-ops/overview).
|
||||
[this Dev Ops page](https://bafybeidpuq6pgpryd22wwykhqh2etw44pzofuncm5ldczk5v733tkwk55y.ipfs.dweb.link/docs/dev-ops/overview).
|
||||
|
||||
- Customize the [bash script](docker/mainnet/start-local-mainnet.sh) for your
|
||||
installation.
|
||||
@@ -80,10 +73,16 @@ installation.
|
||||
|
||||
- Run the Docker container with:
|
||||
|
||||
`docker-compose up`
|
||||
`docker-compose up -d`
|
||||
|
||||
## 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:
|
||||
Rate limiting of the REST API can be controlled in three different ways:
|
||||
- JWT tokens for individual users
|
||||
- Basic Authentication tokens for organization-wide usage.
|
||||
- Whitelisting for specific domain names
|
||||
|
||||
### JWT Tokens
|
||||
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://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 (without bch-js), you can then include the JWT token in the HTTP header like this:
|
||||
|
||||
- `Authorization: Token <JWT token>`
|
||||
|
||||
@@ -96,15 +95,26 @@ export BCHJSTOKEN=eyJhbGciOiJ....
|
||||
curl "https://bchn.fullstack.cash/v4/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -X GET -H "Content-Type: application/json" -H "Authorization: Token ${BCHJSTOKEN}"
|
||||
```
|
||||
|
||||
## Support
|
||||
Have questions? Need help? Join our community support
|
||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
||||
### Basic Authentication
|
||||
For private installations, Basic Authentication can be used to give everyone in an organization access to higher rate limits, while restricting use by the public. More than one Basic Authentication password can be generated by using the colon `:` to separate entries in the `PRO_PASS` environment variable. See the [bash script](docker/mainnet/start-local-mainnet.sh) for an example.
|
||||
|
||||
## IPFS
|
||||
Copies of this repository will occasionally be uploaded and hosted on [IPFS](https://ipfs.io):
|
||||
### Whitelists
|
||||
Website domain names can be added to the `WHITELIST_DOMAINS` environment variable. Multiple domains can be separated with a comma `,`. Any web app making a request from that domain will be granted top-level rate limits. See the [bash script](docker/mainnet/start-local-mainnet.sh) for an example.
|
||||
|
||||
## Donate
|
||||
|
||||
This open source software is developed and maintained by the [Permissionless Software Foundation](https://psfoundation.cash). If this library provides value to you, please consider making a donation to support the PSF developers:
|
||||
|
||||
<div align="center">
|
||||
<img src="./diagrams/donation-qr.png" />
|
||||
<p>bitcoincash:qqsrke9lh257tqen99dkyy2emh4uty0vky9y0z0lsr</p>
|
||||
</div>
|
||||
|
||||
|
||||
## Censorship Resistance
|
||||
Copies of this repository will occasionally be uploaded and hosted on the Filecoin blockchain, [IPFS](https://ipfs.io), and the [Radicle](https://radicle.xyz) p2p network.
|
||||
|
||||
- v1.25.1: [QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX](https://ipfs.io/ipfs/QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX)
|
||||
|
||||
## License
|
||||
[MIT](./LICENSE.md)
|
||||
a
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"sampleUrl": null
|
||||
|
||||
}
|
||||
"sampleUrl": null
|
||||
}
|
||||
|
||||
+4
-2
@@ -12,7 +12,7 @@ const config = {
|
||||
// Rate Limits
|
||||
anonRateLimit: process.env.ANON_RATE_LIMIT
|
||||
? Number(process.env.ANON_RATE_LIMIT)
|
||||
: 500,
|
||||
: 1000,
|
||||
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
||||
? Number(process.env.WHITELIST_RATE_LIMIT)
|
||||
: 10,
|
||||
@@ -24,7 +24,9 @@ const config = {
|
||||
: 10000,
|
||||
whitelistDomains: process.env.WHITELIST_DOMAINS
|
||||
? process.env.WHITELIST_DOMAINS.split(',')
|
||||
: ['fullstack.cash', 'psfoundation.cash', '10.0.']
|
||||
: ['fullstack.cash', 'psfoundation.cash', '10.0.'],
|
||||
|
||||
restURL: process.env.LOCAL_RESTURL ? process.env.LOCAL_RESTURL : 'http://127.0.0.1:3000/v5/'
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,19 +1,33 @@
|
||||
bch-api-main:
|
||||
build: .
|
||||
dockerfile: Dockerfile
|
||||
#image: bitcore
|
||||
container_name: bch-api-main
|
||||
links:
|
||||
- redis-main
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ../../logs:/home/safeuser/bch-api/logs
|
||||
restart: always
|
||||
version: '3.9'
|
||||
|
||||
redis-main:
|
||||
image: redis:latest
|
||||
container_name: redis-main
|
||||
restart: always
|
||||
ports:
|
||||
- "172.17.0.1:6379:6379"
|
||||
services:
|
||||
bch-api-main:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
#image: bitcore
|
||||
container_name: bch-api-main
|
||||
links:
|
||||
- redis-main
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ../../logs:/home/safeuser/bch-api/logs
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
restart: always
|
||||
|
||||
redis-main:
|
||||
image: redis:latest
|
||||
container_name: redis-main
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '10'
|
||||
restart: always
|
||||
ports:
|
||||
- "172.17.0.1:6379:6379"
|
||||
|
||||
Generated
+61
-557
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -18,9 +18,9 @@
|
||||
"lint": "standard --env mocha --fix",
|
||||
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v4/",
|
||||
"test-v5": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v5/",
|
||||
"test:integration": "mocha test/v4/integration",
|
||||
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js",
|
||||
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v4/integration/nft.js",
|
||||
"test:integration": "mocha test/v5/integration",
|
||||
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v5/integration/slp*.js",
|
||||
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v5/integration/nft.js",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs",
|
||||
@@ -31,7 +31,7 @@
|
||||
"node": ">=10.15.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "^4.20.7",
|
||||
"@psf/bch-js": "6.3.4",
|
||||
"apidoc": "^0.26.0",
|
||||
"axios": "^0.21.1",
|
||||
"bitcore-lib-cash": "^8.23.1",
|
||||
|
||||
+2
-2
@@ -90,8 +90,8 @@ const logStream = {
|
||||
}
|
||||
app.use(logger(morganFormat, { stream: logStream }))
|
||||
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(bodyParser.json({ limit: '50mb' }))
|
||||
app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' }))
|
||||
app.use(cookieParser())
|
||||
app.use(express.static(path.join(__dirname, 'public')))
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ const { RateLimiterRedis } = require('rate-limiter-flexible')
|
||||
const wlogger = require('../util/winston-logging')
|
||||
const config = require('../../config')
|
||||
|
||||
// The amount of delay in milliseconds to add to anonymous requests, in order
|
||||
// to slow down freeloaders that are hitting the system too hard.
|
||||
const ANON_DELAY = 3000
|
||||
|
||||
let _this // Global pointer to instance of class, when 'this' context is lost.
|
||||
|
||||
// Setup Redis to track rate limits for each user.
|
||||
@@ -213,6 +217,10 @@ class RateLimits {
|
||||
next()
|
||||
}
|
||||
|
||||
sleep (ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -267,6 +275,11 @@ class RateLimits {
|
||||
|
||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
||||
|
||||
// Add artificial delay to slow down freeloaders.
|
||||
if (pointsToConsume === ANON_LIMITS) {
|
||||
await this.sleep(ANON_DELAY)
|
||||
}
|
||||
|
||||
// Signal that the user has not exceeded their rate limits.
|
||||
return false
|
||||
} catch (err) {
|
||||
@@ -290,6 +303,11 @@ class RateLimits {
|
||||
}
|
||||
wlogger.info(logData)
|
||||
|
||||
// Add artificial delay to slow down freeloaders.
|
||||
if (pointsToConsume === ANON_LIMITS) {
|
||||
await this.sleep(ANON_DELAY)
|
||||
}
|
||||
|
||||
// Rate limited was triggered
|
||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||
return res.json({
|
||||
|
||||
@@ -115,6 +115,8 @@ class Electrum {
|
||||
cashAddr
|
||||
)
|
||||
|
||||
// console.log('req.locals: ', req.locals)
|
||||
|
||||
const response = await _this.axios.get(
|
||||
`${_this.fulcrumApi}electrumx/balance/${address}`
|
||||
)
|
||||
@@ -195,9 +197,18 @@ class Electrum {
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('req.locals: ', req.locals)
|
||||
|
||||
// Use the cache for anonymous users. If the user has a valid JWT token
|
||||
// or is using Basic Authentiation, then do not use the cache.
|
||||
let useCache = true
|
||||
if (req.locals.proLimit || req.locals.apiLevel > 10) {
|
||||
useCache = false
|
||||
}
|
||||
|
||||
const response = await _this.axios.post(
|
||||
`${_this.fulcrumApi}electrumx/balance/`,
|
||||
{ addresses }
|
||||
{ addresses, useCache }
|
||||
)
|
||||
|
||||
res.status(200)
|
||||
|
||||
@@ -7,18 +7,15 @@ const express = require('express')
|
||||
const router = express.Router()
|
||||
const axios = require('axios')
|
||||
const util = require('util')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
|
||||
// Local libraries
|
||||
const RouteUtils = require('../../util/route-utils')
|
||||
const routeUtils = new RouteUtils()
|
||||
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
const bchjs = new BCHJS()
|
||||
|
||||
// Local libraries
|
||||
// const wlogger = require('../../../util/winston-logging')
|
||||
const config = require('../../../config')
|
||||
|
||||
const routeUtils = new RouteUtils()
|
||||
const bchjs = new BCHJS({ restURL: config.restURL })
|
||||
|
||||
let _this
|
||||
|
||||
class PsfSlpIndexer {
|
||||
@@ -35,6 +32,7 @@ class PsfSlpIndexer {
|
||||
this.router.post('/address', this.getAddress)
|
||||
this.router.post('/txid', this.getTxid)
|
||||
this.router.post('/token', this.getTokenStats)
|
||||
this.router.post('/token/data', this.getTokenData)
|
||||
|
||||
_this = this
|
||||
}
|
||||
@@ -99,27 +97,29 @@ class PsfSlpIndexer {
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure the input is a valid BCH address.
|
||||
try {
|
||||
_this.bchjs.SLP.Address.toCashAddress(address)
|
||||
} catch (err) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
||||
})
|
||||
}
|
||||
if (!address.includes('ecash')) {
|
||||
// Ensure the input is a valid BCH address.
|
||||
try {
|
||||
_this.bchjs.SLP.Address.toCashAddress(address)
|
||||
} catch (err) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
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 cashAddr = _this.bchjs.SLP.Address.toCashAddress(address)
|
||||
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
|
||||
if (!networkIsValid) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error:
|
||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||
})
|
||||
// Prevent a common user error. Ensure they are using the correct network address.
|
||||
const cashAddr = _this.bchjs.SLP.Address.toCashAddress(address)
|
||||
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
|
||||
if (!networkIsValid) {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error:
|
||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const response = await _this.axios.post(
|
||||
@@ -224,6 +224,219 @@ class PsfSlpIndexer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /psf/slp/token/data Get token data
|
||||
* @apiName Get token data
|
||||
* @apiGroup PSF SLP
|
||||
* @apiDescription Get mutable and immutable data if the token contains them.
|
||||
*
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "f055256b938f1ecfa270459d6f12c7c8c82b66d3263c03d5074445a2b1a498a3" }' localhost:3000/v5/psf/slp/token/data
|
||||
*
|
||||
*
|
||||
*/
|
||||
// Get mutable and immutable data for a token, if the token was created with
|
||||
// such data.
|
||||
//
|
||||
// Example:
|
||||
// curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "f055256b938f1ecfa270459d6f12c7c8c82b66d3263c03d5074445a2b1a498a3" }' localhost:3000/v5/psf/slp/token/data
|
||||
async getTokenData (req, res, next) {
|
||||
try {
|
||||
// Verify env var is set for interacting with the indexer.
|
||||
_this.checkEnvVar()
|
||||
const tokenData = {}
|
||||
|
||||
const tokenId = req.body.tokenId
|
||||
if (!tokenId || tokenId === '') {
|
||||
res.status(400)
|
||||
return res.json({
|
||||
success: false,
|
||||
error: 'tokenId can not be empty'
|
||||
})
|
||||
}
|
||||
|
||||
// get token stats from the Genesis TX of the token.
|
||||
const withTxHistory = false
|
||||
const response = await _this.axios.post(
|
||||
`${_this.psfSlpIndexerApi}slp/token/`,
|
||||
{ tokenId, withTxHistory }
|
||||
)
|
||||
// console.log('response', response.data)
|
||||
|
||||
const tokenStats = response.data.tokenData
|
||||
|
||||
tokenData.genesisData = tokenStats
|
||||
|
||||
// try to get immutable data
|
||||
try {
|
||||
// const immutableData = await _this.getCIDData(tokenStats.documentUri)
|
||||
const immutableData = tokenStats.documentUri
|
||||
tokenData.immutableData = immutableData
|
||||
} catch (error) {
|
||||
tokenData.immutableData = ''
|
||||
}
|
||||
|
||||
// try to get mutable data
|
||||
try {
|
||||
const mutableData = await _this.getMutableData(tokenStats.documentHash)
|
||||
tokenData.mutableData = mutableData
|
||||
} catch (error) {
|
||||
tokenData.mutableData = ''
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
return res.json(tokenData)
|
||||
} catch (err) {
|
||||
console.log('Error in getTokenData(): ', err)
|
||||
return _this.errorHandler(err, res)
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieves the mutable data associated with a token document hash.
|
||||
async getMutableData (documentHash) {
|
||||
try {
|
||||
if (!documentHash || typeof documentHash !== 'string') {
|
||||
throw new Error(
|
||||
'documentHash string required when calling mutableData().'
|
||||
)
|
||||
}
|
||||
|
||||
// Get the OP_RETURN data and decode it.
|
||||
const mutableData = await _this.decodeOpReturn(documentHash)
|
||||
const jsonData = JSON.parse(mutableData)
|
||||
// console.log(`jsonData: ${JSON.stringify(jsonData, null, 2)}`)
|
||||
|
||||
const mutableDataAddr = jsonData.mda
|
||||
|
||||
// Gets the mutable data address (MDA) transaction history.
|
||||
const transactions = await _this.bchjs.Electrumx.transactions(mutableDataAddr)
|
||||
|
||||
const mdaTxs = transactions.transactions
|
||||
// console.log(`mdaTxs: ${JSON.stringify(mdaTxs, null, 2)}`)
|
||||
|
||||
let data = false
|
||||
// Maps each transaction of the mutableDataAddr.
|
||||
// If it finds an OP_RETURN, decode it. Exit the loop once the first
|
||||
// valid mutable data entry is found.
|
||||
// Start with the newest TXID entry and scan the history to find the first
|
||||
// entry with an IPFS CID.
|
||||
for (let i = mdaTxs.length - 1; i > -1; i--) {
|
||||
const tx = mdaTxs[i]
|
||||
const txid = tx.tx_hash
|
||||
console.log(`Retrieving and decoding txid ${txid}`)
|
||||
|
||||
data = await _this.decodeOpReturn(txid)
|
||||
// console.log('data: ', data)
|
||||
|
||||
// Try parse the OP_RETURN data to a JSON object.
|
||||
if (data) {
|
||||
try {
|
||||
// console.log('Mutable Data : ', data)
|
||||
|
||||
// Convert the OP_RETURN data to a JSON object.
|
||||
const obj = JSON.parse(data)
|
||||
console.log(`obj: ${JSON.stringify(obj, null, 2)}`)
|
||||
|
||||
// Keep searching if this TX does not have a cid value.
|
||||
if (!obj.cid) continue
|
||||
|
||||
// Ensure data was generated by the MDA
|
||||
const txData = await this.bchjs.Transaction.get(txid)
|
||||
const vinAddress = txData.txData.vin[0].address
|
||||
|
||||
// Skip entry if it was not made by the MDA private key.
|
||||
if (mutableDataAddr !== vinAddress) {
|
||||
console.log('Data is not generated by MDA, skipping.')
|
||||
data = false
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
} catch (error) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!data) {
|
||||
console.log('Mutable Data Not found ')
|
||||
}
|
||||
|
||||
// Get the CID:
|
||||
const obj = JSON.parse(data)
|
||||
const cid = obj.cid
|
||||
if (!cid) {
|
||||
throw new Error('CID could not be found in OP_RETURN data')
|
||||
}
|
||||
const result = cid
|
||||
|
||||
// const result = await _this.getCIDData(cid)
|
||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
// console.log('Error in getMutableData().')
|
||||
console.log('Error in getMutableData(): ', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes the OP_RETURN of a transaction if this exists
|
||||
async decodeOpReturn (txid) {
|
||||
try {
|
||||
if (!txid || typeof txid !== 'string') {
|
||||
throw new Error('txid must be a string.')
|
||||
}
|
||||
|
||||
// get transaction data
|
||||
const txData = await _this.bchjs.Electrumx.txData(txid)
|
||||
let data = false
|
||||
|
||||
// Maps the vout of the transaction in search of an OP_RETURN
|
||||
for (let i = 0; i < txData.details.vout.length; i++) {
|
||||
const vout = txData.details.vout[i]
|
||||
|
||||
const script = _this.bchjs.Script.toASM(
|
||||
Buffer.from(vout.scriptPubKey.hex, 'hex')
|
||||
).split(' ')
|
||||
|
||||
// Exit on the first OP_RETURN found.
|
||||
if (script[0] === 'OP_RETURN') {
|
||||
data = Buffer.from(script[1], 'hex').toString('ascii')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.log('Error in decodeOpReturn().')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Get the immutable data stored in the documentUrl field of the token.
|
||||
async getCIDData (cid) {
|
||||
try {
|
||||
if (!cid || typeof cid !== 'string') {
|
||||
throw new Error('cid must be a string.')
|
||||
}
|
||||
|
||||
// Assuming that CID starts with ipfs://. Cutting out that prefix.
|
||||
cid = cid.substring(7)
|
||||
|
||||
const dataUrl = `https://${cid}.ipfs.dweb.link/data.json`
|
||||
console.log(`dataUrl: ${dataUrl}`)
|
||||
|
||||
const response = await _this.axios.get(dataUrl)
|
||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.log('Error in getCIDData(): ', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Check the the environment variable is set correctly.
|
||||
checkEnvVar () {
|
||||
_this.psfSlpIndexerApi = process.env.SLP_INDEXER_API
|
||||
|
||||
+4
-14
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker containers for the below infrastructure is described here:
|
||||
# https://psfoundation.cash/blog/cash-stack
|
||||
# https://CashStack.info
|
||||
# Pre-synced databases can be downloaded on the CashStrap page:
|
||||
# https://fullstack.cash/cashstrap
|
||||
|
||||
@@ -20,20 +20,10 @@ export RPC_PASSWORD=password
|
||||
# double spends.
|
||||
#export RPC_SENDURL=http://$RPC_IP/
|
||||
|
||||
# SLPDB
|
||||
export SLPDB_PASS_GP=somelongpassword
|
||||
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
|
||||
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/
|
||||
# psf-slp-indxer
|
||||
export SLP_INDEXER_API=http://<ip>:<port>/
|
||||
|
||||
# Mainnet Fulcrum / ElectrumX
|
||||
export FULCRUM_URL=192.168.0.6
|
||||
export FULCRUM_PORT=50002
|
||||
# Fulcrum API (used for /v5+ routes)
|
||||
export FULCRUM_API=http://172.17.0.1:3001/v1/
|
||||
|
||||
# Redis DB - Used for rate limiting - customize to your own Redis installation.
|
||||
@@ -46,7 +36,7 @@ export REDIS_HOST=172.17.0.1
|
||||
export TOKENSECRET=somelongpassword
|
||||
|
||||
# So that bch-api can call bch-js locally.
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v4/
|
||||
export LOCAL_RESTURL=http://127.0.0.1:3000/v5/
|
||||
|
||||
# Basic Authentication password
|
||||
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
||||
|
||||
@@ -29,6 +29,61 @@ const tokenStats = {
|
||||
]
|
||||
}
|
||||
}
|
||||
// txData generated by MDA
|
||||
const txDataByMDA = {
|
||||
txData: {
|
||||
txid: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
hash: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
version: 2,
|
||||
size: 339,
|
||||
locktime: 0,
|
||||
vin: [
|
||||
{
|
||||
txid: '8370db30d94761ab9a11b71ecd22541151bf6125c8c613f0f6fab8ab794565a7',
|
||||
vout: 0,
|
||||
scriptSig: {
|
||||
asm: '304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4[ALL|FORKID] 02791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851',
|
||||
hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851'
|
||||
},
|
||||
sequence: 4294967295,
|
||||
address: 'bitcoincash:qrg77j4jf2pl7azgvzrz2z567ls464gkuuhplt30dp',
|
||||
value: 0.00051303,
|
||||
tokenQty: 0,
|
||||
tokenQtyStr: '0',
|
||||
tokenId: null
|
||||
}
|
||||
],
|
||||
vout: [
|
||||
{
|
||||
value: 0,
|
||||
n: 0,
|
||||
scriptPubKey: {
|
||||
asm: 'OP_RETURN 5262419 1 47454e45534953 54524f5554 54726f75742773207465737420746f6b656e 74726f757473626c6f672e636f6d 0 2 2 000000174876e800',
|
||||
hex: '6a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e800',
|
||||
type: 'nulldata'
|
||||
},
|
||||
tokenQtyStr: '0',
|
||||
tokenQty: 0
|
||||
}
|
||||
],
|
||||
hex: '0200000001a7654579abb8faf6f013c6c82561bf51115422cd1eb7119aab6147d930db7083000000006a47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851ffffffff040000000000000000476a04534c500001010747454e455349530554524f55541254726f75742773207465737420746f6b656e0e74726f757473626c6f672e636f6d4c000102010208000000174876e80022020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088ac22020000000000001976a914db4d39ceb7794ffe5d06855f249e1d3a7f1b024088accec20000000000001976a9145904159f2f69bfa63eefa712633a0d96dc2e7e8888ac00000000',
|
||||
blockhash: '0000000000000000009f65225a3e12e23a7ea057c869047e0f36563a1f410267',
|
||||
confirmations: 97398,
|
||||
time: 1581773131,
|
||||
blocktime: 1581773131,
|
||||
blockheight: 622414,
|
||||
isSlpTx: true,
|
||||
tokenTxType: 'GENESIS',
|
||||
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||
tokenType: 1,
|
||||
tokenTicker: 'TROUT',
|
||||
tokenName: "Trout's test token",
|
||||
tokenDecimals: 2,
|
||||
tokenUri: 'troutsblog.com',
|
||||
tokenDocHash: '',
|
||||
isValidSlp: true
|
||||
}
|
||||
}
|
||||
|
||||
const txData = {
|
||||
txData: {
|
||||
@@ -46,7 +101,7 @@ const txData = {
|
||||
hex: '47304402207e9631c53dfc8a9a793d1916469628c6b7c5780c01c2f676d51ef21b0ba4926f022069feb471ec869a49f8d108d0aaba04e7cd36e60a7500109d86537f55698930d4412102791b19a39165dbd83403d6df268d44fd621da30581b0b6e5cb15a7101ed58851'
|
||||
},
|
||||
sequence: 4294967295,
|
||||
address: 'bitcoincash:qpvsg9vl9a5mlf37a7n3yce6pktdctn73qwgaqm3wq',
|
||||
address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s',
|
||||
value: 0.00051303,
|
||||
tokenQty: 0,
|
||||
tokenQtyStr: '0',
|
||||
@@ -114,6 +169,30 @@ const balance = {
|
||||
]
|
||||
}
|
||||
}
|
||||
const transactions = {
|
||||
success: true,
|
||||
transactions: [
|
||||
{
|
||||
height: 734439,
|
||||
tx_hash: '0bd2a8a72108659cd39a59bde89c45fff7d51c334531f036b1e102ab4b62f33f'
|
||||
},
|
||||
{
|
||||
height: 734441,
|
||||
tx_hash: '4f2837b7fff325c0442b550863de3470e016df234561d9151cbb6949fe43ac17'
|
||||
},
|
||||
{
|
||||
height: 735564,
|
||||
tx_hash: '1bfa83355839c0ef0f974463415fa983466e0286d7bcdfdd825f244e357ad1e5'
|
||||
},
|
||||
{
|
||||
height: 735564,
|
||||
tx_hash: '6eba09627175af4b50ac75fa8b3a15a9015df8a19b5b28a992f26044f4fd8891'
|
||||
},
|
||||
{
|
||||
height: 735564,
|
||||
tx_hash: 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
}]
|
||||
}
|
||||
|
||||
const status = {
|
||||
status: {
|
||||
@@ -122,9 +201,27 @@ const status = {
|
||||
chainBlockHeight: 722679
|
||||
}
|
||||
}
|
||||
const immutableData = {
|
||||
payloadCid: 'QmY3EaRaUcc5bNuqDfc7TaeNPThBGUefbqJeJVDjCqxqFZ',
|
||||
about: 'This is a placeholder'
|
||||
}
|
||||
const mutableData = {
|
||||
tokenIcon: 'https://gateway.ipfs.io/ipfs/bafybeiehitanirn5gmhqjg44xrmdtomn4n5lu5yjoepsvgpswk5mggaw6i/LP_logo-1.png',
|
||||
about: 'Mutable data managed with npm package: https://www.npmjs.com/package/slp-mutable-data'
|
||||
}
|
||||
|
||||
const decodedOpReturn = JSON.stringify({
|
||||
mda: 'bitcoincash:qrg77j4jf2pl7azgvzrz2z567ls464gkuuhplt30dp',
|
||||
cid: 'bafybeie6t5uyupddc7azms737xg4hxrj7i5t5ov3lb5g2qeehaujj6ak64'
|
||||
})
|
||||
module.exports = {
|
||||
tokenStats,
|
||||
txDataByMDA,
|
||||
txData,
|
||||
balance,
|
||||
status
|
||||
status,
|
||||
transactions,
|
||||
immutableData,
|
||||
mutableData,
|
||||
decodedOpReturn
|
||||
}
|
||||
|
||||
@@ -151,6 +151,34 @@ describe('#PsfSlpIndexer', () => {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
const result = await uut.getTokenStats(req, res)
|
||||
const tokenData = result.tokenData
|
||||
assert.property(tokenData, 'type')
|
||||
assert.property(tokenData, 'ticker')
|
||||
assert.property(tokenData, 'name')
|
||||
assert.property(tokenData, 'tokenId')
|
||||
assert.property(tokenData, 'documentUri')
|
||||
assert.property(tokenData, 'documentHash')
|
||||
assert.property(tokenData, 'decimals')
|
||||
assert.property(tokenData, 'mintBatonIsActive')
|
||||
assert.property(tokenData, 'tokensInCirculationBN')
|
||||
assert.property(tokenData, 'tokensInCirculationStr')
|
||||
assert.property(tokenData, 'blockCreated')
|
||||
assert.property(tokenData, 'totalBurned')
|
||||
assert.property(tokenData, 'totalMinted')
|
||||
assert.property(tokenData, 'txs')
|
||||
})
|
||||
it('should GET tokens stats with tx hisroty', async function () {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
req.body.withTxHistory = true
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
|
||||
} else {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
const result = await uut.getTokenStats(req, res)
|
||||
const tokenData = result.tokenData
|
||||
assert.property(tokenData, 'type')
|
||||
@@ -402,6 +430,27 @@ describe('#PsfSlpIndexer', () => {
|
||||
assert.isArray(balance.txs)
|
||||
assert.isArray(balance.balances)
|
||||
})
|
||||
|
||||
it('should GET data for ecash address', async function () {
|
||||
req.body.address =
|
||||
'ecash:qr5c4hfy52zn87484cucvzle5pljz0gtr5vhtw9z09'
|
||||
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.balance })
|
||||
} else {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
const result = await uut.getAddress(req, res)
|
||||
const balance = result.balance
|
||||
assert.property(balance, 'utxos')
|
||||
assert.property(balance, 'txs')
|
||||
assert.property(balance, 'balances')
|
||||
assert.isArray(balance.utxos)
|
||||
assert.isArray(balance.txs)
|
||||
assert.isArray(balance.balances)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getStatus', async () => {
|
||||
@@ -470,6 +519,7 @@ describe('#PsfSlpIndexer', () => {
|
||||
assert.property(status, 'chainBlockHeight')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#errorHandler', () => {
|
||||
it('should handle unexpected errors', () => {
|
||||
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
|
||||
@@ -479,6 +529,7 @@ describe('#PsfSlpIndexer', () => {
|
||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
||||
assert.property(result, 'error')
|
||||
})
|
||||
|
||||
it('should handle unknow errors', () => {
|
||||
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
|
||||
|
||||
@@ -488,6 +539,7 @@ describe('#PsfSlpIndexer', () => {
|
||||
assert.property(result, 'error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#checkEnvVar', () => {
|
||||
it('should throw errors if SLP_INDEXER_API env var is not provided', () => {
|
||||
const savedSlpIndexerUrl = process.env.SLP_INDEXER_API
|
||||
@@ -505,4 +557,375 @@ describe('#PsfSlpIndexer', () => {
|
||||
process.env.SLP_INDEXER_API = savedSlpIndexerUrl
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getCIDData', () => {
|
||||
it('should throw errors if cid is not provided', async () => {
|
||||
try {
|
||||
await uut.getCIDData()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'cid must be a string.',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle axios error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.axios, 'get').throws(new Error('test error'))
|
||||
const cid = 'bafybeigp3bfmj6woms7pywb7s7r6npcdudvsabvzne2chyspxtdendrwmy'
|
||||
await uut.getCIDData(cid)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'test error',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return cid object data', async () => {
|
||||
sandbox.stub(uut.axios, 'get').resolves({ data: mockData.immutableData })
|
||||
const cid = 'bafybeigp3bfmj6woms7pywb7s7r6npcdudvsabvzne2chyspxtdendrwmy'
|
||||
const result = await uut.getCIDData(cid)
|
||||
assert.isObject(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#decodeOpReturn', () => {
|
||||
it('should throw errors if txid is not provided', async () => {
|
||||
try {
|
||||
await uut.decodeOpReturn()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'txid must be a string.',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle bchjs error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'txData').throws(new Error('test error'))
|
||||
const txid = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
await uut.decodeOpReturn(txid)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'test error',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return data', async () => {
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'txData').resolves({ details: mockData.txData.txData })
|
||||
const txid = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
const result = await uut.decodeOpReturn(txid)
|
||||
assert.isString(result)
|
||||
})
|
||||
|
||||
it('should return false if data is not found', async () => {
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'txData').resolves({ details: { vout: [] } })
|
||||
const txid = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
const result = await uut.decodeOpReturn(txid)
|
||||
assert.isFalse(result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getTokenData', async () => {
|
||||
it('should throw 400 error if tokenId is missing', async () => {
|
||||
const result = await uut.getTokenData(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
assert.hasAllKeys(result, ['error', 'success'])
|
||||
assert.isFalse(result.success)
|
||||
assert.include(result.error, 'tokenId can not be empty')
|
||||
})
|
||||
|
||||
it('should throw 503 when network issues', async () => {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
|
||||
// Save the existing RPC URL.
|
||||
const savedUrl2 = process.env.SLP_INDEXER_API
|
||||
|
||||
// Manipulate the URL to cause a 500 network error.
|
||||
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
|
||||
|
||||
await uut.getTokenData(req, res)
|
||||
// console.log(`result: ${util.inspect(result)}`)
|
||||
|
||||
// Restore the saved URL.
|
||||
process.env.SLP_INDEXER_API = savedUrl2
|
||||
|
||||
assert.isAbove(
|
||||
res.statusCode,
|
||||
499,
|
||||
'HTTP status code 500 or greater expected.'
|
||||
)
|
||||
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service stalls', async () => {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
|
||||
// Mock the timeout error.
|
||||
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
|
||||
|
||||
const result = await uut.getTokenData(req, res)
|
||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||
|
||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||
assert.include(
|
||||
result.error,
|
||||
'Could not communicate with full node',
|
||||
'Error message expected'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns proper error when downstream service is down', async () => {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
|
||||
// Mock the timeout error.
|
||||
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
|
||||
|
||||
const result = await uut.getTokenData(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 tokens data', async function () {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
|
||||
sandbox.stub(uut, 'getCIDData').resolves(mockData.immutableData)
|
||||
sandbox.stub(uut, 'getMutableData').resolves(mockData.mutableData)
|
||||
} else {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
const result = await uut.getTokenData(req, res)
|
||||
assert.property(result, 'genesisData')
|
||||
assert.property(result, 'immutableData')
|
||||
assert.property(result, 'mutableData')
|
||||
|
||||
assert.isObject(result.genesisData)
|
||||
// assert.isObject(result.immutableData)
|
||||
// assert.isObject(result.mutableData)
|
||||
|
||||
const genesisData = result.genesisData
|
||||
assert.property(genesisData, 'ticker')
|
||||
assert.property(genesisData, 'name')
|
||||
assert.property(genesisData, 'type')
|
||||
assert.property(genesisData, 'tokenId')
|
||||
assert.property(genesisData, 'documentUri')
|
||||
assert.property(genesisData, 'documentHash')
|
||||
assert.property(genesisData, 'decimals')
|
||||
assert.property(genesisData, 'mintBatonIsActive')
|
||||
assert.property(genesisData, 'tokensInCirculationBN')
|
||||
assert.property(genesisData, 'tokensInCirculationStr')
|
||||
assert.property(genesisData, 'blockCreated')
|
||||
assert.property(genesisData, 'totalBurned')
|
||||
assert.property(genesisData, 'totalMinted')
|
||||
assert.property(genesisData, 'txs')
|
||||
})
|
||||
|
||||
it('should GET tokens data if immutableData data not found', async function () {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
|
||||
sandbox.stub(uut, 'getCIDData').throws(new Error('test error'))
|
||||
sandbox.stub(uut, 'getMutableData').resolves(mockData.mutableData)
|
||||
} else {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
const result = await uut.getTokenData(req, res)
|
||||
assert.property(result, 'genesisData')
|
||||
assert.property(result, 'immutableData')
|
||||
assert.property(result, 'mutableData')
|
||||
|
||||
assert.isObject(result.genesisData)
|
||||
assert.isObject(result.mutableData)
|
||||
// assert.equal(result.immutableData, '')
|
||||
|
||||
const genesisData = result.genesisData
|
||||
assert.property(genesisData, 'ticker')
|
||||
assert.property(genesisData, 'name')
|
||||
assert.property(genesisData, 'type')
|
||||
assert.property(genesisData, 'tokenId')
|
||||
assert.property(genesisData, 'documentUri')
|
||||
assert.property(genesisData, 'documentHash')
|
||||
assert.property(genesisData, 'decimals')
|
||||
assert.property(genesisData, 'mintBatonIsActive')
|
||||
assert.property(genesisData, 'tokensInCirculationBN')
|
||||
assert.property(genesisData, 'tokensInCirculationStr')
|
||||
assert.property(genesisData, 'blockCreated')
|
||||
assert.property(genesisData, 'totalBurned')
|
||||
assert.property(genesisData, 'totalMinted')
|
||||
assert.property(genesisData, 'txs')
|
||||
})
|
||||
|
||||
it('should GET tokens data if mutable data not found', async function () {
|
||||
req.body.tokenId =
|
||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
||||
// Mock the RPC call for unit tests.
|
||||
if (process.env.TEST === 'unit') {
|
||||
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.tokenStats })
|
||||
sandbox.stub(uut, 'getCIDData').resolves(mockData.immutableData)
|
||||
sandbox.stub(uut, 'getMutableData').throws(new Error('test error'))
|
||||
} else {
|
||||
return this.skip()
|
||||
}
|
||||
|
||||
const result = await uut.getTokenData(req, res)
|
||||
assert.property(result, 'genesisData')
|
||||
assert.property(result, 'immutableData')
|
||||
assert.property(result, 'mutableData')
|
||||
|
||||
assert.isObject(result.genesisData)
|
||||
// assert.isObject(result.immutableData)
|
||||
// assert.equal(result.mutableData, '')
|
||||
|
||||
const genesisData = result.genesisData
|
||||
assert.property(genesisData, 'ticker')
|
||||
assert.property(genesisData, 'name')
|
||||
assert.property(genesisData, 'type')
|
||||
assert.property(genesisData, 'tokenId')
|
||||
assert.property(genesisData, 'documentUri')
|
||||
assert.property(genesisData, 'documentHash')
|
||||
assert.property(genesisData, 'decimals')
|
||||
assert.property(genesisData, 'mintBatonIsActive')
|
||||
assert.property(genesisData, 'tokensInCirculationBN')
|
||||
assert.property(genesisData, 'tokensInCirculationStr')
|
||||
assert.property(genesisData, 'blockCreated')
|
||||
assert.property(genesisData, 'totalBurned')
|
||||
assert.property(genesisData, 'totalMinted')
|
||||
assert.property(genesisData, 'txs')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getMutableData', () => {
|
||||
it('should throw errors if documentHash is not provided', async () => {
|
||||
try {
|
||||
await uut.getMutableData()
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'documentHash string required when calling mutableData().',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw errors if cid is not provided in OP Return', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'decodeOpReturn').resolves('{}')
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves(mockData.transactions)
|
||||
sandbox.stub(uut, 'getCIDData').resolves(mockData.mutableData)
|
||||
|
||||
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
await uut.getMutableData(documentHash)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'CID could not be found in OP_RETURN data',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw errors if data is not found', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'decodeOpReturn').resolves(mockData.decodedOpReturn)
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves({ transactions: [] })
|
||||
sandbox.stub(uut, 'getCIDData').resolves(mockData.mutableData)
|
||||
|
||||
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
await uut.getMutableData(documentHash)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'CID could not be found in OP_RETURN data',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle bchjs error', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'decodeOpReturn').resolves(mockData.decodedOpReturn)
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'transactions').throws(new Error('test error'))
|
||||
|
||||
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
await uut.getMutableData(documentHash)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'test error',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should skip mutable data if was not generated by the MDA private key', async () => {
|
||||
try {
|
||||
sandbox.stub(uut, 'decodeOpReturn').resolves(mockData.decodedOpReturn)
|
||||
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves(mockData.transactions)
|
||||
sandbox.stub(uut.bchjs.Transaction, 'get').resolves(mockData.txData)
|
||||
|
||||
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
|
||||
await uut.getMutableData(documentHash)
|
||||
assert.fail('unexpected code path')
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'CID could not be found in OP_RETURN data',
|
||||
'Error message expected'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return mutable data', async () => {
|
||||
sandbox.stub(uut, 'decodeOpReturn')
|
||||
.onFirstCall().resolves(mockData.decodedOpReturn)
|
||||
.onSecondCall().resolves(JSON.parse(mockData.decodedOpReturn)) // data to force an error while parsing the JSON
|
||||
.onThirdCall().resolves(mockData.decodedOpReturn)
|
||||
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'transactions').resolves(mockData.transactions)
|
||||
sandbox.stub(uut.bchjs.Transaction, 'get').resolves(mockData.txDataByMDA)
|
||||
|
||||
sandbox.stub(uut, 'getCIDData').resolves(mockData.mutableData)
|
||||
|
||||
const documentHash = 'c37ba29f40ecc61662ea56324fdb72a5f1e66add2078854c2144765b9030358a'
|
||||
|
||||
const result = await uut.getMutableData(documentHash)
|
||||
assert.isString(result)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -204,6 +204,9 @@ describe('#rate-routelimit', () => {
|
||||
|
||||
describe('#trackRateLimits', () => {
|
||||
it('should apply anonymous rate limits if no JWT token is provided', async () => {
|
||||
// Mock sleep function
|
||||
sandbox.stub(uut, 'sleep').resolves()
|
||||
|
||||
req.ip = '127.0.0.1'
|
||||
|
||||
const result = await uut.trackRateLimits(req, res)
|
||||
@@ -278,6 +281,9 @@ describe('#rate-routelimit', () => {
|
||||
})
|
||||
|
||||
it('should apply rate limits to anonymous users', async () => {
|
||||
// Mock sleep function
|
||||
sandbox.stub(uut, 'sleep').resolves()
|
||||
|
||||
req.ip = '123.456.7.8'
|
||||
|
||||
// console.log('next.callCount: ', next.callCount)
|
||||
@@ -302,6 +308,9 @@ describe('#rate-routelimit', () => {
|
||||
})
|
||||
|
||||
it('should return 429 error when anonymous users exceed rate limit', async () => {
|
||||
// Mock sleep function
|
||||
sandbox.stub(uut, 'sleep').resolves()
|
||||
|
||||
req.ip = '123.456.7.8'
|
||||
|
||||
// force req.locals.jwtToken to be empty.
|
||||
@@ -317,7 +326,7 @@ describe('#rate-routelimit', () => {
|
||||
assert.property(val, 'error')
|
||||
assert.include(
|
||||
val.error,
|
||||
'Too many requests. Your limits are currently 20 requests per minute.'
|
||||
'Too many requests. Your limits are currently'
|
||||
)
|
||||
|
||||
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
|
||||
|
||||
Reference in New Issue
Block a user