Skip to content

Commit

Permalink
feat: add support for libp2p ContentRouting and PeerRouting (#44)
Browse files Browse the repository at this point in the history
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 <1173416+SgtPooki@users.noreply.github.com>
  • Loading branch information
achingbrain and SgtPooki authored Oct 30, 2023
1 parent a958569 commit ddfff1b
Show file tree
Hide file tree
Showing 7 changed files with 482 additions and 13 deletions.
16 changes: 16 additions & 0 deletions packages/client/.aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import body from 'body-parser'
const options = {
test: {
before: async () => {
let callCount = 0
const providers = new Map()
const peers = new Map()
const ipnsGet = new Map()
Expand All @@ -13,45 +14,60 @@ const options = {
echo.polka.use(body.raw({ type: 'application/vnd.ipfs.ipns-record'}))
echo.polka.use(body.text())
echo.polka.post('/add-providers/:cid', (req, res) => {
callCount++
providers.set(req.params.cid, req.body)
res.end()
})
echo.polka.get('/routing/v1/providers/:cid', (req, res) => {
callCount++
const records = providers.get(req.params.cid) ?? '[]'
providers.delete(req.params.cid)

res.end(records)
})
echo.polka.post('/add-peers/:peerId', (req, res) => {
callCount++
peers.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/routing/v1/peers/:peerId', (req, res) => {
callCount++
const records = peers.get(req.params.peerId) ?? '[]'
peers.delete(req.params.peerId)

res.end(records)
})
echo.polka.post('/add-ipns/:peerId', (req, res) => {
callCount++
ipnsGet.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/routing/v1/ipns/:peerId', (req, res) => {
callCount++
const record = ipnsGet.get(req.params.peerId) ?? ''
ipnsGet.delete(req.params.peerId)

res.end(record)
})
echo.polka.put('/routing/v1/ipns/:peerId', (req, res) => {
callCount++
ipnsPut.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/get-ipns/:peerId', (req, res) => {
callCount++
const record = ipnsPut.get(req.params.peerId) ?? ''
ipnsPut.delete(req.params.peerId)

res.end(record)
})
echo.polka.get('/get-call-count', (req, res) => {
res.end(callCount.toString())
})
echo.polka.get('/reset-call-count', (req, res) => {
callCount = 0
res.end()
})

await echo.start()

Expand Down
30 changes: 26 additions & 4 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,44 @@
## About

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.
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.

### Example

```typescript
import { createRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
import { CID } from 'multiformats/cid'

const client = createRoutingV1HttpApiClient(new URL('https://example.org'))
const client = createDelegatedRoutingV1HttpApiClient('https://example.org')

for await (const prov of getProviders(CID.parse('QmFoo'))) {
// ...
}
```

### How to use with libp2p

The client can be configured as a libp2p service, this will enable it as both a ContentRouting and a PeerRouting implementation

### Example

```typescript
import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
import { createLibp2p } from 'libp2p'
import { peerIdFromString } from '@libp2p/peer-id'

const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
const libp2p = await createLibp2p({
// other config here
services: {
delegatedRouting: client
}
})

// later this will use the configured HTTP gateway
await libp2p.peerRouting.findPeer(peerIdFromString('QmFoo'))
```

## Install

```console
Expand Down
9 changes: 6 additions & 3 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,18 @@
"any-signal": "^4.1.1",
"browser-readablestream-to-it": "^2.0.3",
"ipns": "^7.0.1",
"it-all": "^3.0.2",
"it-first": "^3.0.3",
"it-map": "^3.0.4",
"it-ndjson": "^1.0.4",
"multiformats": "^12.1.1",
"p-defer": "^4.0.0",
"p-queue": "^7.3.4"
"p-queue": "^7.3.4",
"uint8arrays": "^4.0.6"
},
"devDependencies": {
"@libp2p/peer-id-factory": "^3.0.5",
"aegir": "^41.0.0",
"body-parser": "^1.20.2"
"body-parser": "^1.20.2",
"it-all": "^3.0.2"
}
}
15 changes: 15 additions & 0 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { type ContentRouting, contentRouting } from '@libp2p/interface/content-routing'
import { CodeError } from '@libp2p/interface/errors'
import { type PeerRouting, peerRouting } from '@libp2p/interface/peer-routing'
import { logger } from '@libp2p/logger'
import { peerIdFromString } from '@libp2p/peer-id'
import { multiaddr } from '@multiformats/multiaddr'
Expand All @@ -9,6 +11,7 @@ import { ipnsValidator } from 'ipns/validator'
import { parse as ndjson } from 'it-ndjson'
import defer from 'p-defer'
import PQueue from 'p-queue'
import { DelegatedRoutingV1HttpApiClientContentRouting, DelegatedRoutingV1HttpApiClientPeerRouting } from './routings.js'
import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, PeerRecord } from './index.js'
import type { AbortOptions } from '@libp2p/interface'
import type { PeerId } from '@libp2p/interface/peer-id'
Expand All @@ -27,6 +30,8 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
private readonly shutDownController: AbortController
private readonly clientUrl: URL
private readonly timeout: number
private readonly contentRouting: ContentRouting
private readonly peerRouting: PeerRouting

/**
* Create a new DelegatedContentRouting instance
Expand All @@ -39,6 +44,16 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
})
this.clientUrl = url instanceof URL ? url : new URL(url)
this.timeout = init.timeout ?? defaultValues.timeout
this.contentRouting = new DelegatedRoutingV1HttpApiClientContentRouting(this)
this.peerRouting = new DelegatedRoutingV1HttpApiClientPeerRouting(this)
}

get [contentRouting] (): ContentRouting {
return this.contentRouting
}

get [peerRouting] (): PeerRouting {
return this.peerRouting
}

isStarted (): boolean {
Expand Down
34 changes: 28 additions & 6 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
/**
* @packageDocumentation
*
* 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.
* 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.
*
* @example
*
* ```typescript
* import { createRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
* import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
* import { CID } from 'multiformats/cid'
*
* const client = createRoutingV1HttpApiClient(new URL('https://example.org'))
* const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
*
* for await (const prov of getProviders(CID.parse('QmFoo'))) {
* // ...
* }
* ```
*
* ### How to use with libp2p
*
* 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
*
* @example
*
* ```typescript
* import { createDelegatedRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
* import { createLibp2p } from 'libp2p'
* import { peerIdFromString } from '@libp2p/peer-id'
*
* const client = createDelegatedRoutingV1HttpApiClient('https://example.org')
* const libp2p = await createLibp2p({
* // other config here
* services: {
* delegatedRouting: client
* }
* })
*
* // later this will use the configured HTTP gateway
* await libp2p.peerRouting.findPeer(peerIdFromString('QmFoo'))
* ```
*/

import { DefaultDelegatedRoutingV1HttpApiClient } from './client.js'
Expand Down Expand Up @@ -79,6 +101,6 @@ export interface DelegatedRoutingV1HttpApiClient {
/**
* Create and return a client to use with a Routing V1 HTTP API server
*/
export function createDelegatedRoutingV1HttpApiClient (url: URL, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
return new DefaultDelegatedRoutingV1HttpApiClient(url, init)
export function createDelegatedRoutingV1HttpApiClient (url: URL | string, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
return new DefaultDelegatedRoutingV1HttpApiClient(new URL(url), init)
}
111 changes: 111 additions & 0 deletions packages/client/src/routings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { type ContentRouting } from '@libp2p/interface/content-routing'
import { CodeError } from '@libp2p/interface/errors'
import { type PeerRouting } from '@libp2p/interface/peer-routing'
import { peerIdFromBytes } from '@libp2p/peer-id'
import { marshal, unmarshal } from 'ipns'
import first from 'it-first'
import map from 'it-map'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import type { DelegatedRoutingV1HttpApiClient } from './index.js'
import type { AbortOptions } from '@libp2p/interface'
import type { PeerId } from '@libp2p/interface/peer-id'
import type { PeerInfo } from '@libp2p/interface/peer-info'
import type { CID } from 'multiformats/cid'

const IPNS_PREFIX = uint8ArrayFromString('/ipns/')

function isIPNSKey (key: Uint8Array): boolean {
return uint8ArrayEquals(key.subarray(0, IPNS_PREFIX.byteLength), IPNS_PREFIX)
}

const peerIdFromRoutingKey = (key: Uint8Array): PeerId => {
return peerIdFromBytes(key.slice(IPNS_PREFIX.length))
}

/**
* Wrapper class to convert [http-routing-v1 content events](https://specs.ipfs.tech/routing/http-routing-v1/#response-body) into returned values
*/
export class DelegatedRoutingV1HttpApiClientContentRouting implements ContentRouting {
private readonly client: DelegatedRoutingV1HttpApiClient

constructor (client: DelegatedRoutingV1HttpApiClient) {
this.client = client
}

async * findProviders (cid: CID, options: AbortOptions = {}): AsyncIterable<PeerInfo> {
yield * map(this.client.getProviders(cid, options), (record) => {
return {
id: record.ID,
multiaddrs: record.Addrs ?? [],
protocols: []
}
})
}

async provide (): Promise<void> {
// noop
}

async put (key: Uint8Array, value: Uint8Array, options?: AbortOptions): Promise<void> {
if (!isIPNSKey(key)) {
return
}

const peerId = peerIdFromRoutingKey(key)
const record = unmarshal(value)

await this.client.putIPNS(peerId, record, options)
}

async get (key: Uint8Array, options?: AbortOptions): Promise<Uint8Array> {
if (!isIPNSKey(key)) {
throw new CodeError('Not found', 'ERR_NOT_FOUND')
}

const peerId = peerIdFromRoutingKey(key)

try {
const record = await this.client.getIPNS(peerId, options)

return marshal(record)
} catch (err: any) {
// ERR_BAD_RESPONSE is thrown when the response had no body, which means
// the record couldn't be found
if (err.code === 'ERR_BAD_RESPONSE') {
throw new CodeError('Not found', 'ERR_NOT_FOUND')
}

throw err
}
}
}

/**
* Wrapper class to convert [http-routing-v1](https://specs.ipfs.tech/routing/http-routing-v1/#response-body-0) events into expected libp2p values
*/
export class DelegatedRoutingV1HttpApiClientPeerRouting implements PeerRouting {
private readonly client: DelegatedRoutingV1HttpApiClient

constructor (client: DelegatedRoutingV1HttpApiClient) {
this.client = client
}

async findPeer (peerId: PeerId, options: AbortOptions = {}): Promise<PeerInfo> {
const peer = await first(this.client.getPeers(peerId, options))

if (peer != null) {
return {
id: peer.ID,
multiaddrs: peer.Addrs,
protocols: []
}
}

throw new CodeError('Not found', 'ERR_NOT_FOUND')
}

async * getClosestPeers (key: Uint8Array, options: AbortOptions = {}): AsyncIterable<PeerInfo> {
// noop
}
}
Loading

0 comments on commit ddfff1b

Please sign in to comment.