import { Helia, Pin, createHelia } from 'helia';
import { IDBBlockstore } from 'blockstore-idb';
import { IDBDatastore } from 'datastore-idb';
import { Libp2p, createLibp2p } from 'libp2p';
import { noise } from '@chainsafe/libp2p-noise';
import { yamux } from '@chainsafe/libp2p-yamux';
// import { mplex } from '@libp2p/mplex';
import { circuitRelayTransport } from 'libp2p/circuit-relay';
import { UnixFS, unixfs, AddOptions } from '@helia/unixfs';
import { bootstrap } from '@libp2p/bootstrap';
import { webRTC, webRTCDirect } from '@libp2p/webrtc';
import { webSockets } from '@libp2p/websockets';
import { webTransport } from '@libp2p/webtransport';
import { identifyService } from 'libp2p/identify';
import { multiaddr, protocols } from '@multiformats/multiaddr';
import { LsResult } from 'ipfs-core-types/src/pin';
import {
AbortOptions,
CatOptions,
IpfsNodeType,
IpfsFileStats,
IpfsNode,
} from '../../ipfs';
// import { all } from '@libp2p/websockets/filters';
import { stringToCid } from '../../utils/cid';
import { CYBER_GATEWAY_URL } from '../../config';
async function* mapToLsResult(
iterable: AsyncIterable<Pin>
): AsyncIterable<LsResult> {
// eslint-disable-next-line no-restricted-syntax
for await (const item of iterable) {
const { cid, metadata } = item;
yield { cid: cid.toV0(), metadata, type: 'recursive' };
}
}
const libp2pFactory = async (
datastore: IDBDatastore,
bootstrapList: string[] = []
) => {
const libp2p = await createLibp2p({
datastore,
// addresses: {
// listen: [
// '/ip4/127.0.0.1/tcp/0',
// '/dns4/swarm.io.cybernode.ai/tcp/443/wss/p2p/QmUgmRxoLtGERot7Y6G7UyF6fwvnusQZfGR15PuE6pY3aB',
// ],
// },
transports: [
webSockets(),
webTransport(),
webRTC({
rtcConfiguration: {
iceServers: [
{
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
'STUN:freestun.net:3479',
'STUN:stun.bernardoprovenzano.net:3478',
'STUN:stun.aa.net.uk:3478',
],
},
{
credential: 'free',
username: 'free',
urls: ['TURN:freestun.net:3479', 'TURNS:freestun.net:5350'],
},
],
},
}),
webRTCDirect(),
circuitRelayTransport({
discoverRelays: 1,
}),
],
connectionEncryption: [noise()],
streamMuxers: [yamux()],
connectionGater: {
denyDialMultiaddr: () => {
return false;
// by default we refuse to dial local addresses from the browser since they
// are usually sent by remote peers broadcasting undialable multiaddrs but
// here we are explicitly connecting to a local node so do not deny dialing
// any discovered address
},
},
peerDiscovery: [
bootstrap({
list: bootstrapList,
}),
],
services: {
identify: identifyService(),
},
});
return libp2p;
};
const addOptionsV0: Partial<AddOptions> = {
cidVersion: 0,
rawLeaves: false,
};
class HeliaNode implements IpfsNode {
readonly nodeType: IpfsNodeType = 'helia';
get config() {
return { gatewayUrl: CYBER_GATEWAY_URL };
}
private _isStarted = false;
get isStarted() {
return this._isStarted;
}
private node?: Helia;
private fs?: UnixFS;
async init() {
const blockstore = new IDBBlockstore('helia-bs');
await blockstore.open();
const datastore = new IDBDatastore('helia-ds');
await datastore.open();
const bootstrapList = [
'/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
'/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
'/dns4/swarm.io.cybernode.ai/tcp/443/wss/p2p/QmUgmRxoLtGERot7Y6G7UyF6fwvnusQZfGR15PuE6pY3aB',
];
const libp2p = await libp2pFactory(datastore, bootstrapList);
this.node = await createHelia({ blockstore, datastore, libp2p });
this.fs = unixfs(this.node);
if (typeof window !== 'undefined') {
window.libp2p = libp2p;
window.node = this.node;
window.fs = this.fs;
window.toCid = stringToCid;
}
// DEBUG
libp2p.addEventListener('peer:connect', (evt) => {
const peerId = evt.detail.toString();
const conn = libp2p.getConnections(peerId) || [];
const transportsByAddr = Object.fromEntries(
conn.map((c) => [
c.remoteAddr.toString(),
c.remoteAddr.protoCodes().map((v) => protocols(v)?.name),
])
);
console.debug(`Connected to ${peerId}`, transportsByAddr);
// console.log(
// '---------ppppp',
// peerId,
// conn,
// conn?.remoteAddr.protoCodes().map((v) => protocols(v)?.name)
// ); //.includes(WEBRTC_CODE)
// if (conn && conn.stat) {
// const transport = conn.stat.transport; // This might vary based on libp2p version
// console.log(`Connected to ${peerId} using transport ${transport}`);
// } else {
// console.log(`Connected to ${peerId}`);
// }
});
libp2p.addEventListener('peer:disconnect', (evt) => {
console.debug(`Disconnected from ${evt.detail.toString()}`);
});
console.log(
'IPFS - Helia addrs',
libp2p.getMultiaddrs().map((a) => a.toString())
);
// const webrtcConn = await libp2p.dial(
// multiaddr(
// '/ip4/127.0.0.1/udp/4001/quic-v1/webtransport/certhash/uEiDHumbyZRFV1Av7qH9-2l5HGgU2a2UqM6eloqO0vYz5pQ/certhash/uEiDD_TuVgih5_ua31Z4MVbNq7WSw095UAQmZqdUFMDTVRA/p2p/12D3KooWEYGfgK4dEY3spfuDKVq6Jpiyj4KxP1r6HS5RFp5WHebz'
// )
// );
// console.log('----webrtcConn', webrtcConn);
this._isStarted = true;
}
async stat(cid: string, options: AbortOptions = {}): Promise<IpfsFileStats> {
return this.fs!.stat(stringToCid(cid), options).then((result) => {
const { type, fileSize, localFileSize, blocks, dagSize, mtime } = result;
return {
type,
size: fileSize || -1,
sizeLocal: localFileSize || -1,
blocks,
};
});
}
cat(cid: string, options: CatOptions = {}) {
return this.fs!.cat(stringToCid(cid), options);
}
async add(content: File | string, options: AbortOptions = {}) {
// Options to keep CID in V0 format 'Qm....';
const optionsV0 = {
...options,
...addOptionsV0,
} as Partial<AddOptions>;
let cid;
if (content instanceof File) {
const fileName = content.name;
const arrayBuffer = await content.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
cid = await this.fs!.addFile(
{ path: fileName, content: data },
optionsV0
);
} else {
const data = new TextEncoder().encode(content);
cid = await this.fs!.addBytes(data, optionsV0);
}
console.log('----added to helia', cid.toString());
this.pin(cid.toString(), options);
return cid.toString();
}
async pin(cid: string, options: AbortOptions = {}) {
const cid_ = stringToCid(cid);
const isPinned = await this.node?.pins.isPinned(cid_, options);
if (!isPinned) {
const pinResult = (
await this.node?.pins.add(cid_, options)
)?.cid.toString();
// console.log('------pin', pinResult);
}
// console.log('------pinned', cid, isPinned);
return undefined;
}
async getPeers() {
return this.node!.libp2p!.getConnections().map((c) =>
c.remotePeer.toString()
);
}
async stop() {
await this.node?.stop();
}
async start() {
await this.node?.start();
}
async connectPeer(address: string) {
const conn = await this.node!.libp2p!.dial(multiaddr(address));
return true;
}
ls() {
const result = mapToLsResult(this.node!.pins.ls());
return result;
}
async info() {
const id = this.node!.libp2p.peerId.toString();
const agentVersion = this.node!.libp2p!.services!.identify!.host!
.agentVersion as string;
return { id, agentVersion, repoSize: -1 };
}
}
// eslint-disable-next-line import/no-unused-modules
export default HeliaNode;