mirror of
https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-service.git
synced 2026-09-22 09:12:02 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e31dceb07f | ||
|
|
af0e727ba8 | ||
|
|
6f64fc4652 | ||
|
|
e91dd7f834 | ||
|
|
e717124fc7 | ||
|
|
3c8d2cfdd3 | ||
|
|
71fbebe672 | ||
|
|
cab819081a | ||
|
|
4f0adbb773 | ||
|
|
e04cb1f05a | ||
|
|
6f619db666 | ||
|
|
126bee86bd | ||
|
|
3761d139f1 | ||
|
|
006727cc9d | ||
|
|
bcd2a54aa7 | ||
|
|
70155c7b90 | ||
|
|
7035e1777b | ||
|
|
ed82c1102e | ||
|
|
baad128ba2 | ||
|
|
57ce5c12f5 | ||
|
|
6aa2e1e44a | ||
|
|
762e07e662 | ||
|
|
720508e7c4 | ||
|
|
8a558f212f | ||
|
|
4223262323 | ||
|
|
e68200ed9c | ||
|
|
322a10cd74 | ||
|
|
32af7a7233 | ||
|
|
f68e63f7d6 | ||
|
|
568b2df7d0 | ||
|
|
1472a4868a |
@@ -10,6 +10,15 @@ This repository is forked from the [ipfs-service-provider](https://github.com/Pe
|
||||
|
||||
As part of the end-to-end testing, a virtual client can be started that will interrogate every endpoint provided by this API server. This client can be forked by developers to write their own apps that use this IPFS-based service.
|
||||
|
||||
## Roadmap
|
||||
|
||||
This repository is one step in a longer path. Here are the major milestones of this path:
|
||||
|
||||
- Add wallet-based calls to ipfs-bch-wallet-service (this repository).
|
||||
- Create a miniature version of bch-js that makes calls over JSON RPC (bch-js-ipfs).
|
||||
- Given a variable, minimal-slp-wallet could call bch-js or bch-js-ipfs for network calls.
|
||||
- Fork slp-cli-wallet and add `daemon` feature to spin up IPFS node and work with bch-js-ipfs.
|
||||
|
||||
## Requirements
|
||||
|
||||
- node **^14.17.0**
|
||||
|
||||
+9
-3
@@ -54,7 +54,7 @@ async function startServer () {
|
||||
|
||||
// Attach REST API and JSON RPC controllers to the app.
|
||||
const controllers = require('../src/controllers')
|
||||
controllers.attachControllers(app)
|
||||
await controllers.attachControllers(app)
|
||||
|
||||
// Enable CORS for testing
|
||||
// THIS IS A SECURITY RISK. COMMENT OUT FOR PRODUCTION
|
||||
@@ -69,8 +69,14 @@ async function startServer () {
|
||||
console.log(`Server started on ${config.port}`)
|
||||
|
||||
// Create the system admin user.
|
||||
const success = await adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
try {
|
||||
const success = await adminLib.createSystemUser()
|
||||
if (success) console.log('System admin user created.')
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Error trying to create system admin. Perhaps one already exists?'
|
||||
)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
Vendored
+30
-7
@@ -13,9 +13,27 @@ module.exports = {
|
||||
logPass: 'test',
|
||||
|
||||
// Email server settings if nodemailer email notifications are used.
|
||||
emailServer: process.env.EMAILSERVER ? process.env.EMAILSERVER : 'mail.someserver.com',
|
||||
emailUser: process.env.EMAILUSER ? process.env.EMAILUSER : 'noreply@someserver.com',
|
||||
emailPassword: process.env.EMAILPASS ? process.env.EMAILPASS : 'emailpassword',
|
||||
emailServer: process.env.EMAILSERVER
|
||||
? process.env.EMAILSERVER
|
||||
: 'mail.someserver.com',
|
||||
emailUser: process.env.EMAILUSER
|
||||
? process.env.EMAILUSER
|
||||
: 'noreply@someserver.com',
|
||||
emailPassword: process.env.EMAILPASS
|
||||
? process.env.EMAILPASS
|
||||
: 'emailpassword',
|
||||
|
||||
// FullStack.cash account information, used for automatic JWT handling.
|
||||
AUTHSERVER: process.env.AUTHSERVER
|
||||
? process.env.AUTHSERVER
|
||||
: 'https://auth.fullstack.cash',
|
||||
APISERVER: process.env.APISERVER
|
||||
? process.env.APISERVER
|
||||
: 'https://api.fullstack.cash/v5/',
|
||||
FULLSTACKLOGIN: process.env.FULLSTACKLOGIN
|
||||
? process.env.FULLSTACKLOGIN
|
||||
: 'demo@demo.com',
|
||||
FULLSTACKPASS: process.env.FULLSTACKPASS ? process.env.FULLSTACKPASS : 'demo',
|
||||
|
||||
// IPFS settings.
|
||||
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
|
||||
@@ -27,13 +45,18 @@ module.exports = {
|
||||
announceJsonLd: {
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'WebAPI',
|
||||
name: 'ipfs-service-provider',
|
||||
description: 'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
|
||||
documentation: 'https://ipfs-service-provider.fullstack.cash/',
|
||||
name: 'ipfs-bch-wallet-service',
|
||||
description:
|
||||
'IPFS service providing BCH blockchain access needed by a wallet.',
|
||||
documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/',
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: 'Permissionless Software Foundation',
|
||||
url: 'https://PSFoundation.cash'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// IPFS Ports
|
||||
ipfsTcpPort: process.env.IPFS_TCP_PORT ? process.env.IPFS_TCP_PORT : 5668,
|
||||
ipfsWsPort: process.env.IPFS_WS_PORT ? process.env.IPFS_WS_PORT : 5669
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -7,6 +7,6 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/ipfs-service-dev',
|
||||
database: 'mongodb://localhost:27017/bch-service-dev',
|
||||
env: 'dev'
|
||||
}
|
||||
|
||||
Vendored
+3
-1
@@ -10,6 +10,8 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://172.17.0.1:5555/ipfs-service-prod',
|
||||
database: process.env.DBURL
|
||||
? process.env.DBURL
|
||||
: 'mongodb://172.17.0.1:5555/bch-service-prod',
|
||||
env: 'prod'
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -7,6 +7,6 @@
|
||||
module.exports = {
|
||||
session: 'secret-boilerplate-token',
|
||||
token: 'secret-jwt-token',
|
||||
database: 'mongodb://localhost:27017/ipfs-service-test',
|
||||
database: 'mongodb://localhost:27017/bch-service-test',
|
||||
env: 'test'
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Start the testnet server with the command 'docker-compose up -d'
|
||||
|
||||
koa-mongodb:
|
||||
image: mongo
|
||||
container_name: mongo-koa
|
||||
ports:
|
||||
- "5555:27017" # <host port>:<container port>
|
||||
volumes:
|
||||
- ./database:/data/db
|
||||
command: mongod --smallfiles --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
koa:
|
||||
build: ./production/
|
||||
dockerfile: Dockerfile
|
||||
container_name: koa
|
||||
links:
|
||||
- koa-mongodb
|
||||
ports:
|
||||
- "5001:5001" # <host port>:<container port>
|
||||
volumes:
|
||||
# - ./logs:/home/coinjoin/consolidating-coinjoin/logs
|
||||
- ./keys:/home/safeuser/keys
|
||||
|
||||
restart: always
|
||||
Generated
+414
-488
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -8,7 +8,7 @@
|
||||
"test": "npm run test:all",
|
||||
"test:all": "export SVC_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
|
||||
"test:unit": "export SVC_ENV=test && mocha --exit --timeout 15000 --recursive test/unit/",
|
||||
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
|
||||
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 30000 test/e2e/automated/",
|
||||
"test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
|
||||
"lint": "standard --env mocha --fix",
|
||||
"docs": "./node_modules/.bin/apidoc -i src/ -o docs",
|
||||
@@ -23,14 +23,15 @@
|
||||
},
|
||||
"repository": "Permissionless-Software-Foundation/ipfs-bch-wallet-service",
|
||||
"dependencies": {
|
||||
"@psf/bch-js": "^4.20.1",
|
||||
"@psf/bch-js": "^4.20.2",
|
||||
"axios": "^0.21.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"glob": "^7.1.6",
|
||||
"ipfs": "^0.54.4",
|
||||
"ipfs-coord": "^3.2.0",
|
||||
"ipfs-coord": "^4.0.1",
|
||||
"jsonrpc-lite": "^2.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-bch-lib": "^1.3.0",
|
||||
"kcors": "^2.2.2",
|
||||
"koa": "^2.13.1",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
|
||||
+11
-10
@@ -29,13 +29,13 @@ RUN echo 'abcd8765' | sudo -S pwd
|
||||
|
||||
# Clone the rest.bitcoin.com repository
|
||||
WORKDIR /home/safeuser
|
||||
RUN git clone https://github.com/christroutner/koa-api-boilerplate
|
||||
RUN mv koa-api-boilerplate koa
|
||||
RUN mkdir keys
|
||||
RUN git clone https://github.com/Permissionless-Software-Foundation/ipfs-bch-wallet-service
|
||||
# RUN mv koa-api-boilerplate koa
|
||||
# RUN mkdir keys
|
||||
|
||||
# Switch to the desired branch. `master` is usually stable,
|
||||
# and `stage` has the most up-to-date changes.
|
||||
WORKDIR /home/safeuser/koa
|
||||
WORKDIR /home/safeuser/ipfs-bch-wallet-service
|
||||
|
||||
# For development: switch to unstable branch
|
||||
# RUN git checkout unstable
|
||||
@@ -46,13 +46,14 @@ RUN npm install
|
||||
# Generate the API docs
|
||||
RUN npm run docs
|
||||
|
||||
VOLUME /home/safeuser/keys
|
||||
|
||||
# Expose the port the API will be served on.
|
||||
EXPOSE 5001
|
||||
# Calling out the directorys that should be mounted as external volumes.
|
||||
RUN mkdir /home/safeuser/ipfsdata
|
||||
RUN mkdir /home/safeuser/orbitdb
|
||||
VOLUME /home/safeuser/ipfsdata
|
||||
VOLUME /home/safeuser/orbitdb
|
||||
|
||||
# Start the application.
|
||||
COPY start-production start-production
|
||||
CMD ["./start-production"]
|
||||
COPY start-production.sh start-production.sh
|
||||
CMD ["./start-production.sh"]
|
||||
|
||||
#CMD ["npm", "start"]
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Remove all untagged docker images.
|
||||
docker rmi $(docker images | grep "^<none>" | awk '{print $3}')
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Start the testnet server with the command 'docker-compose up -d'
|
||||
|
||||
bch-wallet-mongodb:
|
||||
image: mongo
|
||||
container_name: mongo-bch-service
|
||||
ports:
|
||||
- '5555:27017' # <host port>:<container port>
|
||||
volumes:
|
||||
- ./database:/data/db
|
||||
command: mongod --logpath=/dev/null # -- quiet
|
||||
restart: always
|
||||
|
||||
bch-wallet-service:
|
||||
build: ./
|
||||
dockerfile: Dockerfile
|
||||
container_name: bch-wallet-service
|
||||
links:
|
||||
- bch-wallet-mongodb
|
||||
ports:
|
||||
# <host port>:<container port>
|
||||
- '5001:5001' # REST API
|
||||
- '5668:5668' # IPFS TCP Port
|
||||
- '5669:5669' # IPFS WS Port
|
||||
volumes:
|
||||
- ../ipfsdata:/home/safeuser/ipfsdata
|
||||
- ../orbitdb:/home/safeuser/orbitdb
|
||||
|
||||
restart: always
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
export KOA_ENV=production
|
||||
npm start
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Customize these environment variables for your own installation.
|
||||
export AUTHSERVER=https://auth.fullstack.cash
|
||||
export APISERVER=https://api.fullstack.cash/v5/
|
||||
export FULLSTACKLOGIN=demo@demo.com
|
||||
export FULLSTACKPASS=demo
|
||||
|
||||
# Ports
|
||||
export PORT=5001 # REST API
|
||||
export IPFS_TCP_PORT=5668 # IPSF TCP Port
|
||||
export IPFS_WS_PORT=5669 # IPFS WS Port
|
||||
|
||||
# Production database connection string.
|
||||
export DBURL=mongodb://172.17.0.1:5555/ipfs-service-prod
|
||||
|
||||
#export ENABLE_CIRCUIT_RELAY=1
|
||||
|
||||
export SVC_ENV=production
|
||||
npm start
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
A library of utility functions for working with FullStack.cash JWT tokens.
|
||||
|
||||
Feel free to copy this library into your own app, as well as the unit tests
|
||||
for this file.
|
||||
*/
|
||||
|
||||
const JwtLib = require('jwt-bch-lib')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
|
||||
class FullStackJWT {
|
||||
constructor (localConfig = {}) {
|
||||
// Input Validation
|
||||
this.authServer = localConfig.authServer
|
||||
if (!this.authServer || typeof this.authServer !== 'string') {
|
||||
throw new Error(
|
||||
'Must pass a url for the AUTH server when instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
this.apiServer = localConfig.apiServer
|
||||
if (!this.apiServer || typeof this.apiServer !== 'string') {
|
||||
throw new Error(
|
||||
'Must pass a url for the API server when instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
this.login = localConfig.login
|
||||
if (!this.login || typeof this.login !== 'string') {
|
||||
throw new Error(
|
||||
'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
this.password = localConfig.password
|
||||
if (!this.password || typeof this.password !== 'string') {
|
||||
throw new Error(
|
||||
'Must pass a FullStack.cash account password when instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.jwtLib = new JwtLib({
|
||||
// Overwrite default values with the values in the config file.
|
||||
server: this.server,
|
||||
login: this.login,
|
||||
password: this.password
|
||||
})
|
||||
|
||||
// State
|
||||
this.apiToken = '' // Default value.
|
||||
this.bchjs = {}
|
||||
}
|
||||
|
||||
// Get's a JWT token from FullStack.cash.
|
||||
async getJWT () {
|
||||
try {
|
||||
// Skip connecting FullStack.cash auth server to the network if this is an E2E test.
|
||||
if (process.env.E2ETEST) {
|
||||
this.apiToken = 'faketoken'
|
||||
return this.apiToken
|
||||
}
|
||||
|
||||
// Log into the auth server.
|
||||
await this.jwtLib.register()
|
||||
|
||||
this.apiToken = this.jwtLib.userData.apiToken
|
||||
if (!this.apiToken) {
|
||||
throw new Error('This account does not have a JWT')
|
||||
}
|
||||
console.log(`Retrieved JWT token: ${this.apiToken}\n`)
|
||||
|
||||
// Ensure the JWT token is valid to use.
|
||||
const isValid = await this.jwtLib.validateApiToken()
|
||||
|
||||
// Get a new token with the same API level, if the existing token is not
|
||||
// valid (probably expired).
|
||||
if (!isValid.isValid) {
|
||||
this.apiToken = await this.jwtLib.getApiToken(
|
||||
this.jwtLib.userData.apiLevel
|
||||
)
|
||||
console.log(
|
||||
`The JWT token was not valid. Retrieved new JWT token: ${this.apiToken}\n`
|
||||
)
|
||||
} else {
|
||||
console.log('JWT token is valid.\n')
|
||||
}
|
||||
|
||||
return this.apiToken
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Error trying to log into ${this.server} and retrieve JWT token.`
|
||||
)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Create an instance of bchjs with the validated JWT token. Returns this
|
||||
// instance of bch-js.
|
||||
instanceBchjs () {
|
||||
this.bchjs = new BCHJS({
|
||||
restURL: this.apiServer,
|
||||
apiToken: this.apiToken
|
||||
})
|
||||
|
||||
return this.bchjs
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FullStackJWT
|
||||
+16
-1
@@ -4,6 +4,9 @@
|
||||
https://troutsblog.com/blog/clean-architecture
|
||||
*/
|
||||
|
||||
// Load the config file
|
||||
const config = require('../../config')
|
||||
|
||||
// Load individual adapter libraries.
|
||||
const IPFSAdapter = require('./ipfs')
|
||||
const LocalDB = require('./localdb')
|
||||
@@ -12,6 +15,7 @@ const Passport = require('./passport')
|
||||
const Nodemailer = require('./nodemailer')
|
||||
const { wlogger } = require('./wlogger')
|
||||
const JSONFiles = require('./json-files')
|
||||
const FullStackJWT = require('./fullstack-jwt')
|
||||
|
||||
// Instantiate adapter libraries.
|
||||
const ipfs = new IPFSAdapter()
|
||||
@@ -21,6 +25,15 @@ const passport = new Passport()
|
||||
const nodemailer = new Nodemailer()
|
||||
const jsonFiles = new JSONFiles()
|
||||
|
||||
// Get a valid JWT API key and instance bch-js.
|
||||
const fullStackJwt = new FullStackJWT({
|
||||
authServer: config.AUTHSERVER,
|
||||
apiServer: config.APISERVER,
|
||||
login: config.FULLSTACKLOGIN,
|
||||
password: config.FULLSTACKPASS
|
||||
})
|
||||
const bchjs = {} // Placeholder.
|
||||
|
||||
module.exports = {
|
||||
ipfs,
|
||||
localdb,
|
||||
@@ -28,5 +41,7 @@ module.exports = {
|
||||
passport,
|
||||
nodemailer,
|
||||
wlogger,
|
||||
jsonFiles
|
||||
jsonFiles,
|
||||
fullStackJwt,
|
||||
bchjs
|
||||
}
|
||||
|
||||
@@ -19,8 +19,15 @@ class IPFS {
|
||||
|
||||
// Provides a global start() function that triggers the start() function in
|
||||
// the underlying libraries.
|
||||
async start () {
|
||||
async start (localConfig = {}) {
|
||||
try {
|
||||
const bchjs = localConfig.bchjs
|
||||
if (!bchjs) {
|
||||
throw new Error(
|
||||
'Instance of bch-js must be passed when instantiating IPFS adapter.'
|
||||
)
|
||||
}
|
||||
|
||||
// Start IPFS
|
||||
await this.ipfsAdapter.start()
|
||||
console.log('IPFS is ready.')
|
||||
@@ -30,7 +37,8 @@ class IPFS {
|
||||
|
||||
// Start ipfs-coord
|
||||
this.ipfsCoordAdapter = new this.IpfsCoordAdapter({
|
||||
ipfs: this.ipfs
|
||||
ipfs: this.ipfs,
|
||||
bchjs
|
||||
})
|
||||
await this.ipfsCoordAdapter.start()
|
||||
console.log('ipfs-coord is ready.')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
// Global npm libraries
|
||||
const IpfsCoord = require('ipfs-coord')
|
||||
const BCHJS = require('@psf/bch-js')
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
|
||||
// Local libraries
|
||||
const config = require('../../../config')
|
||||
@@ -23,11 +23,16 @@ class IpfsCoordAdapter {
|
||||
'Instance of IPFS must be passed when instantiating ipfs-coord.'
|
||||
)
|
||||
}
|
||||
this.bchjs = localConfig.bchjs
|
||||
if (!this.bchjs) {
|
||||
throw new Error(
|
||||
'Instance of bch-js must be passed when instantiating ipfs-coord.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.IpfsCoord = IpfsCoord
|
||||
this.ipfsCoord = {}
|
||||
this.bchjs = new BCHJS()
|
||||
// this.rpc = new JSONRPC()
|
||||
this.config = config
|
||||
|
||||
@@ -37,7 +42,7 @@ class IpfsCoordAdapter {
|
||||
_this = this
|
||||
}
|
||||
|
||||
async start () {
|
||||
async start (localConfig = {}) {
|
||||
this.ipfsCoord = new this.IpfsCoord({
|
||||
ipfs: this.ipfs,
|
||||
type: 'node.js',
|
||||
@@ -49,8 +54,12 @@ class IpfsCoordAdapter {
|
||||
announceJsonLd: this.config.announceJsonLd
|
||||
})
|
||||
|
||||
// Wait for the ipfs-coord library to signal that it is ready.
|
||||
await this.ipfsCoord.isReady()
|
||||
// Skip connecting ipfs-coord to the network if this is an E2E test.
|
||||
if (!process.env.E2ETEST) {
|
||||
// Wait for the ipfs-coord library to signal that it is ready.
|
||||
await this.ipfsCoord.ipfs.start()
|
||||
await this.ipfsCoord.isReady()
|
||||
}
|
||||
|
||||
// Signal that this adapter is ready.
|
||||
this.isReady = true
|
||||
|
||||
@@ -63,6 +63,7 @@ class IpfsAdapter {
|
||||
|
||||
// Stop the IPFS node if we're running tests.
|
||||
if (this.config.env === 'test') {
|
||||
console.log('Stopping IPFS for tests.')
|
||||
await this.ipfs.stop()
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,24 @@ const RESTControllers = require('./rest-api')
|
||||
// Top-level function for this library.
|
||||
// Start the various Controllers and attach them to the app.
|
||||
async function attachControllers (app) {
|
||||
// Attach the REST controllers to the Koa app.
|
||||
attachRESTControllers(app)
|
||||
try {
|
||||
// Get a JWT token and instantiate bch-js with it. Then pass that instance
|
||||
// to all the rest of the apps controllers and adapters.
|
||||
await adapters.fullStackJwt.getJWT()
|
||||
// Instantiate bch-js with the JWT token, and overwrite the placeholder for bch-js.
|
||||
adapters.bchjs = await adapters.fullStackJwt.instanceBchjs()
|
||||
|
||||
// Start IPFS.
|
||||
await adapters.ipfs.start()
|
||||
// Attach the REST controllers to the Koa app.
|
||||
attachRESTControllers(app)
|
||||
|
||||
attachRPCControllers()
|
||||
// Start IPFS.
|
||||
await adapters.ipfs.start({ bchjs: adapters.bchjs })
|
||||
|
||||
attachRPCControllers()
|
||||
} catch (err) {
|
||||
console.error('Error in attachControllers()')
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function attachRESTControllers (app) {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
This is the JSON RPC router for the Fulcrum API
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
|
||||
// Local libraries
|
||||
// const UserLib = require('../../../use-cases/user')
|
||||
const Validators = require('../validators')
|
||||
const RateLimit = require('../rate-limit')
|
||||
|
||||
class BCHRPC {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating BCH JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating BCH JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// Encapsulate dependencies
|
||||
this.userLib = this.useCases.user
|
||||
this.jsonrpc = jsonrpc
|
||||
this.validators = new Validators(localConfig)
|
||||
this.rateLimit = new RateLimit()
|
||||
// this.bchjs = new BCHJS()
|
||||
this.bchjs = this.adapters.bchjs
|
||||
}
|
||||
|
||||
// Top-level router for this library. All other methods in this class are for
|
||||
// a specific endpoint. This method routes incoming calls to one of those
|
||||
// methods.
|
||||
async bchRouter (rpcData) {
|
||||
let endpoint = 'unknown'
|
||||
try {
|
||||
// console.log('fulcrumRouter rpcData: ', rpcData)
|
||||
|
||||
endpoint = rpcData.payload.params.endpoint
|
||||
// let user
|
||||
|
||||
// Route the call based on the value of the method property.
|
||||
switch (endpoint) {
|
||||
case 'transactions':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.transactions(rpcData)
|
||||
|
||||
case 'balance':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.balance(rpcData)
|
||||
|
||||
case 'utxos':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.utxos(rpcData)
|
||||
|
||||
case 'broadcast':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.broadcast(rpcData)
|
||||
|
||||
case 'transaction':
|
||||
await this.rateLimit.limiter(rpcData.from)
|
||||
return await this.transaction(rpcData)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in BCHRPC/rpcRouter()')
|
||||
// throw err
|
||||
|
||||
return {
|
||||
success: false,
|
||||
status: err.status || 500,
|
||||
message: err.message,
|
||||
endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /bch Transactions
|
||||
* @apiPermission public
|
||||
* @apiName Transactions
|
||||
* @apiGroup JSON BCH
|
||||
* @apiDescription This endpoint wraps the bchjs.Electrumx.transactions([]) function.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transactions", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"]}}
|
||||
*
|
||||
*/
|
||||
async transactions (rpcData) {
|
||||
try {
|
||||
// console.log('createUser rpcData: ', rpcData)
|
||||
|
||||
const addrs = rpcData.payload.params.addresses
|
||||
|
||||
const data = await this.bchjs.Electrumx.transactions(addrs)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
const retObj = data
|
||||
retObj.status = 200
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in createUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'transactions'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /bch Balance
|
||||
* @apiPermission public
|
||||
* @apiName Balance
|
||||
* @apiGroup JSON BCH
|
||||
* @apiDescription This endpoint wraps the bchjs.Electrumx.balance([]) function.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "balance", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"]}}
|
||||
*
|
||||
*/
|
||||
async balance (rpcData) {
|
||||
try {
|
||||
// console.log('createUser rpcData: ', rpcData)
|
||||
|
||||
const addrs = rpcData.payload.params.addresses
|
||||
|
||||
const data = await this.bchjs.Electrumx.balance(addrs)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
const retObj = data
|
||||
retObj.status = 200
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in createUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'balance'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /bch UTXOs
|
||||
* @apiPermission public
|
||||
* @apiName UTXOs
|
||||
* @apiGroup JSON BCH
|
||||
* @apiDescription This endpoint wraps the bchjs.Utxos.get() function. This
|
||||
* endpoint returns UTXOs held at an address, hydrated
|
||||
* with token information.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "utxos", "address": "bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"}}
|
||||
*
|
||||
*/
|
||||
async utxos (rpcData) {
|
||||
try {
|
||||
// console.log('createUser rpcData: ', rpcData)
|
||||
|
||||
const addr = rpcData.payload.params.address
|
||||
|
||||
const data = await this.bchjs.Utxo.get(addr)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
const retObj = data
|
||||
retObj.status = 200
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in createUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'utxos'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /bch Broadcast
|
||||
* @apiPermission public
|
||||
* @apiName Broadcast
|
||||
* @apiGroup JSON BCH
|
||||
* @apiDescription Broadcast a transaction to the BCH network.
|
||||
* The transaction should be encoded as a hexidecimal string.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "broadcast", "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000"}}
|
||||
*
|
||||
*/
|
||||
async broadcast (rpcData) {
|
||||
try {
|
||||
// console.log('createUser rpcData: ', rpcData)
|
||||
|
||||
const hex = rpcData.payload.params.hex
|
||||
|
||||
const data = await this.bchjs.RawTransactions.sendRawTransaction(hex)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
const retObj = data
|
||||
retObj.status = 200
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in createUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'broadcast'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {JSON} /bch Transaction
|
||||
* @apiPermission public
|
||||
* @apiName Transaction
|
||||
* @apiGroup JSON BCH
|
||||
* @apiDescription Get data about a specific transaction.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transaction", "txid": "01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b"}}
|
||||
*
|
||||
*/
|
||||
async transaction (rpcData) {
|
||||
try {
|
||||
// console.log('transaction rpcData: ', rpcData)
|
||||
|
||||
const txid = rpcData.payload.params.txid
|
||||
|
||||
const data = await this.bchjs.Transaction.get(txid.toString())
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
const retObj = data
|
||||
retObj.status = 200
|
||||
|
||||
return retObj
|
||||
} catch (err) {
|
||||
// console.error('Error in createUser()')
|
||||
// throw err
|
||||
|
||||
// Return an error response
|
||||
return {
|
||||
success: false,
|
||||
status: 422,
|
||||
message: err.message,
|
||||
endpoint: 'transaction'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BCHRPC
|
||||
@@ -10,6 +10,7 @@ const { wlogger } = require('../../adapters/wlogger')
|
||||
const UserController = require('./users')
|
||||
const AuthController = require('./auth')
|
||||
const AboutController = require('./about')
|
||||
const BCHController = require('./bch')
|
||||
|
||||
let _this
|
||||
|
||||
@@ -35,6 +36,7 @@ class JSONRPC {
|
||||
this.userController = new UserController(localConfig)
|
||||
this.authController = new AuthController(localConfig)
|
||||
this.aboutController = new AboutController()
|
||||
this.bchController = new BCHController(localConfig)
|
||||
|
||||
_this = this
|
||||
}
|
||||
@@ -80,6 +82,9 @@ class JSONRPC {
|
||||
break
|
||||
case 'about':
|
||||
retObj = await _this.aboutController.aboutRouter(parsedData)
|
||||
break
|
||||
case 'bch':
|
||||
retObj = await _this.bchController.bchRouter(parsedData)
|
||||
}
|
||||
|
||||
// console.log('retObj: ', retObj)
|
||||
|
||||
@@ -14,7 +14,7 @@ class RateLimit {
|
||||
// Set default rate limit options.
|
||||
this.defaultOptions = {
|
||||
interval: { min: 1 },
|
||||
max: 60,
|
||||
max: 600, // 600 RPM while prototyping.
|
||||
onLimitReached: this.onLimitReached
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class RateLimit {
|
||||
onLimitReached () {
|
||||
try {
|
||||
const error = new Error() // Establish provided options as the default options.
|
||||
error.message = 'Too many requests, please try again later.'
|
||||
error.message = 'Too many requests, please slow down your requests.'
|
||||
error.status = 429
|
||||
throw error
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
Controller for the /fulcrum REST API endpoints.
|
||||
*/
|
||||
|
||||
// const BCHJS = require('@psf/bch-js')
|
||||
|
||||
let _this
|
||||
|
||||
class BCHRESTController {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
// this.bchjs = new BCHJS()
|
||||
this.bchjs = this.adapters.bchjs
|
||||
|
||||
_this = this
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /bch/transactions Transactions
|
||||
* @apiName Transactions
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription This endpoint wraps the bchjs.Electrumx.transactions([]) function.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"] }' localhost:5001/bch/transactions
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
*
|
||||
* success:true,
|
||||
* data: <data>
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async transactions (ctx) {
|
||||
try {
|
||||
const addrs = ctx.request.body.addresses
|
||||
|
||||
const data = await _this.bchjs.Electrumx.transactions(addrs)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
ctx.body = data
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /bch/balance Balance
|
||||
* @apiName Balance
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription This endpoint returns the balance in BCH for an address.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"] }' localhost:5001/bch/balance
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* success:true,
|
||||
* data: <data>
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async balance (ctx) {
|
||||
try {
|
||||
const addrs = ctx.request.body.addresses
|
||||
|
||||
const data = await _this.bchjs.Electrumx.balance(addrs)
|
||||
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
|
||||
|
||||
ctx.body = data
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /bch/utxos UTXOs
|
||||
* @apiName UTXOs
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription This endpoint returns UTXOs held at an address, hydrated
|
||||
* with token information.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "address": "bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj" }' localhost:5001/bch/utxos
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* success:true,
|
||||
* data: <data>
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async utxos (ctx) {
|
||||
try {
|
||||
const address = ctx.request.body.address
|
||||
|
||||
const utxos = await _this.bchjs.Utxo.get(address)
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
ctx.body = utxos
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /bch/broadcast Broadcast
|
||||
* @apiName Broadcast
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription Broadcast a transaction to the BCH network.
|
||||
* The transaction should be encoded as a hexidecimal string.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000" }' localhost:5001/bch/broadcast
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* success:true,
|
||||
* data: <data>
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async broadcast (ctx) {
|
||||
try {
|
||||
const hex = ctx.request.body.hex
|
||||
|
||||
const txid = await _this.bchjs.RawTransactions.sendRawTransaction(hex)
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
ctx.body = txid
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /bch/transaction Transaction
|
||||
* @apiName Transaction
|
||||
* @apiGroup REST BCH
|
||||
* @apiDescription Get data about a specific transaction.
|
||||
*
|
||||
* @apiExample Example usage:
|
||||
* curl -H "Content-Type: application/json" -X POST -d '{ "txid": "01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b" }' localhost:5001/bch/transaction
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* HTTP/1.1 200 OK
|
||||
* {
|
||||
* success:true,
|
||||
* data: <data>
|
||||
* }
|
||||
*
|
||||
* @apiError UnprocessableEntity Missing required parameters
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* HTTP/1.1 422 Unprocessable Entity
|
||||
* {
|
||||
* "status": 422,
|
||||
* "error": "Unprocessable Entity"
|
||||
* }
|
||||
*/
|
||||
async transaction (ctx) {
|
||||
try {
|
||||
const txid = ctx.request.body.txid
|
||||
|
||||
const data = await _this.bchjs.Transaction.get(txid)
|
||||
// console.log(`utxos: ${JSON.stringify(utxos, null, 2)}`)
|
||||
|
||||
ctx.body = data
|
||||
} catch (err) {
|
||||
_this.handleError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DRY error handler
|
||||
handleError (ctx, err) {
|
||||
// If an HTTP status is specified by the buisiness logic, use that.
|
||||
if (err.status) {
|
||||
if (err.message) {
|
||||
ctx.throw(err.status, err.message)
|
||||
} else {
|
||||
ctx.throw(err.status)
|
||||
}
|
||||
} else {
|
||||
// By default use a 422 error if the HTTP status is not specified.
|
||||
ctx.throw(422, err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = BCHRESTController
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
REST API library for /fulcrum route.
|
||||
*/
|
||||
|
||||
// Public npm libraries.
|
||||
const Router = require('koa-router')
|
||||
|
||||
// Local libraries.
|
||||
const BCHRESTController = require('./controller')
|
||||
|
||||
class BCHRouter {
|
||||
constructor (localConfig = {}) {
|
||||
// Dependency Injection.
|
||||
this.adapters = localConfig.adapters
|
||||
if (!this.adapters) {
|
||||
throw new Error(
|
||||
'Instance of Adapters library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
this.useCases = localConfig.useCases
|
||||
if (!this.useCases) {
|
||||
throw new Error(
|
||||
'Instance of Use Cases library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
|
||||
const dependencies = {
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
}
|
||||
|
||||
// Encapsulate dependencies.
|
||||
this.bchRESTController = new BCHRESTController(dependencies)
|
||||
|
||||
// Instantiate the router and set the base route.
|
||||
const baseUrl = '/bch'
|
||||
this.router = new Router({ prefix: baseUrl })
|
||||
}
|
||||
|
||||
attach (app) {
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
|
||||
// Define the routes and attach the controller.
|
||||
this.router.post('/transactions', this.bchRESTController.transactions)
|
||||
this.router.post('/balance', this.bchRESTController.balance)
|
||||
this.router.post('/utxos', this.bchRESTController.utxos)
|
||||
this.router.post('/broadcast', this.bchRESTController.broadcast)
|
||||
this.router.post('/transaction', this.bchRESTController.transaction)
|
||||
|
||||
// Attach the Controller routes to the Koa app.
|
||||
app.use(this.router.routes())
|
||||
app.use(this.router.allowedMethods())
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BCHRouter
|
||||
@@ -11,6 +11,7 @@ const AuthRESTController = require('./auth')
|
||||
const UserRouter = require('./users')
|
||||
const ContactRESTController = require('./contact')
|
||||
const LogsRESTController = require('./logs')
|
||||
const BCHRESTController = require('./bch')
|
||||
|
||||
class RESTControllers {
|
||||
constructor (localConfig = {}) {
|
||||
@@ -52,6 +53,10 @@ class RESTControllers {
|
||||
// Attach the REST API Controllers associated with the /logs route
|
||||
const logsRESTController = new LogsRESTController(dependencies)
|
||||
logsRESTController.attach(app)
|
||||
|
||||
// Attach the REST API Controllers associated with the /fulcrum route
|
||||
const bchRESTController = new BCHRESTController(dependencies)
|
||||
bchRESTController.attach(app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,25 +22,31 @@ const LOCALHOST = `http://localhost:${config.port}`
|
||||
|
||||
describe('Auth', () => {
|
||||
before(async () => {
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
try {
|
||||
process.env.E2ETEST = 'true'
|
||||
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
// This should be the first instruction. It starts the REST API server.
|
||||
await app.startServer()
|
||||
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
// Delete all previous users in the database.
|
||||
await testUtils.deleteAllUsers()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
// Create a new admin user.
|
||||
await adminLib.createSystemUser()
|
||||
|
||||
const userObj = {
|
||||
email: 'test@test.com',
|
||||
password: 'pass',
|
||||
name: 'test'
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
} catch (err) {
|
||||
console.log('err in auth beforeAll: ', err)
|
||||
}
|
||||
const testUser = await testUtils.createUser(userObj)
|
||||
// console.log('TestUser: ', testUser)
|
||||
|
||||
context.user = testUser.user
|
||||
context.token = testUser.token
|
||||
})
|
||||
|
||||
describe('POST /auth', () => {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
Unit tests for the jwt-bch-lib and fullstack-jwt.js adapter library.
|
||||
|
||||
*/
|
||||
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const FullStackJWT = require('../../../src/adapters/fullstack-jwt')
|
||||
|
||||
describe('#FullStackJWT', () => {
|
||||
let sandbox
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
const localConfig = {
|
||||
authServer: 'someserver',
|
||||
apiServer: 'someserver',
|
||||
login: 'somelogin',
|
||||
password: 'somepassword'
|
||||
}
|
||||
uut = new FullStackJWT(localConfig)
|
||||
})
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if auth server is not specified', () => {
|
||||
try {
|
||||
uut = new FullStackJWT()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut) // For linting.
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass a url for the AUTH server when instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if api server is not specified', () => {
|
||||
try {
|
||||
const localConfig = {
|
||||
authServer: 'someserver'
|
||||
}
|
||||
|
||||
uut = new FullStackJWT(localConfig)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut) // For linting.
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass a url for the API server when instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if login is not specified', () => {
|
||||
try {
|
||||
const localConfig = {
|
||||
authServer: 'someserver',
|
||||
apiServer: 'someserver'
|
||||
}
|
||||
uut = new FullStackJWT(localConfig)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut) // For linting.
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass a FullStack.cash login (email) instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if login is not specified', () => {
|
||||
try {
|
||||
const localConfig = {
|
||||
authServer: 'someserver',
|
||||
apiServer: 'someserver',
|
||||
login: 'somelogin'
|
||||
}
|
||||
uut = new FullStackJWT(localConfig)
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
console.log(uut) // For linting.
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass a FullStack.cash account password when instantiating FullStackJWT class.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getJWT', () => {
|
||||
it('should return the JWT token', async () => {
|
||||
// Mock dependencies to force a code path.
|
||||
sandbox.stub(uut.jwtLib, 'register').resolves({})
|
||||
uut.jwtLib.userData.apiToken = 'abc123'
|
||||
sandbox.stub(uut.jwtLib, 'validateApiToken').resolves({ isValid: true })
|
||||
|
||||
const result = await uut.getJWT()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, 'abc123')
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.jwtLib, 'register').rejects(new Error('test error'))
|
||||
|
||||
await uut.getJWT()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if user does not have a JWT', async () => {
|
||||
try {
|
||||
// Mock dependencies to force a code path.
|
||||
sandbox.stub(uut.jwtLib, 'register').resolves({})
|
||||
|
||||
await uut.getJWT()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
// console.log('err.message: ', err.message)
|
||||
assert.include(err.message, 'This account does not have a JWT')
|
||||
}
|
||||
})
|
||||
|
||||
it('should retrieve a new JWT token if the old one invalid', async () => {
|
||||
// Mock dependencies to force a code path.
|
||||
sandbox.stub(uut.jwtLib, 'register').resolves({})
|
||||
uut.jwtLib.userData.apiToken = 'abc123'
|
||||
uut.jwtLib.userData.apiLevel = 30
|
||||
sandbox.stub(uut.jwtLib, 'validateApiToken').resolves({ isValid: false })
|
||||
sandbox.stub(uut.jwtLib, 'getApiToken').resolves('xyz789')
|
||||
|
||||
const result = await uut.getJWT()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.equal(result, 'xyz789')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#instanceBchjs', () => {
|
||||
it('should return an instance of bch-js', () => {
|
||||
const result = uut.instanceBchjs()
|
||||
// console.log('result: ', result)
|
||||
|
||||
assert.property(result, 'restURL')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,8 @@ describe('#IPFS', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const ipfs = IPFSMock.create()
|
||||
uut = new IPFSCoordAdapter({ ipfs })
|
||||
const bchjs = {}
|
||||
uut = new IPFSCoordAdapter({ ipfs, bchjs })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
})
|
||||
@@ -35,6 +36,20 @@ describe('#IPFS', () => {
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if bchjs instance is not included', () => {
|
||||
try {
|
||||
const ipfs = IPFSMock.create()
|
||||
uut = new IPFSCoordAdapter({ ipfs })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of bch-js must be passed when instantiating ipfs-coord.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#start', () => {
|
||||
|
||||
@@ -22,12 +22,26 @@ describe('#IPFS-adapter-index', () => {
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#start', () => {
|
||||
it('should throw error if bch-js is not passed', async () => {
|
||||
try {
|
||||
await uut.start()
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of bch-js must be passed when instantiating IPFS adapter.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should return a promise that resolves into an instance of IPFS.', async () => {
|
||||
// Mock dependencies.
|
||||
uut.ipfsAdapter = new IPFSMock()
|
||||
uut.IpfsCoordAdapter = IPFSCoordMock
|
||||
|
||||
const result = await uut.start()
|
||||
const result = await uut.start({ bchjs: {} })
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
@@ -37,7 +51,7 @@ describe('#IPFS-adapter-index', () => {
|
||||
// Force an error
|
||||
sandbox.stub(uut.ipfsAdapter, 'start').rejects(new Error('test error'))
|
||||
|
||||
await uut.start()
|
||||
await uut.start({ bchjs: {} })
|
||||
|
||||
assert.fail('Unexpected code path.')
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
// const assert = require('chai').assert
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
const adapters = require('../../../src/adapters')
|
||||
@@ -23,6 +23,8 @@ describe('#Controllers', () => {
|
||||
it('should attach the controllers', async () => {
|
||||
// mock IPFS
|
||||
sandbox.stub(adapters.ipfs, 'start').resolves({})
|
||||
sandbox.stub(adapters.fullStackJwt, 'getJWT').resolves({})
|
||||
sandbox.stub(adapters.fullStackJwt, 'instanceBchjs').resolves({})
|
||||
adapters.ipfs.ipfsCoordAdapter = {
|
||||
attachRPCRouter: () => {}
|
||||
}
|
||||
@@ -33,5 +35,19 @@ describe('#Controllers', () => {
|
||||
|
||||
await attachControllers(app)
|
||||
})
|
||||
|
||||
it('should catch and throw errors', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(adapters.fullStackJwt, 'getJWT')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
await attachControllers()
|
||||
} catch (err) {
|
||||
// console.log('err.message: ', err.message)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('#rate-limit', () => {
|
||||
assert.equal(error.status, 429)
|
||||
assert.include(
|
||||
error.message,
|
||||
'Too many requests, please try again later.'
|
||||
'Too many requests, please slow down your requests.'
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -101,7 +101,7 @@ describe('#rate-limit', () => {
|
||||
} catch (error) {
|
||||
assert.include(
|
||||
error.message,
|
||||
'Too many requests, please try again later.'
|
||||
'Too many requests, please slow down your requests.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
Unit tests for the json-rpc/auth/index.js file.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const jsonrpc = require('jsonrpc-lite')
|
||||
const sinon = require('sinon')
|
||||
const assert = require('chai').assert
|
||||
const { v4: uid } = require('uuid')
|
||||
|
||||
// Set the environment variable to signal this is a test.
|
||||
process.env.SVC_ENV = 'test'
|
||||
|
||||
// Local libraries
|
||||
const BCHRPC = require('../../../../src/controllers/json-rpc/bch')
|
||||
const RateLimit = require('../../../../src/controllers/json-rpc/rate-limit')
|
||||
const adapters = require('../../mocks/adapters')
|
||||
const UseCasesMock = require('../../mocks/use-cases')
|
||||
|
||||
describe('#BCHRPC', () => {
|
||||
let uut
|
||||
let sandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
const useCases = new UseCasesMock()
|
||||
|
||||
uut = new BCHRPC({ adapters, useCases })
|
||||
uut.rateLimit = new RateLimit({ max: 100 })
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new BCHRPC()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating BCH JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new BCHRPC({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating BCH JSON RPC Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#bchRouter', () => {
|
||||
it('should route to the transactions method', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'transactions').resolves(true)
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const txCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transactions'
|
||||
})
|
||||
const jsonStr = JSON.stringify(txCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
const result = await uut.bchRouter(rpcData)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should route to the balance method', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'balance').resolves(true)
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const txCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'balance'
|
||||
})
|
||||
const jsonStr = JSON.stringify(txCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
const result = await uut.bchRouter(rpcData)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should route to the utxos method', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'utxos').resolves(true)
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const txCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'utxos'
|
||||
})
|
||||
const jsonStr = JSON.stringify(txCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
const result = await uut.bchRouter(rpcData)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should route to the broadcast method', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'broadcast').resolves(true)
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'broadcast'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
const result = await uut.bchRouter(rpcData)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should route to the transaction method', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'transaction').resolves(true)
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transaction'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
const result = await uut.bchRouter(rpcData)
|
||||
|
||||
assert.equal(result, true)
|
||||
})
|
||||
|
||||
it('should return 500 status on routing issue', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut, 'transactions').rejects(new Error('test error'))
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const txCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transactions'
|
||||
})
|
||||
const jsonStr = JSON.stringify(txCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
rpcData.from = 'Origin request'
|
||||
|
||||
const result = await uut.bchRouter(rpcData)
|
||||
|
||||
assert.equal(result.success, false)
|
||||
assert.equal(result.status, 500)
|
||||
assert.equal(result.message, 'test error')
|
||||
assert.equal(result.endpoint, 'transactions')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#transactions', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox
|
||||
.stub(uut.bchjs.Electrumx, 'transactions')
|
||||
.resolves({ success: true })
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const txCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transactions',
|
||||
addresses: 'testAddr'
|
||||
})
|
||||
const jsonStr = JSON.stringify(txCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.transactions(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, true)
|
||||
assert.equal(response.status, 200)
|
||||
})
|
||||
|
||||
it('should return an error for invalid address', async () => {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.Electrumx, 'transactions')
|
||||
.rejects(new Error('Invalid address'))
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const txCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transactions',
|
||||
addresses: 'testAddr'
|
||||
})
|
||||
const jsonStr = JSON.stringify(txCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.transactions(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, false)
|
||||
assert.equal(response.status, 422)
|
||||
assert.equal(response.message, 'Invalid address')
|
||||
assert.equal(response.endpoint, 'transactions')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#balance', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'balance').resolves({ success: true })
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'balance',
|
||||
addresses: 'testAddr'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.balance(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, true)
|
||||
assert.equal(response.status, 200)
|
||||
})
|
||||
|
||||
it('should return an error for invalid address', async () => {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.Electrumx, 'balance')
|
||||
.rejects(new Error('Invalid address'))
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'balance',
|
||||
addresses: 'testAddr'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.balance(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, false)
|
||||
assert.equal(response.status, 422)
|
||||
assert.equal(response.message, 'Invalid address')
|
||||
assert.equal(response.endpoint, 'balance')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#utxos', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.bchjs.Utxo, 'get').resolves({ success: true })
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'utxos',
|
||||
addresses: 'testAddr'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.utxos(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, true)
|
||||
assert.equal(response.status, 200)
|
||||
})
|
||||
|
||||
it('should return an error for invalid address', async () => {
|
||||
// Force an error
|
||||
sandbox.stub(uut.bchjs.Utxo, 'get').rejects(new Error('Invalid address'))
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'utxos',
|
||||
addresses: 'testAddr'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.utxos(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, false)
|
||||
assert.equal(response.status, 422)
|
||||
assert.equal(response.message, 'Invalid address')
|
||||
assert.equal(response.endpoint, 'utxos')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#broadcast', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox
|
||||
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
|
||||
.resolves({ success: true })
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'broadcast',
|
||||
hex: 'testData'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.broadcast(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, true)
|
||||
assert.equal(response.status, 200)
|
||||
})
|
||||
|
||||
it('should return an error for invalid input', async () => {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
|
||||
.rejects(new Error('Invalid data'))
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'broadcast',
|
||||
hex: 'testHex'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.broadcast(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, false)
|
||||
assert.equal(response.status, 422)
|
||||
assert.equal(response.message, 'Invalid data')
|
||||
assert.equal(response.endpoint, 'broadcast')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#transaction', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.bchjs.Transaction, 'get').resolves({ success: true })
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transaction',
|
||||
txid: 'testData'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.transaction(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, true)
|
||||
assert.equal(response.status, 200)
|
||||
})
|
||||
|
||||
it('should return an error for invalid input', async () => {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.Transaction, 'get')
|
||||
.rejects(new Error('Invalid data'))
|
||||
|
||||
// Generate the parsed data that the main router would pass to this
|
||||
// endpoint.
|
||||
const id = uid()
|
||||
const rpcCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transaction',
|
||||
txid: 'testTxid'
|
||||
})
|
||||
const jsonStr = JSON.stringify(rpcCall, null, 2)
|
||||
const rpcData = jsonrpc.parse(jsonStr)
|
||||
|
||||
const response = await uut.transaction(rpcData)
|
||||
// console.log('response: ', response)
|
||||
|
||||
assert.equal(response.success, false)
|
||||
assert.equal(response.status, 422)
|
||||
assert.equal(response.message, 'Invalid data')
|
||||
assert.equal(response.endpoint, 'transaction')
|
||||
})
|
||||
})
|
||||
})
|
||||
+28
@@ -179,5 +179,33 @@ describe('#JSON RPC', () => {
|
||||
assert.equal(obj.result.method, 'about')
|
||||
assert.equal(obj.id, id)
|
||||
})
|
||||
|
||||
it('should route to bch handler', async () => {
|
||||
const id = uid()
|
||||
const userCall = jsonrpc.request(id, 'bch', {
|
||||
endpoint: 'transactions'
|
||||
})
|
||||
const jsonStr = JSON.stringify(userCall, null, 2)
|
||||
|
||||
// Mock the controller.
|
||||
sandbox.stub(uut.bchController, 'bchRouter').resolves('true')
|
||||
|
||||
// Force ipfs-coord communication.
|
||||
uut.ipfsCoord.ipfs = {
|
||||
orbitdb: {
|
||||
sendToDb: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await uut.router(jsonStr, 'peerA')
|
||||
// console.log(result)
|
||||
|
||||
const obj = JSON.parse(result.retStr)
|
||||
// console.log('obj: ', obj)
|
||||
|
||||
assert.equal(obj.result.value, 'true')
|
||||
assert.equal(obj.result.method, 'bch')
|
||||
assert.equal(obj.id, id)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /users endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const BCHRESTController = require('../../../../../src/controllers/rest-api/bch/controller')
|
||||
let uut
|
||||
let sandbox
|
||||
let ctx
|
||||
|
||||
const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#BCH-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new BCHRESTController({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new BCHRESTController()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new BCHRESTController({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#transactions', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox
|
||||
.stub(uut.bchjs.Electrumx, 'transactions')
|
||||
.resolves({ success: true })
|
||||
|
||||
ctx.request.body = {
|
||||
addresses: 'testAddr'
|
||||
}
|
||||
|
||||
await uut.transactions(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.equal(ctx.body.success, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.Electrumx, 'transactions')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
addresses: 'testAddr'
|
||||
}
|
||||
|
||||
await uut.transactions(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#balance', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.bchjs.Electrumx, 'balance').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {
|
||||
addresses: 'testAddr'
|
||||
}
|
||||
|
||||
await uut.balance(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.equal(ctx.body.success, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.Electrumx, 'balance')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
addresses: 'testAddr'
|
||||
}
|
||||
|
||||
await uut.balance(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#utxos', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.bchjs.Utxo, 'get').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {
|
||||
addresses: 'testAddr'
|
||||
}
|
||||
|
||||
await uut.utxos(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.equal(ctx.body.success, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox.stub(uut.bchjs.Utxo, 'get').rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
addresses: 'testAddr'
|
||||
}
|
||||
|
||||
await uut.utxos(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#broadcast', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox
|
||||
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
|
||||
.resolves({ success: true })
|
||||
|
||||
ctx.request.body = {
|
||||
hex: 'testData'
|
||||
}
|
||||
|
||||
await uut.broadcast(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.equal(ctx.body.success, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.RawTransactions, 'sendRawTransaction')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
hex: 'testData'
|
||||
}
|
||||
|
||||
await uut.broadcast(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#transaction', () => {
|
||||
it('should return data from bchjs', async () => {
|
||||
// Mock dependencies
|
||||
sandbox.stub(uut.bchjs.Transaction, 'get').resolves({ success: true })
|
||||
|
||||
ctx.request.body = {
|
||||
txid: 'testData'
|
||||
}
|
||||
|
||||
await uut.transaction(ctx)
|
||||
// console.log('ctx.body: ', ctx.body)
|
||||
|
||||
assert.equal(ctx.body.success, true)
|
||||
})
|
||||
|
||||
it('should catch and throw an error', async () => {
|
||||
try {
|
||||
// Force an error
|
||||
sandbox
|
||||
.stub(uut.bchjs.Transaction, 'get')
|
||||
.rejects(new Error('test error'))
|
||||
|
||||
ctx.request.body = {
|
||||
txid: 'testData'
|
||||
}
|
||||
|
||||
await uut.transaction(ctx)
|
||||
} catch (err) {
|
||||
// console.log('err: ', err)
|
||||
assert.include(err.message, 'test error')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError', () => {
|
||||
it('should pass an error message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 422,
|
||||
message: 'Unprocessable Entity'
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Unprocessable Entity')
|
||||
}
|
||||
})
|
||||
|
||||
it('should still throw error if there is no message', () => {
|
||||
try {
|
||||
const err = {
|
||||
status: 404
|
||||
}
|
||||
|
||||
uut.handleError(ctx, err)
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Not Found')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Unit tests for the REST API handler for the /bch endpoints.
|
||||
*/
|
||||
|
||||
// Public npm libraries
|
||||
const assert = require('chai').assert
|
||||
const sinon = require('sinon')
|
||||
|
||||
// Local support libraries
|
||||
const adapters = require('../../../mocks/adapters')
|
||||
const UseCasesMock = require('../../../mocks/use-cases')
|
||||
// const app = require('../../../mocks/app-mock')
|
||||
|
||||
const BCHRouter = require('../../../../../src/controllers/rest-api/bch')
|
||||
let uut
|
||||
let sandbox
|
||||
// let ctx
|
||||
|
||||
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
|
||||
|
||||
describe('#BCH-REST-Router', () => {
|
||||
// const testUser = {}
|
||||
|
||||
beforeEach(() => {
|
||||
const useCases = new UseCasesMock()
|
||||
uut = new BCHRouter({ adapters, useCases })
|
||||
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock the context object.
|
||||
// ctx = mockContext()
|
||||
})
|
||||
|
||||
afterEach(() => sandbox.restore())
|
||||
|
||||
describe('#constructor', () => {
|
||||
it('should throw an error if adapters are not passed in', () => {
|
||||
try {
|
||||
uut = new BCHRouter()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Adapters library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should throw an error if useCases are not passed in', () => {
|
||||
try {
|
||||
uut = new BCHRouter({ adapters })
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Instance of Use Cases library required when instantiating BCH REST Controller.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#attach', () => {
|
||||
it('should throw an error if app is not passed in.', () => {
|
||||
try {
|
||||
uut.attach()
|
||||
|
||||
assert.fail('Unexpected code path')
|
||||
} catch (err) {
|
||||
assert.include(
|
||||
err.message,
|
||||
'Must pass app object when attaching REST API controllers.'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -47,4 +47,20 @@ const localdb = {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ipfs, localdb }
|
||||
const bchjs = {
|
||||
Electrumx: {
|
||||
transactions: () => {},
|
||||
balance: () => {}
|
||||
},
|
||||
Utxo: {
|
||||
get: () => {}
|
||||
},
|
||||
Transaction: {
|
||||
get: () => {}
|
||||
},
|
||||
RawTransactions: {
|
||||
sendRawTransaction: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ipfs, localdb, bchjs }
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
*/
|
||||
|
||||
class IPFSCoord {
|
||||
constructor () {
|
||||
this.ipfs = {
|
||||
async start () {}
|
||||
}
|
||||
}
|
||||
|
||||
async isReady () {
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user