@gethashd/bytecave-browser 1.0.1
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.
- package/AGENTS.md +176 -0
- package/README.md +699 -0
- package/dist/__tests__/p2p-protocols.test.d.ts +10 -0
- package/dist/chunk-EEZWRIUI.js +160 -0
- package/dist/chunk-OJEETLZQ.js +7087 -0
- package/dist/client.d.ts +107 -0
- package/dist/contracts/ContentRegistry.d.ts +1 -0
- package/dist/discovery.d.ts +28 -0
- package/dist/index.cjs +7291 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/dist/p2p-protocols.d.ts +114 -0
- package/dist/protocol-handler.cjs +185 -0
- package/dist/protocol-handler.d.ts +57 -0
- package/dist/protocol-handler.js +18 -0
- package/dist/provider.d.ts +59 -0
- package/dist/react/components.d.ts +90 -0
- package/dist/react/hooks.d.ts +67 -0
- package/dist/react/index.cjs +6344 -0
- package/dist/react/index.d.ts +8 -0
- package/dist/react/index.js +23 -0
- package/dist/react/useHashdUrl.d.ts +15 -0
- package/dist/types.d.ts +53 -0
- package/package.json +77 -0
- package/src/__tests__/p2p-protocols.test.ts +292 -0
- package/src/client.ts +876 -0
- package/src/contracts/ContentRegistry.ts +6 -0
- package/src/discovery.ts +79 -0
- package/src/index.ts +59 -0
- package/src/p2p-protocols.ts +451 -0
- package/src/protocol-handler.ts +271 -0
- package/src/provider.tsx +275 -0
- package/src/react/components.tsx +177 -0
- package/src/react/hooks.ts +253 -0
- package/src/react/index.ts +9 -0
- package/src/react/useHashdUrl.ts +68 -0
- package/src/types.ts +60 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +17 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const CONTENT_REGISTRY_ABI = [
|
|
2
|
+
"function registerContent(bytes32 cid, address owner, bytes32 appId) external",
|
|
3
|
+
"function registerContent(string cid, string appId) external",
|
|
4
|
+
"function isContentRegistered(bytes32 cid) external view returns (bool)",
|
|
5
|
+
"function getContentRecord(bytes32 cid) external view returns (tuple(address owner, bytes32 appId, uint256 timestamp))"
|
|
6
|
+
] as const;
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-based peer discovery
|
|
3
|
+
*
|
|
4
|
+
* Reads registered nodes from the VaultNodeRegistry smart contract
|
|
5
|
+
* to bootstrap P2P connections without any central server.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ethers } from 'ethers';
|
|
9
|
+
import type { NodeRegistryEntry } from './types.js';
|
|
10
|
+
|
|
11
|
+
const VAULT_REGISTRY_ABI = [
|
|
12
|
+
'function getActiveNodes() external view returns (bytes32[] memory)',
|
|
13
|
+
'function getNode(bytes32 nodeId) external view returns (tuple(address owner, bytes publicKey, string url, bytes32 metadataHash, uint256 registeredAt, bool active))',
|
|
14
|
+
'function getNodeCount() external view returns (uint256 total, uint256 active)'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export class ContractDiscovery {
|
|
18
|
+
private provider: ethers.Provider;
|
|
19
|
+
private contract: ethers.Contract;
|
|
20
|
+
|
|
21
|
+
constructor(vaultNodeRegistryAddress: string, rpcUrl: string) {
|
|
22
|
+
this.provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
23
|
+
this.contract = new ethers.Contract(vaultNodeRegistryAddress, VAULT_REGISTRY_ABI, this.provider);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get all active nodes from the registry contract
|
|
28
|
+
*/
|
|
29
|
+
async getActiveNodes(): Promise<NodeRegistryEntry[]> {
|
|
30
|
+
try {
|
|
31
|
+
const nodeIds: string[] = await this.contract.getActiveNodes();
|
|
32
|
+
const nodes: NodeRegistryEntry[] = [];
|
|
33
|
+
|
|
34
|
+
for (const nodeId of nodeIds) {
|
|
35
|
+
try {
|
|
36
|
+
const node = await this.contract.getNode(nodeId);
|
|
37
|
+
nodes.push({
|
|
38
|
+
nodeId,
|
|
39
|
+
owner: node.owner,
|
|
40
|
+
publicKey: ethers.hexlify(node.publicKey),
|
|
41
|
+
url: node.url,
|
|
42
|
+
active: node.active
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn(`Failed to fetch node ${nodeId}:`, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return nodes;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Failed to fetch active nodes:', error);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get node count from contract
|
|
58
|
+
*/
|
|
59
|
+
async getNodeCount(): Promise<{ total: number; active: number }> {
|
|
60
|
+
try {
|
|
61
|
+
const [total, active] = await this.contract.getNodeCount();
|
|
62
|
+
return { total: Number(total), active: Number(active) };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to get node count:', error);
|
|
65
|
+
return { total: 0, active: 0 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Derive peerId from publicKey (same algorithm as libp2p)
|
|
71
|
+
* Note: This is a simplified version - actual peerId derivation is more complex
|
|
72
|
+
*/
|
|
73
|
+
static publicKeyToPeerId(publicKey: string): string {
|
|
74
|
+
// In practice, peerId is derived from the libp2p identity key
|
|
75
|
+
// For now, we'll use the publicKey hash as a placeholder
|
|
76
|
+
// The actual peerId will come from the node's P2P announcements
|
|
77
|
+
return ethers.keccak256(publicKey).slice(0, 54);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ByteCave Browser Client
|
|
3
|
+
*
|
|
4
|
+
* WebRTC P2P client for connecting browsers directly to ByteCave storage nodes.
|
|
5
|
+
* No central gateway required - fully decentralized.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Simple test export
|
|
9
|
+
export const TEST_EXPORT = "ByteCave Browser Package v1.0.0";
|
|
10
|
+
|
|
11
|
+
export { ByteCaveClient } from './client.js';
|
|
12
|
+
export { ContractDiscovery } from './discovery.js';
|
|
13
|
+
export { p2pProtocolClient } from './p2p-protocols.js';
|
|
14
|
+
|
|
15
|
+
// Provider exports
|
|
16
|
+
export { ByteCaveProvider, useByteCaveContext } from './provider.js';
|
|
17
|
+
|
|
18
|
+
// Type exports
|
|
19
|
+
export type {
|
|
20
|
+
P2PHealthResponse,
|
|
21
|
+
P2PInfoResponse
|
|
22
|
+
} from './p2p-protocols.js';
|
|
23
|
+
export type {
|
|
24
|
+
ByteCaveConfig,
|
|
25
|
+
PeerInfo,
|
|
26
|
+
StoreResult,
|
|
27
|
+
RetrieveResult,
|
|
28
|
+
ConnectionState
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
// Protocol handler exports
|
|
32
|
+
export {
|
|
33
|
+
parseHashdUrl,
|
|
34
|
+
createHashdUrl,
|
|
35
|
+
fetchHashdContent,
|
|
36
|
+
prefetchHashdContent,
|
|
37
|
+
clearHashdCache,
|
|
38
|
+
getHashdCacheStats,
|
|
39
|
+
revokeHashdUrl
|
|
40
|
+
} from './protocol-handler.js';
|
|
41
|
+
export type {
|
|
42
|
+
HashdUrl,
|
|
43
|
+
FetchOptions,
|
|
44
|
+
FetchResult
|
|
45
|
+
} from './protocol-handler.js';
|
|
46
|
+
|
|
47
|
+
// React hooks exports
|
|
48
|
+
export {
|
|
49
|
+
useHashdContent,
|
|
50
|
+
useHashdImage,
|
|
51
|
+
useHashdMedia,
|
|
52
|
+
useHashdBatch
|
|
53
|
+
} from './react/hooks.js';
|
|
54
|
+
export {
|
|
55
|
+
HashdImage,
|
|
56
|
+
HashdVideo,
|
|
57
|
+
HashdAudio
|
|
58
|
+
} from './react/components.js';
|
|
59
|
+
export { useHashdUrl } from './react/useHashdUrl.js';
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ByteCave Browser - P2P Protocol Client
|
|
3
|
+
*
|
|
4
|
+
* Implements libp2p stream protocols for pure P2P communication from browser:
|
|
5
|
+
* - /bytecave/blob/1.0.0 - Blob storage and retrieval
|
|
6
|
+
* - /bytecave/health/1.0.0 - Health status
|
|
7
|
+
* - /bytecave/info/1.0.0 - Node info (for registration)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Libp2p } from 'libp2p';
|
|
11
|
+
import type { Stream } from '@libp2p/interface';
|
|
12
|
+
import { fromString, toString } from 'uint8arrays';
|
|
13
|
+
import { peerIdFromString } from '@libp2p/peer-id';
|
|
14
|
+
|
|
15
|
+
// Protocol identifiers - must match bytecave-core
|
|
16
|
+
export const PROTOCOL_BLOB = '/bytecave/blob/1.0.0';
|
|
17
|
+
export const PROTOCOL_HEALTH = '/bytecave/health/1.0.0';
|
|
18
|
+
export const PROTOCOL_INFO = '/bytecave/info/1.0.0';
|
|
19
|
+
export const PROTOCOL_PEER_DIRECTORY = '/bytecave/relay/peers/1.0.0';
|
|
20
|
+
export const PROTOCOL_HAVE_LIST = '/bytecave/have-list/1.0.0';
|
|
21
|
+
|
|
22
|
+
// Response types
|
|
23
|
+
export interface BlobResponse {
|
|
24
|
+
success: boolean;
|
|
25
|
+
ciphertext?: string; // base64 encoded
|
|
26
|
+
mimeType?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface P2PHealthResponse {
|
|
31
|
+
peerId: string;
|
|
32
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
33
|
+
blobCount: number;
|
|
34
|
+
storageUsed: number;
|
|
35
|
+
storageMax: number;
|
|
36
|
+
uptime: number;
|
|
37
|
+
version: string;
|
|
38
|
+
multiaddrs: string[];
|
|
39
|
+
nodeId?: string;
|
|
40
|
+
publicKey?: string;
|
|
41
|
+
ownerAddress?: string;
|
|
42
|
+
contentTypes?: string[] | 'all';
|
|
43
|
+
metrics?: {
|
|
44
|
+
requestsLastHour: number;
|
|
45
|
+
avgResponseTime: number;
|
|
46
|
+
successRate: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface P2PInfoResponse {
|
|
51
|
+
peerId: string;
|
|
52
|
+
publicKey: string;
|
|
53
|
+
ownerAddress?: string;
|
|
54
|
+
version: string;
|
|
55
|
+
contentTypes: string[] | 'all';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PeerDirectoryResponse {
|
|
59
|
+
peers: Array<{
|
|
60
|
+
peerId: string;
|
|
61
|
+
multiaddrs: string[];
|
|
62
|
+
lastSeen: number;
|
|
63
|
+
}>;
|
|
64
|
+
timestamp: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface HaveListResponse {
|
|
68
|
+
cids: string[];
|
|
69
|
+
total: number;
|
|
70
|
+
hasMore: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface StoreRequest {
|
|
74
|
+
cid: string;
|
|
75
|
+
mimeType: string;
|
|
76
|
+
ciphertext: string; // base64 encoded
|
|
77
|
+
appId?: string;
|
|
78
|
+
shouldVerifyOnChain?: boolean;
|
|
79
|
+
sender?: string;
|
|
80
|
+
timestamp?: number;
|
|
81
|
+
metadata?: Record<string, any>;
|
|
82
|
+
authorization?: any;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface StoreResponse {
|
|
86
|
+
success: boolean;
|
|
87
|
+
cid?: string;
|
|
88
|
+
error?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* P2P Protocol client for browser-to-node communication
|
|
93
|
+
*/
|
|
94
|
+
export class P2PProtocolClient {
|
|
95
|
+
private node: Libp2p | null = null;
|
|
96
|
+
|
|
97
|
+
setNode(node: Libp2p): void {
|
|
98
|
+
this.node = node;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Store a blob on a peer via P2P stream
|
|
103
|
+
*/
|
|
104
|
+
async storeToPeer(
|
|
105
|
+
peerId: string,
|
|
106
|
+
ciphertext: Uint8Array,
|
|
107
|
+
mimeType: string,
|
|
108
|
+
authorization?: any,
|
|
109
|
+
shouldVerifyOnChain?: boolean
|
|
110
|
+
): Promise<StoreResponse> {
|
|
111
|
+
if (!this.node) {
|
|
112
|
+
return { success: false, error: 'P2P node not initialized' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Convert string peerId to PeerId object
|
|
117
|
+
const peerIdObj = peerIdFromString(peerId);
|
|
118
|
+
|
|
119
|
+
// Calculate timeout based on file size (30s base + 10s per MB)
|
|
120
|
+
const fileSizeMB = ciphertext.length / (1024 * 1024);
|
|
121
|
+
const timeoutMs = 30000 + (fileSizeMB * 10000);
|
|
122
|
+
|
|
123
|
+
console.log(`[ByteCave P2P] Store timeout: ${Math.round(timeoutMs / 1000)}s for ${fileSizeMB.toFixed(2)}MB`);
|
|
124
|
+
|
|
125
|
+
// Wrap the entire operation in a timeout
|
|
126
|
+
const storePromise = (async () => {
|
|
127
|
+
console.log('[ByteCave P2P] Step 1: Dialing store protocol...');
|
|
128
|
+
// Use the store protocol for browser-to-node storage (with authorization)
|
|
129
|
+
const stream = await this.node!.dialProtocol(peerIdObj, '/bytecave/store/1.0.0');
|
|
130
|
+
console.log('[ByteCave P2P] Step 2: Stream established');
|
|
131
|
+
|
|
132
|
+
// Generate CID using SHA-256 (matches bytecave-core format: 64-char hex)
|
|
133
|
+
const dataCopy = new Uint8Array(ciphertext);
|
|
134
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', dataCopy);
|
|
135
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
136
|
+
const cid = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
137
|
+
console.log('[ByteCave P2P] Step 3: CID generated:', cid.slice(0, 16) + '...');
|
|
138
|
+
|
|
139
|
+
const request: StoreRequest = {
|
|
140
|
+
cid,
|
|
141
|
+
mimeType,
|
|
142
|
+
ciphertext: this.uint8ArrayToBase64(ciphertext),
|
|
143
|
+
appId: authorization?.appId || 'hashd',
|
|
144
|
+
shouldVerifyOnChain: shouldVerifyOnChain ?? false,
|
|
145
|
+
sender: authorization?.sender,
|
|
146
|
+
timestamp: authorization?.timestamp || Date.now(),
|
|
147
|
+
authorization
|
|
148
|
+
};
|
|
149
|
+
console.log('[ByteCave P2P] Step 4: Request prepared, size:', JSON.stringify(request).length, 'bytes');
|
|
150
|
+
|
|
151
|
+
console.log('[ByteCave P2P] Step 5: Writing message to stream...');
|
|
152
|
+
await this.writeMessage(stream, request);
|
|
153
|
+
console.log('[ByteCave P2P] Step 6: Message written, waiting for response...');
|
|
154
|
+
const response = await this.readMessage<StoreResponse>(stream);
|
|
155
|
+
console.log('[ByteCave P2P] Step 7: Response received:', response);
|
|
156
|
+
|
|
157
|
+
await stream.close();
|
|
158
|
+
|
|
159
|
+
if (response?.success) {
|
|
160
|
+
return { success: true, cid };
|
|
161
|
+
} else {
|
|
162
|
+
return { success: false, error: response?.error || 'Store failed' };
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
|
|
166
|
+
// Race between store operation and timeout
|
|
167
|
+
const timeoutPromise = new Promise<StoreResponse>((_, reject) => {
|
|
168
|
+
setTimeout(() => reject(new Error(`Store timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return await Promise.race([storePromise, timeoutPromise]);
|
|
172
|
+
|
|
173
|
+
} catch (error: any) {
|
|
174
|
+
console.warn('[ByteCave P2P] Failed to store to peer:', peerId, error.message);
|
|
175
|
+
return { success: false, error: error.message };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Retrieve a blob from a peer via P2P stream
|
|
181
|
+
*/
|
|
182
|
+
async retrieveFromPeer(peerId: string, cid: string): Promise<{ data: Uint8Array; mimeType: string } | null> {
|
|
183
|
+
if (!this.node) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const peerIdObj = peerIdFromString(peerId);
|
|
189
|
+
const stream = await this.node.dialProtocol(peerIdObj, PROTOCOL_BLOB);
|
|
190
|
+
|
|
191
|
+
const request = { cid };
|
|
192
|
+
await this.writeMessage(stream, request);
|
|
193
|
+
|
|
194
|
+
const response = await this.readMessage<BlobResponse>(stream);
|
|
195
|
+
|
|
196
|
+
// Close stream (may already be closed by server)
|
|
197
|
+
try {
|
|
198
|
+
await stream.close();
|
|
199
|
+
} catch {
|
|
200
|
+
// Stream may already be closed by server
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (response?.success && response.ciphertext) {
|
|
204
|
+
const data = this.base64ToUint8Array(response.ciphertext);
|
|
205
|
+
return {
|
|
206
|
+
data,
|
|
207
|
+
mimeType: response.mimeType || 'application/octet-stream'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
|
|
213
|
+
} catch (error: any) {
|
|
214
|
+
// Suppress expected errors
|
|
215
|
+
if (error.name !== 'StreamResetError' && error.code !== 'ERR_STREAM_RESET') {
|
|
216
|
+
console.error('[ByteCave P2P] Retrieve error:', error);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get health info from a peer via P2P stream
|
|
224
|
+
*/
|
|
225
|
+
async getHealthFromPeer(peerId: string): Promise<P2PHealthResponse | null> {
|
|
226
|
+
if (!this.node) {
|
|
227
|
+
console.warn('[ByteCave P2P] No node available for health request');
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
|
|
233
|
+
// Convert string peerId to PeerId object
|
|
234
|
+
const peerIdObj = peerIdFromString(peerId);
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
const stream = await this.node.dialProtocol(peerIdObj, PROTOCOL_HEALTH);
|
|
238
|
+
|
|
239
|
+
await this.writeMessage(stream, {});
|
|
240
|
+
|
|
241
|
+
const response = await this.readMessage<P2PHealthResponse>(stream);
|
|
242
|
+
|
|
243
|
+
await stream.close();
|
|
244
|
+
return response;
|
|
245
|
+
|
|
246
|
+
} catch (error: any) {
|
|
247
|
+
// console.error('[ByteCave P2P] Failed to get health from peer:', peerId.slice(0, 12), error);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Query relay for peer directory
|
|
254
|
+
*/
|
|
255
|
+
async getPeerDirectoryFromRelay(relayPeerId: string): Promise<PeerDirectoryResponse | null> {
|
|
256
|
+
if (!this.node) {
|
|
257
|
+
console.warn('[ByteCave P2P] No node available for peer directory request');
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
console.log('[ByteCave P2P] Querying relay for peer directory:', relayPeerId.slice(0, 12) + '...');
|
|
263
|
+
|
|
264
|
+
const peerIdObj = peerIdFromString(relayPeerId);
|
|
265
|
+
const stream = await this.node.dialProtocol(peerIdObj, PROTOCOL_PEER_DIRECTORY);
|
|
266
|
+
|
|
267
|
+
// Relay sends response immediately, just read it
|
|
268
|
+
const response = await this.readMessage<PeerDirectoryResponse>(stream);
|
|
269
|
+
|
|
270
|
+
await stream.close();
|
|
271
|
+
|
|
272
|
+
if (response) {
|
|
273
|
+
console.log('[ByteCave P2P] Received peer directory:', response.peers.length, 'peers');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return response;
|
|
277
|
+
|
|
278
|
+
} catch (error: any) {
|
|
279
|
+
console.warn('[ByteCave P2P] Failed to get peer directory from relay:', relayPeerId.slice(0, 12), error.message);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get node info from a peer via P2P stream (for registration)
|
|
286
|
+
*/
|
|
287
|
+
async getInfoFromPeer(peerId: string): Promise<P2PInfoResponse | null> {
|
|
288
|
+
if (!this.node) return null;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Convert string peerId to PeerId object
|
|
292
|
+
const peerIdObj = peerIdFromString(peerId);
|
|
293
|
+
|
|
294
|
+
const stream = await this.node.dialProtocol(peerIdObj, PROTOCOL_INFO);
|
|
295
|
+
|
|
296
|
+
await this.writeMessage(stream, {});
|
|
297
|
+
const response = await this.readMessage<P2PInfoResponse>(stream);
|
|
298
|
+
|
|
299
|
+
await stream.close();
|
|
300
|
+
return response;
|
|
301
|
+
|
|
302
|
+
} catch (error: any) {
|
|
303
|
+
console.warn('[ByteCave P2P] Failed to get info from peer:', peerId, error.message);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Check if a peer has a specific CID
|
|
310
|
+
*/
|
|
311
|
+
async peerHasCid(peerId: string, cid: string): Promise<boolean> {
|
|
312
|
+
if (!this.node) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const peerIdObj = peerIdFromString(peerId);
|
|
318
|
+
const stream = await this.node.dialProtocol(peerIdObj, PROTOCOL_HAVE_LIST);
|
|
319
|
+
|
|
320
|
+
const request = { cids: [cid] };
|
|
321
|
+
await this.writeMessage(stream, request);
|
|
322
|
+
|
|
323
|
+
const response = await this.readMessage<HaveListResponse>(stream);
|
|
324
|
+
|
|
325
|
+
await stream.close();
|
|
326
|
+
|
|
327
|
+
return response?.cids?.includes(cid) || false;
|
|
328
|
+
} catch (error: any) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Stream utilities - custom length-prefixed encoding
|
|
334
|
+
private async readMessage<T>(stream: any): Promise<T | null> {
|
|
335
|
+
try {
|
|
336
|
+
let firstChunk = true;
|
|
337
|
+
let length = 0;
|
|
338
|
+
let messageBytes: Uint8Array | null = null;
|
|
339
|
+
let messageBytesRead = 0;
|
|
340
|
+
|
|
341
|
+
for await (const chunk of stream) {
|
|
342
|
+
const array = chunk instanceof Uint8Array ? chunk : chunk.subarray();
|
|
343
|
+
|
|
344
|
+
if (firstChunk && array.length >= 4) {
|
|
345
|
+
// Read length prefix (4 bytes, big-endian)
|
|
346
|
+
length = new DataView(array.buffer, array.byteOffset, array.byteLength).getUint32(0, false);
|
|
347
|
+
|
|
348
|
+
// Allocate buffer for message
|
|
349
|
+
messageBytes = new Uint8Array(length);
|
|
350
|
+
|
|
351
|
+
// Copy remaining bytes from first chunk (after length prefix)
|
|
352
|
+
if (array.length > 4) {
|
|
353
|
+
const copyLength = Math.min(array.length - 4, length);
|
|
354
|
+
messageBytes.set(array.subarray(4, 4 + copyLength), 0);
|
|
355
|
+
messageBytesRead = copyLength;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
firstChunk = false;
|
|
359
|
+
|
|
360
|
+
// If we got the complete message in the first chunk, break immediately
|
|
361
|
+
if (messageBytesRead >= length) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
} else if (!firstChunk && messageBytes) {
|
|
365
|
+
// Continue reading subsequent chunks
|
|
366
|
+
const copyLength = Math.min(array.length, length - messageBytesRead);
|
|
367
|
+
messageBytes.set(array.subarray(0, copyLength), messageBytesRead);
|
|
368
|
+
messageBytesRead += copyLength;
|
|
369
|
+
|
|
370
|
+
// Break as soon as we have the complete message
|
|
371
|
+
if (messageBytesRead >= length) {
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (messageBytes && messageBytesRead === length) {
|
|
378
|
+
const data = new TextDecoder().decode(messageBytes);
|
|
379
|
+
return JSON.parse(data) as T;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return null;
|
|
383
|
+
} catch (error: any) {
|
|
384
|
+
// Only log if it's not a stream reset error (which is expected when we close the stream)
|
|
385
|
+
if (error.name !== 'StreamResetError' && error.code !== 'ERR_STREAM_RESET') {
|
|
386
|
+
console.error('[ByteCave P2P] Failed to read message:', error);
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async writeMessage(stream: any, message: any): Promise<void> {
|
|
393
|
+
const data = new TextEncoder().encode(JSON.stringify(message));
|
|
394
|
+
|
|
395
|
+
// Create length prefix (4 bytes, big-endian)
|
|
396
|
+
const lengthPrefix = new Uint8Array(4);
|
|
397
|
+
new DataView(lengthPrefix.buffer).setUint32(0, data.length, false);
|
|
398
|
+
|
|
399
|
+
// Combine length prefix and data
|
|
400
|
+
const combined = new Uint8Array(lengthPrefix.length + data.length);
|
|
401
|
+
combined.set(lengthPrefix, 0);
|
|
402
|
+
combined.set(data, lengthPrefix.length);
|
|
403
|
+
|
|
404
|
+
// For large messages (>64KB), send in chunks to avoid buffer overflow
|
|
405
|
+
const CHUNK_SIZE = 65536; // 64KB chunks
|
|
406
|
+
if (combined.length > CHUNK_SIZE) {
|
|
407
|
+
console.log(`[ByteCave P2P] Sending large message in chunks: ${combined.length} bytes`);
|
|
408
|
+
|
|
409
|
+
for (let offset = 0; offset < combined.length; offset += CHUNK_SIZE) {
|
|
410
|
+
const chunk = combined.subarray(offset, Math.min(offset + CHUNK_SIZE, combined.length));
|
|
411
|
+
const needsDrain = !stream.send(chunk);
|
|
412
|
+
|
|
413
|
+
if (needsDrain) {
|
|
414
|
+
await stream.onDrain();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Small delay between chunks to prevent overwhelming the stream
|
|
418
|
+
if (offset + CHUNK_SIZE < combined.length) {
|
|
419
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
// Small message, send all at once
|
|
424
|
+
const needsDrain = !stream.send(combined);
|
|
425
|
+
|
|
426
|
+
if (needsDrain) {
|
|
427
|
+
await stream.onDrain();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Base64 utilities for browser
|
|
433
|
+
private uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
434
|
+
let binary = '';
|
|
435
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
436
|
+
binary += String.fromCharCode(bytes[i]);
|
|
437
|
+
}
|
|
438
|
+
return btoa(binary);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private base64ToUint8Array(base64: string): Uint8Array {
|
|
442
|
+
const binary = atob(base64);
|
|
443
|
+
const bytes = new Uint8Array(binary.length);
|
|
444
|
+
for (let i = 0; i < binary.length; i++) {
|
|
445
|
+
bytes[i] = binary.charCodeAt(i);
|
|
446
|
+
}
|
|
447
|
+
return bytes;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export const p2pProtocolClient = new P2PProtocolClient();
|