Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: use helia router for IPNS put/get #387

Merged
merged 1 commit into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-env mocha */

import { ipns } from '@helia/ipns'
import { libp2p } from '@helia/ipns/routing'
import { peerIdFromString } from '@libp2p/peer-id'
import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
Expand All @@ -24,7 +23,7 @@ import type { HeliaLibp2p } from 'helia'
import type { Controller } from 'ipfsd-ctl'

keyTypes.forEach(type => {
describe(`@helia/ipns - libp2p routing with ${type} keys`, () => {
describe(`@helia/ipns - default routing with ${type} keys`, () => {
let helia: HeliaLibp2p
let kubo: Controller
let name: IPNS
Expand Down Expand Up @@ -114,11 +113,7 @@ keyTypes.forEach(type => {
message: 'Kubo could not find Helia on the DHT'
})

name = ipns(helia, {
routers: [
libp2p(helia)
]
})
name = ipns(helia)
}

afterEach(async () => {
Expand Down
44 changes: 41 additions & 3 deletions packages/ipns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,58 @@

IPNS operations using a Helia node

## Example - Using libp2p and pubsub routers
## Example - Getting started

With IPNSRouting routers:

```typescript
import { createHelia } from 'helia'
import { ipns } from '@helia/ipns'
import { libp2p, pubsub } from '@helia/ipns/routing'
import { unixfs } from '@helia/unixfs'

const helia = await createHelia()
const name = ipns(helia)

// create a public key to publish as an IPNS name
const keyInfo = await helia.libp2p.services.keychain.createKey('my-key')
const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name)

// store some data to publish
const fs = unixfs(helia)
const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4]))

// publish the name
await name.publish(peerId, cid)

// resolve the name
const cid = name.resolve(peerId)
```

## Example - Using custom PubSub router

Additional IPNS routers can be configured - these enable alternative means to
publish and resolve IPNS names.

One example is the PubSub router - this requires an instance of Helia with
libp2p PubSub configured.

It works by subscribing to a pubsub topic for each IPNS name that we try to
resolve. Updated IPNS records are shared on these topics so an update must
occur before the name is resolvable.

This router is only suitable for networks where IPNS updates are frequent
and multiple peers are listening on the topic(s), otherwise update messages
may fail to be published with "Insufficient peers" errors.

```typescript
import { createHelia } from 'helia'
import { ipns } from '@helia/ipns'
import { pubsub } from '@helia/ipns/routing'
import { unixfs } from '@helia/unixfs'

const helia = await createHelia()
const name = ipns(helia, {
routers: [
libp2p(helia),
pubsub(helia)
]
})
Expand Down
5 changes: 2 additions & 3 deletions packages/ipns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"lint": "aegir lint",
"dep-check": "aegir dep-check",
"build": "aegir build",
"docs": "aegir docs",
"test": "aegir test",
"test:chrome": "aegir test -t browser --cov",
"test:chrome-webworker": "aegir test -t webworker",
Expand All @@ -163,6 +164,7 @@
"release": "aegir release"
},
"dependencies": {
"@helia/interface": "^3.0.1",
"@libp2p/interface": "^1.1.1",
"@libp2p/kad-dht": "^12.0.2",
"@libp2p/logger": "^4.0.4",
Expand All @@ -188,8 +190,5 @@
},
"browser": {
"./dist/src/dns-resolvers/resolver.js": "./dist/src/dns-resolvers/resolver.browser.js"
},
"typedoc": {
"entryPoint": "./src/index.ts"
}
}
54 changes: 49 additions & 5 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,58 @@
*
* IPNS operations using a Helia node
*
* @example Using libp2p and pubsub routers
* @example Getting started
*
* With {@link IPNSRouting} routers:
*
* ```typescript
* import { createHelia } from 'helia'
* import { ipns } from '@helia/ipns'
* import { libp2p, pubsub } from '@helia/ipns/routing'
* import { unixfs } from '@helia/unixfs'
*
* const helia = await createHelia()
* const name = ipns(helia)
*
* // create a public key to publish as an IPNS name
* const keyInfo = await helia.libp2p.services.keychain.createKey('my-key')
* const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name)
*
* // store some data to publish
* const fs = unixfs(helia)
* const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4]))
*
* // publish the name
* await name.publish(peerId, cid)
*
* // resolve the name
* const cid = name.resolve(peerId)
* ```
*
* @example Using custom PubSub router
*
* Additional IPNS routers can be configured - these enable alternative means to
* publish and resolve IPNS names.
*
* One example is the PubSub router - this requires an instance of Helia with
* libp2p PubSub configured.
*
* It works by subscribing to a pubsub topic for each IPNS name that we try to
* resolve. Updated IPNS records are shared on these topics so an update must
* occur before the name is resolvable.
*
* This router is only suitable for networks where IPNS updates are frequent
* and multiple peers are listening on the topic(s), otherwise update messages
* may fail to be published with "Insufficient peers" errors.
*
* ```typescript
* import { createHelia } from 'helia'
* import { ipns } from '@helia/ipns'
* import { pubsub } from '@helia/ipns/routing'
* import { unixfs } from '@helia/unixfs'
*
* const helia = await createHelia()
* const name = ipns(helia, {
* routers: [
* libp2p(helia),
* pubsub(helia)
* ]
* })
Expand Down Expand Up @@ -121,9 +159,11 @@ import { ipnsValidator } from 'ipns/validator'
import { CID } from 'multiformats/cid'
import { CustomProgressEvent } from 'progress-events'
import { defaultResolver } from './dns-resolvers/default.js'
import { helia } from './routing/helia.js'
import { localStore, type LocalStore } from './routing/local-store.js'
import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
import type { DNSResponse } from './utils/dns.js'
import type { Routing } from '@helia/interface'
import type { AbortOptions, PeerId } from '@libp2p/interface'
import type { Datastore } from 'interface-datastore'
import type { IPNSRecord } from 'ipns'
Expand Down Expand Up @@ -249,6 +289,7 @@ export type { IPNSRouting } from './routing/index.js'

export interface IPNSComponents {
datastore: Datastore
routing: Routing
}

class DefaultIPNS implements IPNS {
Expand All @@ -258,7 +299,10 @@ class DefaultIPNS implements IPNS {
private readonly defaultResolvers: DNSResolver[]

constructor (components: IPNSComponents, routers: IPNSRouting[] = [], resolvers: DNSResolver[] = []) {
this.routers = routers
this.routers = [
helia(components.routing),
...routers
]
this.localStore = localStore(components.datastore)
this.defaultResolvers = resolvers.length > 0 ? resolvers : [defaultResolver()]
}
Expand Down Expand Up @@ -407,7 +451,7 @@ export interface IPNSOptions {
resolvers?: DNSResolver[]
}

export function ipns (components: IPNSComponents, { routers = [], resolvers = [] }: IPNSOptions): IPNS {
export function ipns (components: IPNSComponents, { routers = [], resolvers = [] }: IPNSOptions = {}): IPNS {
return new DefaultIPNS(components, routers, resolvers)
}

Expand Down
45 changes: 45 additions & 0 deletions packages/ipns/src/routing/helia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CustomProgressEvent, type ProgressEvent } from 'progress-events'
import type { GetOptions, PutOptions } from './index.js'
import type { IPNSRouting } from '../index.js'
import type { Routing } from '@helia/interface'

export interface HeliaRoutingComponents {
routing: Routing
}

export type HeliaRoutingProgressEvents =
ProgressEvent<'ipns:routing:helia:error', Error>

export class HeliaRouting implements IPNSRouting {
private readonly routing: Routing

constructor (routing: Routing) {
this.routing = routing
}

async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}): Promise<void> {
try {
await this.routing.put(routingKey, marshaledRecord, options)
} catch (err: any) {
options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:helia:error', err))
}

Check warning on line 25 in packages/ipns/src/routing/helia.ts

View check run for this annotation

Codecov / codecov/patch

packages/ipns/src/routing/helia.ts#L24-L25

Added lines #L24 - L25 were not covered by tests
}

async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<Uint8Array> {
try {
return await this.routing.get(routingKey, options)
} catch (err: any) {
options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:helia:error', err))
}

throw new Error('Not found')
}

Check warning on line 36 in packages/ipns/src/routing/helia.ts

View check run for this annotation

Codecov / codecov/patch

packages/ipns/src/routing/helia.ts#L32-L36

Added lines #L32 - L36 were not covered by tests
}

/**
* The helia routing uses any available Routers configured on the passed Helia
* node. This could be libp2p, HTTP API Delegated Routing, etc.
*/
export function helia (routing: Routing): IPNSRouting {
return new HeliaRouting(routing)
}
6 changes: 3 additions & 3 deletions packages/ipns/src/routing/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Libp2pContentRoutingProgressEvents } from './libp2p.js'
import type { HeliaRoutingProgressEvents } from './helia.js'
import type { DatastoreProgressEvents } from './local-store.js'
import type { PubSubProgressEvents } from './pubsub.js'
import type { AbortOptions } from '@libp2p/interface'
Expand All @@ -19,8 +19,8 @@ export interface IPNSRouting {

export type IPNSRoutingEvents =
DatastoreProgressEvents |
Libp2pContentRoutingProgressEvents |
HeliaRoutingProgressEvents |
PubSubProgressEvents

export { libp2p } from './libp2p.js'
export { helia } from './helia.js'
export { pubsub } from './pubsub.js'
47 changes: 0 additions & 47 deletions packages/ipns/src/routing/libp2p.ts

This file was deleted.

17 changes: 12 additions & 5 deletions packages/ipns/test/publish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import Sinon from 'sinon'
import { type StubbedInstance, stubInterface } from 'sinon-ts'
import { ipns } from '../src/index.js'
import type { IPNS, IPNSRouting } from '../src/index.js'
import type { Routing } from '@helia/interface'

const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

describe('publish', () => {
let name: IPNS
let routing: StubbedInstance<IPNSRouting>
let customRouting: StubbedInstance<IPNSRouting>
let heliaRouting: StubbedInstance<Routing>

beforeEach(async () => {
const datastore = new MemoryDatastore()
routing = stubInterface<IPNSRouting>()
routing.get.throws(new Error('Not found'))
customRouting = stubInterface<IPNSRouting>()
customRouting.get.throws(new Error('Not found'))
heliaRouting = stubInterface<Routing>()

name = ipns({ datastore }, { routers: [routing] })
name = ipns({ datastore, routing: heliaRouting }, { routers: [customRouting] })
})

it('should publish an IPNS record with the default params', async function () {
Expand All @@ -40,6 +43,9 @@ describe('publish', () => {

expect(ipnsEntry).to.have.property('sequence', 1n)
expect(ipnsEntry).to.have.property('ttl', BigInt(lifetime) * 100000n)

expect(heliaRouting.put.called).to.be.true()
expect(customRouting.put.called).to.be.true()
})

it('should publish a record offline', async () => {
Expand All @@ -48,7 +54,8 @@ describe('publish', () => {
offline: true
})

expect(routing.put.called).to.be.false()
expect(heliaRouting.put.called).to.be.false()
expect(customRouting.put.called).to.be.false()
})

it('should emit progress events', async function () {
Expand Down
Loading