helia-coord 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.on-save.json +8 -0
- package/PEDIGREE.md +3 -0
- package/README.md +3 -0
- package/config/bootstrap-circuit-relays.js +48 -0
- package/examples/start-external.js +56 -0
- package/index.js +129 -0
- package/lib/adapters/bch-adapter.js +94 -0
- package/lib/adapters/encryption-adapter.js +123 -0
- package/lib/adapters/gist.js +42 -0
- package/lib/adapters/index.js +66 -0
- package/lib/adapters/ipfs-adapter.js +263 -0
- package/lib/adapters/logs-adapter.js +44 -0
- package/lib/adapters/pubsub-adapter/README.md +86 -0
- package/lib/adapters/pubsub-adapter/about-adapter.js +117 -0
- package/lib/adapters/pubsub-adapter/index.js +275 -0
- package/lib/adapters/pubsub-adapter/messaging.js +389 -0
- package/lib/adapters/pubsub-adapter/msg-router.js +58 -0
- package/lib/adapters/pubsub-adapter/resend-msg.js +58 -0
- package/lib/controllers/index.js +24 -0
- package/lib/controllers/timer-controller.js +417 -0
- package/lib/entities/this-node-entity.js +102 -0
- package/lib/use-cases/index.js +36 -0
- package/lib/use-cases/peer-use-cases.js +146 -0
- package/lib/use-cases/pubsub-use-cases.js +56 -0
- package/lib/use-cases/relay-use-cases.js +479 -0
- package/lib/use-cases/schema.js +158 -0
- package/lib/use-cases/this-node-use-cases.js +443 -0
- package/lib/util/utils.js +12 -0
- package/package.json +52 -0
- package/test/mocks/adapter-mock.js +119 -0
- package/test/mocks/circuit-relay-mocks.js +67 -0
- package/test/mocks/ipfs-mock.js +46 -0
- package/test/mocks/peers-mock.js +75 -0
- package/test/mocks/pubsub-mocks.js +37 -0
- package/test/mocks/thisnode-mocks.js +82 -0
- package/test/mocks/use-case-mocks.js +24 -0
- package/test/unit/adapters/bch-adapter-unit.js +96 -0
- package/test/unit/adapters/encryption-adapter-unit.js +129 -0
- package/test/unit/adapters/gist.unit.adapters.js +58 -0
- package/test/unit/adapters/index-adapters-unit.js +79 -0
- package/test/unit/adapters/ipfs-adapter-unit.js +215 -0
- package/test/unit/adapters/logs-adapter-unit.js +55 -0
- package/test/unit/adapters/pubsub/about-adapter-unit.js +129 -0
- package/test/unit/adapters/pubsub/messaging-adapter-unit.js +576 -0
- package/test/unit/adapters/pubsub/pubsub-adapter-unit.js +367 -0
- package/test/unit/adapters/pubsub/resend-msg-adapter-unit.js +58 -0
- package/test/unit/controllers/controllers-index-unit.js +30 -0
- package/test/unit/controllers/timer-controller-unit.js +261 -0
- package/test/unit/entities/this-node.unit.entity.js +157 -0
- package/test/unit/index-unit.js +160 -0
- package/test/unit/use-cases/peer.unit.use-cases.js +186 -0
- package/test/unit/use-cases/pubsub.unit.use-cases.js +114 -0
- package/test/unit/use-cases/relay-use-cases-unit.js +658 -0
- package/test/unit/use-cases/schema-use-case-unit.js +101 -0
- package/test/unit/use-cases/this-node-use-cases-unit.js +427 -0
- package/test/unit/use-cases/use-cases-index-unit.js +47 -0
- package/test/unit/util/utils-unit.js +31 -0
|
@@ -0,0 +1,158 @@
|
|
|
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,
|
|
135
|
+
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
|
+
export default Schema
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Use Cases library for the thisNode entity.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import ThisNodeEntity from '../entities/this-node-entity.js'
|
|
6
|
+
import Schema from './schema.js'
|
|
7
|
+
|
|
8
|
+
// A global variable that maintains scope to the instance of this class, when
|
|
9
|
+
// the context of 'this' is lost.
|
|
10
|
+
let _this
|
|
11
|
+
|
|
12
|
+
class ThisNodeUseCases {
|
|
13
|
+
constructor (localConfig = {}) {
|
|
14
|
+
// Dependency Injection.
|
|
15
|
+
this.adapters = localConfig.adapters
|
|
16
|
+
if (!this.adapters) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Must inject instance of adapters when instantiating thisNode Use Cases library.'
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Optional JSON-LD used for announcements. If present, will override
|
|
23
|
+
// default announcement object in Schema library.
|
|
24
|
+
this.announceJsonLd = localConfig.announceJsonLd
|
|
25
|
+
|
|
26
|
+
// If consuming app wants to configure itself as a Circuit Relay, it can
|
|
27
|
+
// override the default value of false.
|
|
28
|
+
this.isCircuitRelay = localConfig.isCircuitRelay
|
|
29
|
+
if (!this.isCircuitRelay) this.isCircuitRelay = false
|
|
30
|
+
|
|
31
|
+
// Additional information for connecting to the circuit relay.
|
|
32
|
+
this.circuitRelayInfo = localConfig.circuitRelayInfo
|
|
33
|
+
|
|
34
|
+
_this = this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Update this instance with copies of the other Use Case libraries.
|
|
38
|
+
updateUseCases (useCaseParent) {
|
|
39
|
+
this.useCases = {
|
|
40
|
+
relays: useCaseParent.relays,
|
|
41
|
+
pubsub: useCaseParent.pubsub,
|
|
42
|
+
peer: useCaseParent.peer
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Create an instance of the 'self' of thisNode. This function aggregates
|
|
47
|
+
// a lot of information pulled from the different adapters.
|
|
48
|
+
async createSelf (initValues = {}) {
|
|
49
|
+
const selfData = {
|
|
50
|
+
// The type of IPFS node this is: browser or node.js
|
|
51
|
+
type: initValues.type
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Aggregate data from the IPFS adapter.
|
|
55
|
+
selfData.ipfsId = this.adapters.ipfs.ipfsPeerId
|
|
56
|
+
selfData.ipfsMultiaddrs = this.adapters.ipfs.ipfsMultiaddrs
|
|
57
|
+
|
|
58
|
+
// Aggregate data from the BCH adapter.
|
|
59
|
+
const bchData = await this.adapters.bch.generateBchId()
|
|
60
|
+
selfData.bchAddr = bchData.cashAddress
|
|
61
|
+
selfData.slpAddr = bchData.slpAddress
|
|
62
|
+
selfData.publicKey = bchData.publicKey
|
|
63
|
+
// selfData.mnemonic = this.adapters.bch.mnemonic
|
|
64
|
+
|
|
65
|
+
// Generate an OrbitDB for other peers to pass messages to this node.
|
|
66
|
+
// TODO: deprecate
|
|
67
|
+
// selfData.orbit = await this.adapters.orbit.createRcvDb(selfData)
|
|
68
|
+
|
|
69
|
+
const schemaConfig = {
|
|
70
|
+
ipfsId: selfData.ipfsId,
|
|
71
|
+
type: selfData.type,
|
|
72
|
+
ipfsMultiaddrs: selfData.ipfsMultiaddrs,
|
|
73
|
+
isCircuitRelay: this.isCircuitRelay,
|
|
74
|
+
circuitRelayInfo: this.circuitRelayInfo,
|
|
75
|
+
cashAddress: selfData.bchAddr,
|
|
76
|
+
slpAddress: selfData.slpAddr,
|
|
77
|
+
publicKey: selfData.publicKey,
|
|
78
|
+
// orbitdbId: selfData.orbit.id,
|
|
79
|
+
// apiInfo: '',
|
|
80
|
+
announceJsonLd: this.announceJsonLd
|
|
81
|
+
}
|
|
82
|
+
selfData.schema = new Schema(schemaConfig)
|
|
83
|
+
|
|
84
|
+
// console.log('selfData: ', selfData)
|
|
85
|
+
|
|
86
|
+
// Create the thisNode entity
|
|
87
|
+
const thisNode = new ThisNodeEntity(selfData)
|
|
88
|
+
this.thisNode = thisNode
|
|
89
|
+
|
|
90
|
+
// Attach ther useCases (which includes adapters and controllers) to the
|
|
91
|
+
// thisNode entity.
|
|
92
|
+
this.thisNode.useCases = this.useCases
|
|
93
|
+
|
|
94
|
+
// Subscribe to my own pubsub channel, for receiving info from other peers.
|
|
95
|
+
selfData.pubsub = await this.adapters.pubsub.subscribeToPubsubChannel(
|
|
96
|
+
selfData.ipfsId,
|
|
97
|
+
this.tempHander,
|
|
98
|
+
this.thisNode
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return thisNode
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// This is an event handler that is triggered by a new announcement object
|
|
105
|
+
// being recieved on the general coordination pubsub channel.
|
|
106
|
+
// pubsub-use-cases.js/initializePubSub() depends on this function.
|
|
107
|
+
async addSubnetPeer (announceObj) {
|
|
108
|
+
try {
|
|
109
|
+
// console.log('announceObj: ', announceObj)
|
|
110
|
+
|
|
111
|
+
// Exit if the announcement object is stale.
|
|
112
|
+
if (!_this.isFreshPeer(announceObj)) return
|
|
113
|
+
|
|
114
|
+
const thisPeerId = announceObj.from.toString()
|
|
115
|
+
_this.adapters.log.statusLog(
|
|
116
|
+
2,
|
|
117
|
+
`announcement recieved from ${thisPeerId}`
|
|
118
|
+
)
|
|
119
|
+
_this.adapters.log.statusLog(
|
|
120
|
+
3,
|
|
121
|
+
`announcement recieved from ${thisPeerId}: `,
|
|
122
|
+
announceObj
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// console.log('_this.thisNode.peerList: ', _this.thisNode.peerList)
|
|
126
|
+
|
|
127
|
+
// Add a timestamp.
|
|
128
|
+
const now = new Date()
|
|
129
|
+
announceObj.data.updatedAt = now.toISOString()
|
|
130
|
+
|
|
131
|
+
// If the peer is not already in the list of known peers, then add it.
|
|
132
|
+
if (!_this.thisNode.peerList.includes(thisPeerId)) {
|
|
133
|
+
_this.adapters.log.statusLog(0, `New peer found: ${thisPeerId}`)
|
|
134
|
+
|
|
135
|
+
// Add this peer to the list of subnet peers tracked by this node.
|
|
136
|
+
_this.thisNode.peerList.push(thisPeerId)
|
|
137
|
+
|
|
138
|
+
// Add the announcement data object to the peerData array tracked by This Node.
|
|
139
|
+
_this.thisNode.peerData.push(announceObj)
|
|
140
|
+
|
|
141
|
+
// Subscribe to pubsub channel for private messages to peer.
|
|
142
|
+
// Ignore any messages on this channel, since it is only used for
|
|
143
|
+
// broadcasting encrypted messages to the new peer, and they will
|
|
144
|
+
// respond on our own channel.
|
|
145
|
+
await _this.adapters.ipfs.ipfs.pubsub.subscribe(
|
|
146
|
+
thisPeerId,
|
|
147
|
+
async msg => {
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// If the new peer has the isCircuitRelay flag set, then try to add it
|
|
152
|
+
// to the list of Circuit Relays.
|
|
153
|
+
if (announceObj.data.isCircuitRelay) {
|
|
154
|
+
// console.log('_this.thisNode: ', _this.thisNode)
|
|
155
|
+
await _this.useCases.relays.addRelay(thisPeerId, _this.thisNode)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Subscribe to the private OrbitDB for this peer.
|
|
159
|
+
// await _this.adapters.orbit.connectToPeerDb({
|
|
160
|
+
// peerId: announceObj.from,
|
|
161
|
+
// orbitdbId: announceObj.data.orbitdb,
|
|
162
|
+
// thisNode: _this.thisNode
|
|
163
|
+
// })
|
|
164
|
+
// _this.adapters.log.statusLog(
|
|
165
|
+
// 2,
|
|
166
|
+
// `Connected to peer private OrbitDB: ${announceObj.data.orbitdb}`
|
|
167
|
+
// )
|
|
168
|
+
} else {
|
|
169
|
+
// Peer already exists in the list.
|
|
170
|
+
// console.log(`debug: Updating existing peer: ${thisPeerId}`)
|
|
171
|
+
|
|
172
|
+
// Get the data for this peer.
|
|
173
|
+
let thisPeerData = _this.thisNode.peerData.filter(
|
|
174
|
+
x => x.from === thisPeerId
|
|
175
|
+
)
|
|
176
|
+
thisPeerData = thisPeerData[0]
|
|
177
|
+
const dataIndex = _this.thisNode.peerData.indexOf(thisPeerData)
|
|
178
|
+
// console.log(`dataIndex: ${dataIndex}`)
|
|
179
|
+
|
|
180
|
+
// If the new announceObj is older than the last announceObj, then
|
|
181
|
+
// ignore it.
|
|
182
|
+
const oldBroadcastDate = new Date(thisPeerData.data.broadcastedAt)
|
|
183
|
+
const newBroadcastDate = new Date(announceObj.data.broadcastedAt)
|
|
184
|
+
if (newBroadcastDate.getTime() < oldBroadcastDate.getTime()) {
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Replace the old data with the new data.
|
|
189
|
+
_this.thisNode.peerData[dataIndex] = announceObj
|
|
190
|
+
|
|
191
|
+
// console.log('announceObj.data.orbitdb: ', announceObj.data.orbitdb)
|
|
192
|
+
// console.log('thisPeerData: ', thisPeerData)
|
|
193
|
+
|
|
194
|
+
// Update the connection to the peers OrbitDB, if needed.
|
|
195
|
+
// const announceOrbitId = announceObj.data.orbitdb
|
|
196
|
+
// const peerOrbitId = thisPeerData.data.orbitdb
|
|
197
|
+
// if (!announceOrbitId.includes(peerOrbitId)) {
|
|
198
|
+
// // console.log(`debug: Switching to new OrbitDB for known peer: ${announceOrbitId}`)
|
|
199
|
+
// _this.adapters.log.statusLog(
|
|
200
|
+
// 0,
|
|
201
|
+
// `debug: Switching to new OrbitDB for known peer: ${announceOrbitId}`
|
|
202
|
+
// )
|
|
203
|
+
// // console.log(
|
|
204
|
+
// // `debug: announceObj: ${JSON.stringify(announceObj, null, 2)}`
|
|
205
|
+
// // )
|
|
206
|
+
//
|
|
207
|
+
// await _this.adapters.orbit.connectToPeerDb({
|
|
208
|
+
// peerId: announceObj.from,
|
|
209
|
+
// orbitdbId: announceObj.data.orbitdb,
|
|
210
|
+
// thisNode: _this.thisNode
|
|
211
|
+
// })
|
|
212
|
+
// }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return true
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('Error in this-node-use-cases.js/addSubnetPeer(): ', err)
|
|
218
|
+
|
|
219
|
+
_this.adapters.log.statusLog(
|
|
220
|
+
2,
|
|
221
|
+
'Error in this-node-use-cases.js/addSubnetPeer(): ',
|
|
222
|
+
err
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
// Do not throw an error. This is a top-level function called by the
|
|
226
|
+
// pubsub handler for the general coordination channel.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Detects if a peer has gone 'stale' (inactive), or is still 'fresh' (active).
|
|
231
|
+
// Stale means it hasn't updated it's
|
|
232
|
+
// broadcastedAt property in more than 10 minutes.
|
|
233
|
+
// Return true if the peer is 'fresh', and false if 'stale'.
|
|
234
|
+
isFreshPeer (announceObj) {
|
|
235
|
+
try {
|
|
236
|
+
// Ignore announcements that do not have a broadcastedAt timestamp.
|
|
237
|
+
if (!announceObj.data.broadcastedAt) return false
|
|
238
|
+
|
|
239
|
+
// Ignore items that are older than 10 minutes.
|
|
240
|
+
const now = new Date()
|
|
241
|
+
const broadcastTime = new Date(announceObj.data.broadcastedAt)
|
|
242
|
+
const tenMinutes = 60000 * 10
|
|
243
|
+
const timeDiff = now.getTime() - broadcastTime.getTime()
|
|
244
|
+
if (timeDiff > tenMinutes) return false
|
|
245
|
+
|
|
246
|
+
return true
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error('Error in stalePeer()')
|
|
249
|
+
throw err
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Called by an Interval, ensures connections are maintained to known pubsub
|
|
254
|
+
// peers. This will heal connections if nodes drop in and out of the network.
|
|
255
|
+
async refreshPeerConnections () {
|
|
256
|
+
try {
|
|
257
|
+
// console.log('this.thisNode: ', this.thisNode)
|
|
258
|
+
|
|
259
|
+
// const relays = this.cr.state.relays
|
|
260
|
+
const relays = this.thisNode.relayData
|
|
261
|
+
const peers = this.thisNode.peerList
|
|
262
|
+
// console.log('peers: ', peers)
|
|
263
|
+
|
|
264
|
+
// Get connected peers
|
|
265
|
+
const connectedPeers = await this.adapters.ipfs.getPeers()
|
|
266
|
+
// console.log('connectedPeers: ', connectedPeers)
|
|
267
|
+
|
|
268
|
+
// Loop through each known peer
|
|
269
|
+
for (let i = 0; i < peers.length; i++) {
|
|
270
|
+
// const thisPeer = this.state.peers[peer]
|
|
271
|
+
const thisPeer = peers[i]
|
|
272
|
+
// console.log(`thisPeer: ${JSON.stringify(thisPeer, null, 2)}`)
|
|
273
|
+
|
|
274
|
+
// Check if target peer is currently conected to the node.
|
|
275
|
+
const connectedPeer = connectedPeers.filter(
|
|
276
|
+
peerObj => peerObj.peer === thisPeer
|
|
277
|
+
)
|
|
278
|
+
// console.log('connectedPeer: ', connectedPeer)
|
|
279
|
+
|
|
280
|
+
// If this node is already connected to the peer, then skip this peers.
|
|
281
|
+
// We do not need to do anything.
|
|
282
|
+
if (connectedPeer.length) {
|
|
283
|
+
this.adapters.log.statusLog(
|
|
284
|
+
2,
|
|
285
|
+
`Skipping peer in refreshPeerConnections(). Already connected to peer ${thisPeer}`
|
|
286
|
+
)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Get the peer data for the current peer.
|
|
291
|
+
let peerData = this.thisNode.peerData.filter(x => x.from.includes(thisPeer)
|
|
292
|
+
)
|
|
293
|
+
peerData = peerData[0]
|
|
294
|
+
// console.log('peerData: ', peerData)
|
|
295
|
+
|
|
296
|
+
// TODO: if broadcastedAt value is older than 10 minutes, skip connecting
|
|
297
|
+
// to the peer. It may be stale information.
|
|
298
|
+
if (!this.isFreshPeer(peerData)) {
|
|
299
|
+
this.adapters.log.statusLog(
|
|
300
|
+
2,
|
|
301
|
+
`Peer ${peerData.from} is stale. Skipping.`
|
|
302
|
+
)
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Sort the Circuit Relays by the average of the aboutLatency
|
|
307
|
+
// array. Connect to peers through the Relays with the lowest latencies
|
|
308
|
+
// first.
|
|
309
|
+
const sortedRelays = this.thisNode.useCases.relays.sortRelays(relays)
|
|
310
|
+
// console.log(`sortedRelays: ${JSON.stringify(sortedRelays, null, 2)}`)
|
|
311
|
+
|
|
312
|
+
// Loop through each known circuit relay and attempt to connect to the
|
|
313
|
+
// peer through a relay.
|
|
314
|
+
for (let i = 0; i < sortedRelays.length; i++) {
|
|
315
|
+
const thisRelay = sortedRelays[i]
|
|
316
|
+
// console.log(`thisRelay: ${JSON.stringify(thisRelay, null, 2)}`)
|
|
317
|
+
|
|
318
|
+
// Generate a multiaddr for connecting to the peer through a circuit relay.
|
|
319
|
+
const multiaddr = `${thisRelay.multiaddr}/p2p-circuit/p2p/${thisPeer}`
|
|
320
|
+
// console.log(`multiaddr: ${multiaddr}`)
|
|
321
|
+
|
|
322
|
+
// Skip the relay if this node is not connected to it.
|
|
323
|
+
if (thisRelay.connected) {
|
|
324
|
+
// Attempt to connect to the node through a circuit relay.
|
|
325
|
+
const connected = await this.adapters.ipfs.connectToPeer(multiaddr)
|
|
326
|
+
|
|
327
|
+
// If the connection was successful, break out of the relay loop.
|
|
328
|
+
// Otherwise try to connect through the next relay.
|
|
329
|
+
if (connected) {
|
|
330
|
+
// this.adapters.log.statusLog(0,
|
|
331
|
+
// 'Successfully connected to peer through circuit relay.'
|
|
332
|
+
// )
|
|
333
|
+
|
|
334
|
+
// Break out of the loop once we've made a successful connection.
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const now = new Date()
|
|
342
|
+
this.adapters.log.statusLog(
|
|
343
|
+
2,
|
|
344
|
+
`Renewed connections to all known peers at ${now.toLocaleString()}`
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return true
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.error('Error in refreshPeerConnections()')
|
|
350
|
+
throw err
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Enforce the blacklist by actively disconnecting from nodes in the blacklist.
|
|
355
|
+
async enforceBlacklist () {
|
|
356
|
+
try {
|
|
357
|
+
const blacklistPeers = this.thisNode.blacklistPeers
|
|
358
|
+
for (let i = 0; i < blacklistPeers.length; i++) {
|
|
359
|
+
const ipfsId = blacklistPeers[i]
|
|
360
|
+
|
|
361
|
+
await this.adapters.ipfs.disconnectFromPeer(ipfsId)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const blacklistMultiaddrs = this.thisNode.blacklistMultiaddrs
|
|
365
|
+
for (let i = 0; i < blacklistMultiaddrs.length; i++) {
|
|
366
|
+
const multiaddr = blacklistMultiaddrs[i]
|
|
367
|
+
|
|
368
|
+
await this.adapters.ipfs.disconnectFromMultiaddr(multiaddr)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return true
|
|
372
|
+
} catch (err) {
|
|
373
|
+
console.error('Error in enforceBlacklist()')
|
|
374
|
+
throw err
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// An alternative to the blacklist, it's a whitelist, where all nodes are
|
|
379
|
+
// disconnected except ones that have a 'name' property. This ensures that
|
|
380
|
+
// the node only connects to other nodes that are using ipfs-coord.
|
|
381
|
+
async enforceWhitelist () {
|
|
382
|
+
try {
|
|
383
|
+
let showDebugData = false
|
|
384
|
+
|
|
385
|
+
// Get all peers.
|
|
386
|
+
const allPeers = await this.adapters.ipfs.getPeers()
|
|
387
|
+
// console.log(`allPeers: ${JSON.stringify(allPeers, null, 2)}`)
|
|
388
|
+
|
|
389
|
+
// Get ipfs-coord peers.
|
|
390
|
+
const coordPeers = this.thisNode.peerData
|
|
391
|
+
// console.log(`coordPeers: ${JSON.stringify(coordPeers, null, 2)}`)
|
|
392
|
+
|
|
393
|
+
// Try to match each peer up with ipfs-coord info.
|
|
394
|
+
// Add the name from the ipfs-coord info.
|
|
395
|
+
for (let i = 0; i < allPeers.length; i++) {
|
|
396
|
+
const thisPeer = allPeers[i]
|
|
397
|
+
thisPeer.name = ''
|
|
398
|
+
|
|
399
|
+
// If IPFS peer exists in the ipfs-coord peer list, then foundPeer
|
|
400
|
+
// will be an array with 1 element.
|
|
401
|
+
const foundPeer = coordPeers.filter(x => x.from.toString().includes(thisPeer.peer.toString())
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
// If a connected peer matches an ipfs-coord peer, add the 'name'
|
|
405
|
+
// property to it.
|
|
406
|
+
if (!foundPeer.length) {
|
|
407
|
+
this.adapters.log.statusLog(
|
|
408
|
+
3,
|
|
409
|
+
`Whitelist enforcement disconnecting peer ${thisPeer.peer.toString()}`
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
showDebugData = true
|
|
413
|
+
|
|
414
|
+
// If a connected peer can not be matched with the list of ipfs-coord
|
|
415
|
+
// peers, then disconnect from it.
|
|
416
|
+
await this.adapters.ipfs.disconnectFromPeer(thisPeer.peer.toString())
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Show the debugging data once, instead of inside the for-loop, which would
|
|
421
|
+
// show the debugging data each time a peer is disconnected.
|
|
422
|
+
if (showDebugData) {
|
|
423
|
+
// Deep debugging.
|
|
424
|
+
this.adapters.log.statusLog(
|
|
425
|
+
3,
|
|
426
|
+
`allPeers: ${JSON.stringify(allPeers, null, 2)}`
|
|
427
|
+
)
|
|
428
|
+
this.adapters.log.statusLog(
|
|
429
|
+
3,
|
|
430
|
+
`coordPeers: ${JSON.stringify(coordPeers, null, 2)}`
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return true
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.error('Error in enforceWhitelist()')
|
|
437
|
+
throw err
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// module.exports = ThisNodeUseCases
|
|
443
|
+
export default ThisNodeUseCases
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "helia-coord",
|
|
3
|
+
"version": "1.1.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
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "npm run lint && cross-env 'NODE_OPTIONS=--experimental-loader @istanbuljs/esm-loader-hook' nyc mocha --recursive --exit test/unit/",
|
|
9
|
+
"lint": "standard --env mocha --fix",
|
|
10
|
+
"coverage:report": "cross-env 'NODE_OPTIONS=--experimental-loader @istanbuljs/esm-loader-hook' nyc --reporter=html mocha test/unit/ --recursive --exit"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Permissionless-Software-Foundation/helia-coord.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "Chris Troutner <chris.troutner@gmail.com>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/Permissionless-Software-Foundation/helia-coord/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/Permissionless-Software-Foundation/helia-coord#readme",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@istanbuljs/esm-loader-hook": "0.2.0",
|
|
24
|
+
"chai": "4.3.6",
|
|
25
|
+
"cross-env": "7.0.3",
|
|
26
|
+
"lodash.clonedeep": "4.5.0",
|
|
27
|
+
"minimal-slp-wallet": "5.8.9",
|
|
28
|
+
"mocha": "10.0.0",
|
|
29
|
+
"nyc": "15.1.0",
|
|
30
|
+
"sinon": "14.0.0",
|
|
31
|
+
"standard": "17.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"bch-encrypt-lib": "2.0.0",
|
|
35
|
+
"uuid": "9.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"ipfs-http-client": ">= 58.0.0"
|
|
39
|
+
},
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"import": {
|
|
43
|
+
"browser": "./index.js",
|
|
44
|
+
"node": "./index.js",
|
|
45
|
+
"default": "./index.js"
|
|
46
|
+
},
|
|
47
|
+
"require": {
|
|
48
|
+
"default": "./index.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|