Compare commits

...
45 Commits
Author SHA1 Message Date
Chris Troutner 3ba866d2d6 Merge pull request #13 from Permissionless-Software-Foundation/ct-unstable
Syncing with upstream ipfs-service-provider
2021-07-31 15:57:18 -07:00
Chris Troutner 3d058d9164 fix(ipfs-service-provider): Synced with upstream 2021-07-31 15:55:47 -07:00
Chris Troutner fde8b16fef Merge pull request #16 from Permissionless-Software-Foundation/dh-required
fix(users): Using the users entity in the use cases
2021-07-31 15:41:25 -07:00
Daniel Gonzalez c5a1eb6f81 fix(users): Using the users entity in the use cases 2021-07-31 15:00:38 -04:00
Chris Troutner 74f8b60472 Adding link to API documentation 2021-07-28 13:28:53 -07:00
Chris Troutner 53846762f0 Merge pull request #12 from Permissionless-Software-Foundation/dh-improve-documentation
feat(documentation): Improved ipfs-bch-wallet-service documentation
2021-07-28 13:25:13 -07:00
Daniel Gonzalez 788d5ba4ee feat(documentation): Improved ipfs-bch-wallet-service documentation 2021-07-26 21:00:24 -04:00
Chris Troutner d57085949c Merge pull request #15 from Permissionless-Software-Foundation/ct-unstable
Removing comments
2021-07-25 17:31:48 -07:00
Chris Troutner 9dc40a417a Removing comments 2021-07-25 17:30:15 -07:00
Chris Troutner 81e5592b81 Merge pull request #14 from Permissionless-Software-Foundation/ct-unstable
Converting wlogger and Controller libraries to classes
2021-07-25 16:51:30 -07:00
Chris Troutner 44867f0173 fix(controllers): Made Controllers library a class 2021-07-25 16:49:01 -07:00
Chris Troutner c46f7357c3 fix(wlogger): Refactored into Class for better unit test control 2021-07-25 16:34:56 -07:00
Chris Troutner 47b8dce966 Adding link to YouTube video in README. 2021-07-21 15:10:05 -07:00
Chris Troutner 63b08d1acb Merge pull request #11 from Permissionless-Software-Foundation/ct-unstable
feat(test): Added e2e test simulating a consumer of JSON RPC
2021-07-18 15:51:22 -07:00
Chris Troutner fa05fcf3d4 feat(test): Added e2e test simulating a consumer of JSON RPC 2021-07-18 15:46:32 -07:00
Chris Troutner f9846e95fd Created client scheme for JSON RPC 2021-07-18 08:09:27 -07:00
Chris Troutner def4f9003c Got simulated client to send a JSON RPC command 2021-07-17 19:51:56 -07:00
Chris Troutner 06cdc03287 Got client to connect to service 2021-07-17 19:14:11 -07:00
Chris Troutner c3bfbbb6e1 Merge pull request #10 from Permissionless-Software-Foundation/ct-unstable
fix(ipfs-coord): Setting name with an env var
2021-07-17 16:22:51 -07:00
Chris Troutner c7ed9eb3fd fix(ipfs-coord): Setting name with an env var 2021-07-17 16:19:03 -07:00
Chris Troutner a023202703 Adding debug log for mongodb connection string 2021-07-17 11:33:17 -07:00
Chris Troutner e31dceb07f Merge pull request #9 from Permissionless-Software-Foundation/ct-unstable
fix(production): Adjusting production settings
2021-07-17 11:25:38 -07:00
Chris Troutner af0e727ba8 fix(production): Fixed merge conflicts 2021-07-17 11:24:00 -07:00
Chris Troutner 6f64fc4652 fix(production): Adjusting production settings 2 2021-07-17 11:16:57 -07:00
Chris Troutner e91dd7f834 feat(production): Adjusting production settings 2021-07-17 20:16:19 +02:00
Chris Troutner e717124fc7 Merge pull request #8 from Permissionless-Software-Foundation/ct-unstable
fix(production): Updated Docker container
2021-07-17 10:55:48 -07:00
Chris Troutner 3c8d2cfdd3 fix(production): Updated Docker container 2021-07-17 10:54:00 -07:00
Chris Troutner 71fbebe672 Merge pull request #7 from Permissionless-Software-Foundation/ct-unstable
feat(JWT): Added automatic JWT handling for FullStack.cash
2021-07-17 10:08:00 -07:00
Chris Troutner cab819081a Got everything passing and working 2021-07-17 10:06:29 -07:00
Chris Troutner 4f0adbb773 Updated to ipfs-coord v4.0.1 2021-07-17 09:12:37 -07:00
Chris Troutner e04cb1f05a Got all tests passing again 2021-07-17 08:15:02 -07:00
Chris Troutner 6f619db666 Updated unit tests 2021-07-16 18:51:38 -07:00
Chris Troutner 126bee86bd feat(JWT): Added automatic JWT handling for FullStack.cash 2021-07-16 17:26:51 -07:00
Chris Troutner 3761d139f1 Merge pull request #6 from Permissionless-Software-Foundation/ct-unstable
fix(endpoints): Tested endpoints, fixed some bugs and docs
2021-07-16 15:27:55 -07:00
Chris Troutner 006727cc9d fix(bch-js): Updating bch-js to v4.20.2 2021-07-16 15:26:21 -07:00
Chris Troutner bcd2a54aa7 fix(endpoints): Tested endpoints, fixed some bugs and docs 2021-07-16 15:19:03 -07:00
Chris Troutner 70155c7b90 Merge pull request #5 from Permissionless-Software-Foundation/ct-unstable
Added broadcast and transaction endpoints
2021-07-16 10:53:10 -07:00
Chris Troutner 7035e1777b feat(transaction): Added transaction endpoint to REST and JSON 2021-07-16 10:51:47 -07:00
Chris Troutner ed82c1102e feat(broadcast): Added endpoint for REST and JSON APIs 2021-07-16 08:00:40 -07:00
Chris Troutner baad128ba2 Merge pull request #4 from Permissionless-Software-Foundation/ct-unstable
feat(utxos): Added utxos endpoing to REST and JSON RPC
2021-07-15 19:07:11 -07:00
Chris Troutner 57ce5c12f5 feat(utxos): Added utxos endpoing to REST and JSON RPC 2021-07-15 19:04:57 -07:00
Chris Troutner 6aa2e1e44a Converted fulcrum endpoints to bch endpoints 2021-07-15 13:31:24 -07:00
Chris Troutner 762e07e662 Merge pull request #3 from Permissionless-Software-Foundation/ct-unstable
feat(balance): Added Fulcrum balance call to REST and JSON
2021-07-15 09:33:46 -07:00
Chris Troutner 720508e7c4 Merge branch 'master' into ct-unstable 2021-07-15 09:31:58 -07:00
Chris Troutner 8a558f212f feat(balance): Added Fulcrum balance call to REST and JSON 2021-07-15 09:30:31 -07:00
51 changed files with 3080 additions and 1277 deletions
+3
View File
@@ -4,6 +4,9 @@
This is a censorship-resistant, IPFS-based microservice that provides access for wallets to access the Bitcoin Cash (BCH) blockchain. It leverages [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js).
- [Decentralized Blockchain Service Providers](https://youtu.be/m_33rRXEats) YouTube video demos this software.
- [JSON RPC and REST API Documentation](https://ipfs-bch-wallet-service.fullstack.cash/)
## About This Repository
This repository is forked from the [ipfs-service-provider](https://github.com/Permissionless-Software-Foundation/ipfs-service-provider) repository. That code has been customized to provide BCH blockchain access for wallets. This server-side node.js app provides both REST API over HTTP and JSON RPC over IPFS endpoints for wallets to access the blockchain through.
+14 -4
View File
@@ -29,6 +29,9 @@ async function startServer () {
// Connect to the Mongo Database.
mongoose.Promise = global.Promise
mongoose.set('useCreateIndex', true) // Stop deprecation warning.
console.log(
`Connecting to MongoDB with this connection string: ${config.database}`
)
await mongoose.connect(config.database, {
useUnifiedTopology: true,
useNewUrlParser: true
@@ -52,8 +55,9 @@ async function startServer () {
app.use(passport.initialize())
app.use(passport.session())
// Attach REST API and JSON RPC controllers to the app.
const controllers = require('../src/controllers')
// Attach REST API and JSON RPC controllersCt unstable to the app.
const Controllers = require('../src/controllers')
const controllers = new Controllers()
controllers.attachControllers(app)
// Enable CORS for testing
@@ -69,8 +73,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
}
+25 -3
View File
@@ -5,6 +5,10 @@
/* eslint no-unneeded-ternary:0 */
const ipfsCoordName = process.env.COORD_NAME
? process.env.COORD_NAME
: 'ipfs-bch-wallet-service'
module.exports = {
// Configure TCP port.
port: process.env.PORT || 5001,
@@ -23,17 +27,31 @@ module.exports = {
? 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,
// Information passed to other IPFS peers about this node.
apiInfo: 'https://ipfs-service-provider.fullstack.cash/',
apiInfo: 'https://ipfs-bch-wallet-service.fullstack.cash/',
ipfsCoordName: ipfsCoordName,
// JSON-LD and Schema.org schema with info about this app.
announceJsonLd: {
'@context': 'https://schema.org/',
'@type': 'WebAPI',
name: 'ipfs-bch-wallet-service',
name: ipfsCoordName,
description:
'IPFS service providing BCH blockchain access needed by a wallet.',
documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/',
@@ -42,5 +60,9 @@ module.exports = {
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
}
+1 -1
View File
@@ -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'
}
+3 -1
View File
@@ -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'
}
+1 -1
View File
@@ -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'
}
-25
View File
@@ -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
+489 -488
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -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
View File
@@ -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"]
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Remove all untagged docker images.
docker rmi $(docker images | grep "^<none>" | awk '{print $3}')
+28
View File
@@ -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
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
export KOA_ENV=production
npm start
+20
View File
@@ -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
+107
View File
@@ -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
View File
@@ -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
}
+10 -2
View File
@@ -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.')
+14 -5
View File
@@ -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
+1
View File
@@ -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()
}
+78 -37
View File
@@ -11,52 +11,93 @@ require('winston-daily-rotate-file')
const config = require('../../config')
// Configure daily-rotation transport.
const transport = new winston.transports.DailyRotateFile({
filename: `${__dirname.toString()}/../../logs/koa-${config.env}-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: false,
maxSize: '1m', // 1 megabyte
maxFiles: '5d', // 5 days
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
)
})
class Wlogger {
constructor (localConfig = {}) {
this.config = config
transport.on('rotate', notifyRotation)
// Configure daily-rotation transport.
this.transport = new winston.transports.DailyRotateFile({
filename: `${__dirname.toString()}/../../logs/koa-${
this.config.env
}-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: false,
maxSize: '1m', // 1 megabyte
maxFiles: '5d', // 5 days
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
)
})
function notifyRotation (oldFilename, newFilename) {
wlogger.info('Rotating log files')
this.transport.on('rotate', this.notifyRotation)
// This controls what goes into the log FILES
this.wlogger = winston.createLogger({
level: 'verbose',
format: winston.format.json(),
transports: [
//
// - Write to all logs with level `info` and below to `combined.log`
// - Write all logs error (and below) to `error.log`.
//
// new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
// new winston.transports.File({ filename: 'logs/combined.log' })
this.transport
]
})
}
notifyRotation (oldFilename, newFilename) {
this.wlogger.info('Rotating log files')
}
outputToConsole () {
this.wlogger.add(
new winston.transports.Console({
format: winston.format.simple(),
level: 'info'
})
)
}
}
// transport.on('rotate', notifyRotation)
// function notifyRotation (oldFilename, newFilename) {
// wlogger.info('Rotating log files')
// }
// This controls what goes into the log FILES
const wlogger = winston.createLogger({
level: 'verbose',
format: winston.format.json(),
transports: [
//
// - Write to all logs with level `info` and below to `combined.log`
// - Write all logs error (and below) to `error.log`.
//
// new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
// new winston.transports.File({ filename: 'logs/combined.log' })
transport
]
})
// const wlogger = winston.createLogger({
// level: 'verbose',
// format: winston.format.json(),
// transports: [
// //
// // - Write to all logs with level `info` and below to `combined.log`
// // - Write all logs error (and below) to `error.log`.
// //
// // new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
// // new winston.transports.File({ filename: 'logs/combined.log' })
// transport
// ]
// })
function outputToConsole () {
wlogger.add(
new winston.transports.Console({
format: winston.format.simple(),
level: 'info'
})
)
}
// function outputToConsole () {
// wlogger.add(
// new winston.transports.Console({
// format: winston.format.simple(),
// level: 'info'
// })
// )
// }
// This controls the logs to CONSOLE
// if (config.env !== 'test') {
// outputToConsole()
// }
module.exports = { wlogger, notifyRotation, outputToConsole }
const logger = new Wlogger()
const wlogger = logger.wlogger
module.exports = { wlogger, Wlogger }
+46 -28
View File
@@ -14,39 +14,57 @@ const JSONRPC = require('./json-rpc')
// Load the Clean Architecture Use Case libraries.
const UseCases = require('../use-cases')
const useCases = new UseCases({ adapters })
// const useCases = new UseCases({ adapters })
// Load the REST API Controllers.
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)
class Controllers {
constructor (localConfig = {}) {
this.adapters = adapters
this.useCases = new UseCases({ adapters })
}
// Start IPFS.
await adapters.ipfs.start()
async attachControllers (app) {
// 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()
attachRPCControllers()
// Attach the REST controllers to the Koa app.
this.attachRESTControllers(app)
// Start IPFS.
await this.adapters.ipfs.start({ bchjs: adapters.bchjs })
this.attachRPCControllers()
}
// Top-level function for this library.
// Start the various Controllers and attach them to the app.
attachRESTControllers (app) {
const rESTControllers = new RESTControllers({
adapters: this.adapters,
useCases: this.useCases
})
// Attach the REST API Controllers associated with the boilerplate code to the Koa app.
rESTControllers.attachRESTControllers(app)
}
// Add the JSON RPC router to the ipfs-coord adapter.
attachRPCControllers () {
const jsonRpcController = new JSONRPC({
adapters: this.adapters,
useCases: this.useCases
})
// Attach the input of the JSON RPC router to the output of ipfs-coord.
this.adapters.ipfs.ipfsCoordAdapter.attachRPCRouter(
jsonRpcController.router
)
}
}
function attachRESTControllers (app) {
const rESTControllers = new RESTControllers({
adapters,
useCases
})
// Attach the REST API Controllers associated with the boilerplate code to the Koa app.
rESTControllers.attachRESTControllers(app)
}
// Add the JSON RPC router to the ipfs-coord adapter.
function attachRPCControllers () {
const jsonRpcController = new JSONRPC({ adapters, useCases })
// Attach the input of the JSON RPC router to the output of ipfs-coord.
adapters.ipfs.ipfsCoordAdapter.attachRPCRouter(jsonRpcController.router)
}
module.exports = { attachControllers }
module.exports = Controllers
+531
View File
@@ -0,0 +1,531 @@
/*
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.
* Given the 'addresses' property this endpoint returns an object with the following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
* - result: {} - Result of the petition with the RPC information
* - method: "" - Method used in the petition
* - receiver: "" - Receiver address
* - value: {} - Final result value of the petition
* - success : - Petition status
* - transactions : [] - Transactions of the provided adresses
* - transactions: [] - Transaction details
* - height : - Reference to the blockchain size
* - tx_hash: "" - Hash of the transaction
* - address : "" - Address asociated to the transactions
* - status: - HTTP Status Code
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transactions", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"]}}
*
* @apiSuccessExample {json} Success-Response:
* {
* "jsonrpc":"2.0",
* "id":"555",
* "result":{
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value":{
* "success":true,
* "transactions":[
* {
* "transactions":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f"
* }
* ],
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* }
* ],
* "status":200
* }
* }
* }
*/
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.
*
* Given the 'addresses' property it returns an object
* with the following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
* - result: {} - Result of the petition with the RPC information
* - method: "" - Method used in the petition
* - receiver: "" - Receiver address
* - value: {} - Final result value of the petition
* - success : - Petition status
* - balances : [] - Balance of the provided addresses
* - balance : {} - Object with the balance types of an address
* - confirmed : - Confirmed balance
* - unconfirmed : - Unconfirmed balance
* - address : "" - Address related to the balance
* - status: - HTTP Status Code
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "balance", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"]}}
*
* @apiSuccessExample {json} Success-Response:
* {
* "jsonrpc":"2.0",
* "id":"555",
* "result":{
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value":{
* "success":true,
* "balances":[
* {
* "balance":{
* "confirmed":1000,
* "unconfirmed":0
* },
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* }
* ],
* "status":200
* }
* }
* }
*/
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.
*
* Given an address this endpoint will return an object
* with the following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
* - result: {} - Result of the petition with the RPC information
* - method: "" - Method used in the petition
* - receiver: "" - Receiver address
* - value: [] - Final result value of the petition
* - address: "" - the address these UTXOs are associated with
* - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH
* - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and
* not spent, to be safe.
* - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens
* - type1: {}
* - tokens: [] - SLP token Type 1 tokens.
* - mintBatons: [] - SLP token Type 1 mint batons.
* - nft: {}
* - tokens: [] - NFT tokens
* - groupTokens: [] - NFT Group tokens, used to create NFT tokens.
* - groupMintBatons: [] - Minting baton to create more NFT Group tokens.
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "utxos", "address": "bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"}}
*
* @apiSuccessExample {json} Success-Response:
*
* {
* "jsonrpc":"2.0",
* "id":"555",
* "result":{
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value":[
* {
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj",
* "bchUtxos":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "tx_pos":0,
* "value":1000,
* "txid":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "vout":0,
* "isValid":false
* }
* ],
* "nullUtxos":[
*
* ],
* "slpUtxos":{
* "type1":{
* "mintBatons":[
*
* ],
* "tokens":[
*
* ]
* },
* "nft":{
* "groupMintBatons":[
*
* ],
* "groupTokens":[
*
* ],
* "tokens":[
*
* ]
* }
* }
* }
* ]
* }
* }
*
*/
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.
*
* This endpoint will return an object with the following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
* - result: {} - Result of the petition with the RPC information
* - method: "" - Method used in the petition
* - receiver: "" - Receiver address
* - value: "" - Final result value of the petition
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "broadcast", "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000"}}
*
* @apiSuccessExample {json} Success-Response:
*
* "jsonrpc":"2.0",
* "id":"555",
* "result":{
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value": "951299775f68a599b95239bfc385423f87a33e11747c299a22ef9dcf3d1557ec"
*
*/
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.
* Given a transaction the endpoint will return an object with the
* following properties
*
* - jsonrpc: "" - jsonrpc version
* - id: "" - jsonrpc id
* - result: {} - Result of the petition with the RPC information
* - method: "" - Method used in the petition
* - receiver: "" - Receiver address
* - value: {} - Final result value of the petition
* - txid: "" - Transaction ID
* - hash: "" - Transaction hash
* - version: - Version number
* - size: - Transaction size
* - locktime: -
* - vin: [] - Transaction inputs
* - vout: [] - Transaction outputs
* - hex: "" - hexadecimal script
* - blockhash: "" - Reference to the block register
* - confirmations : "" - Transaction confirmations
* - time: - Execution time
* - blocktime: - Execution time
* - isValidSLPTx: - Determines if the transaction was under SLP
* - status: - HTTP Status Code
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"bch","params":{ "endpoint": "transaction", "txid": "01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b"}}
*
* @apiSuccessExample {json} Success-Response:
* {
* "jsonrpc":"2.0",
* "id":"555",
* "result":{
* "method":"bch",
* "reciever":"QmU86vLVbUY1UhziKB6rak7GPKRA2QHWvzNm2AjEvXNsT6",
* "value":{
* "txid":"01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b",
* "hash":"01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b",
* "version":1,
* "size":272,
* "locktime":0,
* "vin":[
* {
* "txid":"4deef6de4b973706cd6e8fc8105a41a84be349e4e9717225ee5e7c63538e95e8",
* "vout":1,
* "scriptSig":{
* "asm":"3045022100fce6ef975fa7ec66e0ce0c51d839fd8f56510897252c0b238e7265974bc7c07202200d1d1429154e6775eecdc2829965650bc3ca714a86088d705bd58f8c034f2496[ALL|FORKID] 0467ff2df20f28bc62ad188525868f41d461f7dab3c1e500314cdb5218e5637bfd0f9c02eb5b3f383f698d28ff13547eaf05dd9216130861dd0216824e9d7337e3",
* "hex":"483045022100fce6ef975fa7ec66e0ce0c51d839fd8f56510897252c0b238e7265974bc7c07202200d1d1429154e6775eecdc2829965650bc3ca714a86088d705bd58f8c034f249641410467ff2df20f28bc62ad188525868f41d461f7dab3c1e500314cdb5218e5637bfd0f9c02eb5b3f383f698d28ff13547eaf05dd9216130861dd0216824e9d7337e3"
* },
* "sequence":4294967295,
* "address":"bitcoincash:qqrxa0h9jqnc7v4wmj9ysetsp3y7w9l36u8gnnjulq",
* "value":0.00001824
* }
* ],
* "vout":[
* {
* "value":0,
* "n":0,
* "scriptPubKey":{
* "asm":"OP_RETURN -385055325 46226800369048b83cea897639bb39273c8e4b883bd8c2a435bbe7a237cc433a",
* "hex":"6a045d7af3962046226800369048b83cea897639bb39273c8e4b883bd8c2a435bbe7a237cc433a",
* "type":"nulldata"
* }
* },
* {
* "value":0.0000155,
* "n":1,
* "scriptPubKey":{
* "asm":"OP_DUP OP_HASH160 066ebee590278f32aedc8a4865700c49e717f1d7 OP_EQUALVERIFY OP_CHECKSIG",
* "hex":"76a914066ebee590278f32aedc8a4865700c49e717f1d788ac",
* "reqSigs":1,
* "type":"pubkeyhash",
* "addresses":[
* "bitcoincash:qqrxa0h9jqnc7v4wmj9ysetsp3y7w9l36u8gnnjulq"
* ]
* }
* }
* ],
* "hex":"0100000001e8958e53637c5eee257271e9e449e34ba8415a10c88f6ecd0637974bdef6ee4d010000008b483045022100fce6ef975fa7ec66e0ce0c51d839fd8f56510897252c0b238e7265974bc7c07202200d1d1429154e6775eecdc2829965650bc3ca714a86088d705bd58f8c034f249641410467ff2df20f28bc62ad188525868f41d461f7dab3c1e500314cdb5218e5637bfd0f9c02eb5b3f383f698d28ff13547eaf05dd9216130861dd0216824e9d7337e3ffffffff020000000000000000276a045d7af3962046226800369048b83cea897639bb39273c8e4b883bd8c2a435bbe7a237cc433a0e060000000000001976a914066ebee590278f32aedc8a4865700c49e717f1d788ac00000000",
* "blockhash":"0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536",
* "confirmations":98000,
* "time":1568338904,
* "blocktime":1568338904,
* "isValidSLPTx":false,
* "status":200
* }
* }
* }
*/
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
-129
View File
@@ -1,129 +0,0 @@
/*
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 FulcrumRPC {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Fulcrum JSON RPC Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating Fulcrum 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()
}
// 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 fulcrumRouter (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 'getAllUsers':
// await this.validators.ensureUser(rpcData)
// await this.rateLimit.limiter(rpcData.from)
// return await this.getAll(rpcData)
//
// case 'getUser':
// user = await this.validators.ensureUser(rpcData)
// await this.rateLimit.limiter(rpcData.from)
// return await this.getUser(rpcData, user)
//
// case 'updateUser':
// user = await this.validators.ensureTargetUserOrAdmin(rpcData)
// await this.rateLimit.limiter(rpcData.from)
// return await this.updateUser(rpcData, user)
//
// case 'deleteUser':
// user = await this.validators.ensureTargetUserOrAdmin(rpcData)
// await this.rateLimit.limiter(rpcData.from)
// return await this.deleteUser(rpcData, user)
}
} catch (err) {
console.error('Error in FulcrumRPC/rpcRouter()')
// throw err
return {
success: false,
status: err.status || 500,
message: err.message,
endpoint
}
}
}
/**
* @api {JSON} /fulcrum Transactions
* @apiPermission public
* @apiName Transactions
* @apiGroup JSON Fulcrum
* @apiDescription This endpoint wraps the bchjs.Electrumx.transactions([]) function.
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"fulcrum","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'
}
}
}
// TODO create deleteUser()
}
module.exports = FulcrumRPC
+6 -6
View File
@@ -10,7 +10,7 @@ const { wlogger } = require('../../adapters/wlogger')
const UserController = require('./users')
const AuthController = require('./auth')
const AboutController = require('./about')
const FulcrumController = require('./fulcrum')
const BCHController = require('./bch')
let _this
@@ -36,7 +36,7 @@ class JSONRPC {
this.userController = new UserController(localConfig)
this.authController = new AuthController(localConfig)
this.aboutController = new AboutController()
this.fulcrumController = new FulcrumController(localConfig)
this.bchController = new BCHController(localConfig)
_this = this
}
@@ -45,8 +45,8 @@ class JSONRPC {
// which controller to route the instruction to.
async router (str, from) {
try {
// console.log('router str: ', str)
// console.log('router from: ', from)
console.log('router str: ', str)
console.log('router from: ', from)
// Exit quietly if 'from' is not specified.
if (!from || typeof from !== 'string') {
@@ -83,8 +83,8 @@ class JSONRPC {
case 'about':
retObj = await _this.aboutController.aboutRouter(parsedData)
break
case 'fulcrum':
retObj = await _this.fulcrumController.fulcrumRouter(parsedData)
case 'bch':
retObj = await _this.bchController.bchRouter(parsedData)
}
// console.log('retObj: ', retObj)
+2 -2
View File
@@ -14,7 +14,7 @@ class RateLimit {
// Set default rate limit options.
this.defaultOptions = {
interval: { min: 1 },
max: 1000, // 1000 RPM while prototyping.
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) {
+396
View File
@@ -0,0 +1,396 @@
/*
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.
*
* Given the 'addresses' property returns an array of objects
* with the following properties
*
* - success : - Petition status
* - transactions : [] - Transaction of the provided address
* - transactions: [] - Transaction details
* - height : - Reference to the blockchain size
* - tx_hash: "" - Transaction hash
* - address : "" - Address associated to the transactions
*
* Note: For a single address pass the 'addresses' of string type
*
* @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,
* "transactions":[
* {
* "transactions":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f"
* }
* ],
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* }
* ]
* }
*
* @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 Devuelve el balance de un address o un array de adresses.
*
* Given the 'addresses' property returns an array of objects
* with the following properties
*
* - success : - Petition status
* - balances : [] - Balance of the provided addresses
* - balance : {} - Object with the balance types of an address
* - confirmed : - Confirmed balance
* - unconfirmed : - Unconfirmed Balance
* - address : "" - Address related to the balance
*
* Note: For a single address pass the 'addresses' of string type
*
* @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,
* "balances":[
* {
* "balance":{
* "confirmed":1000,
* "unconfirmed":0
* },
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"
* }
* ]
* }
*
* @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.
*
* Given an address, this endpoint will return an object with thre following
* properties:
*
* - address: "" - the address these UTXOs are associated with
* - bchUtxos: [] - UTXOs confirmed to be spendable as normal BCH
* - nullUtxo: [] - UTXOs that did not pass SLP validation. Should be ignored and
* not spent, to be safe.
* - slpUtxos: {} - UTXOs confirmed to be colored as valid SLP tokens
* - type1: {}
* - tokens: [] - SLP token Type 1 tokens.
* - mintBatons: [] - SLP token Type 1 mint batons.
* - nft: {}
* - tokens: [] - NFT tokens
* - groupTokens: [] - NFT Group tokens, used to create NFT tokens.
* - groupMintBatons: [] - Minting baton to create more NFT Group tokens.
*
*
* @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
* [
* {
* "address":"bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj",
* "bchUtxos":[
* {
* "height":631219,
* "tx_hash":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "tx_pos":0,
* "value":1000,
* "txid":"ae2daa01c8172545b5edd205ea438706bcb74e63d4084a26b9ff2a46d46dc97f",
* "vout":0,
* "isValid":false
* }
* ],
* "nullUtxos":[
*
* ],
* "slpUtxos":{
* "type1":{
* "mintBatons":[
*
* ],
* "tokens":[
*
* ]
* },
* "nft":{
* "groupMintBatons":[
*
* ],
* "groupTokens":[
*
* ],
* "tokens":[
*
* ]
* }
* }
* }
* ]
*
* @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.
*
* This endpoint will return a transaction id
*
* @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
* "951299775f68a599b95239bfc385423f87a33e11747c299a22ef9dcf3d1557ec"
*
* @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(`txid: ${JSON.stringify(txid, 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.
* Given a transaction id this endpoint will return an object
* with the following properties
*
* - txid: "" - Transaction ID
* - hash: "" - Transaction hash
* - version: - Version number
* - size: - Transaction size
* - locktime: -
* - vin: [] - Transaction inputs
* - vout: [] - Transaction outputs
* - hex: "" - hexadecimal script
* - blockhash: "" - Reference to the block register
* - confirmations : "" - Transaction confirmations
* - time: - Execution time
* - blocktime: - Execution time
* - isValidSLPTx: - Determines if the transaction was under SLP
*
* @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
* {
* "txid":"01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b",
* "hash":"01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b",
* "version":1,
* "size":272,
* "locktime":0,
* "vin":[
* {
* "txid":"4deef6de4b973706cd6e8fc8105a41a84be349e4e9717225ee5e7c63538e95e8",
* "vout":1,
* "scriptSig":{
* "asm":"3045022100fce6ef975fa7ec66e0ce0c51d839fd8f56510897252c0b238e7265974bc7c07202200d1d1429154e6775eecdc2829965650bc3ca714a86088d705bd58f8c034f2496[ALL|FORKID] 0467ff2df20f28bc62ad188525868f41d461f7dab3c1e500314cdb5218e5637bfd0f9c02eb5b3f383f698d28ff13547eaf05dd9216130861dd0216824e9d7337e3",
* "hex":"483045022100fce6ef975fa7ec66e0ce0c51d839fd8f56510897252c0b238e7265974bc7c07202200d1d1429154e6775eecdc2829965650bc3ca714a86088d705bd58f8c034f249641410467ff2df20f28bc62ad188525868f41d461f7dab3c1e500314cdb5218e5637bfd0f9c02eb5b3f383f698d28ff13547eaf05dd9216130861dd0216824e9d7337e3"
* },
* "sequence":4294967295,
* "address":"bitcoincash:qqrxa0h9jqnc7v4wmj9ysetsp3y7w9l36u8gnnjulq",
* "value":0.00001824
* }
* ],
* "vout":[
* {
* "value":0,
* "n":0,
* "scriptPubKey":{
* "asm":"OP_RETURN -385055325 46226800369048b83cea897639bb39273c8e4b883bd8c2a435bbe7a237cc433a",
* "hex":"6a045d7af3962046226800369048b83cea897639bb39273c8e4b883bd8c2a435bbe7a237cc433a",
* "type":"nulldata"
* }
* },
* {
* "value":0.0000155,
* "n":1,
* "scriptPubKey":{
* "asm":"OP_DUP OP_HASH160 066ebee590278f32aedc8a4865700c49e717f1d7 OP_EQUALVERIFY OP_CHECKSIG",
* "hex":"76a914066ebee590278f32aedc8a4865700c49e717f1d788ac",
* "reqSigs":1,
* "type":"pubkeyhash",
* "addresses":[
* "bitcoincash:qqrxa0h9jqnc7v4wmj9ysetsp3y7w9l36u8gnnjulq"
* ]
* }
* }
* ],
* "hex":"0100000001e8958e53637c5eee257271e9e449e34ba8415a10c88f6ecd0637974bdef6ee4d010000008b483045022100fce6ef975fa7ec66e0ce0c51d839fd8f56510897252c0b238e7265974bc7c07202200d1d1429154e6775eecdc2829965650bc3ca714a86088d705bd58f8c034f249641410467ff2df20f28bc62ad188525868f41d461f7dab3c1e500314cdb5218e5637bfd0f9c02eb5b3f383f698d28ff13547eaf05dd9216130861dd0216824e9d7337e3ffffffff020000000000000000276a045d7af3962046226800369048b83cea897639bb39273c8e4b883bd8c2a435bbe7a237cc433a0e060000000000001976a914066ebee590278f32aedc8a4865700c49e717f1d788ac00000000",
* "blockhash":"0000000000000000008e8d83cba6d45a9314bc2ef4538d4e0577c6bed8593536",
* "confirmations":97988,
* "time":1568338904,
* "blocktime":1568338904,
* "isValidSLPTx":false
* }
*
* @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
@@ -6,21 +6,21 @@
const Router = require('koa-router')
// Local libraries.
const FulcrumRESTController = require('./controller')
const BCHRESTController = require('./controller')
class FulcrumRouter {
class BCHRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Fulcrum REST Controller.'
'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 Fulcrum REST Controller.'
'Instance of Use Cases library required when instantiating BCH REST Controller.'
)
}
@@ -30,10 +30,10 @@ class FulcrumRouter {
}
// Encapsulate dependencies.
this.fulcrumRESTController = new FulcrumRESTController(dependencies)
this.bchRESTController = new BCHRESTController(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/fulcrum'
const baseUrl = '/bch'
this.router = new Router({ prefix: baseUrl })
}
@@ -45,7 +45,11 @@ class FulcrumRouter {
}
// Define the routes and attach the controller.
this.router.post('/transactions', this.fulcrumRESTController.transactions)
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())
@@ -53,4 +57,4 @@ class FulcrumRouter {
}
}
module.exports = FulcrumRouter
module.exports = BCHRouter
@@ -1,84 +0,0 @@
/*
Controller for the /fulcrum REST API endpoints.
*/
const BCHJS = require('@psf/bch-js')
let _this
class FulcrumRESTController {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating Fulcrum REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating Fulcrum REST Controller.'
)
}
this.bchjs = new BCHJS()
_this = this
}
/**
* @api {post} /fulcrum/transactions Transactions
* @apiName Transactions
* @apiGroup REST Fulcrum
* @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/fulcrum/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)
}
}
// 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 = FulcrumRESTController
+3 -3
View File
@@ -11,7 +11,7 @@ const AuthRESTController = require('./auth')
const UserRouter = require('./users')
const ContactRESTController = require('./contact')
const LogsRESTController = require('./logs')
const FulcrumRESTController = require('./fulcrum')
const BCHRESTController = require('./bch')
class RESTControllers {
constructor (localConfig = {}) {
@@ -55,8 +55,8 @@ class RESTControllers {
logsRESTController.attach(app)
// Attach the REST API Controllers associated with the /fulcrum route
const fulcrumRESTController = new FulcrumRESTController(dependencies)
fulcrumRESTController.attach(app)
const bchRESTController = new BCHRESTController(dependencies)
bchRESTController.attach(app)
}
}
+4 -11
View File
@@ -3,7 +3,7 @@
functions are called by the /user REST API endpoints.
*/
// const UserModel = require('../adapters/localdb/models/users')
const UserEntity = require('../entities/user')
const { wlogger } = require('../adapters/wlogger')
class UserLib {
@@ -17,6 +17,7 @@ class UserLib {
}
// Encapsulate dependencies
this.UserEntity = new UserEntity()
this.UserModel = this.adapters.localdb.Users
}
@@ -24,17 +25,9 @@ class UserLib {
async createUser (userObj) {
try {
// Input Validation
if (!userObj.email || typeof userObj.email !== 'string') {
throw new Error("Property 'email' must be a string!")
}
if (!userObj.password || typeof userObj.password !== 'string') {
throw new Error("Property 'password' must be a string!")
}
if (!userObj.name || typeof userObj.name !== 'string') {
throw new Error("Property 'name' must be a string!")
}
const user = new this.UserModel(userObj)
const userEntity = this.UserEntity.validate(userObj)
const user = new this.UserModel(userEntity)
// Enforce default value of 'user'
user.type = 'user'
+21 -15
View File
@@ -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', () => {
+11
View File
@@ -0,0 +1,11 @@
# Consumer Simulation E2E Test
The files in this directory simulates a consumer of web services, using JSON RPC over IPFS. It starts a new IPFS node that connects to the ipfs-bch-wallet-service and exercises each JSON RPC endpoint.
## Instructions
Execute these steps in order to run the test:
1. Run your local copy of ipfs-bch-wallet-service service provider.
2. Update the [test-data.json](./test-data.json) file with the IPFS ID of the ipfs-bch-wallet-service.
3. In another terminal, run the client simulation with `node client.e2e.js`
+29
View File
@@ -0,0 +1,29 @@
/*
An end-to-end (e2e) test simulating a client connecting over IPFS and
that exercises each JSON RPC endpoint.
*/
const TestUtils = require('./lib/test-utils')
const testUtils = new TestUtils()
const testData = require('./test-data.json')
async function startTest () {
try {
console.log('E2E TEST: Starting consumer E2E tests...')
await testUtils.startIpfs()
await testUtils.connectToUut(testData.uutAddr, testData.uutId)
await testUtils.testBalance(testData.uutId)
await testUtils.testTransaction(testData.uutId)
console.log('E2E TEST: Tests completed successfully!')
process.exit()
} catch (err) {
console.error('Error in startTest(): ', err)
}
}
startTest()
+217
View File
@@ -0,0 +1,217 @@
/*
A class library of test utility functions.
*/
// Public npm libraries
const BCHJS = require('@psf/bch-js')
const IpfsCoord = require('ipfs-coord')
const IPFS = require('ipfs')
const EventEmitter = require('events')
const { v4: uid } = require('uuid')
const jsonrpc = require('jsonrpc-lite')
let _this
class TestUtils {
constructor (localConfig = {}) {
// Encapsulate dependencies.
this.bchjs = new BCHJS()
this.eventEmitter = new EventEmitter()
this.uid = uid
this.jsonrpc = jsonrpc
_this = this
}
// This handler function recieves data from other ipfs-coord peers, like
// the ipfs-bch-wallet-service that we're testing.
// It emits the data as an 'rpcData' event. The tests will listen for this
// event.
rpcHandler (inData) {
try {
// console.log('Data recieved by rpcHandler: ', inData)
const jsonData = JSON.parse(inData)
_this.eventEmitter.emit('rpcData', jsonData)
} catch (err) {
console.error('Error in rpcHandler: ', err)
// Do not throw an error. This is a top-level function.
}
}
// Send the RPC command to the service, wait a period of time for a response.
// Timeout if a response is not recieved.
async sendRPC (ipfsId, cmdStr) {
try {
// Send the RPC command to the server/service.
await this.ipfsCoord.ipfs.orbitdb.sendToDb(ipfsId, cmdStr)
let retData
// This event is triggered when the response comes back.
this.eventEmitter.on('rpcData', inData => {
retData = inData
})
// Used for calculating the timeout.
const start = new Date()
let now = start
let timeDiff = 0
// Wait for the response from the server. Exit once the response is
// recieved, or a timeout occurs.
do {
await this.bchjs.Util.sleep(1000)
now = new Date()
timeDiff = now.getTime() - start.getTime()
// console.log('timeDiff: ', timeDiff)
} while (
// Exit once the RPC data comes back, or if a period of time passes.
!retData || // eslint-disable-line no-unmodified-loop-condition
timeDiff > 10000
)
return retData
} catch (err) {
console.error('Error in sendRPC')
throw err
}
}
async startIpfs () {
try {
// Start the IPFS node.
this.ipfs = await IPFS.create()
await this.ipfs.config.profiles.apply('server')
// Start ipfs-coord.
this.ipfsCoord = new IpfsCoord({
ipfs: this.ipfs,
type: 'node.js',
// type: 'browser',
bchjs: this.bchjs,
privateLog: this.rpcHandler, // Default to console.log
isCircuitRelay: false,
apiInfo: 'none',
announceJsonLd: announceJsonLd
})
await this.ipfsCoord.ipfs.start()
await this.ipfsCoord.isReady()
// Wait to let ipfs-coord connect to subnet peers.
await this.bchjs.Util.sleep(30000)
} catch (err) {
console.error('Error in startIpfs().')
throw err
}
}
// Connect to the unit under test (uut)
async connectToUut (addr, ipfsId) {
try {
try {
await this.ipfs.swarm.connect(addr)
console.log(`E2E TEST: Connected to IPFS node ${addr}`)
} catch (err) {
throw new Error('Could not connect to UUT IPFS node.')
}
// console.log(
// 'ipfs-coord peer info: ',
// this.ipfsCoord.ipfs.peers.state.peers[ipfsId]
// )
if (!this.ipfsCoord.ipfs.peers.state.peers[ipfsId]) {
throw new Error('Could not find UUT in ipfs-coord list of peers.')
}
} catch (err) {
console.error('Error in connectToUut()')
console.log(
'Is the service running? Did you update the test-data.json file?'
)
process.exit()
}
}
// Get the balance for a BCH address over JSON RPC.
async testBalance (ipfsId) {
try {
// Generate the JSON RPC command
const id = uid()
const cmd = jsonrpc.request(id, 'bch', {
endpoint: 'balance',
addresses: ['bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj']
})
const cmdStr = JSON.stringify(cmd)
// console.log(`Publishing message to ${ipfsId}`)
console.log('E2E TEST: Sending Balance command...')
const result = await this.sendRPC(ipfsId, cmdStr)
// console.log('result: ', result)
if (result.result.value.success && result.result.value.balances) {
console.log('E2E TEST: balance test passed.')
return true
} else {
console.log('E2E TEST: balance test failed.')
this.failTest()
}
} catch (err) {
console.error('Error in testBalance()')
throw err
}
}
// Get the info a on transaction over JSON RPC.
async testTransaction (ipfsId) {
try {
// Generate the JSON RPC command
const id = uid()
const cmd = jsonrpc.request(id, 'bch', {
endpoint: 'transaction',
txid: '01517ff1587fa5ffe6f5eb91c99cf3f2d22330cd7ee847e928ce90ca95bf781b'
})
const cmdStr = JSON.stringify(cmd)
// console.log(`Publishing message to ${ipfsId}`)
console.log('E2E TEST: Sending Transaction command...')
const result = await this.sendRPC(ipfsId, cmdStr)
// console.log('result: ', result)
if (result.result.value.status && result.result.value.confirmations) {
console.log('E2E TEST: transaction test passed.')
return true
} else {
console.log('E2E TEST: transaction test failed.')
this.failTest()
}
} catch (err) {
console.error('Error in testTransaction()')
throw err
}
}
failTest () {
console.log('Exiting test program.')
process.exit()
}
}
const announceJsonLd = {
'@context': 'https://schema.org/',
'@type': 'WebAPI',
name: 'e2e-test-client',
description: 'A test client runing e2e tests on ipfs-bch-wallet-service.',
documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/',
provider: {
'@type': 'Organization',
name: 'Permissionless Software Foundation',
url: 'https://PSFoundation.cash'
}
}
module.exports = TestUtils
+4
View File
@@ -0,0 +1,4 @@
{
"uutAddr": "/ip4/127.0.0.1/tcp/5668/p2p/QmZSyLnRQWMBknVNjUroLQnrcmDnUVAUU4pMQ2LGhh6b9v",
"uutId": "QmZSyLnRQWMBknVNjUroLQnrcmDnUVAUU4pMQ2LGhh6b9v"
}
@@ -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')
})
})
})
+16 -1
View File
@@ -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', () => {
+16 -2
View File
@@ -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) {
+20 -10
View File
@@ -1,30 +1,40 @@
// const assert = require('chai').assert
const {
notifyRotation,
outputToConsole
} = require('../../../src/adapters/wlogger')
const assert = require('chai').assert
const { Wlogger } = require('../../../src/adapters/wlogger')
const sinon = require('sinon')
// let uut
let uut
let sandbox
describe('#wlogger.js', () => {
describe('#wlogger', () => {
beforeEach(() => {
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
afterEach(() => {
sandbox.restore()
uut = new Wlogger()
})
describe('#constructor', () => {
it('should create a new wlogger instance', () => {
uut = new Wlogger()
// console.log('uut: ', uut)
assert.property(uut, 'transport')
})
})
describe('#notifyRotation', () => {
it('should notify of a log rotation', () => {
notifyRotation()
uut.notifyRotation()
})
})
describe('#envronment', () => {
it('should write to console in non-test environment', () => {
outputToConsole()
uut.outputToConsole()
})
})
})
+31 -4
View File
@@ -3,18 +3,21 @@
*/
// Public npm libraries
// const assert = require('chai').assert
const assert = require('chai').assert
const sinon = require('sinon')
const adapters = require('../../../src/adapters')
const { attachControllers } = require('../../../src/controllers')
// const { attachControllers } = require('../../../src/controllers')
const Controllers = require('../../../src/controllers')
describe('#Controllers', () => {
// let uut
let uut
let sandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
uut = new Controllers()
})
afterEach(() => sandbox.restore())
@@ -22,6 +25,8 @@ describe('#Controllers', () => {
describe('#attachControllers', () => {
it('should attach the controllers', async () => {
// mock IPFS
sandbox.stub(adapters.fullStackJwt, 'getJWT').resolves({})
sandbox.stub(adapters.fullStackJwt, 'instanceBchjs').resolves({})
sandbox.stub(adapters.ipfs, 'start').resolves({})
adapters.ipfs.ipfsCoordAdapter = {
attachRPCRouter: () => {}
@@ -31,7 +36,29 @@ describe('#Controllers', () => {
use: () => {}
}
await attachControllers(app)
await uut.attachControllers(app)
assert.isOk(true, 'Not throwing an error is a success')
})
it('should catch and throw errors', async () => {
try {
// Force an error
sandbox
.stub(adapters.fullStackJwt, 'getJWT')
.rejects(new Error('test error'))
const app = {
use: () => {}
}
await uut.attachControllers(app)
assert.fail('Unexpected code path')
} 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')
})
})
})
@@ -1,248 +0,0 @@
/*
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 FulcrumRPC = require('../../../../src/controllers/json-rpc/fulcrum')
const RateLimit = require('../../../../src/controllers/json-rpc/rate-limit')
const adapters = require('../../mocks/adapters')
const UseCasesMock = require('../../mocks/use-cases')
describe('#FulcrumRPC', () => {
let uut
let sandbox
beforeEach(() => {
sandbox = sinon.createSandbox()
const useCases = new UseCasesMock()
uut = new FulcrumRPC({ 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 FulcrumRPC()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating Fulcrum JSON RPC Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new FulcrumRPC({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Fulcrum JSON RPC Controller.'
)
}
})
})
describe('#fulcrumRouter', () => {
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, 'fulcrum', {
endpoint: 'transactions'
})
const jsonStr = JSON.stringify(txCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.fulcrumRouter(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, 'fulcrum', {
endpoint: 'transactions'
})
const jsonStr = JSON.stringify(txCall, null, 2)
const rpcData = jsonrpc.parse(jsonStr)
rpcData.from = 'Origin request'
const result = await uut.fulcrumRouter(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, 'fulcrum', {
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, 'fulcrum', {
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('#authUser', () => {
// it('should return a JWT token if user successfully authenticates', async () => {
// // Generate the parsed data that the main router would pass to this
// // endpoint.
// const id = uid()
// const authCall = jsonrpc.request(id, 'auth', {
// endpoint: 'authUser',
// login: 'test543@test.com',
// password: 'password'
// })
// const jsonStr = JSON.stringify(authCall, null, 2)
// const rpcData = jsonrpc.parse(jsonStr)
//
// const response = await uut.authUser(rpcData)
// // console.log('response: ', response)
//
// assert.equal(response.endpoint, 'authUser')
// assert.property(response, 'userId')
// // assert.equal(response.userType, 'user')
// assert.property(response, 'userName')
// assert.property(response, 'userEmail')
// assert.property(response, 'apiToken')
// assert.equal(response.status, 200)
// assert.equal(response.success, true)
// assert.property(response, 'message')
// })
//
// it('should return an error for invalid credentials', async () => {
// // Generate the parsed data that the main router would pass to this
// // endpoint.
// const id = uid()
// const authCall = jsonrpc.request(id, 'auth', {
// endpoint: 'authUser',
// login: 'test543@test.com',
// password: 'badpassword'
// })
// const jsonStr = JSON.stringify(authCall, null, 2)
// const rpcData = jsonrpc.parse(jsonStr)
//
// // Force an error.
// sandbox
// .stub(uut.userLib, 'authUser')
// .rejects(new Error('Login credential do not match'))
//
// const response = await uut.authUser(rpcData)
// // console.log('response: ', response)
//
// assert.equal(response.success, false)
// assert.equal(response.status, 422)
// assert.equal(response.message, 'Login credential do not match')
// assert.equal(response.endpoint, 'authUser')
// })
//
// it('should throw an error if login is not provided', async () => {
// // Generate the parsed data that the main router would pass to this
// // endpoint.
// const id = uid()
// const authCall = jsonrpc.request(id, 'auth', {
// endpoint: 'authUser'
// })
// const jsonStr = JSON.stringify(authCall, null, 2)
// const rpcData = jsonrpc.parse(jsonStr)
//
// const response = await uut.authUser(rpcData)
// // console.log('response: ', response)
//
// assert.equal(response.success, false)
// assert.equal(response.status, 422)
// assert.equal(response.message, 'login must be specified')
// assert.equal(response.endpoint, 'authUser')
// })
//
// it('should throw an error if password is not provided', async () => {
// // Generate the parsed data that the main router would pass to this
// // endpoint.
// const id = uid()
// const authCall = jsonrpc.request(id, 'auth', {
// endpoint: 'authUser',
// login: 'test543@test.com'
// })
// const jsonStr = JSON.stringify(authCall, null, 2)
// const rpcData = jsonrpc.parse(jsonStr)
//
// const response = await uut.authUser(rpcData)
// // console.log('response: ', response)
//
// assert.equal(response.success, false)
// assert.equal(response.status, 422)
// assert.equal(response.message, 'password must be specified')
// assert.equal(response.endpoint, 'authUser')
// })
// })
})
@@ -180,15 +180,15 @@ describe('#JSON RPC', () => {
assert.equal(obj.id, id)
})
it('should route to fulcrum handler', async () => {
it('should route to bch handler', async () => {
const id = uid()
const userCall = jsonrpc.request(id, 'fulcrum', {
const userCall = jsonrpc.request(id, 'bch', {
endpoint: 'transactions'
})
const jsonStr = JSON.stringify(userCall, null, 2)
// Mock the controller.
sandbox.stub(uut.fulcrumController, 'fulcrumRouter').resolves('true')
sandbox.stub(uut.bchController, 'bchRouter').resolves('true')
// Force ipfs-coord communication.
uut.ipfsCoord.ipfs = {
@@ -204,7 +204,7 @@ describe('#JSON RPC', () => {
// console.log('obj: ', obj)
assert.equal(obj.result.value, 'true')
assert.equal(obj.result.method, 'fulcrum')
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')
}
})
})
})
@@ -1,5 +1,5 @@
/*
Unit tests for the REST API handler for the /fulcrum endpoints.
Unit tests for the REST API handler for the /bch endpoints.
*/
// Public npm libraries
@@ -11,19 +11,19 @@ const adapters = require('../../../mocks/adapters')
const UseCasesMock = require('../../../mocks/use-cases')
// const app = require('../../../mocks/app-mock')
const FulcrumRouter = require('../../../../../src/controllers/rest-api/fulcrum')
const BCHRouter = require('../../../../../src/controllers/rest-api/bch')
let uut
let sandbox
// let ctx
// const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Fulcrum-REST-Router', () => {
describe('#BCH-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new FulcrumRouter({ adapters, useCases })
uut = new BCHRouter({ adapters, useCases })
sandbox = sinon.createSandbox()
@@ -36,26 +36,26 @@ describe('#Fulcrum-REST-Router', () => {
describe('#constructor', () => {
it('should throw an error if adapters are not passed in', () => {
try {
uut = new FulcrumRouter()
uut = new BCHRouter()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating Fulcrum REST Controller.'
'Instance of Adapters library required when instantiating BCH REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new FulcrumRouter({ adapters })
uut = new BCHRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Fulcrum REST Controller.'
'Instance of Use Cases library required when instantiating BCH REST Controller.'
)
}
})
@@ -1,126 +0,0 @@
/*
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 FulcrumRESTController = require('../../../../../src/controllers/rest-api/fulcrum/controller')
let uut
let sandbox
let ctx
const mockContext = require('../../../../unit/mocks/ctx-mock').context
describe('#Fulcrum-REST-Router', () => {
// const testUser = {}
beforeEach(() => {
const useCases = new UseCasesMock()
uut = new FulcrumRESTController({ 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 FulcrumRESTController()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Adapters library required when instantiating Fulcrum REST Controller.'
)
}
})
it('should throw an error if useCases are not passed in', () => {
try {
uut = new FulcrumRESTController({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Fulcrum 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('#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')
}
})
})
})
+17 -1
View File
@@ -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 }
+6
View File
@@ -3,6 +3,12 @@
*/
class IPFSCoord {
constructor () {
this.ipfs = {
async start () {}
}
}
async isReady () {
return true
}
+1 -1
View File
@@ -57,7 +57,7 @@ describe('#users-use-case', () => {
} catch (err) {
// console.log(err)
// assert.equal(err.status, 422)
assert.include(err.message, 'Cannot read property')
assert.include(err.message, "Property 'email' must be a string!")
}
})