JS peer improvements (#116)

* feat: delegated routing for bootstrap multiaddrs

* feat: allow getting messages when not on chat page

also clean up and refactor some of the logic

* docs: add log line

* chore: disable eslint warning

* chore: enable indexeddb datastore

---------

Co-authored-by: Daniel N <2color@users.noreply.github.com>
This commit is contained in:
Daniel Norman
2024-04-26 14:56:39 +02:00
committed by GitHub
parent 31a99ed91e
commit 86462f2b4c
11 changed files with 411 additions and 290 deletions

View File

@@ -11,6 +11,7 @@
"@chainsafe/libp2p-yamux": "^6.0.2",
"@download/blockies": "^1.0.3",
"@headlessui/react": "^1.7.19",
"@helia/delegated-routing-v1-http-api-client": "^3.0.1",
"@heroicons/react": "^2.1.3",
"@libp2p/bootstrap": "^10.0.20",
"@libp2p/circuit-relay-v2": "^1.0.20",
@@ -21,6 +22,7 @@
"@libp2p/websockets": "^8.0.20",
"@libp2p/webtransport": "^4.0.27",
"@multiformats/multiaddr": "^12.2.1",
"datastore-idb": "^2.1.9",
"debug": "^4.3.4",
"it-length-prefixed": "^9.0.4",
"it-map": "^3.0.5",
@@ -2489,6 +2491,32 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@helia/delegated-routing-v1-http-api-client": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@helia/delegated-routing-v1-http-api-client/-/delegated-routing-v1-http-api-client-3.0.1.tgz",
"integrity": "sha512-Gkaw3B8IjgCCXtasa17j8wQaFdzph2s0RiLyrJTS2htmwVFWZEjLNLUDJeWsMHBoLBza8SrVGvQ9fiN3XD3rfg==",
"dependencies": {
"@libp2p/interface": "^1.1.1",
"@libp2p/logger": "^4.0.4",
"@libp2p/peer-id": "^4.0.4",
"@multiformats/multiaddr": "^12.1.3",
"any-signal": "^4.1.1",
"browser-readablestream-to-it": "^2.0.3",
"ipns": "^9.0.0",
"it-first": "^3.0.3",
"it-map": "^3.0.4",
"it-ndjson": "^1.0.4",
"multiformats": "^13.0.0",
"p-defer": "^4.0.0",
"p-queue": "^8.0.1",
"uint8arrays": "^5.0.1"
}
},
"node_modules/@helia/delegated-routing-v1-http-api-client/node_modules/multiformats": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz",
"integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ=="
},
"node_modules/@heroicons/react": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz",
@@ -5257,6 +5285,11 @@
"node": ">=8"
}
},
"node_modules/browser-readablestream-to-it": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/browser-readablestream-to-it/-/browser-readablestream-to-it-2.0.6.tgz",
"integrity": "sha512-csJm66U/gTC6VHjeaOaziK6Y6ENdrzlNLdXnsdnvGX+3hGvedkxTyiMk2WbgKR8F15ACxDLJhDuE/cmovLPBQQ=="
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
@@ -5451,6 +5484,14 @@
}
]
},
"node_modules/cborg": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/cborg/-/cborg-4.2.0.tgz",
"integrity": "sha512-q6cFW5m3KxfP/9xGI3yGLaC1l5DP6DWM9IvjiJojnIwohL5CQDl02EXViPV852mOfQo+7PJGPN01MI87vFGzyA==",
"bin": {
"cborg": "lib/bin.js"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"license": "MIT",
@@ -5860,6 +5901,18 @@
"it-take": "^3.0.4"
}
},
"node_modules/datastore-idb": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/datastore-idb/-/datastore-idb-2.1.9.tgz",
"integrity": "sha512-o1LAE2VgVMeEWOP/zHYHV6MetGzbN+C72nRRUFwPXk0xLopsPN9wlH73D3De3pyDZKoKp2875XDFpRkDJ8mGWA==",
"dependencies": {
"datastore-core": "^9.0.0",
"idb": "^8.0.0",
"interface-datastore": "^8.0.0",
"it-filter": "^3.0.4",
"it-sort": "^3.0.4"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
@@ -7607,6 +7660,11 @@
"node": ">=10.17.0"
}
},
"node_modules/idb": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz",
"integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -7725,6 +7783,30 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/ipns": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/ipns/-/ipns-9.1.0.tgz",
"integrity": "sha512-up2o1Qx9tSSfh73k69j3/Acacua6JbffTe5xA8+/fv6ibkQyhriMPHlgae1896DwmQkJrusKgBs7EAOi3yrO2w==",
"dependencies": {
"@libp2p/crypto": "^4.0.0",
"@libp2p/interface": "^1.1.0",
"@libp2p/logger": "^4.0.3",
"@libp2p/peer-id": "^4.0.3",
"cborg": "^4.0.1",
"err-code": "^3.0.1",
"interface-datastore": "^8.1.0",
"multiformats": "^13.0.0",
"protons-runtime": "^5.2.1",
"timestamp-nano": "^1.0.0",
"uint8arraylist": "^2.4.8",
"uint8arrays": "^5.0.1"
}
},
"node_modules/ipns/node_modules/multiformats": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz",
"integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ=="
},
"node_modules/is-arguments": {
"version": "1.1.1",
"dev": true,
@@ -8230,6 +8312,11 @@
"it-peekable": "^3.0.0"
}
},
"node_modules/it-first": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/it-first/-/it-first-3.0.5.tgz",
"integrity": "sha512-LAZ4/pOU1GCDR0VjsAhfVixedlXbyXYKvgmUWl9AG9Ran67OySb1PB41EkILYLeiD9UNRxT05JxEjU4e6rlKHg=="
},
"node_modules/it-foreach": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/it-foreach/-/it-foreach-2.0.6.tgz",
@@ -8287,6 +8374,11 @@
"it-pushable": "^3.2.0"
}
},
"node_modules/it-ndjson": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/it-ndjson/-/it-ndjson-1.0.6.tgz",
"integrity": "sha512-44QB+rmfB2C8L2WDv03+J3kcOQYuIPBP3KBN+szZTEsvvNyoQEQSnOXoGouNpRfgMA1rEqqoK6roenCJaBeYSQ=="
},
"node_modules/it-pair": {
"version": "2.0.6",
"license": "Apache-2.0 OR MIT",
@@ -12372,6 +12464,14 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/timestamp-nano": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/timestamp-nano/-/timestamp-nano-1.0.1.tgz",
"integrity": "sha512-4oGOVZWTu5sl89PtCDnhQBSt7/vL1zVEwAfxH1p49JhTosxzVQWYBYFRFZ8nJmo0G6f824iyP/44BFAwIoKvIA==",
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/titleize": {
"version": "3.0.0",
"dev": true,

View File

@@ -12,6 +12,7 @@
"@chainsafe/libp2p-yamux": "^6.0.2",
"@download/blockies": "^1.0.3",
"@headlessui/react": "^1.7.19",
"@helia/delegated-routing-v1-http-api-client": "^3.0.1",
"@heroicons/react": "^2.1.3",
"@libp2p/bootstrap": "^10.0.20",
"@libp2p/circuit-relay-v2": "^1.0.20",
@@ -22,6 +23,7 @@
"@libp2p/websockets": "^8.0.20",
"@libp2p/webtransport": "^4.0.27",
"@multiformats/multiaddr": "^12.2.1",
"datastore-idb": "^2.1.9",
"debug": "^4.3.4",
"it-length-prefixed": "^9.0.4",
"it-map": "^3.0.5",

View File

@@ -3,159 +3,25 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import type { Message } from '@libp2p/interface'
import { CHAT_FILE_TOPIC, CHAT_TOPIC, FILE_EXCHANGE_PROTOCOL } from '@/lib/constants'
import { createIcon } from '@download/blockies'
import { ChatMessage, useChatContext } from '../context/chat-ctx'
import { ChatFile, ChatMessage, useChatContext } from '../context/chat-ctx'
import { v4 as uuidv4 } from 'uuid';
import { ChatFile, useFileChatContext } from '@/context/file-ctx'
import { pipe } from 'it-pipe'
import map from 'it-map'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import * as lp from 'it-length-prefixed'
import { MessageComponent } from './message'
interface MessageProps extends ChatMessage { }
function Message({ msg, fileObjectUrl, from, peerId }: MessageProps) {
const msgref = React.useRef<HTMLLIElement>(null)
const { libp2p } = useLibp2pContext()
useEffect(() => {
const icon = createIcon({
seed: peerId,
size: 15,
scale: 3,
})
icon.className = 'rounded mr-2 max-h-10 max-w-10'
const childrenCount = msgref.current?.childElementCount
// Prevent inserting an icon more than once.
if (childrenCount && childrenCount < 2) {
msgref.current?.insertBefore(icon, msgref.current?.firstChild)
}
}, [peerId])
return (
<li ref={msgref} className={`flex ${from === 'me' ? 'justify-end' : 'justify-start'}`}>
<div
className="flex relative max-w-xl px-4 py-2 text-gray-700 rounded shadow bg-white"
>
<div className="block">
{msg}
<p>{fileObjectUrl ? <a href={fileObjectUrl} target="_blank"><b>Download</b></a> : ""}</p>
<p className="italic text-gray-400">{peerId !== libp2p.peerId.toString() ? `from: ${peerId.slice(-4)}` : null} </p>
</div>
</div>
</li>
)
}
export default function ChatContainer() {
const { libp2p } = useLibp2pContext()
const { messageHistory, setMessageHistory } = useChatContext();
const { files, setFiles } = useFileChatContext();
const { messageHistory, setMessageHistory, files, setFiles } = useChatContext();
const [input, setInput] = useState<string>('')
const fileRef = useRef<HTMLInputElement>(null);
// Effect hook to subscribe to pubsub events and update the message state hook
useEffect(() => {
const messageCB = async (evt: CustomEvent<Message>) => {
console.log('gossipsub console log', evt.detail)
// FIXME: Why does 'from' not exist on type 'Message'?
const { topic, data } = evt.detail
switch (topic) {
case CHAT_TOPIC: {
chatMessageCB(evt, topic, data)
break
}
case CHAT_FILE_TOPIC: {
chatFileMessageCB(evt, topic, data)
break
}
default: {
throw new Error(`Unexpected gossipsub topic: ${topic}`)
}
}
}
const messageCBWrapper = (evt: Event) => {
const customEvent = evt as CustomEvent<Message>;
(async () => messageCB(customEvent))();
};
const chatMessageCB = (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
const msg = new TextDecoder().decode(data)
console.log(`${topic}: ${msg}`)
// Append signed messages, otherwise discard
if (evt.detail.type === 'signed') {
setMessageHistory([...messageHistory, { msg, fileObjectUrl: undefined, from: 'other', peerId: evt.detail.from.toString() }])
}
}
const chatFileMessageCB = async (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
const fileId = new TextDecoder().decode(data)
// if the message isn't signed, discard it.
if (evt.detail.type !== 'signed') {
return
}
const senderPeerId = evt.detail.from;
try {
const stream = await libp2p.dialProtocol(senderPeerId, FILE_EXCHANGE_PROTOCOL)
await pipe(
[uint8ArrayFromString(fileId)],
(source) => lp.encode(source),
stream,
(source) => lp.decode(source),
async function(source) {
for await (const data of source) {
const body: Uint8Array = data.subarray()
console.log(`request_response: response received: size:${body.length}`)
const msg: ChatMessage = {
msg: newChatFileMessage(fileId, body),
fileObjectUrl: window.URL.createObjectURL(new Blob([body])),
from: 'other',
peerId: senderPeerId.toString(),
}
setMessageHistory([...messageHistory, msg])
}
}
)
} catch (e) {
console.error(e)
}
}
libp2p.services.pubsub.addEventListener('message', messageCBWrapper)
libp2p.handle(FILE_EXCHANGE_PROTOCOL, ({ stream }) => {
pipe(
stream.source,
(source) => lp.decode(source),
(source) => map(source, async (msg) => {
const fileId = uint8ArrayToString(msg.subarray())
const file = files.get(fileId)!
return file.body
}),
(source) => lp.encode(source),
stream.sink,
)
})
return () => {
(async () => {
// Cleanup handlers 👇
// libp2p.services.pubsub.unsubscribe(CHAT_TOPIC)
// libp2p.services.pubsub.unsubscribe(CHAT_FILE_TOPIC)
libp2p.services.pubsub.removeEventListener('message', messageCBWrapper)
await libp2p.unhandle(FILE_EXCHANGE_PROTOCOL)
})();
}
}, [libp2p, messageHistory, setMessageHistory, files])
const sendMessage = useCallback(async () => {
if (input === '') return
@@ -284,7 +150,7 @@ export default function ChatContainer() {
<ul className="space-y-2">
{/* messages start */}
{messageHistory.map(({ msg, fileObjectUrl, from, peerId }, idx) => (
<Message key={idx} msg={msg} fileObjectUrl={fileObjectUrl} from={from} peerId={peerId} />
<MessageComponent key={idx} msg={msg} fileObjectUrl={fileObjectUrl} from={from} peerId={peerId} />
))}
{/* messages end */}
</ul>
@@ -370,95 +236,3 @@ export default function ChatContainer() {
)
}
export function RoomList() {
return (
<div className="border-r border-gray-300 lg:col-span-1">
<div className="mx-3 my-3">
<div className="relative text-gray-600">
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className="w-6 h-6 text-gray-300"
>
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</span>
<input
type="search"
className="block w-full py-2 pl-10 bg-gray-100 rounded outline-none"
name="search"
placeholder="Search"
required
/>
</div>
</div>
<ul className="overflow-auto h-[32rem]">
<h2 className="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2>
<li>
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
<img
className="object-cover w-10 h-10 rounded-full"
src="https://github.com/2color.png"
alt="username"
/>
<div className="w-full pb-2">
<div className="flex justify-between">
<span className="block ml-2 font-semibold text-gray-600">
Daniel
</span>
<span className="block ml-2 text-sm text-gray-600">
25 minutes
</span>
</div>
<span className="block ml-2 text-sm text-gray-600">bye</span>
</div>
</a>
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out bg-gray-100 border-b border-gray-300 cursor-pointer focus:outline-none">
<img
className="object-cover w-10 h-10 rounded-full"
src="https://github.com/achingbrain.png"
alt="username"
/>
<div className="w-full pb-2">
<div className="flex justify-between">
<span className="block ml-2 font-semibold text-gray-600">
Alex
</span>
<span className="block ml-2 text-sm text-gray-600">
50 minutes
</span>
</div>
<span className="block ml-2 text-sm text-gray-600">
Good night
</span>
</div>
</a>
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
<img
className="object-cover w-10 h-10 rounded-full"
src="https://github.com/hannahhoward.png"
alt="username"
/>
<div className="w-full pb-2">
<div className="flex justify-between">
<span className="block ml-2 font-semibold text-gray-600">
Hannah
</span>
<span className="block ml-2 text-sm text-gray-600">6 hour</span>
</div>
<span className="block ml-2 text-sm text-gray-600">
Good Morning
</span>
</div>
</a>
</li>
</ul>
</div>
)
}

View File

@@ -0,0 +1,43 @@
import { useLibp2pContext } from '@/context/ctx'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { createIcon } from '@download/blockies'
import { ChatMessage } from '@/context/chat-ctx'
interface MessageProps extends ChatMessage { }
export function MessageComponent({ msg, fileObjectUrl, from, peerId }: MessageProps) {
const msgref = React.useRef<HTMLLIElement>(null)
const { libp2p } = useLibp2pContext()
useEffect(() => {
const icon = createIcon({
seed: peerId,
size: 15,
scale: 3,
})
icon.className = 'rounded mr-2 max-h-10 max-w-10'
const childrenCount = msgref.current?.childElementCount
// Prevent inserting an icon more than once.
if (childrenCount && childrenCount < 2) {
msgref.current?.insertBefore(icon, msgref.current?.firstChild)
}
}, [peerId])
return (
<li ref={msgref} className={`flex ${from === 'me' ? 'justify-end' : 'justify-start'}`}>
<div
className="flex relative max-w-xl px-4 py-2 text-gray-700 rounded shadow bg-white"
>
<div className="block">
{msg}
<p>{fileObjectUrl ? <a href={fileObjectUrl} target="_blank"><b>Download</b></a> : ""}</p>
<p className="italic text-gray-400">{peerId !== libp2p.peerId.toString() ? `from: ${peerId.slice(-4)}` : null} </p>
</div>
</div>
</li>
)
}

View File

@@ -0,0 +1,92 @@
/* eslint-disable @next/next/no-img-element */
import React from 'react';
export function RoomList() {
return (
<div className="border-r border-gray-300 lg:col-span-1">
<div className="mx-3 my-3">
<div className="relative text-gray-600">
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
<svg
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
className="w-6 h-6 text-gray-300"
>
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</span>
<input
type="search"
className="block w-full py-2 pl-10 bg-gray-100 rounded outline-none"
name="search"
placeholder="Search"
required />
</div>
</div>
<ul className="overflow-auto h-[32rem]">
<h2 className="my-2 mb-2 ml-2 text-lg text-gray-600">Chats</h2>
<li>
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
<img
className="object-cover w-10 h-10 rounded-full"
src="https://github.com/2color.png"
alt="username" />
<div className="w-full pb-2">
<div className="flex justify-between">
<span className="block ml-2 font-semibold text-gray-600">
Daniel
</span>
<span className="block ml-2 text-sm text-gray-600">
25 minutes
</span>
</div>
<span className="block ml-2 text-sm text-gray-600">bye</span>
</div>
</a>
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out bg-gray-100 border-b border-gray-300 cursor-pointer focus:outline-none">
<img
className="object-cover w-10 h-10 rounded-full"
src="https://github.com/achingbrain.png"
alt="username" />
<div className="w-full pb-2">
<div className="flex justify-between">
<span className="block ml-2 font-semibold text-gray-600">
Alex
</span>
<span className="block ml-2 text-sm text-gray-600">
50 minutes
</span>
</div>
<span className="block ml-2 text-sm text-gray-600">
Good night
</span>
</div>
</a>
<a className="flex items-center px-3 py-2 text-sm transition duration-150 ease-in-out border-b border-gray-300 cursor-pointer hover:bg-gray-100 focus:outline-none">
<img
className="object-cover w-10 h-10 rounded-full"
src="https://github.com/hannahhoward.png"
alt="username" />
<div className="w-full pb-2">
<div className="flex justify-between">
<span className="block ml-2 font-semibold text-gray-600">
Hannah
</span>
<span className="block ml-2 text-sm text-gray-600">6 hour</span>
</div>
<span className="block ml-2 text-sm text-gray-600">
Good Morning
</span>
</div>
</a>
</li>
</ul>
</div>
);
}

View File

@@ -1,4 +1,13 @@
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext, useEffect, useState } from 'react';
import { useLibp2pContext } from './ctx';
import type { Message } from '@libp2p/interface'
import { CHAT_FILE_TOPIC, CHAT_TOPIC, FILE_EXCHANGE_PROTOCOL } from '@/lib/constants'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { pipe } from 'it-pipe'
import map from 'it-map'
import * as lp from 'it-length-prefixed'
export interface ChatMessage {
msg: string
@@ -7,13 +16,23 @@ export interface ChatMessage {
peerId: string
}
export interface ChatFile {
id: string
body: Uint8Array
sender: string
}
export interface ChatContextInterface {
messageHistory: ChatMessage[];
setMessageHistory: (messageHistory: ChatMessage[]) => void;
files: Map<string, ChatFile>
setFiles: (files: Map<string, ChatFile>) => void;
}
export const chatContext = createContext<ChatContextInterface>({
messageHistory: [],
setMessageHistory: () => { }
files: new Map<string, ChatFile>(),
setMessageHistory: () => { },
setFiles: () => { }
})
export const useChatContext = () => {
@@ -22,9 +41,107 @@ export const useChatContext = () => {
export const ChatProvider = ({ children }: any) => {
const [messageHistory, setMessageHistory] = useState<ChatMessage[]>([]);
const [files, setFiles] = useState<Map<string, ChatFile>>(new Map<string, ChatFile>());
const { libp2p } = useLibp2pContext()
const messageCB = (evt: CustomEvent<Message>) => {
console.log('gossipsub console log', evt.detail)
// FIXME: Why does 'from' not exist on type 'Message'?
const { topic, data } = evt.detail
switch (topic) {
case CHAT_TOPIC: {
chatMessageCB(evt, topic, data)
break
}
case CHAT_FILE_TOPIC: {
chatFileMessageCB(evt, topic, data)
break
}
default: {
throw new Error(`Unexpected gossipsub topic: ${topic}`)
}
}
}
const chatMessageCB = (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
const msg = new TextDecoder().decode(data)
console.log(`${topic}: ${msg}`)
// Append signed messages, otherwise discard
if (evt.detail.type === 'signed') {
setMessageHistory([...messageHistory, { msg, fileObjectUrl: undefined, from: 'other', peerId: evt.detail.from.toString() }])
}
}
const chatFileMessageCB = async (evt: CustomEvent<Message>, topic: string, data: Uint8Array) => {
const newChatFileMessage = (id: string, body: Uint8Array) => {
return `File: ${id} (${body.length} bytes)`
}
const fileId = new TextDecoder().decode(data)
// if the message isn't signed, discard it.
if (evt.detail.type !== 'signed') {
return
}
const senderPeerId = evt.detail.from;
try {
const stream = await libp2p.dialProtocol(senderPeerId, FILE_EXCHANGE_PROTOCOL)
await pipe(
[uint8ArrayFromString(fileId)],
(source) => lp.encode(source),
stream,
(source) => lp.decode(source),
async function(source) {
for await (const data of source) {
const body: Uint8Array = data.subarray()
console.log(`request_response: response received: size:${body.length}`)
const msg: ChatMessage = {
msg: newChatFileMessage(fileId, body),
fileObjectUrl: window.URL.createObjectURL(new Blob([body])),
from: 'other',
peerId: senderPeerId.toString(),
}
setMessageHistory([...messageHistory, msg])
}
}
)
} catch (e) {
console.error(e)
}
}
useEffect(() => {
libp2p.services.pubsub.addEventListener('message', messageCB)
libp2p.handle(FILE_EXCHANGE_PROTOCOL, ({ stream }) => {
pipe(
stream.source,
(source) => lp.decode(source),
(source) => map(source, async (msg) => {
const fileId = uint8ArrayToString(msg.subarray())
const file = files.get(fileId)!
return file.body
}),
(source) => lp.encode(source),
stream.sink,
)
})
return () => {
(async () => {
// Cleanup handlers 👇
libp2p.services.pubsub.removeEventListener('message', messageCB)
await libp2p.unhandle(FILE_EXCHANGE_PROTOCOL)
})();
}
})
return (
<chatContext.Provider value={{ messageHistory, setMessageHistory }}>
<chatContext.Provider value={{ messageHistory, setMessageHistory, files, setFiles }}>
{children}
</chatContext.Provider>
);

View File

@@ -11,7 +11,8 @@ import { startLibp2p } from '../lib/libp2p'
import { ChatProvider } from './chat-ctx'
import { PeerProvider } from './peer-ctx'
import { ListenAddressesProvider } from './listen-addresses-ctx'
import { PubSub } from '@libp2p/interface'
import { PubSub, } from '@libp2p/interface'
import { Identify } from '@libp2p/identify'
// 👇 The context type will be avilable "anywhere" in the app
interface Libp2pContextInterface {
@@ -39,7 +40,7 @@ export function AppWrapper({ children }: WrapperProps) {
// @ts-ignore
window.libp2p = libp2p
setLibp2p(libp2p as Libp2p<{ pubsub: any; dht: any; identify: any }>)
setLibp2p(libp2p as Libp2p<{ pubsub: PubSub; identify: Identify }>)
} catch (e) {
console.error('failed to start libp2p', e)
}

View File

@@ -1,30 +0,0 @@
import React, { createContext, useContext, useState } from 'react';
export interface ChatFile {
id: string
body: Uint8Array
sender: string
}
export interface FileChatContextInterface {
files: Map<string, ChatFile>
setFiles: (files: Map<string, ChatFile>) => void;
}
export const fileContext = createContext<FileChatContextInterface>({
files: new Map<string, ChatFile>(),
setFiles: () => { }
})
export const useFileChatContext = () => {
return useContext(fileContext);
};
export const FileProvider = ({ children }: any) => {
const [files, setFiles] = useState<Map<string, ChatFile>>(new Map<string, ChatFile>());
return (
<fileContext.Provider value={{ files, setFiles }}>
{children}
</fileContext.Provider>
);
};

View File

@@ -4,5 +4,9 @@ export const FILE_EXCHANGE_PROTOCOL = "/universal-connectivity-file/1"
export const CIRCUIT_RELAY_CODE = 290
export const WEBRTC_BOOTSTRAP_NODE = "/ip4/147.28.186.157/udp/9090/webrtc-direct/certhash/uEiBbC9bbdvraVWDvcvCEdJAWDymmUqiJQ964FuyEq0hELw/p2p/12D3KooWGahRw3ZnM4gAyd9FK75v4Bp5keFYTvkcAwhpEm28wbV3"
export const WEBTRANSPORT_BOOTSTRAP_NODE = "/ip4/147.28.186.157/udp/9095/quic-v1/webtransport/certhash/uEiCmLPMgXJ1F1wQ-OgOWJEVa_SYB_jLSf5IkQ_d3V98GBQ/certhash/uEiB-ti6URtr64LV8HYDMvZzzzrb1iNEIT-vGY0yd6UYk2g/p2p/12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr"
// 👇 The multiaddrs below are ephemeral and not recommended for use as they expire after a couple of weeks. Instead PeerIDs with peer routing is used
// export const WEBRTC_BOOTSTRAP_MULTIADDR = "/ip4/147.28.186.157/udp/9090/webrtc-direct/certhash/uEiBbC9bbdvraVWDvcvCEdJAWDymmUqiJQ964FuyEq0hELw/p2p/12D3KooWGahRw3ZnM4gAyd9FK75v4Bp5keFYTvkcAwhpEm28wbV3"
// export const WEBTRANSPORT_BOOTSTRAP_MULTIADDR = "/ip4/147.28.186.157/udp/9095/quic-v1/webtransport/certhash/uEiCmLPMgXJ1F1wQ-OgOWJEVa_SYB_jLSf5IkQ_d3V98GBQ/certhash/uEiB-ti6URtr64LV8HYDMvZzzzrb1iNEIT-vGY0yd6UYk2g/p2p/12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr"
export const WEBRTC_BOOTSTRAP_PEER_ID = "12D3KooWGahRw3ZnM4gAyd9FK75v4Bp5keFYTvkcAwhpEm28wbV3"
export const WEBTRANSPORT_BOOTSTRAP_PEER_ID = "12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr"

View File

@@ -1,42 +1,46 @@
import { IDBDatastore } from 'datastore-idb'
import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
import { createLibp2p, Libp2p } from 'libp2p'
import { identify } from '@libp2p/identify'
import {peerIdFromString} from '@libp2p/peer-id'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { bootstrap } from '@libp2p/bootstrap'
import { kadDHT } from '@libp2p/kad-dht'
import {
Multiaddr,
} from '@multiformats/multiaddr'
import { Multiaddr } from '@multiformats/multiaddr'
import { sha256 } from 'multiformats/hashes/sha2'
import type { Message, SignedMessage } from '@libp2p/interface'
import { gossipsub } from '@chainsafe/libp2p-gossipsub'
import { webSockets } from '@libp2p/websockets'
import { webTransport } from '@libp2p/webtransport'
import { webRTC, webRTCDirect } from '@libp2p/webrtc'
import { CHAT_FILE_TOPIC, CHAT_TOPIC, WEBRTC_BOOTSTRAP_NODE, WEBTRANSPORT_BOOTSTRAP_NODE } from './constants'
import { CHAT_FILE_TOPIC, CHAT_TOPIC, WEBRTC_BOOTSTRAP_PEER_ID, WEBTRANSPORT_BOOTSTRAP_PEER_ID } from './constants'
import * as filters from "@libp2p/websockets/filters"
import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
export async function startLibp2p() {
// enable verbose logging in browser console to view debug logs
// localStorage.debug = 'libp2p*,-*:trace'
localStorage.debug = 'libp2p*,-*:trace'
// application-specific data lives in the datastore
const datastore = new IDBDatastore('universal-connectivity')
await datastore.open()
const libp2p = await createLibp2p({
datastore,
addresses: {
listen: [
// 👇 Listen for webRTC connection
'/webrtc'
]
},
transports: [
webTransport(),
webSockets({
filter: filters.all,
}),
webSockets(),
webRTC({
rtcConfiguration: {
iceServers: [{
// STUN servers help the browser discover its own public IPs
urls: [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478'
@@ -45,36 +49,45 @@ export async function startLibp2p() {
}
}),
webRTCDirect(),
// 👇 Required to create circuit relay reservations in order to hole punch browser-to-browser WebRTC connections
circuitRelayTransport({
// When set to >0, this will look up the magic CID in order to discover circuit relay peers it can create a reservation with
discoverRelays: 1,
})
],
connectionManager: {
maxConnections: 10,
minConnections: 5
minConnections: 3
},
connectionEncryption: [noise()],
streamMuxers: [yamux()],
connectionGater: {
denyDialMultiaddr: async () => false,
},
peerDiscovery: [
bootstrap({
list: [
WEBRTC_BOOTSTRAP_NODE,
WEBTRANSPORT_BOOTSTRAP_NODE,
],
}),
],
// The app-specific go and rust peers use WebTransport and WebRTC-direct which have ephemeral multiadrrs that change.
// Thus, we dial them using only their peer id below, with delegated routing to discovery their multiaddrs
// peerDiscovery: [
// bootstrap({
// list: [
// '12D3KooWFhXabKDwALpzqMbto94sB7rvmZ6M28hs9Y9xSopDKwQr'
// WEBRTC_BOOTSTRAP_NODE,
// WEBTRANSPORT_BOOTSTRAP_NODE,
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN',
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
// '/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
// ],
// }),
// ],
services: {
pubsub: gossipsub({
allowPublishToZeroTopicPeers: true,
msgIdFn: msgIdFnStrictNoSign,
ignoreDuplicatePublishError: true,
}),
dht: kadDHT({
clientMode: true,
}),
// Delegated routing helps us discover the ephemeral multiaddrs of the dedicated go and rust bootstrap peers
// This relies on the public delegated routing endpoint https://docs.ipfs.tech/concepts/public-utilities/#delegated-routing
delegatedRouting: () => createDelegatedRoutingV1HttpApiClient('https://delegated-ipfs.dev'),
identify: identify()
},
})
@@ -82,6 +95,13 @@ export async function startLibp2p() {
libp2p.services.pubsub.subscribe(CHAT_TOPIC)
libp2p.services.pubsub.subscribe(CHAT_FILE_TOPIC)
// Try connecting to bootstrap ppers
Promise.all([
libp2p.dial(peerIdFromString(WEBRTC_BOOTSTRAP_PEER_ID)),
libp2p.dial(peerIdFromString(WEBTRANSPORT_BOOTSTRAP_PEER_ID))
])
.catch(e => {console.log('woot', e)})
libp2p.addEventListener('self:peer:update', ({ detail: { peer } }) => {
const multiaddrs = peer.addresses.map(({ multiaddr }) => multiaddr)

View File

@@ -106,8 +106,6 @@ export default function Home() {
[libp2p, maddr],
)
// handleConnectToMultiaddr
const handleMultiaddrChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setMultiaddr(e.target.value)