Skip to content

Commit 353b91b

Browse files
committed
nodejs-peer-initial-setup
1 parent b994e11 commit 353b91b

8 files changed

+12219
-0
lines changed

nodejs-peer/jest.config.cjs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
preset: 'jest-puppeteer',
3+
testTimeout: 60000, // Increase timeout for WebRTC handshakes
4+
};
5+

nodejs-peer/package-lock.json

+11,624
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs-peer/package.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "universal-connectivity-nodejs-peer",
3+
"type": "module",
4+
"main": "src/libp2p.js",
5+
"scripts": {
6+
"start": "node src/libp2p.js",
7+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
8+
"test:webrtc": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/webrtc.test.js"
9+
},
10+
"dependencies": {
11+
"@chainsafe/libp2p-gossipsub": "^14.1.0",
12+
"@chainsafe/libp2p-noise": "^16.0.1",
13+
"@chainsafe/libp2p-yamux": "^7.0.1",
14+
"@helia/delegated-routing-v1-http-api-client": "^4.2.2",
15+
"@libp2p/autonat": "^2.0.20",
16+
"@libp2p/bootstrap": "^11.0.23",
17+
"@libp2p/circuit-relay-v2": "^3.1.13",
18+
"@libp2p/dcutr": "^2.0.19",
19+
"@libp2p/identify": "^3.0.19",
20+
"@libp2p/interface": "^2.5.0",
21+
"@libp2p/kad-dht": "^14.2.8",
22+
"@libp2p/mdns": "^11.0.28",
23+
"@libp2p/mplex": "^11.0.28",
24+
"@libp2p/peer-id": "^5.0.12",
25+
"@libp2p/peer-id-factory": "^4.2.4",
26+
"@libp2p/ping": "^2.0.23",
27+
"@libp2p/pubsub-peer-discovery": "^11.0.1",
28+
"@libp2p/tcp": "^10.0.20",
29+
"@libp2p/webrtc": "^5.1.1",
30+
"@libp2p/webrtc-direct": "^6.0.0",
31+
"@libp2p/websockets": "^9.1.5",
32+
"@libp2p/webtransport": "^5.0.33",
33+
"@multiformats/multiaddr": "^12.4.0",
34+
"@types/node": "^22.13.4",
35+
"express": "^4.21.2",
36+
"it-all": "^3.0.6",
37+
"it-length-prefixed": "^10.0.1",
38+
"it-map": "^3.1.1",
39+
"it-pipe": "^3.0.1",
40+
"libp2p": "^2.6.3",
41+
"multiaddr": "^10.0.1",
42+
"multiformats": "^13.3.2",
43+
"ts-node": "^10.9.2",
44+
"typescript": "^5.7.3",
45+
"uint8arrays": "^5.1.0",
46+
"ws": "^8.18.1"
47+
},
48+
"devDependencies": {
49+
"jest": "^29.7.0",
50+
"jest-puppeteer": "^11.0.0",
51+
"puppeteer": "^24.3.0"
52+
}
53+
}

nodejs-peer/src/constants.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const CHAT_TOPIC = 'universal-connectivity'
2+
export const CHAT_FILE_TOPIC = 'universal-connectivity-nodejs-file'
3+
export const PUBSUB_PEER_DISCOVERY = 'universal-connectivity-nodejs-peer-discovery'
4+
export const FILE_EXCHANGE_PROTOCOL = '/universal-connectivity-nodejs-file/1'
5+
export const DIRECT_MESSAGE_PROTOCOL = '/universal-connectivity-nodejs/dm/1.0.0'
6+
7+
export const CIRCUIT_RELAY_CODE = 291
8+
9+
export const MIME_TEXT_PLAIN = 'text/plain'
10+
11+
// 👇 App specific dedicated bootstrap PeerIDs
12+
// Their multiaddrs are ephemeral so peer routing is used to resolve multiaddr
13+
export const WEBTRANSPORT_BOOTSTRAP_PEER_ID = '12D3KooWH7MdJvo6L1ZvBmr9mgg5fZPCzQNG7UKKduxRqwiNDX6E'
14+
15+
export const BOOTSTRAP_PEER_IDS = [WEBTRANSPORT_BOOTSTRAP_PEER_ID]

