@libp2p/daemon-client 0.0.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/src/index.ts ADDED
@@ -0,0 +1,263 @@
1
+ import errcode from 'err-code'
2
+ import { TCP } from '@libp2p/tcp'
3
+ import { IRequest, Request, Response } from '@libp2p/daemon-protocol'
4
+ import { StreamHandler } from './stream-handler.js'
5
+ import { Multiaddr } from '@multiformats/multiaddr'
6
+ import { DHT } from './dht.js'
7
+ import { Pubsub } from './pubsub.js'
8
+ import { isPeerId, PeerId } from '@libp2p/interfaces/peer-id'
9
+ import { passThroughUpgrader } from './util/index.js'
10
+ import type { ConnectionHandler, Listener } from '@libp2p/interfaces/transport'
11
+ import { peerIdFromBytes } from '@libp2p/peer-id'
12
+ import type { Duplex } from 'it-stream-types'
13
+
14
+ class Client implements DaemonClient {
15
+ private multiaddr: Multiaddr
16
+ public dht: DHT
17
+ public pubsub: Pubsub
18
+ private tcp: TCP
19
+ private listener?: Listener
20
+
21
+ constructor (addr: Multiaddr) {
22
+ this.multiaddr = addr
23
+ this.tcp = new TCP()
24
+
25
+ this.dht = new DHT(this)
26
+ this.pubsub = new Pubsub(this)
27
+ }
28
+
29
+ /**
30
+ * Connects to a daemon at the unix socket path the daemon
31
+ * was created with
32
+ *
33
+ * @async
34
+ * @returns {MultiaddrConnection}
35
+ */
36
+ connectDaemon () {
37
+ return this.tcp.dial(this.multiaddr, {
38
+ upgrader: passThroughUpgrader
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Starts a server listening at `socketPath`. New connections
44
+ * will be sent to the `connectionHandler`.
45
+ *
46
+ * @param {Multiaddr} addr
47
+ * @param {function(Stream)} connectionHandler
48
+ * @returns {Promise}
49
+ */
50
+ async start (addr: Multiaddr, connectionHandler: ConnectionHandler) {
51
+ if (this.listener) {
52
+ await this.close()
53
+ }
54
+
55
+ this.listener = this.tcp.createListener({
56
+ handler: maConn => connectionHandler(maConn),
57
+ upgrader: passThroughUpgrader
58
+ })
59
+
60
+ await this.listener.listen(addr)
61
+ }
62
+
63
+ /**
64
+ * Sends the request to the daemon and returns a stream. This
65
+ * should only be used when sending daemon requests.
66
+ */
67
+ async send (request: IRequest) {
68
+ const maConn = await this.connectDaemon()
69
+
70
+ const streamHandler = new StreamHandler({ stream: maConn })
71
+ streamHandler.write(Request.encode(request).finish())
72
+ return streamHandler
73
+ }
74
+
75
+ /**
76
+ * Closes the socket
77
+ */
78
+ async close () {
79
+ this.listener && await this.listener.close()
80
+ this.listener = undefined
81
+ }
82
+
83
+ /**
84
+ * Connect requests a connection to a known peer on a given set of addresses
85
+ *
86
+ * @param {PeerId} peerId
87
+ * @param {Array.<multiaddr>} addrs
88
+ */
89
+ async connect (peerId: PeerId, addrs: Multiaddr[]) {
90
+ if (!isPeerId(peerId)) {
91
+ throw errcode(new Error('invalid peer id received'), 'ERR_INVALID_PEER_ID')
92
+ }
93
+
94
+ if (!Array.isArray(addrs)) {
95
+ throw errcode(new Error('addrs received are not in an array'), 'ERR_INVALID_ADDRS_TYPE')
96
+ }
97
+
98
+ addrs.forEach((addr) => {
99
+ if (!Multiaddr.isMultiaddr(addr)) {
100
+ throw errcode(new Error('received an address that is not a multiaddr'), 'ERR_NO_MULTIADDR_RECEIVED')
101
+ }
102
+ })
103
+
104
+ const sh = await this.send({
105
+ type: Request.Type.CONNECT,
106
+ connect: {
107
+ peer: peerId.toBytes(),
108
+ addrs: addrs.map((a) => a.bytes)
109
+ }
110
+ })
111
+
112
+ const message = await sh.read()
113
+ if (!message) {
114
+ throw errcode(new Error('unspecified'), 'ERR_CONNECT_FAILED')
115
+ }
116
+
117
+ const response = Response.decode(message)
118
+ if (response.type !== Response.Type.OK) {
119
+ const errResponse = response.error ?? { msg: 'unspecified'}
120
+ throw errcode(new Error(errResponse.msg ?? 'unspecified'), 'ERR_CONNECT_FAILED')
121
+ }
122
+
123
+ await sh.close()
124
+ }
125
+
126
+ /**
127
+ * @typedef {Object} IdentifyResponse
128
+ * @property {PeerId} peerId
129
+ * @property {Array.<multiaddr>} addrs
130
+ */
131
+
132
+ /**
133
+ * Identify queries the daemon for its peer ID and listen addresses.
134
+ */
135
+ async identify (): Promise<IdentifyResult> {
136
+ const sh = await this.send({
137
+ type: Request.Type.IDENTIFY
138
+ })
139
+
140
+ const message = await sh.read()
141
+ const response = Response.decode(message)
142
+
143
+ if (response.type !== Response.Type.OK) {
144
+ throw errcode(new Error(response.error?.msg ?? 'Identify failed'), 'ERR_IDENTIFY_FAILED')
145
+ }
146
+
147
+ if (response.identify == null || response.identify.addrs == null) {
148
+ throw errcode(new Error('Invalid response'), 'ERR_IDENTIFY_FAILED')
149
+ }
150
+
151
+ const peerId = peerIdFromBytes(response.identify?.id)
152
+ const addrs = response.identify.addrs.map((a) => new Multiaddr(a))
153
+
154
+ await sh.close()
155
+
156
+ return ({ peerId, addrs })
157
+ }
158
+
159
+ /**
160
+ * Get a list of IDs of peers the node is connected to
161
+ */
162
+ async listPeers (): Promise<PeerId[]> {
163
+ const sh = await this.send({
164
+ type: Request.Type.LIST_PEERS
165
+ })
166
+
167
+ const message = await sh.read()
168
+ const response = Response.decode(message)
169
+
170
+ if (response.type !== Response.Type.OK) {
171
+ throw errcode(new Error(response.error?.msg ?? 'List peers failed'), 'ERR_LIST_PEERS_FAILED')
172
+ }
173
+
174
+ await sh.close()
175
+
176
+ return response.peers.map((peer) => peerIdFromBytes(peer.id))
177
+ }
178
+
179
+ /**
180
+ * Initiate an outbound stream to a peer on one of a set of protocols.
181
+ */
182
+ async openStream (peerId: PeerId, protocol: string): Promise<Duplex<Uint8Array>> {
183
+ if (!isPeerId(peerId)) {
184
+ throw errcode(new Error('invalid peer id received'), 'ERR_INVALID_PEER_ID')
185
+ }
186
+
187
+ if (typeof protocol !== 'string') {
188
+ throw errcode(new Error('invalid protocol received'), 'ERR_INVALID_PROTOCOL')
189
+ }
190
+
191
+ const sh = await this.send({
192
+ type: Request.Type.STREAM_OPEN,
193
+ streamOpen: {
194
+ peer: peerId.toBytes(),
195
+ proto: [protocol]
196
+ }
197
+ })
198
+
199
+ const message = await sh.read()
200
+ const response = Response.decode(message)
201
+
202
+ if (response.type !== Response.Type.OK) {
203
+ await sh.close()
204
+ throw errcode(new Error(response.error?.msg ?? 'Open stream failed'), 'ERR_OPEN_STREAM_FAILED')
205
+ }
206
+
207
+ return sh.rest()
208
+ }
209
+
210
+ /**
211
+ * Register a handler for inbound streams on a given protocol
212
+ */
213
+ async registerStreamHandler (addr: Multiaddr, protocol: string) {
214
+ if (!Multiaddr.isMultiaddr(addr)) {
215
+ throw errcode(new Error('invalid multiaddr received'), 'ERR_INVALID_MULTIADDR')
216
+ }
217
+
218
+ if (typeof protocol !== 'string') {
219
+ throw errcode(new Error('invalid protocol received'), 'ERR_INVALID_PROTOCOL')
220
+ }
221
+
222
+ const sh = await this.send({
223
+ type: Request.Type.STREAM_HANDLER,
224
+ streamOpen: null,
225
+ streamHandler: {
226
+ addr: addr.bytes,
227
+ proto: [protocol]
228
+ }
229
+ })
230
+
231
+ const message = await sh.read()
232
+ const response = Response.decode(message)
233
+
234
+ await sh.close()
235
+
236
+ if (response.type !== Response.Type.OK) {
237
+ throw errcode(new Error(response.error?.msg ?? 'Register stream handler failed'), 'ERR_REGISTER_STREAM_HANDLER_FAILED')
238
+ }
239
+ }
240
+ }
241
+
242
+ export interface IdentifyResult {
243
+ peerId: PeerId
244
+ addrs: Multiaddr[]
245
+ }
246
+
247
+ export interface DHTClient {
248
+ put: (key: Uint8Array, value: Uint8Array) => Promise<void>
249
+ get: (key: Uint8Array) => Promise<Uint8Array>
250
+ }
251
+
252
+ export interface DaemonClient {
253
+ identify: () => Promise<IdentifyResult>
254
+ listPeers: () => Promise<PeerId[]>
255
+ connect: (peerId: PeerId, addrs: Multiaddr[]) => Promise<void>
256
+ dht: DHTClient
257
+
258
+ send: (request: IRequest) => Promise<StreamHandler>
259
+ }
260
+
261
+ export function createClient (multiaddr: Multiaddr): DaemonClient {
262
+ return new Client(multiaddr)
263
+ }
package/src/pubsub.ts ADDED
@@ -0,0 +1,106 @@
1
+ import errcode from 'err-code'
2
+ import {
3
+ Request,
4
+ Response,
5
+ PSRequest,
6
+ PSMessage
7
+ } from '@libp2p/daemon-protocol'
8
+ import type { DaemonClient } from './index.js'
9
+
10
+ export class Pubsub {
11
+ private client: DaemonClient
12
+
13
+ constructor (client: DaemonClient) {
14
+ this.client = client
15
+ }
16
+
17
+ /**
18
+ * Get a list of topics the node is subscribed to.
19
+ *
20
+ * @returns {Array<string>} topics
21
+ */
22
+ async getTopics (): Promise<string[]> {
23
+ const sh = await this.client.send({
24
+ type: Request.Type.PUBSUB,
25
+ pubsub: {
26
+ type: PSRequest.Type.GET_TOPICS
27
+ }
28
+ })
29
+
30
+ const message = await sh.read()
31
+ const response = Response.decode(message)
32
+
33
+ await sh.close()
34
+
35
+ if (response.type !== Response.Type.OK) {
36
+ throw errcode(new Error(response.error?.msg ?? 'Pubsub get topics failed'), 'ERR_PUBSUB_GET_TOPICS_FAILED')
37
+ }
38
+
39
+ if (response.pubsub == null || response.pubsub.topics == null) {
40
+ throw errcode(new Error('Invalid response'), 'ERR_PUBSUB_GET_TOPICS_FAILED')
41
+ }
42
+
43
+ return response.pubsub.topics
44
+ }
45
+
46
+ /**
47
+ * Publish data under a topic
48
+ */
49
+ async publish (topic: string, data: Uint8Array) {
50
+ if (typeof topic !== 'string') {
51
+ throw errcode(new Error('invalid topic received'), 'ERR_INVALID_TOPIC')
52
+ }
53
+
54
+ if (!(data instanceof Uint8Array)) {
55
+ throw errcode(new Error('data received is not a Uint8Array'), 'ERR_INVALID_DATA')
56
+ }
57
+
58
+ const sh = await this.client.send({
59
+ type: Request.Type.PUBSUB,
60
+ pubsub: {
61
+ type: PSRequest.Type.PUBLISH,
62
+ topic,
63
+ data
64
+ }
65
+ })
66
+
67
+ const message = await sh.read()
68
+ const response = Response.decode(message)
69
+
70
+ await sh.close()
71
+
72
+ if (response.type !== Response.Type.OK) {
73
+ throw errcode(new Error(response.error?.msg ?? 'Pubsub publish failed'), 'ERR_PUBSUB_PUBLISH_FAILED')
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Request to subscribe a certain topic
79
+ */
80
+ async * subscribe (topic: string) {
81
+ if (typeof topic !== 'string') {
82
+ throw errcode(new Error('invalid topic received'), 'ERR_INVALID_TOPIC')
83
+ }
84
+
85
+ const sh = await this.client.send({
86
+ type: Request.Type.PUBSUB,
87
+ pubsub: {
88
+ type: PSRequest.Type.SUBSCRIBE,
89
+ topic
90
+ }
91
+ })
92
+
93
+ let message = await sh.read()
94
+ let response = Response.decode(message)
95
+
96
+ if (response.type !== Response.Type.OK) {
97
+ throw errcode(new Error(response.error?.msg ?? 'Pubsub publish failed'), 'ERR_PUBSUB_PUBLISH_FAILED')
98
+ }
99
+
100
+ // stream messages
101
+ while (true) {
102
+ message = await sh.read()
103
+ yield PSMessage.decode(message)
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,65 @@
1
+ import * as lp from 'it-length-prefixed'
2
+ import { handshake } from 'it-handshake'
3
+ import { logger } from '@libp2p/logger'
4
+ import type { Duplex, Source } from 'it-stream-types'
5
+ import type { Handshake } from 'it-handshake'
6
+
7
+ const log = logger('libp2p:daemon-client:stream-handler')
8
+
9
+ export interface StreamHandlerOptions {
10
+ stream: Duplex<Uint8Array>
11
+ maxLength?: number
12
+ }
13
+
14
+ export class StreamHandler {
15
+ private stream: Duplex<Uint8Array>
16
+ private shake: Handshake
17
+ private decoder: Source<Uint8Array>
18
+ /**
19
+ * Create a stream handler for connection
20
+ */
21
+ constructor (opts: StreamHandlerOptions) {
22
+ const { stream, maxLength } = opts
23
+
24
+ this.stream = stream
25
+ this.shake = handshake(this.stream)
26
+ this.decoder = lp.decode.fromReader(this.shake.reader, { maxDataLength: maxLength ?? 4096 })
27
+ }
28
+
29
+ /**
30
+ * Read and decode message
31
+ */
32
+ async read () {
33
+ // @ts-expect-error decoder is really a generator
34
+ const msg = await this.decoder.next()
35
+ if (msg.value) {
36
+ return msg.value.slice()
37
+ }
38
+ log('read received no value, closing stream')
39
+ // End the stream, we didn't get data
40
+ this.close()
41
+ }
42
+
43
+ write (msg: Uint8Array) {
44
+ log('write message')
45
+ this.shake.write(
46
+ lp.encode.single(msg).slice()
47
+ )
48
+ }
49
+
50
+ /**
51
+ * Return the handshake rest stream and invalidate handler
52
+ */
53
+ rest () {
54
+ this.shake.rest()
55
+ return this.shake.stream
56
+ }
57
+
58
+ /**
59
+ * Close the stream
60
+ */
61
+ close () {
62
+ log('closing the stream')
63
+ this.rest().sink([])
64
+ }
65
+ }
@@ -0,0 +1,27 @@
1
+ import type { Upgrader } from '@libp2p/interfaces/transport'
2
+ import type { Multiaddr } from '@multiformats/multiaddr'
3
+ import { resolve } from 'path'
4
+
5
+ export const passThroughUpgrader: Upgrader = {
6
+ // @ts-expect-error
7
+ upgradeInbound: maConn => maConn,
8
+ // @ts-expect-error
9
+ upgradeOutbound: maConn => maConn
10
+ }
11
+
12
+ /**
13
+ * Converts the multiaddr to a nodejs NET compliant option
14
+ * for .connect or .listen
15
+ *
16
+ * @param {Multiaddr} addr
17
+ * @returns {string|object} A nodejs NET compliant option
18
+ */
19
+ export function multiaddrToNetConfig (addr: Multiaddr) {
20
+ const listenPath = addr.getPath()
21
+ // unix socket listening
22
+ if (listenPath) {
23
+ return resolve(listenPath)
24
+ }
25
+ // tcp listening
26
+ return addr.nodeAddress()
27
+ }