Compare commits

...
10 Commits
12 changed files with 847 additions and 7 deletions
+9
View File
@@ -10,6 +10,15 @@ This repository is forked from the [ipfs-service-provider](https://github.com/Pe
As part of the end-to-end testing, a virtual client can be started that will interrogate every endpoint provided by this API server. This client can be forked by developers to write their own apps that use this IPFS-based service.
## Roadmap
This repository is one step in a longer path. Here are the major milestones of this path:
- Add wallet-based calls to ipfs-bch-wallet-service (this repository).
- Create a miniature version of bch-js that makes calls over JSON RPC (bch-js-ipfs).
- Given a variable, minimal-slp-wallet could call bch-js or bch-js-ipfs for network calls.
- Fork slp-cli-wallet and add `daemon` feature to spin up IPFS node and work with bch-js-ipfs.
## Requirements
- node **^14.17.0**
+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.'
)
}
})
})
})