diff --git a/packages/helia/package.json b/packages/helia/package.json index f787322c..1b7b81a2 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -138,18 +138,30 @@ "release": "aegir release" }, "dependencies": { + "@chainsafe/libp2p-gossipsub": "^8.0.0", + "@chainsafe/libp2p-noise": "^12.0.0", + "@chainsafe/libp2p-yamux": "^4.0.2", "@helia/interface": "^1.0.0", - "@ipld/dag-pb": "^4.0.2", - "@libp2p/interface-libp2p": "^1.1.0", - "@libp2p/interfaces": "^3.3.1", - "@libp2p/logger": "^2.0.5", + "@ipld/dag-pb": "^4.0.3", + "@libp2p/bootstrap": "^8.0.0", + "@libp2p/interface-libp2p": "^3.1.0", + "@libp2p/interface-pubsub": "^4.0.1", + "@libp2p/interfaces": "^3.3.2", + "@libp2p/ipni-content-routing": "^1.0.0", + "@libp2p/kad-dht": "^9.3.3", + "@libp2p/logger": "^2.0.7", + "@libp2p/mdns": "^8.0.0", + "@libp2p/mplex": "^8.0.3", + "@libp2p/tcp": "^7.0.1", + "@libp2p/webrtc": "^2.0.4", + "@libp2p/webtransport": "^2.0.1", "blockstore-core": "^4.0.0", "cborg": "^1.10.0", "datastore-core": "^9.0.0", "interface-blockstore": "^5.0.0", "interface-datastore": "^8.0.0", "interface-store": "^5.0.1", - "ipfs-bitswap": "^17.0.0", + "ipfs-bitswap": "^18.0.0", "it-all": "^3.0.1", "it-drain": "^3.0.1", "it-filter": "^3.0.1", @@ -162,18 +174,19 @@ "uint8arrays": "^4.0.3" }, "devDependencies": { - "@chainsafe/libp2p-noise": "^11.0.0", - "@chainsafe/libp2p-yamux": "^3.0.5", "@ipld/dag-cbor": "^9.0.0", "@ipld/dag-json": "^10.0.1", - "@libp2p/websockets": "^5.0.3", + "@libp2p/websockets": "^6.0.1", "@types/sinon": "^10.0.14", "aegir": "^39.0.4", "delay": "^5.0.0", - "libp2p": "^0.44.0", + "libp2p": "^0.45.1", "sinon": "^15.0.2", "sinon-ts": "^1.0.0" }, + "browser": { + "./dist/src/utils/libp2p.js": "./dist/src/utils/libp2p.browser.js" + }, "typedoc": { "entryPoint": "./src/index.ts" } diff --git a/packages/helia/src/helia.ts b/packages/helia/src/helia.ts index c6edd79f..be541370 100644 --- a/packages/helia/src/helia.ts +++ b/packages/helia/src/helia.ts @@ -1,6 +1,4 @@ import { logger } from '@libp2p/logger' -import { MemoryBlockstore } from 'blockstore-core' -import { MemoryDatastore } from 'datastore-core' import { type Bitswap, createBitswap } from 'ipfs-bitswap' import drain from 'it-drain' import { identity } from 'multiformats/hashes/identity' @@ -13,12 +11,19 @@ import type { HeliaInit } from '.' import type { GCOptions, Helia } from '@helia/interface' import type { Pins } from '@helia/interface/pins' import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Blockstore } from 'interface-blockstore' import type { Datastore } from 'interface-datastore' import type { CID } from 'multiformats/cid' import type { MultihashHasher } from 'multiformats/hashes/interface' const log = logger('helia') +interface HeliaImplInit extends HeliaInit { + libp2p: T + blockstore: Blockstore + datastore: Datastore +} + export class HeliaImpl implements Helia { public libp2p: Libp2p public blockstore: BlockStorage @@ -27,7 +32,7 @@ export class HeliaImpl implements Helia { #bitswap?: Bitswap - constructor (init: HeliaInit) { + constructor (init: HeliaImplInit) { const hashers: MultihashHasher[] = [ sha256, sha512, @@ -35,56 +40,30 @@ export class HeliaImpl implements Helia { ...(init.hashers ?? []) ] - const datastore = init.datastore ?? new MemoryDatastore() - const blockstore = init.blockstore ?? new MemoryBlockstore() + this.#bitswap = createBitswap(init.libp2p, init.blockstore, { + hashLoader: { + getHasher: async (codecOrName: string | number): Promise> => { + const hasher = hashers.find(hasher => { + return hasher.code === codecOrName || hasher.name === codecOrName + }) - // @ts-expect-error incomplete libp2p implementation - const libp2p = init.libp2p ?? new Proxy({}, { - get (_, prop): true | (() => void) { - const noop = (): void => {} - const noops = ['start', 'stop'] - - if (noops.includes(prop.toString())) { - return noop - } + if (hasher != null) { + return hasher + } - if (prop === 'isProxy') { - return true + throw new Error(`Could not load hasher for code/name "${codecOrName}"`) } - - throw new Error('Please configure Helia with a libp2p instance') - }, - set (): never { - throw new Error('Please configure Helia with a libp2p instance') } }) - if (init.libp2p != null) { - this.#bitswap = createBitswap(libp2p, blockstore, { - hashLoader: { - getHasher: async (codecOrName: string | number): Promise> => { - const hasher = hashers.find(hasher => { - return hasher.code === codecOrName || hasher.name === codecOrName - }) - - if (hasher != null) { - return hasher - } - - throw new Error(`Could not load hasher for code/name "${codecOrName}"`) - } - } - }) - } - - this.pins = new PinsImpl(datastore, blockstore, init.dagWalkers ?? []) + this.pins = new PinsImpl(init.datastore, init.blockstore, init.dagWalkers ?? []) - this.libp2p = libp2p - this.blockstore = new BlockStorage(blockstore, this.pins, { + this.libp2p = init.libp2p + this.blockstore = new BlockStorage(init.blockstore, this.pins, { bitswap: this.#bitswap, holdGcLock: init.holdGcLock }) - this.datastore = datastore + this.datastore = init.datastore } async start (): Promise { diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 1a7e8146..668e303e 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -6,7 +6,6 @@ * @example * * ```typescript - * import { createLibp2p } from 'libp2p' * import { MemoryDatastore } from 'datastore-core' * import { MemoryBlockstore } from 'blockstore-core' * import { createHelia } from 'helia' @@ -15,19 +14,21 @@ * * const node = await createHelia({ * blockstore: new MemoryBlockstore(), - * datastore: new MemoryDatastore(), - * libp2p: await createLibp2p({ - * //... libp2p options - * }) + * datastore: new MemoryDatastore() * }) * const fs = unixfs(node) * fs.cat(CID.parse('bafyFoo')) * ``` */ +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' import { HeliaImpl } from './helia.js' +import { createLibp2p } from './utils/libp2p.js' import type { Helia } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' +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 { CID } from 'multiformats/cid' @@ -44,11 +45,11 @@ export interface DAGWalker { /** * Options used to create a Helia node. */ -export interface HeliaInit { +export interface HeliaInit { /** * A libp2p node is required to perform network operations */ - libp2p?: Libp2p + libp2p?: T /** * The blockstore is where blocks are stored @@ -100,8 +101,22 @@ export interface HeliaInit { /** * Create and return a Helia node */ -export async function createHelia (init: HeliaInit = {}): Promise { - const helia = new HeliaImpl(init) +export async function createHelia (init: HeliaInit): Promise> +export async function createHelia (init?: HeliaInit>): Promise>> +export async function createHelia (init: HeliaInit = {}): Promise> { + const datastore = init.datastore ?? new MemoryDatastore() + const blockstore = init.blockstore ?? new MemoryBlockstore() + const libp2p = init.libp2p ?? await createLibp2p({ + datastore, + start: false + }) + + const helia = new HeliaImpl({ + ...init, + datastore, + blockstore, + libp2p + }) if (init.start !== false) { await helia.start() diff --git a/packages/helia/src/utils/libp2p.browser.ts b/packages/helia/src/utils/libp2p.browser.ts new file mode 100644 index 00000000..17a44332 --- /dev/null +++ b/packages/helia/src/utils/libp2p.browser.ts @@ -0,0 +1,68 @@ +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 { kadDHT, type DualKadDHT } 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 { createLibp2p as create } from 'libp2p' +import { autoNATService } from 'libp2p/autonat' +import { circuitRelayTransport, circuitRelayServer } from 'libp2p/circuit-relay' +import { identifyService } from 'libp2p/identify' +import type { CreateLibp2pOptions } from './libp2p.js' +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { PubSub } from '@libp2p/interface-pubsub' + +export async function createLibp2p (opts: CreateLibp2pOptions): Promise> { + return create({ + ...opts, + addresses: { + listen: [ + '/webrtc' + ] + }, + transports: [ + webRTC(), + webRTCDirect(), + webTransport(), + webSockets(), + circuitRelayTransport({ + discoverRelays: 1 + }) + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux(), + mplex() + ], + peerDiscovery: [ + bootstrap({ + list: [ + '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' + ] + }) + ], + contentRouters: [ + ipniContentRouting('https://cid.contact') + ], + services: { + identify: identifyService(), + autoNAT: autoNATService(), + pubsub: gossipsub(), + dht: kadDHT({ + clientMode: true + }), + relay: circuitRelayServer({ + advertise: true + }) + } + }) +} diff --git a/packages/helia/src/utils/libp2p.ts b/packages/helia/src/utils/libp2p.ts new file mode 100644 index 00000000..c73e436b --- /dev/null +++ b/packages/helia/src/utils/libp2p.ts @@ -0,0 +1,72 @@ +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 { 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 type { Libp2p } from '@libp2p/interface-libp2p' +import type { PubSub } from '@libp2p/interface-pubsub' +import type { Datastore } from 'interface-datastore' + +export interface CreateLibp2pOptions { + datastore: Datastore + start?: boolean +} + +export async function createLibp2p (opts: CreateLibp2pOptions): Promise> { + 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({ + list: [ + '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb', + '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt' + ] + }) + ], + contentRouters: [ + ipniContentRouting('https://cid.contact') + ], + services: { + identify: identifyService(), + autoNAT: autoNATService(), + upnp: uPnPNATService(), + pubsub: gossipsub(), + dht: kadDHT(), + relay: circuitRelayServer({ + advertise: true + }) + } + }) +} diff --git a/packages/helia/test/factory.spec.ts b/packages/helia/test/factory.spec.ts new file mode 100644 index 00000000..b1505741 --- /dev/null +++ b/packages/helia/test/factory.spec.ts @@ -0,0 +1,38 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { Key } from 'interface-datastore' +import { CID } from 'multiformats/cid' +import { createHelia } from '../src/index.js' +import type { Helia } from '@helia/interface' + +describe('helia factory', () => { + let helia: Helia + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + }) + + it('allows creating offline node', async () => { + helia = await createHelia({ + start: false + }) + + expect(helia.libp2p.isStarted()).to.be.false() + }) + + it('does not require any constructor args', async () => { + helia = await createHelia() + + const cid = CID.parse('QmaQwYWpchozXhFv8nvxprECWBSCEppN9dfd2VQiJfRo3F') + const block = Uint8Array.from([0, 1, 2, 3]) + await helia.blockstore.put(cid, block) + expect(await helia.blockstore.has(cid)).to.be.true() + + const key = new Key(`/${cid.toString()}`) + await helia.datastore.put(key, block) + expect(await helia.datastore.has(key)).to.be.true() + }) +}) diff --git a/packages/helia/test/gc.spec.ts b/packages/helia/test/gc.spec.ts index 506159e6..ce0e3702 100644 --- a/packages/helia/test/gc.spec.ts +++ b/packages/helia/test/gc.spec.ts @@ -1,4 +1,5 @@ /* eslint-env mocha */ + import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import * as dagCbor from '@ipld/dag-cbor' @@ -41,7 +42,7 @@ describe('gc', () => { } }) - it('pins a dag-pb node and does not garbage collect it or it\'s children', async () => { + it('pins a dag-pb node and does not garbage collect it or its children', async () => { const child1 = await createAndPutBlock(dagPb.code, dagPb.encode({ Data: Uint8Array.from([0, 1, 2, 3]), Links: [] @@ -82,7 +83,7 @@ describe('gc', () => { await expect(helia.blockstore.has(doomed)).to.eventually.be.false() }) - it('pins a dag-cbor node and does not garbage collect it or it\'s children', async () => { + it('pins a dag-cbor node and does not garbage collect it or its children', async () => { const child1 = await createAndPutBlock(dagCbor.code, dagCbor.encode({ foo: 'bar' }), helia.blockstore) @@ -117,7 +118,7 @@ describe('gc', () => { await expect(helia.blockstore.has(doomed)).to.eventually.be.false() }) - it('pins a dag-json node and does not garbage collect it or it\'s children', async () => { + it('pins a dag-json node and does not garbage collect it or its children', async () => { const child1 = await createAndPutBlock(dagJson.code, dagJson.encode({ foo: 'bar' }), helia.blockstore) diff --git a/packages/helia/test/index.spec.ts b/packages/helia/test/index.spec.ts index 1c1dd732..d70d041b 100644 --- a/packages/helia/test/index.spec.ts +++ b/packages/helia/test/index.spec.ts @@ -5,9 +5,7 @@ import { webSockets } from '@libp2p/websockets' import { expect } from 'aegir/chai' import { MemoryBlockstore } from 'blockstore-core' import { MemoryDatastore } from 'datastore-core' -import { Key } from 'interface-datastore' import { createLibp2p } from 'libp2p' -import { CID } from 'multiformats/cid' import { createHelia } from '../src/index.js' import type { Helia } from '@helia/interface' @@ -57,43 +55,4 @@ describe('helia', () => { it('should have a libp2p', async () => { expect(helia).to.have.property('libp2p').that.is.ok() }) - - it('allows creating offline node', async () => { - const helia = await createHelia({ - start: false, - datastore: new MemoryDatastore(), - blockstore: new MemoryBlockstore(), - libp2p: await createLibp2p({ - start: false, - transports: [ - webSockets() - ], - connectionEncryption: [ - noise() - ], - streamMuxers: [ - yamux() - ] - }) - }) - - expect(helia.libp2p.isStarted()).to.be.false() - }) - - it('does not require any constructor args', async () => { - const helia = await createHelia() - - const cid = CID.parse('QmaQwYWpchozXhFv8nvxprECWBSCEppN9dfd2VQiJfRo3F') - const block = Uint8Array.from([0, 1, 2, 3]) - await helia.blockstore.put(cid, block) - expect(await helia.blockstore.has(cid)).to.be.true() - - const key = new Key(`/${cid.toString()}`) - await helia.datastore.put(key, block) - expect(await helia.datastore.has(key)).to.be.true() - - expect(() => { - helia.libp2p.isStarted() - }).to.throw('Please configure Helia with a libp2p instance') - }) }) diff --git a/packages/interface/package.json b/packages/interface/package.json index ece335ca..fa7a901e 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -155,13 +155,12 @@ "release": "aegir release" }, "dependencies": { - "@libp2p/interface-libp2p": "^1.1.0", - "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/interfaces": "^3.3.1", + "@libp2p/interface-libp2p": "^3.1.0", + "@libp2p/interfaces": "^3.3.2", "interface-blockstore": "^5.0.0", "interface-datastore": "^8.0.0", "interface-store": "^5.0.1", - "ipfs-bitswap": "^17.0.0", + "ipfs-bitswap": "^18.0.0", "multiformats": "^11.0.1", "progress-events": "^1.0.0" }, diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index b7330964..83fafa15 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -17,7 +17,6 @@ import type { Blocks } from './blocks.js' import type { Pins } from './pins.js' import type { Libp2p } from '@libp2p/interface-libp2p' -import type { PeerId } from '@libp2p/interface-peer-id' import type { AbortOptions } from '@libp2p/interfaces' import type { Datastore } from 'interface-datastore' import type { CID } from 'multiformats/cid' @@ -28,11 +27,11 @@ export type { Await, AwaitIterable } from 'interface-store' /** * The API presented by a Helia node. */ -export interface Helia { +export interface Helia { /** * The underlying libp2p node */ - libp2p: Libp2p + libp2p: T /** * Where the blocks are stored @@ -72,11 +71,3 @@ export type GcEvents = export interface GCOptions extends AbortOptions, ProgressOptions { } - -export interface InfoOptions extends AbortOptions { - /** - * If passed, return information about this PeerId, defaults - * to the ID of the current node. - */ - peerId?: PeerId -} diff --git a/packages/interop/package.json b/packages/interop/package.json index 528ca330..cc0fe21d 100644 --- a/packages/interop/package.json +++ b/packages/interop/package.json @@ -52,21 +52,21 @@ "test:electron-main": "aegir test -t electron-main" }, "devDependencies": { - "@chainsafe/libp2p-noise": "^11.0.0", - "@chainsafe/libp2p-yamux": "^3.0.5", + "@chainsafe/libp2p-noise": "^12.0.0", + "@chainsafe/libp2p-yamux": "^4.0.2", "@helia/interface": "^1.0.0", - "@libp2p/tcp": "^6.1.2", - "@libp2p/websockets": "^5.0.3", + "@libp2p/tcp": "^7.0.1", + "@libp2p/websockets": "^6.0.1", "@multiformats/sha3": "^2.0.15", "aegir": "^39.0.4", "blockstore-core": "^4.0.0", "datastore-core": "^9.0.0", - "go-ipfs": "^0.19.0", + "go-ipfs": "^0.20.0", "helia": "^1.0.0", "ipfsd-ctl": "^13.0.0", "it-to-buffer": "^4.0.1", "kubo-rpc-client": "^3.0.0", - "libp2p": "^0.44.0", + "libp2p": "^0.45.1", "multiformats": "^11.0.1" }, "browser": { diff --git a/packages/interop/test/blockstore.spec.ts b/packages/interop/test/blockstore.spec.ts index bf206165..f91a526b 100644 --- a/packages/interop/test/blockstore.spec.ts +++ b/packages/interop/test/blockstore.spec.ts @@ -19,7 +19,9 @@ describe('blockstore', () => { kubo = await createKuboNode() // connect the two nodes - await helia.libp2p.peerStore.addressBook.add(kubo.peer.id, kubo.peer.addresses) + await helia.libp2p.peerStore.merge(kubo.peer.id, { + multiaddrs: kubo.peer.addresses + }) await helia.libp2p.dial(kubo.peer.id) }) diff --git a/packages/interop/test/fixtures/create-helia.browser.ts b/packages/interop/test/fixtures/create-helia.browser.ts index 1678d862..38864c44 100644 --- a/packages/interop/test/fixtures/create-helia.browser.ts +++ b/packages/interop/test/fixtures/create-helia.browser.ts @@ -6,6 +6,7 @@ import { MemoryBlockstore } from 'blockstore-core' import { MemoryDatastore } from 'datastore-core' import { createHelia, type HeliaInit } from 'helia' import { createLibp2p } from 'libp2p' +import { identifyService } from 'libp2p/identify' import type { Helia } from '@helia/interface' export async function createHeliaNode (init?: Partial): Promise { @@ -26,8 +27,12 @@ export async function createHeliaNode (init?: Partial): Promise false } }) diff --git a/packages/interop/test/fixtures/create-helia.ts b/packages/interop/test/fixtures/create-helia.ts index 16c940de..ed30023a 100644 --- a/packages/interop/test/fixtures/create-helia.ts +++ b/packages/interop/test/fixtures/create-helia.ts @@ -5,6 +5,7 @@ import { MemoryBlockstore } from 'blockstore-core' import { MemoryDatastore } from 'datastore-core' import { createHelia, type HeliaInit } from 'helia' import { createLibp2p } from 'libp2p' +import { identifyService } from 'libp2p/identify' import type { Helia } from '@helia/interface' export async function createHeliaNode (init?: Partial): Promise { @@ -27,8 +28,8 @@ export async function createHeliaNode (init?: Partial): Promise { kubo = await createKuboNode() // connect the two nodes - await helia.libp2p.peerStore.addressBook.add(kubo.peer.id, kubo.peer.addresses) + await helia.libp2p.peerStore.merge(kubo.peer.id, { + multiaddrs: kubo.peer.addresses + }) await helia.libp2p.dial(kubo.peer.id) })