Compare commits

...
7 Commits
11 changed files with 838 additions and 7 deletions
+13 -6
View File
@@ -13,9 +13,15 @@ module.exports = {
logPass: 'test',
// Email server settings if nodemailer email notifications are used.
emailServer: process.env.EMAILSERVER ? process.env.EMAILSERVER : 'mail.someserver.com',
emailUser: process.env.EMAILUSER ? process.env.EMAILUSER : 'noreply@someserver.com',
emailPassword: process.env.EMAILPASS ? process.env.EMAILPASS : 'emailpassword',
emailServer: process.env.EMAILSERVER
? process.env.EMAILSERVER
: 'mail.someserver.com',
emailUser: process.env.EMAILUSER
? process.env.EMAILUSER
: 'noreply@someserver.com',
emailPassword: process.env.EMAILPASS
? process.env.EMAILPASS
: 'emailpassword',
// IPFS settings.
isCircuitRelay: process.env.ENABLE_CIRCUIT_RELAY ? true : false,
@@ -27,9 +33,10 @@ module.exports = {
announceJsonLd: {
'@context': 'https://schema.org/',
'@type': 'WebAPI',
name: 'ipfs-service-provider',
description: 'This is a generic IPFS Serivice Provider that uses JSON RPC over IPFS to communicate with it. This instance has not been customized. Source code: https://github.com/Permissionless-Software-Foundation/ipfs-service-provider',
documentation: 'https://ipfs-service-provider.fullstack.cash/',
name: 'ipfs-bch-wallet-service',
description:
'IPFS service providing BCH blockchain access needed by a wallet.',
documentation: 'https://ipfs-bch-wallet-service.fullstack.cash/',
provider: {
'@type': 'Organization',
name: 'Permissionless Software Foundation',
+167
View File
@@ -0,0 +1,167 @@
/*
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'
}
}
}
/**
* @api {JSON} /fulcrum Balance
* @apiPermission public
* @apiName Balance
* @apiGroup JSON Fulcrum
* @apiDescription This endpoint wraps the bchjs.Electrumx.balance([]) function.
*
* @apiExample Example usage:
* {"jsonrpc":"2.0","id":"555","method":"fulcrum","params":{ "endpoint": "balance", "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"]}}
*
*/
async balance (rpcData) {
try {
// console.log('createUser rpcData: ', rpcData)
const addrs = rpcData.payload.params.addresses
const data = await this.bchjs.Electrumx.balance(addrs)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
const retObj = data
retObj.status = 200
return retObj
} catch (err) {
// console.error('Error in createUser()')
// throw err
// Return an error response
return {
success: false,
status: 422,
message: err.message,
endpoint: 'balance'
}
}
}
// TODO create deleteUser()
}
module.exports = FulcrumRPC
+5
View File
@@ -10,6 +10,7 @@ const { wlogger } = require('../../adapters/wlogger')
const UserController = require('./users')
const AuthController = require('./auth')
const AboutController = require('./about')
const FulcrumController = require('./fulcrum')
let _this
@@ -35,6 +36,7 @@ class JSONRPC {
this.userController = new UserController(localConfig)
this.authController = new AuthController(localConfig)
this.aboutController = new AboutController()
this.fulcrumController = new FulcrumController(localConfig)
_this = this
}
@@ -80,6 +82,9 @@ class JSONRPC {
break
case 'about':
retObj = await _this.aboutController.aboutRouter(parsedData)
break
case 'fulcrum':
retObj = await _this.fulcrumController.fulcrumRouter(parsedData)
}
// console.log('retObj: ', retObj)
+1 -1
View File
@@ -14,7 +14,7 @@ class RateLimit {
// Set default rate limit options.
this.defaultOptions = {
interval: { min: 1 },
max: 60,
max: 1000, // 1000 RPM while prototyping.
onLimitReached: this.onLimitReached
}
@@ -0,0 +1,122 @@
/*
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)
}
}
/**
* @api {post} /fulcrum/balance Balance
* @apiName Balance
* @apiGroup REST Fulcrum
* @apiDescription This endpoint returns the balance in BCH for an address.
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "addresses": ["bitcoincash:qrl2nlsaayk6ekxn80pq0ks32dya8xfclyktem2mqj"] }' localhost:5001/fulcrum/balance
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* success:true,
* data: <data>
* }
*
* @apiError UnprocessableEntity Missing required parameters
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 422 Unprocessable Entity
* {
* "status": 422,
* "error": "Unprocessable Entity"
* }
*/
async balance (ctx) {
try {
const addrs = ctx.request.body.addresses
const data = await _this.bchjs.Electrumx.balance(addrs)
// console.log(`data: ${JSON.stringify(data, null, 2)}`)
ctx.body = data
} catch (err) {
_this.handleError(ctx, err)
}
}
// 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
+56
View File
@@ -0,0 +1,56 @@
/*
REST API library for /fulcrum route.
*/
// Public npm libraries.
const Router = require('koa-router')
// Local libraries.
const FulcrumRESTController = require('./controller')
class FulcrumRouter {
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.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.fulcrumRESTController = new FulcrumRESTController(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/fulcrum'
this.router = new Router({ prefix: baseUrl })
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attaching REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.post('/transactions', this.fulcrumRESTController.transactions)
// Attach the Controller routes to the Koa app.
app.use(this.router.routes())
app.use(this.router.allowedMethods())
}
}
module.exports = FulcrumRouter
+5
View File
@@ -11,6 +11,7 @@ const AuthRESTController = require('./auth')
const UserRouter = require('./users')
const ContactRESTController = require('./contact')
const LogsRESTController = require('./logs')
const FulcrumRESTController = require('./fulcrum')
class RESTControllers {
constructor (localConfig = {}) {
@@ -52,6 +53,10 @@ class RESTControllers {
// Attach the REST API Controllers associated with the /logs route
const logsRESTController = new LogsRESTController(dependencies)
logsRESTController.attach(app)
// Attach the REST API Controllers associated with the /fulcrum route
const fulcrumRESTController = new FulcrumRESTController(dependencies)
fulcrumRESTController.attach(app)
}
}
@@ -0,0 +1,203 @@
/*
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('#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, 'fulcrum', {
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, 'fulcrum', {
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')
})
})
})
@@ -179,5 +179,33 @@ describe('#JSON RPC', () => {
assert.equal(obj.result.method, 'about')
assert.equal(obj.id, id)
})
it('should route to fulcrum handler', async () => {
const id = uid()
const userCall = jsonrpc.request(id, 'fulcrum', {
endpoint: 'transactions'
})
const jsonStr = JSON.stringify(userCall, null, 2)
// Mock the controller.
sandbox.stub(uut.fulcrumController, 'fulcrumRouter').resolves('true')
// Force ipfs-coord communication.
uut.ipfsCoord.ipfs = {
orbitdb: {
sendToDb: () => {}
}
}
const result = await uut.router(jsonStr, 'peerA')
// console.log(result)
const obj = JSON.parse(result.retStr)
// console.log('obj: ', obj)
assert.equal(obj.result.value, 'true')
assert.equal(obj.result.method, 'fulcrum')
assert.equal(obj.id, id)
})
})
})
@@ -0,0 +1,160 @@
/*
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('#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('#handleError', () => {
it('should pass an error message', () => {
try {
const err = {
status: 422,
message: 'Unprocessable Entity'
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Unprocessable Entity')
}
})
it('should still throw error if there is no message', () => {
try {
const err = {
status: 404
}
uut.handleError(ctx, err)
} catch (err) {
assert.include(err.message, 'Not Found')
}
})
})
})
@@ -0,0 +1,78 @@
/*
Unit tests for the REST API handler for the /fulcrum 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 FulcrumRouter = require('../../../../../src/controllers/rest-api/fulcrum')
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 FulcrumRouter({ 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 FulcrumRouter()
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 FulcrumRouter({ adapters })
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Instance of Use Cases library required when instantiating Fulcrum REST Controller.'
)
}
})
})
describe('#attach', () => {
it('should throw an error if app is not passed in.', () => {
try {
uut.attach()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(
err.message,
'Must pass app object when attaching REST API controllers.'
)
}
})
})
})