diff --git a/.gitignore b/.gitignore index d0dd9cc..2c1bf6c 100644 --- a/.gitignore +++ b/.gitignore @@ -66,5 +66,6 @@ orbitdb ipfsdata .ipfsdata data/ +run-dev.sh !README.md diff --git a/src/adapters/ipfs/index.js b/src/adapters/ipfs/index.js index be27bb7..fb5eba1 100644 --- a/src/adapters/ipfs/index.js +++ b/src/adapters/ipfs/index.js @@ -58,6 +58,134 @@ class IPFS { throw err } } + + // Get the status of this IPFS node. + // Create an object with different metrics about this IPFS node, then return + // the object. + getStatus () { + try { + const statusObj = { + ipfsId: this.ipfsCoordAdapter.ipfsCoord.thisNode.ipfsId, + multiAddrs: this.ipfsCoordAdapter.ipfsCoord.thisNode.ipfsMultiaddrs, + bchAddr: this.ipfsCoordAdapter.ipfsCoord.thisNode.bchAddr, + slpAddr: this.ipfsCoordAdapter.ipfsCoord.thisNode.slpAddr, + pubKey: this.ipfsCoordAdapter.ipfsCoord.thisNode.pubKey, + peers: this.ipfsCoordAdapter.ipfsCoord.thisNode.peerList.length, + relays: this.ipfsCoordAdapter.ipfsCoord.thisNode.relayData.length + } + + return statusObj + } catch (err) { + console.error('Error in ipfs-coord.js/getStatus()') + throw err + } + } + + // Get details on the other peers this node is connected to. + async getPeers (showAll) { + try { + const peerData = this.ipfsCoordAdapter.ipfsCoord.thisNode.peerData + console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`) + + let ipfsPeers = + await this.ipfsCoordAdapter.ipfsCoord.adapters.ipfs.getPeers() + console.log('ipfsPeers: ', ipfsPeers) + + ipfsPeers = this._removeDuplicatePeers(ipfsPeers) + console.log('filtered ipfsPeers: ', ipfsPeers) + + const peerDataOut = [] + + // Loop through each IPFS peer and hydrate it with data from the peerData. + for (let i = 0; i < ipfsPeers.length; i++) { + const thisPeer = { + peer: ipfsPeers[i] + } + + // if (!showAll) { + // // Delete properties that don't contain good info. + // delete thisPeer.muxer + // delete thisPeer.latency + // delete thisPeer.streams + // } + + // Get the ipfs-coord peer data for this peer. + let thisPeerData = peerData.filter((x) => + x.from.includes(thisPeer.peer) + ) + thisPeerData = thisPeerData[0] + + // Skip if peerData for this IPFS peer could not be found. + // if (!thisPeerData) continue + + try { + // console.log('thisPeerData: ', thisPeerData) + + // Add data to the IPFS peer data. + thisPeer.name = thisPeerData.data.jsonLd.name + thisPeer.protocol = thisPeerData.data.jsonLd.protocol + thisPeer.version = thisPeerData.data.jsonLd.version + + if (showAll) { + // Add all the peer data. + thisPeer.peerData = thisPeerData + } + } catch (err) { + console.log( + `Error trying to hydrate peer ${thisPeer.peer}: ${err.message}` + ) + } + + peerDataOut.push(thisPeer) + } + + return peerDataOut + } catch (err) { + console.error('Error in getPeers(): ', err) + throw err + } + } + + // Expects an array of peers and returns an array of peers with duplicates + // removed. + // This function is used by getPeers() + _removeDuplicatePeers (arr) { + // https://stackoverflow.com/questions/2218999/how-to-remove-all-duplicates-from-an-array-of-objects + return arr.filter((v, i, a) => a.findIndex((t) => t === v) === i) + } + + // Get data about the known Circuit Relays. Hydrate with data from peers list. + getRelays () { + try { + const relayData = this.ipfsCoordAdapter.ipfsCoord.thisNode.relayData + const peerData = this.ipfsCoordAdapter.ipfsCoord.thisNode.peerData + // console.log(`relayData: ${JSON.stringify(relayData, null, 2)}`) + // console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`) + + for (let i = 0; i < relayData.length; i++) { + const thisRelay = relayData[i] + + // Find the peer that corresponds to this relay. + const thisPeer = peerData.filter((x) => + x.from.includes(thisRelay.ipfsId) + ) + // console.log('thisPeer: ', thisPeer) + + // If the peer couldn't be found, skip. + if (!thisPeer.length) { + thisRelay.name = '' + continue + } + + thisRelay.name = thisPeer[0].data.jsonLd.name + } + + return relayData + } catch (err) { + console.error('Error in getRelays(): ', err) + throw err + } + } } export default IPFS diff --git a/src/controllers/rest-api/index.js b/src/controllers/rest-api/index.js index 5b5f916..63e18a6 100644 --- a/src/controllers/rest-api/index.js +++ b/src/controllers/rest-api/index.js @@ -11,6 +11,7 @@ import AuthRESTController from './auth/index.js' import UserRouter from './users/index.js' import ContactRESTController from './contact/index.js' import LogsRESTController from './logs/index.js' +import IpfsRESTController from './ipfs/index.js' 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 /ipfs route + const ipfsRESTController = new IpfsRESTController(dependencies) + ipfsRESTController.attach(app) } } diff --git a/src/controllers/rest-api/ipfs/controller.js b/src/controllers/rest-api/ipfs/controller.js new file mode 100644 index 0000000..5300f4a --- /dev/null +++ b/src/controllers/rest-api/ipfs/controller.js @@ -0,0 +1,99 @@ +/* + REST API Controller library for the /ipfs route +*/ + +import wlogger from '../../../adapters/wlogger.js' + +let _this + +class IpfsRESTControllerLib { + constructor (localConfig = {}) { + // Dependency Injection. + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating /ipfs REST Controller.' + ) + } + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error( + 'Instance of Use Cases library required when instantiating /ipfs REST Controller.' + ) + } + + // Encapsulate dependencies + // this.UserModel = this.adapters.localdb.Users + // this.userUseCases = this.useCases.user + + _this = this + } + + /** + * @api {get} /ipfs Get status on IPFS infrastructure + * @apiPermission public + * @apiName GetIpfsStatus + * @apiGroup REST BCH + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs + * + */ + async getStatus (ctx) { + try { + const status = await _this.adapters.ipfs.getStatus() + + ctx.body = { status } + } catch (err) { + wlogger.error('Error in ipfs/controller.js/getStatus(): ') + // ctx.throw(422, err.message) + _this.handleError(ctx, err) + } + } + + // Return information on IPFS peers this node is connected to. + async getPeers (ctx) { + try { + const showAll = ctx.request.body.showAll + + const peers = await _this.adapters.ipfs.getPeers(showAll) + + ctx.body = { peers } + } catch (err) { + wlogger.error('Error in ipfs/controller.js/getPeers(): ') + // ctx.throw(422, err.message) + _this.handleError(ctx, err) + } + } + + // Get data about the known Circuit Relays. Hydrate with data from peers list. + async getRelays (ctx) { + try { + const relays = await _this.adapters.ipfs.getRelays() + + ctx.body = { relays } + } catch (err) { + wlogger.error('Error in ipfs/controller.js/getRelays(): ') + // ctx.throw(422, err.message) + _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 = IpfsRESTControllerLib +export default IpfsRESTControllerLib diff --git a/src/controllers/rest-api/ipfs/index.js b/src/controllers/rest-api/ipfs/index.js new file mode 100644 index 0000000..c3b65e4 --- /dev/null +++ b/src/controllers/rest-api/ipfs/index.js @@ -0,0 +1,69 @@ +/* + REST API library for the /ipfs route. +*/ + +// Public npm libraries. +import Router from 'koa-router' + +// Local libraries. +import IPFSRESTControllerLib from './controller.js' +import Validators from '../middleware/validators.js' + +// let _this + +class BchRouter { + constructor (localConfig = {}) { + // Dependency Injection. + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of Adapters library required when instantiating IPFS REST Controller.' + ) + } + this.useCases = localConfig.useCases + if (!this.useCases) { + throw new Error( + 'Instance of Use Cases library required when instantiating IPFS REST Controller.' + ) + } + + const dependencies = { + adapters: this.adapters, + useCases: this.useCases + } + + // Encapsulate dependencies. + this.ipfsRESTController = new IPFSRESTControllerLib(dependencies) + this.validators = new Validators() + + // Instantiate the router and set the base route. + const baseUrl = '/ipfs' + this.router = new Router({ prefix: baseUrl }) + + // _this = this + } + + 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.get('/', this.ipfsRESTController.getStatus) + this.router.post('/peers', this.ipfsRESTController.getPeers) + this.router.post('/relays', this.ipfsRESTController.getRelays) + + // Attach the Controller routes to the Koa app. + app.use(this.router.routes()) + app.use(this.router.allowedMethods()) + } + + // async getStatus (ctx, next) { + // await _this.ipfsRESTController.getStatus(ctx, next) + // } +} + +// module.exports = BchRouter +export default BchRouter diff --git a/test/unit/adapters/ipfs-index.adapter.unit.js b/test/unit/adapters/ipfs-index.adapter.unit.js index f3cb8ed..2d2f8b0 100644 --- a/test/unit/adapters/ipfs-index.adapter.unit.js +++ b/test/unit/adapters/ipfs-index.adapter.unit.js @@ -66,4 +66,79 @@ describe('#IPFS-adapter-index', () => { } }) }) + + describe('#getStatus', () => { + it('should return an object with node metrics', () => { + // Force uut to have the needed properties + uut.ipfsCoordAdapter = { + ipfsCoord: { + thisNode: { + ipfsId: 'fake-id', + ipfsMultiaddrs: [], + bchAddr: 'fake-bch-addr', + slpAddr: 'fake-slp-addr', + pubKey: 'fake-pubkey', + peerList: [], + relayData: [] + } + } + } + + const result = uut.getStatus() + // console.log('result: ', result) + + assert.property(result, 'ipfsId') + assert.property(result, 'multiAddrs') + assert.property(result, 'bchAddr') + assert.property(result, 'slpAddr') + assert.property(result, 'pubKey') + assert.property(result, 'peers') + assert.property(result, 'relays') + }) + + it('should catch, report, and throw errors', () => { + try { + uut.getStatus() + + assert.fail('Unexpected code path') + } catch (err) { + // console.log('err.message: ', err.message) + assert.include(err.message, 'Cannot read properties') + } + }) + }) + + describe('#_removeDuplicatePeers', () => { + it('should return the same array when there are no duplicates', () => { + const inArray = [ + '12D3KooWPpBXhhAeoCZCGuQ3KR4xwHzzvtP57f6zLmo8P7ZFBJFE', + '12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA', + '12D3KooWGsgHWyDLKuV4ZSfRJfsxQJj77rxx3i8Px3qXKHsLN7a2', + '12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY', + '12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu' + ] + + const result = uut._removeDuplicatePeers(inArray) + // console.log('result: ', result) + + assert.equal(result.length, inArray.length) + }) + + it('should remove duplicates from an array', () => { + const inArray = [ + '12D3KooWPpBXhhAeoCZCGuQ3KR4xwHzzvtP57f6zLmo8P7ZFBJFE', + '12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA', + '12D3KooWGsgHWyDLKuV4ZSfRJfsxQJj77rxx3i8Px3qXKHsLN7a2', + '12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY', + '12D3KooWDtj9cfj1SKuLbDNKvKRKSsGN8qivq9M8CYpLPDpcD5pu', + '12D3KooWPpBXhhAeoCZCGuQ3KR4xwHzzvtP57f6zLmo8P7ZFBJFE', + '12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA' + ] + + const result = uut._removeDuplicatePeers(inArray) + // console.log('result: ', result) + + assert.equal(result.length, inArray.length - 2) + }) + }) })