@peerbit/react 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ import React, { useContext } from "react";
2
+ import { multiaddr, Multiaddr } from "@multiformats/multiaddr";
3
+ import { Peerbit } from "peerbit";
4
+ import { webSockets } from "@libp2p/websockets";
5
+ import { mplex } from "@libp2p/mplex";
6
+ import { getFreeKeypair, getTabId, inIframe } from "./utils.js";
7
+ import { resolveBootstrapAddresses } from "@peerbit/network-utils";
8
+ import { noise } from "@dao-xyz/libp2p-noise";
9
+ import { v4 as uuid } from "uuid";
10
+ import { Ed25519Keypair } from "@peerbit/crypto";
11
+ import { FastMutex } from "./lockstorage.js";
12
+ import { serialize, deserialize } from "@dao-xyz/borsh";
13
+ import { waitFor } from "@peerbit/time";
14
+ import sodium from "libsodium-wrappers";
15
+ import * as filters from "@libp2p/websockets/filters";
16
+ import { useMount } from "./useMount.js";
17
+ export type ConnectionStatus =
18
+ | "disconnected"
19
+ | "connected"
20
+ | "connecting"
21
+ | "failed";
22
+ interface IPeerContext {
23
+ peer: Peerbit | undefined;
24
+ promise: Promise<void> | undefined;
25
+ loading: boolean;
26
+ status: ConnectionStatus;
27
+ }
28
+
29
+ if (!window.name) {
30
+ window.name = uuid();
31
+ }
32
+
33
+ interface KeypairMessage {
34
+ type: "keypair";
35
+ bytes: Uint8Array;
36
+ }
37
+ export const subscribeToKeypairChange = (
38
+ onChange: (keypair: Ed25519Keypair) => any
39
+ ) => {
40
+ window.onmessage = (c: MessageEvent) => {
41
+ if ((c.data as KeypairMessage).type == "keypair") {
42
+ onChange(
43
+ deserialize((c.data as KeypairMessage).bytes, Ed25519Keypair)
44
+ );
45
+ }
46
+ };
47
+ };
48
+
49
+ export const submitKeypairChange = (
50
+ element: HTMLIFrameElement,
51
+ keypair: Ed25519Keypair,
52
+ origin: string
53
+ ) => {
54
+ element.contentWindow!.postMessage(
55
+ { type: "keypair", bytes: serialize(keypair) } as KeypairMessage,
56
+ origin
57
+ );
58
+ };
59
+
60
+ let keypairMessages: Ed25519Keypair[] = [];
61
+ subscribeToKeypairChange((keypair) => {
62
+ console.log("got keypair!", keypair);
63
+ keypairMessages.push(keypair);
64
+ });
65
+
66
+ export const PeerContext = React.createContext<IPeerContext>({} as any);
67
+ export const usePeer = () => useContext(PeerContext);
68
+ export const PeerProvider = ({
69
+ network,
70
+ bootstrap,
71
+ children,
72
+ inMemory,
73
+ keypair,
74
+ waitForConnnected,
75
+ waitForKeypairInIFrame,
76
+ }: {
77
+ network: "local" | "remote";
78
+ inMemory?: boolean;
79
+ waitForConnnected?: boolean;
80
+ keypair?: Ed25519Keypair;
81
+ waitForKeypairInIFrame?: boolean;
82
+ bootstrap?: (Multiaddr | string)[];
83
+ children: JSX.Element;
84
+ }) => {
85
+ const [peer, setPeer] = React.useState<Peerbit | undefined>(undefined);
86
+ const [promise, setPromise] = React.useState<Promise<void> | undefined>(
87
+ undefined
88
+ );
89
+
90
+ const [loading, setLoading] = React.useState<boolean>(false);
91
+ const [connectionState, setConnectionState] =
92
+ React.useState<ConnectionStatus>("disconnected");
93
+ const memo = React.useMemo<IPeerContext>(
94
+ () => ({
95
+ peer,
96
+ promise,
97
+ loading,
98
+ connectionState,
99
+ status: connectionState,
100
+ }),
101
+ [
102
+ loading,
103
+ !!promise,
104
+ connectionState,
105
+ peer?.identity?.publicKey.toString(),
106
+ ]
107
+ );
108
+
109
+ useMount(() => {
110
+ setLoading(true);
111
+ const fn = async (
112
+ keypair: Ed25519Keypair = keypairMessages[
113
+ keypairMessages.length - 1
114
+ ]
115
+ ) => {
116
+ await sodium.ready;
117
+
118
+ if (!keypair && waitForKeypairInIFrame && inIframe()) {
119
+ await waitFor(
120
+ () =>
121
+ (keypair = keypairMessages[keypairMessages.length - 1])
122
+ );
123
+ }
124
+
125
+ if (
126
+ keypair &&
127
+ keypairMessages[keypairMessages.length - 1] &&
128
+ keypairMessages[keypairMessages.length - 1].equals(keypair)
129
+ ) {
130
+ console.log(
131
+ "Creating client from identity sent from parent window: " +
132
+ keypair.publicKey.hashcode()
133
+ );
134
+ } else {
135
+ if (!keypair) {
136
+ console.log("Generating new keypair for client");
137
+ } else {
138
+ console.log(
139
+ "Keypair missmatch with latest keypair message",
140
+ keypairMessages.map((x) => x.publicKey.hashcode()),
141
+ keypair.publicKey.hashcode()
142
+ );
143
+ }
144
+ }
145
+
146
+ if (peer) {
147
+ await peer.stop();
148
+ setPeer(undefined);
149
+ }
150
+
151
+ const nodeId =
152
+ keypair ||
153
+ (
154
+ await getFreeKeypair(
155
+ "",
156
+ new FastMutex({ clientId: getTabId(), timeout: 1000 }),
157
+ undefined,
158
+ true // reuse keypairs from same tab, (force release)
159
+ )
160
+ ).key;
161
+
162
+ // We create a new directrory to make tab to tab communication go smoothly
163
+ const newPeer = await Peerbit.create({
164
+ libp2p: {
165
+ addresses: {
166
+ listen: [
167
+ /* '/webrtc' */
168
+ ],
169
+ },
170
+ connectionEncryption: [noise()],
171
+ peerId: await nodeId.toPeerId(), //, having the same peer accross broswers does not work, only one tab will be recognized by other peers
172
+ connectionManager: {
173
+ maxConnections: 100,
174
+ minConnections: 0,
175
+ },
176
+ streamMuxers: [mplex()],
177
+ ...(network === "local"
178
+ ? {
179
+ connectionGater: {
180
+ denyDialMultiaddr: () => {
181
+ // by default we refuse to dial local addresses from the browser since they
182
+ // are usually sent by remote peers broadcasting undialable multiaddrs but
183
+ // here we are explicitly connecting to a local node so do not deny dialing
184
+ // any discovered address
185
+ return false;
186
+ },
187
+ },
188
+ transports: [
189
+ // Add websocket impl so we can connect to "unsafe" ws (production only allows wss)
190
+ webSockets({
191
+ filter: filters.all,
192
+ }),
193
+ /* circuitRelayTransport({ discoverRelays: 1 }),
194
+ webRTC(), */
195
+ ],
196
+ }
197
+ : {
198
+ connectionGater: {
199
+ denyDialMultiaddr: () => {
200
+ // by default we refuse to dial local addresses from the browser since they
201
+ // are usually sent by remote peers broadcasting undialable multiaddrs but
202
+ // here we are explicitly connecting to a local node so do not deny dialing
203
+ // any discovered address
204
+ return false;
205
+ },
206
+ },
207
+ transports: [
208
+ webSockets({ filter: filters.all }),
209
+ /* circuitRelayTransport({ discoverRelays: 1 }),
210
+ webRTC(), */
211
+ ],
212
+ }),
213
+ },
214
+ directory: !inMemory ? "./repo" : undefined,
215
+ limitSigning: true,
216
+ });
217
+
218
+ setConnectionState("connecting");
219
+
220
+ // Resolve bootstrap nodes async (we want to return before this is done)
221
+ const connectFn = async () => {
222
+ try {
223
+ const addresses = await (bootstrap
224
+ ? Promise.resolve(bootstrap)
225
+ : resolveBootstrapAddresses(network));
226
+ if (addresses && addresses?.length > 0) {
227
+ try {
228
+ await Promise.all(
229
+ addresses
230
+ .map((a) =>
231
+ typeof a === "string" ? multiaddr(a) : a
232
+ )
233
+ .map((a) => newPeer.dial(a))
234
+ );
235
+ setConnectionState("connected");
236
+ } catch (error) {
237
+ console.error(
238
+ "Failed to resolve relay node. Please come back later or start the demo locally"
239
+ );
240
+ setConnectionState("failed");
241
+ throw error;
242
+ }
243
+ } else {
244
+ console.error("No addresses to connect to");
245
+ setConnectionState("failed");
246
+ }
247
+ } catch (err: any) {
248
+ console.error(
249
+ "Failed to resolve relay addresses. " + err?.message
250
+ );
251
+ setConnectionState("failed");
252
+ }
253
+ };
254
+
255
+ const promise = connectFn();
256
+
257
+ // Make sure data flow as expected between tabs and windows locally (offline states)
258
+
259
+ if (waitForConnnected) {
260
+ await promise;
261
+ }
262
+
263
+ setPeer(newPeer);
264
+ setLoading(false);
265
+ };
266
+ setPromise(fn(keypair));
267
+ });
268
+
269
+ return <PeerContext.Provider value={memo}>{children}</PeerContext.Provider>;
270
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { serialize, deserialize } from "@dao-xyz/borsh";
2
+ import { Ed25519Keypair, toBase64, fromBase64 } from "@peerbit/crypto";
3
+ import { FastMutex } from "./lockstorage";
4
+ import { v4 as uuid } from "uuid";
5
+ import sodium from "libsodium-wrappers";
6
+
7
+ export const getTabId = () => {
8
+ const idFromStorage = sessionStorage.getItem("TAB_ID");
9
+ if (idFromStorage) {
10
+ return idFromStorage;
11
+ } else {
12
+ const id = uuid(); // generate unique UUID
13
+ sessionStorage.setItem("TAB_ID", id);
14
+ return id;
15
+ }
16
+ };
17
+
18
+ const ID_COUNTER_KEY = "idc/";
19
+
20
+ const getKeyId = (prefix: string, id: number) => prefix + "/" + id;
21
+
22
+ export const releaseKey = (
23
+ path: string,
24
+ lock: FastMutex = new FastMutex({ clientId: getTabId() })
25
+ ) => {
26
+ lock.release(path);
27
+ };
28
+
29
+ export const getFreeKeypair = async (
30
+ id: string = "",
31
+ lock: FastMutex = new FastMutex({ clientId: getTabId() }),
32
+ lockCondition: () => boolean = () => true,
33
+ releaseLockIfSameId?: boolean
34
+ ) => {
35
+ await sodium.ready;
36
+ const idCounterKey = ID_COUNTER_KEY + id;
37
+ await lock.lock(idCounterKey, () => true);
38
+ let idCounter = JSON.parse(localStorage.getItem(idCounterKey) || "0");
39
+ for (let i = 0; i < 10000; i++) {
40
+ const key = getKeyId(id, i);
41
+ let lockedInfo = lock.getLockedInfo(key);
42
+ if (lockedInfo) {
43
+ if (lockedInfo === lock.clientId && releaseLockIfSameId) {
44
+ await lock.release(key); // Release lock
45
+ } else {
46
+ continue;
47
+ }
48
+ }
49
+ console.log("aquire id at", i);
50
+ await lock.lock(key, lockCondition);
51
+
52
+ localStorage.setItem(
53
+ idCounterKey,
54
+ JSON.stringify(Math.max(idCounter, i + 1))
55
+ );
56
+ await lock.release(idCounterKey);
57
+ return {
58
+ path: key,
59
+ key: await getKeypair(key),
60
+ };
61
+ }
62
+ throw new Error("Failed to resolve key");
63
+ };
64
+
65
+ export const getAllKeyPairs = async (id: string = "") => {
66
+ const idCounterKey = ID_COUNTER_KEY + id;
67
+ const counter = JSON.parse(localStorage.getItem(idCounterKey) || "0");
68
+ let ret: Ed25519Keypair[] = [];
69
+ for (let i = 0; i < counter; i++) {
70
+ const key = getKeyId(id, i);
71
+ const kp = loadKeypair(key);
72
+ if (kp) {
73
+ ret.push(kp);
74
+ }
75
+ }
76
+ return ret;
77
+ };
78
+
79
+ let _getKeypair: Promise<any>;
80
+
81
+ export const getKeypair = async (keyName: string): Promise<Ed25519Keypair> => {
82
+ await _getKeypair;
83
+ const fn = async () => {
84
+ let keypair: Ed25519Keypair | undefined = loadKeypair(keyName);
85
+ if (keypair) {
86
+ return keypair;
87
+ }
88
+
89
+ keypair = await Ed25519Keypair.create();
90
+ saveKeypair(keyName, keypair);
91
+ return keypair;
92
+ };
93
+ _getKeypair = fn();
94
+ return _getKeypair;
95
+ };
96
+
97
+ const saveKeypair = (path: string, key: Ed25519Keypair) => {
98
+ const str = toBase64(serialize(key));
99
+ localStorage.setItem("_keys/" + path, str);
100
+ };
101
+
102
+ const loadKeypair = (path: string) => {
103
+ let item = localStorage.getItem("_keys/" + path);
104
+ if (!item) {
105
+ return;
106
+ }
107
+ return deserialize(fromBase64(item), Ed25519Keypair);
108
+ };
109
+
110
+ export const inIframe = () => {
111
+ try {
112
+ return window.self !== window.top;
113
+ } catch (e) {
114
+ return true;
115
+ }
116
+ };