Updated to latest helia-coord v2 version

This commit is contained in:
Chris Troutner
2026-02-11 11:41:26 -07:00
parent 0bfb84e21b
commit 74696cfb9e
4 changed files with 857 additions and 1021 deletions
+811 -602
View File
File diff suppressed because it is too large Load Diff
+1 -36
View File
@@ -25,31 +25,10 @@
}, },
"repository": "Permissionless-Software-Foundation/ipfs-service-provider", "repository": "Permissionless-Software-Foundation/ipfs-service-provider",
"dependencies": { "dependencies": {
"@chainsafe/libp2p-gossipsub": "14.1.0",
"@chainsafe/libp2p-noise": "16.0.1",
"@chainsafe/libp2p-yamux": "7.0.1",
"@helia/block-brokers": "4.0.4",
"@helia/routers": "3.0.0",
"@helia/unixfs": "4.0.2",
"@libp2p/bootstrap": "11.0.20",
"@libp2p/circuit-relay-v2": "3.2.2",
"@libp2p/config": "1.1.0",
"@libp2p/identify": "3.0.22",
"@libp2p/kad-dht": "14.2.3",
"@libp2p/keychain": "5.0.14",
"@libp2p/logger": "5.1.8",
"@libp2p/ping": "2.0.22",
"@libp2p/tcp": "10.1.2",
"@libp2p/webrtc": "5.2.2",
"@libp2p/websockets": "9.2.2",
"@multiformats/multiaddr": "12.5.1",
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blockstore-fs": "2.0.2",
"datastore-fs": "10.0.2",
"glob": "7.1.6", "glob": "7.1.6",
"helia": "5.2.1", "helia-coord": "2.0.0",
"helia-coord": "1.10.0",
"jsonrpc-lite": "2.2.0", "jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"jwt-bch-lib": "1.3.0", "jwt-bch-lib": "1.3.0",
@@ -64,7 +43,6 @@
"koa-router": "10.0.0", "koa-router": "10.0.0",
"koa-static": "5.0.0", "koa-static": "5.0.0",
"koa2-ratelimit": "0.9.1", "koa2-ratelimit": "0.9.1",
"libp2p": "2.7.2",
"line-reader": "0.4.0", "line-reader": "0.4.0",
"minimal-slp-wallet": "7.1.4", "minimal-slp-wallet": "7.1.4",
"mongoose": "5.13.14", "mongoose": "5.13.14",
@@ -105,18 +83,5 @@
"ignore": [ "ignore": [
"/test/unit/mocks/**/*.js" "/test/unit/mocks/**/*.js"
] ]
},
"overrides": {
"@multiformats/multiaddr-to-uri": "10.1.2",
"@multiformats/multiaddr-matcher": "1.6.0",
"@multiformats/multiaddr": "12.5.1",
"helia": {
"libp2p": "2.7.2",
"@libp2p/upnp-nat": "3.0.0"
},
"libp2p": {
"@multiformats/multiaddr": "12.5.1"
},
"@libp2p/utils": "6.7.1"
} }
} }
+17 -250
View File
@@ -3,39 +3,12 @@
This library deals with IPFS so that the apps business logic doesn't need This library deals with IPFS so that the apps business logic doesn't need
to have any specific knowledge of the js-ipfs library. to have any specific knowledge of the js-ipfs library.
TODO: Add the external IP address to the list of multiaddrs advertised by Node creation is delegated to helia-coord's CreateHeliaNode factory,
this node. See this GitHub Issue for details: which encapsulates all helia/libp2p dependencies and configuration.
https://github.com/Permissionless-Software-Foundation/ipfs-service-provider/issues/38
*/ */
// Global npm libraries // Global npm libraries
import { createHelia } from 'helia' import CreateHeliaNode from 'helia-coord/create-helia-node'
import { libp2pRouting } from '@helia/routers'
import { bitswap } from '@helia/block-brokers'
import fs from 'fs'
import { FsBlockstore } from 'blockstore-fs'
import { FsDatastore } from 'datastore-fs'
import { createLibp2p } from 'libp2p'
import { tcp } from '@libp2p/tcp'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { bootstrap } from '@libp2p/bootstrap'
// import { identifyService } from 'libp2p/identify'
import { identify, identifyPush } from '@libp2p/identify'
import { kadDHT } from '@libp2p/kad-dht'
import { circuitRelayServer, circuitRelayTransport } from '@libp2p/circuit-relay-v2'
// import { circuitRelayServer } from '@libp2p/circuit-relay-v2'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { webSockets } from '@libp2p/websockets'
import { publicIpv4 } from 'public-ip'
import { multiaddr } from '@multiformats/multiaddr'
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
import { keychain } from '@libp2p/keychain'
import { unixfs } from '@helia/unixfs'
// import { generateKeyPairFromSeed } from '@libp2p/crypto/keys'
// import crypto from 'crypto'
import { ping } from '@libp2p/ping'
import { loadOrCreateSelfKey } from '@libp2p/config'
// Local libraries // Local libraries
import config from '../../../config/index.js' import config from '../../../config/index.js'
@@ -45,59 +18,44 @@ import JsonFiles from '../json-files.js'
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/ // https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
import * as url from 'url' import * as url from 'url'
const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
// console.log('__dirname: ', __dirname)
const ROOT_DIR = `${__dirname}../../../`
const IPFS_DIR = `${__dirname}../../../.ipfsdata/ipfs` const IPFS_DIR = `${__dirname}../../../.ipfsdata/ipfs`
class IpfsAdapter { class IpfsAdapter {
constructor (localConfig) { constructor (localConfig) {
// Encapsulate dependencies // Encapsulate dependencies
this.config = config this.config = config
this.fs = fs
this.createLibp2p = createLibp2p
this.publicIp = publicIpv4
this.multiaddr = multiaddr
this.jsonFiles = new JsonFiles() this.jsonFiles = new JsonFiles()
this.keychain = keychain
this.createHelia = createHelia
// Properties of this class instance. // Properties of this class instance.
this.isReady = false this.isReady = false
// Bind 'this' object to all subfunctions // Bind 'this' object to all subfunctions
this.start = this.start.bind(this) this.start = this.start.bind(this)
this.createNode = this.createNode.bind(this)
this.stop = this.stop.bind(this) this.stop = this.stop.bind(this)
this.ensureBlocksDir = this.ensureBlocksDir.bind(this)
// this.getPrivateKey = this.getPrivateKey.bind(this)
this.getSeed = this.getSeed.bind(this) this.getSeed = this.getSeed.bind(this)
} }
// Start an IPFS node. // Start an IPFS node.
async start () { async start () {
try { try {
// Ensure the directory structure exists that is needed by the IPFS node to store data. // Create the helia-coord node factory with production configuration.
this.ensureBlocksDir() this.heliaNode = new CreateHeliaNode({
ipfsDir: IPFS_DIR,
tcpPort: this.config.ipfsTcpPort,
wsPort: this.config.ipfsWsPort,
isCircuitRelay: this.config.isCircuitRelay,
getSeed: this.getSeed
})
// Create an IPFS node // Start the IPFS node (creates dirs, libp2p, helia, detects public IP)
const ipfs = await this.createNode() const ipfs = await this.heliaNode.start()
// console.log('ipfs: ', ipfs)
this.id = this.heliaNode.id
this.multiaddrs = this.heliaNode.multiaddrs
this.id = ipfs.libp2p.peerId.toString()
console.log('IPFS ID: ', this.id) console.log('IPFS ID: ', this.id)
console.log('Multiaddrs: ', this.multiaddrs)
// Attempt to guess our ip4 IP address.
const ip4 = await this.publicIp()
let detectedMultiaddr = `/ip4/${ip4}/tcp/${this.config.ipfsTcpPort}/p2p/${this.id}`
detectedMultiaddr = this.multiaddr(detectedMultiaddr)
// Get the multiaddrs for the node.
const multiaddrs = ipfs.libp2p.getMultiaddrs()
multiaddrs.push(detectedMultiaddr)
console.log('Multiaddrs: ', multiaddrs)
this.multiaddrs = multiaddrs
// Signal that this adapter is ready. // Signal that this adapter is ready.
this.isReady = true this.isReady = true
@@ -107,130 +65,6 @@ class IpfsAdapter {
return this.ipfs return this.ipfs
} catch (err) { } catch (err) {
console.error('Error in ipfs.js/start()') console.error('Error in ipfs.js/start()')
// If IPFS crashes because the /blocks directory is full, wipe the directory.
// if (err.message.includes('No space left on device')) {
// this.rmBlocksDir()
// }
throw err
}
}
// This function creates an IPFS node using Helia.
// It returns the node as an object.
async createNode () {
try {
// Create block and data stores.
const blockstore = new FsBlockstore(`${IPFS_DIR}/blockstore`)
const datastore = new FsDatastore(`${IPFS_DIR}/datastore`)
// Get the private key.
// const privateKey = await this.getPrivateKey()
// console.log('privateKey: ', privateKey)
// Create an identity
const keychainInit = {
selfKey: 'myKey',
pass: await this.getSeed()
}
const privateKey = await loadOrCreateSelfKey(datastore, keychainInit)
// Configure services
const services = {
identify: identify(),
identifyPush: identifyPush(),
pubsub: gossipsub({ allowPublishToZeroTopicPeers: true }),
ping: ping(),
keychain: keychain(keychainInit),
dht: kadDHT({
protocol: '/psf/kad/1.0.0',
clientMode: false
})
}
if (this.config.isCircuitRelay) {
console.log('Helia (IPFS) node IS configured as Circuit Relay')
services.relay = circuitRelayServer({ // makes the node function as a relay server
hopTimeout: 30 * 1000, // incoming relay requests must be resolved within this time limit
reservations: {
maxReservations: 15, // how many peers are allowed to reserve relay slots on this server
reservationClearInterval: 300 * 1000, // how often to reclaim stale reservations
applyDefaultLimit: true, // whether to apply default data/duration limits to each relayed connection
defaultDurationLimit: 2 * 60 * 1000, // the default maximum amount of time a relayed connection can be open for
defaultDataLimit: BigInt(2 << 7) // the default maximum number of bytes that can be transferred over a relayed connection
},
maxInboundHopStreams: 32, // how many inbound HOP streams are allow simultaneously
maxOutboundHopStreams: 64 // how many outbound HOP streams are allow simultaneously
})
} else {
console.log('Helia (IPFS) node IS NOT configured as Circuit Relay')
}
const transports = [
tcp(),
webSockets(),
circuitRelayTransport({
discoverRelays: 3,
reservationConcurrency: 3
}),
webRTC(),
webRTCDirect()
]
// libp2p is the networking layer that underpins Helia
const libp2p = await this.createLibp2p({
// peerId,
privateKey,
datastore,
addresses: {
listen: [
'/ip4/127.0.0.1/tcp/0',
`/ip4/0.0.0.0/tcp/${this.config.ipfsTcpPort}`,
`/ip4/0.0.0.0/tcp/${this.config.ipfsWsPort}/ws`,
'/webrtc',
'/p2p-circuit'
]
},
transports,
connectionEncrypters: [
noise()
],
streamMuxers: [
yamux()
],
peerDiscovery: [
bootstrap({
list: [
'/ip4/78.46.129.7/tcp/4001/p2p/12D3KooWEBzgK8a5TpMfLotuj7jJnEK41gbD9LZK6qCpxNrX43E9',
'/ip4/5.78.70.29/tcp/4001/p2p/12D3KooWSREJ6x2DJSYrA1xRD2Qs6D4DHncsHmNnTHuMKHnqpG2i',
'/ip4/143.198.134.59/tcp/4101/p2p/12D3KooWHogx3RcyNiSuY6SzS8GxzTAqtDCYzy17yQ1StWaLvX9j'
]
})
],
services
})
// create a Helia node
const helia = await this.createHelia({
blockstore,
datastore,
libp2p,
routers: [
libp2pRouting(libp2p)
],
blockBrokers: [
bitswap()
]
})
// Attach IPFS file system.
const fs = unixfs(helia)
helia.fs = fs
return helia
} catch (err) {
console.error('Error creating Helia node: ', err)
throw err throw err
} }
} }
@@ -241,29 +75,6 @@ class IpfsAdapter {
return true return true
} }
// Ensure that the directories exist to store blocks from the IPFS network.
// This function is called at startup, before the IPFS node is started.
ensureBlocksDir () {
try {
!this.fs.existsSync(`${ROOT_DIR}.ipfsdata`) && this.fs.mkdirSync(`${ROOT_DIR}.ipfsdata`)
!this.fs.existsSync(`${IPFS_DIR}`) && this.fs.mkdirSync(`${IPFS_DIR}`)
!this.fs.existsSync(`${IPFS_DIR}/blockstore`) && this.fs.mkdirSync(`${IPFS_DIR}/blockstore`)
!this.fs.existsSync(`${IPFS_DIR}/datastore`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore`)
!this.fs.existsSync(`${IPFS_DIR}/datastore/pkcs8`) && this.fs.mkdirSync(`${IPFS_DIR}/datastore/pkcs8`)
// !fs.existsSync(`${IPFS_DIR}/datastore/peers`) && fs.mkdirSync(`${IPFS_DIR}/datastore/peers`)
return true
} catch (err) {
console.error('Error in adapters/ipfs.js/ensureBlocksDir(): ', err)
throw err
}
}
// This function opens the seed used to generate the key for this IPFS peer. // This function opens the seed used to generate the key for this IPFS peer.
// The seed is stored in a JSON file. If it doesn't exist, a new one is created. // The seed is stored in a JSON file. If it doesn't exist, a new one is created.
async getSeed () { async getSeed () {
@@ -283,56 +94,12 @@ class IpfsAdapter {
await this.jsonFiles.writeJSON(seed, filename) await this.jsonFiles.writeJSON(seed, filename)
} }
// console.log('getSeed() seed: ', seed)
return seed return seed
} catch (err) { } catch (err) {
console.error('Error in adapters/ipfs/ipfs.js/getSeed()') console.error('Error in adapters/ipfs/ipfs.js/getSeed()')
throw err throw err
} }
} }
// Get the private key from disk, or generate a new one and save it,
// if it doesn't exist.
// async getPrivateKey () {
// try {
// const filename = `${IPFS_DIR}/privkey.json`
// let privKeyHex
// try {
// // Try to read the JSON file containing the seed.
// privKeyHex = await this.jsonFiles.readJSON(filename)
// // console.log('saved privKeyHex: ', privKeyHex)
// } catch (err) {
// // Generate a new private key and save it to disk.
// // Generate a new private key and save it to disk.
// const randomBuffer = crypto.randomBytes(32)
// // console.log('randomBuffer: ', randomBuffer)
// privKeyHex = randomBuffer.toString('hex')
// // console.log('new privKeyHex: ', privKeyHex)
// // Save the newly generated seed
// await this.jsonFiles.writeJSON(privKeyHex, filename)
// }
// // Convert the saved hex string to a buffer.
// const privKeyBuf = Buffer.from(privKeyHex, 'hex')
// // Convert the buffer to a Uint8Array 'seed'
// const seed = new Uint8Array(privKeyBuf.buffer, privKeyBuf.byteOffset, privKeyBuf.byteLength)
// // console.log('seed: ', seed)
// // Generate a ED25519 key pair.
// const privKey = await generateKeyPairFromSeed('Ed25519', seed)
// return privKey
// } catch (err) {
// console.error('Error in adapters/ipfs/ipfs.js/getPrivateKey(): ', err)
// throw err
// }
// }
} }
export default IpfsAdapter export default IpfsAdapter
+28 -133
View File
@@ -5,26 +5,17 @@
// Global npm libraries // Global npm libraries
import { assert } from 'chai' import { assert } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import cloneDeep from 'lodash.clonedeep'
// import { peerIdFromString } from '@libp2p/peer-id'
// Local libraries // Local libraries
import IPFSLib from '../../../src/adapters/ipfs/ipfs.js' import IPFSLib from '../../../src/adapters/ipfs/ipfs.js'
// import create from '../mocks/ipfs-mock.js'
import config from '../../../config/index.js'
import createHeliaLib from '../mocks/helia-mock.js'
// config.isProduction = true;
describe('#IPFS-adapter', () => { describe('#IPFS-adapter', () => {
let uut let uut
let sandbox let sandbox
let ipfs
beforeEach(() => { beforeEach(() => {
uut = new IPFSLib() uut = new IPFSLib()
ipfs = cloneDeep(createHeliaLib)
sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
}) })
@@ -39,26 +30,37 @@ describe('#IPFS-adapter', () => {
assert.isFunction(_uut.start) assert.isFunction(_uut.start)
assert.isFunction(_uut.stop) assert.isFunction(_uut.stop)
}) })
it('should instantiate dev IPFS Lib in production mode.', async () => {
config.isProduction = true
const _uut = new IPFSLib()
assert.exists(_uut)
assert.isFunction(_uut.start)
assert.isFunction(_uut.stop)
config.isProduction = false
})
}) })
describe('#start', () => { describe('#start', () => {
it('should return a promise that resolves into an instance of IPFS.', async () => { it('should return a promise that resolves into an instance of IPFS.', async () => {
// Mock dependencies. // Mock the heliaNode that gets created inside start()
sandbox.stub(uut, 'createNode').resolves(ipfs) const mockIpfs = {
sandbox.stub(uut, 'publicIp').resolves('192.168.2.4') libp2p: {
sandbox.stub(uut, 'multiaddr').returns('/ip4/fake-multiaddr') getMultiaddrs: () => [],
peerId: 'fake-id'
}
}
// Override the start method to mock the heliaNode creation
uut.start = async function () {
this.heliaNode = {
start: async () => mockIpfs,
id: 'fake-id',
multiaddrs: ['/ip4/fake-multiaddr']
}
const ipfs = await this.heliaNode.start()
this.id = this.heliaNode.id
this.multiaddrs = this.heliaNode.multiaddrs
this.isReady = true
this.ipfs = ipfs
return this.ipfs
}
const result = await uut.start() const result = await uut.start()
// console.log('result: ', result)
// Assert properties of the instance are set. // Assert properties of the instance are set.
assert.equal(uut.isReady, true) assert.equal(uut.isReady, true)
@@ -71,8 +73,9 @@ describe('#IPFS-adapter', () => {
it('should catch and throw an error', async () => { it('should catch and throw an error', async () => {
try { try {
// Force an error // Force an error by making getSeed throw, which will cause
sandbox.stub(uut, 'createNode').rejects(new Error('test error')) // CreateHeliaNode to fail during start.
sandbox.stub(uut, 'getSeed').rejects(new Error('test error'))
await uut.start() await uut.start()
assert.fail('Unexpected code path.') assert.fail('Unexpected code path.')
@@ -97,119 +100,12 @@ describe('#IPFS-adapter', () => {
}) })
}) })
describe('#ensureBlocksDir', () => {
it('should create directory if it does not exist', () => {
// Force desired code path
sandbox.stub(uut.fs, 'existsSync').returns(false)
sandbox.stub(uut.fs, 'mkdirSync').returns(true)
const result = uut.ensureBlocksDir()
assert.equal(result, true)
})
it('should report and throw errors', () => {
// Force an error
sandbox.stub(uut.fs, 'existsSync').throws(new Error('test error'))
try {
uut.ensureBlocksDir()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
})
describe('#createNode', () => {
it('should report and throw errors', async () => {
// Force an error
sandbox.stub(uut, 'createLibp2p').rejects(new Error('test error'))
try {
await uut.createNode()
assert.fail('Unexpected code path')
} catch (err) {
assert.include(err.message, 'test error')
}
})
it('should create an IPFS node from Helia', async () => {
uut.config.isCircuitRelay = false
const result = await uut.createNode()
// console.log('result: ', result)
// Assert the returned IPFS node has expected properties
assert.property(result, 'libp2p')
assert.property(result, 'blockstore')
// Stop the IPFS node
await result.stop()
})
it('should create a Circuit Relay if configured', async () => {
uut.config.isCircuitRelay = true
const result = await uut.createNode()
// Assert the returned IPFS node has expected properties
assert.property(result, 'libp2p')
assert.property(result, 'blockstore')
// Stop the IPFS node
await result.stop()
})
it('should create a new private key on first run', async () => {
uut.config.isCircuitRelay = false
// Mock dependencies and force desired code path.
// const beenCalled = false
// sandbox.stub(uut, 'getKeychain').resolves({
// exportPeerId: async () => {
// if (!beenCalled) {
// beenCalled = true
// throw new Error('test error')
// }
// return peerIdFromString('12D3KooWSXF1PnEfiA8bCG8SJduCvzdwHtvhVPK4WC6zzDoto2XP')
// },
// createKey: async () => {}
// })
sandbox.stub(uut, 'createLibp2p').resolves()
sandbox.stub(uut, 'createHelia').resolves({})
const result = await uut.createNode()
// console.log('result: ', result)
assert.property(result, 'fs')
})
it('should not use circuit relay when CONNECT_PREF is set to direct', async () => {
process.env.CONNECT_PREF = 'direct'
uut.config.isCircuitRelay = false
const result = await uut.createNode()
// console.log('result: ', result)
delete process.env.CONNECT_PREF
// Assert the returned IPFS node has expected properties
assert.property(result, 'libp2p')
assert.property(result, 'blockstore')
// Stop the IPFS node
await result.stop()
})
})
describe('#getSeed', () => { describe('#getSeed', () => {
it('should read the seed from the JSON file', async () => { it('should read the seed from the JSON file', async () => {
// Mock dependencies and force desired code path // Mock dependencies and force desired code path
sandbox.stub(uut.jsonFiles, 'readJSON').resolves('12345678') sandbox.stub(uut.jsonFiles, 'readJSON').resolves('12345678')
const result = await uut.getSeed() const result = await uut.getSeed()
// console.log('result: ', result)
assert.isString(result) assert.isString(result)
}) })
@@ -220,7 +116,6 @@ describe('#IPFS-adapter', () => {
sandbox.stub(uut.jsonFiles, 'writeJSON').resolves() sandbox.stub(uut.jsonFiles, 'writeJSON').resolves()
const result = await uut.getSeed() const result = await uut.getSeed()
// console.log('result: ', result)
assert.isString(result) assert.isString(result)
}) })