├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── img ├── badge.png ├── badge.sketch └── badge.svg ├── package.json ├── src ├── connection.js ├── index.js └── tests.js └── test ├── compliance.spec.js ├── connection.js └── utils └── peers.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | dist 30 | package-lock.json 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | test 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: npm 3 | stages: 4 | - check 5 | - test 6 | - cov 7 | 8 | node_js: 9 | - '10' 10 | - '12' 11 | 12 | os: 13 | - linux 14 | - osx 15 | - windows 16 | 17 | script: npx nyc -s npm run test:node -- --bail 18 | after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov 19 | 20 | jobs: 21 | include: 22 | - stage: check 23 | script: 24 | - npx aegir dep-check 25 | - npm run lint 26 | 27 | - stage: test 28 | name: chrome 29 | addons: 30 | chrome: stable 31 | script: npx aegir test -t browser -t webworker 32 | 33 | - stage: test 34 | name: firefox 35 | addons: 36 | firefox: latest 37 | script: npx aegir test -t browser -t webworker -- --browsers FirefoxHeadless 38 | 39 | notifications: 40 | email: false 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.4.1](https://github.com/libp2p/interface-connection/compare/v0.4.0...v0.4.1) (2019-10-17) 3 | 4 | 5 | ### Features 6 | 7 | * add support for timeline proxying ([#31](https://github.com/libp2p/interface-connection/issues/31)) ([541bf83](https://github.com/libp2p/interface-connection/commit/541bf83)) 8 | 9 | 10 | 11 | 12 | # [0.4.0](https://github.com/libp2p/interface-connection/compare/v0.3.3...v0.4.0) (2019-09-27) 13 | 14 | 15 | ### Code Refactoring 16 | 17 | * API changes and switch to async iterators ([#29](https://github.com/libp2p/interface-connection/issues/29)) ([bf5c646](https://github.com/libp2p/interface-connection/commit/bf5c646)) 18 | 19 | 20 | ### BREAKING CHANGES 21 | 22 | * all the callbacks in the provided API were removed and each function uses async/await. Additionally, pull-streams are no longer being used. See the README for new usage. 23 | 24 | 25 | 26 | 27 | ## [0.3.3](https://github.com/libp2p/interface-connection/compare/v0.3.1...v0.3.3) (2018-11-29) 28 | 29 | 30 | 31 | 32 | ## [0.3.1](https://github.com/libp2p/interface-connection/compare/v0.3.0...v0.3.1) (2017-02-09) 33 | 34 | 35 | 36 | 37 | # [0.3.0](https://github.com/libp2p/interface-connection/compare/v0.2.1...v0.3.0) (2016-11-03) 38 | 39 | 40 | ### Features 41 | 42 | * async crypto + sauce labs + aegir 9 ([b40114c](https://github.com/libp2p/interface-connection/commit/b40114c)) 43 | 44 | 45 | 46 | 47 | ## [0.2.1](https://github.com/libp2p/interface-connection/compare/v0.2.0...v0.2.1) (2016-09-05) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * **package.json:** point to right main ([84cd2ca](https://github.com/libp2p/interface-connection/commit/84cd2ca)) 53 | 54 | 55 | 56 | 57 | # [0.2.0](https://github.com/libp2p/interface-connection/compare/v0.1.8...v0.2.0) (2016-09-05) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * **deps:** fix package.json ([e0f7db3](https://github.com/libp2p/interface-connection/commit/e0f7db3)) 63 | 64 | 65 | ### Features 66 | 67 | * **connection:** migrate to pull-streams ([ed5727a](https://github.com/libp2p/interface-connection/commit/ed5727a)) 68 | 69 | 70 | 71 | 72 | ## [0.1.8](https://github.com/libp2p/interface-connection/compare/v0.1.7...v0.1.8) (2016-08-03) 73 | 74 | 75 | 76 | 77 | ## [0.1.7](https://github.com/libp2p/interface-connection/compare/v0.1.6...v0.1.7) (2016-06-27) 78 | 79 | 80 | 81 | 82 | ## [0.1.6](https://github.com/libp2p/interface-connection/compare/v0.1.5...v0.1.6) (2016-06-23) 83 | 84 | 85 | 86 | 87 | ## [0.1.5](https://github.com/libp2p/interface-connection/compare/v0.1.4...v0.1.5) (2016-06-23) 88 | 89 | 90 | 91 | 92 | ## [0.1.4](https://github.com/libp2p/interface-connection/compare/v0.1.3...v0.1.4) (2016-06-23) 93 | 94 | 95 | 96 | 97 | ## [0.1.3](https://github.com/libp2p/interface-connection/compare/v0.1.0...v0.1.3) (2016-06-16) 98 | 99 | 100 | 101 | 102 | # [0.1.0](https://github.com/libp2p/interface-connection/compare/v0.0.3...v0.1.0) (2016-06-16) 103 | 104 | 105 | 106 | 107 | ## [0.0.3](https://github.com/libp2p/interface-connection/compare/v0.0.1...v0.0.3) (2015-12-12) 108 | 109 | 110 | 111 | 112 | ## 0.0.1 (2015-09-17) 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 David Dias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⛔️ DEPRECATED: interface-connection is now included in [libp2p-interfaces](https://github.com/libp2p/js-interfaces) 2 | ====== 3 | 4 | # interface-connection 5 | 6 | [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) 7 | [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) 8 | [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) 9 | [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) 10 | [![](https://img.shields.io/codecov/c/github/libp2p/interface-connection.svg?style=flat-square)](https://codecov.io/gh/libp2p/interface-connection) 11 | [![](https://img.shields.io/travis/libp2p/interface-connection.svg?style=flat-square)](https://travis-ci.com/libp2p/interface-connection) 12 | [![Dependency Status](https://david-dm.org/libp2p/interface-connection.svg?style=flat-square)](https://david-dm.org/libp2p/interface-connection) 13 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) 14 | 15 | This is a test suite and interface you can use to implement a connection. The connection interface contains all the metadata associated with it, as well as an array of the streams opened through this connection. In the same way as the connection, a stream contains properties with its metadata, plus an iterable duplex object that offers a mechanism for writing and reading data, with back pressure. This module and test suite were heavily inspired by abstract-blob-store and interface-stream-muxer. 16 | 17 | The primary goal of this module is to enable developers to pick, swap or upgrade their connection without losing the same API expectations and mechanisms such as back pressure and the ability to half close a connection. 18 | 19 | Publishing a test suite as a module lets multiple modules ensure compatibility since they use the same test suite. 20 | 21 | ## Lead Maintainer 22 | 23 | [Jacob Heun](https://github.com/jacobheun/) 24 | 25 | ## Usage 26 | 27 | ### Connection 28 | 29 | Before creating a connection from a transport compatible with `libp2p` it is important to understand some concepts: 30 | 31 | - **socket**: the underlying raw duplex connection between two nodes. It is created by the transports during a dial/listen. 32 | - **[multiaddr connection](https://github.com/libp2p/interface-transport#multiaddrconnection)**: an abstraction over the socket to allow it to work with multiaddr addresses. It is a duplex connection that transports create to wrap the socket before passing to an upgrader that turns it into a standard connection (see below). 33 | - **connection**: a connection between two _peers_ that has built in multiplexing and info about the connected peer. It is created from a [multiaddr connection](https://github.com/libp2p/interface-transport#multiaddrconnection) by an upgrader. The upgrader uses multistream-select to add secio and multiplexing and returns this object. 34 | - **stream**: a muxed duplex channel of the `connection`. Each connection may have many streams. 35 | 36 | A connection stands for the libp2p communication duplex layer between two nodes. It is **not** the underlying raw transport duplex layer (socket), such as a TCP socket, but an abstracted layer that sits on top of the raw socket. 37 | 38 | This helps ensuring that the transport is responsible for socket management, while also allowing the application layer to handle the connection management. 39 | 40 | ### Test suite 41 | 42 | #### JS 43 | 44 | ```js 45 | describe('your connection', () => { 46 | require('interface-connection/src/tests')({ 47 | async setup () { 48 | return YourConnection 49 | }, 50 | async teardown () { 51 | // cleanup resources created by setup() 52 | } 53 | }) 54 | }) 55 | ``` 56 | 57 | #### Go 58 | 59 | > WIP 60 | 61 | ## API 62 | 63 | ### Connection 64 | 65 | A valid connection (one that follows this abstraction), must implement the following API: 66 | 67 | - type: `Connection` 68 | ```js 69 | new Connection({ 70 | localAddr, 71 | remoteAddr, 72 | localPeer, 73 | remotePeer, 74 | newStream, 75 | close, 76 | getStreams, 77 | stat: { 78 | direction, 79 | timeline: { 80 | open, 81 | upgraded 82 | }, 83 | multiplexer, 84 | encryption 85 | } 86 | }) 87 | ``` 88 | - ` conn.localAddr` 89 | - ` conn.remoteAddr` 90 | - ` conn.localPeer` 91 | - ` conn.remotePeer` 92 | - ` conn.stat` 93 | - ` conn.registry` 94 | - `Array conn.streams` 95 | - `Promise conn.newStream(Array)` 96 | - ` conn.removeStream(id)` 97 | - ` conn.addStream(stream, protocol, metadata)` 98 | - `Promise<> conn.close()` 99 | 100 | It can be obtained as follows: 101 | 102 | ```js 103 | const { Connection } = require('interface-connection') 104 | 105 | const conn = new Connection({ 106 | localAddr: maConn.localAddr, 107 | remoteAddr: maConn.remoteAddr, 108 | localPeer: this._peerId, 109 | remotePeer, 110 | newStream, 111 | close: err => maConn.close(err), 112 | getStreams, 113 | stats: { 114 | direction: 'outbound', 115 | timeline: { 116 | open: maConn.timeline.open, 117 | upgraded: Date.now() 118 | }, 119 | multiplexer, 120 | encryption 121 | } 122 | }) 123 | ``` 124 | 125 | #### Creating a connection instance 126 | 127 | - `JavaScript` - `const conn = new Connection({localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, direction, multiplexer, encryption})` 128 | 129 | Creates a new Connection instance. 130 | 131 | `localAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used by the local peer to reach the remote. 132 | `remoteAddr` is the [multiaddr](https://github.com/multiformats/multiaddr) address used to communicate with the remote peer. 133 | `localPeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the local peer. 134 | `remotePeer` is the [PeerId](https://github.com/libp2p/js-peer-id) of the remote peer. 135 | `newStream` is the `function` responsible for getting a new muxed+multistream-selected stream. 136 | `close` is the `function` responsible for closing the raw connection. 137 | `getStreams` is the `function` responsible for getting the streams muxed within the connection. 138 | `stats` is an `object` with the metadata of the connection. It contains: 139 | - `direction` is a `string` indicating whether the connection is `inbound` or `outbound`. 140 | - `timeline` is an `object` with the relevant events timestamps of the connection (`open`, `upgraded` and `closed`; the `closed` will be added when the connection is closed). 141 | - `multiplexer` is a `string` with the connection multiplexing codec (optional). 142 | - `encryption` is a `string` with the connection encryption method identifier (optional). 143 | 144 | #### Create a new stream 145 | 146 | - `JavaScript` - `conn.newStream(protocols)` 147 | 148 | Create a new stream within the connection. 149 | 150 | `protocols` is an array of the intended protocol to use (by order of preference). Example: `[/echo/1.0.0]` 151 | 152 | It returns a `Promise` with an object with the following properties: 153 | 154 | ```js 155 | { 156 | stream, 157 | protocol 158 | } 159 | ``` 160 | 161 | The stream property contains the muxed stream, while the protocol contains the protocol codec used by the stream. 162 | 163 | #### Add stream metadata 164 | 165 | - `JavaScript` - `conn.addStream(stream, { protocol, ...metadata })` 166 | 167 | Add a new stream to the connection registry. 168 | 169 | `stream` is a muxed stream. 170 | `protocol` is the string codec for the protocol used by the stream. Example: `/echo/1.0.0` 171 | `metadata` is an object containing any additional, optional, stream metadata that you wish to track (such as its `tags`). 172 | 173 | #### Remove a from the registry 174 | 175 | - `JavaScript` - `conn.removeStream(id)` 176 | 177 | Removes the stream with the given id from the connection registry. 178 | 179 | `id` is the unique id of the stream for this connection. 180 | 181 | 182 | #### Close connection 183 | 184 | - `JavaScript` - `conn.close()` 185 | 186 | This method closes the connection to the remote peer, as well as all the streams muxed within the connection. 187 | 188 | It returns a `Promise`. 189 | 190 | #### Connection identifier 191 | 192 | - `JavaScript` - `conn.id` 193 | 194 | This property contains the identifier of the connection. 195 | 196 | #### Connection streams registry 197 | 198 | - `JavaScript` - `conn.registry` 199 | 200 | This property contains a map with the muxed streams indexed by their id. This registry contains the protocol used by the stream, as well as its metadata. 201 | 202 | #### Remote peer 203 | 204 | - `JavaScript` - `conn.remotePeer` 205 | 206 | This property contains the remote `peer-id` of this connection. 207 | 208 | #### Local peer 209 | 210 | - `JavaScript` - `conn.localPeer` 211 | 212 | This property contains the local `peer-id` of this connection. 213 | 214 | #### Get the connection Streams 215 | 216 | - `JavaScript` - `conn.streams` 217 | 218 | This getter returns all the muxed streams within the connection. 219 | 220 | It returns an `Array`. 221 | 222 | #### Remote address 223 | 224 | - `JavaScript` - `conn.remoteAddr` 225 | 226 | This getter returns the `remote` [multiaddr](https://github.com/multiformats/multiaddr) address. 227 | 228 | #### Local address 229 | 230 | - `JavaScript` - `conn.localAddr` 231 | 232 | This getter returns the `local` [multiaddr](https://github.com/multiformats/multiaddr) address. 233 | 234 | #### Stat 235 | 236 | - `JavaScript` - `conn.stat` 237 | 238 | This getter returns an `Object` with the metadata of the connection, as follows: 239 | 240 | - `status`: 241 | 242 | This property contains the status of the connection. It can be either `open`, `closing` or `closed`. Once the connection is created it is in an `open` status. When a `conn.close()` happens, the status will change to `closing` and finally, after all the connection streams are properly closed, the status will be `closed`. 243 | 244 | - `timeline`: 245 | 246 | This property contains an object with the `open`, `upgraded` and `close` timestamps of the connection. Note that, the `close` timestamp is `undefined` until the connection is closed. 247 | 248 | - `direction`: 249 | 250 | This property contains the direction of the peer in the connection. It can be `inbound` or `outbound`. 251 | 252 | - `multiplexer`: 253 | 254 | This property contains the `multiplexing` codec being used in the connection. 255 | 256 | - `encryption`: 257 | 258 | This property contains the encryption method being used in the connection. It is `undefined` if the connection is not encrypted. 259 | 260 | #### Tags 261 | 262 | - `JavaScript` - `conn.tags` 263 | 264 | This property contains an array of tags associated with the connection. New tags can be pushed to this array during the connection's lifetime. 265 | -------------------------------------------------------------------------------- /img/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libp2p/interface-connection/acd42ed51ad3453ed6bf382eacf1e7b16298a032/img/badge.png -------------------------------------------------------------------------------- /img/badge.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libp2p/interface-connection/acd42ed51ad3453ed6bf382eacf1e7b16298a032/img/badge.sketch -------------------------------------------------------------------------------- /img/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | badge 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | Connectio 12 | n 13 | 14 | 15 | Compatibl 16 | e 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interface-connection", 3 | "version": "0.4.1", 4 | "description": "A test suite and interface you can use to implement a connection interface.", 5 | "leadMaintainer": "Jacob Heun ", 6 | "main": "src/index.js", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "scripts": { 12 | "lint": "aegir lint", 13 | "build": "aegir build", 14 | "test": "aegir test", 15 | "test:node": "aegir test -t node", 16 | "test:browser": "aegir test -t browser -t webworker", 17 | "release": "aegir release", 18 | "release-minor": "aegir release --type minor", 19 | "release-major": "aegir release --type major" 20 | }, 21 | "pre-push": [ 22 | "lint" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/libp2p/interface-connection.git" 27 | }, 28 | "keywords": [ 29 | "IPFS" 30 | ], 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/libp2p/interface-connection/issues" 34 | }, 35 | "homepage": "https://github.com/libp2p/interface-connection", 36 | "dependencies": { 37 | "abortable-iterator": "^2.1.0", 38 | "chai": "^4.2.0", 39 | "class-is": "^1.1.0", 40 | "dirty-chai": "^2.0.1", 41 | "err-code": "^2.0.0", 42 | "multiaddr": "^7.1.0", 43 | "peer-id": "~0.13.2", 44 | "sinon": "^7.5.0" 45 | }, 46 | "devDependencies": { 47 | "aegir": "^20.2.0", 48 | "it-pair": "^1.0.0", 49 | "it-pipe": "^1.0.1", 50 | "mocha": "^6.2.0" 51 | }, 52 | "engines": { 53 | "node": ">=10.0.0", 54 | "npm": ">=6.0.0" 55 | }, 56 | "contributors": [ 57 | "David Dias ", 58 | "Friedel Ziegelmayer ", 59 | "Greenkeeper ", 60 | "Jacob Heun ", 61 | "James Ray <16969914+jamesray1@users.noreply.github.com>", 62 | "Pau Ramon Revilla ", 63 | "Richard Littauer ", 64 | "Vasco Santos " 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const PeerId = require('peer-id') 4 | const multiaddr = require('multiaddr') 5 | 6 | const withIs = require('class-is') 7 | 8 | const assert = require('assert') 9 | const errCode = require('err-code') 10 | 11 | /** 12 | * An implementation of the js-libp2p connection. 13 | * Any libp2p transport should use an upgrader to return this connection. 14 | */ 15 | class Connection { 16 | /** 17 | * Creates an instance of Connection. 18 | * @param {object} properties properties of the connection. 19 | * @param {multiaddr} properties.localAddr local multiaddr of the connection. 20 | * @param {multiaddr} properties.remoteAddr remote multiaddr of the connection. 21 | * @param {PeerId} properties.localPeer local peer-id. 22 | * @param {PeerId} properties.remotePeer remote peer-id. 23 | * @param {function} properties.newStream new stream muxer function. 24 | * @param {function} properties.close close raw connection function. 25 | * @param {function} properties.getStreams get streams from muxer function. 26 | * @param {object} properties.stat metadata of the connection. 27 | * @param {string} properties.stat.direction connection establishment direction ("inbound" or "outbound"). 28 | * @param {object} properties.stat.timeline connection relevant events timestamp. 29 | * @param {string} properties.stat.timeline.open connection opening timestamp. 30 | * @param {string} properties.stat.timeline.upgraded connection upgraded timestamp. 31 | * @param {string} [properties.stat.multiplexer] connection multiplexing identifier. 32 | * @param {string} [properties.stat.encryption] connection encryption method identifier. 33 | */ 34 | constructor ({ localAddr, remoteAddr, localPeer, remotePeer, newStream, close, getStreams, stat }) { 35 | assert(multiaddr.isMultiaddr(localAddr), 'localAddr must be an instance of multiaddr') 36 | assert(multiaddr.isMultiaddr(remoteAddr), 'remoteAddr must be an instance of multiaddr') 37 | assert(PeerId.isPeerId(localPeer), 'localPeer must be an instance of peer-id') 38 | assert(PeerId.isPeerId(remotePeer), 'remotePeer must be an instance of peer-id') 39 | assert(typeof newStream === 'function', 'new stream must be a function') 40 | assert(typeof close === 'function', 'close must be a function') 41 | assert(typeof getStreams === 'function', 'getStreams must be a function') 42 | assert(stat, 'connection metadata object must be provided') 43 | assert(stat.direction === 'inbound' || stat.direction === 'outbound', 'direction must be "inbound" or "outbound"') 44 | assert(stat.timeline, 'connection timeline object must be provided in the stat object') 45 | assert(stat.timeline.open, 'connection open timestamp must be provided') 46 | assert(stat.timeline.upgraded, 'connection upgraded timestamp must be provided') 47 | 48 | /** 49 | * Connection identifier. 50 | */ 51 | this.id = (parseInt(Math.random() * 1e9)).toString(36) + Date.now() 52 | 53 | /** 54 | * Observed multiaddr of the local peer 55 | */ 56 | this.localAddr = localAddr 57 | 58 | /** 59 | * Observed multiaddr of the remote peer 60 | */ 61 | this.remoteAddr = remoteAddr 62 | 63 | /** 64 | * Local peer id. 65 | */ 66 | this.localPeer = localPeer 67 | 68 | /** 69 | * Remote peer id. 70 | */ 71 | this.remotePeer = remotePeer 72 | 73 | /** 74 | * Connection metadata. 75 | */ 76 | this._stat = { 77 | ...stat, 78 | status: 'open' 79 | } 80 | 81 | /** 82 | * Reference to the new stream function of the multiplexer 83 | */ 84 | this._newStream = newStream 85 | 86 | /** 87 | * Reference to the close function of the raw connection 88 | */ 89 | this._close = close 90 | 91 | /** 92 | * Reference to the getStreams function of the muxer 93 | */ 94 | this._getStreams = getStreams 95 | 96 | /** 97 | * Connection streams registry 98 | */ 99 | this.registry = new Map() 100 | 101 | /** 102 | * User provided tags 103 | */ 104 | this.tags = [] 105 | } 106 | 107 | /** 108 | * Get connection metadata 109 | * @return {Object} 110 | */ 111 | get stat () { 112 | return this._stat 113 | } 114 | 115 | /** 116 | * Get all the streams of the muxer. 117 | * @return {Array<*>} 118 | */ 119 | get streams () { 120 | return this._getStreams() 121 | } 122 | 123 | /** 124 | * Create a new stream from this connection 125 | * @param {string[]} protocols intended protocol for the stream 126 | * @return {Promise} with muxed+multistream-selected stream and selected protocol 127 | */ 128 | async newStream (protocols) { 129 | if (this.stat.status === 'closing') { 130 | throw errCode(new Error('the connection is being closed'), 'ERR_CONNECTION_BEING_CLOSED') 131 | } 132 | 133 | if (this.stat.status === 'closed') { 134 | throw errCode(new Error('the connection is closed'), 'ERR_CONNECTION_CLOSED') 135 | } 136 | 137 | if (!Array.isArray(protocols)) protocols = [protocols] 138 | 139 | const { stream, protocol } = await this._newStream(protocols) 140 | 141 | this.addStream(stream, protocol) 142 | 143 | return { 144 | stream, 145 | protocol 146 | } 147 | } 148 | 149 | /** 150 | * Add a stream when it is opened to the registry. 151 | * @param {*} muxedStream a muxed stream 152 | * @param {object} properties the stream properties to be registered 153 | * @param {string} properties.protocol the protocol used by the stream 154 | * @param {object} properties.metadata metadata of the stream 155 | * @return {void} 156 | */ 157 | addStream (muxedStream, { protocol, metadata = {} }) { 158 | // Add metadata for the stream 159 | this.registry.set(muxedStream.id, { 160 | protocol, 161 | ...metadata 162 | }) 163 | } 164 | 165 | /** 166 | * Remove stream registry after it is closed. 167 | * @param {string} id identifier of the stream 168 | */ 169 | removeStream (id) { 170 | this.registry.delete(id) 171 | } 172 | 173 | /** 174 | * Close the connection. 175 | * @return {Promise} 176 | */ 177 | async close () { 178 | if (this.stat.status === 'closed') { 179 | return 180 | } 181 | 182 | if (this._closing) { 183 | return this._closing 184 | } 185 | 186 | this.stat.status = 'closing' 187 | 188 | // Close raw connection 189 | this._closing = await this._close() 190 | 191 | this._stat.timeline.close = Date.now() 192 | this.stat.status = 'closed' 193 | } 194 | } 195 | 196 | module.exports = withIs(Connection, { className: 'Connection', symbolName: '@libp2p/interface-connection/connection' }) 197 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.Connection = require('./connection') 4 | -------------------------------------------------------------------------------- /src/tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict' 4 | 5 | const connectionSuite = require('../test/connection') 6 | 7 | module.exports = (test) => { 8 | connectionSuite(test) 9 | } 10 | -------------------------------------------------------------------------------- /test/compliance.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const tests = require('../src/tests') 5 | const Connection = require('../src/connection') 6 | const peers = require('./utils/peers') 7 | const PeerId = require('peer-id') 8 | const multiaddr = require('multiaddr') 9 | const pair = require('it-pair') 10 | 11 | describe('compliance tests', () => { 12 | tests({ 13 | /** 14 | * Test setup. `properties` allows the compliance test to override 15 | * certain values for testing. 16 | * @param {*} properties 17 | */ 18 | async setup (properties) { 19 | const localAddr = multiaddr('/ip4/127.0.0.1/tcp/8080') 20 | const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') 21 | const [localPeer, remotePeer] = await Promise.all([ 22 | PeerId.createFromJSON(peers[0]), 23 | PeerId.createFromJSON(peers[1]) 24 | ]) 25 | const openStreams = [] 26 | let streamId = 0 27 | 28 | return new Connection({ 29 | localPeer, 30 | remotePeer, 31 | localAddr, 32 | remoteAddr, 33 | stat: { 34 | timeline: { 35 | open: Date.now() - 10, 36 | upgraded: Date.now() 37 | }, 38 | direction: 'outbound', 39 | encryption: '/secio/1.0.0', 40 | multiplexer: '/mplex/6.7.0' 41 | }, 42 | newStream: (protocols) => { 43 | const id = streamId++ 44 | const stream = pair() 45 | 46 | stream.close = () => stream.sink([]) 47 | stream.id = id 48 | 49 | openStreams.push(stream) 50 | 51 | return { 52 | stream, 53 | protocol: protocols[0] 54 | } 55 | }, 56 | close: () => {}, 57 | getStreams: () => openStreams, 58 | ...properties 59 | }) 60 | }, 61 | async teardown () { 62 | // cleanup resources created by setup() 63 | } 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | 'use strict' 4 | 5 | const chai = require('chai') 6 | const expect = chai.expect 7 | chai.use(require('dirty-chai')) 8 | const sinon = require('sinon') 9 | 10 | module.exports = (test) => { 11 | describe('connection', () => { 12 | describe('open connection', () => { 13 | let connection 14 | 15 | beforeEach(async () => { 16 | connection = await test.setup() 17 | if (!connection) throw new Error('missing connection') 18 | }) 19 | 20 | afterEach(async () => { 21 | await connection.close() 22 | await test.teardown() 23 | }) 24 | 25 | it('should have properties set', () => { 26 | expect(connection.id).to.exist() 27 | expect(connection.localPeer).to.exist() 28 | expect(connection.remotePeer).to.exist() 29 | expect(connection.localAddr).to.exist() 30 | expect(connection.remoteAddr).to.exist() 31 | expect(connection.stat.status).to.equal('open') 32 | expect(connection.stat.timeline.open).to.exist() 33 | expect(connection.stat.timeline.upgraded).to.exist() 34 | expect(connection.stat.timeline.close).to.not.exist() 35 | expect(connection.stat.direction).to.exist() 36 | expect(connection.streams).to.eql([]) 37 | expect(connection.tags).to.eql([]) 38 | }) 39 | 40 | it('should get the metadata of an open connection', () => { 41 | const stat = connection.stat 42 | 43 | expect(stat.status).to.equal('open') 44 | expect(stat.direction).to.exist() 45 | expect(stat.timeline.open).to.exist() 46 | expect(stat.timeline.upgraded).to.exist() 47 | expect(stat.timeline.close).to.not.exist() 48 | }) 49 | 50 | it('should return an empty array of streams', () => { 51 | const streams = connection.streams 52 | 53 | expect(streams).to.eql([]) 54 | }) 55 | 56 | it('should be able to create a new stream', async () => { 57 | const protocolToUse = '/echo/0.0.1' 58 | const { stream, protocol } = await connection.newStream(protocolToUse) 59 | 60 | expect(protocol).to.equal(protocolToUse) 61 | 62 | const connStreams = await connection.streams 63 | 64 | expect(stream).to.exist() 65 | expect(connStreams).to.exist() 66 | expect(connStreams).to.have.lengthOf(1) 67 | expect(connStreams[0]).to.equal(stream) 68 | }) 69 | }) 70 | 71 | describe('close connection', () => { 72 | let connection 73 | let timelineProxy 74 | const proxyHandler = { 75 | set () { 76 | return Reflect.set(...arguments) 77 | } 78 | } 79 | 80 | beforeEach(async () => { 81 | timelineProxy = new Proxy({ 82 | open: Date.now() - 10, 83 | upgraded: Date.now() 84 | }, proxyHandler) 85 | 86 | connection = await test.setup({ 87 | stat: { 88 | timeline: timelineProxy, 89 | direction: 'outbound', 90 | encryption: '/crypto/1.0.0', 91 | multiplexer: '/muxer/1.0.0' 92 | } 93 | }) 94 | if (!connection) throw new Error('missing connection') 95 | }) 96 | 97 | afterEach(async () => { 98 | await test.teardown() 99 | }) 100 | 101 | it('should be able to close the connection after being created', async () => { 102 | expect(connection.stat.timeline.close).to.not.exist() 103 | await connection.close() 104 | 105 | expect(connection.stat.timeline.close).to.exist() 106 | expect(connection.stat.status).to.equal('closed') 107 | }) 108 | 109 | it('should be able to close the connection after opening a stream', async () => { 110 | // Open stream 111 | const protocol = '/echo/0.0.1' 112 | await connection.newStream(protocol) 113 | 114 | // Close connection 115 | expect(connection.stat.timeline.close).to.not.exist() 116 | await connection.close() 117 | 118 | expect(connection.stat.timeline.close).to.exist() 119 | expect(connection.stat.status).to.equal('closed') 120 | }) 121 | 122 | it('should support a proxy on the timeline', async () => { 123 | sinon.spy(proxyHandler, 'set') 124 | expect(connection.stat.timeline.close).to.not.exist() 125 | 126 | await connection.close() 127 | expect(proxyHandler.set.callCount).to.equal(1) 128 | const [obj, key, value] = proxyHandler.set.getCall(0).args 129 | expect(obj).to.eql(connection.stat.timeline) 130 | expect(key).to.equal('close') 131 | expect(value).to.be.a('number').that.equals(connection.stat.timeline.close) 132 | }) 133 | 134 | it('should fail to create a new stream if the connection is closing', async () => { 135 | expect(connection.stat.timeline.close).to.not.exist() 136 | connection.close() 137 | 138 | try { 139 | const protocol = '/echo/0.0.1' 140 | await connection.newStream(protocol) 141 | } catch (err) { 142 | expect(err).to.exist() 143 | return 144 | } 145 | 146 | throw new Error('should fail to create a new stream if the connection is closing') 147 | }) 148 | 149 | it('should fail to create a new stream if the connection is closed', async () => { 150 | expect(connection.stat.timeline.close).to.not.exist() 151 | await connection.close() 152 | 153 | try { 154 | const protocol = '/echo/0.0.1' 155 | await connection.newStream(protocol) 156 | } catch (err) { 157 | expect(err).to.exist() 158 | expect(err.code).to.equal('ERR_CONNECTION_CLOSED') 159 | return 160 | } 161 | 162 | throw new Error('should fail to create a new stream if the connection is closing') 163 | }) 164 | }) 165 | }) 166 | } 167 | -------------------------------------------------------------------------------- /test/utils/peers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = [{ 4 | id: 'QmNMMAqSxPetRS1cVMmutW5BCN1qQQyEr4u98kUvZjcfEw', 5 | privKey: 'CAASpQkwggShAgEAAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAECggEAB2H2uPRoRCAKU+T3gO4QeoiJaYKNjIO7UCplE0aMEeHDnEjAKC1HQ1G0DRdzZ8sb0fxuIGlNpFMZv5iZ2ZFg2zFfV//DaAwTek9tIOpQOAYHUtgHxkj5FIlg2BjlflGb+ZY3J2XsVB+2HNHkUEXOeKn2wpTxcoJE07NmywkO8Zfr1OL5oPxOPlRN1gI4ffYH2LbfaQVtRhwONR2+fs5ISfubk5iKso6BX4moMYkxubYwZbpucvKKi/rIjUA3SK86wdCUnno1KbDfdXSgCiUlvxt/IbRFXFURQoTV6BOi3sP5crBLw8OiVubMr9/8WE6KzJ0R7hPd5+eeWvYiYnWj4QKBgQD6jRlAFo/MgPO5NZ/HRAk6LUG+fdEWexA+GGV7CwJI61W/Dpbn9ZswPDhRJKo3rquyDFVZPdd7+RlXYg1wpmp1k54z++L1srsgj72vlg4I8wkZ4YLBg0+zVgHlQ0kxnp16DvQdOgiRFvMUUMEgetsoIx1CQWTd67hTExGsW+WAZQKBgQDT/WaHWvwyq9oaZ8G7F/tfeuXvNTk3HIJdfbWGgRXB7lJ7Gf6FsX4x7PeERfL5a67JLV6JdiLLVuYC2CBhipqLqC2DB962aKMvxobQpSljBBZvZyqP1IGPoKskrSo+2mqpYkeCLbDMuJ1nujgMP7gqVjabs2zj6ACKmmpYH/oNowJ/T0ZVtvFsjkg+1VsiMupUARRQuPUWMwa9HOibM1NIZcoQV2NGXB5Z++kR6JqxQO0DZlKArrviclderUdY+UuuY4VRiSEprpPeoW7ZlbTku/Ap8QZpWNEzZorQDro7bnfBW91fX9/81ets/gCPGrfEn+58U3pdb9oleCOQc/ifpQKBgBTYGbi9bYbd9vgZs6bd2M2um+VFanbMytS+g5bSIn2LHXkVOT2UEkB+eGf9KML1n54QY/dIMmukA8HL1oNAyalpw+/aWj+9Ui5kauUhGEywHjSeBEVYM9UXizxz+m9rsoktLLLUI0o97NxCJzitG0Kub3gn0FEogsUeIc7AdinZAoGBANnM1vcteSQDs7x94TDEnvvqwSkA2UWyLidD2jXgE0PG4V6tTkK//QPBmC9eq6TIqXkzYlsErSw4XeKO91knFofmdBzzVh/ddgx/NufJV4tXF+a2iTpqYBUJiz9wpIKgf43/Ob+P1EA99GAhSdxz1ess9O2aTqf3ANzn6v6g62Pv', 6 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPek2aeHMa0blL42RTKd6xgtkk4Zkldvq4LHxzcag5uXepiQzWANEUvoD3KcUTmMRmx14PvsxdLCNst7S2JSa0R2n5wSRs14zGy6892lx4H4tLBD1KSpQlJ6vabYM1CJhIQRG90BtzDPrJ/X1iJ2HA0PPDz0Mflam2QUMDDrU0IuV2m7gSCJ5r4EmMs3U0xnH/1gShkVx4ir0WUdoWf5KQUJOmLn1clTRHYPv4KL9A/E38+imNAXfkH3c2T7DrCcYRkZSpK+WecjMsH1dCX15hhhggNqfp3iulO1tGPxHjm7PDGTPUjpCWKpD5e50sLqsUwexac1ja6ktMfszIR+FPAgMBAAE=' 7 | }, { 8 | id: 'QmW8rAgaaA6sRydK1k6vonShQME47aDxaFidbtMevWs73t', 9 | privKey: 'CAASpwkwggSjAgEAAoIBAQCTU3gVDv3SRXLOsFln9GEf1nJ/uCEDhOG10eC0H9l9IPpVxjuPT1ep+ykFUdvefq3D3q+W3hbmiHm81o8dYv26RxZIEioToUWp7Ec5M2B/niYoE93za9/ZDwJdl7eh2hNKwAdxTmdbXUPjkIU4vLyHKRFbJIn9X8w9djldz8hoUvC1BK4L1XrT6F2l0ruJXErH2ZwI1youfSzo87TdXIoFKdrQLuW6hOtDCGKTiS+ab/DkMODc6zl8N47Oczv7vjzoWOJMUJs1Pg0ZsD1zmISY38P0y/QyEhatZn0B8BmSWxlLQuukatzOepQI6k+HtfyAAjn4UEqnMaXTP1uwLldVAgMBAAECggEAHq2f8MqpYjLiAFZKl9IUs3uFZkEiZsgx9BmbMAb91Aec+WWJG4OLHrNVTG1KWp+IcaQablEa9bBvoToQnS7y5OpOon1d066egg7Ymfmv24NEMM5KRpktCNcOSA0CySpPIB6yrg6EiUr3ixiaFUGABKkxmwgVz/Q15IqM0ZMmCUsC174PMAz1COFZxD0ZX0zgHblOJQW3dc0X3XSzhht8vU02SMoVObQHQfeXEHv3K/RiVj/Ax0bTc5JVkT8dm8xksTtsFCNOzRBqFS6MYqX6U/u0Onz3Jm5Jt7fLWb5n97gZR4SleyGrqxYNb46d9X7mP0ie7E6bzFW0DsWBIeAqVQKBgQDW0We2L1n44yOvJaMs3evpj0nps13jWidt2I3RlZXjWzWHiYQfvhWUWqps/xZBnAYgnN/38xbKzHZeRNhrqOo+VB0WK1IYl0lZVE4l6TNKCsLsUfQzsb1pePkd1eRZA+TSqsi+I/IOQlQU7HA0bMrah/5FYyUBP0jYvCOvYTlZuwKBgQCvkcVRydVlzjUgv7lY5lYvT8IHV5iYO4Qkk2q6Wjv9VUKAJZauurMdiy05PboWfs5kbETdwFybXMBcknIvZO4ihxmwL8mcoNwDVZHI4bXapIKMTCyHgUKvJ9SeTcKGC7ZuQJ8mslRmYox/HloTOXEJgQgPRxXcwa3amzvdZI+6LwKBgQCLsnQqgxKUi0m6bdR2qf7vzTH4258z6X34rjpT0F5AEyF1edVFOz0XU/q+lQhpNEi7zqjLuvbYfSyA026WXKuwSsz7jMJ/oWqev/duKgAjp2npesY/E9gkjfobD+zGgoS9BzkyhXe1FCdP0A6L2S/1+zg88WOwMvJxl6/xLl24XwKBgCm60xSajX8yIQyUpWBM9yUtpueJ2Xotgz4ST+bVNbcEAddll8gWFiaqgug9FLLuFu5lkYTHiPtgc1RNdphvO+62/9MRuLDixwh/2TPO+iNqwKDKJjda8Nei9vVddCPaOtU/xNQ0xLzFJbG9LBmvqH9izOCcu8SJwGHaTcNUeJj/AoGADCJ26cY30c13F/8awAAmFYpZWCuTP5ppTsRmjd63ixlrqgkeLGpJ7kYb5fXkcTycRGYgP0e1kssBGcmE7DuG955fx3ZJESX3GQZ+XfMHvYGONwF1EiK1f0p6+GReC2VlQ7PIkoD9o0hojM6SnWvv9EXNjCPALEbfPFFvcniKVsE=', 10 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTU3gVDv3SRXLOsFln9GEf1nJ/uCEDhOG10eC0H9l9IPpVxjuPT1ep+ykFUdvefq3D3q+W3hbmiHm81o8dYv26RxZIEioToUWp7Ec5M2B/niYoE93za9/ZDwJdl7eh2hNKwAdxTmdbXUPjkIU4vLyHKRFbJIn9X8w9djldz8hoUvC1BK4L1XrT6F2l0ruJXErH2ZwI1youfSzo87TdXIoFKdrQLuW6hOtDCGKTiS+ab/DkMODc6zl8N47Oczv7vjzoWOJMUJs1Pg0ZsD1zmISY38P0y/QyEhatZn0B8BmSWxlLQuukatzOepQI6k+HtfyAAjn4UEqnMaXTP1uwLldVAgMBAAE=' 11 | }, { 12 | id: 'QmZqCdSzgpsmB3Qweb9s4fojAoqELWzqku21UVrqtVSKi4', 13 | privKey: 'CAASpgkwggSiAgEAAoIBAQCdbSEsTmw7lp5HagRcx57DaLiSUEkh4iBcKc7Y+jHICEIA8NIVi9FlfGEZj9G21FpiTR4Cy+BLVEuf8Nm90bym4iV+cSumeS21fvD8xGTEbeKGljs6OYHy3M45JhWF85gqHQJOqZufI2NRDuRgMZEO2+qGEXmSlv9mMXba/+9ecze8nSpB7bG2Z2pnKDeYwhF9Cz+ElMyn7TBWDjJERGVgFbTpdM3rBnbhB/TGpvs732QqZmIBlxnDb/Jn0l1gNZCgkEDcJ/0NDMBJTQ8vbvcdmaw3eaMPLkn1ix4wdu9QWCA0IBtuY1R7vSUtf4irnLJG7DnAw2GfM5QrF3xF1GLXAgMBAAECggEAQ1N0qHoxl5pmvqv8iaFlqLSUmx5y6GbI6CGJMQpvV9kQQU68yjItr3VuIXx8d/CBZyEMAK4oko7OeOyMcr3MLKLy3gyQWnXgsopDjhZ/8fH8uwps8g2+IZuFJrO+6LaxEPGvFu06fOiphPUVfn40R2KN/iBjGeox+AaXijmCqaV2vEdNJJPpMfz6VKZBDLTrbiqvo/3GN1U99PUqfPWpOWR29oAhh/Au6blSqvqTUPXB2+D/X6e1JXv31mxMPK68atDHSUjZWKB9lE4FMK1bkSKJRbyXmNIlbZ9V8X4/0r8/6T7JnW7ZT8ugRkquohmwgG7KkDXB1YsOCKXYUqzVYQKBgQDtnopFXWYl7XUyePJ/2MA5i7eoko9jmF44L31irqmHc5unNf6JlNBjlxTNx3WyfzhUzrn3c18psnGkqtow0tkBj5hmqn8/WaPbc5UA/5R1FNaNf8W5khn7MDm6KtYRPjN9djqTDiVHyC6ljONYd+5S+MqyKVWZ3t/xvG60sw85qwKBgQCpmpDtL+2JBwkfeUr3LyDcQxvbfzcv8lXj2otopWxWiLiZF1HzcqgAa2CIwu9kCGEt9Zr+9E4uINbe1To0b01/FhvR6xKO/ukceGA/mBB3vsKDcRmvpBUp+3SmnhY0nOk+ArQl4DhJ34k8pDM3EDPrixPf8SfVdU/8IM32lsdHhQKBgHLgpvCKCwxjFLnmBzcPzz8C8TOqR3BbBZIcQ34l+wflOGdKj1hsfaLoM8KYn6pAHzfBCd88A9Hg11hI0VuxVACRL5jS7NnvuGwsIOluppNEE8Ys86aXn7/0vLPoab3EWJhbRE48FIHzobmft3nZ4XpzlWs02JGfUp1IAC2UM9QpAoGAeWy3pZhSr2/iEC5+hUmwdQF2yEbj8+fDpkWo2VrVnX506uXPPkQwE1zM2Bz31t5I9OaJ+U5fSpcoPpDaAwBMs1fYwwlRWB8YNdHY1q6/23svN3uZsC4BGPV2JnO34iMUudilsRg+NGVdk5TbNejbwx7nM8Urh59djFzQGGMKeSECgYA0QMCARPpdMY50Mf2xQaCP7HfMJhESSPaBq9V3xY6ToEOEnXgAR5pNjnU85wnspHp+82r5XrKfEQlFxGpj2YA4DRRmn239sjDa29qP42UNAFg1+C3OvXTht1d5oOabaGhU0udwKmkEKUbb0bG5xPQJ5qeSJ5T1gLzLk3SIP0GlSw==', 14 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdbSEsTmw7lp5HagRcx57DaLiSUEkh4iBcKc7Y+jHICEIA8NIVi9FlfGEZj9G21FpiTR4Cy+BLVEuf8Nm90bym4iV+cSumeS21fvD8xGTEbeKGljs6OYHy3M45JhWF85gqHQJOqZufI2NRDuRgMZEO2+qGEXmSlv9mMXba/+9ecze8nSpB7bG2Z2pnKDeYwhF9Cz+ElMyn7TBWDjJERGVgFbTpdM3rBnbhB/TGpvs732QqZmIBlxnDb/Jn0l1gNZCgkEDcJ/0NDMBJTQ8vbvcdmaw3eaMPLkn1ix4wdu9QWCA0IBtuY1R7vSUtf4irnLJG7DnAw2GfM5QrF3xF1GLXAgMBAAE=' 15 | }, { 16 | id: 'QmR5VwgsL7jyfZHAGyp66tguVrQhCRQuRc3NokocsCZ3fA', 17 | privKey: 'CAASpwkwggSjAgEAAoIBAQCGXYU+uc2nn1zuJhfdFOl34upztnrD1gpHu58ousgHdGlGgYgbqLBAvIAauXdEL0+e30HofjA634SQxE+9nV+0FQBam1DDzHQlXsuwHV+2SKvSDkk4bVllMFpu2SJtts6VH+OXC/2ANJOm+eTALykQPYXgLIBxrhp/eD+Jz5r6wW2nq3k6OmYyK/4pgGzFjo5UyX+fa/171AJ68UPboFpDy6BZCcUjS0ondxPvD7cv5jMNqqMKIB/7rpi8n+Q3oeccRqVL56wH+FE3/QLjwYHwY6ILNRyvNXRqHjwBEXB2R5moXN0AFUWTw9rt3KhFiEjR1U81BTw5/xS7W2Iu0FgZAgMBAAECggEAS64HK8JZfE09eYGJNWPe8ECmD1C7quw21BpwVe+GVPSTizvQHswPohbKDMNj0srXDMPxCnNw1OgqcaOwyjsGuZaOoXoTroTM8nOHRIX27+PUqzaStS6aCG2IsiCozKUHjGTuupftS7XRaF4eIsUtWtFcQ1ytZ9pJYHypRQTi5NMSrTze5ThjnWxtHilK7gnBXik+aR0mYEVfSn13czQEC4rMOs+b9RAc/iibDNoLopfIdvmCCvfxzmySnR7Cu1iSUAONkir7PB+2Mt/qRFCH6P+jMamtCgQ8AmifXgVmDUlun+4MnKg3KrPd6ZjOEKhVe9mCHtGozk65RDREShfDdQKBgQDi+x2MuRa9peEMOHnOyXTS+v+MFcfmG0InsO08rFNBKZChLB+c9UHBdIvexpfBHigSyERfuDye4z6lxi8ZnierWMYJP30nxmrnxwTGTk1MQquhfs1A0kpmDnPsjlOS/drEIEIssNx2WbfJ7YtMxLWBtp+BJzGpQmr0LKC+NHRSrwKBgQCXiy2kJESIUkIs2ihV55hhT6/bZo1B1O5DPA2nkjOBXqXF6fvijzMDX82JjLd07lQZlI0n1Q/Hw0p4iYi9YVd2bLkLXF5UIb2qOeHj76enVFOrPHUSkC9Y2g/0Xs+60Ths2xRd8RrrfQU3kl5iVpBywkCIrb2M5+wRnNTk1W3TtwKBgQCvplyrteAfSurpJhs9JzE8w/hWU9SqAZYkWQp91W1oE95Um2yrbjBAoQxMjaqKS+f/APPIjy56Vqj4aHGyhW11b/Fw3qzfxvCcBKtxOs8eoMlo5FO6QgJJEA4tlcafDcvp0nzjUMqK28safLU7503+33B35fjMXxWdd5u9FaKfCQKBgC4W6j6tuRosymuRvgrCcRnHfpify/5loEFallyMnpWOD6Tt0OnK25z/GifnYDRz96gAAh5HMpFy18dpLOlMHamqz2yhHx8/U8vd5tHIJZlCkF/X91M5/uxrBccwvsT2tM6Got8fYSyVzWxlW8dUxIHiinYHQUsFjkqdBDLEpq5pAoGASoTw5RBEWFM0GuAZdXsyNyxU+4S+grkTS7WdW/Ymkukh+bJZbnvF9a6MkSehqXnknthmufonds2AFNS//63gixENsoOhzT5+2cdfc6tJECvJ9xXVXkf85AoQ6T/RrXF0W4m9yQyCngNJUrKUOIH3oDIfdZITlYzOC3u1ojj7VuQ=', 18 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCGXYU+uc2nn1zuJhfdFOl34upztnrD1gpHu58ousgHdGlGgYgbqLBAvIAauXdEL0+e30HofjA634SQxE+9nV+0FQBam1DDzHQlXsuwHV+2SKvSDkk4bVllMFpu2SJtts6VH+OXC/2ANJOm+eTALykQPYXgLIBxrhp/eD+Jz5r6wW2nq3k6OmYyK/4pgGzFjo5UyX+fa/171AJ68UPboFpDy6BZCcUjS0ondxPvD7cv5jMNqqMKIB/7rpi8n+Q3oeccRqVL56wH+FE3/QLjwYHwY6ILNRyvNXRqHjwBEXB2R5moXN0AFUWTw9rt3KhFiEjR1U81BTw5/xS7W2Iu0FgZAgMBAAE=' 19 | }, { 20 | id: 'QmScLDqRg7H6ipCYxm9fVk152UWavQFKscTdoT4YNHxgqp', 21 | privKey: 'CAASpwkwggSjAgEAAoIBAQCWEHaTZ6LBLFP5OPrUqjDM/cF4b2zrfh1Zm3kd02ZtgQB3iYtZqRPJT5ctT3A7WdVF/7dCxPGOCkJlLekTx4Y4gD8JtjA+EfN9fR/2RBKbti2N3CD4vkGp9ss4hbBFcXIhl8zuD/ELHutbV6b8b4QXJGnxfp/B+1kNPnyd7SJznS0QyvI8OLI1nAkVKdYLDRW8kPKeHyx1xhdNDuTQVTFyAjRGQ4e3UYFB7bYIHW3E6kCtCoJDlj+JPC02Yt1LHzIzZVLvPvNFnYY2mag6OiGFuh/oMBIqvnPc1zRZ3eLUqeGZjQVaoR0kdgZUKz7Q2TBeNldxK/s6XO0DnkQTlelNAgMBAAECggEAdmt1dyswR2p4tdIeNpY7Pnj9JNIhTNDPznefI0dArCdBvBMhkVaYk6MoNIxcj6l7YOrDroAF8sXr0TZimMY6B/pERKCt/z1hPWTxRQBBAvnHhwvwRPq2jK6BfhAZoyM8IoBNKowP9mum5QUNdGV4Al8s73KyFX0IsCfgZSvNpRdlt+DzPh+hu/CyoZaMpRchJc1UmK8Fyk3KfO+m0DZNfHP5P08lXNfM6MZLgTJVVgERHyG+vBOzTd2RElMe19nVCzHwb3dPPRZSQ7Fnz3rA+GeLqsM2Zi4HNhfbD1OcD9C4wDj5tYL6hWTkdz4IlfVcjCeUHxgIOhdDV2K+OwbuAQKBgQD0FjUZ09UW2FQ/fitbvIB5f1SkXWPxTF9l6mAeuXhoGv2EtQUO4vq/PK6N08RjrZdWQy6UsqHgffi7lVQ8o3hvCKdbtf4sP+cM92OrY0WZV89os79ndj4tyvmnP8WojwRjt/2XEfgdoWcgWxW9DiYINTOQVimZX+X/3on4s8hEgQKBgQCdY3kOMbyQeLTRkqHXjVTY4ddO+v4S4wOUa1l4rTqAbq1W3JYWwoDQgFuIu3limIHmjnSJpCD4EioXFsM7p6csenoc20sHxsaHnJ6Mn5Te41UYmY9EW0otkQ0C3KbXM0hwQkjyplnEmZawGKmjEHW8DJ3vRYTv9TUCgYKxDHgOzQKBgB4A/NYH7BG61eBYKgxEx6YnuMfbkwV+Vdu5S8d7FQn3B2LgvZZu4FPRqcNVXLbEB+5ao8czjiKCWaj1Wj15+rvrXGcxn+Tglg5J+r5+nXeUC7LbJZQaPNp0MOwWMr3dlrSLUWjYlJ9Pz9VyXOG4c4Rexc/gR4zK9QLW4C7qKpwBAoGAZzyUb0cYlPtYQA+asTU3bnvVKy1f8yuNcZFowst+EDiI4u0WVh+HNzy6zdmLKa03p+/RaWeLaK0hhrubnEnAUmCUMNF3ScaM+u804LDcicc8TkKLwx7ObU0z56isl4RAA8K27tNHFrpYKXJD834cfBkaj5ReOrfw6Y/iFhhDuBECgYEA8gbC76uz7LSHhW30DSRTcqOzTyoe2oYKQaxuxYNp7vSSOkcdRen+mrdflDvud2q/zN2QdL4pgqdldHlR35M/lJ0f0B6zp74jlzbO9700wzsOqreezGc5eWiroDL100U9uIZ50BKb8CKtixIHpinUSPIUcVDkSAZ2y7mbfCxQwqQ=', 22 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWEHaTZ6LBLFP5OPrUqjDM/cF4b2zrfh1Zm3kd02ZtgQB3iYtZqRPJT5ctT3A7WdVF/7dCxPGOCkJlLekTx4Y4gD8JtjA+EfN9fR/2RBKbti2N3CD4vkGp9ss4hbBFcXIhl8zuD/ELHutbV6b8b4QXJGnxfp/B+1kNPnyd7SJznS0QyvI8OLI1nAkVKdYLDRW8kPKeHyx1xhdNDuTQVTFyAjRGQ4e3UYFB7bYIHW3E6kCtCoJDlj+JPC02Yt1LHzIzZVLvPvNFnYY2mag6OiGFuh/oMBIqvnPc1zRZ3eLUqeGZjQVaoR0kdgZUKz7Q2TBeNldxK/s6XO0DnkQTlelNAgMBAAE=' 23 | }, { 24 | id: 'QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN', 25 | privKey: 'CAASpwkwggSjAgEAAoIBAQC1/GFud/7xutux7qRfMj1sIdMRh99/chR6HqVj6LQqrgk4jil0mdN/LCk/tqPqmDtObHdmEhCoybzuhLbCKgUqryKDwO6yBJHSKWY9QqrKZtLJ37SgKwGjE3+NUD4r1dJHhtQrICFdOdSCBzs/v8gi+J+KZLHo7+Nms4z09ysy7qZh94Pd7cW4gmSMergqUeANLD9C0ERw1NXolswOW7Bi7UGr7yuBxejICLO3nkxe0OtpQBrYrqdCD9vs3t/HQZbPWVoiRj4VO7fxkAPKLl30HzcIfxj/ayg8NHcH59d08D+N2v5Sdh28gsiYKIPE9CXvuw//HUY2WVRY5fDC5JglAgMBAAECggEBAKb5aN/1w3pBqz/HqRMbQpYLNuD33M3PexBNPAy+P0iFpDo63bh5Rz+A4lvuFNmzUX70MFz7qENlzi6+n/zolxMB29YtWBUH8k904rTEjXXl//NviQgITZk106tx+4k2x5gPEm57LYGfBOdFAUzNhzDnE2LkXwRNzkS161f7zKwOEsaGWRscj6UvhO4MIFxjb32CVwt5eK4yOVqtyMs9u30K4Og+AZYTlhtm+bHg6ndCCBO6CQurCQ3jD6YOkT+L3MotKqt1kORpvzIB0ujZRf49Um8wlcjC5G9aexBeGriXaVdPF62zm7GA7RMsbQM/6aRbA1fEQXvJhHUNF9UFeaECgYEA8wCjKqQA7UQnHjRwTsktdwG6szfxd7z+5MTqHHTWhWzgcQLgdh5/dO/zanEoOThadMk5C1Bqjq96gH2xim8dg5XQofSVtV3Ui0dDa+XRB3E3fyY4D3RF5hHv85O0GcvQc6DIb+Ja1oOhvHowFB1C+CT3yEgwzX/EK9xpe+KtYAkCgYEAv7hCnj/DcZFU3fAfS+unBLuVoVJT/drxv66P686s7J8UM6tW+39yDBZ1IcwY9vHFepBvxY2fFfEeLI02QFM+lZXVhNGzFkP90agNHK01psGgrmIufl9zAo8WOKgkLgbYbSHzkkDeqyjEPU+B0QSsZOCE+qLCHSdsnTmo/TjQhj0CgYAz1+j3yfGgrS+jVBC53lXi0+2fGspbf2jqKdDArXSvFqFzuudki/EpY6AND4NDYfB6hguzjD6PnoSGMUrVfAtR7X6LbwEZpqEX7eZGeMt1yQPMDr1bHrVi9mS5FMQR1NfuM1lP9Xzn00GIUpE7WVrWUhzDEBPJY/7YVLf0hFH08QKBgDWBRQZJIVBmkNrHktRrVddaSq4U/d/Q5LrsCrpymYwH8WliHgpeTQPWmKXwAd+ZJdXIzYjCt202N4eTeVqGYOb6Q/anV2WVYBbM4avpIxoA28kPGY6nML+8EyWIt2ApBOmgGgvtEreNzwaVU9NzjHEyv6n7FlVwlT1jxCe3XWq5AoGASYPKQoPeDlW+NmRG7z9EJXJRPVtmLL40fmGgtju9QIjLnjuK8XaczjAWT+ySI93Whu+Eujf2Uj7Q+NfUjvAEzJgwzuOd3jlQvoALq11kuaxlNQTn7rx0A1QhBgUJE8AkvShPC9FEnA4j/CLJU0re9H/8VvyN6qE0Mho0+YbjpP8=', 26 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1/GFud/7xutux7qRfMj1sIdMRh99/chR6HqVj6LQqrgk4jil0mdN/LCk/tqPqmDtObHdmEhCoybzuhLbCKgUqryKDwO6yBJHSKWY9QqrKZtLJ37SgKwGjE3+NUD4r1dJHhtQrICFdOdSCBzs/v8gi+J+KZLHo7+Nms4z09ysy7qZh94Pd7cW4gmSMergqUeANLD9C0ERw1NXolswOW7Bi7UGr7yuBxejICLO3nkxe0OtpQBrYrqdCD9vs3t/HQZbPWVoiRj4VO7fxkAPKLl30HzcIfxj/ayg8NHcH59d08D+N2v5Sdh28gsiYKIPE9CXvuw//HUY2WVRY5fDC5JglAgMBAAE=' 27 | }] 28 | --------------------------------------------------------------------------------