nodejs-peer/src/dialer.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { createLibp2p } from "libp2p";
2+
import { noise } from "@chainsafe/libp2p-noise";
3+
import { yamux } from "@chainsafe/libp2p-yamux";
4+
import { tcp } from "@libp2p/tcp";
5+
import { webSockets } from "@libp2p/websockets";
6+
import { webRTC, webRTCDirect } from "@libp2p/webrtc";
7+
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
8+
import { identify } from "@libp2p/identify";
9+
import { multiaddr } from "@multiformats/multiaddr";
10+
import { pubsubPeerDiscovery } from "@libp2p/pubsub-peer-discovery";
11+
import { ping } from "@libp2p/ping";
12+
import { gossipsub } from "@chainsafe/libp2p-gossipsub";
13+
import { PUBSUB_PEER_DISCOVERY } from "./constants";
14+
15+
const dialPeer = async (peerMultiaddr) => {
16+
const dialer = await createLibp2p({
17+
addresses: {
18+
listen: [
19+
"/ip4/0.0.0.0/tcp/0",
20+
"/ip4/0.0.0.0/tcp/0/ws",
21+
"/webrtc",
22+
"/webrtc-direct",
23+
],
24+
},
25+
transports: [
26+
tcp(),
27+
webSockets(),
28+
webRTC(),
29+
webRTCDirect(),
30+
circuitRelayTransport({ discoverRelays: 1 }),
31+
],
32+
connectionEncrypters: [noise()],
33+
streamMuxers: [yamux(), mplex()],
34+
services: {
35+
identify: identify(),
36+
ping: ping(),
37+
pubsub: gossipsub(),
38+
},
39+
peerDiscovery: [
40+
pubsubPeerDiscovery({
41+
interval: 60000,
42+
topics: [PUBSUB_PEER_DISCOVERY],
43+
listenOnly: false,
44+
}),
45+
],
46+
});
47+
48+
console.log("Dialer started, listening on:");
49+
dialer.getMultiaddrs().forEach((addr) => console.log(addr.toString()));
50+
51+
try {
52+
console.log(`🔄 Dialing peer: ${peerMultiaddr}`);
53+
const conn = await dialer.dial(multiaddr(peerMultiaddr));
54+
console.log(`✅ Successfully dialed ${conn.remotePeer.toString()}`);
55+
} catch (err) {
56+
console.error(`❌ Dialing failed: ${err.message}`);
57+
}
58+
};
59+
60+
const peerMultiaddr = process.argv[2];
61+
if (!peerMultiaddr) {
62+
console.error("❌ Please provide a peer multiaddr to dial.");
63+
process.exit(1);
64+
}
65+
66+
dialPeer(peerMultiaddr).catch((err) =>
67+
console.log("❌ Unexpected error:", err.message)
68+
);

