├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── config └── bootstrap-circuit-relays.js ├── dev-docs ├── README.md ├── ipfs-coord-specifications.md └── temp.md ├── examples ├── debugging │ ├── dev-external.mjs │ └── orbit-data.mjs ├── start-external.js ├── start-max-config.js └── start-minimal.js ├── index.js ├── lib ├── adapters │ ├── bch-adapter.js │ ├── encryption-adapter.js │ ├── gist.js │ ├── index.js │ ├── ipfs-adapter.js │ ├── logs-adapter.js │ ├── pubsub-adapter │ │ ├── README.md │ │ ├── about-adapter.js │ │ ├── index.js │ │ └── messaging.js │ └── schema.js ├── controllers │ ├── index.js │ └── timer-controller.js ├── entities │ └── this-node-entity.js ├── ipfs-coord-logo.png └── use-cases │ ├── index.js │ ├── peer-use-cases.js │ ├── pubsub-use-cases.js │ ├── relay-use-cases.js │ └── this-node-use-cases.js ├── package-lock.json ├── package.json └── test ├── mocks ├── adapter-mock.js ├── circuit-relay-mocks.js ├── ipfs-mock.js ├── orbitdb-mock.js ├── peers-mock.js ├── pubsub-mocks.js ├── thisnode-mocks.js ├── use-case-mocks.js └── util-mocks.js └── unit ├── adapters ├── about.unit.adapters.js ├── bch.unit.adapter.js ├── encryption.unit.adapters.js ├── gist.unit.adapters.js ├── index.unit.adapters.js ├── ipfs.unit.adapters.js ├── logs.unit.adapter.js ├── messaging.unit.adapter.js ├── pubsub.unit.adapter.js └── schema.unit.adapter.js ├── controllers ├── index.unit.controllers.js └── timer.unit.controller.js ├── entities └── this-node.unit.entity.js ├── index.unit.js └── use-cases ├── index.unit.use-cases.js ├── peer.unit.use-cases.js ├── pubsub.unit.use-cases.js ├── relay.unit.use-cases.js └── this-node.unit.use-cases.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 8 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Created by https://www.gitignore.io/api/node,sublimetext 4 | 5 | ### Node ### 6 | # Logs 7 | #logs 8 | logs/*.json 9 | *.log 10 | npm-debug.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directory 33 | node_modules 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | 42 | ### SublimeText ### 43 | # cache files for sublime text 44 | *.tmlanguage.cache 45 | *.tmPreferences.cache 46 | *.stTheme.cache 47 | 48 | # workspace files are user-specific 49 | *.sublime-workspace 50 | 51 | # project files should be checked into the repository, unless a significant 52 | # proportion of contributors will probably not be using SublimeText 53 | *.sublime-project 54 | 55 | # sftp configuration file 56 | sftp-config.json 57 | 58 | #Documentation 59 | docs 60 | 61 | .nyc_output 62 | coverage 63 | database/ 64 | system-user-*.json 65 | .ipfsdata 66 | 67 | !README.md 68 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This is a node.js v8+ JavaScript project 2 | language: node_js 3 | node_js: 4 | - "10" 5 | 6 | # Build on Ubuntu Trusty (14.04) 7 | # https://docs.travis-ci.com/user/reference/trusty/#javascript-and-nodejs-images 8 | dist: xenial 9 | sudo: required 10 | 11 | # Use Docker 12 | services: 13 | - docker 14 | 15 | before_install: 16 | #- npm install -g mocha 17 | 18 | # https://github.com/greenkeeperio/greenkeeper-lockfile/issues/156 19 | #install: case $TRAVIS_BRANCH in greenkeeper*) npm i;; *) npm ci;; esac; 20 | install: 21 | - npm install 22 | 23 | script: "npm run test" 24 | 25 | # Send coverage data to Coveralls 26 | after_success: 27 | - npm run coverage 28 | 29 | deploy: 30 | provider: script 31 | skip_cleanup: true 32 | script: 33 | - npx semantic-release 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2022 Chris Troutner 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ipfs-coord logo](./lib/ipfs-coord-logo.png) 2 | 3 | # ipfs-coord 4 | 5 | This is a JavaScript npm library built on top of [js-ipfs](https://github.com/ipfs/js-ipfs). It provides the following high-level features: 6 | 7 | - **Subnets** - Helps IPFS nodes create an on-the-fly subnetwork, using pubsub channels. 8 | - **Peer Discovery** - Allows new peers entering the subnetwork to find the other subnetwork peers. 9 | - **E2EE** - Creates end-to-end encrypted (e2ee) communication channels between peers. 10 | - **Censorship Resistance** - Allows automatic networking between peers, even if they are behind a firewall. 11 | - **Payments** - Allows peers to easily pay one another in cryptocurrency for access to web services. 12 | 13 | This library will help IPFS peers discover one another, coordinate around a common interest, and then stay connected around that interest. It's main sub-components are: 14 | 15 | - IPFS pubsub channels for communication 16 | - OrbitDB for persistence and to prevent 'dropped messages' 17 | - Circuit Relays for censorship resistance and tunneling through firewalls 18 | - Bitcoin Cash for end-to-end encryption and payments. 19 | 20 | This library will automatically track peers, connects to them through circuit-relays, and end-to-end encrypts all communication with each node. For more details, read the [ipfs-coord specification](./dev-docs/ipfs-coord-specifications.md). 21 | 22 | Here are some use cases where IPFS node coordination is needed: 23 | 24 | - e2e encrypted chat 25 | - Circuit-relay as-a-service 26 | - Creating CoinJoin transactions 27 | - Decentralized exchange of currencies 28 | - Compute-as-a-service 29 | - Storage-as-a-service 30 | 31 | The ultimate goal for this library is to be a building block for building a replacement to the conventional REST API. APIs like REST or gRPC are incredibly valuable, but suffer from the same censorship risks as the rest of the web (location-based addressing). An IPFS-based API, in a fully distributed network like IPFS, must have sophisticated coordination in order for it to function properly. ipfs-coord is that coordination library. 32 | 33 | Here is some videos and blog posts that preceded this work: 34 | 35 | - Demo video 1: [Demo of chat.fullstack.cash](https://youtu.be/zMklhvq_NFM) 36 | - Demo video 2: [ipfs-coord Architecture](https://youtu.be/jUFY7hM1xpk) 37 | 38 | Additional content that preceded this work: 39 | 40 | - [Building Uncensorable REST APIs](https://youtu.be/VVc0VbOD4co) 41 | - [IPFS API](https://troutsblog.com/blog/ipfs-api) 42 | - [Introducing chat.fullstack.cash](https://troutsblog.com/blog/chat-fullstack-cash) 43 | - [UncensorablePublishing.com](https://uncensorablepublishing.com) 44 | - [PS004 Collaborative CoinJoin](https://github.com/Permissionless-Software-Foundation/specifications/blob/master/ps004-collaborative-coinjoin.md) 45 | 46 | A live demo of using this library to build an e2e encrypted chat app can be interacted with here: 47 | 48 | - [chat.fullstack.cash](https://chat.fullstack.cash) 49 | 50 | ## Install 51 | 52 | Install the npm library: 53 | `npm install --save ipfs-coord` 54 | 55 | This library requires a _peer_ dependency of: 56 | 57 | - [@psf/bch-js](https://www.npmjs.com/package/@psf/bch-js) 58 | - [ipfs](https://www.npmjs.com/package/ipfs) (version 0.55.4 or _lower_) 59 | 60 | ### Example in a node.js app: 61 | 62 | Here is an example of adding ipfs-coord to your own node.js app: 63 | 64 | ```javascript 65 | const IPFS = require('ipfs') 66 | const BCHJS = require('@psf/bch-js') 67 | const IpfsCoord = require('ipfs-coord') 68 | 69 | async function start() { 70 | // Create an instance of bch-js and IPFS. 71 | const bchjs = new BCHJS() 72 | const ipfs = await IPFS.create() 73 | 74 | // Pass bch-js and IPFS to ipfs-coord when instantiating it. 75 | const ipfsCoord = new IpfsCoord({ 76 | ipfs, 77 | bchjs, 78 | type: 'node.js' 79 | }) 80 | 81 | await ipfsCoord.start() 82 | console.log('IPFS and the coordination library is ready.') 83 | } 84 | start() 85 | ``` 86 | 87 | ### Example in a browser app: 88 | 89 | This example is exactly the same, except when instantiating the ipfs-coord library, you want to specify the `type` as `browser`. 90 | 91 | ```javascript 92 | import IPFS from 'ipfs' 93 | import BCHJS from '@psf/bch-js' 94 | import IpfsCoord from 'ipfs-coord' 95 | 96 | async function start() { 97 | // Create an instance of bch-js and IPFS. 98 | const bchjs = new BCHJS() 99 | const ipfs = await IPFS.create() 100 | 101 | // Pass bch-js and IPFS to ipfs-coord when instantiating it. 102 | const ipfsCoord = new IpfsCoord({ 103 | ipfs, 104 | bchjs, 105 | type: 'browser' 106 | }) 107 | 108 | await ipfsCoord.start() 109 | console.log('IPFS and the coordination library is ready.') 110 | } 111 | start() 112 | ``` 113 | 114 | ### Development Environment 115 | 116 | Setup a development environment: 117 | 118 | ``` 119 | git clone https://github.com/christroutner/ipfs-coord 120 | cd ipfs-coord 121 | npm install 122 | npm test 123 | ``` 124 | 125 | # Licence 126 | 127 | [MIT](LICENSE.md) 128 | 129 | 130 | -------------------------------------------------------------------------------- /config/bootstrap-circuit-relays.js: -------------------------------------------------------------------------------- 1 | /* 2 | This library contains known circuit relays that new IPFS nodes can use to 3 | bootstrap themselves into the network and join the pubsub network. 4 | */ 5 | /* eslint camelcase: 0 */ 6 | 7 | const BOOTSTRAP_BROWSER_CRs = [ 8 | { 9 | // Based in USA, east coast. 10 | name: 'bchd.nl', 11 | multiaddr: '/dns4/bchd.nl/tcp/443/wss/p2p/12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA', 12 | connected: false, 13 | ipfsId: '12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA', 14 | isBootstrap: true 15 | }, 16 | { 17 | name: 'ipfs-cr.fullstackslp.nl', 18 | multiaddr: '/dns4/ipfs-cr.fullstackslp.nl/tcp/443/wss/p2p/12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY', 19 | connected: false, 20 | ipfsId: '12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY', 21 | isBootstrap: true 22 | } 23 | ] 24 | 25 | const BOOTSTRAP_NODE_CRs = [ 26 | { 27 | name: 'bchd.nl', 28 | multiaddr: '/ip4/5.161.95.233/tcp/4001/p2p/12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA', 29 | connected: false, 30 | ipfsId: '12D3KooWRBhwfeP2Y9CDkFRBAZ1pmxUadH36TKuk3KtKm5XXP8mA', 31 | isBootstrap: true 32 | }, 33 | { 34 | name: 'ipfs-cr.fullstackslp.nl', 35 | multiaddr: '/ip4/78.46.129.7/tcp/4001/p2p/12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY', 36 | connected: false, 37 | ipfsId: '12D3KooWJyc54njjeZGbLew4D8u1ghrmZTTPyh3QpBF7dxtd3zGY', 38 | isBootstrap: true 39 | } 40 | ] 41 | 42 | const bootstrapCircuitRelays = { 43 | browser: BOOTSTRAP_BROWSER_CRs, 44 | node: BOOTSTRAP_NODE_CRs 45 | } 46 | 47 | module.exports = bootstrapCircuitRelays 48 | -------------------------------------------------------------------------------- /dev-docs/README.md: -------------------------------------------------------------------------------- 1 | # Developer Documentation 2 | 3 | This directory contains documentation for developers who work with the ipfs-coord library. 4 | 5 | - [Specification](./ipfs-coord-specifications.md) 6 | -------------------------------------------------------------------------------- /dev-docs/ipfs-coord-specifications.md: -------------------------------------------------------------------------------- 1 | # ipfs-coord Specifications 2 | 3 | ## Overview 4 | 5 | ipfs-coord is a shortening of the word 'coordination'. It is a JavaScript npm library that helps applications using [js-ipfs](https://github.com/ipfs/js-ipfs) coordinate with other peers running related applications. 6 | 7 | This document contains a high-level, human-readable specification for the four major architectural areas of the ipfs-coord library: 8 | 9 | - Entities 10 | - Use Cases 11 | - Controllers (inputs) 12 | - Adapters (outputs) 13 | 14 | This reflects the [Clean Architecture](https://troutsblog.com/blog/clean-architecture) design pattern. 15 | 16 | After the ipfs-coord library is instantiated, it will have properties `useCases`, `controllers`, and `adapters` that have methods corresponding to the descriptions in this document. Apps can exercise the features of the ipfs-coord library through this object-oriented structure. 17 | 18 | ## Configuration 19 | 20 | When instantiating the ipfs-coord library, the following configuration inputs can be passed to its constructor via an object with the properties indicated below. Be sure to check out the [examples directory](../examples) for examples on how to instantiate the library with different configurations. 21 | 22 | - `ipfs`: (required) An instance of [js-ipfs](https://www.npmjs.com/package/ipfs). IPFS must be instantiated outside of ipfs-coord and passed into it when instantiating the ipfs-coord library. 23 | - `bchjs`: (required) An instance of [bch-js](https://www.npmjs.com/package/@psf/bch-js). bch-js must be instantiated outside of ipfs-coord and passed into it when instantiating the ipfs-coord library. 24 | - `type`: (required) A string with the value of 'browser' or 'node.js', to indicate what type of app is instantiating the library. This will determine the types of Circuit Relays the library can connect to. 25 | - `statusLog`: A function for handling status output strings on the status of ipfs-coord. This defaults to `console.log` if not specified. 26 | - `privateLog`: A function for handling private messages passed to this node from peer nodes. This defaults to `console.log` if not specified. 27 | 28 | ## Entities 29 | 30 | Entities make up the core business concepts. If these entities change, they fundamentally change the entire app. 31 | 32 | ### thisNode 33 | 34 | `thisNode` is the IPFS node consuming the ipfs-coord library. The thisNode Entity creates a representation the 'self' and maintains the state of the IPFS node, BCH wallet, peers, relays, and pubsub channels that the node is tracking. 35 | 36 | ## Use Cases 37 | 38 | Use cases are verbs or actions that is done _to_ an Entity or _between_ Entities. 39 | 40 | ### thisNode 41 | 42 | The `this-node-use-cases.js` library contains the following Use Cases: 43 | 44 | - `createSelf()` - initializes the `thisNode` Entity. It takes the following actions: 45 | 46 | - It retrieves basic information about the IPFS node like the ID and multiaddresses. 47 | - It creates a BCH wallet and generates addresses for payments and a public key used for end-to-end encryption (e2ee). 48 | - It creates an OrbitDB used to receive private e2ee messages. 49 | - It initializes the Schema library for passing standardized messages. 50 | 51 | - `addSubnetPeer()` - This is an event handler that is triggered when an 'announcement object' is recieved on the general coordination pubsub channel. That object is passed to `addSubnetPeer()` to be processed. It will analyze the announcement object and add the peer to the array of peers tracked by the thisNode Entity. If the peer is already known, its data will be updated. 52 | 53 | - `refreshPeerConnections()` - is periodically called by the Timer Controller. It checks to see if thisNode is still connected to all the subnet peers. It will refresh the connection if they have been disconnected. Circuit Relays are used to connect to other subnet peers, and each known circuit relay will be cycled through until a connection can be established between thisNode and the subnet peer. 54 | 55 | ### Relays 56 | 57 | The `relay-use-cases.js` library controls the interactions between thisNode and the Circuit Relays that it knows about. 58 | 59 | - `initializeRelays()` - The ipfs-coord library comes with a pre-programmed list of Circuit Relay nodes. This list is stored in `config/bootstrap-circuit-relays.js`. The `initializeRelays()` method is called once at startup to connect to these relays. This is what 'bootstraps' thisNode to the IPFS sub-network and allows it to find subnetwork peers. After that initial bootstrap connection, thisNode will automatically learn about and connect to other peers and circuit relays. 60 | 61 | - `connectToCRs()` - This method is called periodically by the Timer Controller. It checks the connection between thisNode and each Circuit Relay node. If thisNode has lost its connection, the connection is restored. 62 | 63 | ### Pubsub 64 | 65 | The `pubsub-use-cases.js` has a single method: 66 | 67 | - `initializePubsub()` is called at startup to connect the node to the general coordination pubsub channel. This is the channel where other apps running the ipfs-coord library announce themselves to other peers in the subnet. 68 | 69 | ## Controllers 70 | 71 | Controllers are inputs to the system. When a controller is activated, it causes the system to react in some way. 72 | 73 | ### Timers 74 | 75 | The controllers listed in this section are activated periodically by a timer. They do routine maintenance. 76 | 77 | - `startTimers()` is called at startup. It initializes the other timer controllers. 78 | 79 | - `manageCircuitRelays()` calls the `connectToCRs()` Use Case to refresh connections to other circuit relays. 80 | 81 | - `manageAnnouncement()` periodically announces the presence of thisNode Entity on the general coordination pubsub channel. It allows other subnet peers to find the node. 82 | 83 | - `managePeers()` checks the list of known subnet peers tracked by thisNode Entity. It will restore the connection to each peer if they get disconnected. 84 | 85 | ## Adapters 86 | 87 | Adapters are the 'outputs' of the system. They are the interfaces that this library manipulates in order to maintain the state of the Entities. Adapters ensure that the business logic doesn't need to know any specific information about the outputs. 88 | 89 | ### bch-adapter.js 90 | 91 | [bch-js](https://github.com/Permissionless-Software-Foundation/bch-js) is the Bitcoin Cash (BCH) library used to handle payments and end-to-end encryption in peer communication. When the IPFS node is started, it generates a BCH address to receive payments in BCH, and an SLP address to receive payments in SLP tokens. The same private key used to generate these addresses is used to decrypt incoming pubsub messages, and the public key is passed on to other peers so that they can encrypt messages they want to send thisNode. 92 | 93 | ### ipfs-adapter.js 94 | 95 | This library is designed primarily to control an IPFS node. However, it does not load IPFS directly. It expects the developer to inject an instance of js-ipfs when instantiating this library. 96 | 97 | ### orbitdb-adapter.js 98 | 99 | [OrbitDB](https://orbitdb.org/) is a database that runs on top of IPFS. It's used in this library to prevent 'dropped calls'. As nodes are constantly adjusting their network connections, they can sometimes miss pubsub messages. Other peers in the subnet maintain short-lived logs of the encrypted messages directed at the peers they are connected to. This allows them to pass the message on when the peer reconnects to them, preventing 'dropped calls'. These logs are abandoned after an hour and a new log is created, to prevent them from growing too large. 100 | 101 | ### encryption-adapter.js 102 | 103 | The encryption adapter is responsible for encryption and decryption of pubsub messages. It uses the same Eliptic Curve cryptography used by the Bitcoin protocol. The same private key that is used to generate the BCH address assigned to thisNode is the same private key used to decrypt incoming messages. 104 | 105 | Other subnet peers that thisNode tracks will pass on their public key. All messages sent between nodes is encrypted with the receivers public key. Any unencrypted messages are ignored. 106 | 107 | ### pubsub-adapter.js 108 | 109 | The pubsub adapter can publish a message to a pubsub channel, and route incoming messages to an appropriate handler. There are (public) coordination channels that many peers subscribe to, and messages are published unencrypted. 110 | 111 | Private messages between peers are _not_ controlled by this library. Those messages are published to OrbitDB and use pubsub messages indirectly, rather than being directly published to a pubsub channel by this library. 112 | 113 | ### schema.js 114 | 115 | The schema library contain formatted JSON objects that are used to generate a standardized messages for communication between peers. The schema.js library is instantiated at startup and appended to the thisNode Entity. 116 | -------------------------------------------------------------------------------- /dev-docs/temp.md: -------------------------------------------------------------------------------- 1 | This markdown is for holding temporary piece of text for editing. 2 | 3 | ## Use cases 4 | 5 | ### Peers 6 | 7 | Peers are other IPFS nodes that the application wants to keep track of. These are peers that make up the subnet. 8 | 9 | - `peerList` - An array of IPFS IDs (strings), identifying each peer this node knows about. 10 | - `peerData` - An object with root properties that match the peer IPFS ID. Each root property represents a peer and contains the data about that peer. 11 | 12 | ### Pubsub Channels 13 | 14 | [Pubsub Channels](https://blog.ipfs.io/29-js-ipfs-pubsub/) are the way that IPFS nodes form a subnet. The members of the subnet are all subscribed to the same pubsub channel. 15 | 16 | ### Relays 17 | 18 | Some nodes using ipfs-coord can elect to become [Circuit Relays](https://docs.libp2p.io/concepts/circuit-relay/). Circuit Relays are critical for keeping the network censorship resistant. They allow nodes that otherwise would not be able to communicate with one another, do so. They assist in punching through network firewalls that would otherwise block communication. They allow the subnet to route around damage and dynamically adjust as nodes enter and leave the subnet. 19 | 20 | ipfs-coord will start by connecting to a small set of pre-configured Relays. As it discovers new peers in the subnetwork that have their `isCircuitRelay` flag set, it will expand its connections to as many Relays as it can find. 21 | 22 | - `relayList` - An array of IPFS IDs (strings), identifying each Relay this node knows about. 23 | - `relayData` - An object with root properties that match the relay IPFS ID. Each root property represents a relay and contains the data about that relay. 24 | 25 | ### Services 26 | 27 | Some nodes are 'service providers' while other nodes are 'service consumers'. These [Decentralized Service Providers (Video)](https://youtu.be/m_33rRXEats) can provide traditional 'back end' web services while leveraging the censorship resistance and automatic networking of this library. Apps consuming these services can use this library can track different service providers, to dynamically load the services it needs to function. 28 | 29 | - `serviceList` - An array of IPFS IDs (strings), identifying each Service this node knows about. 30 | - `serviceData` - An object with root properties that match the Services IPFS ID. Each root property represents a Service and contains the data about that Service. 31 | 32 | Properties maintained for each Service: 33 | 34 | - jwtFee - If the Service requires the purchase of a JWT token to access it, this is the cost of purchasing one. This is an array of objects, with each object describing different fee options. For example, there may be one fee for paying in BCH. There may be another for paying in SLP tokens. 35 | - jwtDuration - The duration a JWT token lasts before expiring. A number representing hours. 36 | - `protocol` - The service protocol that this Service offers. 37 | - `version` - The protocol version that this Service offers. 38 | - `description` - A brief description of the Service. 39 | - `documentation` - An optional link to any API documentation needed to consume the Service. 40 | 41 | ## Controllers 42 | 43 | ### Pubsub 44 | 45 | New messages arriving for a pubsub channel will trigger an event that will cause this library to process and route the message to the appropriate handler. A few classes of message: 46 | 47 | - Announcement - Announcement messages from other peers will be routed to the Peer Entity. If its a new peer, it will be added to the list of known peers. 48 | 49 | - Services - A peer advertising specific services will be passed on to the Services Entity. 50 | - Relays - A peer advertising Circuit Relay capability will be passed on to the Relays Entity. 51 | 52 | - Private Messages - Each peer has a pubsub channel that is the same name as its IPFS ID. Messages arriving on this channel are expected to be e2e encrypted with the peers public key. Any unencrypted messages are ignored. 53 | 54 | ### Timers 55 | 56 | - Announce Self - This controller announces the presence of the IPFS node by publishing a message to the general coordination pubsub channel. If it's a service provider, it will also announce itself in the service-specific coordination channel. 57 | 58 | - Update Peers - This controller reviews the data about each known peer, and prunes away any peers that have not announced themselves for a period of time. It attempts to renew connections to each peer in the list. 59 | 60 | - Update Relays - This controller reviews the data about each known Circuit Relay. It attempts to renew the connection to each known Circuit Relay. 61 | 62 | - Update Services - This controller reviews the data about each known Service Provider. It prunes any services that it has not been able to connect to over a period of time. 63 | -------------------------------------------------------------------------------- /examples/debugging/dev-external.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | Forked from start-external.js. This is used with a local copy of ipfs-http-client 3 | source code that can be hacked. 4 | */ 5 | 6 | // const IPFS = require('@chris.troutner/ipfs') 7 | // const IPFS = require('ipfs-http-client') 8 | import { create } from 'ipfs-http-client' 9 | // const IPFS = require('/home/trout/work/personal/js-ipfs/packages/ipfs') 10 | import http from 'http' 11 | import IpfsCoord from '../index.js' 12 | import BCHJS from '@psf/bch-js' 13 | 14 | // Configuration for external IPFS node. 15 | const ipfsOptions = { 16 | protocol: 'http', 17 | host: 'localhost', 18 | port: 5001, 19 | agent: http.Agent({ keepAlive: true, maxSockets: 100 }) 20 | } 21 | 22 | async function start () { 23 | // Create an instance of bch-js and IPFS. 24 | const bchjs = new BCHJS() 25 | const ipfs = await create(ipfsOptions) 26 | 27 | // Pass bch-js and IPFS to ipfs-coord when instantiating it. 28 | const ipfsCoord = new IpfsCoord({ 29 | ipfs, 30 | bchjs, 31 | type: 'node.js', 32 | // type: 'browser' 33 | nodeType: 'external', 34 | debugLevel: 2 35 | }) 36 | 37 | await ipfsCoord.start() 38 | console.log('IPFS and the coordination library is ready.') 39 | 40 | // Used for debugging 41 | // setTimeout(async function () { 42 | // const thisNode = ipfsCoord.thisNode 43 | // console.log('\nthisNode: ', thisNode) 44 | // // console.log( 45 | // // `thisNode.peerData: ${JSON.stringify(thisNode.peerData, null, 2)}` 46 | // // ) 47 | // }, 20000) 48 | } 49 | start() 50 | -------------------------------------------------------------------------------- /examples/debugging/orbit-data.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | This script is used to analyize OrbitDB data that is detected by running the 3 | dev-external.mjs script with DEBUG=* LOG=debug 4 | 5 | That will print out OrbitDB hashs that start with the letter 'z'. This script 6 | can be used to decode the content of those hashes. 7 | */ 8 | 9 | // const dbname = "12D3KooWLwTfBrb5hxi6pPWqxFA38Zp1MAzParsvBVLUZLgnSQTN"; 10 | 11 | // const IPFS = require('@chris.troutner/ipfs') 12 | // const IPFS = require('ipfs-http-client') 13 | import { create } from 'ipfs-http-client' 14 | // const IPFS = require('/home/trout/work/personal/js-ipfs/packages/ipfs') 15 | import http from 'http' 16 | // import IpfsCoord from '../index.js' 17 | // import BCHJS from "@psf/bch-js"; 18 | import OrbitDB from 'orbit-db' 19 | 20 | const directory = 21 | './.ipfsdata/orbitdb-ipfs-coord/12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f' 22 | const dbAddr = 23 | '/orbitdb/zdpuAnxfwpb9FzxV9RrmdxqSCCZwHrJ63Denf7Zn9SRmLZN6d/12D3KooWQx2NBJdnTt1wwhcPu34hHP2uSB1PgxNWCiwvGv7PkC4122022505' 24 | // const entry = "zdpuAz7TEfEqFwNGzMPsBmK2uiz4mE7vqHMuKohqLcQ6Q3q3D"; 25 | 26 | // Configuration for external IPFS node. 27 | const ipfsOptions = { 28 | protocol: 'http', 29 | host: 'localhost', 30 | port: 5001, 31 | agent: http.Agent({ keepAlive: true, maxSockets: 100 }) 32 | } 33 | 34 | async function start () { 35 | // Create an instance of bch-js and IPFS. 36 | // const bchjs = new BCHJS(); 37 | const ipfs = await create(ipfsOptions) 38 | 39 | const idData = await ipfs.id() 40 | const id = idData.id 41 | console.log(id) 42 | 43 | const orbitdb = await OrbitDB.createInstance(ipfs, { directory }) 44 | console.log('orbitdb: ', orbitdb) 45 | 46 | const db = await orbitdb.eventlog(dbAddr) 47 | 48 | // const db = await orbitdb.eventlog(dbname); 49 | // 50 | // await db.load(); 51 | // 52 | // // console.log(db); 53 | // 54 | const all = db 55 | .iterator({ limit: -1 }) 56 | .collect() 57 | .map(e => e.payload.value) 58 | console.log(all) 59 | 60 | const oneEntry = await db.get('') 61 | console.log('oneEntry: ', oneEntry) 62 | } 63 | start() 64 | -------------------------------------------------------------------------------- /examples/start-external.js: -------------------------------------------------------------------------------- 1 | /* 2 | This example shows how to start an IPFS node, using ipfs-coord, with the 3 | IPFS node running as an external node that can be controlled via the 4 | ipfs-http-client library. 5 | 6 | Designed to use IPFS running in this Docker container: 7 | https://github.com/christroutner/docker-ipfs 8 | */ 9 | 10 | // const IPFS = require('@chris.troutner/ipfs') 11 | const IPFS = require('ipfs-http-client') 12 | // const IPFS = require('/home/trout/work/personal/js-ipfs/packages/ipfs') 13 | const BCHJS = require('@psf/bch-js') 14 | // const IpfsCoord = require('ipfs-coord') 15 | const IpfsCoord = require('../index') 16 | const http = require('http') 17 | 18 | // Configuration for external IPFS node. 19 | const ipfsOptions = { 20 | protocol: 'http', 21 | host: 'localhost', 22 | port: 5001, 23 | agent: http.Agent({ keepAlive: true, maxSockets: 100 }) 24 | } 25 | 26 | async function start () { 27 | // Create an instance of bch-js and IPFS. 28 | const bchjs = new BCHJS() 29 | const ipfs = await IPFS.create(ipfsOptions) 30 | 31 | // Pass bch-js and IPFS to ipfs-coord when instantiating it. 32 | const ipfsCoord = new IpfsCoord({ 33 | ipfs, 34 | bchjs, 35 | type: 'node.js', 36 | // type: 'browser' 37 | nodeType: 'external', 38 | debugLevel: 2 39 | }) 40 | 41 | await ipfsCoord.start() 42 | console.log('IPFS and the coordination library is ready.') 43 | 44 | // Used for debugging 45 | // setTimeout(async function () { 46 | // const thisNode = ipfsCoord.thisNode 47 | // console.log('\nthisNode: ', thisNode) 48 | // // console.log( 49 | // // `thisNode.peerData: ${JSON.stringify(thisNode.peerData, null, 2)}` 50 | // // ) 51 | // }, 20000) 52 | } 53 | start() 54 | -------------------------------------------------------------------------------- /examples/start-max-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | This example shows how to start an IPFS node, using ipfs-coord, with the 3 | maximum amount of configuration options. 4 | */ 5 | 6 | // const IPFS = require('@chris.troutner/ipfs') 7 | const IPFS = require('ipfs') 8 | const BCHJS = require('@psf/bch-js') 9 | // const IpfsCoord = require('ipfs-coord') 10 | const IpfsCoord = require('../index') 11 | 12 | const announceJson = { 13 | '@context': 'https://schema.org/', 14 | '@type': 'WebAPI', 15 | name: 'ipfs-coord-example', 16 | version: '1.0.0', 17 | protocol: 'none', 18 | description: 'This is an example IPFS node using ipfs-coord.', 19 | documentation: 20 | 'https://github.com/Permissionless-Software-Foundation/ipfs-coord', 21 | provider: { 22 | '@type': 'Organization', 23 | name: 'Permissionless Software Foundation', 24 | url: 'https://PSFoundation.cash' 25 | } 26 | } 27 | 28 | // Ipfs Options 29 | const ipfsOptions = { 30 | repo: './.ipfsdata', 31 | start: true, 32 | config: { 33 | relay: { 34 | enabled: true, // enable circuit relay dialer and listener 35 | hop: { 36 | enabled: false // enable circuit relay HOP (make this node a relay) 37 | } 38 | }, 39 | pubsub: true, // enable pubsub 40 | Swarm: { 41 | ConnMgr: { 42 | HighWater: 30, 43 | LowWater: 10 44 | } 45 | }, 46 | preload: { 47 | enabled: false 48 | }, 49 | offline: true 50 | } 51 | } 52 | 53 | async function start () { 54 | // Create an instance of bch-js and IPFS. 55 | const bchjs = new BCHJS() 56 | const ipfs = await IPFS.create(ipfsOptions) 57 | 58 | // Pass bch-js and IPFS to ipfs-coord when instantiating it. 59 | const ipfsCoord = new IpfsCoord({ 60 | ipfs, 61 | bchjs, 62 | type: 'node.js', 63 | privateLog: console.log, // Replace with your own log. 64 | isCircuitRelay: false, // Set to true to provide Circuit Relay functionality. 65 | apiInfo: 'Link to API documentation if applicable', 66 | announceJsonLd: announceJson, 67 | debugLevel: 2 68 | }) 69 | 70 | await ipfsCoord.start() 71 | console.log('IPFS and the coordination library is ready.') 72 | } 73 | start() 74 | -------------------------------------------------------------------------------- /examples/start-minimal.js: -------------------------------------------------------------------------------- 1 | /* 2 | This example shows how to start an IPFS node, using ipfs-coord, with the 3 | minimum amount of configuration options. 4 | */ 5 | 6 | // const IPFS = require('@chris.troutner/ipfs') 7 | const IPFS = require('ipfs') 8 | // const IPFS = require('/home/trout/work/personal/js-ipfs/packages/ipfs') 9 | const BCHJS = require('@psf/bch-js') 10 | // const IpfsCoord = require('ipfs-coord') 11 | const IpfsCoord = require('../index') 12 | 13 | const ipfsOptions = { 14 | repo: './.ipfsdata' 15 | } 16 | 17 | async function start () { 18 | // Create an instance of bch-js and IPFS. 19 | const bchjs = new BCHJS() 20 | const ipfs = await IPFS.create(ipfsOptions) 21 | 22 | // Pass bch-js and IPFS to ipfs-coord when instantiating it. 23 | const ipfsCoord = new IpfsCoord({ 24 | ipfs, 25 | bchjs, 26 | debugLevel: 1, 27 | type: 'node.js' 28 | // type: 'browser' 29 | }) 30 | 31 | await ipfsCoord.start() 32 | console.log('IPFS and the coordination library is ready.') 33 | 34 | // Used for debugging 35 | // setTimeout(async function () { 36 | // const thisNode = ipfsCoord.thisNode 37 | // console.log('\nthisNode: ', thisNode) 38 | // // console.log( 39 | // // `thisNode.peerData: ${JSON.stringify(thisNode.peerData, null, 2)}` 40 | // // ) 41 | // }, 20000) 42 | } 43 | start() 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | A JS npm library for helping IPFS peers coordinate, find a common interest, 3 | and stay connected around that interest. 4 | 5 | See the specification document in the dev-docs directory. 6 | */ 7 | 8 | // local libraries 9 | const Adapters = require('./lib/adapters') 10 | const Controllers = require('./lib/controllers') 11 | const UseCases = require('./lib/use-cases') 12 | 13 | class IpfsCoord { 14 | constructor (localConfig = {}) { 15 | // Input Validation 16 | if (!localConfig.ipfs) { 17 | throw new Error( 18 | 'An instance of IPFS must be passed when instantiating the ipfs-coord library.' 19 | ) 20 | } 21 | if (!localConfig.bchjs) { 22 | throw new Error( 23 | 'An instance of @psf/bch-js must be passed when instantiating the ipfs-coord library.' 24 | ) 25 | } 26 | this.type = localConfig.type 27 | if (!this.type) { 28 | throw new Error( 29 | 'The type of IPFS node (browser or node.js) must be specified.' 30 | ) 31 | } 32 | 33 | // 'embedded' node type used as default, will use embedded js-ipfs. 34 | // Alternative is 'external' which will use ipfs-http-client to control an 35 | // external IPFS node. 36 | this.nodeType = localConfig.nodeType 37 | if (!this.nodeType) { 38 | console.log('No node type specified. Assuming embedded js-ipfs.') 39 | this.nodeType = 'embedded' 40 | } 41 | 42 | // Retrieve and/or set the debug level. 43 | // 0 = no debug information. 44 | // 1 = status logs 45 | // 2 = verbose errors about peer connections 46 | this.debugLevel = parseInt(localConfig.debugLevel) 47 | if (!this.debugLevel) this.debugLevel = 0 48 | localConfig.debugLevel = this.debugLevel 49 | console.log(`ipfs-coord debug level: ${localConfig.debugLevel}`) 50 | 51 | // localConfiguration of an optional 'status' log handler for log reports. If none 52 | // is specified, defaults to console.log. 53 | if (localConfig.statusLog) { 54 | this.statusLog = localConfig.statusLog 55 | } else { 56 | this.statusLog = console.log 57 | } 58 | // If the statusLog handler wasn't specified, then define it. 59 | localConfig.statusLog = this.statusLog 60 | 61 | // localConfiguration of an optional 'private' log handler for recieving e2e 62 | // encrypted message. If none is specified, default to console.log. 63 | if (localConfig.privateLog) { 64 | this.privateLog = localConfig.privateLog 65 | } else { 66 | this.privateLog = console.log 67 | } 68 | // If the privateLog handler wasn't specified, then define it. 69 | localConfig.privateLog = this.privateLog 70 | 71 | // Load the adapter libraries. 72 | this.adapters = new Adapters(localConfig) 73 | localConfig.adapters = this.adapters 74 | 75 | // Load the controller libraries 76 | this.controllers = new Controllers(localConfig) 77 | localConfig.controllers = this.controllers 78 | 79 | // Load the Use Cases 80 | this.useCases = new UseCases(localConfig) 81 | } 82 | 83 | // Returns a Promise that resolves to true once the IPFS node has been 84 | // initialized and has had a chance to connect to circuit relays and 85 | // coordination pubsub channels. 86 | async start () { 87 | // Wait for the IPFS to finish initializing, then retrieve information 88 | // about the node like it's ID and multiaddrs. 89 | await this.adapters.ipfs.start() 90 | 91 | // Create an instance of the 'self' which represents this IPFS node, BCH 92 | // wallet, and other things that make up this ipfs-coord powered IPFS node. 93 | this.thisNode = await this.useCases.thisNode.createSelf({ type: this.type }) 94 | // console.log('thisNode: ', this.thisNode) 95 | 96 | // Subscribe to Pubsub Channels 97 | await this.useCases.pubsub.initializePubsub(this.thisNode) 98 | 99 | // Start timer-based controllers. 100 | await this.controllers.timer.startTimers(this.thisNode, this.useCases) 101 | 102 | // Kick-off initial connection to Circuit Relays and Peers. 103 | // Note: Deliberatly *not* using await here, so that it doesn't block startup 104 | // of ipfs-service-provider. 105 | this._initializeConnections() 106 | 107 | return true 108 | } 109 | 110 | // This function kicks off initial connections to the circuit relays and then 111 | // peers. This function is intended to be called once at startup. This handles 112 | // the initial connections, but the timer-controller manages the connections 113 | // after this initial function. 114 | async _initializeConnections () { 115 | try { 116 | // Connect to Circuit Relays 117 | await this.useCases.relays.initializeRelays(this.thisNode) 118 | console.log('Initial connections to Circuit Relays complete.') 119 | 120 | // Load list of Circuit Relays from GitHub Gist. 121 | await this.useCases.relays.getCRGist(this.thisNode) 122 | console.log('Finished connecting to Circuit Relays in GitHub Gist.') 123 | 124 | await this.useCases.thisNode.refreshPeerConnections() 125 | console.log('Initial connections to subnet Peers complete.') 126 | 127 | return true 128 | } catch (err) { 129 | console.error('Error in _initializeConnections(): ', err) 130 | // throw err 131 | 132 | // Do not throw errors as it will prevent the node from starting. 133 | return false 134 | } 135 | } 136 | } 137 | 138 | module.exports = IpfsCoord 139 | -------------------------------------------------------------------------------- /lib/adapters/bch-adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | A top level library for Bitcoin Cash (BCH) handling. This library will 3 | encapsulate a lot of support libraries and contain BCH-specific methods. 4 | */ 5 | 6 | class BchAdapter { 7 | constructor (bchConfig) { 8 | // Input Validation 9 | if (!bchConfig.bchjs) { 10 | throw new Error( 11 | 'An instance of bch-js must be passed when instantiating the BCH adapter library.' 12 | ) 13 | } 14 | 15 | this.bchjs = bchConfig.bchjs 16 | this.mnemonic = bchConfig.mnemonic 17 | } 18 | 19 | // Generate a BCH key pair and address. Returns an object with this information, 20 | // which can be used for payments and e2e encryption. 21 | // 12-word mnemonic string input is optional. Will generate a new wallet and 22 | // keypair if not provided. Will use the first address on the SLIP44 23 | // derivation path of 245 if a 12 word mnemonic is provided. 24 | async generateBchId () { 25 | try { 26 | // Generate a 12-word mnemonic, if one isn't provided. 27 | if (!this.mnemonic) { 28 | this.mnemonic = this.bchjs.Mnemonic.generate( 29 | 128, 30 | this.bchjs.Mnemonic.wordLists().english 31 | ) 32 | } 33 | // console.log(`mnemonic: ${mnemonic}`) 34 | 35 | // root seed buffer 36 | const rootSeed = await this.bchjs.Mnemonic.toSeed(this.mnemonic) 37 | 38 | const masterHDNode = this.bchjs.HDNode.fromSeed(rootSeed) 39 | 40 | const childNode = masterHDNode.derivePath("m/44'/245'/0'/0/0") 41 | 42 | const outObj = {} 43 | 44 | // Generate the cash and SLP addresses. 45 | outObj.cashAddress = this.bchjs.HDNode.toCashAddress(childNode) 46 | outObj.slpAddress = this.bchjs.SLP.Address.toSLPAddress( 47 | outObj.cashAddress 48 | ) 49 | 50 | // No need to export the private key? 51 | // outObj.WIF = this.bchjs.HDNode.toWIF(childNode) 52 | 53 | // Export the public key as a hex string. This will be used for e2e 54 | // encryption. 55 | const pubKey = this.bchjs.HDNode.toPublicKey(childNode) 56 | outObj.publicKey = pubKey.toString('hex') 57 | 58 | return outObj 59 | } catch (err) { 60 | console.error('Error in bch-lib.js/generateBchId()') 61 | throw err 62 | } 63 | } 64 | 65 | async generatePrivateKey () { 66 | try { 67 | // Generate a 12-word mnemonic, if one isn't provided. 68 | if (!this.mnemonic) { 69 | this.mnemonic = this.bchjs.Mnemonic.generate( 70 | 128, 71 | this.bchjs.Mnemonic.wordLists().english 72 | ) 73 | } 74 | // console.log(`mnemonic: ${mnemonic}`) 75 | 76 | // root seed buffer 77 | const rootSeed = await this.bchjs.Mnemonic.toSeed(this.mnemonic) 78 | 79 | const masterHDNode = this.bchjs.HDNode.fromSeed(rootSeed) 80 | 81 | const childNode = masterHDNode.derivePath("m/44'/245'/0'/0/0") 82 | 83 | const privKey = this.bchjs.HDNode.toWIF(childNode) 84 | 85 | return privKey 86 | } catch (err) { 87 | console.error('Error in bch-lib.js/generatePrivateKey()') 88 | throw err 89 | } 90 | } 91 | } 92 | 93 | module.exports = BchAdapter 94 | -------------------------------------------------------------------------------- /lib/adapters/encryption-adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | A library for end-to-end encryption (e2ee). This will largely be a wrapper 3 | for existing encryption libraries. 4 | */ 5 | 6 | const BchEncrypt = require('bch-encrypt-lib/index.js') 7 | 8 | class EncryptionAdapter { 9 | constructor (localConfig = {}) { 10 | // Dependency injection 11 | this.bch = localConfig.bch 12 | if (!this.bch) { 13 | throw new Error( 14 | 'Must pass in an instance of bch Adapter when instantiating the encryption Adapter library.' 15 | ) 16 | } 17 | this.bchjs = this.bch.bchjs // Copy of bch-js 18 | 19 | this.bchEncrypt = new BchEncrypt({ bchjs: this.bchjs }) 20 | // this.ipfs = encryptConfig.ipfs 21 | // this.orbitdb = encryptConfig.orbitdb 22 | } 23 | 24 | // Decrypt incoming messages on the pubsub channel for this node. 25 | async decryptMsg (msg) { 26 | try { 27 | // console.log('decryptMsg msgObj: ', msg) 28 | 29 | const privKey = await this.bch.generatePrivateKey() 30 | // console.log(`privKey: ${privKey}`) 31 | 32 | const decryptedHexStr = await this.bchEncrypt.encryption.decryptFile( 33 | privKey, 34 | msg 35 | ) 36 | // console.log(`decryptedHexStr ${decryptedHexStr}`) 37 | 38 | const decryptedBuff = Buffer.from(decryptedHexStr, 'hex') 39 | 40 | const decryptedStr = decryptedBuff.toString() 41 | // console.log(`decryptedStr: ${decryptedStr}`) 42 | 43 | return decryptedStr 44 | } catch (err) { 45 | // Exit quietly if the issue is a 'Bad MAC'. This seems to be a startup 46 | // issue. 47 | if (err.message.includes('Bad MAC')) { 48 | throw new Error( 49 | 'Bad MAC. Could not decrypt message. Peer may have stale encryption data for this node.' 50 | ) 51 | // return '' 52 | } 53 | 54 | console.error('Error in decryptMsg()') 55 | throw err 56 | } 57 | } 58 | 59 | // Returns an encrypted hexidecimal string derived from an input message 60 | // (string), encrypted with the public key of a peer. 61 | async encryptMsg (peer, msg) { 62 | try { 63 | // console.log('peer: ', peer) 64 | 65 | const pubKey = peer.data.encryptPubKey 66 | // console.log('msg to encrypt: ', msg) 67 | // console.log(`Encrypting with public key: ${pubKey}`) 68 | 69 | const msgBuf = Buffer.from(msg, 'utf8').toString('hex') 70 | 71 | const encryptedHexStr = await this.bchEncrypt.encryption.encryptFile( 72 | pubKey, 73 | msgBuf 74 | ) 75 | 76 | return encryptedHexStr 77 | } catch (err) { 78 | console.error('Error in encryption.js/encryptMsg()') 79 | throw err 80 | } 81 | } 82 | 83 | // Send an e2e encrypted message to a peer. 84 | // async sendEncryptedMsg (peer, msg) { 85 | // try { 86 | // // console.log('sendEncryptedMsg peer: ', peer) 87 | // // console.log('sendEncryptedMsg msg: ', msg) 88 | // 89 | // // const channel = peer.ipfsId.toString() 90 | // const pubKey = peer.encryptPubKey 91 | // // const orbitdbId = peer.orbitdb 92 | // 93 | // const msgBuf = Buffer.from(msg, 'utf8').toString('hex') 94 | // // console.log(`msgBuf: ${msgBuf}`) 95 | // 96 | // const encryptedHexStr = await this.bchEncrypt.encryption.encryptFile( 97 | // pubKey, 98 | // msgBuf 99 | // ) 100 | // console.log(`encryptedHexStr: ${encryptedHexStr}`) 101 | // 102 | // // const msgBuf2 = Buffer.from(encryptedHexStr, 'hex') 103 | // 104 | // // Publish the message to the pubsub channel. 105 | // // TODO: This will be deprecated in the future in favor of publishing to 106 | // // the peers OrbitDB. 107 | // // await this.ipfs.pubsub.publish(channel, msgBuf2) 108 | // 109 | // // if (orbitdbId) { 110 | // // console.log( 111 | // // `Ready to send encrypted message to peer ${channel} on orbitdb ID ${orbitdbId}` 112 | // // ) 113 | // // await this.orbitdb.sendToDb(channel, encryptedHexStr, orbitdbId) 114 | // // } 115 | // } catch (err) { 116 | // console.error('Error in sendEncryptedMsg()') 117 | // throw err 118 | // } 119 | // } 120 | } 121 | 122 | module.exports = EncryptionAdapter 123 | -------------------------------------------------------------------------------- /lib/adapters/gist.js: -------------------------------------------------------------------------------- 1 | /* 2 | This adapter library is concerned with interfacing with the GitHub Gist API. 3 | It's used to download a more easily maintained list of Circuit Relays 4 | operated by members of the PSF. 5 | */ 6 | 7 | const axios = require('axios') 8 | 9 | class Gist { 10 | constructor (localConfig = {}) { 11 | this.axios = axios 12 | } 13 | 14 | // Retrieve a JSON file from a GitHub Gist 15 | async getCRList () { 16 | try { 17 | // Public CRs 18 | // https://gist.github.com/christroutner/048ea1a4b635a055c6bb63d48c373806 19 | const gistUrl = 20 | 'https://api.github.com/gists/048ea1a4b635a055c6bb63d48c373806' 21 | 22 | // Retrieve the gist from github.com. 23 | const result = await this.axios.get(gistUrl) 24 | // console.log('result.data: ', result.data) 25 | 26 | // Get the current content of the gist. 27 | const content = result.data.files['psf-public-circuit-relays.json'].content 28 | // console.log('content: ', content) 29 | 30 | // Parse the JSON string into an Object. 31 | const object = JSON.parse(content) 32 | // console.log('object: ', object) 33 | 34 | return object 35 | } catch (err) { 36 | console.error('Error in getCRList()') 37 | throw err 38 | } 39 | } 40 | } 41 | 42 | module.exports = Gist 43 | -------------------------------------------------------------------------------- /lib/adapters/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a top-level library for the Adapters. This file loads all other 3 | adapter libraries. 4 | */ 5 | 6 | // Public npm libraries 7 | // const EventEmitter = require('events') 8 | // EventEmitter.defaultMaxListeners = 200 9 | 10 | // Local libraries 11 | const BchAdapter = require('./bch-adapter') 12 | const IpfsAdapter = require('./ipfs-adapter') 13 | const PubsubAdapter = require('./pubsub-adapter') 14 | const EncryptionAdapter = require('./encryption-adapter') 15 | const LogsAdapter = require('./logs-adapter') 16 | const Gist = require('./gist') 17 | 18 | class Adapters { 19 | constructor (localConfig = {}) { 20 | // Dependency injection 21 | if (!localConfig.ipfs) { 22 | throw new Error( 23 | 'An instance of IPFS must be passed when instantiating the Adapters library.' 24 | ) 25 | } 26 | if (!localConfig.bchjs) { 27 | throw new Error( 28 | 'An instance of @psf/bch-js must be passed when instantiating the Adapters library.' 29 | ) 30 | } 31 | 32 | // Input Validation 33 | if (!localConfig.type) { 34 | throw new Error( 35 | 'The type of IPFS node (browser or node.js) must be specified.' 36 | ) 37 | } 38 | 39 | // BEGIN: Encapsulate dependencies 40 | 41 | // Create an event emitter for use by adapters. 42 | // this.eventEmitter = new EventEmitter() 43 | // this.eventEmitter.setMaxListeners(50) 44 | // localConfig.eventEmitter = this.eventEmitter 45 | 46 | this.log = new LogsAdapter(localConfig) 47 | // Pass the log adapter to all other adapters. 48 | localConfig.log = this.log 49 | this.bch = new BchAdapter(localConfig) 50 | // Some adapter libraries depend on other adapter libraries. Pass them 51 | // in the localConfig object. 52 | localConfig.bch = this.bch 53 | this.ipfs = new IpfsAdapter(localConfig) 54 | localConfig.ipfs = this.ipfs 55 | this.encryption = new EncryptionAdapter(localConfig) 56 | // this.about = new AboutAdapter(localConfig) 57 | localConfig.encryption = this.encryption 58 | this.pubsub = new PubsubAdapter(localConfig) 59 | // this.orbit = new OrbitDBAdapter(localConfig) 60 | this.gist = new Gist(localConfig) 61 | 62 | // END: Encapsulate dependencies 63 | } 64 | } 65 | 66 | module.exports = Adapters 67 | -------------------------------------------------------------------------------- /lib/adapters/ipfs-adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Adapter library for IPFS, so the rest of the business logic doesn't need to 3 | know specifics about the IPFS API. 4 | */ 5 | 6 | // The amount of time to wait to connect to a peer, in milliseconds. 7 | // Increasing the time makes the network slower but more resilient to latency. 8 | // Decreasing the time makes the network faster, but more smaller and more fragile. 9 | const CONNECTION_TIMEOUT = 10000 10 | 11 | const bootstapNodes = require('../../config/bootstrap-circuit-relays') 12 | 13 | class IpfsAdapter { 14 | constructor (localConfig = {}) { 15 | // Input Validation 16 | this.ipfs = localConfig.ipfs 17 | if (!this.ipfs) { 18 | throw new Error( 19 | 'An instance of IPFS must be passed when instantiating the IPFS adapter library.' 20 | ) 21 | } 22 | this.log = localConfig.log 23 | if (!this.log) { 24 | throw new Error( 25 | 'A status log handler must be specified when instantiating IPFS adapter library.' 26 | ) 27 | } 28 | 29 | // 'embedded' node type used as default, will use embedded js-ipfs. 30 | // Alternative is 'external' which will use ipfs-http-client to control an 31 | // external IPFS node. 32 | this.nodeType = localConfig.nodeType 33 | if (!this.nodeType) { 34 | // console.log('No node type specified. Assuming embedded js-ipfs.') 35 | this.nodeType = 'embedded' 36 | } 37 | 38 | // Port Settings. Defaults are overwritten if specified in the localConfig. 39 | this.tcpPort = 4001 40 | if (localConfig.tcpPort) this.tcpPort = localConfig.tcpPort 41 | this.wsPort = 4003 42 | if (localConfig.wsPort) this.wsPort = localConfig.wsPort 43 | 44 | // Placeholders that will be filled in after the node finishes initializing. 45 | this.ipfsPeerId = '' 46 | this.ipfsMultiaddrs = '' 47 | } 48 | 49 | // Start the IPFS node if it hasn't already been started. 50 | // Update the state of this adapter with the IPFS node information. 51 | async start () { 52 | try { 53 | // Wait until the IPFS creation Promise has resolved, and the node is 54 | // fully instantiated. 55 | this.ipfs = await this.ipfs 56 | 57 | // Get ID information about this IPFS node. 58 | const id2 = await this.ipfs.id() 59 | // this.state.ipfsPeerId = id2.id 60 | this.ipfsPeerId = id2.id 61 | 62 | // Get multiaddrs that can be used to connect to this node. 63 | const addrs = id2.addresses.map(elem => elem.toString()) 64 | // this.state.ipfsMultiaddrs = addrs 65 | this.ipfsMultiaddrs = addrs 66 | 67 | // Remove bootstrap nodes, as we have our own bootstrap nodes, and the 68 | // the default ones can spam our nodes with a ton of bandwidth. 69 | await this.ipfs.config.set('Bootstrap', [ 70 | bootstapNodes.node[0].multiaddr, 71 | bootstapNodes.node[1].multiaddr 72 | ]) 73 | 74 | // Settings specific to embedded js-ipfs. 75 | if (this.nodeType === 'embedded') { 76 | // Also remove default Delegates, as they are the same as the default 77 | // Bootstrap nodes. 78 | await this.ipfs.config.set('Addresses.Delegates', []) 79 | } 80 | 81 | // Settings specific to external go-ipfs node. 82 | if (this.nodeType === 'external') { 83 | // Enable RelayClient 84 | // https://github.com/ipfs/go-ipfs/blob/master/docs/config.md#swarmrelayclient 85 | // https://github.com/ipfs/go-ipfs/releases/tag/v0.11.0 86 | await this.ipfs.config.set('Swarm.RelayClient.Enabled', true) 87 | 88 | // Enable hole punching for better p2p interaction. 89 | // https://github.com/ipfs/go-ipfs/blob/master/docs/config.md#swarmenableholepunching 90 | await this.ipfs.config.set('Swarm.EnableHolePunching', true) 91 | 92 | // Enable websocket connections 93 | await this.ipfs.config.set('Addresses.Swarm', [ 94 | `/ip4/0.0.0.0/tcp/${this.tcpPort}`, 95 | `/ip6/::/tcp/${this.tcpPort}`, 96 | `/ip4/0.0.0.0/udp/${this.tcpPort}/quic`, 97 | `/ip6/::/udp/${this.tcpPort}/quic`, 98 | `/ip4/0.0.0.0/tcp/${this.wsPort}`, // Websockets 99 | `/ip6/::/tcp/${this.wsPort}` 100 | ]) 101 | 102 | // Disable scanning of IP ranges. This is largely driven by Hetzner 103 | await this.ipfs.config.set('Swarm.AddrFilters', [ 104 | '/ip4/10.0.0.0/ipcidr/8', 105 | '/ip4/100.0.0.0/ipcidr/8', 106 | '/ip4/169.254.0.0/ipcidr/16', 107 | '/ip4/172.16.0.0/ipcidr/12', 108 | '/ip4/192.0.0.0/ipcidr/24', 109 | '/ip4/192.0.2.0/ipcidr/24', 110 | '/ip4/192.168.0.0/ipcidr/16', 111 | '/ip4/198.18.0.0/ipcidr/15', 112 | '/ip4/198.51.100.0/ipcidr/24', 113 | '/ip4/203.0.113.0/ipcidr/24', 114 | '/ip4/240.0.0.0/ipcidr/4', 115 | '/ip6/100::/ipcidr/64', 116 | '/ip6/2001:2::/ipcidr/48', 117 | '/ip6/2001:db8::/ipcidr/32', 118 | '/ip6/fc00::/ipcidr/7', 119 | '/ip6/fe80::/ipcidr/10' 120 | ]) 121 | 122 | // go-ipfs v0.10.0 123 | // await this.ipfs.config.set('Swarm.EnableRelayHop', true) 124 | // await this.ipfs.config.set('Swarm.EnableAutoRelay', true) 125 | 126 | // Disable peer discovery 127 | // await this.ipfs.config.set('Routing.Type', 'none') 128 | } 129 | 130 | // Disable preloading 131 | await this.ipfs.config.set('preload.enabled', false) 132 | 133 | // Reduce the default number of peers thisNode connects to at one time. 134 | await this.ipfs.config.set('Swarm.ConnMgr', { 135 | LowWater: 10, 136 | HighWater: 30, 137 | GracePeriod: '2s' 138 | }) 139 | 140 | // Reduce the storage size, as this node should not be retaining much data. 141 | await this.ipfs.config.set('Datastore.StorageMax', '2GB') 142 | } catch (err) { 143 | console.error('Error in ipfs-adapter.js/start()') 144 | throw err 145 | } 146 | } 147 | 148 | // Attempts to connect to an IPFS peer, given its IPFS multiaddr. 149 | // Returns true if the connection succeeded. Otherwise returns false. 150 | async connectToPeer (ipfsAddr) { 151 | try { 152 | // TODO: Throw error if ipfs ID is passed, instead of a multiaddr. 153 | 154 | await this.ipfs.swarm.connect(ipfsAddr, { timeout: CONNECTION_TIMEOUT }) 155 | 156 | // if (this.debugLevel) { 157 | // this.statusLog( 158 | // `status: Successfully connected to peer node ${ipfsAddr}` 159 | // ) 160 | // } 161 | this.log.statusLog(1, `Successfully connected to peer node ${ipfsAddr}`) 162 | 163 | return true 164 | } catch (err) { 165 | /* exit quietly */ 166 | // console.warn( 167 | // `Error trying to connect to peer node ${ipfsId}: ${err.message}` 168 | // ) 169 | 170 | // if (this.debugLevel === 1) { 171 | // this.statusLog( 172 | // `status: Error trying to connect to peer node ${ipfsAddr}` 173 | // ) 174 | // } else if (this.debugLevel === 2) { 175 | // this.statusLog( 176 | // `status: Error trying to connect to peer node ${ipfsAddr}: `, 177 | // err 178 | // ) 179 | // } 180 | this.log.statusLog(2, `Error trying to connect to peer node ${ipfsAddr}`) 181 | // console.log(`Error trying to connect to peer node ${ipfsAddr}: `, err) 182 | 183 | // this.log.statusLog( 184 | // 3, 185 | // `Error trying to connect to peer node ${ipfsAddr}: `, 186 | // err 187 | // ) 188 | 189 | return false 190 | } 191 | } 192 | 193 | // Disconnect from a peer. 194 | async disconnectFromPeer (ipfsId) { 195 | try { 196 | // TODO: If given a multiaddr, extract the IPFS ID. 197 | 198 | // Get the list of peers that we're connected to. 199 | const connectedPeers = await this.getPeers() 200 | // console.log('connectedPeers: ', connectedPeers) 201 | 202 | // See if we're connected to the given IPFS ID 203 | const connectedPeer = connectedPeers.filter(x => x.peer === ipfsId) 204 | 205 | // If we're not connected, exit. 206 | if (!connectedPeer.length) { 207 | // console.log(`debug: Not connected to ${ipfsId}`) 208 | return true 209 | } 210 | 211 | // If connected, disconnect from the peer. 212 | await this.ipfs.swarm.disconnect(connectedPeer[0].addr, { 213 | timeout: CONNECTION_TIMEOUT 214 | }) 215 | 216 | return true 217 | } catch (err) { 218 | // exit quietly 219 | return false 220 | } 221 | } 222 | 223 | async disconnectFromMultiaddr (multiaddr) { 224 | try { 225 | await this.ipfs.swarm.disconnect(multiaddr, { 226 | timeout: CONNECTION_TIMEOUT 227 | }) 228 | 229 | return true 230 | } catch (err) { 231 | return false 232 | } 233 | } 234 | 235 | // Get a list of all the IPFS peers This Node is connected to. 236 | async getPeers () { 237 | try { 238 | // Get connected peers 239 | const connectedPeers = await this.ipfs.swarm.peers({ 240 | direction: true, 241 | streams: true, 242 | verbose: true, 243 | latency: true 244 | }) 245 | 246 | return connectedPeers 247 | } catch (err) { 248 | console.error('Error in ipfs-adapter.js/getPeers()') 249 | throw err 250 | } 251 | } 252 | } 253 | 254 | module.exports = IpfsAdapter 255 | -------------------------------------------------------------------------------- /lib/adapters/logs-adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Controls the verbosity of the status log 3 | 4 | Default verbosity is zero, which is the least verbose messages. 5 | As the value increases, the amount of messages also increases. 6 | 7 | 0 - Normal logs 8 | 1 - More information on connections and error connections. 9 | 2 - Verbose Error messages 10 | 3 - Error messages about connections. 11 | */ 12 | 13 | const util = require('util') 14 | 15 | class LogsAdapter { 16 | constructor (localConfig = {}) { 17 | // Default to debugLevel 0 if not specified. 18 | this.debugLevel = localConfig.debugLevel 19 | if (!this.debugLevel) this.debugLevel = 0 20 | 21 | this.logHandler = localConfig.statusLog 22 | if (!this.logHandler) { 23 | throw new Error( 24 | 'statusLog must be specified when instantiating Logs adapter library.' 25 | ) 26 | } 27 | } 28 | 29 | // Print out the data, if the log level is less than or equal to the debug 30 | // level set when instantiating the library. 31 | statusLog (level, str, object) { 32 | if (level <= this.debugLevel) { 33 | if (object === undefined) { 34 | this.logHandler('status: ' + str) 35 | } else { 36 | this.logHandler('status: ' + str + ' ' + util.inspect(object)) 37 | } 38 | } 39 | } 40 | } 41 | 42 | module.exports = LogsAdapter 43 | -------------------------------------------------------------------------------- /lib/adapters/pubsub-adapter/README.md: -------------------------------------------------------------------------------- 1 | # Pubsub Adapter 2 | 3 | The [index.js](./index.js) file contains the primary PubSub adapter, which controls the pubsub channel connections between peers. [messaging.js](./messaging.js) controls the message handling for communicating over pubsub channels. 4 | 5 | Because IPFS nodes are constantly changing their network connections, it's frequently observed that pubsub messages between peers get 'lost'. Version 6 and older used [orbit-db](https://www.npmjs.com/package/orbit-db) to prevent these lost messages. However, it turned out to not be a very scalable solution. Orbit-db is way too CPU heavy to work as a speedy form of inter-node communication. 6 | 7 | Version 7 introduced the [messaging.js](./messaging.js) library. The primary problem this library solves is the 'lost message' issue. The IPFS pubsub channels handle the bulk of the low-level messaging. 8 | 9 | ## Messaging Protocol 10 | 11 | To handle 'lost messages', two peers engage in a message-acknowledge scheme: 12 | 13 | Happy Path: 14 | 15 | - Node 1 publishes a message to the pubsub channel for Node 2. 16 | - Node 2 publishes an ACK (acknowledge) message to the pubsub channel for Node 1. 17 | - If both messages are received, the transaction is complete. 18 | 19 | Each message is wrapped in a data object with the following properties: 20 | 21 | - timestamp 22 | - UUID 23 | - sender (IPFS ID) 24 | - receiver (IPFS ID) 25 | - payload 26 | 27 | If Node 1 does not receive an ACK message after 5 seconds, it will publish the message to the pubsub channel again. It will do this every 5 seconds, until either an ACK message is received or a retry threshold is met (3 tries). 28 | 29 | Node 2 will attempt to send an ACK message any time it receives a message. 30 | 31 | It's important to note that two pubsub channels are used. Node 1 sends data on Node 2's pubsub channel. Node 2 responds by publishing data on Node 1's pubsub channel. 32 | 33 | ## Libraries 34 | 35 | To understand the relationships between the messaing (messaging.js) and pubsub (index.js) libraries, and comparison to the [OSI model](https://www.imperva.com/learn/application-security/osi-model/) can be made. In the OSI model, from top to bottom, there is: 36 | 37 | - A presentation layer (6). 38 | - A session layer (5) 39 | - A transport layer (4) 40 | 41 | Analogously: 42 | - The pubsub library ([index.js](./index.js)) is like the presentation layer (6). 43 | - The messaging library ([messaging.js](./messaging.js)) is like the session layer (5). 44 | - The IPFS pubsub API is like the transport layer (4). 45 | 46 | The point is that there are two different communication protocols happening at the same time: 47 | - The underlying messaging protocol described here *does not* care about the content of the messages. It's just trying to ensure messages are passes reliably. 48 | - The message handling in the pubsub library *does* care about the content of the messages. 49 | 50 | 51 | ## Example 52 | 53 | Here is an example of an RPC command wrapped inside of a message envelope. The payload contains the encrypted RPC command. Notice the message itself has a UUID (universally unique identifier). 54 | 55 | ```javascript 56 | { 57 | "timestamp": "2022-03-03T18:03:01.217Z", 58 | "uuid": "e0f08e3f-6e23-43ea-8fa9-39b828fd4fdc", 59 | "sender": "12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa", 60 | "receiver": "12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f", 61 | "payload": "0453769b22a27adbb288bb2a05c19605a1fb033d4aeca92dfbf59f5c61732f89e6668a9a645dea3fe82e1bda20b8b11e713586b2c39d33f00dcf8fddac401263c320f06136a494e965f34193c3c6bb670a146c2ec06cdb5fd564b11a25c8715d574b8e1fd57f90e697c1edf21eb27ec0c431ce83293a4611e9593a53490c220019867208a1e241f23851c91646521b2e47" 62 | } 63 | ``` 64 | 65 | Once the payload is decrypted and parsed, it looks like this. Notice that the RPC command has it's own UUID. 66 | 67 | ```javascript 68 | { 69 | jsonrpc: '2.0', 70 | id: 'bb788af4-b7d1-4650-a7ae-bfd03b7f5140', 71 | method: 'bch', 72 | params: { 73 | endpoint: 'utxos', 74 | address: 'bitcoincash:qr2u4f2dmva6yvf3npkd5lquryp09qk7gs5vxl423h' 75 | } 76 | } 77 | ``` 78 | 79 | ### Workflow 80 | 81 | Using the above RPC command as an example, here is the messaging workflow between Node 1 and Node 2: 82 | 83 | - Node 1 sends the RPC command to Node 2 84 | - Node 2 immediately responds with an ACK message 85 | - Node 2 processes the RPC command and sends the response to Node 1 86 | - Node 1 immediately reponds with an ACK message 87 | -------------------------------------------------------------------------------- /lib/adapters/pubsub-adapter/about-adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | This adapter is designed to poll the /about JSON RPC endpoint for IPFS Service 3 | Providers that leverage ipfs-coord. This allows other consumers of the 4 | ipfs-coord library to measure the latency between themselves and potential 5 | Circuit Relays. 6 | 7 | Using these metrics, IPFS nodes can prioritize the Circuit Relays with the 8 | lowest latency. At scale, this allows the entire IPFS subnet to adapt to 9 | changing network conditions. 10 | */ 11 | 12 | class AboutAdapter { 13 | constructor (localConfig = {}) { 14 | // Time to wait for a reponse from the RPC. 15 | this.waitPeriod = 10000 16 | 17 | // Used to pass asynchronous data when pubsub data is received. 18 | this.incomingData = false 19 | } 20 | 21 | // Query the /about JSON RPC endpoint for a subnet peer. 22 | // This function will return true on success or false on failure or timeout 23 | // of 10 seconds. 24 | // This function is used to measure the time for a response from the peer. 25 | async queryAbout (ipfsId, thisNode) { 26 | try { 27 | // console.log(`Querying Relay ${ipfsId}`) 28 | // console.log('thisNode: ', thisNode) 29 | 30 | // Generate the JSON RPC command 31 | const idNum = Math.floor(Math.random() * 10000).toString() 32 | const id = `metrics${idNum}` 33 | const cmdStr = `{"jsonrpc":"2.0","id":"${id}","method":"about"}` 34 | // console.log(`cmdStr: ${cmdStr}`) 35 | 36 | // console.log(`Sending JSON RPC /about command to ${ipfsId}`) 37 | const result = await this.sendRPC(ipfsId, cmdStr, id, thisNode) 38 | // console.log('sendRPC result: ', result) 39 | 40 | return result 41 | } catch (err) { 42 | console.error('Error in queryAbout()') 43 | 44 | // Do not throw an error. 45 | return false 46 | } 47 | } 48 | 49 | // This function is called by pubsub.captureMetrics() when a response is 50 | // recieved to an /about request. The data is used by sendRPC(). 51 | relayMetricsReceived (inData) { 52 | this.incomingData = inData 53 | } 54 | 55 | // Send the RPC command to the service, wait a period of time for a response. 56 | // Timeout if a response is not recieved. 57 | async sendRPC (ipfsId, cmdStr, id, thisNode) { 58 | try { 59 | let retData = this.incomingData 60 | 61 | // Send the RPC command to the server/service. 62 | await thisNode.useCases.peer.sendPrivateMessage(ipfsId, cmdStr, thisNode) 63 | 64 | // Used for calculating the timeout. 65 | const start = new Date() 66 | let now = start 67 | let timeDiff = 0 68 | 69 | // Wait for the response from the server. Exit once the response is 70 | // recieved, or a timeout occurs. 71 | do { 72 | await thisNode.useCases.peer.adapters.bch.bchjs.Util.sleep(250) 73 | 74 | now = new Date() 75 | 76 | timeDiff = now.getTime() - start.getTime() 77 | // console.log('timeDiff: ', timeDiff) 78 | 79 | retData = this.incomingData 80 | 81 | // If data came in on the event emitter, analize it. 82 | if (retData) { 83 | // console.log('retData: ', retData) 84 | 85 | const jsonData = JSON.parse(retData) 86 | const respId = jsonData.id 87 | 88 | // If the JSON RPC ID matches, then it's the response thisNode was 89 | // waiting for. 90 | if (respId === id) { 91 | // responseRecieved = true 92 | // this.eventEmitter.removeListener('relayMetrics', cb) 93 | 94 | retData = false 95 | 96 | return true 97 | } 98 | } 99 | 100 | // console.log('retData: ', retData) 101 | } while ( 102 | // Exit once the RPC data comes back, or if a period of time passes. 103 | timeDiff < this.waitPeriod 104 | ) 105 | 106 | // this.eventEmitter.removeListener('relayMetrics', cb) 107 | return false 108 | } catch (err) { 109 | console.error('Error in sendRPC') 110 | // this.eventEmitter.removeListener('relayMetrics', cb) 111 | throw err 112 | } 113 | } 114 | } 115 | 116 | module.exports = AboutAdapter 117 | -------------------------------------------------------------------------------- /lib/adapters/pubsub-adapter/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Adapter library for working with pubsub channels and messages 3 | */ 4 | 5 | // Local libraries 6 | const Messaging = require('./messaging') 7 | const AboutAdapter = require('./about-adapter') 8 | 9 | class PubsubAdapter { 10 | constructor (localConfig = {}) { 11 | // Dependency Injection 12 | this.ipfs = localConfig.ipfs 13 | if (!this.ipfs) { 14 | throw new Error( 15 | 'Instance of IPFS adapter required when instantiating Pubsub Adapter.' 16 | ) 17 | } 18 | this.log = localConfig.log 19 | if (!this.log) { 20 | throw new Error( 21 | 'A status log handler function required when instantitating Pubsub Adapter' 22 | ) 23 | } 24 | this.encryption = localConfig.encryption 25 | if (!this.encryption) { 26 | throw new Error( 27 | 'An instance of the encryption Adapter must be passed when instantiating the Pubsub Adapter library.' 28 | ) 29 | } 30 | this.privateLog = localConfig.privateLog 31 | if (!this.privateLog) { 32 | throw new Error( 33 | 'A private log handler must be passed when instantiating the Pubsub Adapter library.' 34 | ) 35 | } 36 | 37 | // Encapsulate dependencies 38 | this.messaging = new Messaging(localConfig) 39 | this.about = new AboutAdapter(localConfig) 40 | 41 | // 'embedded' node type used as default, will use embedded js-ipfs. 42 | // Alternative is 'external' which will use ipfs-http-client to control an 43 | // external IPFS node. 44 | this.nodeType = localConfig.nodeType 45 | if (!this.nodeType) { 46 | // console.log('No node type specified. Assuming embedded js-ipfs.') 47 | this.nodeType = 'embedded' 48 | } 49 | // console.log(`PubsubAdapter contructor node type: ${this.nodeType}`) 50 | } 51 | 52 | // Subscribe to a pubsub channel. Any data received on that channel is passed 53 | // to the handler. 54 | async subscribeToPubsubChannel (chanName, handler, thisNode) { 55 | try { 56 | // console.log('thisNode: ', thisNode) 57 | const thisNodeId = thisNode.ipfsId 58 | 59 | // Normal use-case, where the pubsub channel is NOT the receiving channel 60 | // for this node. This applies to general broadcast channels like 61 | // the coordination channel that all nodes use to annouce themselves. 62 | if (chanName !== thisNodeId) { 63 | // console.log('this.ipfs: ', this.ipfs) 64 | await this.ipfs.ipfs.pubsub.subscribe(chanName, async (msg) => { 65 | await this.parsePubsubMessage(msg, handler, thisNode) 66 | }) 67 | 68 | // 69 | } else { 70 | // Subscribing to our own pubsub channel. This is the channel other nodes 71 | // will use to send RPC commands and send private messages. 72 | 73 | await this.ipfs.ipfs.pubsub.subscribe(chanName, async (msg) => { 74 | const msgObj = await this.messaging.handleIncomingData(msg, thisNode) 75 | 76 | // If msgObj is false, then ignore it. Typically indicates an already 77 | // processed message. 78 | if (msgObj) { await this.handleNewMessage(msgObj, thisNode) } 79 | }) 80 | } 81 | 82 | this.log.statusLog( 83 | 0, 84 | `status: Subscribed to pubsub channel: ${chanName}` 85 | ) 86 | 87 | return true 88 | } catch (err) { 89 | console.error('Error in subscribeToPubsubChannel()') 90 | throw err 91 | } 92 | } 93 | 94 | // After the messaging.js library does the lower-level message handling and 95 | // decryption, it passes the message on to this function, which does any 96 | // additional parsing needed, and 97 | // then routes the parsed data on to the user-specified handler. 98 | async handleNewMessage (msgObj, thisNode) { 99 | try { 100 | // console.log('handleNewMessage() will forward this data onto the handler: ', msgObj) 101 | 102 | // Check to see if this is metrics data or user-requested data. 103 | // If it the response to a metrics query, trigger the handler for that. 104 | // Dev note: This only handles the response. The JSON RPC must pass 105 | // through this function to the privateLog, to be handled by the service 106 | // being measured. 107 | const isAbout = await this.captureMetrics(msgObj.data.payload, msgObj.from, thisNode) 108 | 109 | // Pass the JSON RPC data to the private log to be handled by the app 110 | // consuming this library. 111 | if (!isAbout) { 112 | // console.log('handleNewMessage() forwarding payload on to handler.') 113 | this.privateLog(msgObj.data.payload, msgObj.from) 114 | 115 | return true 116 | } 117 | 118 | return false 119 | } catch (err) { 120 | console.error('Error in handleNewMessage()') 121 | throw err 122 | } 123 | } 124 | 125 | // Scans input data. If the data is determined to be an 'about' JSON RPC 126 | // reponse used for metrics, then the relayMetrics event is triggered and 127 | // true is returned. Otherwise, false is returned. 128 | async captureMetrics (decryptedStr, from, thisNode) { 129 | try { 130 | // console.log('decryptedStr: ', decryptedStr) 131 | // console.log('thisNode: ', thisNode) 132 | 133 | const data = JSON.parse(decryptedStr) 134 | // console.log('data: ', data) 135 | 136 | // Handle /about JSON RPC queries. 137 | if (data.id.includes('metrics') && data.method === 'about') { 138 | // Request recieved, send response. 139 | 140 | // console.log('/about JSON RPC captured. Sending back announce object.') 141 | 142 | // console.log('thisNode.schema.state.announceJsonLd: ', thisNode.schema.state.announceJsonLd) 143 | 144 | const jsonResponse = `{"jsonrpc": "2.0", "id": "${data.id}", "result": {"method": "about", "receiver": "${from}", "value": ${JSON.stringify(thisNode.schema.state)}}}` 145 | // console.log(`Responding with this JSON RPC response: ${jsonResponse}`) 146 | 147 | // Encrypt the string with the peers public key. 148 | const peerData = thisNode.peerData.filter(x => x.from === from) 149 | const payload = await this.encryption.encryptMsg( 150 | peerData[0], 151 | jsonResponse 152 | ) 153 | 154 | await this.messaging.sendMsg(from, payload, thisNode) 155 | 156 | return true 157 | 158 | // 159 | } else if (data.id.includes('metrics') && data.result && data.result.method === 'about') { 160 | // Response received. 161 | 162 | // console.log('JSON RPC /about response aquired.') 163 | 164 | // This event is handled by the about-adapter.js. It measures the 165 | // latency between peers. 166 | this.about.relayMetricsReceived(decryptedStr) 167 | 168 | return data.result.value 169 | } 170 | 171 | // This is not an /about JSON RPC query. 172 | // console.log('JSON RPC is not targeting the /about endpoint') 173 | return false 174 | } catch (err) { 175 | console.error('Error in captureMetrics: ', err) 176 | return false 177 | } 178 | } 179 | 180 | // Attempts to parse data coming in from a pubsub channel. It is assumed that 181 | // the data is a string in JSON format. If it isn't, parsing will throw an 182 | // error and the message will be ignored. 183 | async parsePubsubMessage (msg, handler, thisNode) { 184 | try { 185 | // console.log('message data: ', msg.data) 186 | // console.log('parsePubsubMessage msg: ', msg) 187 | // console.log('thisNode: ', thisNode) 188 | 189 | const thisNodeId = thisNode.ipfsId 190 | 191 | // Get data about the message. 192 | const from = msg.from 193 | const channel = msg.topicIDs[0] 194 | 195 | // Used for debugging. 196 | this.log.statusLog( 197 | 2, 198 | `Broadcast pubsub message recieved from ${from} on channel ${channel}` 199 | ) 200 | 201 | // Ignore this message if it originated from this IPFS node. 202 | if (from === thisNodeId) return true 203 | 204 | // The data on browsers comes through as a uint8array, and on node.js 205 | // implementiong it comes through as a string. Browsers need to 206 | // convert the message from a uint8array to a string. 207 | // console.log('this.nodeType: ', this.nodeType) 208 | if (thisNode.type === 'browser' || this.nodeType === 'external') { 209 | // console.log('Node type is browser') 210 | msg.data = new TextDecoder('utf-8').decode(msg.data) 211 | } 212 | 213 | // Parse the data into a JSON object. It starts as a Buffer that needs 214 | // to be converted to a string, then parsed to a JSON object. 215 | // For some reason I have to JSON parse it twice. Not sure why. 216 | const data = JSON.parse(JSON.parse(msg.data.toString())) 217 | // console.log('data: ', data) 218 | 219 | const retObj = { from, channel, data } 220 | // console.log(`new pubsub message received: ${JSON.stringify(retObj, null, 2)}`) 221 | 222 | // Hand retObj to the callback. 223 | handler(retObj) 224 | 225 | return true 226 | } catch (err) { 227 | console.error('Error in parsePubsubMessage(): ', err.message) 228 | // Do not throw an error. This is a top-level function. 229 | 230 | return false 231 | } 232 | } 233 | } 234 | 235 | module.exports = PubsubAdapter 236 | -------------------------------------------------------------------------------- /lib/adapters/pubsub-adapter/messaging.js: -------------------------------------------------------------------------------- 1 | /* 2 | A library for broadcasting messages over pubsub and managing 'lost messages'. 3 | */ 4 | 5 | // Global npm libraries 6 | const { v4: uid } = require('uuid') 7 | 8 | // Local libraries 9 | 10 | // Constants 11 | const TIME_BETWEEN_RETRIES = 5000 // time in milliseconds 12 | const RETRY_LIMIT = 3 13 | 14 | let _this 15 | 16 | class Messaging { 17 | constructor (localConfig = {}) { 18 | // Dependency Injection 19 | this.ipfs = localConfig.ipfs 20 | if (!this.ipfs) { 21 | throw new Error( 22 | 'Instance of IPFS adapter required when instantiating Messaging Adapter.' 23 | ) 24 | } 25 | this.log = localConfig.log 26 | if (!this.log) { 27 | throw new Error( 28 | 'A status log handler function required when instantitating Messaging Adapter' 29 | ) 30 | } 31 | this.encryption = localConfig.encryption 32 | if (!this.encryption) { 33 | throw new Error( 34 | 'An instance of the encryption Adapter must be passed when instantiating the Messaging Adapter library.' 35 | ) 36 | } 37 | 38 | // 'embedded' node type used as default, will use embedded js-ipfs. 39 | // Alternative is 'external' which will use ipfs-http-client to control an 40 | // external IPFS node. 41 | this.nodeType = localConfig.nodeType 42 | if (!this.nodeType) { 43 | // console.log('No node type specified. Assuming embedded js-ipfs.') 44 | this.nodeType = 'embedded' 45 | } 46 | 47 | // Encapsulate dependencies 48 | this.uid = uid 49 | 50 | // State 51 | this.msgQueue = [] 52 | // Cache to store UUIDs of processed messages. Used to prevent duplicate 53 | // processing. 54 | this.msgCache = [] 55 | this.MSG_CACHE_SIZE = 30 56 | 57 | _this = this 58 | } 59 | 60 | // Send a message to a peer 61 | // The message is added to a queue that will automatically track ACK messages 62 | // and re-send the message if an ACK message is not received. 63 | async sendMsg (receiver, payload, thisNode) { 64 | try { 65 | // console.log(`sendMsg thisNode: `, thisNode) 66 | 67 | // Generate a message object 68 | const sender = thisNode.ipfsId 69 | const inMsgObj = { 70 | sender, 71 | receiver, 72 | payload 73 | } 74 | const msgObj = this.generateMsgObj(inMsgObj) 75 | 76 | // const msgId = msgObj.uuid 77 | 78 | // Send message 79 | await this.publishToPubsubChannel(receiver, msgObj) 80 | 81 | // Add the message to the retry queue 82 | this.addMsgToQueue(msgObj) 83 | 84 | return true 85 | } catch (err) { 86 | console.error('Error in messaging.js/sendMsg()') 87 | throw err 88 | } 89 | } 90 | 91 | // Publish an ACK (acknowldge) message. Does not wait for any reply. Just fires 92 | // and returns. 93 | async sendAck (data, thisNode) { 94 | try { 95 | const ackMsgObj = await this.generateAckMsg(data, thisNode) 96 | 97 | // Send Ack message 98 | await this.publishToPubsubChannel(data.sender, ackMsgObj) 99 | 100 | return true 101 | } catch (err) { 102 | console.error('Error in sendAck()') 103 | throw err 104 | } 105 | } 106 | 107 | // A handler function that is called when a new message is recieved on the 108 | // pubsub channel for this IPFS node. It does the following: 109 | // - Decrypts the message. 110 | // - Sends an ACK message to the sender of the message. 111 | // - Returns an object containing the message and metadata. 112 | async handleIncomingData (msg, thisNode) { 113 | try { 114 | // console.log('handleIncomingData() msg: ', msg) 115 | // console.log('thisNode: ', thisNode) 116 | 117 | const thisNodeId = thisNode.ipfsId 118 | 119 | // Get data about the message. 120 | const from = msg.from 121 | const channel = msg.topicIDs[0] 122 | 123 | // Ignore this message if it originated from this IPFS node. 124 | if (from === thisNodeId) return 125 | 126 | // The data on browsers comes through as a uint8array, and on node.js 127 | // implementiong it comes through as a string when using js-ipfs. But when 128 | // using ipfs-http-client with an external go-ipfs node, it comes thorugh 129 | // as a uint8Array too. Browsers need to 130 | // convert the message from a uint8array to a string. 131 | if (thisNode.type === 'browser' || this.nodeType === 'external') { 132 | // console.log('Node type is browser') 133 | msg.data = new TextDecoder('utf-8').decode(msg.data) 134 | } 135 | // console.log('msg.data: ', msg.data) 136 | 137 | // Parse the data into a JSON object. It starts as a Buffer that needs 138 | // to be converted to a string, then parsed to a JSON object. 139 | // For some reason I have to JSON parse it twice. Not sure why. 140 | const data = JSON.parse(msg.data.toString()) 141 | // console.log('message data: ', data) 142 | 143 | // Decrypt the payload 144 | const decryptedPayload = await this.encryption.decryptMsg(data.payload) 145 | // console.log(`decrypted payload: ${decryptedPayload}`) 146 | 147 | // Filter ACK messages from other messages 148 | if (decryptedPayload.includes('"apiName":"ACK"')) { 149 | this.log.statusLog(2, `ACK message received for ${data.uuid}`) 150 | 151 | this.delMsgFromQueue(data) 152 | 153 | return false 154 | } else { 155 | this.log.statusLog( 156 | 2, 157 | `Private pubsub message recieved from ${from} on channel ${channel} with message ID ${data.uuid}` 158 | ) 159 | } 160 | 161 | // Debug logs 162 | if (decryptedPayload.includes('"id"')) { 163 | const obj = JSON.parse(decryptedPayload) 164 | 165 | if (obj.result) { 166 | this.log.statusLog(2, `Message ID ${data.uuid} contains RPC response with ID ${obj.id}`) 167 | } else { 168 | this.log.statusLog(2, `Message ID ${data.uuid} contains RPC request with ID ${obj.id}`) 169 | } 170 | } 171 | 172 | // Send an ACK message 173 | await this.sendAck(data, thisNode) 174 | 175 | // Ignore message if its already been processed. 176 | const alreadyProcessed = this._checkIfAlreadyProcessed(data.uuid) 177 | if (alreadyProcessed) { 178 | console.log(`Message ${data.uuid} already processed`) 179 | return false 180 | } 181 | 182 | // Replace the encrypted data with the decrypted data. 183 | data.payload = decryptedPayload 184 | 185 | const retObj = { from, channel, data } 186 | // console.log( 187 | // `new pubsub message received: ${JSON.stringify(retObj, null, 2)}` 188 | // ) 189 | 190 | return retObj 191 | } catch (err) { 192 | console.error('Error in handleIncomingData(): ', err) 193 | // throw err 194 | // Do not throw an error. This is a top-level function called by an Interval. 195 | return false 196 | } 197 | } 198 | 199 | // Generate a message object with UUID and timestamp. 200 | generateMsgObj (inMsgObj = {}) { 201 | try { 202 | const { sender, receiver, payload } = inMsgObj 203 | 204 | // Input validation 205 | if (!sender) { 206 | throw new Error('Sender required when calling generateMsgObj()') 207 | } 208 | if (!receiver) { 209 | throw new Error('Receiver required when calling generateMsgObj()') 210 | } 211 | if (!payload) { 212 | throw new Error('Payload required when calling generateMsgObj()') 213 | } 214 | 215 | const uuid = this.uid() 216 | 217 | const now = new Date() 218 | const timestamp = now.toISOString() 219 | 220 | const outMsgObj = { 221 | timestamp, 222 | uuid, 223 | sender, 224 | receiver, 225 | payload 226 | } 227 | 228 | return outMsgObj 229 | } catch (err) { 230 | console.log('Error in generateMsgObj()') 231 | throw err 232 | } 233 | } 234 | 235 | // Generate an ACK (acknowledge) message. 236 | async generateAckMsg (data, thisNode) { 237 | try { 238 | // console.log('thisNode: ', thisNode) 239 | 240 | // The sender of the original messages is the receiver of the ACK message. 241 | const receiver = data.sender 242 | const uuid = data.uuid 243 | 244 | const ackMsg = { 245 | apiName: 'ACK' 246 | } 247 | 248 | const peerData = thisNode.peerData.filter(x => x.from === receiver) 249 | 250 | // Encrypt the string with the peers public key. 251 | const payload = await this.encryption.encryptMsg( 252 | peerData[0], 253 | JSON.stringify(ackMsg) 254 | ) 255 | 256 | const sender = thisNode.ipfsId 257 | 258 | const inMsgObj = { 259 | sender, 260 | receiver, 261 | payload 262 | } 263 | 264 | const outMsgObj = this.generateMsgObj(inMsgObj) 265 | 266 | // Replace the message UUID with the UUID from the original message. 267 | outMsgObj.uuid = uuid 268 | 269 | this.log.statusLog( 270 | 2, 271 | `Sending ACK message for ID ${uuid}` 272 | ) 273 | 274 | return outMsgObj 275 | } catch (err) { 276 | console.error('Error in generateAckMsg()') 277 | throw err 278 | } 279 | } 280 | 281 | // Converts an input string to a Buffer and then broadcasts it to the given 282 | // pubsub room. 283 | async publishToPubsubChannel (chanName, msgObj) { 284 | try { 285 | const msgBuf = Buffer.from(JSON.stringify(msgObj)) 286 | 287 | // Publish the message to the pubsub channel. 288 | await this.ipfs.ipfs.pubsub.publish(chanName, msgBuf) 289 | 290 | // console.log('msgObj: ', msgObj) 291 | 292 | // Used for debugging. 293 | if (msgObj.uuid) { 294 | this.log.statusLog( 295 | 2, 296 | `New message published to private channel ${chanName} with ID ${msgObj.uuid}` 297 | ) 298 | } else { 299 | this.log.statusLog( 300 | 2, 301 | `New announcement message published to broadcast channel ${chanName}` 302 | ) 303 | } 304 | 305 | return true 306 | } catch (err) { 307 | console.error('Error in publishToPubsubChannel()') 308 | throw err 309 | } 310 | } 311 | 312 | // Checks the UUID to see if the message has already been processed. Returns 313 | // true if the UUID exists in the list of processed messages. 314 | _checkIfAlreadyProcessed (uuid) { 315 | // Check if the hash is in the array of already processed message. 316 | const alreadyProcessed = this.msgCache.includes(uuid) 317 | 318 | // Update the msgCache if this is a new message. 319 | if (!alreadyProcessed) { 320 | // Add the uuid to the array. 321 | this.msgCache.push(uuid) 322 | 323 | // If the array is at its max size, then remove the oldest element. 324 | if (this.msgCache.length > this.MSG_CACHE_SIZE) { 325 | this.msgCache.shift() 326 | } 327 | } 328 | 329 | return alreadyProcessed 330 | } 331 | 332 | // Stops the Interval Timer and deletes a message from the queue when an 333 | // ACK message is received. 334 | delMsgFromQueue (msgObj) { 335 | // Loop through the message queue 336 | for (let i = 0; i < this.msgQueue.length; i++) { 337 | const thisMsg = this.msgQueue[i] 338 | 339 | // Find the matching entry. 340 | if (msgObj.uuid === thisMsg.uuid) { 341 | // console.log(`thisMsg: `, thisMsg) 342 | 343 | // Stop the Interval 344 | try { 345 | clearInterval(thisMsg.intervalHandle) 346 | // console.log('Interval stopped') 347 | } catch (err) { /* exit quietly */ } 348 | 349 | // Delete the entry from the msgQueue array. 350 | this.msgQueue.splice(i, 1) 351 | break 352 | } 353 | } 354 | 355 | return true 356 | } 357 | 358 | // Adds a message object to the message queue. Starts an interval timer that 359 | // will repeat the message periodically until an ACK message is received. 360 | addMsgToQueue (msgObj) { 361 | try { 362 | msgObj.retryCnt = 1 363 | 364 | // Start interval for repeating message 365 | const intervalHandle = setInterval(function () { 366 | _this.resendMsg(msgObj) 367 | }, TIME_BETWEEN_RETRIES) 368 | 369 | // Add interval handle to message object. 370 | msgObj.intervalHandle = intervalHandle 371 | 372 | // Add message object to the queue. 373 | this.msgQueue.push(msgObj) 374 | 375 | return msgObj 376 | } catch (err) { 377 | console.error('Error in addMsgToQueue') 378 | throw err 379 | } 380 | } 381 | 382 | // Called by an Interval Timer. This function re-publishes a message to a 383 | // pubsub channel. 384 | async resendMsg (msgObj) { 385 | try { 386 | // console.log(`resendMsg() msgObj: ${JSON.stringify(msgObj, null, 2)}`) 387 | 388 | const { retryCnt, intervalHandle, receiver } = msgObj 389 | 390 | // console.log('resendMsg() retryCnt: ', retryCnt) 391 | 392 | if (retryCnt < RETRY_LIMIT) { 393 | // Increment the retry 394 | msgObj.retryCnt++ 395 | 396 | // Send message 397 | await _this.publishToPubsubChannel(receiver, msgObj) 398 | 399 | return 1 400 | } else { 401 | // Retry count exceeded. 402 | 403 | // Disable the interval handler 404 | clearInterval(intervalHandle) 405 | 406 | return 2 407 | } 408 | } catch (err) { 409 | console.error('Error in resendMsg(): ', err) 410 | // Do not throw an error. This is a top-level function called by an Interval. 411 | return 0 412 | } 413 | } 414 | } 415 | 416 | module.exports = Messaging 417 | -------------------------------------------------------------------------------- /lib/adapters/schema.js: -------------------------------------------------------------------------------- 1 | /* 2 | Schema templates for sending and recieving messages from other IPFS peers. 3 | */ 4 | 5 | class Schema { 6 | constructor (schemaConfig) { 7 | this.ipfsId = schemaConfig.ipfsId ? schemaConfig.ipfsId : null 8 | 9 | // Initialize the state with default values. 10 | this.state = { 11 | ipfsId: this.ipfsId, 12 | name: schemaConfig.name ? schemaConfig.name : this.ipfsId, 13 | type: schemaConfig.type ? schemaConfig.type : null, 14 | ipfsMultiaddrs: schemaConfig.ipfsMultiaddrs 15 | ? schemaConfig.ipfsMultiaddrs 16 | : [], 17 | isCircuitRelay: schemaConfig.isCircuitRelay 18 | ? schemaConfig.isCircuitRelay 19 | : false, 20 | circuitRelayInfo: schemaConfig.circuitRelayInfo 21 | ? schemaConfig.circuitRelayInfo 22 | : {}, 23 | cashAddress: schemaConfig.cashAddress || '', 24 | slpAddress: schemaConfig.slpAddress || '', 25 | publicKey: schemaConfig.publicKey || '', 26 | orbitdbId: schemaConfig.orbitdbId || '', 27 | 28 | // Default API Info. This should be a link to the API documenation, passed 29 | // in by the consumer of the ipfs-coord library. 30 | apiInfo: schemaConfig.apiInfo || 31 | 'You should put an IPFS hash or web URL here to your documentation.' 32 | } 33 | 34 | // Default JSON-LD Schema 35 | this.state.announceJsonLd = schemaConfig.announceJsonLd || { 36 | '@context': 'https://schema.org/', 37 | '@type': 'WebAPI', 38 | name: this.state.name, 39 | description: 'IPFS Coordination Library is used. This app has not been customized.', 40 | documentation: 'https://www.npmjs.com/package/ipfs-coord', 41 | provider: { 42 | '@type': 'Organization', 43 | name: 'Permissionless Software Foundation', 44 | url: 'https://PSFoundation.cash' 45 | } 46 | } 47 | 48 | this.state.announceJsonLd.identifier = this.ipfsId 49 | } 50 | 51 | // Returns a JSON object that represents an announement message. 52 | announcement (announceObj) { 53 | const now = new Date() 54 | 55 | // Update the orbitdb ID in the state, if it's changed. 56 | if ( 57 | announceObj && 58 | announceObj.orbitdbId && 59 | announceObj.orbitdbId !== this.state.orbitdbId 60 | ) { 61 | this.state.orbitdbId = announceObj.orbitdbId 62 | } 63 | 64 | const retObj = { 65 | apiName: 'ipfs-coord-announce', 66 | apiVersion: '1.3.2', 67 | apiInfo: this.state.apiInfo, 68 | 69 | // Add a timestamp 70 | broadcastedAt: now.toISOString(), 71 | 72 | // IPFS specific information for this node. 73 | ipfsId: this.state.ipfsId, 74 | type: this.state.type, 75 | ipfsMultiaddrs: this.state.ipfsMultiaddrs, 76 | orbitdb: this.state.orbitdbId, 77 | 78 | // The circuit relays preferred by this node. 79 | circuitRelays: [], 80 | isCircuitRelay: this.state.isCircuitRelay, 81 | circuitRelayInfo: this.state.circuitRelayInfo, 82 | 83 | // Array of objects, containing addresses for different blockchains. 84 | cryptoAddresses: [ 85 | { 86 | blockchain: 'BCH', 87 | type: 'cashAddr', 88 | address: this.state.cashAddress 89 | }, 90 | { 91 | blockchain: 'BCH', 92 | type: 'slpAddr', 93 | address: this.state.slpAddress 94 | } 95 | ], 96 | 97 | // BCH public key, used for e2e encryption. 98 | encryptPubKey: this.state.publicKey, 99 | 100 | // Schema.org and JSON Linked Data 101 | jsonLd: this.state.announceJsonLd 102 | } 103 | 104 | return retObj 105 | } 106 | 107 | // Returns a JSON object that represents a chat message. 108 | // Inputs: 109 | // - message - string text message 110 | // - handle - the desired display name for the user 111 | chat (msgObj) { 112 | const { message, handle } = msgObj 113 | 114 | const retObj = { 115 | apiName: 'chat', 116 | apiVersion: '1.3.2', 117 | apiInfo: this.state.apiInfo, 118 | 119 | // IPFS specific information for this node. 120 | ipfsId: this.state.ipfsId, 121 | type: this.state.type, 122 | ipfsMultiaddrs: this.state.ipfsMultiaddrs, 123 | 124 | // The circuit relays preferred by this node. 125 | circuitRelays: [], 126 | 127 | // Array of objects, containing addresses for different blockchains. 128 | cryptoAddresses: [], 129 | 130 | // BCH public key, used for e2e encryption. 131 | encryptPubKey: '', 132 | 133 | data: { 134 | message: message, 135 | handle: handle 136 | }, 137 | 138 | // Schema.org and JSON Linked Data 139 | jsonLd: { 140 | '@context': 'https://schema.org/', 141 | '@type': 'CommentAction', 142 | agent: { 143 | '@type': 'WebAPI', 144 | name: this.state.name, 145 | identifier: this.state.ipfsId 146 | }, 147 | resultComment: { 148 | '@type': 'Comment', 149 | text: message 150 | } 151 | } 152 | } 153 | 154 | return retObj 155 | } 156 | } 157 | 158 | module.exports = Schema 159 | -------------------------------------------------------------------------------- /lib/controllers/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a top-level Controllers library. This library loads all other 3 | controller libraries. 4 | */ 5 | 6 | const TimerControllers = require('./timer-controller') 7 | 8 | class Controllers { 9 | constructor (localConfig = {}) { 10 | // Dependency Injection 11 | this.adapters = localConfig.adapters 12 | if (!this.adapters) { 13 | throw new Error( 14 | 'Instance of adapters required when instantiating Controllers' 15 | ) 16 | } 17 | 18 | // Encapsulate dependencies 19 | this.timer = new TimerControllers(localConfig) 20 | } 21 | } 22 | 23 | module.exports = Controllers 24 | -------------------------------------------------------------------------------- /lib/controllers/timer-controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | This Controller library is concerned with timer-based functions that are 3 | kicked off periodicially. These functions maintain connections and state 4 | of the IPFS node. 5 | */ 6 | 7 | const DEFAULT_COORDINATION_ROOM = 'psf-ipfs-coordination-002' 8 | 9 | let _this 10 | 11 | class TimerControllers { 12 | constructor (localConfig = {}) { 13 | // Dependency Injection 14 | this.adapters = localConfig.adapters 15 | if (!this.adapters) { 16 | throw new Error( 17 | 'Instance of adapters required when instantiating Timer Controllers' 18 | ) 19 | } 20 | this.statusLog = localConfig.statusLog 21 | if (!this.statusLog) { 22 | throw new Error( 23 | 'Handler for status logs required when instantiating Timer Controllers' 24 | ) 25 | } 26 | 27 | this.debugLevel = localConfig.debugLevel 28 | this.config = localConfig 29 | 30 | _this = this 31 | } 32 | 33 | startTimers (thisNode, useCases) { 34 | // Periodically maintain the connection to Circuit Relays. 35 | this.circuitRelayTimerHandle = setInterval(async function () { 36 | await _this.manageCircuitRelays(thisNode, useCases) 37 | }, 60000) // One Minute 38 | 39 | // Periodically announce this nodes existance to the network. 40 | this.announceTimerHandle = setInterval(async function () { 41 | await _this.manageAnnouncement(thisNode, useCases) 42 | }, 31000) 43 | 44 | // Periodically maintain the connection to other coordination peers. 45 | this.peerTimerHandle = setInterval(async function () { 46 | await _this.managePeers(thisNode, useCases) 47 | }, 2 * 50000) 48 | 49 | // Periodically try to connect to problematic peers that advertise as 50 | // potential circuit relays. 51 | this.relaySearch = setInterval(async function () { 52 | await _this.searchForRelays(thisNode, useCases) 53 | }, 3 * 60000) 54 | 55 | // Periodically ensure we are disconnected from blacklisted peers. 56 | this.checkBlacklist = setInterval(async function () { 57 | await _this.blacklist(thisNode, useCases) 58 | }, 30000) 59 | 60 | this.bwTimerHandle = setInterval(async function () { 61 | // monitorBandwidth() throws an error in browsers. 62 | // if (_this.config.type !== 'browser') { 63 | // await _this.monitorBandwidth(thisNode, useCases) 64 | // } 65 | // console.log('_this.adapters.ipfs: ', _this.adapters.ipfs) 66 | 67 | const ipfs = _this.adapters.ipfs.ipfs 68 | 69 | const pubsubChans = await ipfs.pubsub.ls() 70 | // console.log(`subscribed pubsub channels: ${JSON.stringify(pubsubChans, null, 2)}`) 71 | _this.adapters.log.statusLog(2, `subscribed pubsub channels: ${JSON.stringify(pubsubChans, null, 2)}`) 72 | }, 32000) 73 | 74 | // Return handles to the different timers. 75 | return { 76 | circuitRelayTimerHandle: this.circuitRelayTimerHandle, 77 | announceTimerHandle: this.announceTimerHandle, 78 | peerTimerHandle: this.peerTimerHandle, 79 | relaySearch: this.relaySearch, 80 | checkBlacklist: this.checkBlacklist 81 | } 82 | } 83 | 84 | // Used mostly for testing. Ensures all timers are stopped. 85 | async stopAllTimers () { 86 | clearInterval(this.circuitRelayTimerHandle) 87 | clearInterval(this.announceTimerHandle) 88 | clearInterval(this.peerTimerHandle) 89 | clearInterval(this.relaySearch) 90 | clearInterval(this.checkBlacklist) 91 | clearInterval(this.bwTimerHandle) 92 | } 93 | 94 | // Monitor the bandwidth being consumed by IPFS peers. Disconnect from peers 95 | // that request too much bandwidth. 96 | async monitorBandwidth (thisNode, useCases) { 97 | try { 98 | const ipfs = this.adapters.ipfs.ipfs 99 | 100 | // const bw = await ipfs.stats.bw() 101 | // console.log('bw: ', bw) 102 | for await (const stats of ipfs.stats.bw()) { 103 | // console.log(stats) 104 | this.adapters.log.statusLog(2, 'Bandwidth stats: ', stats) 105 | // this.adapters.log.statusLog(2, `${JSON.stringify(stats, null, 2)}`) 106 | } 107 | 108 | const bitswap = await ipfs.stats.bitswap() 109 | this.adapters.log.statusLog(2, 'bitswap stats: ', bitswap) 110 | // this.adapters.log.statusLog(2, `${JSON.stringify(bitswap, null, 2)}`) 111 | 112 | // Monitor pubsub channels. 113 | const pubsubs = await ipfs.pubsub.ls() 114 | // this.adapters.log.statusLog(2, 'pubsubs: ', pubsubs) 115 | this.adapters.log.statusLog( 116 | 2, 117 | `pubsubs: ${JSON.stringify(pubsubs, null, 2)}` 118 | ) 119 | 120 | // Shut down node if it runs-away with connections. 121 | const MAX_PEERS = 50 122 | if (bitswap.peers.length > MAX_PEERS) { 123 | console.log( 124 | `IPFS node is misbehaving, by connecting to more than ${MAX_PEERS} peers. Shutting down for 10 seconds.` 125 | ) 126 | await ipfs.stop() 127 | await sleep(10000) 128 | await ipfs.start() 129 | } 130 | } catch (err) { 131 | console.error('Error in timer-controller.js/monitorBandwidth(): ', err) 132 | this.adapters.log.statusLog( 133 | 2, 134 | 'Error in timer-controller.jsmonitorBandwidth(): ', 135 | err 136 | ) 137 | // Note: Do not throw an error. This is a top-level function. 138 | } 139 | } 140 | 141 | // This function is intended to be called periodically by setInterval(). 142 | async manageCircuitRelays (thisNode, useCases) { 143 | try { 144 | console.log('Entering manageCircuitRelays() Controller.') 145 | 146 | // Disable the timer while processing is happening. 147 | clearInterval(this.circuitRelayTimerHandle) 148 | 149 | // Remove any duplicate entries 150 | useCases.relays.removeDuplicates(thisNode) 151 | 152 | // Maintain connections to Relays. 153 | await useCases.relays.connectToCRs(thisNode) 154 | 155 | // Update metrics on Relays. 156 | await useCases.relays.measureRelays(thisNode) 157 | 158 | const now = new Date() 159 | this.adapters.log.statusLog( 160 | 1, 161 | `Renewed connections to all circuit relays at ${now.toLocaleString()}` 162 | ) 163 | 164 | // console.log('Exiting manageCircuitRelays() Controller.') 165 | } catch (err) { 166 | console.error( 167 | 'Error in timer-controller.js/manageCircuitRelays(): ', 168 | err 169 | ) 170 | this.adapters.log.statusLog( 171 | 2, 172 | 'Error in timer-controller.js/manageCircuitRelays(): ', 173 | err 174 | ) 175 | // Note: Do not throw an error. This is a top-level function. 176 | } 177 | 178 | // Periodically maintain the connection to Circuit Relays. 179 | this.circuitRelayTimerHandle = setInterval(async function () { 180 | await _this.manageCircuitRelays(thisNode, useCases) 181 | }, 60000) // One Minute 182 | } 183 | 184 | // This function is intended to be called periodically by setInterval(). 185 | // Announce the existance of this node to the network. 186 | async manageAnnouncement (thisNode, useCases) { 187 | try { 188 | // console.log('thisNode: ', thisNode) 189 | 190 | // Get the information needed for the announcement. 191 | const announceObj = { 192 | ipfsId: thisNode.ipfsId, 193 | ipfsMultiaddrs: thisNode.ipfsMultiaddrs, 194 | type: thisNode.type, 195 | // orbitdbId: thisNode.orbit.id, 196 | 197 | // TODO: Allow node.js apps to pass a config setting to override this. 198 | isCircuitRelay: false 199 | } 200 | 201 | // Generate the announcement message. 202 | const announceMsgObj = thisNode.schema.announcement(announceObj) 203 | // console.log(`announceMsgObj: ${JSON.stringify(announceMsgObj, null, 2)}`) 204 | 205 | const announceMsgStr = JSON.stringify(announceMsgObj) 206 | 207 | // Publish the announcement to the pubsub channel. 208 | await this.adapters.pubsub.messaging.publishToPubsubChannel( 209 | DEFAULT_COORDINATION_ROOM, 210 | announceMsgStr 211 | ) 212 | 213 | if (this.debugLevel) { 214 | const now = new Date() 215 | this.statusLog( 216 | `status: Announced self on ${DEFAULT_COORDINATION_ROOM} pubsub channel at ${now.toLocaleString()}` 217 | ) 218 | } 219 | 220 | return true 221 | } catch (err) { 222 | console.error('Error in timer-controller.js/manageAnnouncement(): ', err) 223 | this.adapters.log.statusLog( 224 | 2, 225 | 'Error in timer-controller.js/manageAnnouncement(): ', 226 | err 227 | ) 228 | // Note: Do not throw an error. This is a top-level function. 229 | } 230 | } 231 | 232 | // This function is intended to be called periodically by setInterval(). 233 | // It refreshes the connection to all subnet peers thisNode is trying to track. 234 | async managePeers (thisNode, useCases) { 235 | let success = false 236 | 237 | try { 238 | // Disable the timer while processing is happening. 239 | clearInterval(this.peerTimerHandle) 240 | 241 | // this.statusLog('managePeers') 242 | await useCases.thisNode.refreshPeerConnections() 243 | 244 | // console.error('Error in timer-controller.js/manageAnnouncement(): ', err) 245 | this.adapters.log.statusLog( 246 | 1, 247 | 'Renewed connections to all subnet peers.' 248 | ) 249 | 250 | success = true 251 | } catch (err) { 252 | this.adapters.log.statusLog( 253 | 2, 254 | 'Error in timer-controller.js/managePeers(): ', 255 | err 256 | ) 257 | // Note: Do not throw an error. This is a top-level function. 258 | 259 | success = false 260 | } 261 | 262 | // Reinstate the timer interval 263 | this.peerTimerHandle = setInterval(async function () { 264 | await _this.managePeers(thisNode, useCases) 265 | }, 21000) 266 | 267 | return success 268 | } 269 | 270 | // Actively disconnect from blacklisted peers. 271 | async blacklist (thisNode, useCases) { 272 | let success = false 273 | 274 | try { 275 | // Disable the timer while processing is happening. 276 | clearInterval(this.checkBlacklist) 277 | 278 | // this.statusLog('managePeers') 279 | // await useCases.thisNode.enforceBlacklist() 280 | await useCases.thisNode.enforceWhitelist() 281 | 282 | this.adapters.log.statusLog(1, 'Finished enforcing whitelist.') 283 | 284 | success = true 285 | } catch (err) { 286 | this.adapters.log.statusLog( 287 | 2, 288 | 'Error in timer-controller.js/blacklist(): ', 289 | err 290 | ) 291 | // Note: Do not throw an error. This is a top-level function. 292 | 293 | success = false 294 | } 295 | 296 | // Reinstate the timer interval 297 | this.checkBlacklist = setInterval(async function () { 298 | await _this.blacklist(thisNode, useCases) 299 | }, 30000) 300 | 301 | return success 302 | } 303 | 304 | // This method looks for subnet peers that have the isCircuitRelay flag set, 305 | // but are not in the list of known relays. These represent potential relays 306 | // that thisNode could not connect to, but it might be able to with another 307 | // try. 308 | async searchForRelays (thisNode, useCases) { 309 | try { 310 | // console.log('Entering searchForRelays() Controller.') 311 | 312 | // Disable the timer while processing is happening. 313 | clearInterval(this.relaySearch) 314 | 315 | // Get all the known relays. 316 | const knownRelays = thisNode.relayData.map(x => x.ipfsId) 317 | // console.log('knownRelays: ', knownRelays) 318 | 319 | // Get all subnet peers that have their circuit relay flag set. 320 | let relayPeers = thisNode.peerData.filter(x => x.data.isCircuitRelay) 321 | relayPeers = relayPeers.map(x => x.from) 322 | // console.log('relayPeers: ', relayPeers) 323 | 324 | // Diff the two arrays to get relays peers that are not in the relay list. 325 | const diffRelayPeers = relayPeers.filter(x => !knownRelays.includes(x)) 326 | // console.log('diffRelayPeers: ', diffRelayPeers) 327 | 328 | // Try to connect to each potential relay. 329 | for (let i = 0; i < diffRelayPeers.length; i++) { 330 | const thisPeer = diffRelayPeers[i] 331 | await useCases.relays.addRelay(thisPeer, thisNode) 332 | } 333 | 334 | // console.log('Exiting searchForRelays() Controller.') 335 | } catch (err) { 336 | // console.error('Error in timer-controller.js/searchForRelays(): ', err) 337 | this.adapters.log.statusLog( 338 | 2, 339 | 'Error in timer-controller.js/searchForRelays(): ', 340 | err 341 | ) 342 | // Note: Do not throw an error. This is a top-level function. 343 | } 344 | 345 | // Periodically try to connect to problematic peers that advertise as 346 | // potential circuit relays. 347 | this.relaySearch = setInterval(async function () { 348 | await _this.searchForRelays(thisNode, useCases) 349 | }, 5 * 60000) 350 | } 351 | } 352 | 353 | function sleep (ms) { 354 | return new Promise(resolve => setTimeout(resolve, ms)) 355 | } 356 | 357 | module.exports = TimerControllers 358 | -------------------------------------------------------------------------------- /lib/entities/this-node-entity.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is an Entity library for creating a representation the 'self' or 3 | the current IPFS node with the adding information of a BCH wallet and 4 | any future features added to ipfs-coord. 5 | 6 | There is only one instance of this class library, as there is only one 7 | IPFS node that is the 'self'. 8 | */ 9 | 10 | class ThisNodeEntity { 11 | // The constructor checks the input data and throws an error if any of the 12 | // required data is missing. 13 | constructor (localConfig = {}) { 14 | this.ipfsId = localConfig.ipfsId 15 | if (!this.ipfsId) { 16 | throw new Error('ipfsId required when instantiating thisNode Entity') 17 | } 18 | 19 | this.ipfsMultiaddrs = localConfig.ipfsMultiaddrs 20 | if (!this.ipfsMultiaddrs) { 21 | throw new Error( 22 | 'ipfsMultiaddrs required when instantiating thisNode Entity' 23 | ) 24 | } 25 | 26 | this.bchAddr = localConfig.bchAddr 27 | if (!this.bchAddr) { 28 | throw new Error('bchAddr required when instantiating thisNode Entity') 29 | } 30 | 31 | this.slpAddr = localConfig.slpAddr 32 | if (!this.slpAddr) { 33 | throw new Error('slpAddr required when instantiating thisNode Entity') 34 | } 35 | 36 | this.publicKey = localConfig.publicKey 37 | if (!this.publicKey) { 38 | throw new Error('publicKey required when instantiating thisNode Entity') 39 | } 40 | 41 | this.type = localConfig.type 42 | if (!this.type) { 43 | throw new Error( 44 | "Node type of 'node.js' or 'browser' required when instantiating thisNode Entity" 45 | ) 46 | } 47 | 48 | this.schema = localConfig.schema 49 | 50 | // This Node will keep track of peers, relays, and services. 51 | // The 'List' array tracks the IPFS ID for that peer. 52 | // The 'Data' array holds instances of the other Entities. 53 | this.peerList = [] 54 | this.peerData = [] 55 | this.relayData = [] 56 | this.serviceList = [] 57 | this.serviceData = [] 58 | 59 | // Create a blacklist of nodes that can burden other nodes with excessive bandwidth. 60 | this.blacklistPeers = [ 61 | // '/dns4/node0.preload.ipfs.io/tcp/443/wss/p2p/QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', 62 | 'QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', 63 | // '/dns4/node1.preload.ipfs.io/tcp/443/wss/p2p/Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6', 64 | 'Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6', 65 | // '/dns4/node2.preload.ipfs.io/tcp/443/wss/p2p/QmV7gnbW5VTcJ3oyM2Xk1rdFBJ3kTkvxc87UFGsun29STS', 66 | 'QmV7gnbW5VTcJ3oyM2Xk1rdFBJ3kTkvxc87UFGsun29STS', 67 | // '/dns4/node3.preload.ipfs.io/tcp/443/wss/p2p/QmY7JB6MQXhxHvq7dBDh4HpbH29v4yE9JRadAVpndvzySN' 68 | 'QmY7JB6MQXhxHvq7dBDh4HpbH29v4yE9JRadAVpndvzySN', 69 | // '/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 70 | 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 71 | // '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 72 | 'QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 73 | // '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', 74 | 'QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', 75 | // '/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp', 76 | 'QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp', 77 | // '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', 78 | 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', 79 | // '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' 80 | 'QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' 81 | ] 82 | this.blacklistMultiaddrs = [ 83 | '/dns4/node0.preload.ipfs.io/tcp/443/wss/p2p/QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', 84 | '/dns4/node1.preload.ipfs.io/tcp/443/wss/p2p/Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6', 85 | '/dns4/node2.preload.ipfs.io/tcp/443/wss/p2p/QmV7gnbW5VTcJ3oyM2Xk1rdFBJ3kTkvxc87UFGsun29STS', 86 | '/dns4/node3.preload.ipfs.io/tcp/443/wss/p2p/QmY7JB6MQXhxHvq7dBDh4HpbH29v4yE9JRadAVpndvzySN', 87 | '/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 88 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 89 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', 90 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp', 91 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', 92 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt', 93 | '/dns4/node0.delegate.ipfs.io/tcp/443/https', 94 | '/dns4/node1.delegate.ipfs.io/tcp/443/https', 95 | '/dns4/node2.delegate.ipfs.io/tcp/443/https', 96 | '/dns4/node3.delegate.ipfs.io/tcp/443/https' 97 | ] 98 | } 99 | } 100 | 101 | module.exports = ThisNodeEntity 102 | -------------------------------------------------------------------------------- /lib/ipfs-coord-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Permissionless-Software-Foundation/ipfs-coord/0b9318f15a7ac2aa65778d2d78baf2e7b6ede066/lib/ipfs-coord-logo.png -------------------------------------------------------------------------------- /lib/use-cases/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a top-level Use Cases library. This library loads all other 3 | use case libraries, and bundles them into a single object. 4 | */ 5 | 6 | // Local libraries 7 | const ThisNodeUseCases = require('./this-node-use-cases.js') 8 | const RelayUseCases = require('./relay-use-cases') 9 | const PubsubUseCases = require('./pubsub-use-cases') 10 | const PeerUseCases = require('./peer-use-cases') 11 | 12 | class UseCases { 13 | constructor (localConfig = {}) { 14 | this.adapters = localConfig.adapters 15 | if (!this.adapters) { 16 | throw new Error( 17 | 'Must inject instance of adapters when instantiating Use Cases library.' 18 | ) 19 | } 20 | 21 | this.controllers = localConfig.controllers 22 | if (!this.controllers) { 23 | throw new Error( 24 | 'Must inject instance of controllers when instantiating Use Cases library.' 25 | ) 26 | } 27 | 28 | // Encapsulate dependencies 29 | this.thisNode = new ThisNodeUseCases(localConfig) 30 | // Other use-cases depend on the thisNode use case. 31 | localConfig.thisNodeUseCases = this.thisNode 32 | this.relays = new RelayUseCases(localConfig) 33 | this.pubsub = new PubsubUseCases(localConfig) 34 | this.peer = new PeerUseCases(localConfig) 35 | 36 | // Pass the instances of the other use cases to the ThisNode Use Cases. 37 | this.thisNode.updateUseCases(this) 38 | } 39 | } 40 | 41 | module.exports = UseCases 42 | -------------------------------------------------------------------------------- /lib/use-cases/peer-use-cases.js: -------------------------------------------------------------------------------- 1 | /* 2 | Use cases for interacting with subnet peer nodes. 3 | */ 4 | 5 | class PeerUseCases { 6 | constructor (localConfig = {}) { 7 | // Dependency Injection. 8 | this.adapters = localConfig.adapters 9 | if (!this.adapters) { 10 | throw new Error( 11 | 'Must inject instance of adapters when instantiating Peer Use Cases library.' 12 | ) 13 | } 14 | this.controllers = localConfig.controllers 15 | if (!this.controllers) { 16 | throw new Error( 17 | 'Must inject instance of controllers when instantiating Peer Use Cases library.' 18 | ) 19 | } 20 | } 21 | 22 | // Connect to a peer through available circuit relays. This ensures a short 23 | // path between peers, *before* broadcasting the OrbitDB message to them. 24 | // This method is primarily used by sendPrivateMessage() to allow for fast- 25 | // startup connection and communication with peers. 26 | async connectToPeer (peerId, thisNode) { 27 | try { 28 | // console.log(`connectToPeer() called on ${peerId}`) 29 | 30 | const relays = thisNode.relayData 31 | 32 | // Get connected peers 33 | const connectedPeers = await this.adapters.ipfs.getPeers() 34 | 35 | // Check if target peer is currently conected to the node. 36 | const connectedPeer = connectedPeers.filter( 37 | peerObj => peerObj.peer === peerId 38 | ) 39 | 40 | // If this node is already connected to the peer, then return. 41 | // We do not need to do anything. 42 | if (connectedPeer.length) { 43 | return true 44 | } 45 | 46 | // Sort the Circuit Relays by the average of the aboutLatency 47 | // array. Connect to peers through the Relays with the lowest latencies 48 | // first. 49 | const sortedRelays = thisNode.useCases.relays.sortRelays(relays) 50 | // console.log(`sortedRelays: ${JSON.stringify(sortedRelays, null, 2)}`) 51 | 52 | // Loop through each known circuit relay and attempt to connect to the 53 | // peer through a relay. 54 | for (let i = 0; i < sortedRelays.length; i++) { 55 | const thisRelay = sortedRelays[i] 56 | // console.log(`thisRelay: ${JSON.stringify(thisRelay, null, 2)}`) 57 | 58 | // Generate a multiaddr for connecting to the peer through a circuit relay. 59 | const multiaddr = `${thisRelay.multiaddr}/p2p-circuit/p2p/${peerId}` 60 | // console.log(`multiaddr: ${multiaddr}`) 61 | 62 | // Skip the relay if this node is not connected to it. 63 | if (thisRelay.connected) { 64 | // Attempt to connect to the node through a circuit relay. 65 | const connected = await this.adapters.ipfs.connectToPeer(multiaddr) 66 | 67 | // If the connection was successful, break out of the relay loop. 68 | // Otherwise try to connect through the next relay. 69 | if (connected) { 70 | // Exit once we've made a successful connection. 71 | return true 72 | } 73 | } 74 | } 75 | 76 | // Return false to indicate connection was unsuccessful. 77 | return false 78 | } catch (err) { 79 | console.error('Error in peer-use-cases.js/connectToPeer()') 80 | throw err 81 | } 82 | } 83 | 84 | // Publish a string of text to another peers OrbitDB recieve database. 85 | // orbitdbId input is optional. 86 | async sendPrivateMessage (peerId, str, thisNode) { 87 | try { 88 | // console.log('sendPrivateMessage() peerId: ', peerId) 89 | // console.log('\nsendPrivateMessage() str: ', str) 90 | 91 | // const peer = this.peers.state.peers[peerId] 92 | // console.log('thisNode.peerData: ', thisNode.peerData) 93 | const peerData = thisNode.peerData.filter(x => x.from === peerId) 94 | // console.log( 95 | // `sendPrivateMessage peerData: ${JSON.stringify(peerData, null, 2)}` 96 | // ) 97 | 98 | // Throw an error if the peer matching the peerId is not found. 99 | if (peerData.length === 0) { 100 | throw new Error(`Data for peer ${peerId} not found.`) 101 | } 102 | 103 | // Encrypt the string with the peers public key. 104 | const encryptedStr = await this.adapters.encryption.encryptMsg( 105 | peerData[0], 106 | str 107 | ) 108 | 109 | // Publish the message to the peers pubsub channel. 110 | await this.adapters.pubsub.messaging.sendMsg( 111 | peerId, 112 | encryptedStr, 113 | thisNode 114 | ) 115 | // console.log('--->Successfully published to pubsub channel<---') 116 | 117 | // const now = new Date() 118 | // 119 | // const peerDb = thisNode.orbitData.filter(x => x.ipfsId === peerId) 120 | // // console.log('peerDb: ', peerDb) 121 | // 122 | // // Throw an error if peer database was not found. 123 | // if (peerDb.length === 0) { 124 | // throw new Error(`OrbitDB for peer ${peerId} not found.`) 125 | // } 126 | // 127 | // // Connect to peer through Circuit Relay, using connectToPeer() 128 | // // Note: isConnected will resolve to false if this node can not connect to 129 | // // the peer. It will resolve to true if it successfully connected. 130 | // await this.connectToPeer(peerId, thisNode) 131 | // 132 | // const db = peerDb[0].db 133 | // 134 | // const dbObj = { 135 | // from: thisNode.ipfsId, 136 | // data: encryptedStr, 137 | // timestamp: now.toISOString() 138 | // } 139 | // // console.log(`dbObj: ${JSON.stringify(dbObj, null, 2)}`) 140 | // 141 | // // Add the encrypted message to the peers OrbitDB. 142 | // await db.add(dbObj) 143 | 144 | return true 145 | } catch (err) { 146 | console.error('Error in peer-use-cases.js/sendPrivateMessage(): ', err) 147 | throw err 148 | } 149 | } 150 | } 151 | 152 | module.exports = PeerUseCases 153 | -------------------------------------------------------------------------------- /lib/use-cases/pubsub-use-cases.js: -------------------------------------------------------------------------------- 1 | /* 2 | A Use Case library for interacting with the Pubsub Entity. 3 | */ 4 | 5 | const DEFAULT_COORDINATION_ROOM = 'psf-ipfs-coordination-002' 6 | const BCH_COINJOIN_ROOM = 'bch-coinjoin-001' 7 | 8 | class PubsubUseCase { 9 | constructor (localConfig = {}) { 10 | // Dependency Injection. 11 | this.adapters = localConfig.adapters 12 | if (!this.adapters) { 13 | throw new Error( 14 | 'Must inject instance of adapters when instantiating Pubsub Use Cases library.' 15 | ) 16 | } 17 | this.controllers = localConfig.controllers 18 | if (!this.controllers) { 19 | throw new Error( 20 | 'Must inject instance of controllers when instantiating Pubsub Use Cases library.' 21 | ) 22 | } 23 | this.thisNodeUseCases = localConfig.thisNodeUseCases 24 | if (!this.thisNodeUseCases) { 25 | throw new Error( 26 | 'thisNode use cases required when instantiating Pubsub Use Cases library.' 27 | ) 28 | } 29 | 30 | // Allow the app to override the default CoinJoin pubsub handler. 31 | this.coinjoinPubsubHandler = () => true 32 | if (localConfig.coinjoinPubsubHandler) this.coinjoinPubsubHandler = localConfig.coinjoinPubsubHandler 33 | } 34 | 35 | // Connect to the default pubsub rooms. 36 | async initializePubsub (thisNode) { 37 | try { 38 | // Subscribe to the coordination channel, where new peers announce themselves 39 | // to the network. 40 | await this.adapters.pubsub.subscribeToPubsubChannel( 41 | DEFAULT_COORDINATION_ROOM, 42 | // this.adapters.peers.addPeer 43 | this.thisNodeUseCases.addSubnetPeer, 44 | thisNode 45 | ) 46 | 47 | // Subscribe to the BCH CoinJoin coordination channel. This code is here 48 | // so that Circuit Relays automatically subscribe to the channel and 49 | // relay the messages. 50 | await this.adapters.pubsub.subscribeToPubsubChannel( 51 | BCH_COINJOIN_ROOM, 52 | this.coinjoinPubsubHandler, 53 | thisNode 54 | ) 55 | } catch (err) { 56 | console.error('Error in pubsub-use-cases.js/initializePubsub()') 57 | throw err 58 | } 59 | } 60 | } 61 | 62 | module.exports = PubsubUseCase 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipfs-coord", 3 | "version": "8.0.0", 4 | "description": "A JS library for helping IPFS peers coordinate, find a common interest, and stay connected around that interest.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "npm run lint && TEST=unit nyc mocha --timeout=15000 --exit --recursive test/unit/", 9 | "test:integration": "mocha --timeout 25000 test/integration/", 10 | "lint": "standard --env mocha --fix", 11 | "docs": "./node_modules/.bin/apidoc -i src/ -o docs", 12 | "coverage": "nyc report --reporter=text-lcov | coveralls", 13 | "coverage:report": "nyc --reporter=html mocha test/unit/ --exit --recursive", 14 | "test:temp": "npm run lint && TEST=unit nyc mocha -g --timeout=15000 '#generatePrivateKey' test/unit/" 15 | }, 16 | "keywords": [ 17 | "bitcoin", 18 | "bitcoin cash", 19 | "wallet", 20 | "javascript", 21 | "cryptocurrency", 22 | "react", 23 | "front end", 24 | "client", 25 | "apidoc", 26 | "slp", 27 | "tokens" 28 | ], 29 | "author": "Chris Troutner ", 30 | "license": "MIT", 31 | "apidoc": { 32 | "title": "ipfs-coord", 33 | "url": "localhost:5000" 34 | }, 35 | "repository": "Permissionless-Software-Foundation/ipfs-coord", 36 | "devDependencies": { 37 | "@psf/bch-js": "6.6.0", 38 | "apidoc": "0.25.0", 39 | "chai": "4.2.0", 40 | "coveralls": "3.1.0", 41 | "eslint": "7.17.0", 42 | "eslint-config-prettier": "7.1.0", 43 | "eslint-config-standard": "16.0.2", 44 | "eslint-plugin-node": "11.1.0", 45 | "eslint-plugin-prettier": "3.3.1", 46 | "eslint-plugin-standard": "4.0.0", 47 | "husky": "4.3.6", 48 | "lodash.clonedeep": "4.5.0", 49 | "mocha": "8.4.0", 50 | "nyc": "15.1.0", 51 | "semantic-release": "17.4.4", 52 | "sinon": "9.2.2", 53 | "standard": "16.0.3" 54 | }, 55 | "release": { 56 | "publish": [ 57 | { 58 | "path": "@semantic-release/npm", 59 | "npmPublish": true 60 | } 61 | ] 62 | }, 63 | "husky": { 64 | "hooks": { 65 | "pre-commit": "npm run lint" 66 | } 67 | }, 68 | "dependencies": { 69 | "axios": "0.21.4", 70 | "bch-encrypt-lib": "2.0.0", 71 | "uuid": "8.3.2" 72 | }, 73 | "peerDependencies": { 74 | "ipfs": ">= 0.58.6", 75 | "ipfs-http-client": ">= 55.0.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/mocks/adapter-mock.js: -------------------------------------------------------------------------------- 1 | /* 2 | A mocked version of the adapters library. 3 | */ 4 | 5 | class AdaptersMock { 6 | constructor () { 7 | this.ipfs = { 8 | ipfsPeerId: 'fake-id', 9 | ipfsMultiaddrs: ['addr1', 'addr2'], 10 | ipfs: { 11 | pubsub: { 12 | subscribe: () => { 13 | } 14 | } 15 | }, 16 | getPeers: () => { 17 | }, 18 | connectToPeer: () => { 19 | }, 20 | disconnectFromPeer: () => { 21 | }, 22 | disconnectFromMultiaddr: () => { 23 | } 24 | } 25 | 26 | this.bchjs = {} 27 | 28 | this.type = 'node.js' 29 | 30 | this.bch = { 31 | generateBchId: () => { 32 | return { 33 | cashAddress: 'cashAddress', 34 | slpAddress: 'slpAddress', 35 | publicKey: 'public-key' 36 | } 37 | }, 38 | bchjs: { 39 | Util: { 40 | sleep: () => { 41 | } 42 | } 43 | } 44 | } 45 | 46 | this.pubsub = { 47 | subscribeToPubsubChannel: () => { 48 | }, 49 | publishToPubsubChannel: () => { 50 | }, 51 | messaging: { 52 | publishToPubsubChannel: () => { 53 | }, 54 | generateMsgObj: () => { 55 | }, 56 | generateAckMsg: () => { 57 | }, 58 | sendMsg: () => { 59 | }, 60 | sendAck: () => { 61 | }, 62 | handleIncomingData: () => { 63 | }, 64 | _checkIfAlreadyProcessed: () => { 65 | }, 66 | delMsgFromQueue: () => { 67 | }, 68 | addMsgToQueue: () => { 69 | }, 70 | resendMsg: () => { 71 | }, 72 | waitForAck: () => { 73 | } 74 | }, 75 | about: { 76 | queryAbout: () => { 77 | } 78 | } 79 | } 80 | 81 | this.encryption = { 82 | encryptMsg: () => { 83 | } 84 | } 85 | 86 | this.orbit = { 87 | createRcvDb: () => { 88 | return { id: 'fake-orbit-id' } 89 | }, 90 | connectToPeerDb: () => { 91 | } 92 | } 93 | 94 | this.log = { 95 | statusLog: () => { 96 | } 97 | } 98 | 99 | this.gist = { 100 | getCRList: async () => { 101 | } 102 | } 103 | } 104 | } 105 | 106 | module.exports = AdaptersMock 107 | -------------------------------------------------------------------------------- /test/mocks/circuit-relay-mocks.js: -------------------------------------------------------------------------------- 1 | /** Circuit Relays mock */ 2 | const circuitRelays = [ 3 | { 4 | name: 'ipfs.fullstack.cash', 5 | multiaddr: 6 | '/ip4/116.203.193.74/tcp/4001/ipfs/QmNZktxkfScScnHCFSGKELH3YRqdxHQ3Le9rAoRLhZ6vgL', 7 | connected: true 8 | } 9 | ] 10 | 11 | const duplicateRelays = [ 12 | { 13 | multiaddr: 14 | '/ip4/139.162.76.54/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', 15 | connected: true, 16 | updatedAt: '2021-09-20T15:59:12.961Z', 17 | ipfsId: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', 18 | isBootstrap: false, 19 | metrics: { aboutLatency: [] }, 20 | latencyScore: 10000 21 | }, 22 | { 23 | multiaddr: 24 | '/ip4/157.90.28.11/tcp/4001/p2p/QmedLCUDSSvsjfPt9rDm65drNL7Dzu1mk1JCRxu9yuxgLL', 25 | connected: false, 26 | updatedAt: '2021-09-20T15:58:22.480Z', 27 | ipfsId: 'QmedLCUDSSvsjfPt9rDm65drNL7Dzu1mk1JCRxu9yuxgLL', 28 | isBootstrap: true, 29 | metrics: { aboutLatency: [] }, 30 | latencyScore: 10000 31 | }, 32 | { 33 | multiaddr: 34 | '/ip4/157.90.28.11/tcp/4001/p2p/QmedLCUDSSvsjfPt9rDm65drNL7Dzu1mk1JCRxu9yuxgLL', 35 | connected: false, 36 | updatedAt: '2021-09-20T15:58:22.480Z', 37 | ipfsId: 'QmedLCUDSSvsjfPt9rDm65drNL7Dzu1mk1JCRxu9yuxgLL', 38 | isBootstrap: true, 39 | metrics: { aboutLatency: [] }, 40 | latencyScore: 10000 41 | }, 42 | { 43 | multiaddr: 44 | '/ip4/157.90.28.11/tcp/4001/p2p/QmedLCUDSSvsjfPt9rDm65drNL7Dzu1mk1JCRxu9yuxgLL', 45 | connected: false, 46 | updatedAt: '2021-09-20T15:58:22.480Z', 47 | ipfsId: 'QmedLCUDSSvsjfPt9rDm65drNL7Dzu1mk1JCRxu9yuxgLL', 48 | isBootstrap: true, 49 | metrics: { aboutLatency: [] }, 50 | latencyScore: 10000 51 | }, 52 | { 53 | multiaddr: 54 | '/ip4/137.184.13.92/tcp/5668/p2p/Qma4iaNqgCAzA3HqNNEkKZzqWhCMnjt19TEHLu8TKhHhRK', 55 | connected: true, 56 | updatedAt: '2021-09-20T15:58:14.963Z', 57 | ipfsId: 'Qma4iaNqgCAzA3HqNNEkKZzqWhCMnjt19TEHLu8TKhHhRK', 58 | isBootstrap: true, 59 | metrics: { aboutLatency: [] }, 60 | latencyScore: 10000 61 | } 62 | ] 63 | 64 | module.exports = { 65 | circuitRelays, 66 | duplicateRelays 67 | } 68 | -------------------------------------------------------------------------------- /test/mocks/ipfs-mock.js: -------------------------------------------------------------------------------- 1 | /* 2 | A mocked instance of ipfs, for use in unit tests. 3 | */ 4 | const ipfs = { 5 | id: () => { 6 | return { 7 | id: 'myID', 8 | addresses: ['addr1', 'addr2'] 9 | } 10 | }, 11 | swarm: { 12 | connect: async () => {}, 13 | peers: async () => { 14 | return [] 15 | }, 16 | disconnect: async () => {} 17 | }, 18 | pubsub: { 19 | subscribe: async () => {}, 20 | publish: async () => {} 21 | }, 22 | config: { 23 | set: () => {}, 24 | get: () => {}, 25 | getAll: () => {} 26 | } 27 | } 28 | 29 | module.exports = ipfs 30 | -------------------------------------------------------------------------------- /test/mocks/orbitdb-mock.js: -------------------------------------------------------------------------------- 1 | /* 2 | Mocks for orbitdb unit tests. 3 | */ 4 | 5 | const mockEventLog = { 6 | load: () => {}, 7 | events: { 8 | on: () => {} 9 | }, 10 | id: 'abc', 11 | iterator: () => { 12 | return { 13 | collect: () => [ 14 | { 15 | payload: { 16 | value: { 17 | data: 'abc123', 18 | from: 'somePeerId' 19 | } 20 | } 21 | } 22 | ] 23 | } 24 | }, 25 | collect: () => [ 26 | { 27 | data: 'abc123', 28 | from: 'somePeerId' 29 | } 30 | ] 31 | } 32 | 33 | const mockCreateInstance = { 34 | eventlog: () => { 35 | return { 36 | load: () => {}, 37 | events: { 38 | on: () => {} 39 | }, 40 | id: 'abc' 41 | } 42 | } 43 | } 44 | 45 | module.exports = { 46 | mockEventLog, 47 | mockCreateInstance 48 | } 49 | -------------------------------------------------------------------------------- /test/mocks/peers-mock.js: -------------------------------------------------------------------------------- 1 | const announceObj = { 2 | from: 'QmcReHFvNgxFLnLWtVa5SYmeGbmnzBdphKEqPMKJ6XfBh4', 3 | channel: 'psf-ipfs-coordination-001', 4 | data: { 5 | apiName: 'ipfs-coord-announce', 6 | apiVersion: '1.3.0', 7 | apiInfo: 'ipfs-hash-to-documentation-to-go-here', 8 | ipfsId: 'QmcReHFvNgxFLnLWtVa5SYmeGbmnzBdphKEqPMKJ6XfBh4', 9 | type: 'browser', 10 | ipfsMultiaddrs: [], 11 | circuitRelays: [], 12 | isCircuitRelay: false, 13 | cryptoAddresses: [], 14 | encryptPubKey: '' 15 | } 16 | } 17 | 18 | const announceObj2 = { 19 | from: 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 20 | channel: 'psf-ipfs-coordination-001', 21 | data: { 22 | apiName: 'ipfs-coord-announce', 23 | apiVersion: '1.3.0', 24 | apiInfo: 'ipfs-hash-to-documentation-to-go-here', 25 | ipfsId: 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 26 | type: 'browser', 27 | ipfsMultiaddrs: [], 28 | circuitRelays: [], 29 | isCircuitRelay: false, 30 | cryptoAddresses: [], 31 | encryptPubKey: '' 32 | } 33 | } 34 | const swarmPeers = [ 35 | { 36 | addr: 37 | '', 38 | peer: 'QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsjd' 39 | }, 40 | { 41 | addr: 42 | '', 43 | peer: 'QmcReHFvNgxFLnLWtVa5SYmeGbmnzBdphKEqPMKJ6XfJh4' 44 | }, 45 | { 46 | addr: 47 | ' ', 48 | peer: 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ' 49 | } 50 | ] 51 | // Peers that match with circuit relay address mock id 52 | const swarmPeers2 = [ 53 | { 54 | addr: 55 | ' ', 56 | peer: 'QmNZktxkfScScnHCFSGKELH3YRqdxHQ3Le9rAoRLhZ6vgL' 57 | } 58 | ] 59 | 60 | const mockRelayData = [ 61 | { 62 | name: 'ipfs.fullstack.cash', 63 | multiaddr: 64 | '/ip4/116.203.193.74/tcp/4001/ipfs/QmNZktxkfScScnHCFSGKELH3YRqdxHQ3Le9rAoRLhZ6vgL', 65 | connected: true 66 | } 67 | ] 68 | 69 | module.exports = { 70 | announceObj, 71 | announceObj2, 72 | swarmPeers, 73 | swarmPeers2, 74 | mockRelayData 75 | } 76 | -------------------------------------------------------------------------------- /test/mocks/pubsub-mocks.js: -------------------------------------------------------------------------------- 1 | const mockData = `${'{"apiName":"ipfs-coord","apiVersion":"1.3.0","apiInfo":"ipfs-hash-to-documentation-to-go-here","ipfsId":"QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7","type":"node.js","ipfsMultiaddrs":["/ip4/10.0.0.3/tcp/4002/p2p/QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7","/ip4/127.0.0.1/tcp/4002/p2p/QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7","/ip4/127.0.0.1/tcp/4003/ws/p2p/QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7","/ip4/157.90.20.129/tcp/4002/p2p/QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7"],"circuitRelays":[],"cryptoAddresses":[],"encryptPubKey":""}'}` 2 | 3 | const mockMsg = { 4 | from: 'QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7', 5 | data: Buffer.from(JSON.stringify(mockData)), 6 | seqno: Buffer.from('test'), 7 | topicIDs: ['psf-ipfs-coordination-001'], 8 | signature: Buffer.from('test'), 9 | key: Buffer.from('test'), 10 | receivedFrom: 'QmRrUu64cAnPntYiUc7xMunLKZgj1XZT5HmqJNtDMqQcD7' 11 | } 12 | 13 | const aboutRequest = '{"jsonrpc":"2.0","id":"metrics3796","method":"about"}' 14 | 15 | const aboutResponse = '{"jsonrpc": "2.0", "id": "metrics3796", "result": {"method": "about", "receiver": "12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f", "value": {"ipfsId":"12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","name":"12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","type":"node.js","ipfsMultiaddrs":["/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","/ip4/5.161.46.163/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","/ip6/2a01:4ff:f0:f76::1/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","/ip6/::1/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa"],"isCircuitRelay":true,"circuitRelayInfo":{"ip4":"5.161.46.163","tcpPort":"4001","crDomain":""},"cashAddress":"bitcoincash:qzerrnr5nfdkr3h62cxf0adn4jykjk53zudz4py26e","slpAddress":"simpleledger:qzerrnr5nfdkr3h62cxf0adn4jykjk53zupe7632y8","publicKey":"02e719acbfd3060fa75503ec7af528f5ba67da8a2b9b8e89dbf9b60676740868a0","orbitdbId":"","apiInfo":"You should put an IPFS hash or web URL here to your documentation.","announceJsonLd":{"@context":"https://schema.org/","@type":"WebAPI","name":"ipfs-bch-service-generic","version":"2.0.0","protocol":"bch-wallet","description":"IPFS service providing BCH blockchain access needed by a wallet.","documentation":"https://ipfs-bch-wallet-service.fullstack.cash/","provider":{"@type":"Organization","name":"Permissionless Software Foundation","url":"https://PSFoundation.cash"},"identifier":"12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa"}}}}' 16 | 17 | const badId = '{"jsonrpc":"2.0","id":"bad-id","method":"about"}' 18 | 19 | const msgObj = { 20 | from: '12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa', 21 | channel: '12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f', 22 | data: { 23 | timestamp: '2022-03-04T18:19:18.897Z', 24 | uuid: '311e15f5-e647-488f-8ff5-8a41b254e7c3', 25 | sender: '12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa', 26 | receiver: '12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f', 27 | payload: '{"jsonrpc": "2.0", "id": "metrics3796", "result": {"method": "about", "receiver": "12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f", "value": {"ipfsId":"12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","name":"12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","type":"node.js","ipfsMultiaddrs":["/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","/ip4/5.161.46.163/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","/ip6/2a01:4ff:f0:f76::1/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa","/ip6/::1/tcp/4001/p2p/12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa"],"isCircuitRelay":true,"circuitRelayInfo":{"ip4":"5.161.46.163","tcpPort":"4001","crDomain":""},"cashAddress":"bitcoincash:qzerrnr5nfdkr3h62cxf0adn4jykjk53zudz4py26e","slpAddress":"simpleledger:qzerrnr5nfdkr3h62cxf0adn4jykjk53zupe7632y8","publicKey":"02e719acbfd3060fa75503ec7af528f5ba67da8a2b9b8e89dbf9b60676740868a0","orbitdbId":"","apiInfo":"You should put an IPFS hash or web URL here to your documentation.","announceJsonLd":{"@context":"https://schema.org/","@type":"WebAPI","name":"ipfs-bch-service-generic","version":"2.0.0","protocol":"bch-wallet","description":"IPFS service providing BCH blockchain access needed by a wallet.","documentation":"https://ipfs-bch-wallet-service.fullstack.cash/","provider":{"@type":"Organization","name":"Permissionless Software Foundation","url":"https://PSFoundation.cash"},"identifier":"12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa"}}}}' 28 | } 29 | } 30 | 31 | module.exports = { 32 | mockMsg, 33 | aboutRequest, 34 | aboutResponse, 35 | badId, 36 | msgObj 37 | } 38 | -------------------------------------------------------------------------------- /test/mocks/thisnode-mocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | 'thisNode' is an object that is passed around to a lot of functions. The object 3 | represents an instance of the IPFS node. This file creates a mock of thisNode 4 | that can be used for unit tests. 5 | */ 6 | 7 | const thisNode = { 8 | ipfsId: '12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f', 9 | ipfsMultiaddrs: [ 10 | '/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f', 11 | '/ip6/::1/tcp/4001/p2p/12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f' 12 | ], 13 | bchAddr: 'bitcoincash:qpjecejl9n90u9vv7cg7p9qfjk4zjwqus5hff6sfpm', 14 | slpAddr: 'simpleledger:qpjecejl9n90u9vv7cg7p9qfjk4zjwqus5mjzp9fl9', 15 | publicKey: '0232ef60e2c545d49d18c95fa7379164693ff6d201221aefea6bee872e4c03be12', 16 | type: 'node.js', 17 | peerList: ['12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa'], 18 | peerData: [ 19 | { 20 | from: '12D3KooWHS5A6Ey4V8fLWD64jpPn2EKi4r4btGN6FfkNgMTnfqVa', 21 | channel: 'psf-ipfs-coordination-002', 22 | data: { 23 | encryptPubKey: '0232ef60e2c545d49d18c95fa7379164693ff6d201221aefea6bee872e4c03be12' 24 | } 25 | } 26 | ], 27 | relayData: [ 28 | { 29 | multiaddr: '/ip4/5.161.43.61/tcp/5268/p2p/QmWPfWgbSjPPFpvmS2QH7NPx14DqxMV8eGAUHLcYfyo1St', 30 | connected: false, 31 | updatedAt: '2022-03-03T22:01:05.715Z', 32 | ipfsId: 'QmWPfWgbSjPPFpvmS2QH7NPx14DqxMV8eGAUHLcYfyo1St', 33 | isBootstrap: false, 34 | metrics: {}, 35 | latencyScore: 10000 36 | }, 37 | { 38 | multiaddr: '/ip4/88.99.188.196/tcp/5268/p2p/QmXbyd4tWzwhGyyZJ9QJctfJJLq7oAJRs39aqpRXUAbu5j', 39 | connected: false, 40 | updatedAt: '2022-03-03T22:01:06.110Z', 41 | ipfsId: 'QmXbyd4tWzwhGyyZJ9QJctfJJLq7oAJRs39aqpRXUAbu5j', 42 | isBootstrap: false, 43 | metrics: {}, 44 | latencyScore: 10000 45 | } 46 | ], 47 | serviceList: [], 48 | serviceData: [], 49 | blacklistPeers: [ 50 | 'QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', 51 | 'Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6', 52 | 'QmV7gnbW5VTcJ3oyM2Xk1rdFBJ3kTkvxc87UFGsun29STS', 53 | 'QmY7JB6MQXhxHvq7dBDh4HpbH29v4yE9JRadAVpndvzySN', 54 | 'QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 55 | 'QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 56 | 'QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', 57 | 'QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp', 58 | 'QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', 59 | 'QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' 60 | ], 61 | blacklistMultiaddrs: [ 62 | '/dns4/node0.preload.ipfs.io/tcp/443/wss/p2p/QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', 63 | '/dns4/node1.preload.ipfs.io/tcp/443/wss/p2p/Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6', 64 | '/dns4/node2.preload.ipfs.io/tcp/443/wss/p2p/QmV7gnbW5VTcJ3oyM2Xk1rdFBJ3kTkvxc87UFGsun29STS', 65 | '/dns4/node3.preload.ipfs.io/tcp/443/wss/p2p/QmY7JB6MQXhxHvq7dBDh4HpbH29v4yE9JRadAVpndvzySN', 66 | '/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ', 67 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', 68 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', 69 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp', 70 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', 71 | '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt', 72 | '/dns4/node0.delegate.ipfs.io/tcp/443/https', 73 | '/dns4/node1.delegate.ipfs.io/tcp/443/https', 74 | '/dns4/node2.delegate.ipfs.io/tcp/443/https', 75 | '/dns4/node3.delegate.ipfs.io/tcp/443/https' 76 | ], 77 | schema: {}, 78 | useCases: {} 79 | } 80 | 81 | module.exports = thisNode 82 | -------------------------------------------------------------------------------- /test/mocks/use-case-mocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | Mocked version of the Use Cases library. 3 | */ 4 | 5 | class UseCasesMock { 6 | constructor () { 7 | this.thisNode = { 8 | refreshPeerConnections: () => {}, 9 | enforceBlacklist: () => {}, 10 | enforceWhitelist: () => {} 11 | } 12 | this.relays = { 13 | connectToCRs: () => {}, 14 | addRelay: () => {}, 15 | measureRelays: () => {}, 16 | sortRelays: obj => obj 17 | } 18 | this.pubsub = {} 19 | this.peer = {} 20 | } 21 | } 22 | 23 | module.exports = UseCasesMock 24 | -------------------------------------------------------------------------------- /test/mocks/util-mocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | A mocking library for util.js unit tests. 3 | A mocking library contains data to use in place of the data that would come 4 | from an external dependency. 5 | */ 6 | 7 | 'use strict' 8 | 9 | const mockBalance = { 10 | success: true, 11 | balance: { 12 | confirmed: 1000, 13 | unconfirmed: 0 14 | } 15 | } 16 | 17 | const mockUtxos = { 18 | success: true, 19 | utxos: [ 20 | { 21 | height: 601861, 22 | tx_hash: '6181c669614fa18039a19b23eb06806bfece1f7514ab457c3bb82a40fe171a6d', 23 | tx_pos: 0, 24 | value: 1000 25 | } 26 | ] 27 | } 28 | 29 | module.exports = { 30 | mockBalance, 31 | mockUtxos 32 | } 33 | -------------------------------------------------------------------------------- /test/unit/adapters/about.unit.adapters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the about adapter library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // local libraries 10 | const AboutAdapter = require('../../../lib/adapters/pubsub-adapter/about-adapter') 11 | 12 | describe('#About-adapter', () => { 13 | let uut 14 | let sandbox 15 | 16 | beforeEach(() => { 17 | // Restore the sandbox before each test. 18 | sandbox = sinon.createSandbox() 19 | 20 | // Instantiate the library under test. Must instantiate dependencies first. 21 | uut = new AboutAdapter() 22 | }) 23 | 24 | afterEach(() => sandbox.restore()) 25 | 26 | describe('#sendRPC', () => { 27 | it('should return false if response is not recieved in time', async () => { 28 | // Prep test data. 29 | uut.waitPeriod = 1 30 | const ipfsId = 'testId' 31 | const cmdStr = 'fakeCmd' 32 | const id = 1 33 | const thisNode = { 34 | useCases: { 35 | peer: { 36 | sendPrivateMessage: async () => { 37 | }, 38 | adapters: { 39 | bch: { 40 | bchjs: { 41 | Util: { 42 | sleep: () => { 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | const result = await uut.sendRPC(ipfsId, cmdStr, id, thisNode) 53 | // console.log('result: ', result) 54 | 55 | assert.equal(result, false) 56 | }) 57 | 58 | it('should return the result of the RPC call', async () => { 59 | // Prep test data. 60 | uut.waitPeriod = 2000 61 | const ipfsId = 'testId' 62 | const cmdStr = 'fakeCmd' 63 | const id = 1 64 | const thisNode = { 65 | useCases: { 66 | peer: { 67 | sendPrivateMessage: async () => { 68 | }, 69 | adapters: { 70 | bch: { 71 | bchjs: { 72 | Util: { 73 | sleep: () => { 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | // Force positive code path. 84 | uut.incomingData = '{"id": 1}' 85 | 86 | const result = await uut.sendRPC(ipfsId, cmdStr, id, thisNode) 87 | // console.log('result: ', result) 88 | 89 | assert.equal(result, true) 90 | }) 91 | 92 | it('should catch and throw errors', async () => { 93 | try { 94 | await uut.sendRPC() 95 | 96 | assert.fail('Unexpected code path') 97 | } catch (err) { 98 | // console.log(err) 99 | assert.include(err.message, 'Cannot read') 100 | } 101 | }) 102 | }) 103 | 104 | describe('#queryAbout', () => { 105 | it('should return true after peer responds to RPC', async () => { 106 | // Mock dependencies 107 | sandbox.stub(uut, 'sendRPC').resolves(true) 108 | 109 | const result = await uut.queryAbout() 110 | assert.equal(result, true) 111 | }) 112 | 113 | it('should return false if peer never responds to RPC', async () => { 114 | // Mock dependencies 115 | sandbox.stub(uut, 'sendRPC').resolves(false) 116 | 117 | const result = await uut.queryAbout() 118 | assert.equal(result, false) 119 | }) 120 | 121 | it('should return false when there is an error', async () => { 122 | // Mock dependencies 123 | sandbox.stub(uut, 'sendRPC').rejects(new Error('test error')) 124 | 125 | const result = await uut.queryAbout() 126 | assert.equal(result, false) 127 | }) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /test/unit/adapters/bch.unit.adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the bch-lib library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | const BCHJS = require('@psf/bch-js') 9 | 10 | // local libraries 11 | const BchLib = require('../../../lib/adapters/bch-adapter') 12 | 13 | describe('#bch-adapter', () => { 14 | let sandbox 15 | let uut 16 | let bchjs 17 | 18 | beforeEach(() => { 19 | // Restore the sandbox before each test. 20 | sandbox = sinon.createSandbox() 21 | 22 | bchjs = new BCHJS() 23 | uut = new BchLib({ bchjs }) 24 | }) 25 | 26 | afterEach(() => sandbox.restore()) 27 | 28 | describe('#constructor', () => { 29 | it('should throw an error if bch-js instance is not passed in', () => { 30 | try { 31 | uut = new BchLib({}) 32 | } catch (err) { 33 | assert.include( 34 | err.message, 35 | 'An instance of bch-js must be passed when instantiating the BCH adapter library.' 36 | ) 37 | } 38 | }) 39 | }) 40 | 41 | describe('#generateBchId', () => { 42 | it('should generate a new BCH ID if mnemonic is not given', async () => { 43 | const result = await uut.generateBchId() 44 | // console.log(`result: ${JSON.stringify(result, null, 2)}`) 45 | 46 | assert.property(result, 'cashAddress') 47 | assert.property(result, 'slpAddress') 48 | assert.property(result, 'publicKey') 49 | }) 50 | 51 | it('should generate same address if mnemonic is given', async () => { 52 | const mnemonic = 53 | 'feature cart obtain exist impulse slab frog run smile elder crucial fatigue' 54 | 55 | uut = new BchLib({ bchjs, mnemonic }) 56 | const result = await uut.generateBchId() 57 | // console.log(`result: ${JSON.stringify(result, null, 2)}`) 58 | 59 | assert.equal( 60 | result.cashAddress, 61 | 'bitcoincash:qpagvxxj29p24nkyheezrfky93jxf6h20uul3ldg73' 62 | ) 63 | assert.equal( 64 | result.slpAddress, 65 | 'simpleledger:qpagvxxj29p24nkyheezrfky93jxf6h20usy6ycgq0' 66 | ) 67 | assert.equal( 68 | result.publicKey, 69 | '02995864fdcf5769b14b16e7cfb86f643a567470a2f557983a4284a8c6f17dc767' 70 | ) 71 | }) 72 | 73 | it('should catch and throw an error', async () => { 74 | try { 75 | sandbox 76 | .stub(uut.bchjs.Mnemonic, 'generate') 77 | .throws(new Error('test error')) 78 | 79 | await uut.generateBchId() 80 | 81 | assert.fail('Unexpected code path. Error was expected to be thrown.') 82 | } catch (err) { 83 | // console.log(err) 84 | assert.include(err.message, 'test error') 85 | } 86 | }) 87 | }) 88 | 89 | describe('#generatePrivateKey', () => { 90 | it('should generate a private key', async () => { 91 | const result = await uut.generatePrivateKey() 92 | console.log('result: ', result) 93 | 94 | // The private key shoul be a string. 95 | assert.isString(result) 96 | 97 | // It shoul be a WIF that starts with a K or L 98 | // assert.equal(result[0], 'K' || 'L') 99 | }) 100 | 101 | it('should catch and throw an error', async () => { 102 | try { 103 | sandbox 104 | .stub(uut.bchjs.Mnemonic, 'generate') 105 | .throws(new Error('test error')) 106 | 107 | await uut.generatePrivateKey() 108 | 109 | assert.fail('Unexpected code path. Error was expected to be thrown.') 110 | } catch (err) { 111 | // console.log(err) 112 | assert.include(err.message, 'test error') 113 | } 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/unit/adapters/encryption.unit.adapters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the encryption adapter library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | const BCHJS = require('@psf/bch-js') 9 | 10 | // local libraries 11 | const EncryptionAdapter = require('../../../lib/adapters/encryption-adapter') 12 | const BchAdapter = require('../../../lib/adapters/bch-adapter') 13 | 14 | describe('#Encryption-adapter', () => { 15 | let uut 16 | let sandbox 17 | 18 | beforeEach(() => { 19 | // Restore the sandbox before each test. 20 | sandbox = sinon.createSandbox() 21 | 22 | const bchjs = new BCHJS() 23 | const bch = new BchAdapter({ bchjs }) 24 | 25 | // Instantiate the library under test. Must instantiate dependencies first. 26 | uut = new EncryptionAdapter({ bch }) 27 | }) 28 | 29 | afterEach(() => sandbox.restore()) 30 | 31 | describe('#constructor', () => { 32 | it('should throw an error if bch adapter is not included', () => { 33 | try { 34 | uut = new EncryptionAdapter() 35 | 36 | assert.fail('Unexpected code path') 37 | } catch (err) { 38 | assert.include( 39 | err.message, 40 | 'Must pass in an instance of bch Adapter when instantiating the encryption Adapter library.' 41 | ) 42 | } 43 | }) 44 | }) 45 | 46 | describe('#decryptMsg', () => { 47 | it('should decrypt a message', async () => { 48 | // Mock dependencies 49 | sandbox 50 | .stub(uut.bchEncrypt.encryption, 'decryptFile') 51 | .resolves('decryptedMsg') 52 | 53 | const result = await uut.decryptMsg('F6') 54 | // console.log('result: ', result) 55 | 56 | assert.isOk(result) 57 | }) 58 | 59 | it('should handle BAD MAC error messages', async () => { 60 | try { 61 | // Force a BAD MAC error 62 | sandbox 63 | .stub(uut.bchEncrypt.encryption, 'decryptFile') 64 | .rejects(new Error('Bad MAC')) 65 | 66 | await uut.decryptMsg('F6') 67 | 68 | assert.fail('Unexpected code path') 69 | } catch (err) { 70 | assert.include(err.message, 'Bad MAC. Could not decrypt message.') 71 | } 72 | }) 73 | 74 | it('should catch and throw an error', async () => { 75 | try { 76 | // Force an error 77 | sandbox 78 | .stub(uut.bchEncrypt.encryption, 'decryptFile') 79 | .rejects(new Error('test error')) 80 | 81 | await uut.decryptMsg('F6') 82 | 83 | assert.fail('Unexpected code path') 84 | } catch (err) { 85 | assert.include(err.message, 'test error') 86 | } 87 | }) 88 | }) 89 | 90 | describe('#encryptMsg', () => { 91 | it('should catch and throw errors', async () => { 92 | try { 93 | const peer = { 94 | data: { 95 | encryptionKey: 'abc123' 96 | } 97 | } 98 | 99 | await uut.encryptMsg(peer, 'testMsg') 100 | // console.log('result: ', result) 101 | 102 | assert.fail('Unexpected code path') 103 | } catch (err) { 104 | assert.include(err.message, 'pubkey must be a hex string') 105 | } 106 | }) 107 | 108 | it('should encrypt a string', async () => { 109 | // Mock dependencies 110 | sandbox 111 | .stub(uut.bchEncrypt.encryption, 'encryptFile') 112 | .resolves('encryptedMsg') 113 | 114 | const peer = { 115 | data: { 116 | encryptionKey: 'abc123' 117 | } 118 | } 119 | 120 | const result = await uut.encryptMsg(peer, 'testMsg') 121 | // console.log('result: ', result) 122 | 123 | assert.equal(result, 'encryptedMsg') 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /test/unit/adapters/gist.unit.adapters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the schema.js library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | // const cloneDeep = require('lodash.clonedeep') 9 | 10 | // local libraries 11 | const Gist = require('../../../lib/adapters/gist') 12 | 13 | describe('#gist', () => { 14 | let sandbox 15 | let uut 16 | 17 | beforeEach(() => { 18 | // Restore the sandbox before each test. 19 | sandbox = sinon.createSandbox() 20 | 21 | uut = new Gist() 22 | }) 23 | 24 | afterEach(() => sandbox.restore()) 25 | 26 | describe('#getCRList', () => { 27 | it('should get data from GitHub', async () => { 28 | // Mock network dependencies 29 | sandbox.stub(uut.axios, 'get').resolves({ 30 | data: { 31 | files: { 32 | 'psf-public-circuit-relays.json': { 33 | content: JSON.stringify({ key: 'value' }) 34 | } 35 | } 36 | } 37 | }) 38 | 39 | const result = await uut.getCRList() 40 | // console.log('result: ', result) 41 | 42 | assert.property(result, 'key') 43 | assert.equal(result.key, 'value') 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/unit/adapters/index.unit.adapters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the main Adapters index.js file. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | 8 | // Local libraries 9 | const Adapters = require('../../../lib/adapters') 10 | const BCHJS = require('@psf/bch-js') 11 | const bchjs = new BCHJS() 12 | const ipfs = require('../../mocks/ipfs-mock') 13 | 14 | describe('#index.js-Adapters', () => { 15 | let uut 16 | 17 | describe('#constructor', () => { 18 | it('should throw an error if ipfs is not included', () => { 19 | try { 20 | uut = new Adapters() 21 | 22 | assert.fail('Unexpected code path') 23 | } catch (err) { 24 | assert.include( 25 | err.message, 26 | 'An instance of IPFS must be passed when instantiating the Adapters library.' 27 | ) 28 | } 29 | }) 30 | 31 | it('should throw an error if bch-js is not included', () => { 32 | try { 33 | uut = new Adapters({ ipfs: {} }) 34 | 35 | assert.fail('Unexpected code path') 36 | } catch (err) { 37 | assert.include( 38 | err.message, 39 | 'An instance of @psf/bch-js must be passed when instantiating the Adapters library.' 40 | ) 41 | } 42 | }) 43 | 44 | it('should throw an error if node type is not specified', () => { 45 | try { 46 | uut = new Adapters({ ipfs: {}, bchjs: {} }) 47 | 48 | assert.fail('Unexpected code path') 49 | } catch (err) { 50 | assert.include( 51 | err.message, 52 | 'The type of IPFS node (browser or node.js) must be specified.' 53 | ) 54 | } 55 | }) 56 | 57 | it('should instantiate other adapter libraries', () => { 58 | uut = new Adapters({ 59 | ipfs, 60 | bchjs, 61 | type: 'node.js', 62 | statusLog: () => {}, 63 | privateLog: () => {} 64 | }) 65 | 66 | assert.property(uut, 'encryption') 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/unit/adapters/ipfs.unit.adapters.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the ipfs-adapters.js library 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // local libraries 10 | const IPFSAdapter = require('../../../lib/adapters/ipfs-adapter') 11 | const ipfs = require('../../mocks/ipfs-mock') 12 | 13 | describe('#ipfs-adapter', () => { 14 | let sandbox 15 | let uut 16 | 17 | beforeEach(() => { 18 | // Restore the sandbox before each test. 19 | sandbox = sinon.createSandbox() 20 | 21 | const log = { 22 | statusLog: () => {} 23 | } 24 | uut = new IPFSAdapter({ ipfs, log }) 25 | }) 26 | 27 | afterEach(() => sandbox.restore()) 28 | 29 | describe('#constructor', () => { 30 | it('should throw an error if ipfs instance is not passed in', () => { 31 | try { 32 | uut = new IPFSAdapter({}) 33 | } catch (err) { 34 | assert.include( 35 | err.message, 36 | 'An instance of IPFS must be passed when instantiating the IPFS adapter library.' 37 | ) 38 | } 39 | }) 40 | 41 | it('should throw an error if log instance is not passed in', () => { 42 | try { 43 | uut = new IPFSAdapter({ ipfs }) 44 | } catch (err) { 45 | assert.include( 46 | err.message, 47 | 'A status log handler must be specified when instantiating IPFS adapter library.' 48 | ) 49 | } 50 | }) 51 | }) 52 | 53 | describe('#start', () => { 54 | it('should populate ID and multiaddrs', async () => { 55 | await uut.start() 56 | 57 | // console.log('uut: ', uut) 58 | 59 | assert.property(uut, 'ipfsPeerId') 60 | assert.property(uut, 'ipfsMultiaddrs') 61 | }) 62 | 63 | it('should catch and throw an error', async () => { 64 | try { 65 | // Force an error 66 | sandbox.stub(uut.ipfs, 'id').rejects(new Error('test error')) 67 | 68 | await uut.start() 69 | 70 | assert.fail('Unexpected code path') 71 | } catch (err) { 72 | assert.include(err.message, 'test error') 73 | } 74 | }) 75 | }) 76 | 77 | describe('#connectToPeer', () => { 78 | it('should return true after connecting to peer', async () => { 79 | const result = await uut.connectToPeer('fakeId') 80 | 81 | assert.equal(result, true) 82 | }) 83 | 84 | it('should report status when debugLevel is greater than zero', async () => { 85 | uut.debugLevel = 1 86 | 87 | const result = await uut.connectToPeer('fakeId') 88 | 89 | assert.equal(result, true) 90 | }) 91 | 92 | it('should return false when issues connecting to peer', async () => { 93 | // Force an error 94 | sandbox.stub(uut.ipfs.swarm, 'connect').rejects(new Error('test error')) 95 | 96 | const result = await uut.connectToPeer('fakeId') 97 | 98 | assert.equal(result, false) 99 | }) 100 | 101 | it('should report connection errors at debugLevel 1', async () => { 102 | uut.debugLevel = 1 103 | 104 | // Force an error 105 | sandbox.stub(uut.ipfs.swarm, 'connect').rejects(new Error('test error')) 106 | 107 | const result = await uut.connectToPeer('fakeId') 108 | 109 | assert.equal(result, false) 110 | }) 111 | 112 | it('should report full errors at debugLevel 2', async () => { 113 | uut.debugLevel = 2 114 | 115 | // Force an error 116 | sandbox.stub(uut.ipfs.swarm, 'connect').rejects(new Error('test error')) 117 | 118 | const result = await uut.connectToPeer('fakeId') 119 | 120 | assert.equal(result, false) 121 | }) 122 | }) 123 | 124 | describe('#getPeers', () => { 125 | it('should return an array of peers', async () => { 126 | const result = await uut.getPeers() 127 | 128 | assert.isArray(result) 129 | }) 130 | 131 | it('should catch and throw an error', async () => { 132 | try { 133 | // Force an error 134 | sandbox.stub(uut.ipfs.swarm, 'peers').rejects(new Error('test error')) 135 | 136 | await uut.getPeers() 137 | 138 | assert.fail('Unexpected code path') 139 | } catch (err) { 140 | assert.include(err.message, 'test error') 141 | } 142 | }) 143 | }) 144 | 145 | describe('#disconnectFromPeer', () => { 146 | it('should return true if thisNode is not connected to the peer', async () => { 147 | // Mock dependencies 148 | sandbox.stub(uut, 'getPeers').resolves([]) 149 | 150 | const result = await uut.disconnectFromPeer('testId') 151 | 152 | assert.equal(result, true) 153 | }) 154 | 155 | it('should disconnect if thisNode is connected to the peer', async () => { 156 | // Mock dependencies 157 | sandbox.stub(uut, 'getPeers').resolves([{ peer: 'testId' }]) 158 | 159 | const result = await uut.disconnectFromPeer('testId') 160 | 161 | assert.equal(result, true) 162 | }) 163 | 164 | it('should return false on error', async () => { 165 | // Force an error 166 | sandbox.stub(uut, 'getPeers').rejects(new Error('test error')) 167 | 168 | const result = await uut.disconnectFromPeer('testId') 169 | 170 | assert.equal(result, false) 171 | }) 172 | }) 173 | 174 | describe('#disconnectFromMultiaddr', () => { 175 | it('should return true when disconnect succeeds', async () => { 176 | // Mock dependencies 177 | sandbox.stub(uut.ipfs.swarm, 'disconnect').resolves() 178 | 179 | const result = await uut.disconnectFromMultiaddr() 180 | 181 | assert.equal(result, true) 182 | }) 183 | 184 | it('should return false on error', async () => { 185 | // Mock dependencies 186 | sandbox 187 | .stub(uut.ipfs.swarm, 'disconnect') 188 | .rejects(new Error('test error')) 189 | 190 | const result = await uut.disconnectFromMultiaddr() 191 | 192 | assert.equal(result, false) 193 | }) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /test/unit/adapters/logs.unit.adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for log adapter. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // local libraries 10 | const LogsAdapter = require('../../../lib/adapters/logs-adapter') 11 | 12 | describe('#LogsAdapter', () => { 13 | let uut 14 | let sandbox 15 | 16 | beforeEach(() => { 17 | // Restore the sandbox before each test. 18 | sandbox = sinon.createSandbox() 19 | 20 | const config = { 21 | debugLevel: 2, 22 | statusLog: () => {} 23 | } 24 | 25 | // Instantiate the library under test. Must instantiate dependencies first. 26 | uut = new LogsAdapter(config) 27 | }) 28 | 29 | afterEach(() => sandbox.restore()) 30 | 31 | describe('#constructor', () => { 32 | it('should throw an error if log handler is specified', async () => { 33 | try { 34 | uut = new LogsAdapter() 35 | 36 | assert.fail('Unexpected code path') 37 | } catch (err) { 38 | assert.include( 39 | err.message, 40 | 'statusLog must be specified when instantiating Logs adapter library.' 41 | ) 42 | } 43 | }) 44 | }) 45 | 46 | describe('#statusLog', () => { 47 | it('should include object if defined', () => { 48 | uut.statusLog(1, 'test string', { message: 'obj message' }) 49 | }) 50 | 51 | it('should exclude object if undefined', () => { 52 | uut.statusLog(1, 'test string') 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/unit/adapters/pubsub.unit.adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for pubsub-adapter.js library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | const cloneDeep = require('lodash.clonedeep') 9 | const BCHJS = require('@psf/bch-js') 10 | 11 | // local libraries 12 | const Pubsub = require('../../../lib/adapters/pubsub-adapter') 13 | const ipfsLib = require('../../mocks/ipfs-mock') 14 | const mockDataLib = require('../../mocks/pubsub-mocks') 15 | const IPFSAdapter = require('../../../lib/adapters/ipfs-adapter') 16 | const thisNodeMock = require('../../mocks/thisnode-mocks') 17 | const EncryptionAdapter = require('../../../lib/adapters/encryption-adapter') 18 | const BchAdapter = require('../../../lib/adapters/bch-adapter') 19 | 20 | describe('#pubsub-adapter', () => { 21 | let sandbox 22 | let uut 23 | let ipfs, encryption 24 | let thisNode 25 | let mockData 26 | 27 | const log = { 28 | statusLog: () => { 29 | } 30 | } 31 | 32 | beforeEach(() => { 33 | // Restore the sandbox before each test. 34 | sandbox = sinon.createSandbox() 35 | 36 | ipfs = cloneDeep(ipfsLib) 37 | thisNode = cloneDeep(thisNodeMock) 38 | mockData = cloneDeep(mockDataLib) 39 | 40 | // Instantiate the IPFS adapter 41 | const ipfsAdapter = new IPFSAdapter({ ipfs, log }) 42 | 43 | // Instantiate the Encryption adapater 44 | const bchjs = new BCHJS() 45 | const bch = new BchAdapter({ bchjs }) 46 | encryption = new EncryptionAdapter({ bch }) 47 | 48 | // Instantiate the library under test. Must instantiate dependencies first. 49 | uut = new Pubsub({ ipfs: ipfsAdapter, log, encryption, privateLog: {} }) 50 | }) 51 | 52 | afterEach(() => sandbox.restore()) 53 | 54 | describe('#constructor', () => { 55 | it('should throw an error if IPFS adapter not specified', () => { 56 | try { 57 | uut = new Pubsub() 58 | 59 | assert.fail('Unexpected result') 60 | } catch (err) { 61 | assert.include( 62 | err.message, 63 | 'Instance of IPFS adapter required when instantiating Pubsub Adapter.' 64 | ) 65 | } 66 | }) 67 | 68 | it('should throw an error if status log handler not specified', () => { 69 | try { 70 | uut = new Pubsub({ ipfs }) 71 | 72 | assert.fail('Unexpected result') 73 | } catch (err) { 74 | assert.include( 75 | err.message, 76 | 'A status log handler function required when instantitating Pubsub Adapter' 77 | ) 78 | } 79 | }) 80 | 81 | it('should throw an error if encryption library is not included', () => { 82 | try { 83 | uut = new Pubsub({ ipfs, log }) 84 | 85 | assert.fail('Unexpected result') 86 | } catch (err) { 87 | assert.include( 88 | err.message, 89 | 'An instance of the encryption Adapter must be passed when instantiating the Pubsub Adapter library.' 90 | ) 91 | } 92 | }) 93 | 94 | it('should throw an error if privateLog is not included', () => { 95 | try { 96 | uut = new Pubsub({ ipfs, log, encryption }) 97 | 98 | assert.fail('Unexpected result') 99 | } catch (err) { 100 | assert.include( 101 | err.message, 102 | 'A private log handler must be passed when instantiating the Pubsub Adapter library.' 103 | ) 104 | } 105 | }) 106 | }) 107 | 108 | describe('#parsePubsubMessage', () => { 109 | it('should parse a pubsub message', async () => { 110 | const handler = () => { 111 | } 112 | 113 | const result = await uut.parsePubsubMessage(mockData.mockMsg, handler, thisNode) 114 | 115 | // assert.equal(true, true, 'Not throwing an error is a pass') 116 | assert.equal(result, true) 117 | }) 118 | 119 | it('should quietly exit if message is from thisNode', async () => { 120 | const handler = () => { 121 | } 122 | 123 | mockData.mockMsg.from = '12D3KooWE6tkdArVpCHG9QN61G1cE7eCq2Q7i4bNx6CJFTDprk9f' 124 | 125 | const result = await uut.parsePubsubMessage(mockData.mockMsg, handler, thisNode) 126 | 127 | // assert.equal(true, true, 'Not throwing an error is a pass') 128 | assert.equal(result, true) 129 | }) 130 | 131 | // This is a top-level function. It should not throw errors, but log 132 | // the error message. 133 | it('should catch and handle errors', async () => { 134 | const result = await uut.parsePubsubMessage() 135 | 136 | // assert.isOk(true, 'Not throwing an error is a pass') 137 | assert.equal(result, false) 138 | }) 139 | 140 | it('should parse a message for an external IPFS node', async () => { 141 | const handler = () => { 142 | } 143 | 144 | uut.nodeType = 'external' 145 | 146 | const result = await uut.parsePubsubMessage(mockData.mockMsg, handler, thisNode) 147 | 148 | // assert.equal(true, true, 'Not throwing an error is a pass') 149 | assert.equal(result, true) 150 | }) 151 | }) 152 | 153 | describe('#captureMetrics', () => { 154 | it('should capture an about REQUEST', async () => { 155 | // Mock dependencies 156 | sandbox.stub(uut.encryption, 'encryptMsg').resolves('encrypted-payload') 157 | sandbox.stub(uut.messaging, 'sendMsg').resolves() 158 | 159 | const decryptedStr = mockData.aboutRequest 160 | const from = 'fake-id' 161 | 162 | const result = await uut.captureMetrics(decryptedStr, from, thisNode) 163 | // console.log(result) 164 | 165 | assert.equal(result, true) 166 | }) 167 | 168 | it('should capture an about RESPONSE', async () => { 169 | const decryptedStr = mockData.aboutResponse 170 | const from = 'fake-id' 171 | 172 | const result = await uut.captureMetrics(decryptedStr, from, thisNode) 173 | // console.log(result) 174 | 175 | // Should return the decrypted response data. 176 | assert.property(result, 'ipfsId') 177 | }) 178 | 179 | it('should return false for message not an /about request or response', async () => { 180 | const decryptedStr = mockData.badId 181 | const from = 'fake-id' 182 | 183 | const result = await uut.captureMetrics(decryptedStr, from, thisNode) 184 | // console.log(result) 185 | 186 | assert.equal(result, false) 187 | }) 188 | 189 | it('should return false on an error', async () => { 190 | const result = await uut.captureMetrics() 191 | // console.log(result) 192 | 193 | assert.equal(result, false) 194 | }) 195 | }) 196 | 197 | describe('#handleNewMessage', () => { 198 | it('should return false if incoming message is an /about request or response', async () => { 199 | // Mock dependencies 200 | sandbox.stub(uut, 'captureMetrics').resolves(true) 201 | 202 | const result = await uut.handleNewMessage(mockData.msgObj, thisNode) 203 | // console.log(result) 204 | 205 | assert.equal(result, false) 206 | }) 207 | 208 | it('return true if incoming message is NOT an /about request or response', async () => { 209 | // Mock dependencies 210 | sandbox.stub(uut, 'captureMetrics').resolves(false) 211 | uut.privateLog = () => { 212 | } 213 | 214 | const result = await uut.handleNewMessage(mockData.msgObj, thisNode) 215 | // console.log(result) 216 | 217 | assert.equal(result, true) 218 | }) 219 | 220 | it('should catch and throw errors', async () => { 221 | try { 222 | await uut.handleNewMessage() 223 | 224 | assert.fail('Unexpected result') 225 | } catch (err) { 226 | // console.log(err) 227 | assert.include( 228 | err.message, 229 | 'Cannot read' 230 | ) 231 | } 232 | }) 233 | }) 234 | 235 | describe('#subscribeToPubsubChannel', () => { 236 | // This tests the ability to subscribe to general broadcast channels like 237 | // the psf-ipfs-coordination-002 channel. 238 | it('should subscribe to a broadcast pubsub channel', async () => { 239 | // Mock dependencies 240 | sandbox.stub(uut.ipfs.ipfs.pubsub, 'subscribe').resolves() 241 | 242 | const chanName = 'test' 243 | const handler = () => { 244 | } 245 | 246 | const result = await uut.subscribeToPubsubChannel(chanName, handler, thisNode) 247 | 248 | // assert.equal(true, true, 'Not throwing an error is a pass') 249 | assert.equal(result, true) 250 | }) 251 | 252 | // This tests the ability to subscribe to private, encrypted channels, like 253 | // the one this node uses to receive messages from other nodes. 254 | it('should subscribe to a private pubsub channel', async () => { 255 | // Mock dependencies 256 | sandbox.stub(uut.ipfs.ipfs.pubsub, 'subscribe').resolves() 257 | 258 | const chanName = thisNode.ipfsId 259 | const handler = () => { 260 | } 261 | 262 | const result = await uut.subscribeToPubsubChannel(chanName, handler, thisNode) 263 | 264 | // assert.equal(true, true, 'Not throwing an error is a pass') 265 | assert.equal(result, true) 266 | }) 267 | 268 | it('should catch and throw errors', async () => { 269 | try { 270 | await uut.subscribeToPubsubChannel() 271 | 272 | assert.fail('Unexpected code path') 273 | } catch (err) { 274 | // console.log('err: ', err) 275 | assert.include(err.message, 'Cannot read') 276 | } 277 | }) 278 | }) 279 | }) 280 | -------------------------------------------------------------------------------- /test/unit/adapters/schema.unit.adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the schema.js library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | // const cloneDeep = require('lodash.clonedeep') 9 | 10 | // local libraries 11 | const Schema = require('../../../lib/adapters/schema') 12 | 13 | describe('#schema', () => { 14 | let sandbox 15 | let uut 16 | 17 | beforeEach(() => { 18 | // Restore the sandbox before each test. 19 | sandbox = sinon.createSandbox() 20 | 21 | const schemaConfig = { 22 | ipfsId: 'myIpfsId', 23 | type: 'node.js', 24 | ipfsMultiaddrs: ['addr1', 'addr2'] 25 | } 26 | uut = new Schema(schemaConfig) 27 | }) 28 | 29 | afterEach(() => sandbox.restore()) 30 | 31 | describe('#announcement', () => { 32 | it('should return an announcement object', () => { 33 | const result = uut.announcement() 34 | // console.log(`result: ${JSON.stringify(result, null, 2)}`) 35 | 36 | // Assert that expected properties exist. 37 | assert.property(result, 'apiName') 38 | assert.property(result, 'apiVersion') 39 | assert.property(result, 'apiInfo') 40 | assert.property(result, 'ipfsId') 41 | assert.property(result, 'type') 42 | assert.property(result, 'ipfsMultiaddrs') 43 | assert.property(result, 'circuitRelays') 44 | assert.property(result, 'isCircuitRelay') 45 | assert.property(result, 'cryptoAddresses') 46 | assert.property(result, 'encryptPubKey') 47 | assert.property(result, 'orbitdb') 48 | 49 | // Assert that properties have the expected type. 50 | assert.isArray(result.ipfsMultiaddrs) 51 | assert.isArray(result.circuitRelays) 52 | assert.isArray(result.cryptoAddresses) 53 | 54 | // Assert expected values. 55 | assert.equal(result.isCircuitRelay, false) 56 | assert.equal(result.apiName, 'ipfs-coord-announce') 57 | assert.equal(result.type, 'node.js') 58 | }) 59 | 60 | it('should update orbitdbId in state when different', () => { 61 | const announceObj = { 62 | orbitdbId: '567' 63 | } 64 | 65 | const result = uut.announcement(announceObj) 66 | // console.log(`result: ${JSON.stringify(result, null, 2)}`) 67 | 68 | assert.property(result, 'orbitdb') 69 | assert.equal(result.orbitdb, '567') 70 | }) 71 | }) 72 | 73 | describe('#chat', () => { 74 | it('should return a chat object', () => { 75 | const msgObj = { 76 | message: 'Some arbitrary text.', 77 | handle: 'Testy Tester' 78 | } 79 | 80 | const result = uut.chat(msgObj) 81 | // console.log(`result: ${JSON.stringify(result, null, 2)}`) 82 | 83 | // Assert that expected properties exist. 84 | assert.property(result, 'apiName') 85 | assert.property(result, 'apiVersion') 86 | assert.property(result, 'apiInfo') 87 | assert.property(result, 'cryptoAddresses') 88 | assert.property(result, 'encryptPubKey') 89 | assert.property(result, 'data') 90 | assert.property(result.data, 'message') 91 | assert.property(result.data, 'handle') 92 | assert.property(result, 'ipfsId') 93 | 94 | // Assert that properties have the expected type. 95 | assert.isArray(result.cryptoAddresses) 96 | 97 | // Assert expected values. 98 | assert.equal(result.apiName, 'chat') 99 | assert.equal(result.type, 'node.js') 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/unit/controllers/index.unit.controllers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the main Controllers index.js file. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | 8 | // Local libraries 9 | const Controllers = require('../../../lib/controllers') 10 | 11 | describe('#index.js-Controllers', () => { 12 | let uut 13 | 14 | describe('#constructor', () => { 15 | it('should throw an error if adapters is not included', () => { 16 | try { 17 | uut = new Controllers() 18 | 19 | assert.fail('Unexpected code path') 20 | 21 | console.log(uut) 22 | } catch (err) { 23 | assert.include( 24 | err.message, 25 | 'Instance of adapters required when instantiating Controllers' 26 | ) 27 | } 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/unit/controllers/timer.unit.controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the main Controllers index.js file. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // Local libraries 10 | const TimerControllers = require('../../../lib/controllers/timer-controller') 11 | const AdapterMock = require('../../mocks/adapter-mock') 12 | const adapters = new AdapterMock() 13 | const UseCasesMock = require('../../mocks/use-case-mocks') 14 | const ThisNodeUseCases = require('../../../lib/use-cases/this-node-use-cases') 15 | 16 | describe('#timer-Controllers', () => { 17 | let uut 18 | let sandbox 19 | let useCases 20 | let thisNode 21 | 22 | beforeEach(async () => { 23 | // Restore the sandbox before each test. 24 | sandbox = sinon.createSandbox() 25 | 26 | uut = new TimerControllers({ 27 | adapters, 28 | statusLog: () => { 29 | } 30 | }) 31 | 32 | const thisNodeUseCases = new ThisNodeUseCases({ 33 | adapters, 34 | controllers: {}, 35 | statusLog: () => { 36 | } 37 | }) 38 | thisNode = await thisNodeUseCases.createSelf({ type: 'node.js' }) 39 | 40 | useCases = new UseCasesMock() 41 | }) 42 | 43 | afterEach(() => sandbox.restore()) 44 | 45 | after(() => { 46 | console.log('Stopping all timers') 47 | uut.stopAllTimers() 48 | }) 49 | 50 | describe('#constructor', () => { 51 | it('should throw an error if adapters is not included', () => { 52 | try { 53 | uut = new TimerControllers() 54 | } catch (err) { 55 | assert.include( 56 | err.message, 57 | 'Instance of adapters required when instantiating Timer Controllers' 58 | ) 59 | } 60 | }) 61 | 62 | it('should throw an error if status log handler is not included', () => { 63 | try { 64 | uut = new TimerControllers({ adapters }) 65 | } catch (err) { 66 | assert.include( 67 | err.message, 68 | 'Handler for status logs required when instantiating Timer Controllers' 69 | ) 70 | } 71 | }) 72 | }) 73 | 74 | describe('#startTimers', () => { 75 | it('should start the timers', () => { 76 | const result = uut.startTimers() 77 | 78 | assert.property(result, 'circuitRelayTimerHandle') 79 | assert.property(result, 'announceTimerHandle') 80 | assert.property(result, 'peerTimerHandle') 81 | 82 | // Clean up test by stopping the timers. 83 | clearInterval(result.circuitRelayTimerHandle) 84 | clearInterval(result.announceTimerHandle) 85 | clearInterval(result.peerTimerHandle) 86 | }) 87 | }) 88 | 89 | describe('#manageCircuitRelays', () => { 90 | it('should refresh connections with known circuit relays', async () => { 91 | await uut.manageCircuitRelays({}, useCases) 92 | 93 | assert.isOk(true, 'Not throwing an error is a pass') 94 | }) 95 | 96 | it('should give status update if debugLevel is true', async () => { 97 | uut.debugLevel = 1 98 | 99 | await uut.manageCircuitRelays({}, useCases) 100 | 101 | assert.isOk(true, 'Not throwing an error is a pass') 102 | }) 103 | 104 | it('should catch and report an error', async () => { 105 | // Force an error 106 | sandbox 107 | .stub(useCases.relays, 'connectToCRs') 108 | .rejects(new Error('test error')) 109 | 110 | await uut.manageCircuitRelays(thisNode, useCases) 111 | 112 | assert.isOk(true, 'Not throwing an error is a pass') 113 | }) 114 | }) 115 | 116 | describe('#manageAnnouncement', () => { 117 | it('should publish an announcement to the general coordination pubsub channel', async () => { 118 | const result = await uut.manageAnnouncement(thisNode, useCases) 119 | 120 | assert.equal(result, true) 121 | }) 122 | 123 | it('should give status update if debugLevel is true', async () => { 124 | uut.debugLevel = 1 125 | 126 | const result = await uut.manageAnnouncement(thisNode, useCases) 127 | 128 | assert.equal(result, true) 129 | }) 130 | 131 | it('should catch and report an error', async () => { 132 | // Force an error 133 | sandbox 134 | .stub(thisNode.schema, 'announcement') 135 | .throws(new Error('test error')) 136 | 137 | await uut.manageAnnouncement(thisNode, useCases) 138 | 139 | assert.isOk(true, 'Not throwing an error is a pass') 140 | }) 141 | }) 142 | 143 | describe('#managePeers', () => { 144 | it('should refresh connections to peers', async () => { 145 | const result = await uut.managePeers(thisNode, useCases) 146 | 147 | assert.equal(result, true) 148 | }) 149 | 150 | it('should give status update if debugLevel is true', async () => { 151 | uut.debugLevel = 1 152 | 153 | const result = await uut.managePeers(thisNode, useCases) 154 | 155 | assert.equal(result, true) 156 | }) 157 | 158 | it('should catch and report an error', async () => { 159 | // Force an error 160 | sandbox 161 | .stub(useCases.thisNode, 'refreshPeerConnections') 162 | .throws(new Error('test error')) 163 | 164 | await uut.managePeers(thisNode, useCases) 165 | 166 | assert.isOk(true, 'Not throwing an error is a pass') 167 | }) 168 | }) 169 | 170 | describe('#searchForRelays', () => { 171 | it('should find and relay-potential peers that are not in the relayData array', async () => { 172 | // Mock test data 173 | const thisNode = { 174 | relayData: [{ ipfsId: 'id1' }], 175 | peerData: [{ from: 'id2', data: { isCircuitRelay: true } }] 176 | } 177 | 178 | await uut.searchForRelays(thisNode, useCases) 179 | 180 | // Cleanup test by disabling the interval 181 | clearInterval(uut.relaySearch) 182 | 183 | assert.isOk(true, 'Not throwing an error is a pass') 184 | }) 185 | 186 | it('should report errors but not throw them', async () => { 187 | await uut.searchForRelays() 188 | 189 | assert.isOk(true, 'Not throwing an error is a pass') 190 | }) 191 | }) 192 | 193 | describe('#blacklist', () => { 194 | it('should return true after executing the use case', async () => { 195 | const result = await uut.blacklist(thisNode, useCases) 196 | 197 | assert.equal(result, true) 198 | }) 199 | 200 | it('should return false on error', async () => { 201 | // Force an error 202 | // sandbox 203 | // .stub(useCases.thisNode, 'enforceBlacklist') 204 | // .rejects(new Error('test error')) 205 | sandbox 206 | .stub(useCases.thisNode, 'enforceWhitelist') 207 | .rejects(new Error('test error')) 208 | 209 | const result = await uut.blacklist(thisNode, useCases) 210 | 211 | assert.equal(result, false) 212 | }) 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /test/unit/entities/this-node.unit.entity.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the thisNode Entity 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // local libraries 10 | const ThisNodeEntity = require('../../../lib/entities/this-node-entity') 11 | 12 | describe('#thisNode-Entity', () => { 13 | let sandbox 14 | let uut // Unit Under Test 15 | 16 | beforeEach(() => { 17 | // Restore the sandbox before each test. 18 | sandbox = sinon.createSandbox() 19 | 20 | // const bchjs = new BCHJS() 21 | // uut = new IpfsCoord({ bchjs, ipfs, type: 'node.js' }) 22 | }) 23 | 24 | afterEach(() => sandbox.restore()) 25 | 26 | describe('#constructor', () => { 27 | it('should throw an error if ipfsId is not included', () => { 28 | try { 29 | const configObj = {} 30 | 31 | uut = new ThisNodeEntity(configObj) 32 | 33 | assert.fail('Unexpected code path') 34 | } catch (err) { 35 | assert.include( 36 | err.message, 37 | 'ipfsId required when instantiating thisNode Entity' 38 | ) 39 | } 40 | }) 41 | 42 | it('should throw an error if multiaddrs are not included', () => { 43 | try { 44 | const configObj = { 45 | ipfsId: 'fake-ipfsId' 46 | } 47 | 48 | uut = new ThisNodeEntity(configObj) 49 | 50 | assert.fail('Unexpected code path') 51 | } catch (err) { 52 | assert.include( 53 | err.message, 54 | 'ipfsMultiaddrs required when instantiating thisNode Entity' 55 | ) 56 | } 57 | }) 58 | 59 | it('should throw an error if bchAddr is not included', () => { 60 | try { 61 | const configObj = { 62 | ipfsId: 'fake-ipfsId', 63 | ipfsMultiaddrs: ['fake-addr'] 64 | } 65 | 66 | uut = new ThisNodeEntity(configObj) 67 | 68 | assert.fail('Unexpected code path') 69 | } catch (err) { 70 | assert.include( 71 | err.message, 72 | 'bchAddr required when instantiating thisNode Entity' 73 | ) 74 | } 75 | }) 76 | 77 | it('should throw an error if slpAddr is not included', () => { 78 | try { 79 | const configObj = { 80 | ipfsId: 'fake-ipfsId', 81 | ipfsMultiaddrs: ['fake-addr'], 82 | bchAddr: 'fake-addr' 83 | } 84 | 85 | uut = new ThisNodeEntity(configObj) 86 | 87 | assert.fail('Unexpected code path') 88 | } catch (err) { 89 | assert.include( 90 | err.message, 91 | 'slpAddr required when instantiating thisNode Entity' 92 | ) 93 | } 94 | }) 95 | 96 | it('should throw an error if publicKey is not included', () => { 97 | try { 98 | const configObj = { 99 | ipfsId: 'fake-ipfsId', 100 | ipfsMultiaddrs: ['fake-addr'], 101 | bchAddr: 'fake-addr', 102 | slpAddr: 'fake-addr' 103 | } 104 | 105 | uut = new ThisNodeEntity(configObj) 106 | 107 | assert.fail('Unexpected code path') 108 | } catch (err) { 109 | assert.include( 110 | err.message, 111 | 'publicKey required when instantiating thisNode Entity' 112 | ) 113 | } 114 | }) 115 | 116 | it('should throw an error if node type is not included', () => { 117 | try { 118 | const configObj = { 119 | ipfsId: 'fake-ipfsId', 120 | ipfsMultiaddrs: ['fake-addr'], 121 | bchAddr: 'fake-addr', 122 | slpAddr: 'fake-addr', 123 | publicKey: 'fake-key' 124 | } 125 | 126 | uut = new ThisNodeEntity(configObj) 127 | 128 | assert.fail('Unexpected code path') 129 | } catch (err) { 130 | assert.include( 131 | err.message, 132 | "Node type of 'node.js' or 'browser' required when instantiating thisNode Entity" 133 | ) 134 | } 135 | }) 136 | 137 | it('should create a thisNode Entity', () => { 138 | const configObj = { 139 | ipfsId: 'fake-ipfsId', 140 | ipfsMultiaddrs: ['fake-addr'], 141 | bchAddr: 'fake-addr', 142 | slpAddr: 'fake-addr', 143 | publicKey: 'fake-key', 144 | type: 'node.js' 145 | } 146 | 147 | uut = new ThisNodeEntity(configObj) 148 | 149 | assert.property(uut, 'ipfsId') 150 | assert.property(uut, 'ipfsMultiaddrs') 151 | assert.property(uut, 'bchAddr') 152 | assert.property(uut, 'slpAddr') 153 | assert.property(uut, 'publicKey') 154 | assert.property(uut, 'type') 155 | }) 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /test/unit/index.unit.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the main index.js file. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | const BCHJS = require('@psf/bch-js') 9 | 10 | // local libraries 11 | const IpfsCoord = require('../../index') 12 | const ipfs = require('../mocks/ipfs-mock') 13 | 14 | describe('#ipfs-coord', () => { 15 | let sandbox 16 | let uut // Unit Under Test 17 | 18 | beforeEach(() => { 19 | // Restore the sandbox before each test. 20 | sandbox = sinon.createSandbox() 21 | 22 | const bchjs = new BCHJS() 23 | uut = new IpfsCoord({ bchjs, ipfs, type: 'node.js' }) 24 | }) 25 | 26 | afterEach(() => sandbox.restore()) 27 | 28 | describe('#constructor', () => { 29 | it('should throw an error if ipfs instance is not passed as input', () => { 30 | try { 31 | uut = new IpfsCoord({}) 32 | 33 | assert.fail('Unexpected code path') 34 | } catch (err) { 35 | assert.include( 36 | err.message, 37 | 'An instance of IPFS must be passed when instantiating the ipfs-coord library.' 38 | ) 39 | } 40 | }) 41 | 42 | it('should throw an error if bch-js instance is not passed as input', () => { 43 | try { 44 | uut = new IpfsCoord({ ipfs }) 45 | 46 | assert.fail('Unexpected code path') 47 | } catch (err) { 48 | assert.include( 49 | err.message, 50 | 'An instance of @psf/bch-js must be passed when instantiating the ipfs-coord library.' 51 | ) 52 | } 53 | }) 54 | 55 | it('should throw an error if node type is not defined', () => { 56 | try { 57 | const bchjs = new BCHJS() 58 | uut = new IpfsCoord({ ipfs, bchjs }) 59 | 60 | assert.fail('Unexpected code path') 61 | } catch (err) { 62 | assert.include( 63 | err.message, 64 | 'The type of IPFS node (browser or node.js) must be specified.' 65 | ) 66 | } 67 | }) 68 | 69 | it('should override default logs', () => { 70 | const bchjs = new BCHJS() 71 | uut = new IpfsCoord({ 72 | bchjs, 73 | ipfs, 74 | type: 'node.js', 75 | statusLog: console.log, 76 | privateLog: console.log 77 | }) 78 | }) 79 | 80 | it('should set debugLevel to 0 if not specified', () => { 81 | const bchjs = new BCHJS() 82 | uut = new IpfsCoord({ 83 | bchjs, 84 | ipfs, 85 | type: 'node.js', 86 | statusLog: console.log, 87 | privateLog: console.log 88 | }) 89 | 90 | assert.equal(uut.debugLevel, 0) 91 | }) 92 | 93 | it('should set debugLevel to 2 if specified', () => { 94 | const bchjs = new BCHJS() 95 | uut = new IpfsCoord({ 96 | bchjs, 97 | ipfs, 98 | type: 'node.js', 99 | statusLog: console.log, 100 | privateLog: console.log, 101 | debugLevel: 2 102 | }) 103 | 104 | assert.equal(uut.debugLevel, 2) 105 | }) 106 | 107 | it('should default debugLevel to 0 non-integer is used', () => { 108 | const bchjs = new BCHJS() 109 | uut = new IpfsCoord({ 110 | bchjs, 111 | ipfs, 112 | type: 'node.js', 113 | statusLog: console.log, 114 | privateLog: console.log, 115 | debugLevel: 'abcd' 116 | }) 117 | 118 | assert.equal(uut.debugLevel, 0) 119 | }) 120 | }) 121 | 122 | describe('#start', () => { 123 | it('should return true after ipfs-coord dependencies have been started.', async () => { 124 | // Mock the dependencies. 125 | sandbox.stub(uut.adapters.ipfs, 'start').resolves({}) 126 | sandbox.stub(uut.useCases.thisNode, 'createSelf').resolves({}) 127 | sandbox.stub(uut.useCases.relays, 'initializeRelays').resolves({}) 128 | sandbox.stub(uut.useCases.pubsub, 'initializePubsub').resolves({}) 129 | sandbox.stub(uut.controllers.timer, 'startTimers').resolves({}) 130 | sandbox.stub(uut, '_initializeConnections').resolves({}) 131 | 132 | const result = await uut.start() 133 | 134 | assert.equal(result, true) 135 | }) 136 | }) 137 | 138 | describe('#_initializeConnections', () => { 139 | it('should kick-off initial connections', async () => { 140 | // Mock dependencies 141 | sandbox.stub(uut.useCases.relays, 'initializeRelays').resolves() 142 | sandbox.stub(uut.useCases.relays, 'getCRGist').resolves() 143 | sandbox.stub(uut.useCases.thisNode, 'refreshPeerConnections').resolves() 144 | 145 | const result = await uut._initializeConnections() 146 | 147 | assert.equal(result, true) 148 | }) 149 | 150 | it('should return falses on error', async () => { 151 | // Force and error 152 | sandbox 153 | .stub(uut.useCases.relays, 'initializeRelays') 154 | .rejects(new Error('test error')) 155 | 156 | const result = await uut._initializeConnections() 157 | 158 | assert.equal(result, false) 159 | }) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /test/unit/use-cases/index.unit.use-cases.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for Use Cases index.js file 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | 8 | // Local libraries 9 | const UseCases = require('../../../lib/use-cases') 10 | 11 | describe('#index.js-Use-Cases', () => { 12 | let uut 13 | 14 | describe('#constructor', () => { 15 | it('should throw an error if adapters is not included', () => { 16 | try { 17 | uut = new UseCases() 18 | 19 | assert.fail('Unexpected code path') 20 | } catch (err) { 21 | assert.include( 22 | err.message, 23 | 'Must inject instance of adapters when instantiating Use Cases library.' 24 | ) 25 | } 26 | }) 27 | 28 | it('should throw an error if controllers are not included', () => { 29 | try { 30 | uut = new UseCases({ adapters: {} }) 31 | 32 | assert.fail('Unexpected code path') 33 | } catch (err) { 34 | assert.include( 35 | err.message, 36 | 'Must inject instance of controllers when instantiating Use Cases library.' 37 | ) 38 | } 39 | }) 40 | 41 | it('should instantiate the use cases library', () => { 42 | uut = new UseCases({ adapters: {}, controllers: {}, statusLog: {} }) 43 | 44 | assert.property(uut, 'adapters') 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/unit/use-cases/peer.unit.use-cases.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the Peer Use Case library. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // Local libraries 10 | const PeerUseCases = require('../../../lib/use-cases/peer-use-cases') 11 | // const RelayUseCases = require('../../../lib/use-cases/relay-use-cases') 12 | const ThisNodeUseCases = require('../../../lib/use-cases/this-node-use-cases') 13 | const AdapterMock = require('../../mocks/adapter-mock') 14 | const adapters = new AdapterMock() 15 | // const mockData = require('../../mocks/peers-mock') 16 | 17 | describe('#Peer-Use-Cases', () => { 18 | let uut 19 | let sandbox 20 | let thisNode 21 | 22 | beforeEach(async () => { 23 | // Restore the sandbox before each test. 24 | sandbox = sinon.createSandbox() 25 | 26 | const thisNodeUseCases = new ThisNodeUseCases({ 27 | adapters, 28 | controllers: {}, 29 | statusLog: () => { 30 | } 31 | }) 32 | thisNode = await thisNodeUseCases.createSelf({ type: 'node.js' }) 33 | // 34 | // uut = new RelayUseCases({ 35 | // adapters, 36 | // controllers: {}, 37 | // statusLog: () => {} 38 | // }) 39 | 40 | uut = new PeerUseCases({ adapters, controllers: {} }) 41 | }) 42 | 43 | afterEach(() => sandbox.restore()) 44 | 45 | describe('#constructor', () => { 46 | it('should throw an error if adapters is not included', () => { 47 | try { 48 | uut = new PeerUseCases() 49 | 50 | assert.fail('Unexpected code path') 51 | } catch (err) { 52 | assert.include( 53 | err.message, 54 | 'Must inject instance of adapters when instantiating Peer Use Cases library.' 55 | ) 56 | } 57 | }) 58 | 59 | it('should throw an error if controllers are not included', () => { 60 | try { 61 | uut = new PeerUseCases({ adapters: {} }) 62 | 63 | assert.fail('Unexpected code path') 64 | } catch (err) { 65 | assert.include( 66 | err.message, 67 | 'Must inject instance of controllers when instantiating Peer Use Cases library.' 68 | ) 69 | } 70 | }) 71 | }) 72 | 73 | describe('#sendPrivateMessage', () => { 74 | it('should throw an error if peer data can not be found', async () => { 75 | try { 76 | const result = await uut.sendPrivateMessage( 77 | 'fakeId', 78 | 'messageStr', 79 | thisNode 80 | ) 81 | console.log('result: ', result) 82 | } catch (err) { 83 | assert.include(err.message, 'Data for peer') 84 | } 85 | }) 86 | 87 | it('should encrypt a message and add it to the peers OrbitDB', async () => { 88 | thisNode.peerData.push({ from: 'fakeId' }) 89 | // thisNode.orbitData.push({ 90 | // ipfsId: 'fakeId', 91 | // db: { 92 | // add: () => { 93 | // } 94 | // } 95 | // }) 96 | 97 | // Mock dependencies 98 | // sandbox.stub(uut.adapters.encryption, 'encryptMsg') 99 | sandbox.stub(uut, 'connectToPeer').resolves(true) 100 | 101 | const result = await uut.sendPrivateMessage( 102 | 'fakeId', 103 | 'messageStr', 104 | thisNode 105 | ) 106 | // console.log('result: ', result) 107 | 108 | assert.equal(result, true) 109 | }) 110 | }) 111 | 112 | describe('#connectToPeer', () => { 113 | it('should skip if peer is already connected', async () => { 114 | // Test data 115 | const peerId = 'QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsjd' 116 | thisNode.peerList = [peerId] 117 | 118 | // Mock dependencies 119 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves([{ peer: peerId }]) 120 | 121 | // Connect to that peer 122 | const result = await uut.connectToPeer(peerId, thisNode) 123 | 124 | assert.equal(result, true) 125 | }) 126 | 127 | it('should connect to peer through circuit relay', async () => { 128 | // Test data 129 | const peerId = 'QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsjd' 130 | thisNode.peerList = [peerId] 131 | thisNode.relayData = [ 132 | { 133 | multiaddr: '/ip4/139.162.76.54/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', 134 | connected: true, 135 | updatedAt: '2021-09-20T15:59:12.961Z', 136 | ipfsId: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', 137 | isBootstrap: false, 138 | metrics: { aboutLatency: [] }, 139 | latencyScore: 10000 140 | } 141 | ] 142 | 143 | // Mock dependencies 144 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves([]) 145 | sandbox.stub(uut.adapters.ipfs, 'connectToPeer').resolves(true) 146 | thisNode.useCases = { 147 | relays: { 148 | sortRelays: () => thisNode.relayData 149 | } 150 | } 151 | 152 | // Connect to that peer 153 | const result = await uut.connectToPeer(peerId, thisNode) 154 | 155 | assert.equal(result, true) 156 | }) 157 | 158 | it('should return false if not able to connect to peer', async () => { 159 | // Test data 160 | const peerId = 'QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsjd' 161 | thisNode.peerList = [peerId] 162 | thisNode.relayData = [ 163 | { 164 | multiaddr: '/ip4/139.162.76.54/tcp/5269/ws/p2p/QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', 165 | connected: true, 166 | updatedAt: '2021-09-20T15:59:12.961Z', 167 | ipfsId: 'QmaKzQTAtoJWYMiG5ATx41uWsMajr1kSxRdtg919s8fK77', 168 | isBootstrap: false, 169 | metrics: { aboutLatency: [] }, 170 | latencyScore: 10000 171 | } 172 | ] 173 | 174 | // Mock dependencies 175 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves([]) 176 | sandbox.stub(uut.adapters.ipfs, 'connectToPeer').resolves(false) 177 | thisNode.useCases = { 178 | relays: { 179 | sortRelays: () => thisNode.relayData 180 | } 181 | } 182 | 183 | // Connect to that peer 184 | const result = await uut.connectToPeer(peerId, thisNode) 185 | 186 | assert.equal(result, false) 187 | }) 188 | 189 | it('should catch and throw errors', async () => { 190 | try { 191 | await uut.connectToPeer() 192 | 193 | assert.fail('Unexpected code path') 194 | } catch (err) { 195 | // console.log(err) 196 | assert.include(err.message, 'Cannot read') 197 | } 198 | }) 199 | }) 200 | }) 201 | -------------------------------------------------------------------------------- /test/unit/use-cases/pubsub.unit.use-cases.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the Pubsub use case. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // Local libraries 10 | const PubsubUseCases = require('../../../lib/use-cases/pubsub-use-cases') 11 | const ThisNodeUseCases = require('../../../lib/use-cases/this-node-use-cases') 12 | const AdapterMock = require('../../mocks/adapter-mock') 13 | const adapters = new AdapterMock() 14 | // const mockData = require('../../mocks/peers-mock') 15 | 16 | describe('#pubsub-Use-Cases', () => { 17 | let uut 18 | let sandbox 19 | 20 | beforeEach(() => { 21 | // Restore the sandbox before each test. 22 | sandbox = sinon.createSandbox() 23 | 24 | const thisNodeUseCases = new ThisNodeUseCases({ 25 | adapters, 26 | controllers: {}, 27 | statusLog: () => {} 28 | }) 29 | 30 | uut = new PubsubUseCases({ 31 | adapters, 32 | controllers: {}, 33 | thisNodeUseCases 34 | }) 35 | }) 36 | 37 | afterEach(() => sandbox.restore()) 38 | 39 | describe('#constructor', () => { 40 | it('should throw an error if adapters is not included', () => { 41 | try { 42 | uut = new PubsubUseCases() 43 | 44 | assert.fail('Unexpected code path') 45 | } catch (err) { 46 | assert.include( 47 | err.message, 48 | 'Must inject instance of adapters when instantiating Pubsub Use Cases library.' 49 | ) 50 | } 51 | }) 52 | 53 | it('should throw an error if controllers are not included', () => { 54 | try { 55 | uut = new PubsubUseCases({ adapters: {} }) 56 | 57 | assert.fail('Unexpected code path') 58 | } catch (err) { 59 | assert.include( 60 | err.message, 61 | 'Must inject instance of controllers when instantiating Pubsub Use Cases library.' 62 | ) 63 | } 64 | }) 65 | 66 | it('should throw an error if thisNodeUseCases instance is not included', () => { 67 | try { 68 | uut = new PubsubUseCases({ 69 | adapters: {}, 70 | controllers: {} 71 | }) 72 | 73 | assert.fail('Unexpected code path') 74 | } catch (err) { 75 | assert.include( 76 | err.message, 77 | 'thisNode use cases required when instantiating Pubsub Use Cases library.' 78 | ) 79 | } 80 | }) 81 | }) 82 | 83 | describe('#initializePubsub', () => { 84 | it('should subscribe to a node', async () => { 85 | await uut.initializePubsub('fakeNode') 86 | 87 | assert.isOk(true, 'No throwing an error is a pass') 88 | }) 89 | 90 | it('should catch and throw an error', async () => { 91 | try { 92 | // Force an error 93 | sandbox 94 | .stub(uut.adapters.pubsub, 'subscribeToPubsubChannel') 95 | .rejects(new Error('test error')) 96 | 97 | await uut.initializePubsub('fakeNode') 98 | 99 | assert.fail('Unexpected code path') 100 | } catch (err) { 101 | // console.log(err) 102 | assert.include(err.message, 'test error') 103 | } 104 | }) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /test/unit/use-cases/this-node.unit.use-cases.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unit tests for the this-node use case. 3 | */ 4 | 5 | // npm libraries 6 | const assert = require('chai').assert 7 | const sinon = require('sinon') 8 | 9 | // Local libraries 10 | const ThisNodeUseCases = require('../../../lib/use-cases/this-node-use-cases') 11 | const AdapterMock = require('../../mocks/adapter-mock') 12 | const adapters = new AdapterMock() 13 | const mockData = require('../../mocks/peers-mock') 14 | const UseCasesMock = require('../../mocks/use-case-mocks') 15 | 16 | describe('#thisNode-Use-Cases', () => { 17 | let uut 18 | let sandbox 19 | 20 | beforeEach(() => { 21 | // Restore the sandbox before each test. 22 | sandbox = sinon.createSandbox() 23 | 24 | uut = new ThisNodeUseCases({ 25 | adapters, 26 | controllers: {} 27 | }) 28 | 29 | const useCases = new UseCasesMock() 30 | uut.updateUseCases(useCases) 31 | }) 32 | 33 | afterEach(() => sandbox.restore()) 34 | 35 | describe('#constructor', () => { 36 | it('should throw an error if adapters is not included', () => { 37 | try { 38 | uut = new ThisNodeUseCases() 39 | 40 | assert.fail('Unexpected code path') 41 | } catch (err) { 42 | assert.include( 43 | err.message, 44 | 'Must inject instance of adapters when instantiating thisNode Use Cases library.' 45 | ) 46 | } 47 | }) 48 | 49 | it('should throw an error if controllers are not included', () => { 50 | try { 51 | uut = new ThisNodeUseCases({ adapters: {} }) 52 | 53 | assert.fail('Unexpected code path') 54 | } catch (err) { 55 | assert.include( 56 | err.message, 57 | 'Must inject instance of controllers when instantiating thisNode Use Cases library.' 58 | ) 59 | } 60 | }) 61 | 62 | it('should instantiate the use cases library', () => { 63 | uut = new ThisNodeUseCases({ 64 | adapters: {}, 65 | controllers: {} 66 | }) 67 | 68 | assert.property(uut, 'adapters') 69 | }) 70 | }) 71 | 72 | describe('#createSelf', () => { 73 | it('should create a thisNode entity', async () => { 74 | uut = new ThisNodeUseCases({ adapters, controllers: {}, statusLog: {} }) 75 | 76 | const result = await uut.createSelf({ type: 'node.js' }) 77 | // console.log('result: ', result) 78 | 79 | assert.property(result, 'ipfsId') 80 | assert.property(result, 'type') 81 | }) 82 | }) 83 | 84 | describe('#addSubnetPeer', () => { 85 | it('should track a new peer', async () => { 86 | // Mock dependencies 87 | sandbox.stub(uut, 'isFreshPeer').returns(true) 88 | 89 | const announceObj = { 90 | from: 'peerId', 91 | data: {} 92 | } 93 | 94 | await uut.createSelf({ type: 'node.js' }) 95 | const result = await uut.addSubnetPeer(announceObj) 96 | // console.log('result: ', result) 97 | 98 | assert.equal(result, true) 99 | }) 100 | 101 | it('should track a new Relay peer', async () => { 102 | // Mock dependencies 103 | sandbox.stub(uut, 'isFreshPeer').returns(true) 104 | sandbox.stub(uut.useCases.relays, 'addRelay').resolves() 105 | 106 | const announceObj = { 107 | from: 'peerId', 108 | data: { 109 | isCircuitRelay: true 110 | } 111 | } 112 | 113 | await uut.createSelf({ type: 'node.js' }) 114 | const result = await uut.addSubnetPeer(announceObj) 115 | // console.log('result: ', result) 116 | 117 | assert.equal(result, true) 118 | }) 119 | 120 | it('should update an existing peer', async () => { 121 | // Mock dependencies 122 | sandbox.stub(uut, 'isFreshPeer').returns(true) 123 | 124 | const announceObj = { 125 | from: 'peerId', 126 | data: { 127 | orbitdb: 'orbitdbId' 128 | } 129 | } 130 | 131 | await uut.createSelf({ type: 'node.js' }) 132 | 133 | // Add the new peer 134 | await uut.addSubnetPeer(announceObj) 135 | 136 | // Simulate a second announcement object. 137 | const result = await uut.addSubnetPeer(announceObj) 138 | // console.log('result: ', result) 139 | 140 | assert.equal(result, true) 141 | 142 | // peerData array should only have one peer. 143 | assert.equal(uut.thisNode.peerData.length, 1) 144 | }) 145 | 146 | // TODO: Create new test case: 147 | // it('should not update an existing peer if broadcast message is older the current one')' 148 | 149 | it('should catch and report an error', async () => { 150 | try { 151 | const announceObj = { 152 | from: 'peerId' 153 | } 154 | 155 | await uut.addSubnetPeer(announceObj) 156 | 157 | assert.isOk(true, 'Not throwing an error is a pass') 158 | } catch (err) { 159 | // console.log(err) 160 | assert.fail('Unexpected code path') 161 | } 162 | }) 163 | }) 164 | 165 | describe('#refreshPeerConnections', () => { 166 | it('should execute with no connected peers', async () => { 167 | await uut.createSelf({ type: 'node.js' }) 168 | 169 | // Add a peer 170 | await uut.addSubnetPeer(mockData.announceObj) 171 | 172 | // Mock dependencies 173 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves(mockData.swarmPeers) 174 | 175 | // Connect to that peer. 176 | await uut.refreshPeerConnections() 177 | }) 178 | 179 | it('should skip if peer is already connected', async () => { 180 | await uut.createSelf({ type: 'node.js' }) 181 | // Add a peer that is already in the list of connected peers. 182 | uut.thisNode.peerList = ['QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsjd'] 183 | 184 | // Add a peer 185 | await uut.addSubnetPeer(mockData.announceObj2) 186 | 187 | // Mock dependencies 188 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves(mockData.swarmPeers) 189 | 190 | // Connect to that peer. 191 | const result = await uut.refreshPeerConnections() 192 | 193 | assert.equal(result, true) 194 | }) 195 | 196 | it('should refresh a connection', async () => { 197 | await uut.createSelf({ type: 'node.js' }) 198 | // Add a peer that is not in the list of connected peers. 199 | const ipfsId = 'QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsje' 200 | uut.thisNode.peerList = [ipfsId] 201 | uut.thisNode.peerData = [{ from: ipfsId }] 202 | 203 | // Add a peer 204 | await uut.addSubnetPeer(mockData.announceObj) 205 | 206 | // Force circuit relay to be used. 207 | uut.thisNode.relayData = mockData.mockRelayData 208 | 209 | // Mock dependencies 210 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves(mockData.swarmPeers) 211 | sandbox.stub(uut.adapters.ipfs, 'connectToPeer').resolves(true) 212 | sandbox.stub(uut, 'isFreshPeer').returns(true) 213 | 214 | // Connect to that peer. 215 | const result = await uut.refreshPeerConnections() 216 | 217 | assert.equal(result, true) 218 | }) 219 | 220 | it('should skip if peer is stale', async () => { 221 | await uut.createSelf({ type: 'node.js' }) 222 | // Add a peer that is not in the list of connected peers. 223 | const ipfsId = 'QmbyYXKbnAmMbMGo8LRBZ58jYs58anqUzY1m4jxDmhDsje' 224 | uut.thisNode.peerList = [ipfsId] 225 | uut.thisNode.peerData = [{ from: ipfsId }] 226 | 227 | // Add a peer 228 | await uut.addSubnetPeer(mockData.announceObj) 229 | 230 | // Force circuit relay to be used. 231 | uut.thisNode.relayData = mockData.mockRelayData 232 | 233 | // Mock dependencies 234 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves(mockData.swarmPeers) 235 | sandbox.stub(uut.adapters.ipfs, 'connectToPeer').resolves(true) 236 | sandbox.stub(uut, 'isFreshPeer').returns(false) 237 | 238 | // Connect to that peer. 239 | const result = await uut.refreshPeerConnections() 240 | 241 | assert.equal(result, true) 242 | }) 243 | 244 | it('should catch and throw an error', async () => { 245 | try { 246 | await uut.createSelf({ type: 'node.js' }) 247 | 248 | // Add a peer 249 | await uut.addSubnetPeer(mockData.announceObj) 250 | 251 | // Force error 252 | sandbox 253 | .stub(uut.adapters.ipfs, 'getPeers') 254 | .rejects(new Error('test error')) 255 | 256 | // Connect to that peer. 257 | await uut.refreshPeerConnections() 258 | 259 | assert.fail('Unexpected code path') 260 | } catch (err) { 261 | // console.log('err: ', err) 262 | assert.include(err.message, 'test error') 263 | } 264 | }) 265 | }) 266 | 267 | describe('#isFreshPeer', () => { 268 | it('should return false if peer data has no broadcastedAt property', () => { 269 | const announceObj = { 270 | data: {} 271 | } 272 | 273 | const result = uut.isFreshPeer(announceObj) 274 | 275 | assert.equal(result, false) 276 | }) 277 | 278 | it('should return false if broadcast is older than 10 minutes', () => { 279 | const now = new Date() 280 | const fifteenMinutes = 15 * 60000 281 | let fifteenMinutesAgo = now.getTime() - fifteenMinutes 282 | fifteenMinutesAgo = new Date(fifteenMinutesAgo) 283 | 284 | const announceObj = { 285 | data: { 286 | broadcastedAt: fifteenMinutesAgo.toISOString() 287 | } 288 | } 289 | 290 | const result = uut.isFreshPeer(announceObj) 291 | 292 | assert.equal(result, false) 293 | }) 294 | 295 | it('should return true if broadcast is newer than 10 minutes', () => { 296 | const now = new Date() 297 | const fiveMinutes = 5 * 60000 298 | let fiveMinutesAgo = now.getTime() - fiveMinutes 299 | fiveMinutesAgo = new Date(fiveMinutesAgo) 300 | 301 | const announceObj = { 302 | data: { 303 | broadcastedAt: fiveMinutesAgo.toISOString() 304 | } 305 | } 306 | 307 | const result = uut.isFreshPeer(announceObj) 308 | 309 | assert.equal(result, true) 310 | }) 311 | }) 312 | 313 | describe('#enforceBlacklist', () => { 314 | it('should disconnect from blacklisted peers', async () => { 315 | await uut.createSelf({ type: 'node.js' }) 316 | 317 | // Set up test data 318 | uut.thisNode.blacklistPeers = ['testId'] 319 | uut.thisNode.blacklistMultiaddrs = ['testId'] 320 | 321 | const result = await uut.enforceBlacklist() 322 | 323 | assert.equal(result, true) 324 | }) 325 | 326 | it('catch and throw an error', async () => { 327 | try { 328 | await uut.enforceBlacklist() 329 | 330 | assert.fail('Unexpected code path') 331 | } catch (err) { 332 | // console.log(err) 333 | assert.include(err.message, 'Cannot read') 334 | } 335 | }) 336 | }) 337 | 338 | describe('#enforceWhitelist', () => { 339 | it('should disconnect from non-ipfs-coord peers', async () => { 340 | await uut.createSelf({ type: 'node.js' }) 341 | 342 | // Mock dependencies 343 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves([{ peer: 'badId' }]) 344 | const spy1 = sandbox 345 | .stub(uut.adapters.ipfs, 'disconnectFromPeer') 346 | .resolves() 347 | 348 | const result = await uut.enforceWhitelist() 349 | 350 | // Assert that the method completed. 351 | assert.equal(result, true) 352 | 353 | // Assert that disconnectFromPeer() was called. 354 | assert.equal(spy1.called, true) 355 | }) 356 | 357 | it('should skip ipfs-coord peers', async () => { 358 | await uut.createSelf({ type: 'node.js' }) 359 | uut.thisNode.peerData = [ 360 | { 361 | from: 'goodId', 362 | data: { 363 | jsonLd: { 364 | name: 'good-name' 365 | } 366 | } 367 | } 368 | ] 369 | 370 | // Mock dependencies 371 | sandbox.stub(uut.adapters.ipfs, 'getPeers').resolves([{ peer: 'goodId' }]) 372 | const spy1 = sandbox 373 | .stub(uut.adapters.ipfs, 'disconnectFromPeer') 374 | .resolves() 375 | 376 | const result = await uut.enforceWhitelist() 377 | 378 | // Assert that the method completed. 379 | assert.equal(result, true) 380 | 381 | // Assert that disconnectFromPeer() was not called. 382 | assert.equal(spy1.called, false) 383 | }) 384 | 385 | it('should catch and throw errors', async () => { 386 | try { 387 | await uut.enforceWhitelist() 388 | 389 | assert.fail('Unexpected code path') 390 | } catch (err) { 391 | // console.log(err) 392 | assert.include(err.message, 'Cannot read') 393 | } 394 | }) 395 | }) 396 | }) 397 | --------------------------------------------------------------------------------