From 33a75d5f80e2f211440c087806f463525de910d9 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 1 Jun 2023 13:22:17 +0100 Subject: [PATCH] feat: allow passing partial libp2p config to helia factory (#140) If the user just wants to override specific parts of the libp2p config don't make them pass a whole libp2p object in, instead accept an Libp2pOptions object and use the keys from that to override the default config. --- packages/helia/package.json | 2 +- packages/helia/src/index.ts | 35 +++++++-- ....browser.ts => libp2p-defaults.browser.ts} | 18 ++--- packages/helia/src/utils/libp2p-defaults.ts | 67 +++++++++++++++++ packages/helia/src/utils/libp2p.ts | 75 +++---------------- packages/helia/test/libp2p.spec.ts | 53 +++++++++++++ 6 files changed, 168 insertions(+), 82 deletions(-) rename packages/helia/src/utils/{libp2p.browser.ts => libp2p-defaults.browser.ts} (74%) create mode 100644 packages/helia/src/utils/libp2p-defaults.ts create mode 100644 packages/helia/test/libp2p.spec.ts diff --git a/packages/helia/package.json b/packages/helia/package.json index 44e093b0..05606032 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -187,7 +187,7 @@ "sinon-ts": "^1.0.0" }, "browser": { - "./dist/src/utils/libp2p.js": "./dist/src/utils/libp2p.browser.js" + "./dist/src/utils/libp2p-defaults.js": "./dist/src/utils/libp2p-defaults.browser.js" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 96a918b8..78b0ffa5 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -33,6 +33,7 @@ import type { PubSub } from '@libp2p/interface-pubsub' import type { DualKadDHT } from '@libp2p/kad-dht' import type { Blockstore } from 'interface-blockstore' import type { Datastore } from 'interface-datastore' +import type { Libp2pOptions } from 'libp2p' import type { CID } from 'multiformats/cid' import type { MultihashHasher } from 'multiformats/hashes/interface' @@ -51,9 +52,15 @@ export interface DAGWalker { */ export interface HeliaInit { /** - * A libp2p node is required to perform network operations + * A libp2p node is required to perform network operations. Either a + * preconfigured node or options to configure a node can be passed + * here. + * + * If node options are passed, they will be merged with the default + * config for the current platform. In this case all passed config + * keys will replace those from the default config. */ - libp2p?: T + libp2p?: T | Libp2pOptions /** * The blockstore is where blocks are stored @@ -110,10 +117,14 @@ export async function createHelia (init?: HeliaInit> { const datastore = init.datastore ?? new MemoryDatastore() const blockstore = init.blockstore ?? new MemoryBlockstore() - const libp2p = init.libp2p ?? await createLibp2p({ - datastore, - start: false - }) + + let libp2p: Libp2p + + if (isLibp2p(init.libp2p)) { + libp2p = init.libp2p + } else { + libp2p = await createLibp2p(datastore, init.libp2p) + } const helia = new HeliaImpl({ ...init, @@ -141,6 +152,18 @@ export async function createHelia (init: HeliaInit = {}): Promise return helia } +function isLibp2p (obj: any): obj is Libp2p { + if (obj == null) { + return false + } + + // a non-exhaustive list of methods found on the libp2p object + const funcs = ['dial', 'dialProtocol', 'hangUp', 'handle', 'unhandle', 'getMultiaddrs', 'getProtocols'] + + // if these are all functions it's probably a libp2p object + return funcs.every(m => typeof obj[m] === 'function') +} + async function addHeliaToAgentVersion (helia: Helia): Promise { // add helia to agent version const peer = await helia.libp2p.peerStore.get(helia.libp2p.peerId) diff --git a/packages/helia/src/utils/libp2p.browser.ts b/packages/helia/src/utils/libp2p-defaults.browser.ts similarity index 74% rename from packages/helia/src/utils/libp2p.browser.ts rename to packages/helia/src/utils/libp2p-defaults.browser.ts index 0cc82457..5554e4d1 100644 --- a/packages/helia/src/utils/libp2p.browser.ts +++ b/packages/helia/src/utils/libp2p-defaults.browser.ts @@ -3,25 +3,22 @@ import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import { bootstrap } from '@libp2p/bootstrap' import { ipniContentRouting } from '@libp2p/ipni-content-routing' -import { kadDHT, type DualKadDHT } from '@libp2p/kad-dht' +import { type DualKadDHT, kadDHT } from '@libp2p/kad-dht' import { mplex } from '@libp2p/mplex' import { webRTC, webRTCDirect } from '@libp2p/webrtc' import { webSockets } from '@libp2p/websockets' import { webTransport } from '@libp2p/webtransport' import { ipnsSelector } from 'ipns/selector' import { ipnsValidator } from 'ipns/validator' -import { createLibp2p as create } from 'libp2p' import { autoNATService } from 'libp2p/autonat' -import { circuitRelayTransport, circuitRelayServer } from 'libp2p/circuit-relay' +import { circuitRelayTransport } from 'libp2p/circuit-relay' import { identifyService } from 'libp2p/identify' import { bootstrapConfig } from './bootstrappers.js' -import type { CreateLibp2pOptions } from './libp2p.js' -import type { Libp2p } from '@libp2p/interface-libp2p' import type { PubSub } from '@libp2p/interface-pubsub' +import type { Libp2pOptions } from 'libp2p' -export async function createLibp2p (opts: CreateLibp2pOptions): Promise> { - return create({ - ...opts, +export function libp2pDefaults (): Libp2pOptions<{ dht: DualKadDHT, pubsub: PubSub, identify: unknown, autoNAT: unknown }> { + return { addresses: { listen: [ '/webrtc' @@ -61,10 +58,7 @@ export async function createLibp2p (opts: CreateLibp2pOptions): Promise { + return { + addresses: { + listen: [ + '/ip4/0.0.0.0/tcp/0' + ] + }, + transports: [ + tcp(), + webSockets(), + circuitRelayTransport({ + discoverRelays: 1 + }) + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux(), + mplex() + ], + peerDiscovery: [ + mdns(), + bootstrap(bootstrapConfig) + ], + contentRouters: [ + ipniContentRouting('https://cid.contact') + ], + services: { + identify: identifyService(), + autoNAT: autoNATService(), + upnp: uPnPNATService(), + pubsub: gossipsub(), + dht: kadDHT({ + validators: { + ipns: ipnsValidator + }, + selectors: { + ipns: ipnsSelector + } + }), + relay: circuitRelayServer({ + advertise: true + }) + } + } +} diff --git a/packages/helia/src/utils/libp2p.ts b/packages/helia/src/utils/libp2p.ts index e41ccf5c..2f52f187 100644 --- a/packages/helia/src/utils/libp2p.ts +++ b/packages/helia/src/utils/libp2p.ts @@ -1,75 +1,24 @@ -import { gossipsub } from '@chainsafe/libp2p-gossipsub' -import { noise } from '@chainsafe/libp2p-noise' -import { yamux } from '@chainsafe/libp2p-yamux' -import { bootstrap } from '@libp2p/bootstrap' -import { ipniContentRouting } from '@libp2p/ipni-content-routing' -import { type DualKadDHT, kadDHT } from '@libp2p/kad-dht' -import { mdns } from '@libp2p/mdns' -import { mplex } from '@libp2p/mplex' -import { tcp } from '@libp2p/tcp' -import { webSockets } from '@libp2p/websockets' -import { ipnsSelector } from 'ipns/selector' -import { ipnsValidator } from 'ipns/validator' -import { createLibp2p as create } from 'libp2p' -import { autoNATService } from 'libp2p/autonat' -import { circuitRelayTransport, circuitRelayServer, type CircuitRelayService } from 'libp2p/circuit-relay' -import { identifyService } from 'libp2p/identify' -import { uPnPNATService } from 'libp2p/upnp-nat' -import { bootstrapConfig } from './bootstrappers.js' +import { createLibp2p as create, type Libp2pOptions } from 'libp2p' +import { libp2pDefaults } from './libp2p-defaults.js' import type { Libp2p } from '@libp2p/interface-libp2p' import type { PubSub } from '@libp2p/interface-pubsub' +import type { DualKadDHT } from '@libp2p/kad-dht' import type { Datastore } from 'interface-datastore' +import type { CircuitRelayService } from 'libp2p/circuit-relay' export interface CreateLibp2pOptions { datastore: Datastore start?: boolean } -export async function createLibp2p (opts: CreateLibp2pOptions): Promise> { +export async function createLibp2p (datastore: Datastore, options?: Libp2pOptions): Promise> { + const defaults = libp2pDefaults() + options = options ?? {} + return create({ - ...opts, - addresses: { - listen: [ - '/ip4/0.0.0.0/tcp/0' - ] - }, - transports: [ - tcp(), - webSockets(), - circuitRelayTransport({ - discoverRelays: 1 - }) - ], - connectionEncryption: [ - noise() - ], - streamMuxers: [ - yamux(), - mplex() - ], - peerDiscovery: [ - mdns(), - bootstrap(bootstrapConfig) - ], - contentRouters: [ - ipniContentRouting('https://cid.contact') - ], - services: { - identify: identifyService(), - autoNAT: autoNATService(), - upnp: uPnPNATService(), - pubsub: gossipsub(), - dht: kadDHT({ - validators: { - ipns: ipnsValidator - }, - selectors: { - ipns: ipnsSelector - } - }), - relay: circuitRelayServer({ - advertise: true - }) - } + datastore, + ...defaults, + ...options, + start: false }) } diff --git a/packages/helia/test/libp2p.spec.ts b/packages/helia/test/libp2p.spec.ts new file mode 100644 index 00000000..930d49dd --- /dev/null +++ b/packages/helia/test/libp2p.spec.ts @@ -0,0 +1,53 @@ +/* eslint-env mocha */ + +import { webSockets } from '@libp2p/websockets' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import { createHelia } from '../src/index.js' +import type { Helia } from '@helia/interface' + +describe('libp2p', () => { + let helia: Helia + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + }) + + it('allows passing libp2p config', async () => { + const config = {} + + helia = await createHelia({ + libp2p: config + }) + + expect(Object.keys(helia.libp2p.services)).to.not.be.empty() + }) + + it('allows overriding libp2p config', async () => { + const config = { + services: {} + } + + helia = await createHelia({ + libp2p: config + }) + + expect(Object.keys(helia.libp2p.services)).to.be.empty() + }) + + it('allows passing a libp2p node', async () => { + const libp2p = await createLibp2p({ + transports: [ + webSockets() + ] + }) + + helia = await createHelia({ + libp2p + }) + + expect(helia.libp2p).to.equal(libp2p) + }) +})