nodejs-peer/src/libp2p.js

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { createLibp2p } from "libp2p";
2+
import { webRTC } from "@libp2p/webrtc";
3+
import { webSockets } from "@libp2p/websockets";
4+
import { tcp } from "@libp2p/tcp";
5+
import { noise } from "@chainsafe/libp2p-noise";
6+
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
7+
import { identify, identifyPush } from "@libp2p/identify";
8+
import { dcutr } from "@libp2p/dcutr";
9+
import { autoNAT } from "@libp2p/autonat";
10+
import { yamux } from "@chainsafe/libp2p-yamux";
11+
import { pubsubPeerDiscovery } from "@libp2p/pubsub-peer-discovery";
12+
import { gossipsub } from "@chainsafe/libp2p-gossipsub";
13+
import { CHAT_FILE_TOPIC, CHAT_TOPIC, PUBSUB_PEER_DISCOVERY } from "./constants.js";
14+
import { kadDHT } from "@libp2p/kad-dht";
15+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
16+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
17+
import { sha256 } from 'multiformats/hashes/sha2';
18+
import { stdinToStream, streamToConsole } from './stream.js';
19+
20+
// Define the universal connectivity protocol constant
21+
const UNIVERSAL_PROTOCOL = '/universal/1.0.0';
22+
23+
export async function createNode() {
24+
const node = await createLibp2p({
25+
addresses: {
26+
listen: [
27+
"/ip4/0.0.0.0/tcp/0/ws",
28+
"/ip4/0.0.0.0/tcp/0",
29+
'/webrtc',
30+
'/webrtc-direct',
31+
],
32+
},
33+
transports: [
34+
webSockets(),
35+
tcp(),
36+
webRTC(),
37+
circuitRelayTransport({ discoverRelays: 1 })
38+
],
39+
connectionEncrypters: [noise()],
40+
connectionManager: {
41+
maxConnections: 100,
42+
minConnections: 5,
43+
autoDialInterval: 30000,
44+
dialTimeout: 30000,
45+
},
46+
connectionGater: {
47+
denyDialMultiaddr: async ({ multiaddr }) => false,
48+
},
49+
streamMuxers: [yamux()],
50+
services: {
51+
pubsub: gossipsub({
52+
allowPublishToZeroTopicPeers: true,
53+
msgIdFn: msgIdFnStrictNoSign,
54+
ignoreDuplicatePublishError: true,
55+
}),
56+
identify: identify(),
57+
identifyPush: identifyPush(),
58+
dcutr: dcutr(),
59+
kadDHT: kadDHT(),
60+
autoNAT: autoNAT({
61+
protocolPrefix: "libp2p",
62+
startupDelay: 5000,
63+
refreshInterval: 60000,
64+
}),
65+
},
66+
peerDiscovery: [
67+
pubsubPeerDiscovery({
68+
interval: 30000,
69+
topics: [PUBSUB_PEER_DISCOVERY],
70+
listenOnly: false,
71+
}),
72+
],
73+
});
74+
return node;
75+
}
76+
77+
export async function msgIdFnStrictNoSign(msg) {
78+
const enc = new TextEncoder();
79+
const encodedSeqNum = enc.encode(msg.sequenceNumber.toString());
80+
return await sha256.encode(encodedSeqNum);
81+
}
82+
83+
/**
84+
* Helper to dial a target multiaddr using the specified protocol.
85+
* Sets up interactive pipes for stdin and stdout.
86+
*/
87+
async function robustDial(sourceNode, targetMultiaddr, protocol = UNIVERSAL_PROTOCOL) {
88+
try {
89+
console.log(`Attempting to dial ${targetMultiaddr} using protocol ${protocol}`);
90+
const stream = await sourceNode.dialProtocol(targetMultiaddr, protocol);
91+
console.log(`Successfully dialed ${targetMultiaddr} with protocol ${protocol}`);
92+
// Set up interactive communication
93+
stdinToStream(stream);
94+
streamToConsole(stream);
95+
return stream;
96+
} catch (error) {
97+
console.error(`Failed to dial ${targetMultiaddr} using protocol ${protocol}: ${error.message}`);
98+
throw error;
99+
}
100+
}
101+
102+
async function main() {
103+
// Create two nodes concurrently for testing purposes.
104+
const [node1, node2] = await Promise.all([createNode(), createNode()]);
105+
106+
console.log(`Node1 ID: ${node1.peerId.toString()}`);
107+
node1.getMultiaddrs().forEach(addr => console.log(`Node1 listening on: ${addr.toString()}`));
108+
109+
console.log(`Node2 ID: ${node2.peerId.toString()}`);
110+
node2.getMultiaddrs().forEach(addr => console.log(`Node2 listening on: ${addr.toString()}`));
111+
112+
// // Setup universal protocol handler on node2.
113+
node2.handle(UNIVERSAL_PROTOCOL, async ({ stream, connection }) => {
114+
console.log(`Node2 received connection on ${UNIVERSAL_PROTOCOL} from ${connection.remotePeer.toString()}`);
115+
try {
116+
// Establish interactive communication.
117+
stdinToStream(stream);
118+
streamToConsole(stream);
119+
} catch (err) {
120+
console.log('Error in universal protocol handler on node2:', err.message);
121+
}
122+
});
123+
124+
// Directly dial node2 from node1 using one of node2's multiaddrs.
125+
const targetAddr = node2.getMultiaddrs()[0];
126+
if (targetAddr) {
127+
await robustDial(node1, targetAddr, UNIVERSAL_PROTOCOL);
128+
} else {
129+
console.warn('No multiaddr found for node2');
130+
}
131+
132+
// Log new connections on node1.
133+
node1.addEventListener('connection:open', (evt) => {
134+
try {
135+
const conn = evt.detail;
136+
console.log(`Node1: New connection opened from peer ${conn.remotePeer}`);
137+
console.log('Connection details:', conn);
138+
} catch (err) {
139+
console.log('Error in connection:open listener on node1:', err.message);
140+
}
141+
});
142+
143+
// When a peer is discovered, attempt to dial using the universal protocol.
144+
node2.addEventListener('peer:discovery', async (evt) => {
145+
console.info('Node1 discovered peer:', evt.detail);
146+
const discoveredMultiaddrs = evt.detail.id;
147+
if (discoveredMultiaddrs && discoveredMultiaddrs.length > 0) {
148+
try {
149+
await robustDial(node1, discoveredMultiaddrs, UNIVERSAL_PROTOCOL);
150+
} catch (error) {
151+
console.log('Error dialing discovered peer:', error.message);
152+
}
153+
}
154+
});
155+
156+
// Setup pubsub subscriptions and logging.
157+
node1.services.pubsub.subscribe(CHAT_TOPIC);
158+
node1.services.pubsub.addEventListener('message', (evt) => {
159+
console.log(`Node1 received on topic ${evt.detail.topic}: ${uint8ArrayToString(evt.detail.data)}`);
160+
});
161+
162+
node2.services.pubsub.subscribe(CHAT_TOPIC);
163+
node2.services.pubsub.addEventListener('message', (evt) => {
164+
console.log(`Node2 received on topic ${evt.detail.topic}: ${uint8ArrayToString(evt.detail.data)}`);
165+
});
166+
167+
// For testing: Node2 periodically publishes messages on several topics.
168+
setInterval(() => {
169+
node2.services.pubsub.publish(CHAT_TOPIC, uint8ArrayFromString('Hello Go & Rust!'))
170+
.catch(err => console.log(`Error publishing to ${CHAT_TOPIC}:`, err.message));
171+
}, 3000);
172+
173+
console.log('Nodes are running and ready for robust dialing and interactions.');
174+
}
175+
176+
main().catch(err => {
177+
console.log('Main execution error:', err.message);
178+
process.exit(1);
179+
});
180+
181+
export class NATManager {
182+
constructor(node) {
183+
node.addEventListener('self:nat:status', (evt) => {
184+
console.log('NAT Status:', evt.detail)
185+
if(evt.detail === 'UNSUPPORTED') {
186+
console.log('Enabling circuit relay as fallback')
187+
node.configure(circuitRelayTransport({ discoverRelays: 2 }))
188+
}
189+
})
190+
}
191+
}

