mirror of
https://github.com/fullstack-cash/bch-api.git
synced 2026-09-22 09:12:05 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f987d4fdf2 | ||
|
|
acdee61a1b | ||
|
|
e2d920fcc7 | ||
|
|
4f74d9486b | ||
|
|
04c9267c3b | ||
|
|
c58e3e0ea9 | ||
|
|
4e18f86e98 | ||
|
|
0d00d340cf | ||
|
|
767a0fd38b | ||
|
|
e2bcb9783e | ||
|
|
5d9565b1d2 | ||
|
|
7e9cac8a72 | ||
|
|
8aad3b6b97 | ||
|
|
d1c35044b4 | ||
|
|
7f995e39e4 | ||
|
|
f138d32022 | ||
|
|
26d530158a | ||
|
|
a3adf1eab5 | ||
|
|
805c6da906 | ||
|
|
bd0ae5fa74 | ||
|
|
777c12776e | ||
|
|
cdf81baf0f | ||
|
|
6b602a1f66 | ||
|
|
77a541733d | ||
|
|
2b219901bd | ||
|
|
92c257ceb3 | ||
|
|
db0a4c466a | ||
|
|
776628ae9e | ||
|
|
7a3e03c4b7 | ||
|
|
7459f7aff6 | ||
|
|
e2dea9518c | ||
|
|
8d83431034 | ||
|
|
160366c17a | ||
|
|
ec6bb25278 | ||
|
|
293213dc33 | ||
|
|
0772782d20 | ||
|
|
6c977a912e | ||
|
|
d60f1f117e | ||
|
|
4dea95c631 | ||
|
|
eb47d422ab | ||
|
|
302f3c6a0e | ||
|
|
fd897a8fb0 | ||
|
|
cc8a8e0947 | ||
|
|
9669d0b16a | ||
|
|
dbc4e3e731 | ||
|
|
88231cb303 | ||
|
|
a33661ddcf | ||
|
|
6c0a7e29c1 | ||
|
|
56c52ab6ed | ||
|
|
d3cf02122c | ||
|
|
f253211a2f | ||
|
|
9958821371 | ||
|
|
d5f7426a99 | ||
|
|
0c7a816842 | ||
|
|
6603a004c4 | ||
|
|
d4df475eee | ||
|
|
e7e9723783 | ||
|
|
51c4272b54 | ||
|
|
7e97765e14 | ||
|
|
29b51e9437 | ||
|
|
b472d15848 | ||
|
|
f13727094a | ||
|
|
882bb065b3 | ||
|
|
c7fbf5ea9f | ||
|
|
6f9f3ee087 | ||
|
|
1062c50f4f | ||
|
|
578efc1f7b | ||
|
|
c9befa0634 | ||
|
|
dd3152b60e | ||
|
|
cec49a17e9 | ||
|
|
e78cb6b102 | ||
|
|
ad65ee7791 | ||
|
|
3a862d7b51 | ||
|
|
22eb41db13 | ||
|
|
f80fde9a81 | ||
|
|
55cb03b627 | ||
|
|
47e2d5e739 | ||
|
|
553054e18c | ||
|
|
3a1ad53a42 | ||
|
|
4255d98c71 | ||
|
|
9aea5533f8 | ||
|
|
ea3801165c | ||
|
|
b72ed28d49 | ||
|
|
cd9f2b5292 | ||
|
|
8946b414e8 | ||
|
|
6eba44641f | ||
|
|
cbbb7f76e4 | ||
|
|
7df1a1a1ed | ||
|
|
1b6bc5acc8 | ||
|
|
2e181cf6ee | ||
|
|
73cfb8c23a | ||
|
|
074610161f | ||
|
|
7c177f8013 | ||
|
|
3d6a8e30fd | ||
|
|
c8fe6f0bfd | ||
|
|
96b5e0e408 | ||
|
|
6ca014e160 | ||
|
|
233e210131 | ||
|
|
e55d421f13 | ||
|
|
1b1bf36a89 | ||
|
|
8de5399818 |
+1
-1
@@ -42,6 +42,6 @@ logs/*
|
|||||||
docs/*
|
docs/*
|
||||||
docker/mainnet/start-local-mainnet.sh
|
docker/mainnet/start-local-mainnet.sh
|
||||||
docker/testnet/start-local-testnet.sh
|
docker/testnet/start-local-testnet.sh
|
||||||
|
start-local-bchn.sh
|
||||||
|
|
||||||
!logs/README.md
|
!logs/README.md
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -3,48 +3,41 @@
|
|||||||
[](https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/LICENSE.md)
|
[](https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/LICENSE.md)
|
||||||
[](https://github.com/feross/standard)
|
[](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)
|
## Documentation
|
||||||
- [Example Code](https://fullstack.cash/examples)
|
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:
|
Have questions? Need help? Join our community support:
|
||||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
- [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.
|
|
||||||
|
|
||||||
### Configure bch-js
|
### 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:
|
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/
|
- BCH Mainnet REST API server: https://bchn.fullstack.cash/v5/
|
||||||
- ABC Mainnet REST API server: https://abc.fullstack.cash/v4/
|
- eCash Mainnet REST API server: https://abc.fullstack.cash/v5/
|
||||||
- Testnet3 REST API server: https://testnet3.fullstack.cash/v4/
|
|
||||||
- Check server status: https://metrics.fullstack.cash
|
- Check server status: https://metrics.fullstack.cash
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
There are two installation paths, depending if you want a *development* or
|
There are two installation paths, depending if you want a *development* or
|
||||||
*production* environment. You'll also need to set up the underlying infrastructure
|
*production* environment. You'll also need to set up the underlying infrastructure
|
||||||
described on [this page](https://psfoundation.cash/blog/cash-stack).
|
described 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
|
### Development
|
||||||
This is a standard node.js project. The installation is as follows:
|
This is a standard node.js project. The installation is as follows:
|
||||||
@@ -69,7 +62,7 @@ For a production environment, a Docker container is provided in the
|
|||||||
containers target the Ubuntu 18.04 LTS Linux OS.
|
containers target the Ubuntu 18.04 LTS Linux OS.
|
||||||
|
|
||||||
- Install Docker and Docker Compose by following the commands on
|
- 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
|
- Customize the [bash script](docker/mainnet/start-local-mainnet.sh) for your
|
||||||
installation.
|
installation.
|
||||||
@@ -80,10 +73,16 @@ installation.
|
|||||||
|
|
||||||
- Run the Docker container with:
|
- Run the Docker container with:
|
||||||
|
|
||||||
`docker-compose up`
|
`docker-compose up -d`
|
||||||
|
|
||||||
## Rate Limits
|
## 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>`
|
- `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}"
|
curl "https://bchn.fullstack.cash/v4/electrumx/balance/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -X GET -H "Content-Type: application/json" -H "Authorization: Token ${BCHJSTOKEN}"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Support
|
### Basic Authentication
|
||||||
Have questions? Need help? Join our community support
|
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.
|
||||||
[Telegram channel](https://t.me/bch_js_toolkit)
|
|
||||||
|
|
||||||
## IPFS
|
### Whitelists
|
||||||
Copies of this repository will occasionally be uploaded and hosted on [IPFS](https://ipfs.io):
|
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)
|
- v1.25.1: [QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX](https://ipfs.io/ipfs/QmXFzFJHenymhReDc9oGAxbpmvK2fJYnisQ2pJn3gGyKKX)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
[MIT](./LICENSE.md)
|
[MIT](./LICENSE.md)
|
||||||
a
|
|
||||||
|
|||||||
+2
-3
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"sampleUrl": null
|
"sampleUrl": null
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
+4
-2
@@ -12,7 +12,7 @@ const config = {
|
|||||||
// Rate Limits
|
// Rate Limits
|
||||||
anonRateLimit: process.env.ANON_RATE_LIMIT
|
anonRateLimit: process.env.ANON_RATE_LIMIT
|
||||||
? Number(process.env.ANON_RATE_LIMIT)
|
? Number(process.env.ANON_RATE_LIMIT)
|
||||||
: 500,
|
: 1000,
|
||||||
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
whitelistRateLimit: process.env.WHITELIST_RATE_LIMIT
|
||||||
? Number(process.env.WHITELIST_RATE_LIMIT)
|
? Number(process.env.WHITELIST_RATE_LIMIT)
|
||||||
: 10,
|
: 10,
|
||||||
@@ -24,7 +24,9 @@ const config = {
|
|||||||
: 10000,
|
: 10000,
|
||||||
whitelistDomains: process.env.WHITELIST_DOMAINS
|
whitelistDomains: process.env.WHITELIST_DOMAINS
|
||||||
? process.env.WHITELIST_DOMAINS.split(',')
|
? process.env.WHITELIST_DOMAINS.split(',')
|
||||||
: ['fullstack.cash', 'psfoundation.cash', '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
|
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,23 +1,53 @@
|
|||||||
FROM christroutner/ct-base-ubuntu
|
FROM ubuntu:20.04
|
||||||
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||||
|
|
||||||
RUN apt-get update -y
|
#Update the OS and install any OS packages needed.
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y sudo git curl nano gnupg wget
|
||||||
|
|
||||||
|
#Install Node and NPM
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
|
||||||
|
RUN bash nodesource_setup.sh
|
||||||
|
RUN apt-get install -y nodejs build-essential
|
||||||
|
|
||||||
|
#Create the user 'safeuser' and add them to the sudo group.
|
||||||
|
RUN useradd -ms /bin/bash safeuser
|
||||||
|
RUN adduser safeuser sudo
|
||||||
|
|
||||||
|
#Set password to 'abcd8765' change value below if you want a different password
|
||||||
|
RUN echo safeuser:abcd8765 | chpasswd
|
||||||
|
|
||||||
|
#Set the working directory to be the users home directory
|
||||||
|
WORKDIR /home/safeuser
|
||||||
|
|
||||||
|
#Setup NPM for non-root global install (like on a mac)
|
||||||
|
RUN mkdir /home/safeuser/.npm-global
|
||||||
|
RUN chown -R safeuser .npm-global
|
||||||
|
RUN echo "export PATH=~/.npm-global/bin:$PATH" >> /home/safeuser/.profile
|
||||||
|
RUN runuser -l safeuser -c "npm config set prefix '~/.npm-global'"
|
||||||
|
|
||||||
|
# Update to the latest version of npm.
|
||||||
|
# Working with npm@7.21.1
|
||||||
|
RUN npm install -g npm
|
||||||
|
|
||||||
|
#FROM christroutner/ct-base-ubuntu
|
||||||
|
#MAINTAINER Chris Troutner <chris.troutner@gmail.com>
|
||||||
|
|
||||||
|
#RUN apt-get update -y
|
||||||
|
|
||||||
#Set the working directory to be the home directory
|
#Set the working directory to be the home directory
|
||||||
WORKDIR /home/safeuser
|
#WORKDIR /home/safeuser
|
||||||
|
|
||||||
# Switch to user account.
|
# Switch to user account.
|
||||||
USER safeuser
|
USER safeuser
|
||||||
# Prep 'sudo' commands.
|
# Prep 'sudo' commands.
|
||||||
#RUN echo 'abcd8765' | sudo -S pwd
|
#RUN echo 'abcd8765' | sudo -S pwd
|
||||||
|
|
||||||
# Install Redis. TODO: Make this a separate container.
|
|
||||||
|
|
||||||
|
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
WORKDIR /home/safeuser
|
WORKDIR /home/safeuser
|
||||||
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
|
RUN git clone https://github.com/Permissionless-Software-Foundation/bch-api
|
||||||
WORKDIR /home/safeuser/bch-api
|
WORKDIR /home/safeuser/bch-api
|
||||||
|
RUN git checkout ct-unstable
|
||||||
RUN npm install --silent
|
RUN npm install --silent
|
||||||
|
|
||||||
# Generate documentation
|
# Generate documentation
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Remove all untagged docker images.
|
||||||
|
docker rmi $(docker images | grep "^<none>" | awk '{print $3}')
|
||||||
|
|
||||||
@@ -1,19 +1,33 @@
|
|||||||
bch-api-main:
|
version: '3.9'
|
||||||
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
|
|
||||||
|
|
||||||
redis-main:
|
services:
|
||||||
image: redis:latest
|
bch-api-main:
|
||||||
container_name: redis-main
|
build:
|
||||||
restart: always
|
context: ./
|
||||||
ports:
|
dockerfile: Dockerfile
|
||||||
- "172.17.0.1:6379:6379"
|
#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"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export SLP_API_URL=http://10.0.0.5:5001/
|
|||||||
# Mainnet Fulcrum / ElectrumX
|
# Mainnet Fulcrum / ElectrumX
|
||||||
export FULCRUM_URL=172.17.0.1
|
export FULCRUM_URL=172.17.0.1
|
||||||
export FULCRUM_PORT=50002
|
export FULCRUM_PORT=50002
|
||||||
|
export FULCRUM_API=http://172.17.0.1:3001/v1/
|
||||||
|
|
||||||
# Redis DB - Used for rate limiting
|
# Redis DB - Used for rate limiting
|
||||||
export REDIS_PORT=6379
|
export REDIS_PORT=6379
|
||||||
@@ -39,7 +40,7 @@ export REDIS_HOST=172.17.0.1
|
|||||||
export TOKENSECRET=somelongsecretvalue
|
export TOKENSECRET=somelongsecretvalue
|
||||||
|
|
||||||
# So that bch-api can call bch-js locally.
|
# 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
|
# Basic Authentication password
|
||||||
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
||||||
@@ -48,6 +49,9 @@ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
|||||||
# that originate froma domain on the whitelist.
|
# that originate froma domain on the whitelist.
|
||||||
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
||||||
|
|
||||||
|
# Disable rate limits
|
||||||
|
export DO_NOT_USE_RATE_LIMITS=1
|
||||||
|
|
||||||
# Rate Limits. Numbers are divided into 1000. e.g. 1000 / 50 = 20 RPM for ANON.
|
# 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.
|
# Requests use the ANON rate limit if they fail to pass in a JWT token.
|
||||||
# ANON = 20 requests per minute (RPM)
|
# ANON = 20 requests per minute (RPM)
|
||||||
@@ -61,4 +65,11 @@ export LOG_MAX_SIZE=1m
|
|||||||
#5d means store no more than 5 days
|
#5d means store no more than 5 days
|
||||||
export LOG_MAX_FILES=5d
|
export LOG_MAX_FILES=5d
|
||||||
|
|
||||||
|
# (Optional) bcash full node
|
||||||
|
export BCASH_SERVER=http://localhost:3002/
|
||||||
|
|
||||||
|
# psf-slp-indexer
|
||||||
|
#export SLP_INDEXER_API=https://psf-slp-indexer.fullstack.cash/
|
||||||
|
export SLP_INDEXER_API=http://172.17.0.1:5010/
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|||||||
@@ -17,29 +17,32 @@ export RPC_PASSWORD=password
|
|||||||
|
|
||||||
# SLPDB
|
# SLPDB
|
||||||
export SLPDB_URL=http://172.17.0.1:13300/
|
export SLPDB_URL=http://172.17.0.1:13300/
|
||||||
export SLPDB_PASS=somelongpassword
|
export SLPDB_PASS=portlandisacityinoregon
|
||||||
|
export SLPDB_PASS_GP=portlandisacityinoregon
|
||||||
|
export SLPDB_PASS_WL=portlandisacityinoregon
|
||||||
# Use the same address as SLPDB_URL if you don't have a separate whitelist server.
|
# 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/
|
export SLPDB_WHITELIST_URL=http://172.17.0.1:13300/
|
||||||
# slp-api alternative SLP validator using slp-validate:
|
# slp-api alternative SLP validator using slp-validate:
|
||||||
# https://github.com/Permissionless-Software-Foundation/slp-api
|
# https://github.com/Permissionless-Software-Foundation/slp-api
|
||||||
export SLP_API_URL=http://10.0.0.5:5001/
|
export SLP_API_URL=https://slpapi-testnet3.fullstackslp.nl/
|
||||||
|
|
||||||
# Mainnet Fulcrum / ElectrumX
|
# Mainnet Fulcrum / ElectrumX
|
||||||
export FULCRUM_URL=172.17.0.1
|
export FULCRUM_URL=172.17.0.1
|
||||||
export FULCRUM_PORT=60002
|
export FULCRUM_PORT=60002
|
||||||
|
export FULCRUM_API=http://172.17.0.1:3000/v1/
|
||||||
|
|
||||||
# Redis DB
|
# Redis DB
|
||||||
export REDIS_PORT=6380
|
export REDIS_PORT=6380
|
||||||
export REDIS_HOST=172.17.0.1
|
export REDIS_HOST=172.17.0.1
|
||||||
|
|
||||||
# JWT Token Secret
|
# JWT Token Secret
|
||||||
export TOKENSECRET=somelongsecretvalue
|
export TOKENSECRET=fridayharbor
|
||||||
|
|
||||||
# So that bch-api can call bch-js locally.
|
# 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/v4/
|
||||||
|
|
||||||
# Basic Authentication password (optional)
|
# Basic Authentication password (optional)
|
||||||
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
export PRO_PASS=thejaasjhehrakseuhakuhr
|
||||||
|
|
||||||
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
|
# Whitelisted domains. Automatically give pro-tier rate limit access to apps
|
||||||
# that originate froma domain on the whitelist.
|
# that originate froma domain on the whitelist.
|
||||||
@@ -50,6 +53,9 @@ export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
|||||||
# ANON = 20 requests per minute (RPM)
|
# ANON = 20 requests per minute (RPM)
|
||||||
export ANON_RATE_LIMIT=50
|
export ANON_RATE_LIMIT=50
|
||||||
# 10 = 100 RPM
|
# 10 = 100 RPM
|
||||||
export WHITELIST_RATE_LIMIT=10
|
export WHITELIST_RATE_LIMIT=1
|
||||||
|
|
||||||
|
export INTERNAL_RATE_LIMIT=100
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
|
|||||||
Generated
+10514
-11866
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bch-api",
|
"name": "bch-api",
|
||||||
"version": "1.16.0",
|
"version": "2.0.0",
|
||||||
"description": "The REST API powering FullStack.cash",
|
"description": "The REST API powering FullStack.cash",
|
||||||
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
@@ -18,20 +18,20 @@
|
|||||||
"lint": "standard --env mocha --fix",
|
"lint": "standard --env mocha --fix",
|
||||||
"test-v4": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v4/",
|
"test-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-v5": "export NETWORK=mainnet && nyc --reporter=text mocha --exit --timeout 60000 test/v5/",
|
||||||
"test:integration": "mocha test/v4/integration",
|
"test:integration": "mocha test/v5/integration",
|
||||||
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v4/integration/slp*.js",
|
"test:integration:slpdb": "mocha --timeout 25000 -g '#validate2Single' test/v5/integration/slp*.js",
|
||||||
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v4/integration/nft.js",
|
"test:integration:nft": "mocha --timeout 25000 -g '#nft' test/v5/integration/nft.js",
|
||||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||||
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
"coverage:report": "export NETWORK=mainnet && nyc --reporter=html mocha --timeout 25000 test/v4/",
|
||||||
"docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs",
|
"docs": "./node_modules/.bin/apidoc -i src/routes/v5 -o docs",
|
||||||
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/",
|
"test:temp1": "export NETWORK=mainnet && export TEST=integration && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/",
|
||||||
"test:temp2": "export NETWORK=mainnet && mocha -g '#utxosBulk' --exit --timeout 30000 test/v5/"
|
"test:temp2": "export SLP_INDEXER_API=http://fakeurl/api/ && export NETWORK=mainnet && mocha --exit --timeout 30000 test/v5/psf-slp-indexer"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.15.1"
|
"node": ">=10.15.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@psf/bch-js": "^4.20.7",
|
"@psf/bch-js": "6.3.4",
|
||||||
"apidoc": "^0.26.0",
|
"apidoc": "^0.26.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"bitcore-lib-cash": "^8.23.1",
|
"bitcore-lib-cash": "^8.23.1",
|
||||||
@@ -62,19 +62,19 @@
|
|||||||
"bignumber.js": "^9.0.0",
|
"bignumber.js": "^9.0.0",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"coveralls": "^3.0.2",
|
"coveralls": "^3.0.2",
|
||||||
"eslint": "5.16.0",
|
"eslint": "7.17.0",
|
||||||
"eslint-config-prettier": "^6.0.0",
|
"eslint-config-prettier": "7.1.0",
|
||||||
"eslint-config-standard": "^13.0.1",
|
"eslint-config-standard": "16.0.2",
|
||||||
"eslint-plugin-prettier": "^3.1.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
|
"eslint-plugin-standard": "4.0.0",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"nock": "^13.0.5",
|
"nock": "^13.0.5",
|
||||||
"nyc": "^15.0.0",
|
"nyc": "^15.0.0",
|
||||||
"prettier": "^2.0.0",
|
|
||||||
"semantic-release": "^17.4.2",
|
"semantic-release": "^17.4.2",
|
||||||
"sinon": "^9.0.0",
|
"sinon": "^9.0.0",
|
||||||
"standard": "^14.3.1"
|
"standard": "^16.0.4"
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"publish": [
|
"publish": [
|
||||||
|
|||||||
+20
-62
@@ -24,21 +24,6 @@ const jwtAuth = require('./middleware/jwt-auth')
|
|||||||
// Logging for API requests.
|
// Logging for API requests.
|
||||||
const logReqInfo = require('./middleware/req-logging')
|
const logReqInfo = require('./middleware/req-logging')
|
||||||
|
|
||||||
// v4
|
|
||||||
const healthCheckV4 = require('./routes/v4/health-check')
|
|
||||||
const BlockchainV4 = require('./routes/v4/full-node/blockchain')
|
|
||||||
const ControlV4 = require('./routes/v4/full-node/control')
|
|
||||||
const MiningV4 = require('./routes/v4/full-node/mining')
|
|
||||||
const networkV4 = require('./routes/v4/full-node/network')
|
|
||||||
const RawtransactionsV4 = require('./routes/v4/full-node/rawtransactions')
|
|
||||||
const UtilV4 = require('./routes/v4/util')
|
|
||||||
const SlpV4 = require('./routes/v4/slp')
|
|
||||||
const xpubV4 = require('./routes/v4/xpub')
|
|
||||||
const ElectrumXV4 = require('./routes/v4/electrumx')
|
|
||||||
const EncryptionV4 = require('./routes/v4/encryption')
|
|
||||||
const PriceV4 = require('./routes/v4/price')
|
|
||||||
const Ninsight = require('./routes/v4/ninsight')
|
|
||||||
|
|
||||||
// v5
|
// v5
|
||||||
const healthCheckV5 = require('./routes/v5/health-check')
|
const healthCheckV5 = require('./routes/v5/health-check')
|
||||||
const BlockchainV5 = require('./routes/v5/full-node/blockchain')
|
const BlockchainV5 = require('./routes/v5/full-node/blockchain')
|
||||||
@@ -54,22 +39,11 @@ const ElectrumXV5 = require('./routes/v5/electrumx')
|
|||||||
const EncryptionV5 = require('./routes/v5/encryption')
|
const EncryptionV5 = require('./routes/v5/encryption')
|
||||||
const PriceV5 = require('./routes/v5/price')
|
const PriceV5 = require('./routes/v5/price')
|
||||||
const JWTV5 = require('./routes/v5/jwt')
|
const JWTV5 = require('./routes/v5/jwt')
|
||||||
// const Ninsight = require('./routes/v5/ninsight')
|
const BcashSLP = require('./routes/v5/bcash/slp')
|
||||||
|
const PsfSlpIndexer = require('./routes/v5/psf-slp-indexer')
|
||||||
|
|
||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
// Instantiate v4 route libraries.
|
|
||||||
const blockchainV4 = new BlockchainV4()
|
|
||||||
const controlV4 = new ControlV4()
|
|
||||||
const miningV4 = new MiningV4()
|
|
||||||
const rawtransactionsV4 = new RawtransactionsV4()
|
|
||||||
const slpV4 = new SlpV4()
|
|
||||||
const electrumxv4 = new ElectrumXV4()
|
|
||||||
electrumxv4.connect()
|
|
||||||
const encryptionv4 = new EncryptionV4()
|
|
||||||
const pricev4 = new PriceV4()
|
|
||||||
const utilV4 = new UtilV4({ electrumx: electrumxv4 })
|
|
||||||
|
|
||||||
// Instantiate v5 route libraries.
|
// Instantiate v5 route libraries.
|
||||||
const blockchainV5 = new BlockchainV5()
|
const blockchainV5 = new BlockchainV5()
|
||||||
const controlV5 = new ControlV5()
|
const controlV5 = new ControlV5()
|
||||||
@@ -82,6 +56,8 @@ const pricev5 = new PriceV5()
|
|||||||
const utilV5 = new UtilV5({ electrumx: electrumxv5 })
|
const utilV5 = new UtilV5({ electrumx: electrumxv5 })
|
||||||
const dsproofV5 = new DSProofV5()
|
const dsproofV5 = new DSProofV5()
|
||||||
const jwtV5 = new JWTV5()
|
const jwtV5 = new JWTV5()
|
||||||
|
const bcashSLP = new BcashSLP()
|
||||||
|
const psfSlpIndexer = new PsfSlpIndexer()
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.locals.env = process.env
|
app.locals.env = process.env
|
||||||
@@ -98,12 +74,12 @@ app.set('views', path.join(__dirname, 'views'))
|
|||||||
app.set('view engine', 'jade')
|
app.set('view engine', 'jade')
|
||||||
|
|
||||||
// Mount the docs
|
// Mount the docs
|
||||||
app.use('/docs', express.static(`${__dirname}/../docs`))
|
app.use('/docs', express.static(`${__dirname.toString()}/../docs`))
|
||||||
|
|
||||||
// Log each request to the console with IP addresses.
|
// Log each request to the console with IP addresses.
|
||||||
// app.use(logger("dev"))
|
// app.use(logger("dev"))
|
||||||
const morganFormat =
|
const morganFormat =
|
||||||
':remote-addr :remote-user :method :url :status :response-time ms - :res[content-length] :user-agent'
|
':remote-addr :remote-user :method :url :status :response-time ms - :res[content-length] :user-agent'
|
||||||
app.use(logger(morganFormat))
|
app.use(logger(morganFormat))
|
||||||
|
|
||||||
// Log the same data to the winston logs.
|
// Log the same data to the winston logs.
|
||||||
@@ -114,22 +90,20 @@ const logStream = {
|
|||||||
}
|
}
|
||||||
app.use(logger(morganFormat, { stream: logStream }))
|
app.use(logger(morganFormat, { stream: logStream }))
|
||||||
|
|
||||||
app.use(bodyParser.json())
|
app.use(bodyParser.json({ limit: '50mb' }))
|
||||||
app.use(bodyParser.urlencoded({ extended: false }))
|
app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' }))
|
||||||
app.use(cookieParser())
|
app.use(cookieParser())
|
||||||
app.use(express.static(path.join(__dirname, 'public')))
|
app.use(express.static(path.join(__dirname, 'public')))
|
||||||
|
|
||||||
// Log requests for later analysis.
|
// Log requests for later analysis.
|
||||||
app.use('/', logReqInfo)
|
app.use('/', logReqInfo)
|
||||||
|
|
||||||
const v4prefix = 'v4'
|
|
||||||
const v5prefix = 'v5'
|
const v5prefix = 'v5'
|
||||||
|
|
||||||
// START Rate Limits
|
// START Rate Limits
|
||||||
const auth = new AuthMW()
|
const auth = new AuthMW()
|
||||||
|
|
||||||
// Ensure req.locals and res.locals objects exist.
|
// Ensure req.locals and res.locals objects exist.
|
||||||
app.use(`/${v4prefix}/`, rateLimits.populateLocals)
|
|
||||||
app.use(`/${v5prefix}/`, rateLimits.populateLocals)
|
app.use(`/${v5prefix}/`, rateLimits.populateLocals)
|
||||||
|
|
||||||
// Allow users to turn off rate limits with an environment variable.
|
// Allow users to turn off rate limits with an environment variable.
|
||||||
@@ -140,43 +114,23 @@ console.log(`DO_NOT_USE_RATE_LIMITS: ${DO_NOT_USE_RATE_LIMITS}`)
|
|||||||
if (!DO_NOT_USE_RATE_LIMITS) {
|
if (!DO_NOT_USE_RATE_LIMITS) {
|
||||||
console.log('Rate limits are being used')
|
console.log('Rate limits are being used')
|
||||||
// Inspect the header for a JWT token.
|
// Inspect the header for a JWT token.
|
||||||
app.use(`/${v4prefix}/`, jwtAuth.getTokenFromHeaders)
|
|
||||||
app.use(`/${v5prefix}/`, jwtAuth.getTokenFromHeaders)
|
app.use(`/${v5prefix}/`, jwtAuth.getTokenFromHeaders)
|
||||||
|
|
||||||
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
|
// Instantiate the authorization middleware, used to implement pro-tier rate limiting.
|
||||||
// Handles Anonymous and Basic Authorization schemes used by passport.js
|
// Handles Anonymous and Basic Authorization schemes used by passport.js
|
||||||
app.use(`/${v4prefix}/`, auth.mw())
|
|
||||||
app.use(`/${v5prefix}/`, auth.mw())
|
app.use(`/${v5prefix}/`, auth.mw())
|
||||||
|
|
||||||
// Experimental rate limits
|
// Experimental rate limits
|
||||||
app.use(`/${v4prefix}/`, rateLimits.applyRateLimits)
|
|
||||||
app.use(`/${v5prefix}/`, rateLimits.applyRateLimits)
|
app.use(`/${v5prefix}/`, rateLimits.applyRateLimits)
|
||||||
|
|
||||||
// Rate limit on all v4 routes
|
// Rate limit on all v4 routes
|
||||||
// Establish and enforce rate limits.
|
// Establish and enforce rate limits.
|
||||||
// app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource)
|
// app.use(`/${v4prefix}/`, rateLimits.rateLimitByResource)
|
||||||
} else {
|
} else {
|
||||||
console.log('Rate limits are NOT being used')
|
console.log('Rate limits are NOT being used')
|
||||||
}
|
}
|
||||||
// END Rate Limits
|
// END Rate Limits
|
||||||
|
|
||||||
// Connect v4 routes
|
|
||||||
app.use(`/${v4prefix}/` + 'health-check', healthCheckV4)
|
|
||||||
app.use(`/${v4prefix}/` + 'blockchain', blockchainV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'control', controlV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'mining', miningV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'network', networkV4)
|
|
||||||
app.use(`/${v4prefix}/` + 'rawtransactions', rawtransactionsV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'slp', slpV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'xpub', xpubV4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'electrumx', electrumxv4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'encryption', encryptionv4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'price', pricev4.router)
|
|
||||||
app.use(`/${v4prefix}/` + 'util', utilV4.router)
|
|
||||||
|
|
||||||
const ninsight = new Ninsight()
|
|
||||||
app.use(`/${v4prefix}/` + 'ninsight', ninsight.router)
|
|
||||||
|
|
||||||
// Connect v5 routes
|
// Connect v5 routes
|
||||||
app.use(`/${v5prefix}/` + 'health-check', healthCheckV5)
|
app.use(`/${v5prefix}/` + 'health-check', healthCheckV5)
|
||||||
app.use(`/${v5prefix}/` + 'blockchain', blockchainV5.router)
|
app.use(`/${v5prefix}/` + 'blockchain', blockchainV5.router)
|
||||||
@@ -193,8 +147,12 @@ app.use(`/${v5prefix}/` + 'util', utilV5.router)
|
|||||||
app.use(`/${v5prefix}/` + 'dsproof', dsproofV5.router)
|
app.use(`/${v5prefix}/` + 'dsproof', dsproofV5.router)
|
||||||
app.use(`/${v5prefix}/` + 'jwt', jwtV5.router)
|
app.use(`/${v5prefix}/` + 'jwt', jwtV5.router)
|
||||||
|
|
||||||
// const ninsight = new Ninsight()
|
app.use(`/${v5prefix}/` + 'bcash/slp', bcashSLP.router)
|
||||||
app.use(`/${v5prefix}/` + 'ninsight', ninsight.router)
|
|
||||||
|
app.use(`/${v5prefix}/` + 'psf/slp', psfSlpIndexer.router)
|
||||||
|
|
||||||
|
// Daniel:
|
||||||
|
// app.use(`/${v5prefix}/` + 'psfslp', psfSlp.router)
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -281,11 +239,11 @@ function onError (error) {
|
|||||||
case 'EACCES':
|
case 'EACCES':
|
||||||
console.error(`${bind} requires elevated privileges`)
|
console.error(`${bind} requires elevated privileges`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
// break
|
break
|
||||||
case 'EADDRINUSE':
|
case 'EADDRINUSE':
|
||||||
console.error(`${bind} is already in use`)
|
console.error(`${bind} is already in use`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
// break
|
break
|
||||||
default:
|
default:
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -301,4 +259,4 @@ function onListening () {
|
|||||||
debug(`Listening on ${bind}`)
|
debug(`Listening on ${bind}`)
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
// module.exports = app;
|
// module.exports = app
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const logReqInfo = function (req, res, next) {
|
|||||||
body: req.body
|
body: req.body
|
||||||
}
|
}
|
||||||
|
|
||||||
wlogger.verbose(`Request: ${ip} ${method} ${url}`, dataToLog)
|
wlogger.info(`Request: ${ip} ${method} ${url}`, dataToLog)
|
||||||
|
|
||||||
next()
|
next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
This file will replace the original rate-limit.js file.
|
|
||||||
|
|
||||||
Sets the rate limits for the anonymous and paid tiers. Current rate limits:
|
Sets the rate limits for the anonymous and paid tiers. Current rate limits:
|
||||||
- 10000 points in 60 seconds
|
- 10000 points in 60 seconds
|
||||||
- 500 points per call for anonymous tier (20 RPM)
|
- 500 points per call for anonymous tier (20 RPM)
|
||||||
@@ -35,6 +33,10 @@ const { RateLimiterRedis } = require('rate-limiter-flexible')
|
|||||||
const wlogger = require('../util/winston-logging')
|
const wlogger = require('../util/winston-logging')
|
||||||
const config = require('../../config')
|
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.
|
let _this // Global pointer to instance of class, when 'this' context is lost.
|
||||||
|
|
||||||
// Setup Redis to track rate limits for each user.
|
// Setup Redis to track rate limits for each user.
|
||||||
@@ -215,6 +217,10 @@ class RateLimits {
|
|||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sleep (ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
// A wrapper for Redis-based rate limiter.
|
// A wrapper for Redis-based rate limiter.
|
||||||
// Will return false if the user has not exceeded the rate limit. Otherwise
|
// 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
|
// it will return the 'res' object with an error status and message, which
|
||||||
@@ -258,8 +264,22 @@ class RateLimits {
|
|||||||
// const rateLimitData = await _this.rateLimiter.consume(key, pointsToConsume)
|
// const rateLimitData = await _this.rateLimiter.consume(key, pointsToConsume)
|
||||||
// console.log(`rateLimitData: `, rateLimitData)
|
// console.log(`rateLimitData: `, rateLimitData)
|
||||||
|
|
||||||
|
// Add data to logs for analytics.
|
||||||
|
const logData = {
|
||||||
|
ip: req.ip,
|
||||||
|
key,
|
||||||
|
pointsToConsume,
|
||||||
|
status: 'OK'
|
||||||
|
}
|
||||||
|
wlogger.info(logData)
|
||||||
|
|
||||||
res.locals.pointsToConsume = pointsToConsume // Feedback for tests.
|
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.
|
// Signal that the user has not exceeded their rate limits.
|
||||||
return false
|
return false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -274,6 +294,20 @@ class RateLimits {
|
|||||||
// `rate limit debug info: ${JSON.stringify(debugInfo, null, 2)}`
|
// `rate limit debug info: ${JSON.stringify(debugInfo, null, 2)}`
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
// Add data to logs for analytics.
|
||||||
|
const logData = {
|
||||||
|
ip: req.ip,
|
||||||
|
key,
|
||||||
|
pointsToConsume,
|
||||||
|
status: 429
|
||||||
|
}
|
||||||
|
wlogger.info(logData)
|
||||||
|
|
||||||
|
// Add artificial delay to slow down freeloaders.
|
||||||
|
if (pointsToConsume === ANON_LIMITS) {
|
||||||
|
await this.sleep(ANON_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
// Rate limited was triggered
|
// Rate limited was triggered
|
||||||
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
res.status(429) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,192 +0,0 @@
|
|||||||
/*
|
|
||||||
Encryption API route
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
const axios = require('axios')
|
|
||||||
const util = require('util')
|
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
const config = require('../../../config')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const restURL = process.env.LOCAL_RESTURL
|
|
||||||
? process.env.LOCAL_RESTURL
|
|
||||||
: 'https://api.fullstack.cash/v4/'
|
|
||||||
const bchjs = new BCHJS({ restURL })
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Encryption {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
_this.config = config
|
|
||||||
_this.axios = axios
|
|
||||||
_this.routeUtils = routeUtils
|
|
||||||
_this.bchjs = bchjs
|
|
||||||
// _this.blockbook = blockbook
|
|
||||||
// _this.rawTransactions = rawTransactions
|
|
||||||
|
|
||||||
_this.router = router
|
|
||||||
_this.router.get('/', _this.root)
|
|
||||||
_this.router.get('/publickey/:address', _this.getPublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ success: false, error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle error patterns specific to this route.
|
|
||||||
if (err.message) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ success: false, error: err.message })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Unhandled error in Encryption library: ', err)
|
|
||||||
|
|
||||||
// If error can be handled, return the stack trace
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root API endpoint. Simply acknowledges that it exists.
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'encryption' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /encryption/publickey/{addr} Get public key for a BCH address.
|
|
||||||
* @apiName Get encryption key for bch address
|
|
||||||
* @apiGroup Encryption
|
|
||||||
* @apiDescription Searches the blockchain for a public key associated with a
|
|
||||||
* BCH address. Returns an object. If successful, the publicKey property will
|
|
||||||
* contain a hexidecimal representation of the public key.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/encryption/publickey/bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getPublicKey (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
|
|
||||||
// Reject if address is an array.
|
|
||||||
if (Array.isArray(address)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
error: 'address can not be an array.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
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.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing encryption/getPublicKey with this address: ',
|
|
||||||
cashAddr
|
|
||||||
)
|
|
||||||
|
|
||||||
const rawTxData = await _this.bchjs.Electrumx.transactions([cashAddr], usrObj)
|
|
||||||
// console.log(`rawTxData: ${JSON.stringify(rawTxData, null, 2)}`)
|
|
||||||
|
|
||||||
// Extract just the TXIDs
|
|
||||||
const txids = rawTxData.transactions[0].transactions.map((elem) => elem.tx_hash)
|
|
||||||
// console.log(`txids: ${JSON.stringify(txids, null, 2)}`)
|
|
||||||
|
|
||||||
// throw error if there is no transaction history.
|
|
||||||
if (!txids || txids.length === 0) {
|
|
||||||
throw new Error('No transaction history.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through the transaction history and search for the public key.
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const thisTx = txids[i]
|
|
||||||
|
|
||||||
const txDetails = await _this.bchjs.RawTransactions.getRawTransaction(
|
|
||||||
[thisTx],
|
|
||||||
true,
|
|
||||||
usrObj
|
|
||||||
)
|
|
||||||
// console.log(`txDetails: ${JSON.stringify(txDetails, null, 2)}`)
|
|
||||||
|
|
||||||
const vin = txDetails[0].vin
|
|
||||||
|
|
||||||
// Loop through each input.
|
|
||||||
for (let j = 0; j < vin.length; j++) {
|
|
||||||
const thisVin = vin[j]
|
|
||||||
// console.log(`thisVin: ${JSON.stringify(thisVin, null, 2)}`)
|
|
||||||
|
|
||||||
// Extract the script signature.
|
|
||||||
const scriptSig = thisVin.scriptSig.asm.split(' ')
|
|
||||||
// console.log(`scriptSig: ${JSON.stringify(scriptSig, null, 2)}`)
|
|
||||||
|
|
||||||
// Extract the public key from the script signature.
|
|
||||||
const pubKey = scriptSig[scriptSig.length - 1]
|
|
||||||
// console.log(`pubKey: ${pubKey}`)
|
|
||||||
|
|
||||||
// Generate cash address from public key.
|
|
||||||
const keyBuf = Buffer.from(pubKey, 'hex')
|
|
||||||
const ec = _this.bchjs.ECPair.fromPublicKey(keyBuf)
|
|
||||||
const cashAddr2 = _this.bchjs.ECPair.toCashAddress(ec)
|
|
||||||
// console.log(`cashAddr2: ${cashAddr2}`)
|
|
||||||
|
|
||||||
// If public keys match, this is the correct public key.
|
|
||||||
if (cashAddr === cashAddr2) {
|
|
||||||
res.status(200)
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
publicKey: pubKey
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
publicKey: 'not found'
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
// console.log('Error in encryption.js/getPublicKey().', err)
|
|
||||||
wlogger.error('Error in encryption.js/getPublicKey().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Encryption
|
|
||||||
@@ -1,989 +0,0 @@
|
|||||||
/*
|
|
||||||
A library for interacting with the Full Node
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../../util/winston-logging')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
// Used to convert error messages to strings, to safely pass to users.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Blockchain {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
this.bchjs = bchjs
|
|
||||||
this.axios = axios
|
|
||||||
this.routeUtils = routeUtils
|
|
||||||
|
|
||||||
this.router = router
|
|
||||||
this.router.get('/', this.root)
|
|
||||||
this.router.get('/getBestBlockHash', this.getBestBlockHash)
|
|
||||||
this.router.get('/getBlockchainInfo', this.getBlockchainInfo)
|
|
||||||
this.router.get('/getBlockCount', this.getBlockCount)
|
|
||||||
this.router.get('/getBlockHeader/:hash', this.getBlockHeaderSingle)
|
|
||||||
this.router.post('/getBlockHeader', this.getBlockHeaderBulk)
|
|
||||||
this.router.get('/getChainTips', this.getChainTips)
|
|
||||||
this.router.get('/getDifficulty', this.getDifficulty)
|
|
||||||
this.router.get('/getMempoolEntry/:txid', this.getMempoolEntrySingle)
|
|
||||||
this.router.post('/getMempoolEntry', this.getMempoolEntryBulk)
|
|
||||||
this.router.get(
|
|
||||||
'/getMempoolAncestors/:txid',
|
|
||||||
this.getMempoolAncestorsSingle
|
|
||||||
)
|
|
||||||
this.router.get('/getMempoolInfo', this.getMempoolInfo)
|
|
||||||
this.router.get('/getRawMempool', this.getRawMempool)
|
|
||||||
this.router.get('/getTxOut/:txid/:n', this.getTxOut)
|
|
||||||
this.router.post('/getTxOut', this.getTxOutPost)
|
|
||||||
this.router.get('/getTxOutProof/:txid', this.getTxOutProofSingle)
|
|
||||||
this.router.post('/getTxOutProof', this.getTxOutProofBulk)
|
|
||||||
this.router.get('/verifyTxOutProof/:proof', this.verifyTxOutProofSingle)
|
|
||||||
this.router.post('/verifyTxOutProof', this.verifyTxOutProofBulk)
|
|
||||||
this.router.post('/getBlock', this.getBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'blockchain' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getBestBlockHash Get best block hash
|
|
||||||
* @apiName GetBestBlockHash
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns the hash of the best (tip) block in the longest
|
|
||||||
* block chain.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBestBlockHash" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiSuccess {String} bestBlockHash 000000000000000002bc884334336d99c9a9c616670a9244c6a8c1fc35aa91a1
|
|
||||||
*/
|
|
||||||
async getBestBlockHash (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
options.data.id = 'getbestblockhash'
|
|
||||||
options.data.method = 'getbestblockhash'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockchain.ts/getBestBlockHash().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getBlockchainInfo Get blockchain info
|
|
||||||
* @apiName GetBlockchainInfo
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns an object containing various state info regarding blockchain processing.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBlockchainInfo" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiSuccess {Object} object Object containing data
|
|
||||||
* @apiSuccess {String} object.chain "main"
|
|
||||||
* @apiSuccess {Number} object.blocks 561838
|
|
||||||
* @apiSuccess {Number} object.headers 561838
|
|
||||||
* @apiSuccess {String} object.bestblockhash "000000000000000002307dd38cd01c7308b8febfcdf5772cf087b5bb023d55bc"
|
|
||||||
* @apiSuccess {Number} object.difficulty 246585566638.1496
|
|
||||||
* @apiSuccess {String} object.mediantime 1545402693
|
|
||||||
* @apiSuccess {Number} object.verificationprogress 0.999998831622689
|
|
||||||
* @apiSuccess {Boolean} object.chainwork "000000000000000000000000000000000000000000d8c09a8ab7262080266b3e"
|
|
||||||
* @apiSuccess {Number} object.pruned false
|
|
||||||
* @apiSuccess {Array} object.softforks Array of objects
|
|
||||||
* @apiSuccess {String} object.softforks.id "bip34"
|
|
||||||
* @apiSuccess {String} object.softforks.version 2
|
|
||||||
* @apiSuccess {Object} object.softforks.reject
|
|
||||||
* @apiSuccess {String} object.softforks.reject.status true
|
|
||||||
*/
|
|
||||||
async getBlockchainInfo (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
options.data.id = 'getblockchaininfo'
|
|
||||||
options.data.method = 'getblockchaininfo'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in blockchain.ts/getBlockchainInfo().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getBlockCount Get Block Count
|
|
||||||
* @apiName GetBlockCount
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns the number of blocks in the longest blockchain.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBlockCount" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiSuccess {Number} bestBlockCount 587665
|
|
||||||
*/
|
|
||||||
async getBlockCount (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
options.data.id = 'getblockcount'
|
|
||||||
options.data.method = 'getblockcount'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getBlockCount().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getBlockHeader/:hash Get single block header
|
|
||||||
* @apiName GetSingleBlockHeader
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription If verbose is false (default), returns a string that is
|
|
||||||
* serialized, hex-encoded data for blockheader 'hash'. If verbose is true,
|
|
||||||
* returns an Object with information about blockheader hash.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getBlockHeader/000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201?verbose=true" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiParam {String} hash block hash
|
|
||||||
* @apiParam {Boolean} verbose Return verbose data
|
|
||||||
*
|
|
||||||
* @apiSuccess {Object} object Object containing data
|
|
||||||
* @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201"
|
|
||||||
* @apiSuccess {Number} object.confirmations 61839
|
|
||||||
* @apiSuccess {Number} object.height 500000
|
|
||||||
* @apiSuccess {Number} object.version 536870912
|
|
||||||
* @apiSuccess {String} object.versionHex "20000000"
|
|
||||||
* @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091"
|
|
||||||
* @apiSuccess {Number} object.time 1509343584
|
|
||||||
* @apiSuccess {Number} object.mediantime 1509336533
|
|
||||||
* @apiSuccess {Number} object.nonce 3604508752
|
|
||||||
* @apiSuccess {String} object.bits "1809b91a"
|
|
||||||
* @apiSuccess {Number} object.difficulty 113081236211.4533
|
|
||||||
* @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714"
|
|
||||||
* @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523"
|
|
||||||
* @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3"
|
|
||||||
*/
|
|
||||||
async getBlockHeaderSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
let verbose = false
|
|
||||||
if (req.query.verbose && req.query.verbose.toString() === 'true') {
|
|
||||||
verbose = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = req.params.hash
|
|
||||||
if (!hash || hash === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hash can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
options.data.id = 'getblockheader'
|
|
||||||
options.data.method = 'getblockheader'
|
|
||||||
options.data.params = [hash, verbose]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getBlockHeaderSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /blockchain/getBlockHeader Get multiple block headers
|
|
||||||
* @apiName GetBulkBlockHeader
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription If verbose is false (default), returns a string that is
|
|
||||||
* serialized, hex-encoded data for blockheader 'hash'. If verbose is true,
|
|
||||||
* returns an Object with information about blockheader hash.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/blockchain/getBlockHeader" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"hashes\":[\"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201\",\"00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3\"],\"verbose\":true}"
|
|
||||||
*
|
|
||||||
* @apiParam {String} hash block hash
|
|
||||||
* @apiParam {Boolean} verbose Return verbose data
|
|
||||||
*
|
|
||||||
* @apiSuccess {Array} array array containing objects
|
|
||||||
* @apiSuccess {String} object.hash "000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201"
|
|
||||||
* @apiSuccess {Number} object.confirmations 61839
|
|
||||||
* @apiSuccess {Number} object.height 500000
|
|
||||||
* @apiSuccess {Number} object.version 536870912
|
|
||||||
* @apiSuccess {String} object.versionHex "20000000"
|
|
||||||
* @apiSuccess {String} object.merkleroot "4af279645e1b337e655ae3286fc2ca09f58eb01efa6ab27adedd1e9e6ec19091"
|
|
||||||
* @apiSuccess {Number} object.time 1509343584
|
|
||||||
* @apiSuccess {Number} object.mediantime 1509336533
|
|
||||||
* @apiSuccess {Number} object.nonce 3604508752
|
|
||||||
* @apiSuccess {String} object.bits "1809b91a"
|
|
||||||
* @apiSuccess {Number} object.difficulty 113081236211.4533
|
|
||||||
* @apiSuccess {String} object.chainwork "0000000000000000000000000000000000000000007ae48aca46e3b449ad9714"
|
|
||||||
* @apiSuccess {String} object.previousblockhash "0000000000000000043831d6ebb013716f0580287ee5e5687e27d0ed72e6e523"
|
|
||||||
* @apiSuccess {String} object.nextblockhash "00000000000000000568f0a96bf4348847bc84e455cbfec389f27311037a20f3"
|
|
||||||
*/
|
|
||||||
async getBlockHeaderBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const hashes = req.body.hashes
|
|
||||||
const verbose = req.body.verbose ? req.body.verbose : false
|
|
||||||
|
|
||||||
if (!Array.isArray(hashes)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'hashes needs to be an array. Use GET for single hash.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, hashes)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockchain/getBlockHeaderBulk with these hashes: ',
|
|
||||||
hashes
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each hash in the array.
|
|
||||||
for (let i = 0; i < hashes.length; i++) {
|
|
||||||
const hash = hashes[i]
|
|
||||||
|
|
||||||
if (hash.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: `This is not a hash: ${hash}` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
// Loop through each hash and creates an array of requests to call in parallel
|
|
||||||
const promises = hashes.map(async (hash) => {
|
|
||||||
options.data.id = 'getblockheader'
|
|
||||||
options.data.method = 'getblockheader'
|
|
||||||
options.data.params = [hash, verbose]
|
|
||||||
|
|
||||||
return _this.axios.request(options)
|
|
||||||
})
|
|
||||||
|
|
||||||
const axiosResult = await _this.axios.all(promises)
|
|
||||||
|
|
||||||
// Extract the data component from the axios response.
|
|
||||||
const result = axiosResult.map((x) => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getBlockHeaderBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getChainTips Get Chain Tips
|
|
||||||
* @apiName getChainTips
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Return information about all known tips in the block tree,
|
|
||||||
* including the main chain as well as orphaned branches.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getChainTips" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getChainTips (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getchaintips'
|
|
||||||
options.data.method = 'getchaintips'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getChainTips().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getDifficulty Get difficulty
|
|
||||||
* @apiName getDifficulty
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Get the current difficulty value, used to regulate mining
|
|
||||||
* power on the network.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getDifficulty" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getDifficulty (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getdifficulty'
|
|
||||||
options.data.method = 'getdifficulty'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getDifficulty().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getMempoolEntry/:txid Get single mempool entry
|
|
||||||
* @apiName getMempoolEntry
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns mempool data for given transaction. TXID must be in
|
|
||||||
* mempool (unconfirmed)
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getMempoolEntry/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getMempoolEntrySingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input parameter
|
|
||||||
const txid = req.params.txid
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getmempoolentry'
|
|
||||||
options.data.method = 'getmempoolentry'
|
|
||||||
options.data.params = [txid]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getMempoolEntrySingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /blockchain/getMempoolEntry Get bulk mempool entry
|
|
||||||
* @apiName getMempoolEntryBulk
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns mempool data for multiple transactions
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST https://api.fullstack.cash/v4/blockchain/getMempoolEntry -H "Content-Type: application/json" -d "{\"txids\":[\"a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1\",\"5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e\"]}"
|
|
||||||
*/
|
|
||||||
async getMempoolEntryBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const txids = req.body.txids
|
|
||||||
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txids needs to be an array. Use GET for single txid.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockchain/getMempoolEntry with these txids: ',
|
|
||||||
txids
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate each element in the array
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const txid = txids[i]
|
|
||||||
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'This is not a txid' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
// Loop through each txid and creates an array of requests to call in parallel
|
|
||||||
const promises = txids.map(async (txid) => {
|
|
||||||
options.data.id = 'getmempoolentry'
|
|
||||||
options.data.method = 'getmempoolentry'
|
|
||||||
options.data.params = [txid]
|
|
||||||
|
|
||||||
return _this.axios.request(options)
|
|
||||||
})
|
|
||||||
|
|
||||||
const axiosResult = await _this.axios.all(promises)
|
|
||||||
|
|
||||||
// Extract the data component from the axios response.
|
|
||||||
const result = axiosResult.map((x) => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getMempoolEntryBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getMempoolAncestors/:txid Get Mempool Ancestors
|
|
||||||
* @apiName getMempoolAncestors
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns mempool ancestors data for given TXID. It must be in
|
|
||||||
* mempool (unconfirmed). This call is handy to tell if a UTXO is bumping up
|
|
||||||
* against the 25 ancestor chain-limit.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getMempoolAncestors/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getMempoolAncestorsSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input parameter
|
|
||||||
const txid = req.params.txid
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
let verbose = req.params.verbose
|
|
||||||
if (verbose === undefined) verbose = false
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getmempoolancestors'
|
|
||||||
options.data.method = 'getmempoolancestors'
|
|
||||||
options.data.params = [txid, verbose]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
// console.log(`response: ${util.inspect(response)}`)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getMempoolAncestorsSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getMempoolInfo Get mempool info
|
|
||||||
* @apiName getMempoolInfo
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns details on the active state of the TX memory pool.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET https://api.fullstack.cash/v4/getMempoolInfo -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getMempoolInfo (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getmempoolinfo'
|
|
||||||
options.data.method = 'getmempoolinfo'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getMempoolInfo().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getRawMempool Get mempool info
|
|
||||||
* @apiName getMempoolInfo
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns details on the active state of the TX memory pool.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET https://api.fullstack.cash/v4/getMempoolInfo -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getRawMempool/?verbose= Get raw mempool
|
|
||||||
* @apiName getRawMempool
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns all transaction ids in memory pool as a json array
|
|
||||||
* of string transaction ids.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/getRawMempool/?verbose=true" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiParam {Boolean} verbose Return verbose data
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getRawMempool (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
let verbose = false
|
|
||||||
if (req.query.verbose && req.query.verbose === 'true') verbose = true
|
|
||||||
|
|
||||||
options.data.id = 'getrawmempool'
|
|
||||||
options.data.method = 'getrawmempool'
|
|
||||||
options.data.params = [verbose]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getRawMempool().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getTxOut/:txid/:n?mempool= Get Tx Out
|
|
||||||
* @apiName getTxOut
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns details about an unspent transaction output.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getTxOut/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33/0?mempool=false" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiParam {String} txid Transaction id (required)
|
|
||||||
* @apiParam {Number} n Output number (required)
|
|
||||||
* @apiParam {Boolean} mempool Check mempool or not (optional)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// Returns details about an unspent transaction output.
|
|
||||||
async getTxOut (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input parameter
|
|
||||||
const txid = req.params.txid
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
let n = req.params.n
|
|
||||||
if (n === undefined || n === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'n can not be empty' })
|
|
||||||
}
|
|
||||||
n = parseInt(n)
|
|
||||||
|
|
||||||
let includeMempool = false
|
|
||||||
if (req.query.includeMempool && req.query.includeMempool === 'true') {
|
|
||||||
includeMempool = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'gettxout'
|
|
||||||
options.data.method = 'gettxout'
|
|
||||||
options.data.params = [txid, n, includeMempool]
|
|
||||||
|
|
||||||
// console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`)
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getTxOut().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /blockchain/getTxOut Validate a UTXO
|
|
||||||
* @apiName getTxOut
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns details about an unspent transaction output (UTXO).
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl "https://api.fullstack.cash/v4/blockchain/getTxOut/" -X POST -H "Content-Type: application/json" --data-binary '{"txid":"d5228d2cdc77fbe5a9aa79f19b0933b6802f9f0067f42847fc4fe343664723e5","vout":0,"mempool":true}'
|
|
||||||
*
|
|
||||||
* @apiParam {String} txid Transaction id (required)
|
|
||||||
* @apiParam {Number} vout of transaction (required)
|
|
||||||
* @apiParam {Boolean} mempool Check mempool or not (optional)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// Returns details about an unspent transaction output.
|
|
||||||
async getTxOutPost (req, res, next) {
|
|
||||||
try {
|
|
||||||
const txid = req.body.txid
|
|
||||||
let n = req.body.vout
|
|
||||||
const mempool = req.body.mempool ? req.body.mempool : true
|
|
||||||
|
|
||||||
// Validate input parameter
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (n === undefined || n === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'vout can not be empty' })
|
|
||||||
}
|
|
||||||
n = parseInt(n)
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'gettxout'
|
|
||||||
options.data.method = 'gettxout'
|
|
||||||
options.data.params = [txid, n, mempool]
|
|
||||||
|
|
||||||
// console.log(`requestConfig: ${JSON.stringify(requestConfig, null, 2)}`)
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getTxOutPost().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /blockchain/getTxOutProofSingle/:txid Get Tx Out Proof
|
|
||||||
* @apiName getTxOutProofSingle
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns a hex-encoded proof that 'txid' was included in a block.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/blockchain/getTxOutProofSingle/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
* @apiParam {String} txid Transaction id (required)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getTxOutProofSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input parameter
|
|
||||||
const txid = req.params.txid
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'gettxoutproof'
|
|
||||||
options.data.method = 'gettxoutproof'
|
|
||||||
options.data.params = [[txid]]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getTxOutProofSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a hex-encoded proof that 'txid' was included in a block.
|
|
||||||
async getTxOutProofBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const txids = req.body.txids
|
|
||||||
|
|
||||||
// Reject if txids is not an array.
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'txids needs to be an array. Use GET for single txid.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
// Validate each element in the array.
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const txid = txids[i]
|
|
||||||
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid txid. Double check your txid is valid: ${txid}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockchain/getTxOutProof with these txids: ',
|
|
||||||
txids
|
|
||||||
)
|
|
||||||
|
|
||||||
// Loop through each txid and creates an array of requests to call in parallel
|
|
||||||
const promises = txids.map(async (txid) => {
|
|
||||||
options.data.id = 'gettxoutproof'
|
|
||||||
options.data.method = 'gettxoutproof'
|
|
||||||
options.data.params = [[txid]]
|
|
||||||
|
|
||||||
return _this.axios.request(options)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all parallel promisses to resolve.
|
|
||||||
const axiosResult = await _this.axios.all(promises)
|
|
||||||
|
|
||||||
// Extract the data component from the axios response.
|
|
||||||
const result = axiosResult.map((x) => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/getTxOutProofBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyTxOutProofSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input parameter
|
|
||||||
const proof = req.params.proof
|
|
||||||
if (!proof || proof === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'proof can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'verifytxoutproof'
|
|
||||||
options.data.method = 'verifytxoutproof'
|
|
||||||
options.data.params = [req.params.proof]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/verifyTxOutProofSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyTxOutProofBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const proofs = req.body.proofs
|
|
||||||
|
|
||||||
// Reject if proofs is not an array.
|
|
||||||
if (!Array.isArray(proofs)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'proofs needs to be an array. Use GET for single proof.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, proofs)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/api.fullstack.cash/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
// Validate each element in the array.
|
|
||||||
for (let i = 0; i < proofs.length; i++) {
|
|
||||||
const proof = proofs[i]
|
|
||||||
|
|
||||||
if (!proof || proof === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: `proof can not be empty: ${proof}` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug(
|
|
||||||
'Executing blockchain/verifyTxOutProof with these proofs: ',
|
|
||||||
proofs
|
|
||||||
)
|
|
||||||
|
|
||||||
// Loop through each proof and creates an array of requests to call in parallel
|
|
||||||
const promises = proofs.map(async (proof) => {
|
|
||||||
options.data.id = 'verifytxoutproof'
|
|
||||||
options.data.method = 'verifytxoutproof'
|
|
||||||
options.data.params = [proof]
|
|
||||||
|
|
||||||
return _this.axios.request(options)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all parallel promisses to resolve.
|
|
||||||
const axiosResult = await _this.axios.all(promises)
|
|
||||||
|
|
||||||
// Extract the data component from the axios response.
|
|
||||||
const result = axiosResult.map((x) => x.data.result[0])
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.ts/verifyTxOutProofBulk().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /blockchain/getBlock/ Get block details
|
|
||||||
* @apiName getBlock
|
|
||||||
* @apiGroup Blockchain
|
|
||||||
* @apiDescription Returns block details
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl "https://api.fullstack.cash/v4/blockchain/getblock/" -X POST -H "Content-Type: application/json" --data-binary '{"blockhash":"000000000000000002a5fe0bdd6e3f04342a975c0f55e57f97e73bb90041676b","verbosity":0 }'
|
|
||||||
*
|
|
||||||
* @apiParam {String} blockhash Block hash (required)
|
|
||||||
* @apiParam {Number} verbosity Default 1 (optional)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getBlock (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input parameter
|
|
||||||
const blockhash = req.body.blockhash
|
|
||||||
let verbosity = req.body.verbosity
|
|
||||||
|
|
||||||
// Default to a value of 1 if another verbosity level is not defined.
|
|
||||||
if (!verbosity && verbosity !== 0) verbosity = 1
|
|
||||||
|
|
||||||
if (!blockhash || blockhash === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'blockhash can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getblock'
|
|
||||||
options.data.method = 'getblock'
|
|
||||||
options.data.params = [blockhash, verbosity]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in blockchain.js/getBlock()', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Blockchain
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
// const routeUtils = require('../route-utils')
|
|
||||||
const wlogger = require('../../../util/winston-logging')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
// Used for processing error messages before sending them to the user.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Control {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
_this.axios = axios
|
|
||||||
_this.routeUtils = routeUtils
|
|
||||||
|
|
||||||
_this.router = router
|
|
||||||
_this.router.get('/', _this.root)
|
|
||||||
_this.router.get('/getNetworkInfo', _this.getNetworkInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'control' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /control/getnetworkinfo Get Network Info
|
|
||||||
* @apiName GetNetworkInfo
|
|
||||||
* @apiGroup Control
|
|
||||||
* @apiDescription RPC call which gets basic full node information.
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/control/getnetworkinfo" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getNetworkInfo (req, res, next) {
|
|
||||||
// Axios options
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getnetworkinfo'
|
|
||||||
options.data.method = 'getnetworkinfo'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
try {
|
|
||||||
// const response = await BitboxHTTP(requestConfig)
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (error) {
|
|
||||||
wlogger.error('Error in control.ts/getNetworkInfo().', error)
|
|
||||||
|
|
||||||
return _this.errorHandler(error, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// router.get('/getMemoryInfo', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getmemoryinfo",
|
|
||||||
// method: "getmemoryinfo"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/help', (req, res, next) => {
|
|
||||||
// BITBOX.Control.help()
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.post('/stop', (req, res, next) => {
|
|
||||||
// BITBOX.Control.stop()
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Control
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
const wlogger = require('../../../util/winston-logging')
|
|
||||||
|
|
||||||
// Used to convert error messages to strings, to safely pass to users.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class Mining {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
_this.axios = axios
|
|
||||||
_this.routeUtils = routeUtils
|
|
||||||
|
|
||||||
_this.router = router
|
|
||||||
_this.router.get('/', _this.root)
|
|
||||||
_this.router.get('/getMiningInfo', _this.getMiningInfo)
|
|
||||||
_this.router.get('/getNetworkHashPS', _this.getNetworkHashPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'mining' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// router.get('/getBlockTemplate/:templateRequest', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getblocktemplate",
|
|
||||||
// method: "getblocktemplate",
|
|
||||||
// params: [
|
|
||||||
// req.params.templateRequest
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /mining/getMiningInfo Get Mining Info.
|
|
||||||
* @apiName Mining info.
|
|
||||||
* @apiGroup Mining
|
|
||||||
* @apiDescription Returns a json object containing mining-related information.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/mining/getMiningInfo" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getMiningInfo (req, res, next) {
|
|
||||||
try {
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getmininginfo'
|
|
||||||
options.data.method = 'getmininginfo'
|
|
||||||
options.data.params = []
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in mining.ts/getMiningInfo().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /mining/getNetworkHashps?nblocks=&height= Get Estimated network hashes per second.
|
|
||||||
* @apiName Estimated network hashes per second.
|
|
||||||
* @apiGroup Mining
|
|
||||||
* @apiDescription Returns the estimated network hashes per second based on the last n blocks. Pass in [blocks] to override # of blocks, -1 specifies since last difficulty change. Pass in [height] to estimate the network speed at the time when a certain block was found.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/mining/getNetworkHashps?nblocks=120&height=-1" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
async getNetworkHashPS (req, res, next) {
|
|
||||||
try {
|
|
||||||
let nblocks = 120 // Default
|
|
||||||
let height = -1 // Default
|
|
||||||
if (req.query.nblocks) nblocks = parseInt(req.query.nblocks)
|
|
||||||
if (req.query.height) height = parseInt(req.query.height)
|
|
||||||
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getnetworkhashps'
|
|
||||||
options.data.method = 'getnetworkhashps'
|
|
||||||
options.data.params = [nblocks, height]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in mining.ts/getNetworkHashPS().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// router.post('/submitBlock/:hex', (req, res, next) => {
|
|
||||||
// let parameters = '';
|
|
||||||
// if(req.query.parameters && req.query.parameters !== '') {
|
|
||||||
// parameters = true;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"submitblock",
|
|
||||||
// method: "submitblock",
|
|
||||||
// params: [
|
|
||||||
// req.params.hex,
|
|
||||||
// parameters
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Mining
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
router.get('/', async (req, res, next) => {
|
|
||||||
res.json({ status: 'network' })
|
|
||||||
})
|
|
||||||
|
|
||||||
// router.post('/addNode/:node/:command', (req, res, next) => {
|
|
||||||
// BITBOX.Network.addNode(req.params.node, req.params.command)
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.post('/clearBanned', (req, res, next) => {
|
|
||||||
// BITBOX.Network.clearBanned()
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.post('/disconnectNode/:address/:nodeid', (req, res, next) => {
|
|
||||||
// BITBOX.Network.disconnectNode(req.params.address, req.params.nodeid)
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/getAddedNodeInfo/:node', (req, res, next) => {
|
|
||||||
// BITBOX.Network.getAddedNodeInfo(req.params.node)
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/getConnectionCount', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getconnectioncount",
|
|
||||||
// method: "getconnectioncount"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/getNetTotals', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getnettotals",
|
|
||||||
// method: "getnettotals"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/getNetworkInfo', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getnetworkinfo",
|
|
||||||
// method: "getnetworkinfo"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/getPeerInfo', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"getpeerinfo",
|
|
||||||
// method: "getpeerinfo"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(response.data.result);
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.get('/ping', (req, res, next) => {
|
|
||||||
// BitboxHTTP({
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: "1.0",
|
|
||||||
// id:"ping",
|
|
||||||
// method: "ping"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .then((response) => {
|
|
||||||
// res.json(JSON.stringify(response.data.result));
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// res.send(error.response.data.error.message);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.post('/setBan/:subnet/:command', (req, res, next) => {
|
|
||||||
// // TODO finish this
|
|
||||||
// BITBOX.Network.getConnectionCount(req.params.subnet, req.params.command)
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// router.post('/setNetworkActive/:state', (req, res, next) => {
|
|
||||||
// let state = true;
|
|
||||||
// if(req.params.state && req.params.state === 'false') {
|
|
||||||
// state = false;
|
|
||||||
// }
|
|
||||||
// BITBOX.Network.getConnectionCount(state)
|
|
||||||
// .then((result) => {
|
|
||||||
// res.json(result);
|
|
||||||
// }, (err) => { console.log(err);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
@@ -1,589 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
const wlogger = require('../../../util/winston-logging')
|
|
||||||
|
|
||||||
// Used to convert error messages to strings, to safely pass to users.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
let _this
|
|
||||||
class RawTransactions {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
// Encapsulate external dependencies.
|
|
||||||
this.axios = axios
|
|
||||||
this.routeUtils = routeUtils
|
|
||||||
|
|
||||||
// Define Express routes.
|
|
||||||
this.router = router
|
|
||||||
this.router.get('/', this.root)
|
|
||||||
this.router.get(
|
|
||||||
'/decodeRawTransaction/:hex',
|
|
||||||
this.decodeRawTransactionSingle
|
|
||||||
)
|
|
||||||
this.router.post('/decodeRawTransaction', this.decodeRawTransactionBulk)
|
|
||||||
this.router.get('/decodeScript/:hex', this.decodeScriptSingle)
|
|
||||||
this.router.post('/decodeScript', this.decodeScriptBulk)
|
|
||||||
this.router.post('/getRawTransaction', this.getRawTransactionBulk)
|
|
||||||
this.router.get('/getRawTransaction/:txid', this.getRawTransactionSingle)
|
|
||||||
this.router.post('/sendRawTransaction', this.sendRawTransactionBulk)
|
|
||||||
this.router.get('/sendRawTransaction/:hex', this.sendRawTransactionSingle)
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'rawtransactions' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode transaction hex into a JSON object.
|
|
||||||
// GET
|
|
||||||
/**
|
|
||||||
* @api {get} /rawtransactions/decodeRawTransaction/{hex} Decode Single Raw Transaction.
|
|
||||||
* @apiName Decode Single Raw Transaction
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Return a JSON object representing the serialized, hex-encoded transaction.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/decodeRawTransaction/02000000010e991f7ccec410f27d333f737f149b5d3be6728687da81072e638aed0063a176010000006b483045022100cd20443b0af090053450bc4ab00d563d4ac5955bb36e0135b00b8a96a19f233302205047f2c70a08c6ef4b76f2d198b33a31d17edfaa7e1e9e865894da0d396009354121024d4e7f522f67105b7bf5f9dbe557e7b2244613fdfcd6fe09304f93877328f6beffffffff02a0860100000000001976a9140ee020c07f39526ac5505c54fa1ab98490979b8388acb5f0f70b000000001976a9143a9b2b0c12fe722fcf653b6ef5dcc38732d6ff5188ac00000000" -H "accept: application/json"
|
|
||||||
*/
|
|
||||||
async decodeRawTransactionSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const hex = req.params.hex
|
|
||||||
|
|
||||||
// Throw an error if hex is empty.
|
|
||||||
if (!hex || hex === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hex can not be empty' })
|
|
||||||
}
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'decoderawtransaction'
|
|
||||||
options.data.method = 'decoderawtransaction'
|
|
||||||
options.data.params = [hex]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeRawTransaction: `, err)
|
|
||||||
wlogger.error(
|
|
||||||
'Error in rawtransactions.ts/decodeRawTransactionSingle().',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /rawtransactions/decodeRawTransaction Decode Bulk Raw Transactions.
|
|
||||||
* @apiName Decode Bulk Raw Transactions
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Return bulk hex encoded transaction.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/decodeRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async decodeRawTransactionBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const hexes = req.body.hexes
|
|
||||||
|
|
||||||
if (!Array.isArray(hexes)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hexes must be an array' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// const results = []
|
|
||||||
|
|
||||||
// Validate each element in the address array.
|
|
||||||
for (let i = 0; i < hexes.length; i++) {
|
|
||||||
const thisHex = hexes[i]
|
|
||||||
|
|
||||||
// Reject if id is empty
|
|
||||||
if (!thisHex || thisHex === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'Encountered empty hex' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
// Loop through each height and creates an array of requests to call in parallel
|
|
||||||
const promises = hexes.map(async (hex) => {
|
|
||||||
options.data.id = 'decoderawtransaction'
|
|
||||||
options.data.method = 'decoderawtransaction'
|
|
||||||
options.data.params = [hex]
|
|
||||||
|
|
||||||
return _this.axios.request(options)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const axiosResult = await _this.axios.all(promises)
|
|
||||||
|
|
||||||
// Retrieve the data part of the result.
|
|
||||||
const result = axiosResult.map((x) => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/getRawTransaction: `, err)
|
|
||||||
wlogger.error(
|
|
||||||
'Error in rawtransactions.ts/decodeRawTransactionBulk().',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode a raw transaction from hex to assembly.
|
|
||||||
// GET single
|
|
||||||
/**
|
|
||||||
* @api {get} /rawtransactions/decodeScript/{hex} Decode Single Script.
|
|
||||||
* @apiName Decode Single Script
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Decode a hex-encoded script.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/decodeScript/4830450221009a51e00ec3524a7389592bc27bea4af5104a59510f5f0cfafa64bbd5c164ca2e02206c2a8bbb47eabdeed52f17d7df668d521600286406930426e3a9415fe10ed592012102e6e1423f7abde8b70bca3e78a7d030e5efabd3eb35c19302542b5fe7879c1a16" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async decodeScriptSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const hex = req.params.hex
|
|
||||||
|
|
||||||
// Throw an error if hex is empty.
|
|
||||||
if (!hex || hex === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hex can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'decodescript'
|
|
||||||
options.data.method = 'decodescript'
|
|
||||||
options.data.params = [hex]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeScript: `, err)
|
|
||||||
wlogger.error('Error in rawtransactions.ts/decodeScriptSingle().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode a raw transaction from hex to assembly.
|
|
||||||
// POST bulk
|
|
||||||
/**
|
|
||||||
* @api {post} /rawtransactions/decodeScript Bulk Decode Script.
|
|
||||||
* @apiName Bulk Decode Script
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Decode multiple hex-encoded scripts.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
*curl -X POST "https://api.fullstack.cash/v4/rawtransactions/decodeScript" -H "accept:" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async decodeScriptBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const hexes = req.body.hexes
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!Array.isArray(hexes)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hexes must be an array' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each hex in the array
|
|
||||||
for (let i = 0; i < hexes.length; i++) {
|
|
||||||
const hex = hexes[i]
|
|
||||||
|
|
||||||
// Throw an error if hex is empty.
|
|
||||||
if (!hex || hex === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'Encountered empty hex' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
// Loop through each hex and create an array of promises
|
|
||||||
const promises = hexes.map(async (hex) => {
|
|
||||||
options.data.id = 'decodescript'
|
|
||||||
options.data.method = 'decodescript'
|
|
||||||
options.data.params = [hex]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
return response
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all parallel promises to return.
|
|
||||||
const resolved = await Promise.all(promises)
|
|
||||||
|
|
||||||
// Retrieve the data from each resolved promise.
|
|
||||||
const result = resolved.map((x) => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/decodeScript: `, err)
|
|
||||||
wlogger.error('Error in rawtransactions.ts/decodeScriptBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve raw transactions details from the full node.
|
|
||||||
|
|
||||||
async getRawTransactionsFromNode (txid, verbose) {
|
|
||||||
try {
|
|
||||||
const options = _this.routeUtils.getAxiosOptions()
|
|
||||||
|
|
||||||
options.data.id = 'getrawtransaction'
|
|
||||||
options.data.method = 'getrawtransaction'
|
|
||||||
options.data.params = [txid, verbose]
|
|
||||||
|
|
||||||
const response = await _this.axios.request(options)
|
|
||||||
|
|
||||||
return response.data.result
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in rawtransactions.ts/getRawTransactionsFromNode().')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a JSON object breakdown of transaction details.
|
|
||||||
// POST
|
|
||||||
/**
|
|
||||||
* @api {post} /rawtransactions/getRawTransaction Get Bulk Raw Transactions.
|
|
||||||
* @apiName Get Bulk Raw Transactions.
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Return the raw transaction data for multiple transactions. If verbose is 'true', returns an Object with information about 'txid'. If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/getRawTransaction" -H "accept: application/json" -H "Content-Type: application/json" -d '{"txids":["a5f972572ee1753e2fd2457dd61ce5f40fa2f8a30173d417e49feef7542c96a1","5165dc531aad05d1149bb0f0d9b7bda99c73e2f05e314bcfb5b4bb9ca5e1af5e"],"verbose":true}'
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getRawTransactionBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
let verbose = 0
|
|
||||||
if (req.body.verbose) verbose = 1
|
|
||||||
|
|
||||||
const txids = req.body.txids
|
|
||||||
if (!Array.isArray(txids)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txids must be an array' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, txids)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each txid in the array.
|
|
||||||
for (let i = 0; i < txids.length; i++) {
|
|
||||||
const txid = txids[i]
|
|
||||||
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'Encountered empty TXID' })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `parameter 1 must be of length 64 (not ${txid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through each txid and create an array of promises
|
|
||||||
const promises = txids.map(async (txid) =>
|
|
||||||
_this.getRawTransactionsFromNode(txid, verbose)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all parallel promises to return.
|
|
||||||
const axiosResult = await _this.axios.all(promises)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(axiosResult)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/getRawTransaction: `, err)
|
|
||||||
wlogger.error('Error in rawtransactions.ts/getRawTransactionBulk().', err)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a JSON object breakdown of transaction details.
|
|
||||||
// GET
|
|
||||||
/**
|
|
||||||
* @api {get} /rawtransactions/getRawTransaction/{txid} Return the raw transaction data.
|
|
||||||
* @apiName Get Raw Transaction
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription return the raw transaction data. If verbose is 'true', returns an Object with information about 'txid'. If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/getRawTransaction/fe28050b93faea61fa88c4c630f0e1f0a1c24d0082dd0e10d369e13212128f33?verbose=true" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getRawTransactionSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
let verbose = 0
|
|
||||||
if (req.query.verbose === 'true') verbose = 1
|
|
||||||
|
|
||||||
const txid = req.params.txid
|
|
||||||
if (!txid || txid === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'txid can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (txid.length !== 64) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `parameter 1 must be of length 64 (not ${txid.length})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await _this.getRawTransactionsFromNode(txid, verbose)
|
|
||||||
|
|
||||||
return res.json(data)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
// logger.error(`Error in rawtransactions/getRawTransaction: `, err)
|
|
||||||
wlogger.error(
|
|
||||||
'Error in rawtransactions.ts/getRawTransactionSingle().',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transmit a raw transaction to the BCH network.
|
|
||||||
/**
|
|
||||||
* @api {post} /rawtransactions/sendRawTransaction Send Bulk Raw Transactions.
|
|
||||||
* @apiName Send Bulk Raw Transactions
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Submits multiple raw transaction (serialized, hex-encoded) to local node and network.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction" -H "accept:application/json" -H "Content-Type: application/json" -d '{"hexes":["01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000","01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000"]}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async sendRawTransactionBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validation
|
|
||||||
const hexes = req.body.hexes
|
|
||||||
|
|
||||||
// Reject if input is not an array
|
|
||||||
if (!Array.isArray(hexes)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hex must be an array' })
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = _this.routeUtils.getAxiosOptions()
|
|
||||||
options = _this.sendTxOptions(options)
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!_this.routeUtils.validateArraySize(req, hexes)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each element
|
|
||||||
for (let i = 0; i < hexes.length; i++) {
|
|
||||||
const hex = hexes[i]
|
|
||||||
|
|
||||||
if (hex === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'Encountered empty hex'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dev Note CT 1/31/2019:
|
|
||||||
// Sending the 'sendrawtrnasaction' RPC call to a full node in parallel will
|
|
||||||
// not work. Testing showed that the full node will return the same TXID for
|
|
||||||
// different TX hexes. I believe this is by design, to prevent double spends.
|
|
||||||
// In parallel, we are essentially asking the node to broadcast a new TX before
|
|
||||||
// it's finished broadcast the previous one. Serial execution is required.
|
|
||||||
|
|
||||||
// How to send TX hexes in parallel the WRONG WAY:
|
|
||||||
/*
|
|
||||||
// Collect an array of promises.
|
|
||||||
const promises = hexes.map(async (hex: any) => {
|
|
||||||
requestConfig.data.id = "sendrawtransaction"
|
|
||||||
requestConfig.data.method = "sendrawtransaction"
|
|
||||||
requestConfig.data.params = [hex]
|
|
||||||
return await BitboxHTTP(requestConfig)
|
|
||||||
})
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const axiosResult: Array<any> = await axios.all(promises)
|
|
||||||
// Retrieve the data part of the result.
|
|
||||||
const result = axiosResult.map(x => x.data.result)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Sending them serially.
|
|
||||||
const result = []
|
|
||||||
for (let i = 0; i < hexes.length; i++) {
|
|
||||||
const hex = hexes[i]
|
|
||||||
|
|
||||||
options.data.id = 'sendrawtransaction'
|
|
||||||
options.data.method = 'sendrawtransaction'
|
|
||||||
options.data.params = [hex]
|
|
||||||
|
|
||||||
const rpcResult = await _this.axios.request(options)
|
|
||||||
|
|
||||||
result.push(rpcResult.data.result)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error(
|
|
||||||
'Error in rawtransactions.ts/sendRawTransactionBulk().',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transmit a raw transaction to the BCH network.
|
|
||||||
/**
|
|
||||||
* @api {get} /rawtransactions/sendRawTransaction/{hex} Send Single Raw Transaction.
|
|
||||||
* @apiName Send Single Raw Transaction
|
|
||||||
* @apiGroup Raw Transaction
|
|
||||||
* @apiDescription Submits single raw transaction (serialized, hex-encoded) to local node and network.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/rawtransactions/sendRawTransaction/01000000013ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a000000006a4730440220540986d1c58d6e76f8f05501c520c38ce55393d0ed7ed3c3a82c69af04221232022058ea43ed6c05fec0eccce749a63332ed4525460105346f11108b9c26df93cd72012103083dfc5a0254613941ddc91af39ff90cd711cdcde03a87b144b883b524660c39ffffffff01807c814a000000001976a914d7e7c4e0b70eaa67ceff9d2823d1bbb9f6df9a5188ac00000000" -H "accept: "
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async sendRawTransactionSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const hex = req.params.hex // URL parameter
|
|
||||||
|
|
||||||
// Reject if input is not an array or a string
|
|
||||||
if (typeof hex !== 'string') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'hex must be a string' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (hex === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'Encountered empty hex'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = _this.routeUtils.getAxiosOptions()
|
|
||||||
options = _this.sendTxOptions(options)
|
|
||||||
|
|
||||||
// RPC call
|
|
||||||
options.data.id = 'sendrawtransaction'
|
|
||||||
options.data.method = 'sendrawtransaction'
|
|
||||||
options.data.params = [hex]
|
|
||||||
|
|
||||||
const rpcResult = await _this.axios.request(options)
|
|
||||||
|
|
||||||
const result = rpcResult.data.result
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error(
|
|
||||||
'Error in rawtransactions.ts/sendRawTransactionSingle().',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method modifies the default axios options. It attempts to inject
|
|
||||||
// a specific full node to use when broadcasting transactions. This is useful
|
|
||||||
// because it leverages the built-in protections that a full node has against
|
|
||||||
// accidental double spends. It mitigates a corner-case when rapidly spending
|
|
||||||
// TXs on load balanced nodes. By piping all TX sends through a single node,
|
|
||||||
// accidental double spends can be reduced.
|
|
||||||
sendTxOptions (options) {
|
|
||||||
try {
|
|
||||||
const sendUrl = process.env.RPC_SENDURL
|
|
||||||
|
|
||||||
if (sendUrl !== 'undefined' && sendUrl !== undefined) {
|
|
||||||
// console.log(`original options: ${JSON.stringify(options, null, 2)}`)
|
|
||||||
|
|
||||||
options.baseURL = process.env.RPC_SENDURL
|
|
||||||
|
|
||||||
// console.log(`modified options: ${JSON.stringify(options, null, 2)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in rawtransactions.js/sendTxOptions()')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = RawTransactions
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
This health-check API can be used to test the server for aliveness and
|
|
||||||
readiness.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
/* GET home page. */
|
|
||||||
router.get('/', (req, res, next) => {
|
|
||||||
res.json({ status: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = router
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
A library for interacting with the Bitcoin.com ninsight (not Insight) indexer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
// const axios = require('axios')
|
|
||||||
// const routeUtils = require('./route-utils')
|
|
||||||
// const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
// const BCHJS = require('@psf/bch-js')
|
|
||||||
// const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
// let _this
|
|
||||||
|
|
||||||
class Ninsight {
|
|
||||||
constructor () {
|
|
||||||
// _this = this
|
|
||||||
|
|
||||||
this.router = router
|
|
||||||
this.router.get('/', this.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'ninsight' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Ninsight
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/*
|
|
||||||
This price route is really just a wrapper for another API price endpoint.
|
|
||||||
The main reason for hosting this price wrapper is so that the price can
|
|
||||||
be accessible over Tor. Tor is typically blocked by other REST API servers.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const axios = require('axios')
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
const util = require('util')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
let _this // Global context for 'this' instance of the Class.
|
|
||||||
|
|
||||||
class Price {
|
|
||||||
constructor () {
|
|
||||||
_this = this
|
|
||||||
|
|
||||||
this.axios = axios
|
|
||||||
this.routeUtils = routeUtils
|
|
||||||
|
|
||||||
this.priceUrl = 'https://api.coinbase.com/v2/exchange-rates?currency=BCH'
|
|
||||||
this.coinexPriceUrl =
|
|
||||||
'https://api.coinex.com/v1/market/ticker?market=xecusdt'
|
|
||||||
|
|
||||||
this.bchCoinexPriceUrl =
|
|
||||||
'https://api.coinex.com/v1/market/ticker?market=bchusdt'
|
|
||||||
|
|
||||||
this.router = express.Router()
|
|
||||||
this.router.get('/', _this.root)
|
|
||||||
this.router.get('/usd', _this.getUSD)
|
|
||||||
this.router.get('/rates', _this.getBCHRate)
|
|
||||||
this.router.get('/bchausd', _this.getBCHAUSD)
|
|
||||||
this.router.get('/bchusd', _this.getBCHUSD)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRY error handler.
|
|
||||||
errorHandler (err, res) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = _this.routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root API endpoint. Simply acknowledges that it exists.
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'price' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /price/usd Get the USD price of BCH
|
|
||||||
* @apiName Get the USD price of BCH
|
|
||||||
* @apiGroup Price
|
|
||||||
* @apiDescription Get the USD price of BCH from Coinbase.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/price/usd" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getUSD (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Request options
|
|
||||||
const opt = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: _this.priceUrl,
|
|
||||||
timeout: 15000
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.request(opt)
|
|
||||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
|
||||||
|
|
||||||
return res.json({ usd: Number(response.data.data.rates.USD) })
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in price.js/getUSD().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /price/usd Get rates for several different currencies
|
|
||||||
* @apiName Get rates for several different currencies
|
|
||||||
* @apiGroup Price
|
|
||||||
* @apiDescription Get rates for several different currencies from Coinbase.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/price/rates" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
// Get rates for several different currencies
|
|
||||||
async getBCHRate (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Request options
|
|
||||||
const opt = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: _this.priceUrl,
|
|
||||||
timeout: 15000
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.request(opt)
|
|
||||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
|
||||||
|
|
||||||
return res.json(response.data.data.rates)
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in price.js/getUSD().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /price/bchausd Get the USD price of BCHA
|
|
||||||
* @apiName Get the USD price of BCHA
|
|
||||||
* @apiGroup Price
|
|
||||||
* @apiDescription Get the USD price of BCHA from Coinex.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/price/bchausd" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getBCHAUSD (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Request options
|
|
||||||
const opt = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: _this.coinexPriceUrl,
|
|
||||||
timeout: 15000
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.request(opt)
|
|
||||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
|
||||||
|
|
||||||
const price = Number(response.data.data.ticker.last)
|
|
||||||
|
|
||||||
return res.json({ usd: price })
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in price.js/getBCHAUSD().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /price/bchusd Get the USD price of BCH
|
|
||||||
* @apiName Get the USD price of BCH
|
|
||||||
* @apiGroup Price
|
|
||||||
* @apiDescription Get the USD price of BCH from Coinex.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/price/bchusd" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async getBCHUSD (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Request options
|
|
||||||
const opt = {
|
|
||||||
method: 'get',
|
|
||||||
baseURL: _this.bchCoinexPriceUrl,
|
|
||||||
timeout: 15000
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.request(opt)
|
|
||||||
// console.log(`response.data: ${JSON.stringify(response.data, null, 2)}`)
|
|
||||||
|
|
||||||
const price = Number(response.data.data.ticker.last)
|
|
||||||
|
|
||||||
return res.json({ usd: price })
|
|
||||||
} catch (err) {
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in price.js/getBCHUSD().', err)
|
|
||||||
|
|
||||||
return _this.errorHandler(err, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Price
|
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const SLPSDK = require('@psf/bch-js')
|
|
||||||
const SLP = new SLPSDK()
|
|
||||||
|
|
||||||
class Slpdb {
|
|
||||||
// Gets transaction history for all tokens for an address. Can also specify
|
|
||||||
// block height, but defaults to 0.
|
|
||||||
async getHistoricalSlpTransactions (addressList, fromBlock = 0) {
|
|
||||||
// Build SLPDB or query from addressList
|
|
||||||
const orQueryArray = []
|
|
||||||
for (const address of addressList) {
|
|
||||||
const cashAddress = SLP.SLP.Address.toCashAddress(address)
|
|
||||||
const slpAddress = SLP.SLP.Address.toSLPAddress(address)
|
|
||||||
|
|
||||||
const cashQuery = {
|
|
||||||
'in.e.a': cashAddress.slice(12)
|
|
||||||
}
|
|
||||||
const slpQuery = {
|
|
||||||
'slp.detail.outputs.address': slpAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
orQueryArray.push(cashQuery)
|
|
||||||
orQueryArray.push(slpQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
find: {
|
|
||||||
db: ['c', 'u'],
|
|
||||||
$query: {
|
|
||||||
$or: orQueryArray,
|
|
||||||
'slp.valid': true,
|
|
||||||
'blk.i': {
|
|
||||||
$not: {
|
|
||||||
$lte: fromBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
$orderby: {
|
|
||||||
'blk.i': -1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
_id: 0,
|
|
||||||
'tx.h': 1,
|
|
||||||
'in.i': 1,
|
|
||||||
'in.e': 1,
|
|
||||||
'out.e': 1,
|
|
||||||
'out.a': 1,
|
|
||||||
'slp.detail': 1,
|
|
||||||
blk: 1
|
|
||||||
},
|
|
||||||
limit: 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
|
|
||||||
|
|
||||||
let transactions = []
|
|
||||||
|
|
||||||
// Add confirmed transactions
|
|
||||||
if (result.data && result.data.c) {
|
|
||||||
transactions = transactions.concat(result.data.c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add unconfirmed transactions
|
|
||||||
if (result.data && result.data.u) {
|
|
||||||
transactions = transactions.concat(result.data.u)
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTokenStats (tokenId) {
|
|
||||||
const [
|
|
||||||
totalMinted,
|
|
||||||
totalBurned,
|
|
||||||
tokenDetails,
|
|
||||||
circulatingSupply
|
|
||||||
] = await Promise.all([
|
|
||||||
this.getTotalMinted(tokenId),
|
|
||||||
this.getTotalBurned(tokenId),
|
|
||||||
this.getTokenDetails(tokenId),
|
|
||||||
this.getTotalCirculating(tokenId)
|
|
||||||
])
|
|
||||||
|
|
||||||
tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted
|
|
||||||
tokenDetails.totalBurned = totalBurned
|
|
||||||
|
|
||||||
// tokenDetails.circulatingSupply =
|
|
||||||
// tokenDetails.totalMinted - tokenDetails.totalBurned
|
|
||||||
tokenDetails.circulatingSupply = circulatingSupply
|
|
||||||
|
|
||||||
return tokenDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCredentials () {
|
|
||||||
// Generate the Basic Authentication header for a private instance of SLPDB.
|
|
||||||
const SLPDB_PASS = process.env.SLPDB_PASS
|
|
||||||
? process.env.SLPDB_PASS
|
|
||||||
: 'BITBOX'
|
|
||||||
const username = 'BITBOX'
|
|
||||||
const password = SLPDB_PASS
|
|
||||||
const combined = `${username}:${password}`
|
|
||||||
var base64Credential = Buffer.from(combined).toString('base64')
|
|
||||||
var readyCredential = `Basic ${base64Credential}`
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
authorization: readyCredential,
|
|
||||||
timeout: 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
async runQuery (query) {
|
|
||||||
const queryString = JSON.stringify(query)
|
|
||||||
const queryBase64 = Buffer.from(queryString).toString('base64')
|
|
||||||
const url = `${process.env.SLPDB_URL}q/${queryBase64}`
|
|
||||||
|
|
||||||
const options = this.generateCredentials()
|
|
||||||
|
|
||||||
const response = await axios.get(url, options)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTotalMinted (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['g'],
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId,
|
|
||||||
'graphTxn.outputs.status': {
|
|
||||||
$in: [
|
|
||||||
'BATON_SPENT_IN_MINT',
|
|
||||||
'BATON_UNSPENT',
|
|
||||||
'BATON_SPENT_NOT_IN_MINT'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$unwind: '$graphTxn.outputs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: null,
|
|
||||||
count: {
|
|
||||||
$sum: '$graphTxn.outputs.slpAmount'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
|
|
||||||
if (!result.data.g.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(result.data.g[0].count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTotalCirculating (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['g'],
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId,
|
|
||||||
'graphTxn.outputs': {
|
|
||||||
$elemMatch: {
|
|
||||||
status: 'UNSPENT',
|
|
||||||
slpAmount: { $gte: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $unwind: '$graphTxn.outputs' },
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'graphTxn.outputs.status': 'UNSPENT',
|
|
||||||
'graphTxn.outputs.slpAmount': { $gte: 0 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: null,
|
|
||||||
circulating_supply: {
|
|
||||||
$sum: '$graphTxn.outputs.slpAmount'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
limit: 100000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
|
|
||||||
|
|
||||||
if (!result.data.g.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(result.data.g[0].circulating_supply)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTotalBurned (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['g'],
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId,
|
|
||||||
'graphTxn.outputs.status': {
|
|
||||||
$in: [
|
|
||||||
'SPENT_NON_SLP',
|
|
||||||
'BATON_SPENT_INVALID_SLP',
|
|
||||||
'SPENT_INVALID_SLP',
|
|
||||||
'BATON_SPENT_NON_SLP',
|
|
||||||
'MISSING_BCH_VOUT',
|
|
||||||
'BATON_MISSING_BCH_VOUT',
|
|
||||||
'BATON_SPENT_NOT_IN_MINT',
|
|
||||||
'EXCESS_INPUT_BURNED'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$unwind: '$graphTxn.outputs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'graphTxn.outputs.status': {
|
|
||||||
$in: [
|
|
||||||
'SPENT_NON_SLP',
|
|
||||||
'BATON_SPENT_INVALID_SLP',
|
|
||||||
'SPENT_INVALID_SLP',
|
|
||||||
'BATON_SPENT_NON_SLP',
|
|
||||||
'MISSING_BCH_VOUT',
|
|
||||||
'BATON_MISSING_BCH_VOUT',
|
|
||||||
'BATON_SPENT_NOT_IN_MINT',
|
|
||||||
'EXCESS_INPUT_BURNED'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: null,
|
|
||||||
count: {
|
|
||||||
$sum: '$graphTxn.outputs.slpAmount'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
|
|
||||||
if (!result.data.g.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(result.data.g[0].count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTokenDetails (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['t'],
|
|
||||||
find: {
|
|
||||||
$query: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
|
|
||||||
if (!result.data.t.length) {
|
|
||||||
throw new Error('Token could not be found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = this.formatTokenOutput(result.data.t[0])
|
|
||||||
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTokenOutput (token) {
|
|
||||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
|
||||||
|
|
||||||
token.tokenDetails.id = token.tokenDetails.tokenIdHex
|
|
||||||
delete token.tokenDetails.tokenIdHex
|
|
||||||
token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex
|
|
||||||
delete token.tokenDetails.documentSha256Hex
|
|
||||||
token.tokenDetails.initialTokenQty = parseFloat(
|
|
||||||
token.tokenDetails.genesisOrMintQuantity
|
|
||||||
)
|
|
||||||
delete token.tokenDetails.genesisOrMintQuantity
|
|
||||||
delete token.tokenDetails.transactionType
|
|
||||||
delete token.tokenDetails.batonVout
|
|
||||||
delete token.tokenDetails.sendOutputs
|
|
||||||
|
|
||||||
token.tokenDetails.blockCreated = token.tokenStats.block_created
|
|
||||||
token.tokenDetails.blockLastActiveSend =
|
|
||||||
token.tokenStats.block_last_active_send
|
|
||||||
token.tokenDetails.blockLastActiveMint =
|
|
||||||
token.tokenStats.block_last_active_mint
|
|
||||||
token.tokenDetails.txnsSinceGenesis =
|
|
||||||
token.tokenStats.qty_valid_txns_since_genesis
|
|
||||||
token.tokenDetails.validAddresses =
|
|
||||||
token.tokenStats.qty_valid_token_addresses
|
|
||||||
token.tokenDetails.mintingBatonStatus =
|
|
||||||
token.tokenStats.minting_baton_status
|
|
||||||
|
|
||||||
delete token.tokenStats.block_last_active_send
|
|
||||||
delete token.tokenStats.block_last_active_mint
|
|
||||||
delete token.tokenStats.qty_valid_txns_since_genesis
|
|
||||||
delete token.tokenStats.qty_valid_token_addresses
|
|
||||||
|
|
||||||
token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix
|
|
||||||
delete token.tokenDetails.timestamp_unix
|
|
||||||
|
|
||||||
return token.tokenDetails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Slpdb
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,606 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
const router = express.Router()
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
// const BCHJS_TESTNET = 'https://testnet.bchjs.cash/v4/'
|
|
||||||
|
|
||||||
// const bchjsHTTP = axios.create({
|
|
||||||
// baseURL: process.env.RPC_BASEURL
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const username = process.env.RPC_USERNAME
|
|
||||||
// const password = process.env.RPC_PASSWORD
|
|
||||||
|
|
||||||
// const requestConfig = {
|
|
||||||
// method: 'post',
|
|
||||||
// auth: {
|
|
||||||
// username: username,
|
|
||||||
// password: password
|
|
||||||
// },
|
|
||||||
// data: {
|
|
||||||
// jsonrpc: '1.0'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
let _this
|
|
||||||
|
|
||||||
class UtilRoute {
|
|
||||||
constructor (utilConfig) {
|
|
||||||
this.bchjs = bchjs
|
|
||||||
// this.blockbook = blockbook
|
|
||||||
|
|
||||||
if (!utilConfig) {
|
|
||||||
throw new Error(
|
|
||||||
'Must pass a config object when instantiating the Util library.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!utilConfig.electrumx) {
|
|
||||||
throw new Error(
|
|
||||||
'Must pass an instance of Electrumx when instantiating the Util library.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.electrumx = utilConfig.electrumx
|
|
||||||
|
|
||||||
this.router = router
|
|
||||||
this.router.get('/', this.root)
|
|
||||||
this.router.get('/validateAddress/:address', this.validateAddressSingle)
|
|
||||||
this.router.post('/validateAddress', this.validateAddressBulk)
|
|
||||||
this.router.post('/sweep', this.sweepWif)
|
|
||||||
|
|
||||||
_this = this
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'util' })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /util/validateAddress/{address} Get information about single bitcoin cash address.
|
|
||||||
* @apiName Information about single bitcoin cash address
|
|
||||||
* @apiGroup Util
|
|
||||||
* @apiDescription Returns information about single bitcoin cash address.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X GET "https://api.fullstack.cash/v4/util/validateAddress/bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c" -H "accept: application/json"
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async validateAddressSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const address = req.params.address
|
|
||||||
if (!address || address === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
BitboxHTTP,
|
|
||||||
// username,
|
|
||||||
// password,
|
|
||||||
requestConfig
|
|
||||||
} = routeUtils.setEnvVars()
|
|
||||||
|
|
||||||
requestConfig.data.id = 'validateaddress'
|
|
||||||
requestConfig.data.method = 'validateaddress'
|
|
||||||
requestConfig.data.params = [address]
|
|
||||||
|
|
||||||
const response = await BitboxHTTP(requestConfig)
|
|
||||||
|
|
||||||
return res.json(response.data.result)
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Error in util.ts/validateAddressSingle().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /util/validateAddress Get information about bulk bitcoin cash addresses..
|
|
||||||
* @apiName Information about bulk bitcoin cash addresses.
|
|
||||||
* @apiGroup Util
|
|
||||||
* @apiDescription Returns information about bulk bitcoin cash addresses..
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"]}'
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/util/validateAddress" -H "accept: application/json" -H "Content-Type: application/json" -d '{"addresses":["bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c","bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0"],"from": 1, "to": 5}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
async validateAddressBulk (req, res, next) {
|
|
||||||
try {
|
|
||||||
const addresses = req.body.addresses
|
|
||||||
|
|
||||||
// Reject if addresses is not an array.
|
|
||||||
if (!Array.isArray(addresses)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'addresses needs to be an array. Use GET for single address.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce array size rate limits
|
|
||||||
if (!routeUtils.validateArraySize(req, addresses)) {
|
|
||||||
res.status(400) // https://github.com/Bitcoin-com/rest.bitcoin.com/issues/330
|
|
||||||
return res.json({
|
|
||||||
error: 'Array too large.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate each element in the array.
|
|
||||||
for (let i = 0; i < addresses.length; i++) {
|
|
||||||
const address = addresses[i]
|
|
||||||
|
|
||||||
// Ensure the input is a valid BCH address.
|
|
||||||
try {
|
|
||||||
bchjs.Address.toLegacyAddress(address)
|
|
||||||
} catch (err) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: `Invalid BCH address. Double check your address is valid: ${address}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = routeUtils.validateNetwork(address)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing util/validate with these addresses: ', addresses)
|
|
||||||
|
|
||||||
const {
|
|
||||||
BitboxHTTP,
|
|
||||||
// username,
|
|
||||||
// password,
|
|
||||||
requestConfig
|
|
||||||
} = routeUtils.setEnvVars()
|
|
||||||
|
|
||||||
// Loop through each address and creates an array of requests to call in parallel
|
|
||||||
const promises = addresses.map(async (address) => {
|
|
||||||
requestConfig.data.id = 'validateaddress'
|
|
||||||
requestConfig.data.method = 'validateaddress'
|
|
||||||
requestConfig.data.params = [address]
|
|
||||||
|
|
||||||
return BitboxHTTP(requestConfig)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all parallel Insight requests to return.
|
|
||||||
const axiosResult = await axios.all(promises)
|
|
||||||
|
|
||||||
// Retrieve the data part of the result.
|
|
||||||
const result = axiosResult.map((x) => x.data.result)
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(result)
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Error in util.ts/validateAddressSingle().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /util/sweep Sweep BCH and tokens
|
|
||||||
* @apiName Sweep BCH and tokens from a paper wallet
|
|
||||||
* @apiGroup Util
|
|
||||||
* @apiDescription This function can be used to check the BCH balance of a
|
|
||||||
* paper wallet. It can also be used to sweep BCH and tokens from a paper
|
|
||||||
* wallet and send them to a destination address.
|
|
||||||
*
|
|
||||||
* Note: It does not yet support multiple token classes on the same paper wallet.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @apiExample Example usage:
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "balanceOnly": true}'
|
|
||||||
* curl -X POST "https://api.fullstack.cash/v4/util/sweep" -H "accept: application/json" -H "Content-Type: application/json" -d '{"wif":"Kz52sdXLAiKtH82iAFRi6aTZanWtW5Eyv37KWdpY6tTv7pjoHste", "toAddr": "bitcoincash:qpt8m4kqu963geedyrur6pdggqmv5kxwnq0rn322qu"}'
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
async sweepWif (req, res, next) {
|
|
||||||
try {
|
|
||||||
// Validate input
|
|
||||||
const wif = req.body.wif
|
|
||||||
const toAddr = req.body.toAddr
|
|
||||||
const balanceOnly = req.body.balanceOnly
|
|
||||||
|
|
||||||
if (typeof wif !== 'string' || wif.length !== 52) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'WIF needs to a proper compressed WIF starting with K or L'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!balanceOnly) {
|
|
||||||
// Only throw error if balanceOnly is false or undefined.
|
|
||||||
if (!toAddr || toAddr === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'address can not be empty' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing util/sweepWif with this address: ', toAddr)
|
|
||||||
|
|
||||||
// Generate a private and public key pair from the WIF.
|
|
||||||
const ecPair = bchjs.ECPair.fromWIF(wif)
|
|
||||||
const fromAddr = bchjs.ECPair.toCashAddress(ecPair)
|
|
||||||
|
|
||||||
// Get a balance on the public address
|
|
||||||
const balances = await _this.electrumx._balanceFromElectrumx(fromAddr)
|
|
||||||
// console.log(`balances: ${JSON.stringify(balances, null, 2)}`)
|
|
||||||
|
|
||||||
// Total balance is the sum of the confirmed and unconfirmed balance.
|
|
||||||
const totalBalance = balances.confirmed + balances.unconfirmed
|
|
||||||
|
|
||||||
// Exit if balance is zero.
|
|
||||||
if (isNaN(totalBalance) || totalBalance === 0) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({ error: 'No balance found at BCH address.' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit if this is a balance-only call.
|
|
||||||
if (balanceOnly) {
|
|
||||||
res.status(200)
|
|
||||||
return res.json(totalBalance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all UTXOs help by the address.
|
|
||||||
const utxos = await _this.electrumx._utxosFromElectrumx(fromAddr)
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
const tokenUtxos = []
|
|
||||||
const bchUtxos = []
|
|
||||||
|
|
||||||
// Exit if there are no UTXOs.
|
|
||||||
if (utxos.length === 0) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({ error: 'No utxos found.' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out which UTXOs are associated with SLP tokens.
|
|
||||||
const isTokenUtxo = await _this.bchjs.SLP.Utils.tokenUtxoDetails(utxos)
|
|
||||||
// console.log(`isTokenUtxo: ${JSON.stringify(isTokenUtxo, null, 2)}`)
|
|
||||||
|
|
||||||
// Separate the bch and token UTXOs.
|
|
||||||
for (let i = 0; i < utxos.length; i++) {
|
|
||||||
// Filter based on isTokenUtxo.
|
|
||||||
if (!isTokenUtxo[i]) bchUtxos.push(utxos[i])
|
|
||||||
else tokenUtxos.push(isTokenUtxo[i])
|
|
||||||
}
|
|
||||||
// console.log(
|
|
||||||
// `bchUtxos.length: ${bchUtxos.length}, tokenUtxos.length: ${tokenUtxos.length}`
|
|
||||||
// )
|
|
||||||
|
|
||||||
// Throw error if no BCH to move tokens.
|
|
||||||
if (bchUtxos.length === 0 && tokenUtxos.length > 0) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({
|
|
||||||
error:
|
|
||||||
'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
ecPair,
|
|
||||||
utxos,
|
|
||||||
fromAddr,
|
|
||||||
toAddr,
|
|
||||||
bchUtxos,
|
|
||||||
tokenUtxos
|
|
||||||
}
|
|
||||||
|
|
||||||
let hex
|
|
||||||
|
|
||||||
// Choose the sweeping algorithm, based on if there are tokens or not.
|
|
||||||
if (tokenUtxos.length === 0) hex = await _this._sweepBCH(options)
|
|
||||||
else hex = await _this._sweepTokens(options, bchUtxos, tokenUtxos)
|
|
||||||
// console.log(`hex: ${hex}`)
|
|
||||||
|
|
||||||
// Throw error if there is more than one token class.
|
|
||||||
|
|
||||||
// Generate a transaction to move tokens and BCH.
|
|
||||||
|
|
||||||
// Broadcast the transaction.
|
|
||||||
const txid = _this.bchjs.RawTransactions.sendRawTransaction([hex])
|
|
||||||
|
|
||||||
res.status(200)
|
|
||||||
return res.json(txid)
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catch the specific case of multiple tokens.
|
|
||||||
if (
|
|
||||||
err.message &&
|
|
||||||
err.message.indexOf('Multiple token classes detected') > -1
|
|
||||||
) {
|
|
||||||
res.status(422)
|
|
||||||
return res.json({ error: err.message })
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.error('Error in util.js/sweepWif().', err)
|
|
||||||
console.error('Error in util.js/sweepWif().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: err.message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sweep BCH only from a private WIF.
|
|
||||||
async _sweepBCH (options) {
|
|
||||||
try {
|
|
||||||
// const wif = flags.wif
|
|
||||||
// const toAddr = flags.address
|
|
||||||
|
|
||||||
const ecPair = options.ecPair
|
|
||||||
const toAddr = options.toAddr
|
|
||||||
|
|
||||||
// const fromAddr = this.BITBOX.ECPair.toCashAddress(ecPair)
|
|
||||||
//
|
|
||||||
// // Get the UTXOs for that address.
|
|
||||||
// let utxos = await this.BITBOX.Blockbook.utxo(fromAddr)
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
let utxos = options.utxos
|
|
||||||
|
|
||||||
// Ensure all utxos have the satoshis property.
|
|
||||||
utxos = utxos.map((x) => {
|
|
||||||
x.satoshis = Number(x.value)
|
|
||||||
return x
|
|
||||||
})
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
// instance of transaction builder
|
|
||||||
let transactionBuilder
|
|
||||||
if (options.testnet) {
|
|
||||||
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
|
|
||||||
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
|
|
||||||
|
|
||||||
let originalAmount = 0
|
|
||||||
|
|
||||||
// Loop through all UTXOs.
|
|
||||||
for (let i = 0; i < utxos.length; i++) {
|
|
||||||
const utxo = utxos[i]
|
|
||||||
|
|
||||||
originalAmount = originalAmount + utxo.value
|
|
||||||
|
|
||||||
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalAmount < 546) {
|
|
||||||
throw new Error(
|
|
||||||
'Original amount less than the dust limit. Not enough BCH to send.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get byte count to calculate fee. paying 1 sat/byte
|
|
||||||
const byteCount = _this.bchjs.BitcoinCash.getByteCount(
|
|
||||||
{ P2PKH: utxos.length },
|
|
||||||
{ P2PKH: 1 }
|
|
||||||
)
|
|
||||||
const fee = Math.ceil(1.1 * byteCount)
|
|
||||||
|
|
||||||
// amount to send to receiver. It's the original amount - 1 sat/byte for tx size
|
|
||||||
const sendAmount = originalAmount - fee
|
|
||||||
|
|
||||||
// add output w/ address and amount to send
|
|
||||||
transactionBuilder.addOutput(
|
|
||||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
|
||||||
sendAmount
|
|
||||||
)
|
|
||||||
|
|
||||||
// Loop through each input and sign
|
|
||||||
let redeemScript
|
|
||||||
for (var i = 0; i < utxos.length; i++) {
|
|
||||||
const utxo = utxos[i]
|
|
||||||
|
|
||||||
transactionBuilder.sign(
|
|
||||||
i,
|
|
||||||
ecPair,
|
|
||||||
redeemScript,
|
|
||||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
|
||||||
utxo.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build tx
|
|
||||||
const tx = transactionBuilder.build()
|
|
||||||
|
|
||||||
// output rawhex
|
|
||||||
const hex = tx.toHex()
|
|
||||||
return hex
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in util.js/sweepBCH().')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sweep BCH and tokens from a WIF.
|
|
||||||
async _sweepTokens (options) {
|
|
||||||
try {
|
|
||||||
// const { ecPair, utxos, fromAddr, toAddr, bchUtxos, tokenUtxos } = options
|
|
||||||
const { ecPair, utxos, toAddr, bchUtxos, tokenUtxos } = options
|
|
||||||
|
|
||||||
// Input validation
|
|
||||||
if (!Array.isArray(bchUtxos) || bchUtxos.length === 0) {
|
|
||||||
throw new Error('bchUtxos need to be an array with one UTXO.')
|
|
||||||
}
|
|
||||||
if (!Array.isArray(tokenUtxos) || tokenUtxos.length === 0) {
|
|
||||||
throw new Error('tokenUtxos need to be an array with one UTXO.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (flags.testnet)
|
|
||||||
// this.BITBOX = new config.BCHLIB({ restURL: config.TESTNET_REST })
|
|
||||||
|
|
||||||
// console.log(`tokenUtxos: ${JSON.stringify(tokenUtxos, null, 2)}`)
|
|
||||||
|
|
||||||
// Ensure there is only one class of token in the wallet. Throw an error if
|
|
||||||
// there is more than one.
|
|
||||||
const tokenId = tokenUtxos[0].tokenId
|
|
||||||
const otherTokens = tokenUtxos.filter((x) => x.tokenId !== tokenId)
|
|
||||||
if (otherTokens.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
'Multiple token classes detected. This function only supports a single class of token.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// instance of transaction builder
|
|
||||||
let transactionBuilder
|
|
||||||
if (options.testnet) {
|
|
||||||
transactionBuilder = new _this.bchjs.TransactionBuilder('testnet')
|
|
||||||
} else transactionBuilder = new _this.bchjs.TransactionBuilder()
|
|
||||||
|
|
||||||
// Combine all the UTXOs into a single array.
|
|
||||||
const allUtxos = utxos
|
|
||||||
// console.log(`allUtxos: ${JSON.stringify(allUtxos, null, 2)}`)
|
|
||||||
|
|
||||||
// Loop through all UTXOs.
|
|
||||||
let originalAmount = 0
|
|
||||||
for (let i = 0; i < allUtxos.length; i++) {
|
|
||||||
const utxo = allUtxos[i]
|
|
||||||
|
|
||||||
originalAmount = originalAmount + utxo.value
|
|
||||||
|
|
||||||
transactionBuilder.addInput(utxo.tx_hash, utxo.tx_pos)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalAmount < 300) {
|
|
||||||
throw new Error(
|
|
||||||
'Not enough BCH to send. Send more BCH to the wallet to pay miner fees.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get byte count to calculate fee. paying 1 sat
|
|
||||||
// Note: This may not be totally accurate. Just guessing on the byteCount size.
|
|
||||||
// const byteCount = this.BITBOX.BitcoinCash.getByteCount(
|
|
||||||
// { P2PKH: 3 },
|
|
||||||
// { P2PKH: 5 }
|
|
||||||
// )
|
|
||||||
// //console.log(`byteCount: ${byteCount}`)
|
|
||||||
// const satoshisPerByte = 1.1
|
|
||||||
// const txFee = Math.floor(satoshisPerByte * byteCount)
|
|
||||||
// console.log(`txFee: ${txFee} satoshis\n`)
|
|
||||||
const txFee = 500
|
|
||||||
|
|
||||||
// amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size
|
|
||||||
const remainder = originalAmount - txFee - 546
|
|
||||||
if (remainder < 1) {
|
|
||||||
throw new Error('Selected UTXO does not have enough satoshis')
|
|
||||||
}
|
|
||||||
// console.log(`remainder: ${remainder}`)
|
|
||||||
|
|
||||||
// Tally up the quantity of tokens
|
|
||||||
let tokenQty = 0
|
|
||||||
for (let i = 0; i < tokenUtxos.length; i++) {
|
|
||||||
tokenQty += tokenUtxos[i].tokenQty
|
|
||||||
}
|
|
||||||
// console.log(`tokenQty: ${tokenQty}`)
|
|
||||||
|
|
||||||
// Generate the OP_RETURN entry for an SLP SEND transaction.
|
|
||||||
// console.log(`Generating op-return.`)
|
|
||||||
const {
|
|
||||||
script,
|
|
||||||
outputs
|
|
||||||
} = _this.bchjs.SLP.TokenType1.generateSendOpReturn(tokenUtxos, tokenQty)
|
|
||||||
// console.log(`token outputs: ${outputs}`)
|
|
||||||
|
|
||||||
// Since we are sweeping all tokens from the WIF, there generateOpReturn()
|
|
||||||
// function should only compute 1 token output. If it returns 2, then there
|
|
||||||
// is something unexpected happening.
|
|
||||||
if (outputs > 1) {
|
|
||||||
throw new Error(
|
|
||||||
'More than one class of token detected. Sweep feature not supported.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add OP_RETURN as first output.
|
|
||||||
const data = _this.bchjs.Script.encode(script)
|
|
||||||
transactionBuilder.addOutput(data, 0)
|
|
||||||
|
|
||||||
// Send dust transaction representing tokens being sent.
|
|
||||||
transactionBuilder.addOutput(
|
|
||||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
|
||||||
546
|
|
||||||
)
|
|
||||||
|
|
||||||
// Last output: send remaining BCH
|
|
||||||
transactionBuilder.addOutput(
|
|
||||||
_this.bchjs.Address.toLegacyAddress(toAddr),
|
|
||||||
remainder
|
|
||||||
)
|
|
||||||
// console.log(`utxo: ${JSON.stringify(utxo, null, 2)}`)
|
|
||||||
|
|
||||||
// Sign each UTXO being consumed.
|
|
||||||
let redeemScript
|
|
||||||
for (let i = 0; i < allUtxos.length; i++) {
|
|
||||||
const thisUtxo = allUtxos[i]
|
|
||||||
// console.log(`thisUtxo: ${JSON.stringify(thisUtxo, null, 2)}`)
|
|
||||||
|
|
||||||
transactionBuilder.sign(
|
|
||||||
i,
|
|
||||||
ecPair,
|
|
||||||
redeemScript,
|
|
||||||
transactionBuilder.hashTypes.SIGHASH_ALL,
|
|
||||||
thisUtxo.value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// build tx
|
|
||||||
const tx = transactionBuilder.build()
|
|
||||||
|
|
||||||
// output rawhex
|
|
||||||
const hex = tx.toHex()
|
|
||||||
// console.log(`Transaction raw hex: `)
|
|
||||||
// console.log(hex)
|
|
||||||
|
|
||||||
return hex
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in util.js/sweepBCH().')
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = UtilRoute
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
xpub route
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
|
|
||||||
const RouteUtils = require('../../util/route-utils')
|
|
||||||
const routeUtils = new RouteUtils()
|
|
||||||
|
|
||||||
const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
// const router = express.Router()
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
// Used for processing error messages before sending them to the user.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
// Connect the route endpoints to their handler functions.
|
|
||||||
router.get('/', root)
|
|
||||||
router.get('/fromXPub/:xpub', fromXPubSingle)
|
|
||||||
|
|
||||||
// Root API endpoint. Simply acknowledges that it exists.
|
|
||||||
function root (req, res, next) {
|
|
||||||
return res.json({ status: 'address' })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fromXPubSingle (req, res, next) {
|
|
||||||
try {
|
|
||||||
const xpub = req.params.xpub
|
|
||||||
const hdPath = req.query.hdPath ? req.query.hdPath : '0'
|
|
||||||
|
|
||||||
if (!xpub || xpub === '') {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({ error: 'xpub can not be empty' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject if xpub is an array.
|
|
||||||
if (Array.isArray(xpub)) {
|
|
||||||
res.status(400)
|
|
||||||
return res.json({
|
|
||||||
error: 'xpub can not be an array. Use POST for bulk upload.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.debug('Executing address/fromXPub with this xpub: ', xpub)
|
|
||||||
|
|
||||||
const cashAddr = bchjs.Address.fromXPub(xpub, hdPath)
|
|
||||||
const legacyAddr = bchjs.Address.toLegacyAddress(cashAddr)
|
|
||||||
res.status(200)
|
|
||||||
return res.json({
|
|
||||||
cashAddress: cashAddr,
|
|
||||||
legacyAddress: legacyAddr
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
// Attempt to decode the error message.
|
|
||||||
const { msg, status } = routeUtils.decodeError(err)
|
|
||||||
if (msg) {
|
|
||||||
res.status(status)
|
|
||||||
return res.json({ error: msg })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write out error to error log.
|
|
||||||
wlogger.error('Error in address.ts/fromXPubSingle().', err)
|
|
||||||
|
|
||||||
res.status(500)
|
|
||||||
return res.json({ error: util.inspect(err) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
router,
|
|
||||||
testableComponents: {
|
|
||||||
root,
|
|
||||||
fromXPubSingle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
bcash node routes for working with SLP tokens.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
const axios = require('axios')
|
||||||
|
const util = require('util')
|
||||||
|
// const bitcore = require('bitcore-lib-cash')
|
||||||
|
|
||||||
|
// const ElectrumCash = require('electrum-cash').ElectrumClient
|
||||||
|
// const ElectrumCash = require('/home/trout/work/personal/electrum-cash/electrum.js').Client // eslint-disable-line
|
||||||
|
|
||||||
|
const wlogger = require('../../../util/winston-logging')
|
||||||
|
const config = require('../../../../config')
|
||||||
|
|
||||||
|
const RouteUtils = require('../../../util/route-utils')
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
|
||||||
|
const BCHJS = require('@psf/bch-js')
|
||||||
|
const bchjs = new BCHJS()
|
||||||
|
|
||||||
|
let _this
|
||||||
|
|
||||||
|
class BcashSlp {
|
||||||
|
constructor () {
|
||||||
|
_this = this
|
||||||
|
this.config = config
|
||||||
|
this.axios = axios
|
||||||
|
this.routeUtils = routeUtils
|
||||||
|
this.bchjs = bchjs
|
||||||
|
// _this.bitcore = bitcore
|
||||||
|
|
||||||
|
this.router = router
|
||||||
|
this.router.get('/', this.root)
|
||||||
|
this.router.get('/utxos/:address', this.getUtxos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /bcash/utxos/{addr} Get utxos for a single address.
|
||||||
|
* @apiName UTXOs for a single address
|
||||||
|
* @apiGroup bcash
|
||||||
|
* @apiDescription Returns an object with UTXOs associated with an address.
|
||||||
|
* This UTXOs will be hydrated with token information.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -X GET "https://api.fullstack.cash/v5/bcash/utxos/bitcoincash:qr69kyzha07dcecrsvjwsj4s6slnlq4r8c30lxnur3" -H "accept: application/json"
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
// GET handler for single balance
|
||||||
|
async getUtxos (req, res, next) {
|
||||||
|
try {
|
||||||
|
const address = req.params.address
|
||||||
|
|
||||||
|
this.bcashServer = process.env.BCASH_SERVER
|
||||||
|
if (!this.bcashServer) {
|
||||||
|
// console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
|
||||||
|
throw new Error(
|
||||||
|
'BCASH_SERVER env var not set. Can not connect to bcash full node.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject if address is an array.
|
||||||
|
if (Array.isArray(address)) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'address can not be an array. Use POST for bulk upload.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cashAddr = _this.bchjs.Address.toCashAddress(address)
|
||||||
|
|
||||||
|
// Prevent a common user error. Ensure they are using the correct network address.
|
||||||
|
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
|
||||||
|
if (!networkIsValid) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Customize this function below here for bcash, based on this gist:
|
||||||
|
// https://gist.github.com/christroutner/dcb25a895900e557d7b43341ee85fa79
|
||||||
|
|
||||||
|
wlogger.debug(
|
||||||
|
'Executing bcash/slp.js/getUtxos(). with this address: ',
|
||||||
|
cashAddr
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get data from ElectrumX server.
|
||||||
|
const response = await _this.axios.get(
|
||||||
|
`${_this.bcashServer}coin/address/${cashAddr}?slp=true`
|
||||||
|
)
|
||||||
|
// Get address UTXOs
|
||||||
|
const utxos = response.data
|
||||||
|
// Hydrate UTXOs
|
||||||
|
const hydratedUtxos = await _this.hydrateUTXOS(utxos)
|
||||||
|
res.status(200)
|
||||||
|
return res.json(hydratedUtxos)
|
||||||
|
} catch (err) {
|
||||||
|
// Write out error to error log.
|
||||||
|
wlogger.error('Error in bcash/slp.js/getUtxos().', err)
|
||||||
|
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps and filter the SLP UTXOs of an UTXOs array
|
||||||
|
// get information about the SLP UTXOs
|
||||||
|
async hydrateUTXOS (UTXOS) {
|
||||||
|
try {
|
||||||
|
if (!Array.isArray(UTXOS)) {
|
||||||
|
throw new Error('UTXOs must be an array of slp utxos')
|
||||||
|
}
|
||||||
|
const slpUtxos = UTXOS.filter((val) => val.slp)
|
||||||
|
const hydrated = []
|
||||||
|
|
||||||
|
// Find information of each token
|
||||||
|
for (let i = 0; i < slpUtxos.length; i++) {
|
||||||
|
const slp = slpUtxos[i].slp
|
||||||
|
const info = await _this.getTokenInfo(slp.tokenId)
|
||||||
|
const obj = Object.assign(slp, info)
|
||||||
|
|
||||||
|
slpUtxos[i].slp = obj
|
||||||
|
hydrated.push(slpUtxos[i])
|
||||||
|
}
|
||||||
|
return hydrated
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get information of a token
|
||||||
|
async getTokenInfo (tokenId) {
|
||||||
|
try {
|
||||||
|
if (!tokenId || typeof tokenId !== 'string') {
|
||||||
|
throw new Error('tokenId must be string')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bcashServer = process.env.BCASH_SERVER
|
||||||
|
if (!this.bcashServer) {
|
||||||
|
// console.warn('FULCRUM_API env var not set. Can not connect to Fulcrum indexer.')
|
||||||
|
throw new Error(
|
||||||
|
'BCASH_SERVER env var not set. Can not connect to bcash full node.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await _this.axios.get(
|
||||||
|
`${_this.bcashServer}token/${tokenId}`
|
||||||
|
)
|
||||||
|
const tokenInfo = result.data
|
||||||
|
// console.log('token info', tokenInfo)
|
||||||
|
return tokenInfo
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler.
|
||||||
|
errorHandler (err, res) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
// console.log('errorHandler msg: ', msg)
|
||||||
|
// console.log('errorHandler status: ', status)
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ success: false, error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error patterns specific to this route.
|
||||||
|
if (err.message) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ success: false, error: err.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If error can be handled, return the stack trace
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root API endpoint. Simply acknowledges that it exists.
|
||||||
|
root (req, res, next) {
|
||||||
|
return res.json({ status: 'bcash-slp' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BcashSlp
|
||||||
@@ -96,25 +96,39 @@ class Electrum {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the address is in cash address format.
|
if (!address) {
|
||||||
const cashAddr = _this.bchjs.Address.toCashAddress(address)
|
|
||||||
|
|
||||||
// Prevent a common user error. Ensure they are using the correct network address.
|
|
||||||
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
|
|
||||||
if (!networkIsValid) {
|
|
||||||
res.status(400)
|
res.status(400)
|
||||||
return res.json({
|
return res.json({
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: 'address is empty'
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cashAddr = address
|
||||||
|
|
||||||
|
if (!address.includes('ecash')) {
|
||||||
|
// Ensure the address is in cash address format.
|
||||||
|
cashAddr = _this.bchjs.Address.toCashAddress(address)
|
||||||
|
|
||||||
|
// Prevent a common user error. Ensure they are using the correct network address.
|
||||||
|
const networkIsValid = _this.routeUtils.validateNetwork(cashAddr)
|
||||||
|
if (!networkIsValid) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wlogger.debug(
|
wlogger.debug(
|
||||||
'Executing electrumx/getBalance with this address: ',
|
'Executing electrumx/getBalance with this address: ',
|
||||||
cashAddr
|
cashAddr
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// console.log('req.locals: ', req.locals)
|
||||||
|
|
||||||
const response = await _this.axios.get(
|
const response = await _this.axios.get(
|
||||||
`${_this.fulcrumApi}electrumx/balance/${address}`
|
`${_this.fulcrumApi}electrumx/balance/${address}`
|
||||||
)
|
)
|
||||||
@@ -195,9 +209,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(
|
const response = await _this.axios.post(
|
||||||
`${_this.fulcrumApi}electrumx/balance/`,
|
`${_this.fulcrumApi}electrumx/balance/`,
|
||||||
{ addresses }
|
{ addresses, useCache }
|
||||||
)
|
)
|
||||||
|
|
||||||
res.status(200)
|
res.status(200)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
A library for interacting with the Bitcoin.com ninsight (not Insight) indexer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const express = require('express')
|
|
||||||
// const axios = require('axios')
|
|
||||||
// const routeUtils = require('./route-utils')
|
|
||||||
// const wlogger = require('../../util/winston-logging')
|
|
||||||
|
|
||||||
const router = express.Router()
|
|
||||||
|
|
||||||
// const BCHJS = require('@psf/bch-js')
|
|
||||||
// const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
// let _this
|
|
||||||
|
|
||||||
class Ninsight {
|
|
||||||
constructor () {
|
|
||||||
// _this = this
|
|
||||||
|
|
||||||
this.router = router
|
|
||||||
this.router.get('/', this.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
root (req, res, next) {
|
|
||||||
return res.json({ status: 'ninsight' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Ninsight
|
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
/*
|
||||||
|
Routes for interacting with the psf-slp-indexer
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Public npm libraries
|
||||||
|
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 config = require('../../../config')
|
||||||
|
|
||||||
|
const routeUtils = new RouteUtils()
|
||||||
|
const bchjs = new BCHJS({ restURL: config.restURL })
|
||||||
|
|
||||||
|
let _this
|
||||||
|
|
||||||
|
class PsfSlpIndexer {
|
||||||
|
constructor () {
|
||||||
|
// Encapsulate dependencies
|
||||||
|
this.axios = axios
|
||||||
|
this.router = router
|
||||||
|
this.routeUtils = routeUtils
|
||||||
|
this.config = config
|
||||||
|
this.bchjs = bchjs
|
||||||
|
// Define routes
|
||||||
|
this.router.get('/', this.root)
|
||||||
|
this.router.get('/status', this.getStatus)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root API endpoint. Simply acknowledges that it exists.
|
||||||
|
root (req, res, next) {
|
||||||
|
return res.json({ status: 'psf-slp-indexer' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /psf/slp/status/ Indexer Status.
|
||||||
|
* @apiName SLP indexer status.
|
||||||
|
* @apiGroup PSF SLP
|
||||||
|
* @apiDescription Return SLP indexer status
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X GET localhost:3000/v5/psf/slp/status
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getStatus (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Verify env var is set for interacting with the indexer.
|
||||||
|
_this.checkEnvVar()
|
||||||
|
|
||||||
|
const response = await _this.axios.get(
|
||||||
|
`${_this.psfSlpIndexerApi}slp/status/`
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
return res.json(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /psf/slp/address/ SLP balance for address.
|
||||||
|
* @apiName SLP balance for address.
|
||||||
|
* @apiGroup PSF SLP
|
||||||
|
* @apiDescription Return SLP balance for address
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{ "address": "bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n" }' localhost:3000/v5/psf/slp/address
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getAddress (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Verify env var is set for interacting with the indexer.
|
||||||
|
_this.checkEnvVar()
|
||||||
|
|
||||||
|
// Validate the input data.
|
||||||
|
const address = req.body.address
|
||||||
|
if (!address || address === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'address can not be empty'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await _this.axios.post(
|
||||||
|
`${_this.psfSlpIndexerApi}slp/address/`,
|
||||||
|
{ address }
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
return res.json(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('err', err)
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /psf/slp/txid/ SLP transaction data.
|
||||||
|
* @apiName SLP transaction data.
|
||||||
|
* @apiGroup PSF SLP
|
||||||
|
* @apiDescription Return slp transaction data.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{ "txid": "f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315" }' localhost:3000/v5/psf/slp/txid
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getTxid (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Verify env var is set for interacting with the indexer.
|
||||||
|
_this.checkEnvVar()
|
||||||
|
|
||||||
|
const txid = req.body.txid
|
||||||
|
if (!txid || txid === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'txid can not be empty'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (txid.length !== 64) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'This is not a txid'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await _this.axios.post(
|
||||||
|
`${_this.psfSlpIndexerApi}slp/tx/`,
|
||||||
|
{ txid }
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
return res.json(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
// console.log('err', err)
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /psf/slp/token List stats for a single slp token.
|
||||||
|
* @apiName List stats for a single slp token.
|
||||||
|
* @apiGroup PSF SLP
|
||||||
|
* @apiDescription Return list stats for a single slp token.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @apiExample Example usage:
|
||||||
|
* curl -H "Content-Type: application/json" -X POST -d '{ "tokenId": "a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2" }' localhost:3000/v5/psf/slp/token
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async getTokenStats (req, res, next) {
|
||||||
|
try {
|
||||||
|
// Verify env var is set for interacting with the indexer.
|
||||||
|
_this.checkEnvVar()
|
||||||
|
|
||||||
|
const tokenId = req.body.tokenId
|
||||||
|
if (!tokenId || tokenId === '') {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'tokenId can not be empty'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag to toggle tx history of the token.
|
||||||
|
let withTxHistory = false
|
||||||
|
if (req.body.withTxHistory) withTxHistory = true
|
||||||
|
|
||||||
|
const response = await _this.axios.post(
|
||||||
|
`${_this.psfSlpIndexerApi}slp/token/`,
|
||||||
|
{ tokenId, withTxHistory }
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200)
|
||||||
|
return res.json(response.data)
|
||||||
|
} catch (err) {
|
||||||
|
return _this.errorHandler(err, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
if (!_this.psfSlpIndexerApi) {
|
||||||
|
throw new Error(
|
||||||
|
'SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRY error handler.
|
||||||
|
errorHandler (err, res) {
|
||||||
|
// Attempt to decode the error message.
|
||||||
|
const { msg, status } = _this.routeUtils.decodeError(err)
|
||||||
|
// console.log('errorHandler msg: ', msg)
|
||||||
|
// console.log('errorHandler status: ', status)
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
|
res.status(status)
|
||||||
|
return res.json({ success: false, error: msg })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle error patterns specific to this route.
|
||||||
|
if (err.message) {
|
||||||
|
res.status(400)
|
||||||
|
return res.json({ success: false, error: err.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
// If error can be handled, return the stack trace
|
||||||
|
res.status(500)
|
||||||
|
return res.json({ error: util.inspect(err) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PsfSlpIndexer
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
const SLPSDK = require('@psf/bch-js')
|
|
||||||
const SLP = new SLPSDK()
|
|
||||||
|
|
||||||
class Slpdb {
|
|
||||||
// Gets transaction history for all tokens for an address. Can also specify
|
|
||||||
// block height, but defaults to 0.
|
|
||||||
async getHistoricalSlpTransactions (addressList, fromBlock = 0) {
|
|
||||||
// Build SLPDB or query from addressList
|
|
||||||
const orQueryArray = []
|
|
||||||
for (const address of addressList) {
|
|
||||||
const cashAddress = SLP.SLP.Address.toCashAddress(address)
|
|
||||||
const slpAddress = SLP.SLP.Address.toSLPAddress(address)
|
|
||||||
|
|
||||||
const cashQuery = {
|
|
||||||
'in.e.a': cashAddress.slice(12)
|
|
||||||
}
|
|
||||||
const slpQuery = {
|
|
||||||
'slp.detail.outputs.address': slpAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
orQueryArray.push(cashQuery)
|
|
||||||
orQueryArray.push(slpQuery)
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
find: {
|
|
||||||
db: ['c', 'u'],
|
|
||||||
$query: {
|
|
||||||
$or: orQueryArray,
|
|
||||||
'slp.valid': true,
|
|
||||||
'blk.i': {
|
|
||||||
$not: {
|
|
||||||
$lte: fromBlock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
$orderby: {
|
|
||||||
'blk.i': -1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
_id: 0,
|
|
||||||
'tx.h': 1,
|
|
||||||
'in.i': 1,
|
|
||||||
'in.e': 1,
|
|
||||||
'out.e': 1,
|
|
||||||
'out.a': 1,
|
|
||||||
'slp.detail': 1,
|
|
||||||
blk: 1
|
|
||||||
},
|
|
||||||
limit: 500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
|
|
||||||
|
|
||||||
let transactions = []
|
|
||||||
|
|
||||||
// Add confirmed transactions
|
|
||||||
if (result.data && result.data.c) {
|
|
||||||
transactions = transactions.concat(result.data.c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add unconfirmed transactions
|
|
||||||
if (result.data && result.data.u) {
|
|
||||||
transactions = transactions.concat(result.data.u)
|
|
||||||
}
|
|
||||||
|
|
||||||
return transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTokenStats (tokenId) {
|
|
||||||
const [
|
|
||||||
totalMinted,
|
|
||||||
totalBurned,
|
|
||||||
tokenDetails,
|
|
||||||
circulatingSupply
|
|
||||||
] = await Promise.all([
|
|
||||||
this.getTotalMinted(tokenId),
|
|
||||||
this.getTotalBurned(tokenId),
|
|
||||||
this.getTokenDetails(tokenId),
|
|
||||||
this.getTotalCirculating(tokenId)
|
|
||||||
])
|
|
||||||
|
|
||||||
tokenDetails.totalMinted = tokenDetails.initialTokenQty + totalMinted
|
|
||||||
tokenDetails.totalBurned = totalBurned
|
|
||||||
|
|
||||||
// tokenDetails.circulatingSupply =
|
|
||||||
// tokenDetails.totalMinted - tokenDetails.totalBurned
|
|
||||||
tokenDetails.circulatingSupply = circulatingSupply
|
|
||||||
|
|
||||||
return tokenDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
generateCredentials () {
|
|
||||||
// Generate the Basic Authentication header for a private instance of SLPDB.
|
|
||||||
const SLPDB_PASS = process.env.SLPDB_PASS
|
|
||||||
? process.env.SLPDB_PASS
|
|
||||||
: 'BITBOX'
|
|
||||||
const username = 'BITBOX'
|
|
||||||
const password = SLPDB_PASS
|
|
||||||
const combined = `${username}:${password}`
|
|
||||||
var base64Credential = Buffer.from(combined).toString('base64')
|
|
||||||
var readyCredential = `Basic ${base64Credential}`
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
authorization: readyCredential,
|
|
||||||
timeout: 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
async runQuery (query) {
|
|
||||||
const queryString = JSON.stringify(query)
|
|
||||||
const queryBase64 = Buffer.from(queryString).toString('base64')
|
|
||||||
const url = `${process.env.SLPDB_URL}q/${queryBase64}`
|
|
||||||
|
|
||||||
const options = this.generateCredentials()
|
|
||||||
|
|
||||||
const response = await axios.get(url, options)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTotalMinted (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['g'],
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId,
|
|
||||||
'graphTxn.outputs.status': {
|
|
||||||
$in: [
|
|
||||||
'BATON_SPENT_IN_MINT',
|
|
||||||
'BATON_UNSPENT',
|
|
||||||
'BATON_SPENT_NOT_IN_MINT'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$unwind: '$graphTxn.outputs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: null,
|
|
||||||
count: {
|
|
||||||
$sum: '$graphTxn.outputs.slpAmount'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
|
|
||||||
if (!result.data.g.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(result.data.g[0].count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTotalCirculating (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['g'],
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId,
|
|
||||||
'graphTxn.outputs': {
|
|
||||||
$elemMatch: {
|
|
||||||
status: 'UNSPENT',
|
|
||||||
slpAmount: { $gte: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $unwind: '$graphTxn.outputs' },
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'graphTxn.outputs.status': 'UNSPENT',
|
|
||||||
'graphTxn.outputs.slpAmount': { $gte: 0 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: null,
|
|
||||||
circulating_supply: {
|
|
||||||
$sum: '$graphTxn.outputs.slpAmount'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
limit: 100000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
// console.log(`result.data: ${JSON.stringify(result.data, null, 2)}`)
|
|
||||||
|
|
||||||
if (!result.data.g.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(result.data.g[0].circulating_supply)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTotalBurned (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['g'],
|
|
||||||
aggregate: [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId,
|
|
||||||
'graphTxn.outputs.status': {
|
|
||||||
$in: [
|
|
||||||
'SPENT_NON_SLP',
|
|
||||||
'BATON_SPENT_INVALID_SLP',
|
|
||||||
'SPENT_INVALID_SLP',
|
|
||||||
'BATON_SPENT_NON_SLP',
|
|
||||||
'MISSING_BCH_VOUT',
|
|
||||||
'BATON_MISSING_BCH_VOUT',
|
|
||||||
'BATON_SPENT_NOT_IN_MINT',
|
|
||||||
'EXCESS_INPUT_BURNED'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$unwind: '$graphTxn.outputs'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
'graphTxn.outputs.status': {
|
|
||||||
$in: [
|
|
||||||
'SPENT_NON_SLP',
|
|
||||||
'BATON_SPENT_INVALID_SLP',
|
|
||||||
'SPENT_INVALID_SLP',
|
|
||||||
'BATON_SPENT_NON_SLP',
|
|
||||||
'MISSING_BCH_VOUT',
|
|
||||||
'BATON_MISSING_BCH_VOUT',
|
|
||||||
'BATON_SPENT_NOT_IN_MINT',
|
|
||||||
'EXCESS_INPUT_BURNED'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: null,
|
|
||||||
count: {
|
|
||||||
$sum: '$graphTxn.outputs.slpAmount'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
|
|
||||||
if (!result.data.g.length) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseFloat(result.data.g[0].count)
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTokenDetails (tokenId) {
|
|
||||||
const query = {
|
|
||||||
v: 3,
|
|
||||||
q: {
|
|
||||||
db: ['t'],
|
|
||||||
find: {
|
|
||||||
$query: {
|
|
||||||
'tokenDetails.tokenIdHex': tokenId
|
|
||||||
}
|
|
||||||
},
|
|
||||||
project: { tokenDetails: 1, tokenStats: 1, _id: 0 },
|
|
||||||
limit: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.runQuery(query)
|
|
||||||
|
|
||||||
if (!result.data.t.length) {
|
|
||||||
throw new Error('Token could not be found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = this.formatTokenOutput(result.data.t[0])
|
|
||||||
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTokenOutput (token) {
|
|
||||||
// console.log(`token: ${JSON.stringify(token, null, 2)}`)
|
|
||||||
|
|
||||||
token.tokenDetails.id = token.tokenDetails.tokenIdHex
|
|
||||||
delete token.tokenDetails.tokenIdHex
|
|
||||||
token.tokenDetails.documentHash = token.tokenDetails.documentSha256Hex
|
|
||||||
delete token.tokenDetails.documentSha256Hex
|
|
||||||
token.tokenDetails.initialTokenQty = parseFloat(
|
|
||||||
token.tokenDetails.genesisOrMintQuantity
|
|
||||||
)
|
|
||||||
delete token.tokenDetails.genesisOrMintQuantity
|
|
||||||
delete token.tokenDetails.transactionType
|
|
||||||
delete token.tokenDetails.batonVout
|
|
||||||
delete token.tokenDetails.sendOutputs
|
|
||||||
|
|
||||||
token.tokenDetails.blockCreated = token.tokenStats.block_created
|
|
||||||
token.tokenDetails.blockLastActiveSend =
|
|
||||||
token.tokenStats.block_last_active_send
|
|
||||||
token.tokenDetails.blockLastActiveMint =
|
|
||||||
token.tokenStats.block_last_active_mint
|
|
||||||
token.tokenDetails.txnsSinceGenesis =
|
|
||||||
token.tokenStats.qty_valid_txns_since_genesis
|
|
||||||
token.tokenDetails.validAddresses =
|
|
||||||
token.tokenStats.qty_valid_token_addresses
|
|
||||||
token.tokenDetails.mintingBatonStatus =
|
|
||||||
token.tokenStats.minting_baton_status
|
|
||||||
|
|
||||||
delete token.tokenStats.block_last_active_send
|
|
||||||
delete token.tokenStats.block_last_active_mint
|
|
||||||
delete token.tokenStats.qty_valid_txns_since_genesis
|
|
||||||
delete token.tokenStats.qty_valid_token_addresses
|
|
||||||
|
|
||||||
token.tokenDetails.timestampUnix = token.tokenDetails.timestamp_unix
|
|
||||||
delete token.tokenDetails.timestamp_unix
|
|
||||||
|
|
||||||
return token.tokenDetails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Slpdb
|
|
||||||
+14
-1839
File diff suppressed because it is too large
Load Diff
@@ -6,24 +6,26 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
var winston = require('winston')
|
const winston = require('winston')
|
||||||
require('winston-daily-rotate-file')
|
require('winston-daily-rotate-file')
|
||||||
|
|
||||||
var NETWORK = process.env.NETWORK
|
const NETWORK = process.env.NETWORK
|
||||||
|
|
||||||
// Default 1 Megabyte
|
// Default 1 Megabyte
|
||||||
var LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
|
const LOG_MAX_SIZE = process.env.LOG_MAX_SIZE ? process.env.LOG_MAX_SIZE : '1m'
|
||||||
|
|
||||||
// Default 5 days.
|
// Default 5 days.
|
||||||
// This was causing a problem with popularity. Creating over a gigabyte of files.
|
// This was causing a problem with popularity. Creating over a gigabyte of files.
|
||||||
// var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
|
// var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '5d'
|
||||||
|
|
||||||
// 250 files @ 1Meg each = 250 megs
|
// 250 files @ 1Meg each = 250 megs
|
||||||
var LOG_MAX_FILES = process.env.LOG_MAX_FILES ? process.env.LOG_MAX_FILES : '250'
|
const LOG_MAX_FILES = process.env.LOG_MAX_FILES
|
||||||
|
? process.env.LOG_MAX_FILES
|
||||||
|
: '250'
|
||||||
|
|
||||||
// Configure daily-rotation transport.
|
// Configure daily-rotation transport.
|
||||||
var transport = new winston.transports.DailyRotateFile({
|
const transport = new winston.transports.DailyRotateFile({
|
||||||
filename: `${__dirname}/../../logs/rest-${NETWORK}-%DATE%.log`,
|
filename: `${__dirname.toString()}/../../logs/rest-${NETWORK}-%DATE%.log`,
|
||||||
datePattern: 'YYYY-MM-DD',
|
datePattern: 'YYYY-MM-DD',
|
||||||
zippedArchive: false,
|
zippedArchive: false,
|
||||||
maxSize: LOG_MAX_SIZE,
|
maxSize: LOG_MAX_SIZE,
|
||||||
@@ -39,7 +41,7 @@ transport.on('rotate', function (oldFilename, newFilename) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// This controls what goes into the log FILES
|
// This controls what goes into the log FILES
|
||||||
var wlogger = winston.createLogger({
|
const wlogger = winston.createLogger({
|
||||||
level: 'verbose',
|
level: 'verbose',
|
||||||
format: winston.format.json(),
|
format: winston.format.json(),
|
||||||
transports: [
|
transports: [
|
||||||
|
|||||||
+13
-13
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Docker containers for the below infrastructure is described here:
|
# 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:
|
# Pre-synced databases can be downloaded on the CashStrap page:
|
||||||
# https://fullstack.cash/cashstrap
|
# https://fullstack.cash/cashstrap
|
||||||
|
|
||||||
@@ -20,19 +20,11 @@ export RPC_PASSWORD=password
|
|||||||
# double spends.
|
# double spends.
|
||||||
#export RPC_SENDURL=http://$RPC_IP/
|
#export RPC_SENDURL=http://$RPC_IP/
|
||||||
|
|
||||||
# SLPDB
|
# psf-slp-indxer
|
||||||
export SLPDB_PASS_GP=somelongpassword
|
export SLP_INDEXER_API=http://<ip>:<port>/
|
||||||
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/
|
|
||||||
|
|
||||||
# Mainnet Fulcrum / ElectrumX
|
# Mainnet Fulcrum / ElectrumX
|
||||||
export FULCRUM_URL=192.168.0.6
|
export FULCRUM_API=http://172.17.0.1:3001/v1/
|
||||||
export FULCRUM_PORT=50002
|
|
||||||
|
|
||||||
# Redis DB - Used for rate limiting - customize to your own Redis installation.
|
# Redis DB - Used for rate limiting - customize to your own Redis installation.
|
||||||
export REDIS_PORT=6379
|
export REDIS_PORT=6379
|
||||||
@@ -44,7 +36,7 @@ export REDIS_HOST=172.17.0.1
|
|||||||
export TOKENSECRET=somelongpassword
|
export TOKENSECRET=somelongpassword
|
||||||
|
|
||||||
# So that bch-api can call bch-js locally.
|
# 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
|
# Basic Authentication password
|
||||||
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
||||||
@@ -53,6 +45,8 @@ export PRO_PASS=somerandomepassword:someotherrandompassword:aThirdPassword
|
|||||||
# that originate froma domain on the whitelist.
|
# that originate froma domain on the whitelist.
|
||||||
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
export WHITELIST_DOMAINS=fullstack.cash,psfoundation.cash,torlist.cash
|
||||||
|
|
||||||
|
# Uncomment the line below if you do not want to use rate limits
|
||||||
|
#export export DO_NOT_USE_RATE_LIMITS=1
|
||||||
# Rate Limits. Numbers are divided into 1000. e.g. 10000 / 500 = 20 RPM for ANON.
|
# Rate Limits. Numbers are divided into 1000. e.g. 10000 / 500 = 20 RPM for ANON.
|
||||||
# Requests use the ANON rate limit if they fail to pass in a JWT token.
|
# Requests use the ANON rate limit if they fail to pass in a JWT token.
|
||||||
# ANON = 20 requests per minute (RPM)
|
# ANON = 20 requests per minute (RPM)
|
||||||
@@ -66,4 +60,10 @@ export LOG_MAX_SIZE=1m
|
|||||||
#5d means store no more than 5 days
|
#5d means store no more than 5 days
|
||||||
export LOG_MAX_FILES=5d
|
export LOG_MAX_FILES=5d
|
||||||
|
|
||||||
|
# (Optional) if using a bcash node, set this variable. Otherwise leave it as-is.
|
||||||
|
export BCASH_SERVER=http://localhost
|
||||||
|
|
||||||
|
# PSF SLP indexer
|
||||||
|
export SLP_INDEXER_API=https://psf-slp-indexer.fullstack.cash/
|
||||||
|
|
||||||
npm start
|
npm start
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE CONTROL.JS LIBRARY
|
|
||||||
|
|
||||||
This test file uses the environment variable TEST to switch between unit
|
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
const ControlRoute = require('../../src/routes/v4/full-node/control')
|
|
||||||
const uut = new ControlRoute()
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
let originalEnvVars // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
|
||||||
const mockData = require('./mocks/control-mock')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
describe('#ControlRouter', () => {
|
|
||||||
let req, res
|
|
||||||
let sandbox
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Save existing environment variables.
|
|
||||||
originalEnvVars = {
|
|
||||||
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
|
||||||
RPC_BASEURL: process.env.RPC_BASEURL,
|
|
||||||
RPC_USERNAME: process.env.RPC_USERNAME,
|
|
||||||
RPC_PASSWORD: process.env.RPC_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Restore Sandbox
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
// Restore any pre-existing environment variables.
|
|
||||||
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
|
|
||||||
process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL
|
|
||||||
process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME
|
|
||||||
process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#root', async () => {
|
|
||||||
// root route handler.
|
|
||||||
// const root = controlRoute.testableComponents.root
|
|
||||||
|
|
||||||
it('should respond to GET for base route', async () => {
|
|
||||||
const result = uut.root(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.equal(result.status, 'control', 'Returns static string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#GetNetworkInfo', () => {
|
|
||||||
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
|
|
||||||
|
|
||||||
it('should throw 500 when network issues', async () => {
|
|
||||||
// Save the existing RPC URL.
|
|
||||||
const savedUrl = process.env.RPC_BASEURL
|
|
||||||
|
|
||||||
// Manipulate the URL to cause a 500 network error.
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getNetworkInfo(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.RPC_BASEURL = savedUrl
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// assert.include(result.error, "ENOTFOUND", "Error message expected")
|
|
||||||
})
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getNetworkInfo(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getNetworkInfo(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 info on the full node', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: { result: mockData.mockGetNetworkInfo } })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getNetworkInfo(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.hasAnyKeys(result, [
|
|
||||||
'version',
|
|
||||||
'subversion',
|
|
||||||
'protocolversion',
|
|
||||||
'localservices',
|
|
||||||
'localrelay',
|
|
||||||
'timeoffset',
|
|
||||||
'networkactive',
|
|
||||||
'connections',
|
|
||||||
'networks',
|
|
||||||
'relayfee',
|
|
||||||
'excessutxocharge',
|
|
||||||
'localaddresses',
|
|
||||||
'warnings'
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
End-to-end tests for the Address endpoint.
|
|
||||||
These tests assume that the repo is running locally and pointed at TESTNET
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const rp = require('request-promise')
|
|
||||||
|
|
||||||
// const rawtransactions = require('../../../dist/routes/v2/address')
|
|
||||||
|
|
||||||
/*
|
|
||||||
This unconfirmed utxo test needs to be expanded in the future to automatically
|
|
||||||
create a transaction. For now, that is done manually and the address provided
|
|
||||||
as a constant.
|
|
||||||
|
|
||||||
1. Start rest running locally, pointed at TESTNET.
|
|
||||||
2. Fund each address with a small amount of tBCH
|
|
||||||
3. Run this program with `node address.js`
|
|
||||||
4. If successful, it will return the unconfirmed UTXOs.
|
|
||||||
*/
|
|
||||||
const addr1 = 'bchtest:qpdcp5pv7qphu5tsjfgwezld9n9aq9ue6swgqpf45c'
|
|
||||||
const addr2 = 'bchtest:qzpvx999fagau0xqmvu3xvll9evapge7u5hcgeknzv'
|
|
||||||
|
|
||||||
async function testSingleUnconfirmed () {
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
method: 'GET',
|
|
||||||
uri: `http://localhost:3000/v2/address/unconfirmed/${addr1}`,
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true
|
|
||||||
}
|
|
||||||
|
|
||||||
await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 2)}`)
|
|
||||||
} catch (err) {
|
|
||||||
// console.log(`Error: `, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testSingleUnconfirmed()
|
|
||||||
|
|
||||||
async function testDoubleUnconfirmed () {
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
uri: 'http://localhost:3000/v2/address/unconfirmed',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
addresses: [addr1, addr2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 2)}`)
|
|
||||||
} catch (err) {
|
|
||||||
// console.log(`Error: `, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
testDoubleUnconfirmed()
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE ENCRYPTION.JS LIBRARY
|
|
||||||
|
|
||||||
This test file uses the environment variable TEST to switch between unit
|
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
|
||||||
|
|
||||||
To-Do:
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
if (!process.env.TEST) process.env.TEST = 'unit'
|
|
||||||
|
|
||||||
// Only load blockbook library after setting BLOCKBOOK_URL env var.
|
|
||||||
const EncryptionRoute = require('../../src/routes/v4/encryption')
|
|
||||||
const encryptionRoute = new EncryptionRoute()
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
|
||||||
const mockData = require('./mocks/encryption-mocks')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
describe('#Encryption Router', () => {
|
|
||||||
let req, res
|
|
||||||
let sandbox
|
|
||||||
before(() => {
|
|
||||||
// console.log(`Testing type is: ${process.env.TEST}`)
|
|
||||||
|
|
||||||
if (!process.env.NETWORK) process.env.NETWORK = 'testnet'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {})
|
|
||||||
|
|
||||||
describe('#root', () => {
|
|
||||||
// root route handler.
|
|
||||||
const root = encryptionRoute.root
|
|
||||||
|
|
||||||
it('should respond to GET for base route', async () => {
|
|
||||||
const result = root(req, res)
|
|
||||||
|
|
||||||
assert.equal(result.status, 'encryption', 'Returns static string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getPublicKey', () => {
|
|
||||||
it('should get public key from blockchain', async () => {
|
|
||||||
req.params.address =
|
|
||||||
'bitcoincash:qrehqueqhw629p6e57994436w730t4rzasnly00ht0'
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(encryptionRoute.bchjs.Electrumx, 'transactions')
|
|
||||||
.resolves(mockData.mockFulcrumTxHistory)
|
|
||||||
sandbox
|
|
||||||
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
|
||||||
.resolves([mockData.mockTxDetails2])
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.property(result, 'success')
|
|
||||||
assert.equal(result.success, true)
|
|
||||||
|
|
||||||
assert.property(result, 'publicKey')
|
|
||||||
assert.equal(
|
|
||||||
result.publicKey,
|
|
||||||
'044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return false for address with no tx history', async () => {
|
|
||||||
req.params.address =
|
|
||||||
'bitcoincash:qqwfpk04ecf69wuprj9yjys9rla5mk7rj5j8uthqel'
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(encryptionRoute.bchjs.Electrumx, 'transactions')
|
|
||||||
.resolves(mockData.mockFulcrumNoTxHistory)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
|
||||||
console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.property(result, 'success')
|
|
||||||
assert.equal(result.success, false)
|
|
||||||
|
|
||||||
assert.property(result, 'error')
|
|
||||||
assert.include(result.error, 'No transaction history')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return false for address with no send history', async () => {
|
|
||||||
req.params.address =
|
|
||||||
'bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg'
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(encryptionRoute.bchjs.Electrumx, 'transactions')
|
|
||||||
.resolves(mockData.mockFulcrumNoSendBalance)
|
|
||||||
sandbox
|
|
||||||
.stub(encryptionRoute.bchjs.RawTransactions, 'getRawTransaction')
|
|
||||||
.resolves([mockData.mockNoSendTx])
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await encryptionRoute.getPublicKey(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.property(result, 'success')
|
|
||||||
assert.equal(result.success, false)
|
|
||||||
|
|
||||||
assert.property(result, 'publicKey')
|
|
||||||
assert.include(result.publicKey, 'not found')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read more about panda here: https://panda-suite.github.io/
|
|
||||||
*/
|
|
||||||
const panda = require('pandacash-core')
|
|
||||||
|
|
||||||
const runLocalNode = done => {
|
|
||||||
const server = panda.server({
|
|
||||||
// always the same mnemonic
|
|
||||||
// mnemonic: "cigar magnet ocean purchase travel damp snack alone theme budget wagon wrong",
|
|
||||||
seedAccounts: true,
|
|
||||||
enableLogs: false,
|
|
||||||
debug: false
|
|
||||||
})
|
|
||||||
|
|
||||||
server.listen({
|
|
||||||
port: 48332,
|
|
||||||
walletPort: 48333
|
|
||||||
}, (err, pandaCashCore) => {
|
|
||||||
if (err) return console.error(err)
|
|
||||||
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
runLocalNode
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
This directory contains integration tests. To run them requires that
|
|
||||||
a local copy of rest.bitcoin.com is running. The tests in this directory are
|
|
||||||
exactly the same calls that end users would use for their app to interact
|
|
||||||
directly with rest.bitcoin.com.
|
|
||||||
|
|
||||||
At present, this directory focuses on raw-transations. But more integration tests
|
|
||||||
will be added.
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
These integration tests need to be run against a live SLPDB. They query
|
|
||||||
against a live SLPDB and test the results against known token stats.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
// const axios = require('axios')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// Exit if SLPDB URL is not defined.
|
|
||||||
if (!process.env.SLPDB_URL) {
|
|
||||||
throw new Error(
|
|
||||||
'SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SLP = require('../../../src/routes/v4/slp')
|
|
||||||
const slp = new SLP()
|
|
||||||
|
|
||||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
|
||||||
|
|
||||||
describe('#nft', () => {
|
|
||||||
let req, res
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getNftChildren', () => {
|
|
||||||
it('should return error on non-existing NFT group token', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0b'
|
|
||||||
const result = await slp.getNftChildren(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'NFT group does not exists',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
it('should return error on non-group NFT token', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
|
||||||
|
|
||||||
const result = await slp.getNftChildren(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'NFT group does not exists',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
it('should get NFT children list', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
|
||||||
|
|
||||||
const result = await slp.getNftChildren(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isArray(result.nftChildren)
|
|
||||||
assert.equal(result.nftChildren.length, 2)
|
|
||||||
assert.equal(
|
|
||||||
result.nftChildren[0],
|
|
||||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
|
||||||
)
|
|
||||||
assert.equal(
|
|
||||||
result.nftChildren[1],
|
|
||||||
'928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getNftGroup', () => {
|
|
||||||
it('should return error on non-existing NFT child token', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a8'
|
|
||||||
const result = await slp.getNftGroup(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'NFT child does not exists',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
it('should get NFT group token info', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'45a30085691d6ea586e3ec2aa9122e9b0e0d6c3c1fd357decccc15d8efde48a9'
|
|
||||||
// req.params.tokenId = '928ce61fe1006b1325a0ba0dce700bf83986a6f0691ba26e121c9ac035d12a55'
|
|
||||||
const result = await slp.getNftGroup(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
assert.property(result, 'nftGroup')
|
|
||||||
assert.property(result.nftGroup, 'id')
|
|
||||||
assert.property(result.nftGroup, 'versionType')
|
|
||||||
assert.property(result.nftGroup, 'symbol')
|
|
||||||
assert.equal(
|
|
||||||
result.nftGroup.id,
|
|
||||||
'68cd33ecd909068fbea318ae5ff1d6207cf754e53b191327d6d73b6916424c0a'
|
|
||||||
)
|
|
||||||
assert.equal(result.nftGroup.versionType, 129)
|
|
||||||
assert.equal(result.nftGroup.symbol, 'PSF.TEST.GROUP')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
Integration tests for the price library.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const assert = require('chai').assert
|
|
||||||
// const axios = require('axios')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
const Price = require('../../../src/routes/v4/price')
|
|
||||||
const price = new Price()
|
|
||||||
|
|
||||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
|
||||||
|
|
||||||
describe('#price', () => {
|
|
||||||
let req, res
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getUSD', () => {
|
|
||||||
it('should get the USD price', async () => {
|
|
||||||
const result = await price.getUSD(req, res)
|
|
||||||
console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getBCHAUSD', () => {
|
|
||||||
it('should get the USD price of BCHA', async () => {
|
|
||||||
const result = await price.getBCHAUSD(req, res)
|
|
||||||
console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
describe('#getBCHUSD', () => {
|
|
||||||
it('should get the USD price of BCH', async () => {
|
|
||||||
const result = await price.getBCHUSD(req, res)
|
|
||||||
console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
/*
|
|
||||||
These tests have been deprecated. To test bch-api rate limits, run the e2e
|
|
||||||
tests in the bch-js repository.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
const axios = require('axios')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// const SERVER = `http://192.168.0.36:12400/v4/`
|
|
||||||
const SERVER = 'http://localhost:3000/v4/'
|
|
||||||
// const SERVER = 'https://api.fullstack.cash/v4/'
|
|
||||||
//
|
|
||||||
// const TEST_JWT =
|
|
||||||
// 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYwMDYyODk1MSwiZXhwIjoxNjAzMjIwOTUxfQ.JPXDJQsxJFtCGZjHOd-hRfJuY41Ef_FQ4ET06CtYdNk'
|
|
||||||
|
|
||||||
describe('#JWT rate limits', () => {
|
|
||||||
it('should get control/getNetworkInfo() with no auth', async () => {
|
|
||||||
const options = {
|
|
||||||
method: 'GET',
|
|
||||||
url: `${SERVER}control/getNetworkInfo`
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await axios(options)
|
|
||||||
// console.log(`result.status: ${result.status}`)
|
|
||||||
// console.log(`result.data: ${util.inspect(result.data)}`)
|
|
||||||
|
|
||||||
assert.equal(result.status, 200)
|
|
||||||
assert.hasAnyKeys(result.data, ['version'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should trigger rate-limit handler if rate limits exceeds 20 request per minute', async () => {
|
|
||||||
try {
|
|
||||||
const options = {
|
|
||||||
method: 'GET',
|
|
||||||
url: `${SERVER}control/getNetworkInfo`
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = []
|
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
const promise = axios(options)
|
|
||||||
promises.push(promise)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
assert.fail('Unexpected result!')
|
|
||||||
} catch (err) {
|
|
||||||
console.log('err: ', err)
|
|
||||||
|
|
||||||
assert.equal(err.response.status, 429)
|
|
||||||
assert.include(err.response.data.error, 'Too many requests')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// it('should not trigger rate-limit handler if correct pro-tier password is used', async () => {
|
|
||||||
// try {
|
|
||||||
// const username = 'BITBOX'
|
|
||||||
//
|
|
||||||
// // Pro-tier is accessed by using the right password.
|
|
||||||
// const password = 'BITBOX'
|
|
||||||
// // const password = "something"
|
|
||||||
//
|
|
||||||
// const combined = `${username}:${password}`
|
|
||||||
// const base64Credential = Buffer.from(combined).toString('base64')
|
|
||||||
// const readyCredential = `Basic ${base64Credential}`
|
|
||||||
//
|
|
||||||
// const options = {
|
|
||||||
// method: 'GET',
|
|
||||||
// url: `${SERVER}control/getNetworkInfo`,
|
|
||||||
// headers: { Authorization: readyCredential }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const promises = []
|
|
||||||
// for (let i = 0; i < 30; i++) {
|
|
||||||
// const promise = axios(options)
|
|
||||||
// promises.push(promise)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await Promise.all(promises)
|
|
||||||
//
|
|
||||||
// assert.equal(true, true, 'Not throwing an error is a pass!')
|
|
||||||
// } catch (err) {
|
|
||||||
// // console.log(`err.response: ${util.inspect(err.response)}`)
|
|
||||||
//
|
|
||||||
// assert.equal(
|
|
||||||
// true,
|
|
||||||
// false,
|
|
||||||
// 'This error handler should not have been triggered. Is the password correct?'
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// it('should trigger rate-limit handler if rate limits exceeds pro-tier limit', async () => {
|
|
||||||
// try {
|
|
||||||
// const username = 'BITBOX'
|
|
||||||
//
|
|
||||||
// // Pro-tier is accessed by using the right password.
|
|
||||||
// const password = 'BITBOX'
|
|
||||||
// // const password = "something"
|
|
||||||
//
|
|
||||||
// const combined = `${username}:${password}`
|
|
||||||
// const base64Credential = Buffer.from(combined).toString('base64')
|
|
||||||
// const readyCredential = `Basic ${base64Credential}`
|
|
||||||
//
|
|
||||||
// // Actual rate limit is 60 per minute X 4 nodes = 240 rpm.
|
|
||||||
// const options = {
|
|
||||||
// method: 'GET',
|
|
||||||
// url: `${SERVER}control/getNetworkInfo`,
|
|
||||||
// headers: { Authorization: readyCredential }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const promises = []
|
|
||||||
// for (let i = 0; i < 80; i++) {
|
|
||||||
// const promise = axios(options)
|
|
||||||
// promises.push(promise)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await Promise.all(promises)
|
|
||||||
//
|
|
||||||
// assert.equal(true, false, 'Unexpected result!')
|
|
||||||
// } catch (err) {
|
|
||||||
// // console.log(`err.response: ${util.inspect(err.response)}`)
|
|
||||||
//
|
|
||||||
// assert.equal(err.response.status, 429)
|
|
||||||
// assert.include(err.response.data.error, 'Too many requests')
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// it('should unlock pro-tier for a valid JWT token', async () => {
|
|
||||||
// try {
|
|
||||||
// // Actual rate limit is 60 per minute X 4 nodes = 240 rpm.
|
|
||||||
// const options = {
|
|
||||||
// method: 'GET',
|
|
||||||
// url: `${SERVER}control/`,
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Token ${TEST_JWT}`
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const promises = []
|
|
||||||
// for (let i = 0; i < 60; i++) {
|
|
||||||
// const promise = axios(options)
|
|
||||||
// promises.push(promise)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await Promise.all(promises)
|
|
||||||
//
|
|
||||||
// // assert.equal(true, false, "Unexpected result!")
|
|
||||||
// assert.equal(true, true, 'Not throwing an error is a pass!')
|
|
||||||
// } catch (err) {
|
|
||||||
// console.log(`err.response: ${util.inspect(err.response)}`)
|
|
||||||
//
|
|
||||||
// assert.equal(true, false, 'Unexpected result!')
|
|
||||||
// }
|
|
||||||
// // Override default timeout for this test.
|
|
||||||
// }).timeout(20000)
|
|
||||||
})
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/*
|
|
||||||
These integration tests are intended to be run against a live local copy of
|
|
||||||
rest.bitcoin.com. They exercise the endpoints in the same way the SDK or
|
|
||||||
end-user application would. These tests were created to replace the parts
|
|
||||||
removed from the swagger UI, that otherwise would have excersiced these endpoints.
|
|
||||||
|
|
||||||
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 rawtransactions = require('../../../dist/routes/v2/rawtransactions')
|
|
||||||
// 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 () => {
|
|
||||||
const options = {
|
|
||||||
method: 'GET',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 0)}`)
|
|
||||||
|
|
||||||
assert.equal(result.body.status, 'rawtransactions')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#whCreateTx', () => {
|
|
||||||
it('should return tx hex', async () => {
|
|
||||||
const minIn = {
|
|
||||||
txid:
|
|
||||||
'f7ed9cf23dee85910f6269c9a101a75fcfd2f3c6fc81f17fad824ff7aaf99ab2',
|
|
||||||
vout: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
method: 'PUT',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/create',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
// inputs: [mockData.mockWHCreateInput],
|
|
||||||
inputs: [minIn],
|
|
||||||
outputs: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
result.body,
|
|
||||||
'0200000001b29af9aaf74f82ad7ff181fcc6f3d2cf5fa701a1c969620f9185ee3df29cedf70100000000ffffffff0000000000'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#whOpReturn', () => {
|
|
||||||
it('should return tx hex', async () => {
|
|
||||||
const options = {
|
|
||||||
method: 'PUT',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/opreturn',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
rawtx: '01000000000000000000',
|
|
||||||
payload: '00000000000000020000000006dac2c0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
result.body,
|
|
||||||
'0100000000010000000000000000166a140877686300000000000000020000000006dac2c000000000'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#whReference', () => {
|
|
||||||
it('should return tx hex', async () => {
|
|
||||||
const options = {
|
|
||||||
method: 'PUT',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/reference',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
rawtx:
|
|
||||||
'0100000001a7a9402ecd77f3c9f745793c9ec805bfa2e14b89877581c734c774864247e6f50400000000ffffffff03aa0a0000000000001976a9146d18edfe073d53f84dd491dae1379f8fb0dfe5d488ac5c0d0000000000004751210252ce4bdd3ce38b4ebbc5a6e1343608230da508ff12d23d85b58c964204c4cef3210294cc195fc096f87d0f813a337ae7e5f961b1c8a18f1f8604a909b3a5121f065b52aeaa0a0000000000001976a914946cb2e08075bcbaf157e47bcb67eb2b2339d24288ac00000000',
|
|
||||||
destination: 'bitcoincash:qrn60nerx5zug4u4hal06atep3lzhtecvy4pxk75lf',
|
|
||||||
amount: 0.005
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
|
|
||||||
|
|
||||||
assert.isString(result.body)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#whChangeOutput', () => {
|
|
||||||
it('should return tx hex', async () => {
|
|
||||||
const options = {
|
|
||||||
method: 'PUT',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/change',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
rawtx:
|
|
||||||
'0100000001b15ee60431ef57ec682790dec5a3c0d83a0c360633ea8308fbf6d5fc10a779670400000000ffffffff025c0d00000000000047512102f3e471222bb57a7d416c82bf81c627bfcd2bdc47f36e763ae69935bba4601ece21021580b888ff56feb27f17f08802ebed26258c23697d6a462d43fc13b565fda2dd52aeaa0a0000000000001976a914946cb2e08075bcbaf157e47bcb67eb2b2339d24288ac00000000',
|
|
||||||
destination: 'bitcoincash:qrn60nerx5zug4u4hal06atep3lzhtecvy4pxk75lf',
|
|
||||||
prevtxs: [
|
|
||||||
{
|
|
||||||
txid:
|
|
||||||
'6779a710fcd5f6fb0883ea3306360c3ad8c0a3c5de902768ec57ef3104e65eb1',
|
|
||||||
vout: 4,
|
|
||||||
scriptPubKey:
|
|
||||||
'76a9147b25205fd98d462880a3e5b0541235831ae959e588ac',
|
|
||||||
value: 0.00068257
|
|
||||||
}
|
|
||||||
],
|
|
||||||
fee: 0.000035
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
|
|
||||||
|
|
||||||
assert.isString(result.body)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#whInput', () => {
|
|
||||||
it('should return tx hex', async () => {
|
|
||||||
const options = {
|
|
||||||
method: 'PUT',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/input',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
txid:
|
|
||||||
'b006729017df05eda586df9ad3f8ccfee5be340aadf88155b784d1fc0e8342ee',
|
|
||||||
n: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result.body: ${JSON.stringify(result.body, null, 0)}`)
|
|
||||||
|
|
||||||
assert.isString(result.body)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getRawTransaction', () => {
|
|
||||||
it('should return tx hex', async () => {
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
uri: 'http://localhost:3000/v2/rawtransactions/getRawTransaction',
|
|
||||||
resolveWithFullResponse: true,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
txids: [
|
|
||||||
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098'
|
|
||||||
],
|
|
||||||
verbose: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await rp(options)
|
|
||||||
// console.log(`result.body: ${util.inspect(result.body)}`)
|
|
||||||
|
|
||||||
assert.isArray(result.body)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
/*
|
|
||||||
These integration tests need to be run against a live SLPDB. They query
|
|
||||||
against a live SLPDB and test the results against known token stats.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const assert = require('chai').assert
|
|
||||||
// const axios = require('axios')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// Exit if SLPDB URL is not defined.
|
|
||||||
if (!process.env.SLPDB_URL) {
|
|
||||||
throw new Error(
|
|
||||||
'SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SLP = require('../../../src/routes/v4/slp')
|
|
||||||
const slp = new SLP()
|
|
||||||
|
|
||||||
const BCHJS = require('@psf/bch-js')
|
|
||||||
const bchjs = new BCHJS()
|
|
||||||
|
|
||||||
const { mockReq, mockRes } = require('../mocks/express-mocks')
|
|
||||||
|
|
||||||
describe('#slp', () => {
|
|
||||||
let req, res
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#tokenStats', () => {
|
|
||||||
it('should get token stats for token with no mint baton', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7'
|
|
||||||
// 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
|
||||||
|
|
||||||
const result = await slp.tokenStats(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Assert that expected properties exist.
|
|
||||||
assert.property(result, 'decimals')
|
|
||||||
assert.property(result, 'timestamp')
|
|
||||||
assert.property(result, 'versionType')
|
|
||||||
assert.property(result, 'documentUri')
|
|
||||||
assert.property(result, 'symbol')
|
|
||||||
assert.property(result, 'name')
|
|
||||||
assert.property(result, 'containsBaton')
|
|
||||||
assert.property(result, 'id')
|
|
||||||
assert.property(result, 'documentHash')
|
|
||||||
assert.property(result, 'initialTokenQty')
|
|
||||||
assert.property(result, 'blockCreated')
|
|
||||||
assert.property(result, 'blockLastActiveSend')
|
|
||||||
assert.property(result, 'blockLastActiveMint')
|
|
||||||
assert.property(result, 'txnsSinceGenesis')
|
|
||||||
assert.property(result, 'validAddresses')
|
|
||||||
assert.property(result, 'mintingBatonStatus')
|
|
||||||
assert.property(result, 'timestampUnix')
|
|
||||||
assert.property(result, 'totalMinted')
|
|
||||||
assert.property(result, 'totalBurned')
|
|
||||||
assert.property(result, 'circulatingSupply')
|
|
||||||
|
|
||||||
// baton was never created.
|
|
||||||
assert.equal(result.containsBaton, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should get token stats for token with a mint baton', async () => {
|
|
||||||
req.params.tokenId =
|
|
||||||
'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2'
|
|
||||||
|
|
||||||
const result = await slp.tokenStats(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Assert that expected properties exist.
|
|
||||||
assert.property(result, 'decimals')
|
|
||||||
assert.property(result, 'timestamp')
|
|
||||||
assert.property(result, 'versionType')
|
|
||||||
assert.property(result, 'documentUri')
|
|
||||||
assert.property(result, 'symbol')
|
|
||||||
assert.property(result, 'name')
|
|
||||||
assert.property(result, 'containsBaton')
|
|
||||||
assert.property(result, 'id')
|
|
||||||
assert.property(result, 'documentHash')
|
|
||||||
assert.property(result, 'initialTokenQty')
|
|
||||||
assert.property(result, 'blockCreated')
|
|
||||||
assert.property(result, 'blockLastActiveSend')
|
|
||||||
assert.property(result, 'blockLastActiveMint')
|
|
||||||
assert.property(result, 'txnsSinceGenesis')
|
|
||||||
assert.property(result, 'validAddresses')
|
|
||||||
assert.property(result, 'mintingBatonStatus')
|
|
||||||
assert.property(result, 'timestampUnix')
|
|
||||||
assert.property(result, 'totalMinted')
|
|
||||||
assert.property(result, 'totalBurned')
|
|
||||||
assert.property(result, 'circulatingSupply')
|
|
||||||
|
|
||||||
// baton was created.
|
|
||||||
assert.equal(result.containsBaton, true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#generateSendOpReturn', () => {
|
|
||||||
it('should return OP_RETURN script', async () => {
|
|
||||||
req.body.tokenUtxos = [
|
|
||||||
{
|
|
||||||
tokenId:
|
|
||||||
'38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0',
|
|
||||||
decimals: 8,
|
|
||||||
tokenQty: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
req.body.sendQty = 1.5
|
|
||||||
|
|
||||||
const result = await slp.generateSendOpReturn(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.hasAllKeys(result, ['script', 'outputs'])
|
|
||||||
assert.isNumber(result.outputs)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#hydrateUtxos', () => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
req.body.utxos = utxos
|
|
||||||
const result = await slp.hydrateUtxos(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 process data directly from Electrumx', async () => {
|
|
||||||
const addrs = [
|
|
||||||
'bitcoincash:qq6mvsm7l92d77zpymmltvaw09p5uzghyuyx7spygg',
|
|
||||||
'bitcoincash:qpjdrs8qruzh8xvusdfmutjx62awcepnhyperm3g89',
|
|
||||||
'bitcoincash:qzygn28zpgeemnptkn26xzyuzzfu9l8f9vfvq7kptk'
|
|
||||||
]
|
|
||||||
|
|
||||||
const utxos = await bchjs.Electrumx.utxo(addrs)
|
|
||||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
|
||||||
|
|
||||||
req.body.utxos = utxos.utxos
|
|
||||||
const result = await slp.hydrateUtxos(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, 3)
|
|
||||||
assert.equal(result.slpUtxos[0].utxos.length, 1)
|
|
||||||
assert.equal(result.slpUtxos[1].utxos.length, 1)
|
|
||||||
assert.equal(result.slpUtxos[2].utxos.length, 2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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 =
|
|
||||||
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a'
|
|
||||||
|
|
||||||
req.params.txid = txid
|
|
||||||
const result = await slp.validate2Single(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(result.txid, txid)
|
|
||||||
assert.equal(result.isValid, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should validate a known valid TXID', async () => {
|
|
||||||
const txid =
|
|
||||||
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488'
|
|
||||||
|
|
||||||
req.params.txid = txid
|
|
||||||
const result = await slp.validate2Single(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(result.txid, txid)
|
|
||||||
assert.equal(result.isValid, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// CT 10-11-2020: This test is valid, but because of the cacheing built
|
|
||||||
// into slp-validate, it will not consistently pass or fail. To manually
|
|
||||||
// Run this test, re-start slp-api or find a token txid with a long DAG.
|
|
||||||
// it('should cancel if validation takes too long', async () => {
|
|
||||||
// const txid =
|
|
||||||
// 'eacb1085dfa296fef6d4ae2c0f4529a1bef096dd2325bdcc6dcb5241b3bdb579'
|
|
||||||
//
|
|
||||||
// req.params.txid = txid
|
|
||||||
// const result = await slp.validate2Single(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'
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#validateBulk', () => {
|
|
||||||
it('should handle a mix of valid, invalid, and non-SLP txs', async () => {
|
|
||||||
const txids = [
|
|
||||||
// Malformed SLP tx
|
|
||||||
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
|
|
||||||
// Normal TX (non-SLP)
|
|
||||||
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
|
|
||||||
// Valid PSF SLP tx
|
|
||||||
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
|
|
||||||
// Valid SLP token not in whitelist
|
|
||||||
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
|
|
||||||
// Token send on BCHN network.
|
|
||||||
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019',
|
|
||||||
// Token send on ABC network.
|
|
||||||
'336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d',
|
|
||||||
// Known invalid SLP token send of PSF tokens.
|
|
||||||
'2bf691ad3679d928fef880b8a45b93b233f8fa0d0a92cf792313dbe77b1deb74'
|
|
||||||
]
|
|
||||||
|
|
||||||
req.body.txids = txids
|
|
||||||
const result = await slp.validateBulk(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
// BCHN expected results
|
|
||||||
if (process.env.ISBCHN) {
|
|
||||||
assert.equal(result[0].txid, txids[0])
|
|
||||||
assert.equal(result[0].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[1].txid, txids[1])
|
|
||||||
assert.equal(result[1].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[2].txid, txids[2])
|
|
||||||
assert.equal(result[2].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[3].txid, txids[3])
|
|
||||||
assert.equal(result[3].valid, true)
|
|
||||||
|
|
||||||
// Note: This should change from null to true once SLPDB finishes indexing.
|
|
||||||
assert.equal(result[4].txid, txids[4])
|
|
||||||
assert.equal(result[4].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[5].txid, txids[5])
|
|
||||||
assert.equal(result[5].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[6].txid, txids[6])
|
|
||||||
assert.equal(result[6].valid, false)
|
|
||||||
assert.include(
|
|
||||||
result[6].invalidReason,
|
|
||||||
'Token outputs are greater than valid token inputs'
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
assert.equal(result[0].txid, txids[0])
|
|
||||||
assert.equal(result[0].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[1].txid, txids[1])
|
|
||||||
assert.equal(result[1].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[2].txid, txids[2])
|
|
||||||
assert.equal(result[2].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[3].txid, txids[3])
|
|
||||||
assert.equal(result[3].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[4].txid, txids[4])
|
|
||||||
assert.equal(result[4].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[5].txid, txids[5])
|
|
||||||
assert.equal(result[5].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[6].txid, txids[6])
|
|
||||||
assert.equal(result[6].valid, false)
|
|
||||||
assert.include(
|
|
||||||
result[6].invalidReason,
|
|
||||||
'Token outputs are greater than valid token inputs'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#validate3Bulk', () => {
|
|
||||||
it('should handle a mix of valid, invalid, and non-SLP txs', async () => {
|
|
||||||
const txids = [
|
|
||||||
// Malformed SLP tx
|
|
||||||
'f7e5199ef6669ad4d078093b3ad56e355b6ab84567e59ad0f08a5ad0244f783a',
|
|
||||||
// Normal TX (non-SLP)
|
|
||||||
'01cdaec2f8b311fc2d6ecc930247bd45fa696dc204ab684596e281fe1b06c1f0',
|
|
||||||
// Valid PSF SLP tx
|
|
||||||
'daf4d8b8045e7a90b7af81bfe2370178f687da0e545511bce1c9ae539eba5ffd',
|
|
||||||
// Valid SLP token not in whitelist
|
|
||||||
'3a4b628cbcc183ab376d44ce5252325f042268307ffa4a53443e92b6d24fb488',
|
|
||||||
// Token send on BCHN network.
|
|
||||||
'402c663379d9699b6e2dd38737061e5888c5a49fca77c97ab98e79e08959e019',
|
|
||||||
// Token send on ABC network.
|
|
||||||
'336bfe2168aac4c3303508a9e8548a0d33797a83b85b76a12d845c8d6674f79d',
|
|
||||||
// Known invalid SLP token send of PSF tokens.
|
|
||||||
'2bf691ad3679d928fef880b8a45b93b233f8fa0d0a92cf792313dbe77b1deb74'
|
|
||||||
]
|
|
||||||
|
|
||||||
req.body.txids = txids
|
|
||||||
const result = await slp.validate3Bulk(req, res)
|
|
||||||
console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
// BCHN expected results
|
|
||||||
if (process.env.ISBCHN) {
|
|
||||||
assert.equal(result[0].txid, txids[0])
|
|
||||||
assert.equal(result[0].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[1].txid, txids[1])
|
|
||||||
assert.equal(result[1].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[2].txid, txids[2])
|
|
||||||
assert.equal(result[2].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[3].txid, txids[3])
|
|
||||||
assert.equal(result[3].valid, null)
|
|
||||||
|
|
||||||
// Note: This should change from null to true once SLPDB finishes indexing.
|
|
||||||
assert.equal(result[4].txid, txids[4])
|
|
||||||
assert.equal(result[4].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[5].txid, txids[5])
|
|
||||||
assert.equal(result[5].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[6].txid, txids[6])
|
|
||||||
assert.equal(result[6].valid, false)
|
|
||||||
assert.include(
|
|
||||||
result[6].invalidReason,
|
|
||||||
'Token outputs are greater than valid token inputs'
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
assert.equal(result[0].txid, txids[0])
|
|
||||||
assert.equal(result[0].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[1].txid, txids[1])
|
|
||||||
assert.equal(result[1].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[2].txid, txids[2])
|
|
||||||
assert.equal(result[2].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[3].txid, txids[3])
|
|
||||||
assert.equal(result[3].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[4].txid, txids[4])
|
|
||||||
assert.equal(result[4].valid, null)
|
|
||||||
|
|
||||||
assert.equal(result[5].txid, txids[5])
|
|
||||||
assert.equal(result[5].valid, true)
|
|
||||||
|
|
||||||
assert.equal(result[6].txid, txids[6])
|
|
||||||
assert.equal(result[6].valid, false)
|
|
||||||
assert.include(
|
|
||||||
result[6].invalidReason,
|
|
||||||
'Token outputs are greater than valid token inputs'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getStatus', () => {
|
|
||||||
it('should get the SLPDB status', async () => {
|
|
||||||
const result = await slp.getStatus(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.property(result, 'bchBlockHeight')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
These integration tests need to be run against a live SLPDB. They query
|
|
||||||
against a live SLPDB and test the results against known token stats.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
// const chai = require('chai')
|
|
||||||
// const assert = chai.assert
|
|
||||||
// const axios = require('axios')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// Exit if SLPDB URL is not defined.
|
|
||||||
if (!process.env.SLPDB_URL) {
|
|
||||||
throw new Error('SLPDB_URL and SLPDB_PASS must be defined in order to run these tests.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const SLPDB = require('../../../src/routes/v4/services/slpdb')
|
|
||||||
const slpdb = new SLPDB()
|
|
||||||
|
|
||||||
describe('#slpdb', () => {
|
|
||||||
describe('#getTotalCirculating', () => {
|
|
||||||
it('should get circulating supply', async () => {
|
|
||||||
const result = await slpdb.getTotalCirculating('b10677aef051b73e6b170c1c0824da33a3e0680ab5a01cd8d76aa77840fccfb4')
|
|
||||||
console.log('result: ', result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE MINING.TS LIBRARY
|
|
||||||
|
|
||||||
This test file uses the environment variable TEST to switch between unit
|
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
const MiningRoute = require('../../src/routes/v4/full-node/mining')
|
|
||||||
const uut = new MiningRoute()
|
|
||||||
|
|
||||||
// const nock = require('nock') // HTTP mocking
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
let originalEnvVars // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
|
||||||
const mockData = require('./mocks/mining-mocks')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
describe('#Mining', () => {
|
|
||||||
let req, res
|
|
||||||
let sandbox
|
|
||||||
before(() => {
|
|
||||||
// Save existing environment variables.
|
|
||||||
originalEnvVars = {
|
|
||||||
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
|
||||||
RPC_BASEURL: process.env.RPC_BASEURL,
|
|
||||||
RPC_USERNAME: process.env.RPC_USERNAME,
|
|
||||||
RPC_PASSWORD: process.env.RPC_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
if (!process.env.TEST) process.env.TEST = 'unit'
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api'
|
|
||||||
process.env.RPC_USERNAME = 'fakeusername'
|
|
||||||
process.env.RPC_PASSWORD = 'fakepassword'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Restore Sandbox
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
// Restore any pre-existing environment variables.
|
|
||||||
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
|
|
||||||
process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL
|
|
||||||
process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME
|
|
||||||
process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#root', async () => {
|
|
||||||
// root route handler.
|
|
||||||
it('should respond to GET for base route', async () => {
|
|
||||||
const result = uut.root(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.equal(result.status, 'mining', 'Returns static string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getMiningInfo', async () => {
|
|
||||||
it('should throw 503 when network issues', async () => {
|
|
||||||
// Save the existing RPC URL.
|
|
||||||
const savedUrl2 = process.env.RPC_BASEURL
|
|
||||||
|
|
||||||
// Manipulate the URL to cause a 500 network error.
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getMiningInfo(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.RPC_BASEURL = savedUrl2
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
|
|
||||||
})
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getMiningInfo(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getMiningInfo(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 mining information', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: { result: mockData.mockMiningInfo } })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getMiningInfo(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.property(result, 'blocks')
|
|
||||||
assert.property(result, 'difficulty')
|
|
||||||
assert.property(result, 'networkhashps')
|
|
||||||
assert.property(result, 'pooledtx')
|
|
||||||
assert.property(result, 'chain')
|
|
||||||
assert.property(result, 'warnings')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getNetworkHashPS', async () => {
|
|
||||||
it('should throw 503 when network issues', async () => {
|
|
||||||
// Save the existing RPC URL.
|
|
||||||
const savedUrl2 = process.env.RPC_BASEURL
|
|
||||||
|
|
||||||
// Manipulate the URL to cause a 500 network error.
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getNetworkHashPS(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.RPC_BASEURL = savedUrl2
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
|
|
||||||
})
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getNetworkHashPS(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getNetworkHashPS(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 Network Hash per second', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: { result: 517604755.6648782 } })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getNetworkHashPS(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockAddressDetails = {
|
|
||||||
addrStr: '1Fg4r9iDrEkCcDmHTy2T79EusNfhyQpu7W',
|
|
||||||
balance: 0.00126419,
|
|
||||||
balanceSat: 126419,
|
|
||||||
totalReceived: 0.02175868,
|
|
||||||
totalReceivedSat: 2175868,
|
|
||||||
totalSent: 0.02049449,
|
|
||||||
totalSentSat: 2049449,
|
|
||||||
unconfirmedBalance: 0,
|
|
||||||
unconfirmedBalanceSat: 0,
|
|
||||||
unconfirmedTxApperances: 0,
|
|
||||||
txApperances: 3,
|
|
||||||
transactions: [
|
|
||||||
'2dc053f55a666a3d2a08b1c680b704d62a55506d14ad884add87edcc56b9277d',
|
|
||||||
'544c15ce35c0f2e808d28f29d6587f1ec9276233e29856b7f2938cf0daef0026',
|
|
||||||
'81039b1d7b855b133f359f9dc65f776bd105650153a941675fedc504228ddbd3'
|
|
||||||
],
|
|
||||||
legacyAddress: '1Fg4r9iDrEkCcDmHTy2T79EusNfhyQpu7W',
|
|
||||||
cashAddress: 'bitcoincash:qzs02v05l7qs5s24srqju498qu55dwuj0cx5ehjm2c'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockUtxoDetails = [
|
|
||||||
{
|
|
||||||
txid: '15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e',
|
|
||||||
vout: 286,
|
|
||||||
amount: 0.00001,
|
|
||||||
satoshis: 1000,
|
|
||||||
height: 546083,
|
|
||||||
confirmations: 3490
|
|
||||||
},
|
|
||||||
{
|
|
||||||
txid: '15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e',
|
|
||||||
vout: 287,
|
|
||||||
amount: 0.00001,
|
|
||||||
satoshis: 1000,
|
|
||||||
height: 546083,
|
|
||||||
confirmations: 3490
|
|
||||||
},
|
|
||||||
{
|
|
||||||
txid: '15f6a584080b04911121fbaca7bfcf3dd64ef2bfa5a01daf31e05a296c3e5e9e',
|
|
||||||
vout: 288,
|
|
||||||
amount: 0.00001,
|
|
||||||
satoshis: 1000,
|
|
||||||
height: 546083,
|
|
||||||
confirmations: 3490
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockUnconfirmed = [
|
|
||||||
{
|
|
||||||
address: '1EzdL6TBbkNhnB2fYiBaKmcs5fxaoqwdAp',
|
|
||||||
txid: '000c00a90fb5031da6e02f7625df2f8b35a4c16e6feb9fc72293e67e5ff75786',
|
|
||||||
vout: 0,
|
|
||||||
scriptPubKey: '76a914997fabcd94a1e2aaa13a7664362e5e7b96c169a988ac',
|
|
||||||
amount: 0.00999626,
|
|
||||||
satoshis: 999626,
|
|
||||||
confirmations: 0,
|
|
||||||
ts: 1537989425
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockTransactions = {
|
|
||||||
pagesTotal: 1,
|
|
||||||
txs: [
|
|
||||||
{
|
|
||||||
txid: '000c00a90fb5031da6e02f7625df2f8b35a4c16e6feb9fc72293e67e5ff75786',
|
|
||||||
version: 2,
|
|
||||||
locktime: 549565,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid:
|
|
||||||
'45c891c6d44619fc85716ba1b593aa83ebc1e500fe611b1ab98531ea203a0f21',
|
|
||||||
vout: 209,
|
|
||||||
sequence: 4294967294,
|
|
||||||
n: 0,
|
|
||||||
scriptSig: {
|
|
||||||
hex:
|
|
||||||
'47304402205b136f348bedae61a87c0500979d47ab02c76f0e4aba866b94c4931fccb5d7dc0220757a141660475b0cd4cb8b1f54b1ace95105f5bea775e817b115fb589fe4477541210246daec651c506f353daa7468714399c773df42ac4d663022d0e446a3331ac64a',
|
|
||||||
asm:
|
|
||||||
'304402205b136f348bedae61a87c0500979d47ab02c76f0e4aba866b94c4931fccb5d7dc0220757a141660475b0cd4cb8b1f54b1ace95105f5bea775e817b115fb589fe44775[ALL|FORKID] 0246daec651c506f353daa7468714399c773df42ac4d663022d0e446a3331ac64a'
|
|
||||||
},
|
|
||||||
addr: '14TdovsQUL69xL5g3zSzhLpmDb93d9rU9m',
|
|
||||||
valueSat: 1979534,
|
|
||||||
value: 0.01979534,
|
|
||||||
doubleSpentTxID: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
txid:
|
|
||||||
'6e5fa79547112a912adf6082c975dc80e282045193512ea369adb6bac7420674',
|
|
||||||
vout: 147,
|
|
||||||
sequence: 4294967294,
|
|
||||||
n: 1,
|
|
||||||
scriptSig: {
|
|
||||||
hex:
|
|
||||||
'473044022056674ac83a37d54ffec2c6d33cfc2ae2256f821b8bd818e1b6783356b49f3d28022046d3dd147c8fc51f2da5d16996a3cea1d7314e327aa1ae1017418737ed20136a4121038f23af565f68d8455e07e5c33c5cfc5114de2f478f035db23eb02f8ff5fe7f6e',
|
|
||||||
asm:
|
|
||||||
'3044022056674ac83a37d54ffec2c6d33cfc2ae2256f821b8bd818e1b6783356b49f3d28022046d3dd147c8fc51f2da5d16996a3cea1d7314e327aa1ae1017418737ed20136a[ALL|FORKID] 038f23af565f68d8455e07e5c33c5cfc5114de2f478f035db23eb02f8ff5fe7f6e'
|
|
||||||
},
|
|
||||||
addr: '1CEtC2fjELvmAzTd6MWuHb1gvuSq8x3Xpb',
|
|
||||||
valueSat: 10466,
|
|
||||||
value: 0.00010466,
|
|
||||||
doubleSpentTxID: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: '0.00999626',
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
hex: '76a914997fabcd94a1e2aaa13a7664362e5e7b96c169a988ac',
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 997fabcd94a1e2aaa13a7664362e5e7b96c169a9 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
addresses: ['1EzdL6TBbkNhnB2fYiBaKmcs5fxaoqwdAp'],
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '0.00990000',
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
hex: '76a914e3335e4d6babe61ea58311293c1c6bfe802801cb88ac',
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 e3335e4d6babe61ea58311293c1c6bfe802801cb OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
addresses: ['1MiKvgaQTFCTEZbSDMZgw9ahaB52Cr4ofb'],
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
blockhash:
|
|
||||||
'00000000000000000158b1a2883688873ec5ea076e3b0f576bcdfe1fd277880f',
|
|
||||||
blockheight: 549601,
|
|
||||||
confirmations: 5,
|
|
||||||
time: 1537990026,
|
|
||||||
blocktime: 1537990026,
|
|
||||||
valueOut: 0.01989626,
|
|
||||||
size: 372,
|
|
||||||
valueIn: 0.0199,
|
|
||||||
fees: 0.00000374
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockAddressDetails,
|
|
||||||
mockUtxoDetails,
|
|
||||||
mockUnconfirmed,
|
|
||||||
mockTransactions
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the bitcore route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockBalance = {
|
|
||||||
confirmed: 10000000,
|
|
||||||
unconfirmed: 0,
|
|
||||||
balance: 10000000
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockUtxos = [
|
|
||||||
{
|
|
||||||
_id: '5cf2c31a33bd46a95ec7e730',
|
|
||||||
chain: 'BCH',
|
|
||||||
network: 'testnet',
|
|
||||||
coinbase: false,
|
|
||||||
mintIndex: 1,
|
|
||||||
spentTxid: '',
|
|
||||||
mintTxid:
|
|
||||||
'5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392',
|
|
||||||
mintHeight: 1265275,
|
|
||||||
spentHeight: -2,
|
|
||||||
address: 'qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4',
|
|
||||||
script: '76a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac',
|
|
||||||
value: 10000000,
|
|
||||||
confirmations: -1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockBalance,
|
|
||||||
mockUtxos
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockBlockDetails = {
|
|
||||||
hash: '00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79',
|
|
||||||
size: 1319,
|
|
||||||
height: 1267544,
|
|
||||||
version: 536870912,
|
|
||||||
merkleroot:
|
|
||||||
'c2cd01ec2cf149acc7631385f89ae103d1a2ad212ab810c862d9680a811618ce',
|
|
||||||
tx: [
|
|
||||||
'52bfa89d449fef8070ec33e7a61f5ec8b5417ff62b050cd6a144ebce9557e0fa',
|
|
||||||
'8988e8742d2523f667c2d1374861919e3a82903e059fec75c10795da2303b93b',
|
|
||||||
'41def004f22b196d675566a6983ecf97611e72c48375b8ba779983d5ccd369d7',
|
|
||||||
'8ca8efb06abb94395f8331f87e101504dcf66aee828f2111922f056971fa7502',
|
|
||||||
'9ab409cbb203be7cf22bd6598d787fcdd5b417d885b72207ecc7253470770649'
|
|
||||||
],
|
|
||||||
time: 1542048973,
|
|
||||||
nonce: 3936284753,
|
|
||||||
bits: '1a03c148',
|
|
||||||
difficulty: 4467892.99177529,
|
|
||||||
chainwork: '00000000000000000000000000000000000000000000003ee27b503a064045bd',
|
|
||||||
confirmations: 3,
|
|
||||||
previousblockhash:
|
|
||||||
'00000000000001891202cbe18729a02a8763cf04da0ca7aded6e6c7d9b500785',
|
|
||||||
nextblockhash:
|
|
||||||
'000000000000039b93b3f3403a0eb21150904b15229d998cf3788fd8bf192cc2',
|
|
||||||
reward: 0.78125,
|
|
||||||
isMainChain: true,
|
|
||||||
poolInfo: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockBlockHash =
|
|
||||||
'00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockBlockDetails,
|
|
||||||
mockBlockHash
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the blockbook route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockBalance = {
|
|
||||||
page: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
itemsOnPage: 1000,
|
|
||||||
address: 'bitcoincash:qrdka2205f4hyukutc2g0s6lykperc8nsu5u2ddpqf',
|
|
||||||
balance: '10000000',
|
|
||||||
totalReceived: '10000000',
|
|
||||||
totalSent: '0',
|
|
||||||
unconfirmedBalance: '0',
|
|
||||||
unconfirmedTxs: 0,
|
|
||||||
txs: 1,
|
|
||||||
txids: ['5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392']
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockUtxos = [
|
|
||||||
{
|
|
||||||
txid: '5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392',
|
|
||||||
vout: 1,
|
|
||||||
value: '10000000',
|
|
||||||
height: 1265275,
|
|
||||||
confirmations: 42704
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockTx = {
|
|
||||||
txid: '5fe9b74056319a8c87f45cc745030715a6180758b94938dbf90d639d55652392',
|
|
||||||
version: 2,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: '85ddb8215fc3701a493cf1c450644c5ef32c55aaa2f48ae2d008944394f3e4d3',
|
|
||||||
sequence: 4294967295,
|
|
||||||
n: 0,
|
|
||||||
addresses: ['bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35'],
|
|
||||||
value: '16983000648',
|
|
||||||
hex:
|
|
||||||
'47304402202378e55f4d02bb932498deef22dfc1f7a4984858c3b55017e225dd567172252e0220373d27710b5d42a72ac9725959f1605a912fee0ae86a7ad36e7d6d796f14ca29412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: '16973000422',
|
|
||||||
n: 0,
|
|
||||||
spent: true,
|
|
||||||
hex: '76a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac',
|
|
||||||
addresses: ['bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '10000000',
|
|
||||||
n: 1,
|
|
||||||
hex: '76a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac',
|
|
||||||
addresses: ['bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
blockHash: '00000000005242edac4635ac2375a454e801cc1be8b131b622328089731e5e30',
|
|
||||||
blockHeight: 1265275,
|
|
||||||
confirmations: 65402,
|
|
||||||
blockTime: 1540912733,
|
|
||||||
value: '16983000422',
|
|
||||||
valueIn: '16983000648',
|
|
||||||
fees: '226',
|
|
||||||
hex:
|
|
||||||
'0200000001d3e4f394439408d0e28af4a2aa552cf35e4c6450c4f13c491a70c35f21b8dd85000000006a47304402202378e55f4d02bb932498deef22dfc1f7a4984858c3b55017e225dd567172252e0220373d27710b5d42a72ac9725959f1605a912fee0ae86a7ad36e7d6d796f14ca29412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02e66eabf3030000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a9140e5b4ad9008bb9a027b7e2d0ef958914e12db20788ac00000000'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockBalance,
|
|
||||||
mockUtxos,
|
|
||||||
mockTx
|
|
||||||
}
|
|
||||||
@@ -1,380 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockBlockHash =
|
|
||||||
'00000000000000645dec6503d3f5eafb0d2537a7a28f181d721dec7c44154c79'
|
|
||||||
|
|
||||||
const mockBlockchainInfo = {
|
|
||||||
chain: 'test',
|
|
||||||
blocks: 1267694,
|
|
||||||
headers: 1267694,
|
|
||||||
bestblockhash:
|
|
||||||
'000000000000013eaf80d1e157e32804c36a58ad0bb26ca59833880c80298780',
|
|
||||||
difficulty: 4105763.969035785,
|
|
||||||
mediantime: 1542131305,
|
|
||||||
verificationprogress: 0.9999968911571566,
|
|
||||||
chainwork: '00000000000000000000000000000000000000000000003f0443b0b5f02ce255',
|
|
||||||
pruned: false,
|
|
||||||
softforks: [
|
|
||||||
{ id: 'bip34', version: 2, reject: { status: true } },
|
|
||||||
{ id: 'bip66', version: 3, reject: { status: true } },
|
|
||||||
{ id: 'bip65', version: 4, reject: { status: true } }
|
|
||||||
],
|
|
||||||
bip9_softforks: {
|
|
||||||
csv: {
|
|
||||||
status: 'active',
|
|
||||||
startTime: 1456790400,
|
|
||||||
timeout: 1493596800,
|
|
||||||
since: 770112
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockChainTips = [
|
|
||||||
{
|
|
||||||
height: 1267696,
|
|
||||||
hash: '000000000000035bfc43a642ebbff8cfc1e88b1d564de8b0a6c7e2797eeafb21',
|
|
||||||
branchlen: 0,
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1267581,
|
|
||||||
hash: '000000000001bd12e4124207682563135b21353ca3087bc6b0c409f7f9e1da91',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1267375,
|
|
||||||
hash: '00000000702036979df70236bcc45dfc72d43d5d0e6834007afa1fa627e49587',
|
|
||||||
branchlen: 1036,
|
|
||||||
status: 'headers-only'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266979,
|
|
||||||
hash: '00000000e851abbde2174ccdc5c2508b909f81f23c4b7abeac864eee1a4891c7',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266973,
|
|
||||||
hash: '000000007e1324d2900ed70947f89ab8fd4a267e87c9244c333215659a7370d5',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266967,
|
|
||||||
hash: '000000000f37f12843d4800f50ec4de44dce0432e4d448242d43ff04a7a2948d',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266963,
|
|
||||||
hash: '00000000ae11ec99e88898cbd72567cb4cb79e7fc13243357bef79632769deb4',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266962,
|
|
||||||
hash: '000000004f97851259b4460a049e44fa5bd4beadee04a85bf81bd2e90c4f24f9',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266950,
|
|
||||||
hash: '00000000d12a0e4a827f73ecaadb563df7e99817294939f2be8e943079dc60b2',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266944,
|
|
||||||
hash: '00000000612d16af273217818e8e7ac5014a19f68ed04e1bc897b1be0a9c744f',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266941,
|
|
||||||
hash: '0000000000002b91cdb15cacf3368da2c9ffce511d64092193b507dad75f4a27',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266896,
|
|
||||||
hash: '000000000002745f67a66617b1f9f65c2dfb4c4b8a9ed9b1320b98d3ca46cbe5',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266481,
|
|
||||||
hash: '0000000000036babee3ba7654cddb29a9fe5e85c9fbeb44bb53d5ffb694b9670',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266452,
|
|
||||||
hash: '00000000502c3ab1490e0839780a4b441bbe21507c454545941e6adcde73c2ff',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266336,
|
|
||||||
hash: '000000001c9a0b7cd9eb2210f69d5a9a3f4546ba3707deb40993d729190582d7',
|
|
||||||
branchlen: 10,
|
|
||||||
status: 'headers-only'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1266097,
|
|
||||||
hash: '000000000028ac0fbd18afe6f001f70cb7549bd2b3082d754dde88ee236368a2',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1265522,
|
|
||||||
hash: '00000000000000c6df7a2063030e6ce51399e470cc46e4bf6d67aa3f0feca98d',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1265470,
|
|
||||||
hash: '0000000000168e6442a595a02340fccf8dc2b0e8912c632d39e1f870685f6d7c',
|
|
||||||
branchlen: 2,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1265275,
|
|
||||||
hash: '000000008bc2b37ae6af1b886b52e7e1c1122c12badc4ae6c13df64f789267cf',
|
|
||||||
branchlen: 11027,
|
|
||||||
status: 'headers-only'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1265257,
|
|
||||||
hash: '00000000005bc3e9973d7273211eab02ae02549c7ebae48764c0bd648d4d7bbe',
|
|
||||||
branchlen: 114,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1265254,
|
|
||||||
hash: '0000000000e5b909d3541857315e9ee103b042d9ba661f25cc3e8fb25a96c8ce',
|
|
||||||
branchlen: 111,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1265245,
|
|
||||||
hash: '000000000066eb8832f6f990baceb379536f437216e22475a6c1b9133d250cb8',
|
|
||||||
branchlen: 102,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1264455,
|
|
||||||
hash: '00000000000001befc1bb17a23208d0b77b2449e37093ce8fbb5e346b19cafcc',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-headers'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1264373,
|
|
||||||
hash: '00000000000001ed3747e04fb95054cac0358fb4d8009d1999fc3ea34413e84a',
|
|
||||||
branchlen: 175,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1263912,
|
|
||||||
hash: '00000000000004635aecbcb97edcd6f5396483338838cb40da804eb354c68bde',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1262906,
|
|
||||||
hash: '00000000b31643d92a3e7c38e9755844fc8607e3f1055580a8ff28854e8a8ece',
|
|
||||||
branchlen: 1,
|
|
||||||
status: 'valid-fork'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1255749,
|
|
||||||
hash: '000000001dde5b015a99137f4cba87871370f4b6fcfbcc8b126547e6c33177da',
|
|
||||||
branchlen: 129,
|
|
||||||
status: 'headers-only'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 1188789,
|
|
||||||
hash: '00000000af942ce4eb60b3213cbcb7c98a7330f1f8f1adb4b6376f5e822e15b2',
|
|
||||||
branchlen: 92,
|
|
||||||
status: 'headers-only'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockMempoolInfo = {
|
|
||||||
size: 87,
|
|
||||||
bytes: 16816,
|
|
||||||
usage: 66408,
|
|
||||||
maxmempool: 300000000,
|
|
||||||
mempoolminfee: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockRawMempool = [
|
|
||||||
'db045bc3bd1088fa91f5ebb05c35cb9e2a91a22377f79b465cc6920b9893123c',
|
|
||||||
'6b3df7febf2b9834f1409155f88b866dd516b36376eae00e2b455df82e290405'
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockBlockHeaderConcise =
|
|
||||||
'000000202ed2723e7590e2b937f6821a99d6764cb8799bf30f8e300000000000000000001d311c02df9a1e3f57b8dbdcf97ec8dbc3109a26779724c63e560b29ad9ea501e2af955d286403183049e39c'
|
|
||||||
|
|
||||||
const mockBlockHeader = {
|
|
||||||
hash: '00000000000008c3679777df34f1a09565f98b2400a05b7c8da72525fdca3900',
|
|
||||||
confirmations: 7,
|
|
||||||
height: 1272859,
|
|
||||||
version: 2147418112,
|
|
||||||
versionHex: '7fff0000',
|
|
||||||
merkleroot:
|
|
||||||
'846f45604ec6cde2528933305dba80971e9ff0874f74d2811c7c690b2c32706d',
|
|
||||||
time: 1544207139,
|
|
||||||
mediantime: 1544202884,
|
|
||||||
nonce: 641041470,
|
|
||||||
bits: '1a09faea',
|
|
||||||
difficulty: 1681035.704111868,
|
|
||||||
chainwork: '00000000000000000000000000000000000000000000003fc4752e608403be04',
|
|
||||||
previousblockhash:
|
|
||||||
'0000000000003e467ebcb7e906645676c2f71b9ac520d6508bea45789b7c217d',
|
|
||||||
nextblockhash:
|
|
||||||
'00000000000006899041cdfd6c0b73a97730c362346dde479b77414ad7f25ace',
|
|
||||||
nTx: 'some value'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTxOut = {
|
|
||||||
bestblock: '00000000003a7f19730dd9f172d7466658a5c8833dd03ed8f4ba36e3d857b5e7',
|
|
||||||
confirmations: 10,
|
|
||||||
value: 0.0001,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 0ee020c07f39526ac5505c54fa1ab98490979b83 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a9140ee020c07f39526ac5505c54fa1ab98490979b8388ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: ['bchtest:qq8wqgxq0uu4y6k92pw9f7s6hxzfp9umsvtg39pzqf']
|
|
||||||
},
|
|
||||||
coinbase: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTxOutProof =
|
|
||||||
'000000200798039affceb7a381e15dc62443c286efe7cae852393b656807000000000000cf9a7d6c654fd1c4558229a619a550ff749b373fa0f0027eed68b1abf6175d5c07c50f5cffff001d0452403b34000000070d0765da76980d542b5670d27ccda9e717073270f8921ad017deb7ee83fe6c4c22830a48cc8392197f1742a3d81805bd2aa4eb7469f38c74f674429d682cb87efcff1713fd0c388a50f0d11adf05363397ef79d16cdb786d47c941c2cb89f2588f42aced9c05f4203b4440616a9e4266ec47909b9580a643f402b5d4e82cc3cddd73df05a33abf6af3975d973d3033d10c4168a73d58d01f685a96d5a2519cd0de9c77faf3f8725ddf155cbdc8ab8102f173e2d0a0d74767f3bff22f588158d6d5bc71c079c7659cc864819c43877635007650502eb4060fcc9feec3280a41d602ad0a'
|
|
||||||
|
|
||||||
const mockAncestors = [
|
|
||||||
'070504de968fba8a23a1362fd7fc0546a420663f2bcc18099d894a41ecf9cce5',
|
|
||||||
'20af6b7fcd21a3f38bb65d32fe2d66fb1ae554ed9c00eaf1b17d7e81362381a9',
|
|
||||||
'227e08663a812cf44d91106b09fc8493fe1a2b7c146f30ca09ebd86c9b84b2a5',
|
|
||||||
'228833be79d4a833165ea199be160efcaeb9325e71e9455f424b27abc9688ba8',
|
|
||||||
'2de27df02001e7243435cef6d41ea2312b94a5964912eb21466b936852c0b822',
|
|
||||||
'2ef76ae690d6f6c9559e20358a75a0ce02250b9b8e80e0aa6f782576239f292a',
|
|
||||||
'3e314678f52d52694726e6da88b94c9f0ea086d2f9af297b01d014b62247839f',
|
|
||||||
'4773f840d5255f16c0e5effce48316d8b8824a1d3ddd5da275c5fd99e7b3ee42',
|
|
||||||
'4804b97112da07e2c187516a915dd4cbd4a3814f3c900b8d08eb1d508ab0ac47',
|
|
||||||
'4ca69f31081a256cb4711315336fe4358d8f04b67792ca5ee17b5f4089a2446a',
|
|
||||||
'6ac17dce10ae3a3d1d8e6f42546ad9a4bb981e62cba8d48bab03e01b9ea38ae3',
|
|
||||||
'6ce71182ef341c481a90d8b081a99d9788bdd91a917f9a6594fdfad2c2024a14',
|
|
||||||
'8413343ac973fb0968ae1809faa8f0ddf18252975481daa683e88b04afa66ccd',
|
|
||||||
'9430300f3d7ecdabe39671b6fd0e9c36f763f01a202e619c070554ddc5e8713e',
|
|
||||||
'94f82b5a2cd1647d8168807ccfdebe41931ea1ab789a050d615f08000a63b624',
|
|
||||||
'9848db6a7d2835002ca7e1097a685d75eb629f44df94007674635b45d0e97199',
|
|
||||||
'98d809993353df6c4d55fda5e7b0ed2e383601cef1180a9aa9c87e67e8f4dd94',
|
|
||||||
'9f5513a35ae8fad3dd061865a97d8127c914e3464527641c089993c06d092535',
|
|
||||||
'a7201ac90abd2aa42112faa809181c45047b6988a2f649a70a1d5bfb95f46884',
|
|
||||||
'a9f1284710c655357412a062b01efaeb174820613784ccbfe44b33b86a8bdbc4',
|
|
||||||
'b814f44c9c8ee24a218b9f295a8c1831106b70c15ffe8aeb7534e10d99935fd0',
|
|
||||||
'e130ccdcc94dc5eaa4ae37fa50e3ee96283b0e157616bc6e28454b64bae34f48',
|
|
||||||
'e68dad4a7292105cfa84fcaef7f99e5d4f2ece9613ca625d4d2ebf61efa84118',
|
|
||||||
'fe94caf5da672be3772d2304a6272eb8bc3d3f5cb4a886f39b7981e1485cf74b'
|
|
||||||
]
|
|
||||||
const mockBlockInfo = {
|
|
||||||
verbosity0: '20000000',
|
|
||||||
verbosity1: {
|
|
||||||
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
|
||||||
confirmations: 1,
|
|
||||||
size: 3725,
|
|
||||||
strippedsize: 3725,
|
|
||||||
weight: 3725,
|
|
||||||
height: 6725,
|
|
||||||
version: 1,
|
|
||||||
versionHex: '00000000',
|
|
||||||
merkleroot: 'xxxx',
|
|
||||||
tx: [
|
|
||||||
'2afb8264508e2bf3e5288ccad01ed2ab766745b6b9747666b519d59212012c01',
|
|
||||||
'18f40b1ae56bba3fa1934b737fbe46ed8d5ca40fa9aed95073eeb5a119530cd3',
|
|
||||||
'349720d878547752607a69eb19e330592fee271fb5376cdfd811bee423558ed8',
|
|
||||||
'35571c80e7d0e9247b467454ef147d1d5833c775bc2d4164b1bebd4c1f69164f',
|
|
||||||
'480937e8efacdafeeb97d401ff0dd9ea8e8ddb27244cefa67a03621315bdb0e6',
|
|
||||||
'523327469d0b90c0de9a905c2fe6e227278fc5b55b9d9911ca151e1c26647065',
|
|
||||||
'61de4af971d94dbc741762f21dcef08b74b62863a56a1cb3496becdd8d47a858',
|
|
||||||
'69f70a288403b5bba23030ccd05d2e5cb00394620fdae3b050cff9432cc590ce',
|
|
||||||
'94472a90fbdba2eb2cba68308415181faedf3585c66b89efcbdb927a0c10ba23',
|
|
||||||
'bc6f781f9e2f2df460f89995c5e1c7224e48ccbe1aa2a575961f3f5330259864',
|
|
||||||
'f2d945a79bec5454a9ab4d570a150d81124daa308f1c81106a977d6413476944'
|
|
||||||
],
|
|
||||||
time: 111,
|
|
||||||
mediantime: 111,
|
|
||||||
nonce: 111,
|
|
||||||
bits: '1d00ffff',
|
|
||||||
difficulty: 99.999,
|
|
||||||
chainwork: 'xxxx',
|
|
||||||
nTx: 1,
|
|
||||||
previousblockhash:
|
|
||||||
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
|
||||||
nextblockhash:
|
|
||||||
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
|
||||||
},
|
|
||||||
verbosity2: {
|
|
||||||
hash: '00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
|
||||||
confirmations: 1,
|
|
||||||
size: 3725,
|
|
||||||
strippedsize: 3725,
|
|
||||||
weight: 3725,
|
|
||||||
height: 6725,
|
|
||||||
version: 1,
|
|
||||||
versionHex: '00000000',
|
|
||||||
merkleroot: 'xxxx',
|
|
||||||
tx: [
|
|
||||||
{
|
|
||||||
hex:
|
|
||||||
'01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000',
|
|
||||||
txid:
|
|
||||||
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
|
||||||
hash:
|
|
||||||
'0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098',
|
|
||||||
size: 134,
|
|
||||||
version: 1,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [],
|
|
||||||
vout: [],
|
|
||||||
blockhash:
|
|
||||||
'00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048',
|
|
||||||
confirmations: 581882,
|
|
||||||
time: 1231469665,
|
|
||||||
blocktime: 1231469665
|
|
||||||
}
|
|
||||||
],
|
|
||||||
time: 111,
|
|
||||||
mediantime: 111,
|
|
||||||
nonce: 111,
|
|
||||||
bits: '1d00ffff',
|
|
||||||
difficulty: 99.999,
|
|
||||||
chainwork: 'xxxx',
|
|
||||||
nTx: 1,
|
|
||||||
previousblockhash:
|
|
||||||
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09',
|
|
||||||
nextblockhash:
|
|
||||||
'00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports = {
|
|
||||||
mockBlockHash,
|
|
||||||
mockBlockchainInfo,
|
|
||||||
mockChainTips,
|
|
||||||
mockMempoolInfo,
|
|
||||||
mockRawMempool,
|
|
||||||
mockBlockHeaderConcise,
|
|
||||||
mockBlockHeader,
|
|
||||||
mockTxOut,
|
|
||||||
mockTxOutProof,
|
|
||||||
mockAncestors,
|
|
||||||
mockBlockInfo
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockGetInfo = {
|
|
||||||
version: 170200,
|
|
||||||
protocolversion: 70015,
|
|
||||||
walletversion: 160300,
|
|
||||||
balance: 0,
|
|
||||||
blocks: 1266726,
|
|
||||||
timeoffset: 0,
|
|
||||||
connections: 8,
|
|
||||||
proxy: '',
|
|
||||||
difficulty: 1,
|
|
||||||
testnet: true,
|
|
||||||
keypoololdest: 1536331195,
|
|
||||||
keypoolsize: 2000,
|
|
||||||
paytxfee: 0,
|
|
||||||
relayfee: 0.00001,
|
|
||||||
errors: 'Warning: unknown new rules activated (versionbit 28)'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockGetNetworkInfo = {
|
|
||||||
version: 190700,
|
|
||||||
subversion: '/Bitcoin ABC:0.19.7(EB32.0)/',
|
|
||||||
protocolversion: 70015,
|
|
||||||
localservices: '0000000000000425',
|
|
||||||
localrelay: true,
|
|
||||||
timeoffset: 0,
|
|
||||||
networkactive: true,
|
|
||||||
connections: 23,
|
|
||||||
networks: [{}, {}, {}],
|
|
||||||
relayfee: 0.00001,
|
|
||||||
excessutxocharge: 0,
|
|
||||||
localaddresses: [],
|
|
||||||
warnings:
|
|
||||||
"Warning: Unknown block versions being mined! It's possible unknown rules are in effect"
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockGetInfo,
|
|
||||||
mockGetNetworkInfo
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
Mocking data for electrumx unit tests
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const utxos = [
|
|
||||||
{
|
|
||||||
height: 604392,
|
|
||||||
tx_hash: '7774e449c5a3065144cefbc4c0c21e6b69c987f095856778ef9f45ddd8ae1a41',
|
|
||||||
tx_pos: 0,
|
|
||||||
value: 1000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 630834,
|
|
||||||
tx_hash: '4fe60a51e0d8f5134bfd8e5f872d6e502d7f01b28a6afebb27f4438a4f638d53',
|
|
||||||
tx_pos: 0,
|
|
||||||
value: 6000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const balance = {
|
|
||||||
confirmed: 7000,
|
|
||||||
unconfirmed: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const txHistory = [
|
|
||||||
{
|
|
||||||
height: 601861,
|
|
||||||
tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mempool = [
|
|
||||||
{
|
|
||||||
tx_hash: '45381031132c57b2ff1cbe8d8d3920cf9ed25efd9a0beb764bdb2f24c7d1c7e3',
|
|
||||||
height: 0,
|
|
||||||
fee: 24310
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const txDetails = {
|
|
||||||
blockhash: '0000000000000000002aaf94953da3b487317508ebd1003a1d75d6d6ec2e75cc',
|
|
||||||
blocktime: 1578327094,
|
|
||||||
confirmations: 31861,
|
|
||||||
hash: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
|
|
||||||
hex: '020000000265d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667010000006441dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309ffffffff65d13ef402840c8a51f39779afb7ae4d49e4b0a3c24a3d0e7742038f2c679667000000006441347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954ffffffff035ac355000000000017a914189ce02e332548f4804bac65cba68202c9dbf822878dfd0800000000001976a914285bb350881b21ac89724c6fb6dc914d096cd53b88acf9ef3100000000001976a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac00000000',
|
|
||||||
locktime: 0,
|
|
||||||
size: 392,
|
|
||||||
time: 1578327094,
|
|
||||||
txid: '4db095f34d632a4daf942142c291f1f2abb5ba2e1ccac919d85bdc2f671fb251',
|
|
||||||
version: 2,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
scriptSig: {
|
|
||||||
asm: 'dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e[ALL|FORKID] 020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309',
|
|
||||||
hex: '41dd1dd72770cadede1a7fd0363574846c48468a398ddfa41a9677c74cac8d2652b682743725a3b08c6c2021a629011e11a264d9036e9d5311e35b5f4937ca7b4e4121020797d8fd4d2fa6fd7cdeabe2526bfea2b90525d6e8ad506ec4ee3c53885aa309'
|
|
||||||
},
|
|
||||||
sequence: 4294967295,
|
|
||||||
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
|
|
||||||
vout: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scriptSig: {
|
|
||||||
asm: '347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f77[ALL|FORKID] 028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954',
|
|
||||||
hex: '41347d7f218c11c04487c1ad8baac28928fb10e5054cd4494b94d078cfa04ccf68e064fb188127ff656c0b98e9ce87f036d183925d0d0860605877d61e90375f774121028a53f95eb631b460854fc836b2e5d31cad16364b4dc3d970babfbdcc3f2e4954'
|
|
||||||
},
|
|
||||||
sequence: 4294967295,
|
|
||||||
txid: '6796672c8f0342770e3d4ac2a3b0e4494daeb7af7997f3518a0c8402f43ed165',
|
|
||||||
vout: 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
addresses: ['bitcoincash: pqvfecpwxvj53ayqfwkxtjaxsgpvnklcyg8xewk9hl'],
|
|
||||||
asm: 'OP_HASH160 189ce02e332548f4804bac65cba68202c9dbf822 OP_EQUAL',
|
|
||||||
hex: 'a914189ce02e332548f4804bac65cba68202c9dbf82287',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'scripthash'
|
|
||||||
},
|
|
||||||
value: 0.0562057
|
|
||||||
},
|
|
||||||
{
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
addresses: ['bitcoincash: qq59hv6s3qdjrtyfwfxxldkuj9xsjmx48vrz882knz'],
|
|
||||||
asm: 'OP_DUP OP_HASH160 285bb350881b21ac89724c6fb6dc914d096cd53b OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a914285bb350881b21ac89724c6fb6dc914d096cd53b88ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
value: 0.00589197
|
|
||||||
},
|
|
||||||
{
|
|
||||||
n: 2,
|
|
||||||
scriptPubKey: {
|
|
||||||
addresses: ['bitcoincash: qpzlruwy4xu5rxjs3z37nsj29y7h59gwvsu4ddp0u4'],
|
|
||||||
asm: 'OP_DUP OP_HASH160 45f1f1c4a9b9419a5088a3e9c24a293d7a150e64 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a91445f1f1c4a9b9419a5088a3e9c24a293d7a150e6488ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash'
|
|
||||||
},
|
|
||||||
value: 0.03272697
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockHeaders = [
|
|
||||||
'010000008b52bbd72c2f49569059f559c1b1794de5192e4f7d6d2b03c7482bad0000000083e4f8a9d502ed0c419075c1abb5d56f878a2e9079e5612bfb76a2dc37d9c42741dd6849ffff001d2b909dd6',
|
|
||||||
'01000000f528fac1bcb685d0cd6c792320af0300a5ce15d687c7149548904e31000000004e8985a786d864f21e9cbb7cbdf4bc9265fe681b7a0893ac55a8e919ce035c2f85de6849ffff001d385ccb7c'
|
|
||||||
]
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
utxos,
|
|
||||||
balance,
|
|
||||||
txHistory,
|
|
||||||
mempool,
|
|
||||||
txDetails,
|
|
||||||
blockHeaders
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
/*
|
|
||||||
Mock data for the encryption test library
|
|
||||||
*/
|
|
||||||
|
|
||||||
const mockNoSendTx = {
|
|
||||||
txid: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
|
|
||||||
hash: 'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023',
|
|
||||||
version: 2,
|
|
||||||
size: 226,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: '681cc1d392975a7e78cbd490b8a940d7eb029c554f2cee332e3a7119d77fb189',
|
|
||||||
vout: 1,
|
|
||||||
scriptSig: {
|
|
||||||
asm:
|
|
||||||
'30450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f[ALL|FORKID] 02bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7',
|
|
||||||
hex:
|
|
||||||
'4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7'
|
|
||||||
},
|
|
||||||
sequence: 4294967295
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: 0.000006,
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 3c79ba54317c4be9cbfa5aaf357f74b97f3d9696 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: ['bitcoincash:qq78nwj5x97yh6wtlfd27dtlwjuh70vkjc59h8tgtg']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0.00003664,
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 58502a73888e0a386fc69b44850af4b2a86ac9a6 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: ['bitcoincash:qpv9q2nn3z8q5wr0c6d5fpg27je2s6kf5cavp4ney0']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
hex:
|
|
||||||
'020000000189b17fd719713a2e33ee2c4f559c02ebd740a9b890d4cb787e5a9792d3c11c68010000006b4830450221008a7c86ce3a9f5765573440143a00631bb5afa9a58b24674a7fa794bb11ffffc30220397dd4c672c3e0dbe0c4eb6390b5727c5fdc3a42cf3ae275ac13b44d27bb234f412102bc9fc26ae2bd9a9a14e9baeda4b4fdd95a00bcbf375732c647488b3ce82a2fd7ffffffff0258020000000000001976a9143c79ba54317c4be9cbfa5aaf357f74b97f3d969688ac500e0000000000001976a91458502a73888e0a386fc69b44850af4b2a86ac9a688ac00000000'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockFulcrumTxHistory = {
|
|
||||||
success: true,
|
|
||||||
transactions: [
|
|
||||||
{
|
|
||||||
transactions: [
|
|
||||||
{
|
|
||||||
height: 511463,
|
|
||||||
tx_hash:
|
|
||||||
'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 511464,
|
|
||||||
tx_hash:
|
|
||||||
'7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 513373,
|
|
||||||
tx_hash:
|
|
||||||
'6960255abe64893073921e96bf3c053c82686e0fc22a565494fbe2a31e766975'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 513373,
|
|
||||||
tx_hash:
|
|
||||||
'9ea667bcfc9cd337bd6c5583d8094c1b1942bd2015d95b54189deac5070eeff0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 560481,
|
|
||||||
tx_hash:
|
|
||||||
'ecc1b51bac767880382bf3190ff17abf78d0936843a022a943d871116ed50368'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 560615,
|
|
||||||
tx_hash:
|
|
||||||
'b3792d28377b975560e1b6f09e48aeff8438d4c6969ca578bd406393bd50bd7d'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 561568,
|
|
||||||
tx_hash:
|
|
||||||
'8bc2134c7e48e56e1769b3d7c4c1e3a0acc68e1e58160eee6fa67f3208c07262'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 561569,
|
|
||||||
tx_hash:
|
|
||||||
'ceb0cab0e37b59caf3ca29e1a698d19ff47f2827dd09cb2f3b91b9100b1dad1c'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 561572,
|
|
||||||
tx_hash:
|
|
||||||
'0f9b49cafeb9ae1d741cdb12137c92816aa8470944c270a78ba2e610bd59190d'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 561582,
|
|
||||||
tx_hash:
|
|
||||||
'e4a0ac48ff3f42fc342717a2a3d34248e5e85bae79d59bd20e1b60e61b1c500f'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 562106,
|
|
||||||
tx_hash:
|
|
||||||
'1afcc63b244182647909539ebe3f4a44b8ea4120a95edb8d9eebe5347b9491bb'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
height: 562106,
|
|
||||||
tx_hash:
|
|
||||||
'c42f8f16d3baa2ee343ea89ef110dfe094992379d08edd30887b8ca7ee671c9a'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTxDetails2 = {
|
|
||||||
txid: '7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
|
|
||||||
hash: '7e9aa7a74de2b30200a2d6fc748ff35a0c753221444194f720bb7f61ef1d9153',
|
|
||||||
version: 1,
|
|
||||||
size: 221,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: 'eff00a9538487ff44243c75fb13de19b5783454c42c81b9aff9afbfd09cbaec3',
|
|
||||||
vout: 0,
|
|
||||||
scriptSig: {
|
|
||||||
asm:
|
|
||||||
'30440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a[ALL|FORKID] 044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c',
|
|
||||||
hex:
|
|
||||||
'4730440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2c'
|
|
||||||
},
|
|
||||||
sequence: 4294967295
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: 0.01266023,
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm: 'OP_HASH160 91bacd6785e3f8c78d5f29b4020b94c753444130 OP_EQUAL',
|
|
||||||
hex: 'a91491bacd6785e3f8c78d5f29b4020b94c75344413087',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'scripthash',
|
|
||||||
addresses: ['bitcoincash:pzgm4nt8sh3l33udtu5mgqstjnr4x3zpxqrrrndnw5']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
hex:
|
|
||||||
'0100000001c3aecb09fdfb9aff9a1bc8424c4583579be13db15fc74342f47f4838950af0ef000000008a4730440220150e7c9b646fe1f7f576456c9808c89a710e8fbb753e9f5ca6ed2d39a0b69ba602202212968163d8dfe35eec5975fce38440082029cb2691a3e0ad1714f9c412c47a4141044eb40b025df18409f2a5197b010dd62a9e65d9a74e415e5b10367721a9c4baa7ebfee22d14b8ece1c9bd70c0d9e5e8b00b61b81b88a1b5ce6f24eac6b8a34b2cffffffff01675113000000000017a91491bacd6785e3f8c78d5f29b4020b94c7534441308700000000',
|
|
||||||
blockhash: '000000000000000001aab6818b4ad0379a3f7a13940d9b3d4838b3dea1d0a083',
|
|
||||||
confirmations: 149222,
|
|
||||||
time: 1515088493,
|
|
||||||
blocktime: 1515088493
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockFulcrumNoTxHistory = {
|
|
||||||
success: true,
|
|
||||||
transactions: [
|
|
||||||
{
|
|
||||||
transactions: [],
|
|
||||||
address: 'bitcoincash:qrgqqkky28jdkv3w0ctrah0mz3jcsnsklc34gtukrh'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockFulcrumNoSendBalance = {
|
|
||||||
success: true,
|
|
||||||
transactions: [
|
|
||||||
{
|
|
||||||
transactions: [
|
|
||||||
{
|
|
||||||
height: 633578,
|
|
||||||
tx_hash:
|
|
||||||
'a3b62cd4f4c56ba52139179db14bffd4ab22a2e077f3c62bd5cf0541bfcaf023'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockNoSendTx,
|
|
||||||
mockFulcrumTxHistory,
|
|
||||||
mockTxDetails2,
|
|
||||||
mockFulcrumNoTxHistory,
|
|
||||||
mockFulcrumNoSendBalance
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
/*
|
|
||||||
Contains mocks of Express req and res objects.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
// Inspect JS Objects.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = {
|
|
||||||
showHidden: true,
|
|
||||||
colors: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock for res.send()
|
|
||||||
function fakeSend (arg) {
|
|
||||||
// console.log(`res.send: ${util.inspect(arg)}`);
|
|
||||||
mockRes.output = arg
|
|
||||||
return arg
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock for res.json()
|
|
||||||
function fakeJson (arg) {
|
|
||||||
// console.log(`res.json: ${util.inspect(arg)}`);
|
|
||||||
mockRes.output = arg
|
|
||||||
return arg
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock for res.setStatus(num)
|
|
||||||
const setStatusCode = arg => {
|
|
||||||
mockRes.statusCode = arg
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockReq = {
|
|
||||||
accepts: sinon.stub().returns({}),
|
|
||||||
acceptsCharsets: sinon.stub().returns({}),
|
|
||||||
acceptsEncodings: sinon.stub().returns({}),
|
|
||||||
acceptsLanguages: sinon.stub().returns({}),
|
|
||||||
body: {},
|
|
||||||
flash: sinon.stub().returns({}),
|
|
||||||
get: sinon.stub().returns({}),
|
|
||||||
is: sinon.stub().returns({}),
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
session: {},
|
|
||||||
locals: {},
|
|
||||||
headers: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockRes = {
|
|
||||||
append: sinon.stub().returns({}),
|
|
||||||
attachement: sinon.stub().returns({}),
|
|
||||||
clearCookie: sinon.stub().returns({}),
|
|
||||||
cookie: sinon.stub().returns({}),
|
|
||||||
download: sinon.stub().returns({}),
|
|
||||||
end: sinon.stub().returns({}),
|
|
||||||
get: sinon.stub().returns({}),
|
|
||||||
headersSent: sinon.stub().returns({}),
|
|
||||||
json: sinon.stub().callsFake(fakeJson),
|
|
||||||
jsonp: sinon.stub().returns({}),
|
|
||||||
links: sinon.stub().returns({}),
|
|
||||||
locals: {},
|
|
||||||
location: sinon.stub().returns({}),
|
|
||||||
output: null, // Used for retrieving output data.
|
|
||||||
redirect: sinon.stub().returns({}),
|
|
||||||
render: sinon.stub().returns({}),
|
|
||||||
send: sinon.stub().callsFake(fakeSend),
|
|
||||||
sendFile: sinon.stub().returns({}),
|
|
||||||
sendStatus: sinon.stub().returns({}),
|
|
||||||
set: sinon.stub().returns({}),
|
|
||||||
status: sinon.stub().callsFake(setStatusCode),
|
|
||||||
statusCode: null, // Default value before calling stats();
|
|
||||||
type: sinon.stub().returns({}),
|
|
||||||
vary: sinon.stub().returns({}),
|
|
||||||
write: sinon.stub().returns({}),
|
|
||||||
setHeader: sinon.stub().returns({}),
|
|
||||||
format: sinon.stub().returns({})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dev-Note on Rate Limits: Since next() is mocked, I can call the Sinon untility
|
|
||||||
// functions on it, like called(), to see if this stub was called.
|
|
||||||
const mockNext = sinon.stub().returns()
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockReq,
|
|
||||||
mockRes,
|
|
||||||
mockNext
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockMiningInfo = {
|
|
||||||
blocks: 1270185,
|
|
||||||
currentblocksize: 0,
|
|
||||||
currentblocktx: 0,
|
|
||||||
difficulty: 1,
|
|
||||||
warnings:
|
|
||||||
"Warning: Unknown block versions being mined! It's possible unknown rules are in effect",
|
|
||||||
networkhashps: 517410290.9365583,
|
|
||||||
pooledtx: 5,
|
|
||||||
chain: 'test'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockMiningInfo
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockCoinbaseFeed = {
|
|
||||||
data: {
|
|
||||||
currency: 'BCH',
|
|
||||||
rates: {
|
|
||||||
AED: '918.11634',
|
|
||||||
AFN: '19208.6584998',
|
|
||||||
ALGO: '823.28722002635056355',
|
|
||||||
ALL: '26365.4673517',
|
|
||||||
AMD: '120379.9761886',
|
|
||||||
ANG: '448.6632494',
|
|
||||||
AOA: '162813.68075',
|
|
||||||
ARS: '19392.84240565',
|
|
||||||
ATOM: '45.3670932026499700525',
|
|
||||||
AUD: '354.4385981',
|
|
||||||
AWG: '449.91',
|
|
||||||
AZN: '425.539875',
|
|
||||||
BAL: '18.824215193276753072',
|
|
||||||
BAM: '415.45514235',
|
|
||||||
BAND: '40.7768732564399590085',
|
|
||||||
BAT: '1168.9711697428450012',
|
|
||||||
BBD: '499.9',
|
|
||||||
BCH: '1.0',
|
|
||||||
BDT: '21194.1523216',
|
|
||||||
BGN: '415.36691',
|
|
||||||
BHD: '94.2501462',
|
|
||||||
BIF: '483670.78174295',
|
|
||||||
BMD: '249.95',
|
|
||||||
BND: '339.227141',
|
|
||||||
BOB: '1725.8732563',
|
|
||||||
BRL: '1401.494645',
|
|
||||||
BSD: '249.95',
|
|
||||||
BSV: '1.567361207606150942625',
|
|
||||||
BTC: '0.021275',
|
|
||||||
BTN: '18323.91073475',
|
|
||||||
BWP: '2859.81217315',
|
|
||||||
BYN: '640.5913561',
|
|
||||||
BYR: '6405913.561',
|
|
||||||
BZD: '503.8107177',
|
|
||||||
CAD: '329.6765515',
|
|
||||||
CDF: '490377.5463717',
|
|
||||||
CGLD: '121.6805004503079241715',
|
|
||||||
CHF: '227.5939721',
|
|
||||||
CLF: '7.13432285',
|
|
||||||
CLP: '196860.63474705',
|
|
||||||
CNH: '1669.50978125',
|
|
||||||
CNY: '1670.21589',
|
|
||||||
COMP: '2.429765723728978241',
|
|
||||||
COP: '961722.29431455',
|
|
||||||
CRC: '150790.4338804',
|
|
||||||
CUC: '250.01273745',
|
|
||||||
CVE: '23620.275',
|
|
||||||
CZK: '5790.09175',
|
|
||||||
DAI: '247.842348666936355635',
|
|
||||||
DASH: '3.338230383973288798',
|
|
||||||
DJF: '44495.8985401',
|
|
||||||
DKK: '1580.608815',
|
|
||||||
DOP: '14597.4179324',
|
|
||||||
DZD: '32214.830745',
|
|
||||||
EEK: '3652.69206545',
|
|
||||||
EGP: '3924.839875',
|
|
||||||
EOS: '96.8423091824874092665',
|
|
||||||
ERN: '3749.25274945',
|
|
||||||
ETB: '9313.6508972',
|
|
||||||
ETC: '45.04821122825989111',
|
|
||||||
ETH: '0.658777328255340453815',
|
|
||||||
EUR: '212.33',
|
|
||||||
FJD: '534.9054975',
|
|
||||||
FKP: '193.01413945',
|
|
||||||
GBP: '193.115',
|
|
||||||
GEL: '807.3385',
|
|
||||||
GGP: '193.01413945',
|
|
||||||
GHS: '1453.70845015',
|
|
||||||
GIP: '193.01413945',
|
|
||||||
GMD: '12934.9125',
|
|
||||||
GNF: '2447970.79115335',
|
|
||||||
GTQ: '1944.576007',
|
|
||||||
GYD: '52260.2868518',
|
|
||||||
HKD: '1937.122498',
|
|
||||||
HNL: '6129.328889',
|
|
||||||
HRK: '1610.802775',
|
|
||||||
HTG: '15746.55080985',
|
|
||||||
HUF: '77512.6058777',
|
|
||||||
IDR: '3675351.03275',
|
|
||||||
ILS: '846.1232415',
|
|
||||||
IMP: '193.01413945',
|
|
||||||
INR: '18345.655135',
|
|
||||||
IQD: '298009.02802165',
|
|
||||||
ISK: '34745.5495',
|
|
||||||
JEP: '193.01413945',
|
|
||||||
JMD: '36409.6696094',
|
|
||||||
JOD: '177.21455',
|
|
||||||
JPY: '26360.601825',
|
|
||||||
KES: '27184.562',
|
|
||||||
KGS: '20308.0755724',
|
|
||||||
KHR: '1025880.7533059',
|
|
||||||
KMF: '105029.03124175',
|
|
||||||
KNC: '274.68542227594924615',
|
|
||||||
KRW: '284915.23080495',
|
|
||||||
KWD: '76.4532063',
|
|
||||||
KYD: '208.2943328',
|
|
||||||
KZT: '107014.42658325',
|
|
||||||
LAK: '2310183.71158185',
|
|
||||||
LBP: '378370.54450325',
|
|
||||||
LINK: '22.894634759817007489',
|
|
||||||
LKR: '46090.95971405',
|
|
||||||
LRC: '1457.8594342373869446',
|
|
||||||
LRD: '48852.72175115',
|
|
||||||
LSL: '4122.3008749',
|
|
||||||
LTC: '5.2062070401999589123',
|
|
||||||
LTL: '806.05000775',
|
|
||||||
LVL: '164.03243695',
|
|
||||||
LYD: '341.5926678',
|
|
||||||
MAD: '2298.47346335',
|
|
||||||
MDL: '4228.5806147',
|
|
||||||
MGA: '982179.7557538',
|
|
||||||
MKD: '13087.73767885',
|
|
||||||
MKR: '0.43764476902003812305',
|
|
||||||
MMK: '322679.7621378',
|
|
||||||
MNT: '709349.8756452',
|
|
||||||
MOP: '1995.2583685',
|
|
||||||
MRO: '89232.15',
|
|
||||||
MTL: '170.9003131',
|
|
||||||
MUR: '9974.21650765',
|
|
||||||
MVR: '3849.23',
|
|
||||||
MWK: '188665.67006785',
|
|
||||||
MXN: '5298.265135',
|
|
||||||
MYR: '1036.517655',
|
|
||||||
MZN: '18229.3548997',
|
|
||||||
NAD: '4134.173',
|
|
||||||
NGN: '95481.03472305',
|
|
||||||
NIO: '8698.6554209',
|
|
||||||
NMR: '8.624556609112112958',
|
|
||||||
NOK: '2334.203066',
|
|
||||||
NPR: '29318.1806909',
|
|
||||||
NZD: '379.2351378',
|
|
||||||
OMG: '74.464123456422320125',
|
|
||||||
OMR: '96.23549905',
|
|
||||||
OXT: '1033.27821413807369045',
|
|
||||||
PAB: '249.95',
|
|
||||||
PEN: '896.6071427',
|
|
||||||
PGK: '874.90073485',
|
|
||||||
PHP: '12136.85639315',
|
|
||||||
PKR: '40613.9575836',
|
|
||||||
PLN: '971.640633',
|
|
||||||
PYG: '1756126.21474795',
|
|
||||||
QAR: '910.1304375',
|
|
||||||
REN: '769.6689761354887733',
|
|
||||||
REP: '18.2912550311013566725',
|
|
||||||
REPV2: '18.1582473904586151205',
|
|
||||||
RON: '1035.54285',
|
|
||||||
RSD: '24943.76025',
|
|
||||||
RUB: '19402.093805',
|
|
||||||
RWF: '244237.3512583',
|
|
||||||
SAR: '937.65668115',
|
|
||||||
SBD: '2019.805958',
|
|
||||||
SCR: '4576.6749819',
|
|
||||||
SEK: '2207.698372',
|
|
||||||
SGD: '339.57582125',
|
|
||||||
SHP: '193.01413945',
|
|
||||||
SLL: '2490751.74625075',
|
|
||||||
SOS: '145044.39506805',
|
|
||||||
SRD: '3537.7923',
|
|
||||||
SSP: '32558.487',
|
|
||||||
STD: '5250063.0293496',
|
|
||||||
SVC: '2187.04525345',
|
|
||||||
SZL: '4118.75383445',
|
|
||||||
THB: '7800.314625',
|
|
||||||
TJS: '2579.48624955',
|
|
||||||
TMT: '874.825',
|
|
||||||
TND: '688.2998125',
|
|
||||||
TOP: '579.953986',
|
|
||||||
TRY: '1969.70598',
|
|
||||||
TTD: '1696.3476626',
|
|
||||||
TWD: '7179.16412995',
|
|
||||||
TZS: '579879.84308155',
|
|
||||||
UAH: '7090.3476468',
|
|
||||||
UGX: '935325.80066935',
|
|
||||||
UMA: '29.276720351390922145',
|
|
||||||
UNI: '78.76161966283282792',
|
|
||||||
USD: '249.95',
|
|
||||||
USDC: '249.95',
|
|
||||||
UYU: '10724.30446005',
|
|
||||||
UZS: '2592145.88136715',
|
|
||||||
VEF: '62109486.17813795',
|
|
||||||
VES: '113073216.80555545',
|
|
||||||
VND: '5792476.0820382',
|
|
||||||
VUV: '28582.2184128',
|
|
||||||
WST: '655.5403657',
|
|
||||||
XAF: '139318.05656485',
|
|
||||||
XAG: '10.233577875',
|
|
||||||
XAU: '0.1313462255',
|
|
||||||
XCD: '675.5023725',
|
|
||||||
XDR: '177.0940741',
|
|
||||||
XLM: '2935.3219224332811736',
|
|
||||||
XOF: '139318.05656485',
|
|
||||||
XPD: '0.1065011955',
|
|
||||||
XPF: '25344.75028595',
|
|
||||||
XPT: '0.290981792',
|
|
||||||
XRP: '1016.6768354687817832',
|
|
||||||
XTZ: '114.010080507218287957',
|
|
||||||
YER: '62574.97275195',
|
|
||||||
YFI: '0.0181099566723181033515',
|
|
||||||
ZAR: '4130.57372',
|
|
||||||
ZEC: '3.88966697790227161905',
|
|
||||||
ZMK: '1313006.15998725',
|
|
||||||
ZMW: '5048.6210738',
|
|
||||||
ZRX: '642.804422350408735165',
|
|
||||||
ZWL: '80483.9'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockCoinexFeed = {
|
|
||||||
code: 0,
|
|
||||||
data: {
|
|
||||||
date: 1605649499848,
|
|
||||||
ticker: {
|
|
||||||
vol: '26717.39062407',
|
|
||||||
low: '10.4000',
|
|
||||||
open: '11.8000',
|
|
||||||
high: '19.0029',
|
|
||||||
last: '18.5000',
|
|
||||||
buy: '18.0100',
|
|
||||||
buy_amount: '200.00000000',
|
|
||||||
sell: '18.5000',
|
|
||||||
sell_amount: '47.89580474'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
message: 'OK'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockCoinbaseFeed,
|
|
||||||
mockCoinexFeed
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockDecodeRawTransaction = {
|
|
||||||
txid: 'a332237d82a2543af1b0e1ae3c8cea1610c290ebcaf084a7e9894a61de0be988',
|
|
||||||
hash: 'a332237d82a2543af1b0e1ae3c8cea1610c290ebcaf084a7e9894a61de0be988',
|
|
||||||
size: 226,
|
|
||||||
version: 2,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: '21cced645eab150585ed7ca7c96edebab5793cc0a3b3b286c42fd7d6d798b5b9',
|
|
||||||
vout: 1,
|
|
||||||
scriptSig: {
|
|
||||||
asm:
|
|
||||||
'3045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd41390[ALL|FORKID] 0360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413d',
|
|
||||||
hex:
|
|
||||||
'483045022100a7b1b08956abb8d6f322aa709d8583c8ea492ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab4667c57b2786f51c413d'
|
|
||||||
},
|
|
||||||
sequence: 4294967295
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: 0.0001,
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 eb4b180def88e3f5625b2d8ae2c098ff7d85f664 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: [Array]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0.09989752,
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 eb4b180def88e3f5625b2d8ae2c098ff7d85f664 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: [Array]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockDecodeScript = {
|
|
||||||
asm:
|
|
||||||
'0 0 -57 OP_NOP6 OP_LSHIFT OP_UNKNOWN OP_UNKNOWN OP_UNKNOWN c486b2b3a3c03c79b5bade6ec9a77ced850515ab5e64edcc21010000006b483045022100a7b1b08956abb8d6f322aa OP_2OVER OP_NUMEQUALVERIFY OP_OR OP_INVERT OP_UNKNOWN OP_UNKNOWN 2ba0585f1a6f4f9983520af74a5a0220411aee4a9a54effab617b0508c504c31681b15f9b187179b4874257badd4139041210360cfc66fdacb650bc4c83b4e351805181ee696b7d5ab 67c57b2786f51c413dffffffff0210270000000000001976a914eb4b180def88e3f5625b2d8ae2c098ff7d85f66488ac786e9800000000001976a914eb4b180def88e3f5625b [error]',
|
|
||||||
type: 'nonstandard',
|
|
||||||
p2sh: 'bchtest:pzy6dwfy6yf373w0dr05a6flfqksurjhwcl3awhdvm'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockRawTransactionConcise =
|
|
||||||
'02000000014e6b52500110b1c30315b85805fb274f0f4afceffc1589f889b27709e59e987d000000006a473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02d1778f950a0000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac00000000'
|
|
||||||
|
|
||||||
const mockRawTransactionVerbose = {
|
|
||||||
hex:
|
|
||||||
'02000000014e6b52500110b1c30315b85805fb274f0f4afceffc1589f889b27709e59e987d000000006a473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792ffffffff02d1778f950a0000001976a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac80969800000000001976a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac00000000',
|
|
||||||
txid: 'bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971',
|
|
||||||
hash: 'bd320377db7026a3dd5c7ec444596c0ee18fc25c4f34ee944adc03e432ce1971',
|
|
||||||
size: 225,
|
|
||||||
version: 2,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: '7d989ee50977b289f88915fceffc4a0f4f27fb0558b81503c3b1100150526b4e',
|
|
||||||
vout: 0,
|
|
||||||
scriptSig: {
|
|
||||||
asm:
|
|
||||||
'3044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7[ALL|FORKID] 03c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792',
|
|
||||||
hex:
|
|
||||||
'473044022052762770baa71c1a0b9544ad0f1ea343d32c22aa87c5f8397b6852f464c15b1e02201f4390745cb470e21e0e3c14229f39fef55ea0643a4c997f99d9f3501eae09b7412103c346eee77a77a8d3e073dacc0532ca7a5b9747bc06d88bf091cac9f4bc8bb792'
|
|
||||||
},
|
|
||||||
sequence: 4294967295
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: 454.58880465,
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 36d2f27bbd826a86db1e93618ce3de89ef331693 OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a91436d2f27bbd826a86db1e93618ce3de89ef33169388ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: ['bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 0.1,
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: {
|
|
||||||
asm:
|
|
||||||
'OP_DUP OP_HASH160 152ea3cd65f18cb8fa9146c84ea1a97af8f051de OP_EQUALVERIFY OP_CHECKSIG',
|
|
||||||
hex: '76a914152ea3cd65f18cb8fa9146c84ea1a97af8f051de88ac',
|
|
||||||
reqSigs: 1,
|
|
||||||
type: 'pubkeyhash',
|
|
||||||
addresses: ['bchtest:qq2jag7dvhccew86j9rvsn4p49a03uz3mcpw3d6aca']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
blockhash: '000000000000026fa244de975ca89ea08008aa566564ce2e8ebb3144361b601b',
|
|
||||||
confirmations: 125,
|
|
||||||
time: 1542646373,
|
|
||||||
blocktime: 1542646373
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockWHDecode = {
|
|
||||||
txid: 'f8a9857fe3b8a288b5fcafb1b0fc196731f433add6d962f77acd7c10b970ff89',
|
|
||||||
fee: '500',
|
|
||||||
sendingaddress: 'bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr',
|
|
||||||
referenceaddress: 'bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr',
|
|
||||||
ismine: false,
|
|
||||||
version: 0,
|
|
||||||
type_int: 0,
|
|
||||||
type: 'Simple Send',
|
|
||||||
propertyid: 368,
|
|
||||||
precision: '8',
|
|
||||||
amount: '10.00000000',
|
|
||||||
valid: true,
|
|
||||||
blockhash: '0000000046ba0bcef78caaa4492622176bbc563cf249ab52340a0449bc8e26f6',
|
|
||||||
blocktime: 1542814183,
|
|
||||||
positioninblock: 57,
|
|
||||||
block: 1269008,
|
|
||||||
confirmations: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockWHCreateInput = {
|
|
||||||
txid: 'f7ed9cf23dee85910f6269c9a101a75fcfd2f3c6fc81f17fad824ff7aaf99ab2',
|
|
||||||
vout: 1,
|
|
||||||
scriptPubKey: '76a914a4b98b0c118de83e7d39c834445f51dce62425f588ac',
|
|
||||||
amount: 0.09984138,
|
|
||||||
satoshis: 9984138,
|
|
||||||
height: 1269011,
|
|
||||||
confirmations: 4,
|
|
||||||
legacyAddress: 'mvXwPH74hW2yVTWwDwzsjGoaUAqJvWk7ZJ',
|
|
||||||
cashAddress: 'bchtest:qzjtnzcvzxx7s0na88yrg3zl28wwvfp97538sgrrmr',
|
|
||||||
value: 0.09984138
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockDecodeRawTransaction,
|
|
||||||
mockDecodeScript,
|
|
||||||
mockRawTransactionConcise,
|
|
||||||
mockRawTransactionVerbose,
|
|
||||||
mockWHDecode,
|
|
||||||
mockWHCreateInput
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
Mocks used for unit tests that interact with slpjs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
// const sinon = require('sinon')
|
|
||||||
// const proxyquire = require('proxyquire')
|
|
||||||
const BigNumber = require('bignumber.js')
|
|
||||||
const slpMocks = require('./slp-mocks')
|
|
||||||
|
|
||||||
// Mock the BitboxNetwork class.
|
|
||||||
class BitboxNetwork {
|
|
||||||
// constructor () {}
|
|
||||||
|
|
||||||
async getAllSlpBalancesAndUtxos (address) {
|
|
||||||
return {
|
|
||||||
satoshis_available_bch: 9996891,
|
|
||||||
satoshis_in_slp_baton: 546,
|
|
||||||
satoshis_in_slp_token: 546,
|
|
||||||
satoshis_in_invalid_token_dag: 0,
|
|
||||||
satoshis_in_invalid_baton_dag: 0,
|
|
||||||
slpTokenBalances: {
|
|
||||||
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796': new BigNumber(
|
|
||||||
123400000000
|
|
||||||
)
|
|
||||||
},
|
|
||||||
slpTokenUtxos: {
|
|
||||||
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796': []
|
|
||||||
},
|
|
||||||
slpBatonUtxos: {
|
|
||||||
'7ac7f4bb50b019fe0f5c81e3fc13fc0720e130282ea460768cafb49785eb2796': []
|
|
||||||
},
|
|
||||||
nonSlpUtxos: [{}],
|
|
||||||
invalidTokenUtxos: [],
|
|
||||||
invalidBatonUtxos: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTokenInformation (txid) {
|
|
||||||
BigNumber.set({ DECIMAL_PLACES: 8, ROUNDING_MODE: 4 })
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
versionType: 1,
|
|
||||||
transactionType: 0,
|
|
||||||
symbol: 'SLPSDK',
|
|
||||||
name: 'SLP SDK example using BITBOX',
|
|
||||||
documentUri: 'developer.bitcoin.com',
|
|
||||||
documentSha256: null,
|
|
||||||
decimals: 8,
|
|
||||||
batonVout: 2,
|
|
||||||
containsBaton: true,
|
|
||||||
genesisOrMintQuantity: new BigNumber(123400000000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTransactionDetails (txid) {
|
|
||||||
return slpMocks.mockTx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock the slpjs library.
|
|
||||||
const slpjs = {
|
|
||||||
BitboxNetwork,
|
|
||||||
slp: {},
|
|
||||||
validator: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = slpjs
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/*
|
|
||||||
const mockDetails = {
|
|
||||||
txid: "6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40",
|
|
||||||
version: 2,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [Array],
|
|
||||||
vout: [Array],
|
|
||||||
blockhash: "00000000e7232ff12462dedf9c11985f5b54202515277c337ccc59812758f28b",
|
|
||||||
blockheight: 1270188,
|
|
||||||
confirmations: 2,
|
|
||||||
time: 1543436253,
|
|
||||||
blocktime: 1543436253,
|
|
||||||
valueOut: 450.78867333,
|
|
||||||
size: 226,
|
|
||||||
valueIn: 450.78867559,
|
|
||||||
fees: 0.00000226
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const mockDetails = {
|
|
||||||
txid: '6f235bd3a689f03c11969cd649ccad592462ca958bc519a30194e7a67b349a40',
|
|
||||||
version: 2,
|
|
||||||
locktime: 0,
|
|
||||||
vin: [
|
|
||||||
{
|
|
||||||
txid: '273d616d1c48f4b075c497f36ffdc79da5c8d6ed75485808b3599aac504f8525',
|
|
||||||
vout: 0,
|
|
||||||
sequence: 4294967295,
|
|
||||||
n: 0,
|
|
||||||
scriptSig: [{}],
|
|
||||||
addr: 'bchtest:qqmd9unmhkpx4pkmr6fkrr8rm6y77vckjvqe8aey35',
|
|
||||||
valueSat: 45078867559,
|
|
||||||
value: 450.78867559,
|
|
||||||
doubleSpentTxID: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
vout: [
|
|
||||||
{
|
|
||||||
value: '450.68867333',
|
|
||||||
n: 0,
|
|
||||||
scriptPubKey: [{}],
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: '0.10000000',
|
|
||||||
n: 1,
|
|
||||||
scriptPubKey: [{}],
|
|
||||||
spentTxId: null,
|
|
||||||
spentIndex: null,
|
|
||||||
spentHeight: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
blockhash: '00000000e7232ff12462dedf9c11985f5b54202515277c337ccc59812758f28b',
|
|
||||||
blockheight: 1270188,
|
|
||||||
confirmations: 3,
|
|
||||||
time: 1543436253,
|
|
||||||
blocktime: 1543436253,
|
|
||||||
valueOut: 450.78867333,
|
|
||||||
size: 226,
|
|
||||||
valueIn: 450.78867559,
|
|
||||||
fees: 0.00000226
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockDetails
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
/*
|
|
||||||
This library contains mocking data for running unit tests on the address route.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const mockAddress = {
|
|
||||||
isvalid: true,
|
|
||||||
address: 'bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y',
|
|
||||||
scriptPubKey: '76a914016a935f87e8deeab04501889b22d87e4e52db0988ac',
|
|
||||||
ismine: false,
|
|
||||||
iswatchonly: false,
|
|
||||||
isscript: false
|
|
||||||
}
|
|
||||||
|
|
||||||
// const mockBalance = {
|
|
||||||
// page: 1,
|
|
||||||
// totalPages: 1,
|
|
||||||
// itemsOnPage: 1000,
|
|
||||||
// address: 'bitcoincash:qzp7gdl52edm24xlpkyqnza9rv43u3mdxyc77j3u6k',
|
|
||||||
// balance: '2546',
|
|
||||||
// totalReceived: '2546',
|
|
||||||
// totalSent: '0',
|
|
||||||
// unconfirmedBalance: '0',
|
|
||||||
// unconfirmedTxs: 0,
|
|
||||||
// txs: 2,
|
|
||||||
// txids: [
|
|
||||||
// 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
|
||||||
// '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6'
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
|
|
||||||
const mockBalance = {
|
|
||||||
confirmed: 2546,
|
|
||||||
unconfirmed: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockUtxos = [
|
|
||||||
{
|
|
||||||
tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
|
||||||
tx_pos: 0,
|
|
||||||
value: 2000,
|
|
||||||
height: 605873
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
|
||||||
tx_pos: 1,
|
|
||||||
value: 546,
|
|
||||||
height: 605873
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockThreeUtxos = [
|
|
||||||
{
|
|
||||||
tx_hash: 'e190d13b88578132608ab912a4d2be3e55aa2792d6042d481ae21d700639de56',
|
|
||||||
tx_pos: 0,
|
|
||||||
value: 2000,
|
|
||||||
height: 605873
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
|
||||||
tx_pos: 1,
|
|
||||||
value: 546,
|
|
||||||
height: 605873
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
|
||||||
tx_pos: 1,
|
|
||||||
value: 546,
|
|
||||||
height: 605873
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const mockIsTokenUtxos = [
|
|
||||||
false,
|
|
||||||
{
|
|
||||||
tx_hash: '44e1f48c4093fc61db1a8fa206aa402fc34e482b3f788cb38c123ca0e1a35db6',
|
|
||||||
tx_out: 1,
|
|
||||||
value: 546,
|
|
||||||
height: 605873,
|
|
||||||
confirmations: 298,
|
|
||||||
satoshis: 546,
|
|
||||||
utxoType: 'token',
|
|
||||||
transactionType: 'send',
|
|
||||||
tokenId: '497291b8a1dfe69c8daea50677a3d31a5ef0e9484d8bebb610dac64bbc202fb7',
|
|
||||||
tokenTicker: 'TOK-CH',
|
|
||||||
tokenName: 'TokyoCash',
|
|
||||||
tokenDocumentUrl: '',
|
|
||||||
tokenDocumentHash: '',
|
|
||||||
decimals: 8,
|
|
||||||
tokenQty: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const tapUtxo = {
|
|
||||||
txid: '2e030df12390186baf817fa2760540b886511e04bc520e88f6b4c2124cc2a7d4',
|
|
||||||
vout: 1,
|
|
||||||
value: '546',
|
|
||||||
height: 606564,
|
|
||||||
confirmations: 3,
|
|
||||||
satoshis: 546,
|
|
||||||
utxoType: 'token',
|
|
||||||
transactionType: 'send',
|
|
||||||
tokenId: 'dd84ca78db4d617221b58eabc6667af8fe2f7eadbfcc213d35be9f1b419beb8d',
|
|
||||||
tokenTicker: 'TAP',
|
|
||||||
tokenName: 'Thoughts and Prayers',
|
|
||||||
tokenDocumentUrl: '',
|
|
||||||
tokenDocumentHash: '',
|
|
||||||
decimals: 0,
|
|
||||||
tokenQty: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokensOnly = [mockIsTokenUtxos[1], tapUtxo]
|
|
||||||
|
|
||||||
const multipleTokens = [false, mockIsTokenUtxos[1], tapUtxo]
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mockAddress,
|
|
||||||
mockBalance,
|
|
||||||
mockUtxos,
|
|
||||||
mockThreeUtxos,
|
|
||||||
mockIsTokenUtxos,
|
|
||||||
tokensOnly,
|
|
||||||
multipleTokens
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE PRICE.JS LIBRARY
|
|
||||||
|
|
||||||
This test file uses the environment variable TEST to switch between unit
|
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
const Price = require('../../src/routes/v4/price')
|
|
||||||
let uut
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
|
||||||
const mockData = require('./mocks/price-mock')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
describe('#PriceRouter', () => {
|
|
||||||
let req, res
|
|
||||||
let sandbox
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
if (!process.env.TEST) process.env.TEST = 'unit'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
|
|
||||||
uut = new Price()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Restore Sandbox
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {})
|
|
||||||
|
|
||||||
describe('#root', async () => {
|
|
||||||
// root route handler.
|
|
||||||
// const root = controlRoute.testableComponents.root
|
|
||||||
|
|
||||||
it('should respond to GET for base route', async () => {
|
|
||||||
const result = uut.root(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.equal(result.status, 'price', 'Returns static string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getUSD', () => {
|
|
||||||
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
|
|
||||||
|
|
||||||
it('should throw 500 when network issues', async () => {
|
|
||||||
uut.priceUrl = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getUSD(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// console.log(res)
|
|
||||||
assert.include(
|
|
||||||
res.output.error,
|
|
||||||
'Network error: Could not communicate with full node or other external service'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getUSD(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getUSD(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should get the USD price of BCH', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: mockData.mockCoinbaseFeed })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getUSD(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getBCHRate', () => {
|
|
||||||
it('should throw 500 when network issues', async () => {
|
|
||||||
uut.priceUrl = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getBCHRate(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// console.log(res)
|
|
||||||
assert.include(
|
|
||||||
res.output.error,
|
|
||||||
'Network error: Could not communicate with full node or other external service'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getBCHRate(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getBCHRate(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 several rates for BCH', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: mockData.mockCoinbaseFeed })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getBCHRate(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// assert.isNumber(result)
|
|
||||||
assert.property(result, 'USD')
|
|
||||||
assert.property(result, 'CAD')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#errorHandler', () => {
|
|
||||||
it('should handle unexpected errors', () => {
|
|
||||||
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
|
|
||||||
|
|
||||||
const result = uut.errorHandler(new Error('test error'), res)
|
|
||||||
// console.log('result: ', result)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.property(result, 'error')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getBCHAUSD', () => {
|
|
||||||
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
|
|
||||||
|
|
||||||
it('should throw 500 when network issues', async () => {
|
|
||||||
uut.coinexPriceUrl = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getBCHAUSD(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// console.log(res)
|
|
||||||
assert.include(
|
|
||||||
res.output.error,
|
|
||||||
'Network error: Could not communicate with full node or other external service'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getBCHAUSD(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getBCHAUSD(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should get the USD price of BCH', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: mockData.mockCoinexFeed })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getBCHAUSD(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getBCHUSD', () => {
|
|
||||||
// const getNetworkInfo = controlRoute.testableComponents.getNetworkInfo
|
|
||||||
|
|
||||||
it('should throw 500 when network issues', async () => {
|
|
||||||
uut.bchCoinexPriceUrl = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
await uut.getBCHUSD(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// console.log(res)
|
|
||||||
assert.include(
|
|
||||||
res.output.error,
|
|
||||||
'Network error: Could not communicate with full node or other external service'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service stalls', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNABORTED' })
|
|
||||||
|
|
||||||
const result = await uut.getBCHUSD(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns proper error when downstream service is down', async () => {
|
|
||||||
// Mock the timeout error.
|
|
||||||
sandbox.stub(uut.axios, 'request').throws({ code: 'ECONNREFUSED' })
|
|
||||||
|
|
||||||
const result = await uut.getBCHUSD(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Could not communicate with full node',
|
|
||||||
'Error message expected'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should get the USD price of BCH', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
sandbox
|
|
||||||
.stub(uut.axios, 'request')
|
|
||||||
.resolves({ data: mockData.mockCoinexFeed })
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await uut.getBCHUSD(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result.usd)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,506 +0,0 @@
|
|||||||
/*
|
|
||||||
Unit tests for the route-ratelimit2.js middleware.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
// Public npm libraries.
|
|
||||||
const assert = require('chai').assert
|
|
||||||
const sinon = require('sinon')
|
|
||||||
const cloneDeep = require('lodash.clonedeep')
|
|
||||||
|
|
||||||
const config = require('../../config')
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes, mockNext } = require('./mocks/express-mocks')
|
|
||||||
|
|
||||||
// Libraries under test
|
|
||||||
const RateLimits = require('../../src/middleware/route-ratelimit')
|
|
||||||
let uut = new RateLimits()
|
|
||||||
|
|
||||||
let req, res, next
|
|
||||||
|
|
||||||
describe('#rate-routelimit', () => {
|
|
||||||
let sandbox
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
if (!process.env.JWT_AUTH_SERVER) {
|
|
||||||
process.env.JWT_AUTH_SERVER = 'http://fakeurl.com/'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wipe the Redis DB, which prevents false negatives when running integration
|
|
||||||
// tests back-to-back.
|
|
||||||
await uut.wipeRedis()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = cloneDeep(mockReq)
|
|
||||||
res = cloneDeep(mockRes)
|
|
||||||
next = mockNext
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
req.locals = {}
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
|
|
||||||
uut = new RateLimits()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
uut.closeRedis()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#checkInternalIp', () => {
|
|
||||||
it('should return true for a request from localhost', () => {
|
|
||||||
req.ip = '::ffff:127.0.0.1'
|
|
||||||
|
|
||||||
const result = uut.checkInternalIp(req)
|
|
||||||
|
|
||||||
assert.equal(result, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return true for a request from a Docker container', () => {
|
|
||||||
req.ip = '172.17.0.3'
|
|
||||||
|
|
||||||
const result = uut.checkInternalIp(req)
|
|
||||||
|
|
||||||
assert.equal(result, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return false for a random ip address', () => {
|
|
||||||
req.ip = '123.456.7.8'
|
|
||||||
|
|
||||||
const result = uut.checkInternalIp(req)
|
|
||||||
|
|
||||||
assert.equal(result, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return false when an error is encountered', () => {
|
|
||||||
req.ip = 4
|
|
||||||
|
|
||||||
const result = uut.checkInternalIp(req)
|
|
||||||
|
|
||||||
assert.equal(result, false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#isInWhitelist', () => {
|
|
||||||
it('should return false when no argument is passed in', () => {
|
|
||||||
const result = uut.isInWhitelist()
|
|
||||||
|
|
||||||
assert.equal(result, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return false when origin is not in the whitelist', () => {
|
|
||||||
req.origin = 'blah.com'
|
|
||||||
req.get = sandbox.stub().returns(req.origin)
|
|
||||||
|
|
||||||
const result = uut.isInWhitelist(req)
|
|
||||||
|
|
||||||
assert.equal(result, false)
|
|
||||||
|
|
||||||
// Used to appease linter. Remove these.
|
|
||||||
res.blah = 4
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return true when origin is in the whitelist', () => {
|
|
||||||
req.origin = 'message.fullstack.cash'
|
|
||||||
req.get = sandbox.stub().returns(req.origin)
|
|
||||||
|
|
||||||
const result = uut.isInWhitelist(req)
|
|
||||||
|
|
||||||
assert.equal(result, true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#decodeJwtToken', () => {
|
|
||||||
it('should return the default JWT payload if decoding fails', () => {
|
|
||||||
const jwt =
|
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlODhhY2JmMDIyMWMxMDAxMmFkOTNmZiIsImVtYWlsIjoiY2hyaXMudHJvdXRuZXJAZ21haWwuY29tIiwiYXBpTGV2ZWwiOjQwLCJyYXRlTGltaXQiOjMsImlhdCI6MTYxNTE1NzA4NywiZXhwIjoxNjE3NzQ5MDg3fQ.RLNGuYAa-CcLdhTGD27tDeaxT6-GIdeR8T4JWZZLDZA'
|
|
||||||
|
|
||||||
const result = uut.decodeJwtToken(jwt)
|
|
||||||
// console.log('result: ', result)
|
|
||||||
|
|
||||||
assert.property(result, 'id')
|
|
||||||
// assert.equal(result.id, '123.456.789.10')
|
|
||||||
assert.property(result, 'email')
|
|
||||||
// assert.equal(result.email, 'test@bchtest.net')
|
|
||||||
// assert.property(result, 'pointsToConsume')
|
|
||||||
// assert.equal(result.pointsToConsume, config.anonRateLimit)
|
|
||||||
// assert.property(result, 'duration')
|
|
||||||
// assert.equal(result.duration, 30)
|
|
||||||
assert.property(result, 'exp')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return the default JWT payload if no input is given', () => {
|
|
||||||
const result = uut.decodeJwtToken()
|
|
||||||
// console.log('result: ', result)
|
|
||||||
|
|
||||||
assert.property(result, 'id')
|
|
||||||
assert.equal(result.id, '123.456.789.10')
|
|
||||||
assert.property(result, 'email')
|
|
||||||
assert.equal(result.email, 'test@bchtest.net')
|
|
||||||
assert.property(result, 'pointsToConsume')
|
|
||||||
assert.equal(result.pointsToConsume, config.anonRateLimit)
|
|
||||||
assert.property(result, 'duration')
|
|
||||||
assert.equal(result.duration, 30)
|
|
||||||
assert.property(result, 'exp')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should correctly decode a JWT token', () => {
|
|
||||||
// Generate a new JWT token for the test.
|
|
||||||
const jwtPayload = {
|
|
||||||
id: '5dade3f5739e6c0ff034b9a1',
|
|
||||||
pointsToConsume: 10,
|
|
||||||
email: 'gooduser@test.com',
|
|
||||||
apiLevel: 40,
|
|
||||||
rateLimit: 100,
|
|
||||||
duration: 30
|
|
||||||
}
|
|
||||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
|
||||||
|
|
||||||
const result = uut.decodeJwtToken(jwtToken)
|
|
||||||
// console.log('result: ', result)
|
|
||||||
|
|
||||||
assert.property(result, 'id')
|
|
||||||
assert.equal(result.id, jwtPayload.id)
|
|
||||||
assert.property(result, 'email')
|
|
||||||
assert.equal(result.email, jwtPayload.email)
|
|
||||||
assert.property(result, 'pointsToConsume')
|
|
||||||
assert.equal(result.pointsToConsume, jwtPayload.pointsToConsume)
|
|
||||||
assert.property(result, 'duration')
|
|
||||||
assert.equal(result.duration, jwtPayload.duration)
|
|
||||||
assert.property(result, 'exp')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return the default payload if there is an unhandled error', () => {
|
|
||||||
// Force an error.
|
|
||||||
sandbox.stub(uut, 'generateJwtToken').throws(new Error('test error'))
|
|
||||||
|
|
||||||
const result = uut.decodeJwtToken()
|
|
||||||
// console.log('result: ', result)
|
|
||||||
|
|
||||||
assert.property(result, 'id')
|
|
||||||
assert.equal(result.id, '123.456.789.10')
|
|
||||||
assert.property(result, 'email')
|
|
||||||
assert.equal(result.email, 'test@bchtest.net')
|
|
||||||
assert.property(result, 'pointsToConsume')
|
|
||||||
assert.equal(result.pointsToConsume, config.anonRateLimit)
|
|
||||||
assert.property(result, 'duration')
|
|
||||||
assert.equal(result.duration, 30)
|
|
||||||
assert.property(result, 'exp')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#trackRateLimits', () => {
|
|
||||||
it('should apply anonymous rate limits if no JWT token is provided', async () => {
|
|
||||||
req.ip = '127.0.0.1'
|
|
||||||
|
|
||||||
const result = await uut.trackRateLimits(req, res)
|
|
||||||
// console.log(`result: `, result)
|
|
||||||
|
|
||||||
// console.log('res.locals.pointsToConsume: ', res.locals.pointsToConsume)
|
|
||||||
|
|
||||||
assert.equal(result, false, 'Rate limits not exceeded')
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
config.anonRateLimit,
|
|
||||||
'Anonymous rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should apply 100 RPM rate limits when JWT token is provided', async () => {
|
|
||||||
// Generate a new JWT token for the test.
|
|
||||||
const jwtPayload = {
|
|
||||||
id: '5dade3f5739e6c0ff034b9a1',
|
|
||||||
pointsToConsume: 10
|
|
||||||
}
|
|
||||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
|
||||||
|
|
||||||
const result = await uut.trackRateLimits(req, res, jwtToken)
|
|
||||||
// console.log(`result: `, result)
|
|
||||||
|
|
||||||
// console.log('res.locals.pointsToConsume: ', res.locals.pointsToConsume)
|
|
||||||
|
|
||||||
assert.equal(result, false, 'Rate limits not exceeded')
|
|
||||||
assert.equal(res.locals.pointsToConsume, 10, '100 RPM limits applied')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#applyRateLimits', () => {
|
|
||||||
it('should skip rate limits if basic auth token is used', async () => {
|
|
||||||
req.locals.proLimit = true
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should skip rate limits if internal call passes basic auth token', async () => {
|
|
||||||
req.ip = '127.0.0.1'
|
|
||||||
req.body.usrObj = {
|
|
||||||
proLimit: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should apply rate limits to anonymous users', async () => {
|
|
||||||
req.ip = '123.456.7.8'
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
config.anonRateLimit,
|
|
||||||
'Anonymous rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 429 error when anonymous users exceed rate limit', async () => {
|
|
||||||
req.ip = '123.456.7.8'
|
|
||||||
|
|
||||||
// force req.locals.jwtToken to be empty.
|
|
||||||
req.locals.jwtToken = undefined
|
|
||||||
|
|
||||||
let val
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
// console.log('req.locals: ', req.locals)
|
|
||||||
val = await uut.applyRateLimits(req, res, next)
|
|
||||||
}
|
|
||||||
// console.log('val: ', val)
|
|
||||||
|
|
||||||
assert.property(val, 'error')
|
|
||||||
assert.include(
|
|
||||||
val.error,
|
|
||||||
'Too many requests. Your limits are currently 20 requests per minute.'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
config.anonRateLimit,
|
|
||||||
'Anonymous rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should apply rate limits when JWT token is provided', async () => {
|
|
||||||
// Generate a new JWT token for the test.
|
|
||||||
const jwtPayload = {
|
|
||||||
id: '5dade3f5739e6c0ff034b9a1',
|
|
||||||
pointsToConsume: 10
|
|
||||||
}
|
|
||||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
|
||||||
|
|
||||||
req.ip = '123.456.7.8'
|
|
||||||
req.locals.jwtToken = jwtToken
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
10,
|
|
||||||
'Anonymous rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should apply internal rate limits to internal calls', async () => {
|
|
||||||
req.ip = '127.0.0.1'
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
10,
|
|
||||||
'Internal rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 429 error when internal calls exceed interal rate limit', async () => {
|
|
||||||
req.ip = '127.0.0.1'
|
|
||||||
|
|
||||||
let val
|
|
||||||
for (let i = 0; i < 1025; i++) {
|
|
||||||
val = await uut.applyRateLimits(req, res, next)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.property(val, 'error')
|
|
||||||
assert.include(
|
|
||||||
val.error,
|
|
||||||
'Too many requests. Your limits are currently 1000 requests per minute.'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
10,
|
|
||||||
'Internal rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should apply JWT rate limits to internal calls when JWT passes through', async () => {
|
|
||||||
// Generate a new JWT token for the test.
|
|
||||||
const jwtPayload = {
|
|
||||||
id: '5dade3f5739e6c0ff034b9a1',
|
|
||||||
pointsToConsume: 10
|
|
||||||
}
|
|
||||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
|
||||||
|
|
||||||
req.ip = '127.0.0.1'
|
|
||||||
req.body.usrObj = {
|
|
||||||
jwtToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
10,
|
|
||||||
'User JWT rate limits applied'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 429 error when internal calls using JWT pass-through exceeds rate limit', async () => {
|
|
||||||
// Generate a new JWT token for the test.
|
|
||||||
const jwtPayload = {
|
|
||||||
id: '5dade3f5739e6c0ff034b9a1',
|
|
||||||
pointsToConsume: 100
|
|
||||||
}
|
|
||||||
const jwtToken = uut.generateJwtToken(jwtPayload)
|
|
||||||
|
|
||||||
req.ip = '127.0.0.1'
|
|
||||||
req.body.usrObj = {
|
|
||||||
jwtToken
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let val
|
|
||||||
for (let i = 0; i < 120; i++) {
|
|
||||||
val = await uut.applyRateLimits(req, res, next)
|
|
||||||
}
|
|
||||||
// console.log('val: ', val)
|
|
||||||
|
|
||||||
assert.property(val, 'error')
|
|
||||||
assert.include(
|
|
||||||
val.error,
|
|
||||||
'Too many requests. Your limits are currently 100 requests per minute.'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
res.locals.pointsToConsume,
|
|
||||||
100,
|
|
||||||
'User JWT rate limits applied'
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.log('err: ', err)
|
|
||||||
assert.fail('Unexpected result')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should move to the next middleware when encountering an unexpected internal error', async () => {
|
|
||||||
// Force the creation of the res and req locals property. Covers an
|
|
||||||
// otherwise untested code path.
|
|
||||||
req.locals = undefined
|
|
||||||
res.locals = undefined
|
|
||||||
|
|
||||||
// Force an error
|
|
||||||
sandbox.stub(uut, 'checkInternalIp').throws(new Error('test error'))
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const startCallCount = next.callCount
|
|
||||||
|
|
||||||
await uut.applyRateLimits(req, res, next)
|
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
|
||||||
const endCallCount = next.callCount
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
endCallCount,
|
|
||||||
startCallCount,
|
|
||||||
'Expecting next() to be called'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
Unit tests for the route-utils.js library.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const assert = require('chai').assert
|
|
||||||
|
|
||||||
const RouteUtils = require('../../src/util/route-utils.js')
|
|
||||||
|
|
||||||
describe('#route-utils', () => {
|
|
||||||
let uut
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
uut = new RouteUtils()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#decodeError', () => {
|
|
||||||
it('should decode a 429 error from nginx', () => {
|
|
||||||
const err = {
|
|
||||||
error: '<html>\r\n<head><title>429 Too Many Requests</title></head>\r\n<body>\r\n<center><h1>429 Too Many Requests</h1></center>\r\n<hr><center>nginx/1.18.0 (Ubuntu)</center>\r\n</body>\r\n</html>\r\n',
|
|
||||||
level: 'error',
|
|
||||||
message: 'Error in slp.js/hydrateUtxos().',
|
|
||||||
timestamp: '2021-03-31T04:13:36.662Z'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = uut.decodeError(err)
|
|
||||||
// console.log('result: ', result)
|
|
||||||
|
|
||||||
assert.property(result, 'msg')
|
|
||||||
assert.equal(result.msg, '429 Too Many Requests')
|
|
||||||
assert.property(result, 'status')
|
|
||||||
assert.equal(result.status, 429)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
-2548
File diff suppressed because it is too large
Load Diff
-532
@@ -1,532 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE UTIL.TS LIBRARY
|
|
||||||
|
|
||||||
This test file uses the environment variable TEST to switch between unit
|
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
|
||||||
|
|
||||||
These tests use this private key:
|
|
||||||
L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq
|
|
||||||
Which corresponds to this address:
|
|
||||||
bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
// Public npm libraries
|
|
||||||
const assert = require('chai').assert
|
|
||||||
const nock = require('nock') // HTTP mocking
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
// Local libraries
|
|
||||||
const Electrumx = require('../../src/routes/v4/electrumx')
|
|
||||||
const UtilRoute = require('../../src/routes/v4/util')
|
|
||||||
|
|
||||||
let originalEnvVars // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
|
||||||
const mockData = require('./mocks/util-mocks')
|
|
||||||
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
// const UtilRoute = utilRoute.UtilRoute
|
|
||||||
// const electrumx = new Electrumx()
|
|
||||||
const utilRouteInst = new UtilRoute({ electrumx: {} })
|
|
||||||
|
|
||||||
describe('#Util', () => {
|
|
||||||
let req, res
|
|
||||||
let sandbox
|
|
||||||
let electrumx
|
|
||||||
let utilRoute
|
|
||||||
|
|
||||||
before(async () => {
|
|
||||||
// Save existing environment variables.
|
|
||||||
originalEnvVars = {
|
|
||||||
BITCOINCOM_BASEURL: process.env.BITCOINCOM_BASEURL,
|
|
||||||
RPC_BASEURL: process.env.RPC_BASEURL,
|
|
||||||
RPC_USERNAME: process.env.RPC_USERNAME,
|
|
||||||
RPC_PASSWORD: process.env.RPC_PASSWORD
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
if (!process.env.TEST) process.env.TEST = 'unit'
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
process.env.BITCOINCOM_BASEURL = 'http://fakeurl/api/'
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api'
|
|
||||||
process.env.RPC_USERNAME = 'fakeusername'
|
|
||||||
process.env.RPC_PASSWORD = 'fakepassword'
|
|
||||||
}
|
|
||||||
|
|
||||||
electrumx = new Electrumx()
|
|
||||||
// await electrumx.connect()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
|
|
||||||
// Activate nock if it's inactive.
|
|
||||||
if (!nock.isActive()) nock.activate()
|
|
||||||
|
|
||||||
sandbox = sinon.createSandbox()
|
|
||||||
|
|
||||||
utilRoute = new UtilRoute({ electrumx })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Clean up HTTP mocks.
|
|
||||||
nock.cleanAll() // clear interceptor list.
|
|
||||||
nock.restore()
|
|
||||||
|
|
||||||
sandbox.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
// Restore any pre-existing environment variables.
|
|
||||||
process.env.BITCOINCOM_BASEURL = originalEnvVars.BITCOINCOM_BASEURL
|
|
||||||
process.env.RPC_BASEURL = originalEnvVars.RPC_BASEURL
|
|
||||||
process.env.RPC_USERNAME = originalEnvVars.RPC_USERNAME
|
|
||||||
process.env.RPC_PASSWORD = originalEnvVars.RPC_PASSWORD
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#root', async () => {
|
|
||||||
// root route handler.
|
|
||||||
const root = utilRouteInst.root
|
|
||||||
|
|
||||||
it('should respond to GET for base route', async () => {
|
|
||||||
const result = root(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.equal(result.status, 'util', 'Returns static string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#validateAddressSingle', async () => {
|
|
||||||
const validateAddress = utilRouteInst.validateAddressSingle
|
|
||||||
|
|
||||||
it('should throw an error for an empty address', async () => {
|
|
||||||
const result = await validateAddress(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'address can not be empty',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw 503 when network issues', async () => {
|
|
||||||
// Save the existing RPC URL.
|
|
||||||
const savedUrl2 = process.env.RPC_BASEURL
|
|
||||||
|
|
||||||
// Manipulate the URL to cause a 500 network error.
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
req.params.address = 'bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y'
|
|
||||||
|
|
||||||
await validateAddress(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.RPC_BASEURL = savedUrl2
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
// assert.include(result.error,"Network error: Could not communicate with full node","Error message expected")
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should validate address', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
nock(`${process.env.RPC_BASEURL}`)
|
|
||||||
.post((uri) => uri.includes('/'))
|
|
||||||
.reply(200, { result: mockData.mockAddress })
|
|
||||||
}
|
|
||||||
|
|
||||||
req.params.address =
|
|
||||||
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
|
|
||||||
|
|
||||||
const result = await validateAddress(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.hasAnyKeys(result, [
|
|
||||||
'isvalid',
|
|
||||||
'address',
|
|
||||||
'scriptPubKey',
|
|
||||||
'ismine',
|
|
||||||
'iswatchonly',
|
|
||||||
'isscript'
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#validateAddressBulk', async () => {
|
|
||||||
const validateAddressBulk = utilRouteInst.validateAddressBulk
|
|
||||||
|
|
||||||
it('should throw an error for an empty body', async () => {
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'addresses needs to be an array. Use GET for single address.',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should error on non-array single address', async () => {
|
|
||||||
req.body = {
|
|
||||||
addresses: 'bchtest:qqqk4y6lsl5da64sg5qc3xezmplyu5kmpyz2ysaa5y'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'addresses needs to be an array. Use GET for single address.',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
|
||||||
const testArray = []
|
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
|
||||||
|
|
||||||
req.body.addresses = testArray
|
|
||||||
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.hasAllKeys(result, ['error'])
|
|
||||||
assert.include(result.error, 'Array too large')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should error on invalid address', async () => {
|
|
||||||
req.body = {
|
|
||||||
addresses: ['bchtest:qqqk4y6lsl5da64sg5qc3xezmpl']
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Invalid BCH address. Double check your address is valid',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should error on mainnet address when using testnet', async () => {
|
|
||||||
req.body = {
|
|
||||||
addresses: ['bchtest:qrls6vzjkkxlds7aqv9075u0fttwc7u9jvczn5fdt9']
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw 503 when network issues', async () => {
|
|
||||||
// Save the existing RPC URL.
|
|
||||||
const savedUrl2 = process.env.RPC_BASEURL
|
|
||||||
|
|
||||||
// Manipulate the URL to cause a 500 network error.
|
|
||||||
process.env.RPC_BASEURL = 'http://fakeurl/api/'
|
|
||||||
|
|
||||||
req.body.addresses = [
|
|
||||||
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
|
|
||||||
]
|
|
||||||
|
|
||||||
await validateAddressBulk(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.RPC_BASEURL = savedUrl2
|
|
||||||
|
|
||||||
assert.isAbove(
|
|
||||||
res.statusCode,
|
|
||||||
499,
|
|
||||||
'HTTP status code 500 or greater expected.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should validate a single address', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
nock(`${process.env.RPC_BASEURL}`)
|
|
||||||
.post((uri) => uri.includes('/'))
|
|
||||||
.reply(200, { result: mockData.mockAddress })
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body.addresses = [
|
|
||||||
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
|
|
||||||
]
|
|
||||||
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isArray(result)
|
|
||||||
assert.hasAnyKeys(result[0], [
|
|
||||||
'isvalid',
|
|
||||||
'address',
|
|
||||||
'scriptPubKey',
|
|
||||||
'ismine',
|
|
||||||
'iswatchonly',
|
|
||||||
'isscript'
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should validate a multiple addresses', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
nock(`${process.env.RPC_BASEURL}`)
|
|
||||||
.post((uri) => uri.includes('/'))
|
|
||||||
.times(2)
|
|
||||||
.reply(200, { result: mockData.mockAddress })
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body.addresses = [
|
|
||||||
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd',
|
|
||||||
'bitcoincash:qpujxqra3jmdlzzapwmmt7uspr7q0c9ff5hzljcrnd'
|
|
||||||
]
|
|
||||||
|
|
||||||
const result = await validateAddressBulk(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.isArray(result)
|
|
||||||
assert.hasAnyKeys(result[0], [
|
|
||||||
'isvalid',
|
|
||||||
'address',
|
|
||||||
'scriptPubKey',
|
|
||||||
'ismine',
|
|
||||||
'iswatchonly',
|
|
||||||
'isscript'
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#sweepWif', () => {
|
|
||||||
it('should throw 400 if WIF is not included', async () => {
|
|
||||||
req.body = {}
|
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'WIF needs to a proper compressed WIF starting with K or L',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw 400 if WIF is malformed', async () => {
|
|
||||||
req.body = {
|
|
||||||
wif: 'abc123'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'WIF needs to a proper compressed WIF starting with K or L',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw 400 if destination address is not included', async () => {
|
|
||||||
req.body = {
|
|
||||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRouteInst.sweepWif(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'address can not be empty',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Unit test only.
|
|
||||||
if (process.env.TEST === 'unit') {
|
|
||||||
it('should generate transaction for valid token sweep', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
|
||||||
.resolves(mockData.mockBalance)
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
|
||||||
.resolves(mockData.mockUtxos)
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
|
||||||
.resolves(mockData.mockIsTokenUtxos)
|
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
|
||||||
// to the network.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
|
||||||
.resolves('test-txid')
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
wif: 'L1wAGEN721LHDoiN8pLwwBb87bYrU6Gs21UPcCRR7LjKypQyVaCq',
|
|
||||||
toAddr: 'bitcoincash:qp2g4cnekxsjspccmtvh5k73mczz6273js4mjr353r'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRoute.sweepWif(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(result, 'test-txid')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return balance if balance-only is true', async () => {
|
|
||||||
// Mock the RPC call for unit tests.
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
|
||||||
.resolves(mockData.mockBalance)
|
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
|
||||||
// to the network.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
|
||||||
.resolves('test-txid')
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
|
||||||
balanceOnly: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRoute.sweepWif(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.isNumber(result)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should generate transaction for valid BCH-only sweep', async () => {
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
|
||||||
.resolves(mockData.mockBalance)
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
|
||||||
.resolves(mockData.mockUtxos)
|
|
||||||
|
|
||||||
// Force token utxo to appear as regular BCH utxo.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
|
||||||
.resolves([false, false])
|
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
|
||||||
// to the network.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
|
||||||
.resolves('test-txid')
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
|
||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRoute.sweepWif(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(result, 'test-txid')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw 422 error if no non-token UTXOs', async () => {
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
|
||||||
.resolves(mockData.mockBalance)
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
|
||||||
.resolves(mockData.mockUtxos)
|
|
||||||
|
|
||||||
// Force token utxo to appear as regular BCH utxo.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
|
||||||
.resolves(mockData.tokensOnly)
|
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
|
||||||
// to the network.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
|
||||||
.resolves('test-txid')
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
|
||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRoute.sweepWif(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 422)
|
|
||||||
assert.property(result, 'error')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Tokens found, but no BCH UTXOs found. Add BCH to wallet to move tokens'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should detect and throw error for multiple token classes', async () => {
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_balanceFromElectrumx')
|
|
||||||
.resolves(mockData.mockBalance)
|
|
||||||
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.electrumx, '_utxosFromElectrumx')
|
|
||||||
.resolves(mockData.mockThreeUtxos)
|
|
||||||
|
|
||||||
// Force token utxo to appear as regular BCH utxo.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.SLP.Utils, 'tokenUtxoDetails')
|
|
||||||
.resolves(mockData.multipleTokens)
|
|
||||||
|
|
||||||
// Mock sendRawTransaction() so that the hex does not actually get broadcast
|
|
||||||
// to the network.
|
|
||||||
sandbox
|
|
||||||
.stub(utilRoute.bchjs.RawTransactions, 'sendRawTransaction')
|
|
||||||
.resolves('test-txid')
|
|
||||||
|
|
||||||
req.body = {
|
|
||||||
wif: 'L5GEFg1tETLWBugmhSo9Zc4ms968qVmfmTroDxsJ982AiudAQGyt',
|
|
||||||
toAddr: 'bitcoincash:qz2qn6zt4qmacf4r6c0e2pdcqsgnkxaa3ql2xpee6p'
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await utilRoute.sweepWif(req, res)
|
|
||||||
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 422)
|
|
||||||
assert.property(result, 'error')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'Multiple token classes detected. This function only supports a single class of token'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
-143
@@ -1,143 +0,0 @@
|
|||||||
/*
|
|
||||||
TESTS FOR THE XPUB.JS LIBRARY
|
|
||||||
|
|
||||||
This test file uses the environment variable TEST to switch between unit
|
|
||||||
and integration tests. By default, TEST is set to 'unit'. Set this variable
|
|
||||||
to 'integration' to run the tests against BCH mainnet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const chai = require('chai')
|
|
||||||
const assert = chai.assert
|
|
||||||
const xpubRoute = require('../../src/routes/v4/xpub')
|
|
||||||
const nock = require('nock') // HTTP mocking
|
|
||||||
|
|
||||||
// let originalUrl // Used during transition from integration to unit tests.
|
|
||||||
|
|
||||||
// Mocking data.
|
|
||||||
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
|
||||||
// const mockData = require('./mocks/address-mock')
|
|
||||||
|
|
||||||
// Used for debugging.
|
|
||||||
const util = require('util')
|
|
||||||
util.inspect.defaultOptions = { depth: 1 }
|
|
||||||
|
|
||||||
describe('#XPUBRouter', () => {
|
|
||||||
let req, res
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
// Set default environment variables for unit tests.
|
|
||||||
if (!process.env.TEST) process.env.TEST = 'unit'
|
|
||||||
// if (process.env.TEST === "unit")
|
|
||||||
// process.env.BITCOINCOM_BASEURL = "http://fakeurl/api
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup the mocks before each test.
|
|
||||||
beforeEach(() => {
|
|
||||||
// Mock the req and res objects used by Express routes.
|
|
||||||
req = mockReq
|
|
||||||
res = mockRes
|
|
||||||
|
|
||||||
// Explicitly reset the parmas and body.
|
|
||||||
req.params = {}
|
|
||||||
req.body = {}
|
|
||||||
req.query = {}
|
|
||||||
|
|
||||||
// Activate nock if it's inactive.
|
|
||||||
if (!nock.isActive()) nock.activate()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// Clean up HTTP mocks.
|
|
||||||
nock.cleanAll() // clear interceptor list.
|
|
||||||
nock.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
after(() => {
|
|
||||||
// process.env.BITCOINCOM_BASEURL = originalUrl
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#root', () => {
|
|
||||||
// root route handler.
|
|
||||||
const root = xpubRoute.testableComponents.root
|
|
||||||
|
|
||||||
it('should respond to GET for base route', async () => {
|
|
||||||
const result = root(req, res)
|
|
||||||
|
|
||||||
assert.equal(result.status, 'address', 'Returns static string')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#FromXPubSingle', () => {
|
|
||||||
// details route handler.
|
|
||||||
const fromXPubSingle = xpubRoute.testableComponents.fromXPubSingle
|
|
||||||
|
|
||||||
it('should throw 400 if xpub is empty', async () => {
|
|
||||||
const result = await fromXPubSingle(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
assert.hasAllKeys(result, ['error'])
|
|
||||||
assert.include(result.error, 'xpub can not be empty')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should error on an array', async () => {
|
|
||||||
req.params.xpub = [
|
|
||||||
'tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM'
|
|
||||||
]
|
|
||||||
|
|
||||||
const result = await fromXPubSingle(req, res)
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
|
||||||
assert.include(
|
|
||||||
result.error,
|
|
||||||
'xpub can not be an array',
|
|
||||||
'Proper error message'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
it("should throw 500 when network issues", async () => {
|
|
||||||
const savedUrl = process.env.BITCOINCOM_BASEURL
|
|
||||||
|
|
||||||
try {
|
|
||||||
req.params.xpub = `tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM`
|
|
||||||
|
|
||||||
// Switch the Insight URL to something that will error out.
|
|
||||||
process.env.BITCOINCOM_BASEURL = "http://fakeurl/api/"
|
|
||||||
|
|
||||||
const result = await fromXPubSingle(req, res)
|
|
||||||
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.BITCOINCOM_BASEURL = savedUrl
|
|
||||||
|
|
||||||
assert.equal(res.statusCode, 500, "HTTP status code 500 expected.")
|
|
||||||
assert.include(result.error, "ENOTFOUND", "Error message expected")
|
|
||||||
} catch (err) {
|
|
||||||
// Restore the saved URL.
|
|
||||||
process.env.BITCOINCOM_BASEURL = savedUrl
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
it('should create an address from xpub', async () => {
|
|
||||||
req.params.xpub = 'tpubDHTK2jqg73w3GwoiHfAMbMYML1HN8FhrUxD9rFgbSgHXdwwrY6pAFqKDfUHhqw7vreaZty5hPGjb1S7ZPQeMmu6TFHAKfY9tJpYbvaGjPRM'
|
|
||||||
|
|
||||||
// Mock the Insight URL for unit tests.
|
|
||||||
// TODO add unit test
|
|
||||||
// if (process.env.TEST === "unit") {
|
|
||||||
// nock(`${process.env.BITCOINCOM_BASEURL}`)
|
|
||||||
// .get(
|
|
||||||
// `/txs/?address=bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4&pageNum=0`
|
|
||||||
// )
|
|
||||||
// .reply(200, mockData.mockTransactions)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Call the details API.
|
|
||||||
const result = await fromXPubSingle(req, res)
|
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
|
||||||
|
|
||||||
// Assert that required fields exist in the returned object.
|
|
||||||
assert.exists(result.legacyAddress)
|
|
||||||
assert.exists(result.cashAddress)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -113,10 +113,10 @@ describe('#Electrumx', () => {
|
|||||||
const result = await electrumxRoute.getBalance(req, res)
|
const result = await electrumxRoute.getBalance(req, res)
|
||||||
// console.log(`result: ${util.inspect(result)}`)
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
assert.equal(res.statusCode, 422, 'Expect 422 status code')
|
assert.equal(res.statusCode, 400, 'Expect 400 status code')
|
||||||
|
|
||||||
assert.property(result, 'error')
|
assert.property(result, 'error')
|
||||||
assert.include(result.error, 'Unsupported address format')
|
assert.include(result.error, 'address is empty')
|
||||||
|
|
||||||
assert.property(result, 'success')
|
assert.property(result, 'success')
|
||||||
assert.equal(result.success, false)
|
assert.equal(result.success, false)
|
||||||
@@ -207,7 +207,33 @@ describe('#Electrumx', () => {
|
|||||||
assert.property(result.balance, 'confirmed')
|
assert.property(result.balance, 'confirmed')
|
||||||
assert.property(result.balance, 'unconfirmed')
|
assert.property(result.balance, 'unconfirmed')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should get balance for a single eCash address', async () => {
|
||||||
|
req.params.address =
|
||||||
|
'ecash:qr5c4hfy52zn87484cucvzle5pljz0gtr5vhtw9z09'
|
||||||
|
|
||||||
|
// Mock unit tests to prevent live network calls.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
electrumxRoute.isReady = true // Force flag.
|
||||||
|
|
||||||
|
sandbox
|
||||||
|
.stub(electrumxRoute.axios, 'get')
|
||||||
|
.resolves({ data: mockData.balance })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the details API.
|
||||||
|
const result = await electrumxRoute.getBalance(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.property(result, 'success')
|
||||||
|
assert.equal(result.success, true)
|
||||||
|
|
||||||
|
assert.property(result, 'balance')
|
||||||
|
assert.property(result.balance, 'confirmed')
|
||||||
|
assert.property(result.balance, 'unconfirmed')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#balanceBulk', () => {
|
describe('#balanceBulk', () => {
|
||||||
it('should throw 400 if addresses is empty', async () => {
|
it('should throw 400 if addresses is empty', async () => {
|
||||||
const result = await electrumxRoute.balanceBulk(req, res)
|
const result = await electrumxRoute.balanceBulk(req, res)
|
||||||
@@ -239,7 +265,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.addresses = testArray
|
req.body.addresses = testArray
|
||||||
|
|
||||||
@@ -485,7 +511,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.addresses = testArray
|
req.body.addresses = testArray
|
||||||
|
|
||||||
@@ -729,7 +755,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.txids = testArray
|
req.body.txids = testArray
|
||||||
|
|
||||||
@@ -1065,7 +1091,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if heights array is too large', async () => {
|
it('should throw 400 error if heights array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.heights = testArray
|
req.body.heights = testArray
|
||||||
|
|
||||||
@@ -1261,7 +1287,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.addresses = testArray
|
req.body.addresses = testArray
|
||||||
|
|
||||||
@@ -1503,7 +1529,7 @@ describe('#Electrumx', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.addresses = testArray
|
req.body.addresses = testArray
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
TESTS FOR THE bcash/slp.js LIBRARY
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const assert = chai.assert
|
||||||
|
|
||||||
|
const sinon = require('sinon')
|
||||||
|
|
||||||
|
const BcashSlp = require('../../src/routes/v5/bcash/slp')
|
||||||
|
|
||||||
|
// Mocking data.
|
||||||
|
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
||||||
|
const mockData = require('./mocks/bcash-slp-mock')
|
||||||
|
|
||||||
|
// Used for debugging.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
if (!process.env.BCASH_SERVER) process.env.BCASH_SERVER = 'http://localhost'
|
||||||
|
|
||||||
|
describe('#bcash-slp', () => {
|
||||||
|
let req, res
|
||||||
|
let sandbox
|
||||||
|
const bcashSlp = new BcashSlp()
|
||||||
|
// let electrumxRoute
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
if (!process.env.TEST) {
|
||||||
|
process.env.TEST = 'unit'
|
||||||
|
}
|
||||||
|
console.log(`Testing type is: ${process.env.TEST}`)
|
||||||
|
|
||||||
|
if (!process.env.NETWORK) process.env.NETWORK = 'testnet'
|
||||||
|
|
||||||
|
// Connect to electrumx servers if this is an integration test.
|
||||||
|
// if (process.env.TEST === 'integration') {
|
||||||
|
// await electrumxRoute.connect()
|
||||||
|
// console.log('Connected to ElectrumX server')
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
// console.log(`electrumxRoute.electrumx: `, electrumxRoute.electrumx)
|
||||||
|
// Disconnect from the electrumx server if this is an integration test.
|
||||||
|
// if (process.env.TEST === 'integration') {
|
||||||
|
// await electrumxRoute.disconnect()
|
||||||
|
// console.log('Disconnected from ElectrumX server')
|
||||||
|
// }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup the mocks before each test.
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the req and res objects used by Express routes.
|
||||||
|
req = mockReq
|
||||||
|
res = mockRes
|
||||||
|
|
||||||
|
// Explicitly reset the parmas and body.
|
||||||
|
req.params = {}
|
||||||
|
req.body = {}
|
||||||
|
req.query = {}
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
|
||||||
|
// electrumxRoute = new ElecrumxRoute()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
//
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#root', () => {
|
||||||
|
// root route handler.
|
||||||
|
const root = bcashSlp.root
|
||||||
|
|
||||||
|
it('should respond to GET for base route', async () => {
|
||||||
|
const result = root(req, res)
|
||||||
|
|
||||||
|
assert.equal(result.status, 'bcash-slp', 'Returns static string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getUtxos', () => {
|
||||||
|
it('should return UTXOs for an address, hydrated with SLP info', async () => {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.utxos })
|
||||||
|
sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo)
|
||||||
|
req.params.address =
|
||||||
|
'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
|
||||||
|
const utxos = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.isArray(utxos)
|
||||||
|
const utxo = utxos[0]
|
||||||
|
|
||||||
|
assert.property(utxo, 'version')
|
||||||
|
assert.property(utxo, 'height')
|
||||||
|
assert.property(utxo, 'value')
|
||||||
|
assert.property(utxo, 'script')
|
||||||
|
assert.property(utxo, 'address')
|
||||||
|
assert.property(utxo, 'coinbase')
|
||||||
|
assert.property(utxo, 'hash')
|
||||||
|
assert.property(utxo, 'index')
|
||||||
|
assert.property(utxo, 'slp')
|
||||||
|
|
||||||
|
assert.property(utxo.slp, 'tokenId')
|
||||||
|
assert.property(utxo.slp, 'ticker')
|
||||||
|
assert.property(utxo.slp, 'name')
|
||||||
|
assert.property(utxo.slp, 'uri')
|
||||||
|
assert.property(utxo.slp, 'hash')
|
||||||
|
assert.property(utxo.slp, 'decimals')
|
||||||
|
assert.property(utxo.slp, 'vout')
|
||||||
|
assert.property(utxo.slp, 'value')
|
||||||
|
assert.property(utxo.slp, 'type')
|
||||||
|
})
|
||||||
|
it('should handle error if address is array', async () => {
|
||||||
|
req.params.address = [
|
||||||
|
'bchtest:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'address can not be an array. Use POST for bulk upload.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should throw error if address has invalid format', async () => {
|
||||||
|
sandbox.stub(bcashSlp.routeUtils, 'validateNetwork').returns(false)
|
||||||
|
|
||||||
|
req.params.address = 'qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
|
||||||
|
const result = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should catch axios error', async () => {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error'))
|
||||||
|
req.params.address =
|
||||||
|
'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk'
|
||||||
|
const result = await bcashSlp.getUtxos(req, res)
|
||||||
|
assert.include(result.error, 'test error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#hydrateUTXOS', () => {
|
||||||
|
it('should hydrate slp utxos', async () => {
|
||||||
|
sandbox.stub(bcashSlp, 'getTokenInfo').resolves(mockData.tokenInfo)
|
||||||
|
|
||||||
|
const hydratedUtxos = await bcashSlp.hydrateUTXOS(mockData.utxos)
|
||||||
|
assert.isArray(hydratedUtxos)
|
||||||
|
const hydratedUtxo = hydratedUtxos[0]
|
||||||
|
assert.property(hydratedUtxo, 'version')
|
||||||
|
assert.property(hydratedUtxo, 'height')
|
||||||
|
assert.property(hydratedUtxo, 'value')
|
||||||
|
assert.property(hydratedUtxo, 'script')
|
||||||
|
assert.property(hydratedUtxo, 'address')
|
||||||
|
assert.property(hydratedUtxo, 'coinbase')
|
||||||
|
assert.property(hydratedUtxo, 'hash')
|
||||||
|
assert.property(hydratedUtxo, 'index')
|
||||||
|
assert.property(hydratedUtxo, 'slp')
|
||||||
|
|
||||||
|
assert.property(hydratedUtxo.slp, 'tokenId')
|
||||||
|
assert.property(hydratedUtxo.slp, 'ticker')
|
||||||
|
assert.property(hydratedUtxo.slp, 'name')
|
||||||
|
assert.property(hydratedUtxo.slp, 'uri')
|
||||||
|
assert.property(hydratedUtxo.slp, 'hash')
|
||||||
|
assert.property(hydratedUtxo.slp, 'decimals')
|
||||||
|
assert.property(hydratedUtxo.slp, 'vout')
|
||||||
|
assert.property(hydratedUtxo.slp, 'value')
|
||||||
|
assert.property(hydratedUtxo.slp, 'type')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error if utxos is not provided', async () => {
|
||||||
|
try {
|
||||||
|
await bcashSlp.hydrateUTXOS()
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'UTXOs must be an array of slp utxos')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should handle error', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(bcashSlp, 'getTokenInfo').throws(new Error('test error'))
|
||||||
|
|
||||||
|
await bcashSlp.getTokenInfo(mockData.utxos)
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTokenInfo', () => {
|
||||||
|
it('should return SLP info for an token id', async () => {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').resolves({ data: mockData.tokenInfo })
|
||||||
|
|
||||||
|
const tokenId =
|
||||||
|
'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900'
|
||||||
|
const slpInfo = await bcashSlp.getTokenInfo(tokenId)
|
||||||
|
assert.isObject(slpInfo)
|
||||||
|
assert.property(slpInfo, 'tokenId')
|
||||||
|
assert.property(slpInfo, 'ticker')
|
||||||
|
assert.property(slpInfo, 'name')
|
||||||
|
assert.property(slpInfo, 'uri')
|
||||||
|
assert.property(slpInfo, 'hash')
|
||||||
|
assert.property(slpInfo, 'decimals')
|
||||||
|
})
|
||||||
|
it('should throw error if token id is not provided', async () => {
|
||||||
|
try {
|
||||||
|
await bcashSlp.getTokenInfo()
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'tokenId must be string')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should catch axios error', async () => {
|
||||||
|
try {
|
||||||
|
sandbox.stub(bcashSlp.axios, 'get').throws(new Error('test error'))
|
||||||
|
const tokenId =
|
||||||
|
'afd88e9afab110e7b75410417edb5c98798c08aa892cee8d97b44f2e5545a900'
|
||||||
|
await bcashSlp.getTokenInfo(tokenId)
|
||||||
|
assert.fail('Unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(error.message, 'test error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('#errorHandler', () => {
|
||||||
|
it('should handle unexpected errors', () => {
|
||||||
|
sandbox.stub(bcashSlp.routeUtils, 'decodeError').returns({ msg: false })
|
||||||
|
|
||||||
|
const result = bcashSlp.errorHandler(new Error('test error'), res)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
assert.equal(res.statusCode, 400, 'HTTP status code 400 expected.')
|
||||||
|
assert.property(result, 'error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -456,7 +456,7 @@ describe('#BlockchainRouter', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.hashes = testArray
|
req.body.hashes = testArray
|
||||||
|
|
||||||
@@ -1006,7 +1006,7 @@ describe('#BlockchainRouter', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.txids = testArray
|
req.body.txids = testArray
|
||||||
|
|
||||||
@@ -1525,7 +1525,7 @@ describe('#BlockchainRouter', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.txids = testArray
|
req.body.txids = testArray
|
||||||
|
|
||||||
@@ -1729,7 +1729,7 @@ describe('#BlockchainRouter', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.proofs = testArray
|
req.body.proofs = testArray
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
This library contains mocking data for running unit tests on the bcash route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const tokenInfo = {
|
||||||
|
tokenId: 'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84',
|
||||||
|
ticker: 'PSI',
|
||||||
|
name: 'Psidium',
|
||||||
|
uri: '',
|
||||||
|
hash: '',
|
||||||
|
decimals: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
const utxos = [
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
height: 707135,
|
||||||
|
value: 546,
|
||||||
|
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
|
||||||
|
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
|
||||||
|
coinbase: false,
|
||||||
|
hash: '77e514d59c0c866e791d08ddab73a1f7827be5f1ca15c1e05515dac9b271b596',
|
||||||
|
index: 2,
|
||||||
|
slp: {
|
||||||
|
vout: 2,
|
||||||
|
tokenId:
|
||||||
|
'5f2914840d0fd82689bdf0a0f8194ea363b9d0e2bd69b01608e18e1c6a4b9861',
|
||||||
|
value: '9999777700000',
|
||||||
|
type: 'SEND'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
height: 707135,
|
||||||
|
value: 546,
|
||||||
|
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
|
||||||
|
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
|
||||||
|
coinbase: false,
|
||||||
|
hash: '350aab06bf9523c062692f62622cb28b18cff9e4aa92be5476143336da217e98',
|
||||||
|
index: 1,
|
||||||
|
slp: {
|
||||||
|
vout: 1,
|
||||||
|
tokenId:
|
||||||
|
'2d860662801067828be3c4f504ce31b2a1a36c2cdbc6d92f6160920f66b0a9ee',
|
||||||
|
value: '1',
|
||||||
|
type: 'SEND'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
height: 687335,
|
||||||
|
value: 546,
|
||||||
|
script: '76a91466b2156f71629c89f5bf882cb3920b0e1e4d4fa888ac',
|
||||||
|
address: 'bitcoincash:qpnty9t0w93fez04h7yzevujpv8pun204qv6yfuahk',
|
||||||
|
coinbase: false,
|
||||||
|
hash: 'b7a0f52a344b1169292c757a36c62dbdaaa4ad684e96837eec732a9a2033dfd1',
|
||||||
|
index: 1,
|
||||||
|
slp: {
|
||||||
|
vout: 1,
|
||||||
|
tokenId:
|
||||||
|
'c7cb019764df3a352d9433749330b4b2eb022d8fbc101e68a6943a7a58a8ee84',
|
||||||
|
value: '100000000',
|
||||||
|
type: 'SEND'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tokenInfo,
|
||||||
|
utxos
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
This library contains mocking data for running unit tests on the psf-slp-indexer route.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const tokenStats = {
|
||||||
|
tokenData: {
|
||||||
|
type: 1,
|
||||||
|
ticker: 'TP03',
|
||||||
|
name: 'Test Plugin 03',
|
||||||
|
tokenId: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669',
|
||||||
|
documentUri: 'fullstack.cash',
|
||||||
|
documentHash: 'i\u0004���3��s\u0003�tz}�/��P�ǚ�Z>T��)��',
|
||||||
|
decimals: 0,
|
||||||
|
mintBatonIsActive: false,
|
||||||
|
tokensInCirculationBN: '1',
|
||||||
|
tokensInCirculationStr: '1',
|
||||||
|
blockCreated: 722420,
|
||||||
|
totalBurned: '0',
|
||||||
|
totalMinted: '1',
|
||||||
|
txs: [
|
||||||
|
{
|
||||||
|
txid: '13cad617d523c8eb4ab11fff19c010e0e0a4ea4360b58e0c8c955a45a146a669',
|
||||||
|
height: 722420,
|
||||||
|
type: 'GENESIS',
|
||||||
|
qty: '1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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: {
|
||||||
|
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:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s',
|
||||||
|
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 balance = {
|
||||||
|
balance: {
|
||||||
|
utxos: [
|
||||||
|
{
|
||||||
|
txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9',
|
||||||
|
vout: 1,
|
||||||
|
type: 'token',
|
||||||
|
qty: '1800',
|
||||||
|
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||||
|
address: 'bitcoincash:qrqy3kj7r822ps6628vwqq5k8hyjl6ey3y4eea2m4s'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
txs: [
|
||||||
|
{
|
||||||
|
txid: '078b2c48ed1db0d5d5996f2889b8d847a49200d0a781f6aa6752f740f312688f',
|
||||||
|
height: 717796
|
||||||
|
},
|
||||||
|
{
|
||||||
|
txid: 'a24a6a4abf06fabd799ecea4f8fac6a9ff21e6a8dd6169a3c2ebc03665329db9',
|
||||||
|
height: 717832
|
||||||
|
}
|
||||||
|
],
|
||||||
|
balances: [
|
||||||
|
{
|
||||||
|
tokenId: 'a4fb5c2da1aa064e25018a43f9165040071d9e984ba190c222a7f59053af84b2',
|
||||||
|
qty: '1800'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: {
|
||||||
|
startBlockHeight: 543376,
|
||||||
|
syncedBlockHeight: 722860,
|
||||||
|
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,
|
||||||
|
transactions,
|
||||||
|
immutableData,
|
||||||
|
mutableData,
|
||||||
|
decodedOpReturn
|
||||||
|
}
|
||||||
@@ -0,0 +1,931 @@
|
|||||||
|
/*
|
||||||
|
TESTS FOR THE PSF-SLP-INDEXER.JS LIBRARY
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const chai = require('chai')
|
||||||
|
const assert = chai.assert
|
||||||
|
const PsfSlpIndexerRouter = require('../../src/routes/v5/psf-slp-indexer')
|
||||||
|
const uut = new PsfSlpIndexerRouter()
|
||||||
|
|
||||||
|
// const nock = require('nock') // HTTP mocking
|
||||||
|
const sinon = require('sinon')
|
||||||
|
let originalEnvVars // Used during transition from integration to unit tests.
|
||||||
|
|
||||||
|
// Mocking data.
|
||||||
|
const { mockReq, mockRes } = require('./mocks/express-mocks')
|
||||||
|
const mockData = require('./mocks/psf-slp-indexer-mocks')
|
||||||
|
|
||||||
|
// Used for debugging.
|
||||||
|
const util = require('util')
|
||||||
|
util.inspect.defaultOptions = { depth: 1 }
|
||||||
|
|
||||||
|
describe('#PsfSlpIndexer', () => {
|
||||||
|
let req, res
|
||||||
|
let sandbox
|
||||||
|
before(() => {
|
||||||
|
// Save existing environment variables.
|
||||||
|
originalEnvVars = {
|
||||||
|
SLP_INDEXER_API: process.env.SLP_INDEXER_API
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default environment variables for unit tests.
|
||||||
|
if (!process.env.TEST) process.env.TEST = 'unit'
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
process.env.SLP_INDEXER_API = 'http://fakeurl/api/'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup the mocks before each test.
|
||||||
|
beforeEach(() => {
|
||||||
|
// Mock the req and res objects used by Express routes.
|
||||||
|
req = mockReq
|
||||||
|
res = mockRes
|
||||||
|
|
||||||
|
// Explicitly reset the parmas and body.
|
||||||
|
req.params = {}
|
||||||
|
req.body = {}
|
||||||
|
req.query = {}
|
||||||
|
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore Sandbox
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
// Restore any pre-existing environment variables.
|
||||||
|
process.env.SLP_INDEXER_API = originalEnvVars.SLP_INDEXER_API
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#root', async () => {
|
||||||
|
// root route handler.
|
||||||
|
it('should respond to GET for base route', async () => {
|
||||||
|
const result = uut.root(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.equal(result.status, 'psf-slp-indexer', 'Returns static string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTokenStats', async () => {
|
||||||
|
it('should throw 400 error if tokenId is missing', async () => {
|
||||||
|
const result = await uut.getTokenStats(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.getTokenStats(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.getTokenStats(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.getTokenStats(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 stats', 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 })
|
||||||
|
} else {
|
||||||
|
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')
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTxid', async () => {
|
||||||
|
it('should throw 400 error if txid is missing', async () => {
|
||||||
|
const result = await uut.getTxid(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
assert.include(result.error, 'txid can not be empty')
|
||||||
|
})
|
||||||
|
it('should throw 400 error if txid is invalid format', async () => {
|
||||||
|
req.body.txid = 'txid'
|
||||||
|
|
||||||
|
const result = await uut.getTxid(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
assert.include(result.error, 'This is not a txid')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw 503 when network issues', async () => {
|
||||||
|
req.body.txid =
|
||||||
|
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
|
||||||
|
|
||||||
|
// 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.getTxid(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.txid =
|
||||||
|
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
|
||||||
|
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
const result = await uut.getTxid(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.txid =
|
||||||
|
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
|
||||||
|
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
const result = await uut.getTxid(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 tx data', async function () {
|
||||||
|
req.body.txid =
|
||||||
|
'f3e14cd871402a766e85045dc552f2c1e87857dd3ea1b15efab6334ccef5e315'
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox.stub(uut.axios, 'post').resolves({ data: mockData.txData })
|
||||||
|
} else {
|
||||||
|
return this.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await uut.getTxid(req, res)
|
||||||
|
const txData = result.txData
|
||||||
|
assert.property(txData, 'txid')
|
||||||
|
assert.property(txData, 'hash')
|
||||||
|
assert.property(txData, 'version')
|
||||||
|
assert.property(txData, 'size')
|
||||||
|
assert.property(txData, 'locktime')
|
||||||
|
assert.property(txData, 'vin')
|
||||||
|
assert.property(txData, 'vout')
|
||||||
|
assert.property(txData, 'hex')
|
||||||
|
assert.property(txData, 'blockhash')
|
||||||
|
assert.property(txData, 'confirmations')
|
||||||
|
assert.property(txData, 'time')
|
||||||
|
assert.property(txData, 'blocktime')
|
||||||
|
assert.property(txData, 'blockheight')
|
||||||
|
assert.property(txData, 'isSlpTx')
|
||||||
|
assert.property(txData, 'tokenTxType')
|
||||||
|
assert.property(txData, 'tokenId')
|
||||||
|
assert.property(txData, 'tokenType')
|
||||||
|
assert.property(txData, 'tokenTicker')
|
||||||
|
assert.property(txData, 'tokenName')
|
||||||
|
assert.property(txData, 'tokenDecimals')
|
||||||
|
assert.property(txData, 'tokenUri')
|
||||||
|
assert.property(txData, 'tokenDocHash')
|
||||||
|
assert.property(txData, 'isValidSlp')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getAddress', async () => {
|
||||||
|
it('should throw 400 error if address is missing', async () => {
|
||||||
|
const result = await uut.getAddress(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
assert.include(result.error, 'address can not be empty')
|
||||||
|
})
|
||||||
|
it('should throw 400 error if address is invalid format', async () => {
|
||||||
|
req.body.address = 'address'
|
||||||
|
|
||||||
|
const result = await uut.getAddress(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'Invalid BCH address. Double check your address is valid'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should throw 400 error for invalid network address', async () => {
|
||||||
|
req.body.address = 'bchtest:qq89kjkeqz9mngp8kl3dpmu43y2wztdjqu500gn4c4'
|
||||||
|
|
||||||
|
const result = await uut.getAddress(req, res)
|
||||||
|
// console.log(`result: ${util.inspect(result)}`)
|
||||||
|
|
||||||
|
assert.hasAllKeys(result, ['error', 'success'])
|
||||||
|
assert.isFalse(result.success)
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'Invalid network. Trying to use a testnet address on mainnet, or vice versa.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw 503 when network issues', async () => {
|
||||||
|
req.body.address =
|
||||||
|
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
|
||||||
|
|
||||||
|
// 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.getAddress(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.address =
|
||||||
|
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
|
||||||
|
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
const result = await uut.getAddress(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.address =
|
||||||
|
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
|
||||||
|
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'post').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
const result = await uut.getAddress(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 address data', async function () {
|
||||||
|
req.body.address =
|
||||||
|
'bitcoincash:qzmd5vxgh9m22m6fgvm57yd6kjnjl9qnwywsf3583n'
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
it('should throw 503 when network issues', async () => {
|
||||||
|
// 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.getStatus(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 () => {
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'get').throws({ code: 'ECONNABORTED' })
|
||||||
|
|
||||||
|
const result = await uut.getStatus(req, res)
|
||||||
|
// console.log(`result: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
assert.isAbove(res.statusCode, 499, 'HTTP status code 503 expected.')
|
||||||
|
assert.include(
|
||||||
|
result.error,
|
||||||
|
'Could not communicate with full node',
|
||||||
|
'Error message expected'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns proper error when downstream service is down', async () => {
|
||||||
|
// Mock the timeout error.
|
||||||
|
sandbox.stub(uut.axios, 'get').throws({ code: 'ECONNREFUSED' })
|
||||||
|
|
||||||
|
const result = await uut.getStatus(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 indexer status', async function () {
|
||||||
|
// Mock the RPC call for unit tests.
|
||||||
|
if (process.env.TEST === 'unit') {
|
||||||
|
sandbox.stub(uut.axios, 'get').resolves({ data: mockData.status })
|
||||||
|
} else {
|
||||||
|
return this.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await uut.getStatus(req, res)
|
||||||
|
const status = result.status
|
||||||
|
assert.property(status, 'startBlockHeight')
|
||||||
|
assert.property(status, 'syncedBlockHeight')
|
||||||
|
assert.property(status, 'chainBlockHeight')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#errorHandler', () => {
|
||||||
|
it('should handle unexpected errors', () => {
|
||||||
|
sandbox.stub(uut.routeUtils, 'decodeError').returns({ msg: false })
|
||||||
|
|
||||||
|
const result = uut.errorHandler(new Error('test error'), res)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
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 })
|
||||||
|
|
||||||
|
const result = uut.errorHandler({}, res)
|
||||||
|
// console.log('result: ', result)
|
||||||
|
assert.equal(res.statusCode, 500, 'HTTP status code 500 expected.')
|
||||||
|
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
|
||||||
|
try {
|
||||||
|
process.env.SLP_INDEXER_API = ''
|
||||||
|
uut.checkEnvVar()
|
||||||
|
assert.fail('unexpected code path')
|
||||||
|
} catch (error) {
|
||||||
|
assert.include(
|
||||||
|
error.message,
|
||||||
|
'SLP_INDEXER_API env var not set. Can not connect to PSF SLP indexer.',
|
||||||
|
'Error message expected'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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', () => {
|
describe('#trackRateLimits', () => {
|
||||||
it('should apply anonymous rate limits if no JWT token is provided', async () => {
|
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'
|
req.ip = '127.0.0.1'
|
||||||
|
|
||||||
const result = await uut.trackRateLimits(req, res)
|
const result = await uut.trackRateLimits(req, res)
|
||||||
@@ -278,6 +281,9 @@ describe('#rate-routelimit', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should apply rate limits to anonymous users', async () => {
|
it('should apply rate limits to anonymous users', async () => {
|
||||||
|
// Mock sleep function
|
||||||
|
sandbox.stub(uut, 'sleep').resolves()
|
||||||
|
|
||||||
req.ip = '123.456.7.8'
|
req.ip = '123.456.7.8'
|
||||||
|
|
||||||
// console.log('next.callCount: ', next.callCount)
|
// 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 () => {
|
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'
|
req.ip = '123.456.7.8'
|
||||||
|
|
||||||
// force req.locals.jwtToken to be empty.
|
// force req.locals.jwtToken to be empty.
|
||||||
@@ -317,7 +326,7 @@ describe('#rate-routelimit', () => {
|
|||||||
assert.property(val, 'error')
|
assert.property(val, 'error')
|
||||||
assert.include(
|
assert.include(
|
||||||
val.error,
|
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')
|
assert.equal(res.locals.rateLimitTriggered, true, 'Rate limits triggered')
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ describe('#Raw-Transactions', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if hexes array is too large', async () => {
|
it('should throw 400 error if hexes array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.hexes = testArray
|
req.body.hexes = testArray
|
||||||
|
|
||||||
@@ -459,7 +459,7 @@ describe('#Raw-Transactions', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if hexes array is too large', async () => {
|
it('should throw 400 error if hexes array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.hexes = testArray
|
req.body.hexes = testArray
|
||||||
|
|
||||||
@@ -599,7 +599,7 @@ describe('#Raw-Transactions', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if txids array is too large', async () => {
|
it('should throw 400 error if txids array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.txids = testArray
|
req.body.txids = testArray
|
||||||
|
|
||||||
@@ -885,7 +885,7 @@ describe('#Raw-Transactions', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if hexs array is too large', async () => {
|
it('should throw 400 error if hexs array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.hexes = testArray
|
req.body.hexes = testArray
|
||||||
|
|
||||||
|
|||||||
+10
-2284
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -203,7 +203,7 @@ describe('#Util', () => {
|
|||||||
|
|
||||||
it('should throw 400 error if addresses array is too large', async () => {
|
it('should throw 400 error if addresses array is too large', async () => {
|
||||||
const testArray = []
|
const testArray = []
|
||||||
for (var i = 0; i < 25; i++) testArray.push('')
|
for (let i = 0; i < 25; i++) testArray.push('')
|
||||||
|
|
||||||
req.body.addresses = testArray
|
req.body.addresses = testArray
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user