Skip to content

Commit ddfff1b

Browse files
feat: add support for libp2p ContentRouting and PeerRouting (#44)
Enables passing a client instance as a libp2p service which will detect it's `ContentRouting` and `PeerRouting` capabilities and configure them for use. Obviates the need for modules like [@libp2p/delegated-routing-v1-http-api-content-routing](https://github.com/libp2p/js-delegated-routing-v1-http-api-content-routing). --------- Co-authored-by: Russell Dempsey <[email protected]>
1 parent a958569 commit ddfff1b

File tree

7 files changed

+482
-13
lines changed

7 files changed

+482
-13
lines changed

packages/client/.aegir.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import body from 'body-parser'
55
const options = {
66
test: {
77
before: async () => {
8+
let callCount = 0
89
const providers = new Map()
910
const peers = new Map()
1011
const ipnsGet = new Map()
@@ -13,45 +14,60 @@ const options = {
1314
echo.polka.use(body.raw({ type: 'application/vnd.ipfs.ipns-record'}))
1415
echo.polka.use(body.text())
1516
echo.polka.post('/add-providers/:cid', (req, res) => {
17+
callCount++
1618
providers.set(req.params.cid, req.body)
1719
res.end()
1820
})
1921
echo.polka.get('/routing/v1/providers/:cid', (req, res) => {
22+
callCount++
2023
const records = providers.get(req.params.cid) ?? '[]'
2124
providers.delete(req.params.cid)
2225

2326
res.end(records)
2427
})
2528
echo.polka.post('/add-peers/:peerId', (req, res) => {
29+
callCount++
2630
peers.set(req.params.peerId, req.body)
2731
res.end()
2832
})
2933
echo.polka.get('/routing/v1/peers/:peerId', (req, res) => {
34+
callCount++
3035
const records = peers.get(req.params.peerId) ?? '[]'
3136
peers.delete(req.params.peerId)
3237

3338
res.end(records)
3439
})
3540
echo.polka.post('/add-ipns/:peerId', (req, res) => {
41+
callCount++
3642
ipnsGet.set(req.params.peerId, req.body)
3743
res.end()
3844
})
3945
echo.polka.get('/routing/v1/ipns/:peerId', (req, res) => {
46+
callCount++
4047
const record = ipnsGet.get(req.params.peerId) ?? ''
4148
ipnsGet.delete(req.params.peerId)
4249

4350
res.end(record)
4451
})
4552
echo.polka.put('/routing/v1/ipns/:peerId', (req, res) => {
53+
callCount++
4654
ipnsPut.set(req.params.peerId, req.body)
4755
res.end()
4856
})
4957
echo.polka.get('/get-ipns/:peerId', (req, res) => {
58+
callCount++
5059
const record = ipnsPut.get(req.params.peerId) ?? ''
5160
ipnsPut.delete(req.params.peerId)
5261

5362
res.end(record)
5463
})
64+
echo.polka.get('/get-call-count', (req, res) => {
65+
res.end(callCount.toString())
66+
})
67+
echo.polka.get('/reset-call-count', (req, res) => {
68+
callCount = 0
69+
res.end()
70+
})
5571

5672
await echo.start()
5773

packages/client/README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,44 @@
1313
1414
## About
1515

16-
A client implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/)
17-
that can be used to interact with any compliant server implementation.
16+
A client implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/) that can be used to interact with any compliant server implementation.
1817

1918
### Example
2019

2120
```typescript
22-
import { createRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
21+
import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
2322
import { CID } from 'multiformats/cid'
2423

25-
const client = createRoutingV1HttpApiClient(new URL('https://example.org'))
24+
const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
2625

2726
for await (const prov of getProviders(CID.parse('QmFoo'))) {
2827
// ...
2928
}
3029
```
3130

31+
### How to use with libp2p
32+
33+
The client can be configured as a libp2p service, this will enable it as both a ContentRouting and a PeerRouting implementation
34+
35+
### Example
36+
37+
```typescript
38+
import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
39+
import { createLibp2p } from 'libp2p'
40+
import { peerIdFromString } from '@libp2p/peer-id'
41+
42+
const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
43+
const libp2p = await createLibp2p({
44+
// other config here
45+
services: {
46+
delegatedRouting: client
47+
}
48+
})
49+
50+
// later this will use the configured HTTP gateway
51+
await libp2p.peerRouting.findPeer(peerIdFromString('QmFoo'))
52+
```
53+
3254
## Install
3355

3456
```console

packages/client/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,18 @@
137137
"any-signal": "^4.1.1",
138138
"browser-readablestream-to-it": "^2.0.3",
139139
"ipns": "^7.0.1",
140-
"it-all": "^3.0.2",
140+
"it-first": "^3.0.3",
141+
"it-map": "^3.0.4",
141142
"it-ndjson": "^1.0.4",
142143
"multiformats": "^12.1.1",
143144
"p-defer": "^4.0.0",
144-
"p-queue": "^7.3.4"
145+
"p-queue": "^7.3.4",
146+
"uint8arrays": "^4.0.6"
145147
},
146148
"devDependencies": {
147149
"@libp2p/peer-id-factory": "^3.0.5",
148150
"aegir": "^41.0.0",
149-
"body-parser": "^1.20.2"
151+
"body-parser": "^1.20.2",
152+
"it-all": "^3.0.2"
150153
}
151154
}

packages/client/src/client.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { type ContentRouting, contentRouting } from '@libp2p/interface/content-routing'
12
import { CodeError } from '@libp2p/interface/errors'
3+
import { type PeerRouting, peerRouting } from '@libp2p/interface/peer-routing'
24
import { logger } from '@libp2p/logger'
35
import { peerIdFromString } from '@libp2p/peer-id'
46
import { multiaddr } from '@multiformats/multiaddr'
@@ -9,6 +11,7 @@ import { ipnsValidator } from 'ipns/validator'
911
import { parse as ndjson } from 'it-ndjson'
1012
import defer from 'p-defer'
1113
import PQueue from 'p-queue'
14+
import { DelegatedRoutingV1HttpApiClientContentRouting, DelegatedRoutingV1HttpApiClientPeerRouting } from './routings.js'
1215
import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, PeerRecord } from './index.js'
1316
import type { AbortOptions } from '@libp2p/interface'
1417
import type { PeerId } from '@libp2p/interface/peer-id'
@@ -27,6 +30,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
2730
private readonly shutDownController: AbortController
2831
private readonly clientUrl: URL
2932
private readonly timeout: number
33+
private readonly contentRouting: ContentRouting
34+
private readonly peerRouting: PeerRouting
3035

3136
/**
3237
* Create a new DelegatedContentRouting instance
@@ -39,6 +44,16 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
3944
})
4045
this.clientUrl = url instanceof URL ? url : new URL(url)
4146
this.timeout = init.timeout ?? defaultValues.timeout
47+
this.contentRouting = new DelegatedRoutingV1HttpApiClientContentRouting(this)
48+
this.peerRouting = new DelegatedRoutingV1HttpApiClientPeerRouting(this)
49+
}
50+
51+
get [contentRouting] (): ContentRouting {
52+
return this.contentRouting
53+
}
54+
55+
get [peerRouting] (): PeerRouting {
56+
return this.peerRouting
4257
}
4358

4459
isStarted (): boolean {

packages/client/src/index.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
11
/**
22
* @packageDocumentation
33
*
4-
* A client implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/)
5-
* that can be used to interact with any compliant server implementation.
4+
* A client implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/) that can be used to interact with any compliant server implementation.
65
*
76
* @example
87
*
98
* ```typescript
10-
* import { createRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
9+
* import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
1110
* import { CID } from 'multiformats/cid'
1211
*
13-
* const client = createRoutingV1HttpApiClient(new URL('https://example.org'))
12+
* const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
1413
*
1514
* for await (const prov of getProviders(CID.parse('QmFoo'))) {
1615
* // ...
1716
* }
1817
* ```
18+
*
19+
* ### How to use with libp2p
20+
*
21+
* The client can be configured as a libp2p service, this will enable it as both a {@link https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface.content_routing.ContentRouting.html | ContentRouting} and a {@link https://libp2p.github.io/js-libp2p/interfaces/_libp2p_interface.peer_routing.PeerRouting.html | PeerRouting} implementation
22+
*
23+
* @example
24+
*
25+
* ```typescript
26+
* import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
27+
* import { createLibp2p } from 'libp2p'
28+
* import { peerIdFromString } from '@libp2p/peer-id'
29+
*
30+
* const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
31+
* const libp2p = await createLibp2p({
32+
* // other config here
33+
* services: {
34+
* delegatedRouting: client
35+
* }
36+
* })
37+
*
38+
* // later this will use the configured HTTP gateway
39+
* await libp2p.peerRouting.findPeer(peerIdFromString('QmFoo'))
40+
* ```
1941
*/
2042

2143
import { DefaultDelegatedRoutingV1HttpApiClient } from './client.js'
@@ -79,6 +101,6 @@ export interface DelegatedRoutingV1HttpApiClient {
79101
/**
80102
* Create and return a client to use with a Routing V1 HTTP API server
81103
*/
82-
export function createDelegatedRoutingV1HttpApiClient (url: URL, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
83-
return new DefaultDelegatedRoutingV1HttpApiClient(url, init)
104+
export function createDelegatedRoutingV1HttpApiClient (url: URL | string, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
105+
return new DefaultDelegatedRoutingV1HttpApiClient(new URL(url), init)
84106
}

packages/client/src/routings.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { type ContentRouting } from '@libp2p/interface/content-routing'
2+
import { CodeError } from '@libp2p/interface/errors'
3+
import { type PeerRouting } from '@libp2p/interface/peer-routing'
4+
import { peerIdFromBytes } from '@libp2p/peer-id'
5+
import { marshal, unmarshal } from 'ipns'
6+
import first from 'it-first'
7+
import map from 'it-map'
8+
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
9+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
10+
import type { DelegatedRoutingV1HttpApiClient } from './index.js'
11+
import type { AbortOptions } from '@libp2p/interface'
12+
import type { PeerId } from '@libp2p/interface/peer-id'
13+
import type { PeerInfo } from '@libp2p/interface/peer-info'
14+
import type { CID } from 'multiformats/cid'
15+
16+
const IPNS_PREFIX = uint8ArrayFromString('/ipns/')
17+
18+
function isIPNSKey (key: Uint8Array): boolean {
19+
return uint8ArrayEquals(key.subarray(0, IPNS_PREFIX.byteLength), IPNS_PREFIX)
20+
}
21+
22+
const peerIdFromRoutingKey = (key: Uint8Array): PeerId => {
23+
return peerIdFromBytes(key.slice(IPNS_PREFIX.length))
24+
}
25+
26+
/**
27+
* Wrapper class to convert [http-routing-v1 content events](https://specs.ipfs.tech/routing/http-routing-v1/#response-body) into returned values
28+
*/
29+
export class DelegatedRoutingV1HttpApiClientContentRouting implements ContentRouting {
30+
private readonly client: DelegatedRoutingV1HttpApiClient
31+
32+
constructor (client: DelegatedRoutingV1HttpApiClient) {
33+
this.client = client
34+
}
35+
36+
async * findProviders (cid: CID, options: AbortOptions = {}): AsyncIterable<PeerInfo> {
37+
yield * map(this.client.getProviders(cid, options), (record) => {
38+
return {
39+
id: record.ID,
40+
multiaddrs: record.Addrs ?? [],
41+
protocols: []
42+
}
43+
})
44+
}
45+
46+
async provide (): Promise<void> {
47+
// noop
48+
}
49+
50+
async put (key: Uint8Array, value: Uint8Array, options?: AbortOptions): Promise<void> {
51+
if (!isIPNSKey(key)) {
52+
return
53+
}
54+
55+
const peerId = peerIdFromRoutingKey(key)
56+
const record = unmarshal(value)
57+
58+
await this.client.putIPNS(peerId, record, options)
59+
}
60+
61+
async get (key: Uint8Array, options?: AbortOptions): Promise<Uint8Array> {
62+
if (!isIPNSKey(key)) {
63+
throw new CodeError('Not found', 'ERR_NOT_FOUND')
64+
}
65+
66+
const peerId = peerIdFromRoutingKey(key)
67+
68+
try {
69+
const record = await this.client.getIPNS(peerId, options)
70+
71+
return marshal(record)
72+
} catch (err: any) {
73+
// ERR_BAD_RESPONSE is thrown when the response had no body, which means
74+
// the record couldn't be found
75+
if (err.code === 'ERR_BAD_RESPONSE') {
76+
throw new CodeError('Not found', 'ERR_NOT_FOUND')
77+
}
78+
79+
throw err
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Wrapper class to convert [http-routing-v1](https://specs.ipfs.tech/routing/http-routing-v1/#response-body-0) events into expected libp2p values
86+
*/
87+
export class DelegatedRoutingV1HttpApiClientPeerRouting implements PeerRouting {
88+
private readonly client: DelegatedRoutingV1HttpApiClient
89+
90+
constructor (client: DelegatedRoutingV1HttpApiClient) {
91+
this.client = client
92+
}
93+
94+
async findPeer (peerId: PeerId, options: AbortOptions = {}): Promise<PeerInfo> {
95+
const peer = await first(this.client.getPeers(peerId, options))
96+
97+
if (peer != null) {
98+
return {
99+
id: peer.ID,
100+
multiaddrs: peer.Addrs,
101+
protocols: []
102+
}
103+
}
104+
105+
throw new CodeError('Not found', 'ERR_NOT_FOUND')
106+
}
107+
108+
async * getClosestPeers (key: Uint8Array, options: AbortOptions = {}): AsyncIterable<PeerInfo> {
109+
// noop
110+
}
111+
}

0 commit comments

Comments
 (0)