nodejs-peer/src/stream.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/* eslint-disable no-console */
2+
import * as lp from 'it-length-prefixed'
3+
import map from 'it-map'
4+
import { pipe } from 'it-pipe'
5+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
6+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
7+
8+
// Helper: Reads data from stdin and writes to a stream with length-prefixed encoding.
9+
export function stdinToStream(stream) {
10+
process.stdin.setEncoding('utf8');
11+
pipe(
12+
process.stdin,
13+
(source) => map(source, (string) => uint8ArrayFromString(string + '\n')),
14+
(source) => lp.encode(source),
15+
stream.sink
16+
).catch(err => {
17+
console.log('Error in stdinToStream:', err.message);
18+
});
19+
}
20+
21+
// Helper: Reads length-prefixed data from a stream and outputs it to the console.
22+
export function streamToConsole(stream) {
23+
pipe(
24+
stream.source,
25+
(source) => lp.decode(source),
26+
(source) => map(source, (buf) => uint8ArrayToString(buf.subarray())),
27+
async function (source) {
28+
for await (const msg of source) {
29+
console.log('> ' + msg.toString().trim());
30+
}
31+
}
32+
).catch(err => {
33+
console.log('Error in streamToConsole:', err.message);
34+
});
35+
}

0 commit comments

Comments
 (0)