@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.
@@ -0,0 +1,271 @@
1
+ /**
2
+ * HashD Protocol Handler
3
+ *
4
+ * Handles hashd:// URLs for loading content from ByteCave network.
5
+ * Format: hashd://{cid}?type={mimeType}&decrypt={boolean}
6
+ */
7
+
8
+ import type { ByteCaveClient } from './client.js';
9
+
10
+ export interface HashdUrl {
11
+ protocol: 'hashd:';
12
+ cid: string;
13
+ mimeType?: string;
14
+ decrypt?: boolean;
15
+ raw: string;
16
+ }
17
+
18
+ export interface FetchOptions {
19
+ signal?: AbortSignal;
20
+ timeout?: number;
21
+ }
22
+
23
+ export interface FetchResult {
24
+ data: Uint8Array;
25
+ mimeType: string;
26
+ blobUrl: string;
27
+ cached: boolean;
28
+ }
29
+
30
+ /**
31
+ * Parse a hashd:// URL into its components
32
+ */
33
+ export function parseHashdUrl(url: string): HashdUrl {
34
+ if (!url.startsWith('hashd://')) {
35
+ throw new Error(`Invalid hashd:// URL: ${url}`);
36
+ }
37
+
38
+ // Remove protocol
39
+ const withoutProtocol = url.slice(8); // Remove 'hashd://'
40
+
41
+ // Split CID and query params
42
+ const [cid, queryString] = withoutProtocol.split('?');
43
+
44
+ if (!cid || cid.length === 0) {
45
+ throw new Error(`Invalid hashd:// URL: missing CID`);
46
+ }
47
+
48
+ const result: HashdUrl = {
49
+ protocol: 'hashd:',
50
+ cid,
51
+ raw: url
52
+ };
53
+
54
+ // Parse query parameters
55
+ if (queryString) {
56
+ const params = new URLSearchParams(queryString);
57
+
58
+ if (params.has('type')) {
59
+ result.mimeType = params.get('type')!;
60
+ }
61
+
62
+ if (params.has('decrypt')) {
63
+ result.decrypt = params.get('decrypt') === 'true';
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Create a hashd:// URL from a CID and options
72
+ */
73
+ export function createHashdUrl(
74
+ cid: string,
75
+ options?: { mimeType?: string; decrypt?: boolean }
76
+ ): string {
77
+ let url = `hashd://${cid}`;
78
+
79
+ if (options) {
80
+ const params = new URLSearchParams();
81
+
82
+ if (options.mimeType) {
83
+ params.set('type', options.mimeType);
84
+ }
85
+
86
+ if (options.decrypt !== undefined) {
87
+ params.set('decrypt', String(options.decrypt));
88
+ }
89
+
90
+ const queryString = params.toString();
91
+ if (queryString) {
92
+ url += `?${queryString}`;
93
+ }
94
+ }
95
+
96
+ return url;
97
+ }
98
+
99
+ /**
100
+ * Blob URL cache to avoid re-fetching content
101
+ */
102
+ class BlobUrlCache {
103
+ private cache = new Map<string, { blobUrl: string; mimeType: string; timestamp: number }>();
104
+ private readonly maxAge = 60 * 60 * 1000; // 1 hour
105
+
106
+ set(cid: string, blobUrl: string, mimeType: string): void {
107
+ this.cache.set(cid, { blobUrl, mimeType, timestamp: Date.now() });
108
+ }
109
+
110
+ get(cid: string): { blobUrl: string; mimeType: string } | null {
111
+ const entry = this.cache.get(cid);
112
+
113
+ if (!entry) {
114
+ return null;
115
+ }
116
+
117
+ // Check if expired
118
+ if (Date.now() - entry.timestamp > this.maxAge) {
119
+ this.revoke(cid);
120
+ return null;
121
+ }
122
+
123
+ return { blobUrl: entry.blobUrl, mimeType: entry.mimeType };
124
+ }
125
+
126
+ revoke(cid: string): void {
127
+ const entry = this.cache.get(cid);
128
+ if (entry) {
129
+ URL.revokeObjectURL(entry.blobUrl);
130
+ this.cache.delete(cid);
131
+ }
132
+ }
133
+
134
+ clear(): void {
135
+ for (const entry of this.cache.values()) {
136
+ URL.revokeObjectURL(entry.blobUrl);
137
+ }
138
+ this.cache.clear();
139
+ }
140
+
141
+ size(): number {
142
+ return this.cache.size;
143
+ }
144
+ }
145
+
146
+ // Global cache instance
147
+ const blobCache = new BlobUrlCache();
148
+
149
+ /**
150
+ * Detect MIME type from data if not provided
151
+ */
152
+ function detectMimeType(data: Uint8Array): string {
153
+ // Check magic bytes for common formats
154
+ if (data.length < 4) {
155
+ return 'application/octet-stream';
156
+ }
157
+
158
+ // PNG
159
+ if (data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E && data[3] === 0x47) {
160
+ return 'image/png';
161
+ }
162
+
163
+ // JPEG
164
+ if (data[0] === 0xFF && data[1] === 0xD8 && data[2] === 0xFF) {
165
+ return 'image/jpeg';
166
+ }
167
+
168
+ // GIF
169
+ if (data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46) {
170
+ return 'image/gif';
171
+ }
172
+
173
+ // WebP
174
+ if (data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 &&
175
+ data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) {
176
+ return 'image/webp';
177
+ }
178
+
179
+ // MP4
180
+ if (data.length >= 12 &&
181
+ data[4] === 0x66 && data[5] === 0x74 && data[6] === 0x79 && data[7] === 0x70) {
182
+ return 'video/mp4';
183
+ }
184
+
185
+ // Default
186
+ return 'application/octet-stream';
187
+ }
188
+
189
+ /**
190
+ * Fetch content from ByteCave network using hashd:// URL
191
+ */
192
+ export async function fetchHashdContent(
193
+ url: string | HashdUrl,
194
+ client: ByteCaveClient,
195
+ options?: FetchOptions
196
+ ): Promise<FetchResult> {
197
+ // Parse URL if string
198
+ const parsed = typeof url === 'string' ? parseHashdUrl(url) : url;
199
+
200
+ // Check cache first
201
+ const cached = blobCache.get(parsed.cid);
202
+ if (cached) {
203
+ console.log(`[HashD] Cache hit for CID: ${parsed.cid.slice(0, 16)}...`);
204
+ return {
205
+ data: new Uint8Array(), // Don't return data for cached items
206
+ mimeType: cached.mimeType,
207
+ blobUrl: cached.blobUrl,
208
+ cached: true
209
+ };
210
+ }
211
+
212
+ console.log(`[HashD] Fetching CID: ${parsed.cid.slice(0, 16)}...`);
213
+
214
+ // Fetch from ByteCave network
215
+ const result = await client.retrieve(parsed.cid);
216
+
217
+ if (!result.success || !result.data) {
218
+ throw new Error(result.error || 'Failed to retrieve content');
219
+ }
220
+
221
+ // Determine MIME type
222
+ const mimeType = parsed.mimeType || detectMimeType(result.data);
223
+
224
+ // Create blob URL (copy data to avoid SharedArrayBuffer issues)
225
+ const dataCopy = new Uint8Array(result.data);
226
+ const blob = new Blob([dataCopy], { type: mimeType });
227
+ const blobUrl = URL.createObjectURL(blob);
228
+
229
+ // Cache the blob URL
230
+ blobCache.set(parsed.cid, blobUrl, mimeType);
231
+
232
+ console.log(`[HashD] Retrieved and cached CID: ${parsed.cid.slice(0, 16)}... (${mimeType})`);
233
+
234
+ return {
235
+ data: result.data,
236
+ mimeType,
237
+ blobUrl,
238
+ cached: false
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Prefetch content and cache it
244
+ */
245
+ export async function prefetchHashdContent(
246
+ url: string | HashdUrl,
247
+ client: ByteCaveClient
248
+ ): Promise<void> {
249
+ await fetchHashdContent(url, client);
250
+ }
251
+
252
+ /**
253
+ * Clear the blob URL cache
254
+ */
255
+ export function clearHashdCache(): void {
256
+ blobCache.clear();
257
+ }
258
+
259
+ /**
260
+ * Get cache statistics
261
+ */
262
+ export function getHashdCacheStats(): { size: number } {
263
+ return { size: blobCache.size() };
264
+ }
265
+
266
+ /**
267
+ * Revoke a specific blob URL from cache
268
+ */
269
+ export function revokeHashdUrl(cid: string): void {
270
+ blobCache.revoke(cid);
271
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * React Context Provider for ByteCave Client
3
+ *
4
+ * Provides P2P connectivity state and methods to React components
5
+ */
6
+
7
+ import React, { createContext, useContext, ReactNode, useState, useEffect, useRef, useCallback } from 'react';
8
+ import { ByteCaveClient } from './client.js';
9
+ import type { ByteCaveConfig, PeerInfo, ConnectionState, StoreResult, RetrieveResult } from './types.js';
10
+
11
+ interface NodeHealth {
12
+ status: string;
13
+ blobCount: number;
14
+ storageUsed: number;
15
+ uptime: number;
16
+ nodeId?: string;
17
+ publicKey?: string;
18
+ secp256k1PublicKey?: string;
19
+ ownerAddress?: string;
20
+ metrics?: {
21
+ requestsLastHour: number;
22
+ avgResponseTime: number;
23
+ successRate: number;
24
+ };
25
+ integrity?: {
26
+ checked: number;
27
+ passed: number;
28
+ failed: number;
29
+ orphaned: number;
30
+ metadataTampered: number;
31
+ failedCids: string[];
32
+ };
33
+ }
34
+
35
+ interface ByteCaveContextValue {
36
+ connectionState: ConnectionState;
37
+ peers: PeerInfo[];
38
+ isConnected: boolean;
39
+ appId: string;
40
+ connect: () => Promise<void>;
41
+ disconnect: () => Promise<void>;
42
+ store: (data: Uint8Array, mimeType?: string, signer?: any) => Promise<StoreResult>;
43
+ retrieve: (cid: string) => Promise<RetrieveResult>;
44
+ registerContent: (cid: string, appId: string, signer: any) => Promise<{ success: boolean; txHash?: string; error?: string }>;
45
+ getNodeHealth: (peerId: string) => Promise<NodeHealth | null>;
46
+ error: string | null;
47
+ }
48
+
49
+ const ByteCaveContext = createContext<ByteCaveContextValue | null>(null);
50
+
51
+ interface ByteCaveProviderProps {
52
+ children: ReactNode;
53
+ vaultNodeRegistryAddress: string;
54
+ contentRegistryAddress?: string;
55
+ rpcUrl: string;
56
+ appId: string;
57
+ relayPeers?: string[];
58
+ directNodeAddrs?: string[];
59
+ }
60
+
61
+ let globalClient: ByteCaveClient | null = null;
62
+
63
+ export function ByteCaveProvider({
64
+ children,
65
+ vaultNodeRegistryAddress,
66
+ contentRegistryAddress,
67
+ rpcUrl,
68
+ appId,
69
+ relayPeers = [],
70
+ directNodeAddrs = []
71
+ }: ByteCaveProviderProps) {
72
+ const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
73
+ const [peers, setPeers] = useState<PeerInfo[]>([]);
74
+ const [error, setError] = useState<string | null>(null);
75
+ const connectCalledRef = useRef(false);
76
+
77
+ const connect = useCallback(async () => {
78
+ if (!globalClient) {
79
+ setError('ByteCave client not initialized');
80
+ return;
81
+ }
82
+
83
+ try {
84
+ const currentState = globalClient.getConnectionState();
85
+ if (currentState !== 'connected') {
86
+ await globalClient.start();
87
+ }
88
+
89
+ const actualState = globalClient.getConnectionState();
90
+ const peers = await globalClient.getPeers();
91
+
92
+ setConnectionState(actualState);
93
+ setPeers(peers);
94
+ } catch (err: any) {
95
+ setError(err.message);
96
+ setConnectionState('error');
97
+ }
98
+ }, []);
99
+
100
+ useEffect(() => {
101
+ if (!vaultNodeRegistryAddress) {
102
+ return;
103
+ }
104
+
105
+ const initializeClient = async () => {
106
+ // Don't reinitialize if client already exists
107
+ if (globalClient) {
108
+ console.log('[ByteCaveProvider] Client already exists, skipping initialization');
109
+ return;
110
+ }
111
+
112
+ console.log('[ByteCaveProvider] Creating new ByteCaveClient');
113
+ globalClient = new ByteCaveClient({
114
+ vaultNodeRegistryAddress,
115
+ contentRegistryAddress,
116
+ rpcUrl,
117
+ appId,
118
+ directNodeAddrs,
119
+ relayPeers,
120
+ maxPeers: 10,
121
+ connectionTimeout: 30000
122
+ });
123
+
124
+ const client = globalClient;
125
+
126
+ const handleStateChange = (state: ConnectionState) => {
127
+ setConnectionState(state);
128
+ };
129
+
130
+ const handlePeerUpdate = async () => {
131
+ if (!client) return;
132
+ try {
133
+ const peers = await client.getPeers();
134
+ setPeers(peers);
135
+ } catch (err) {
136
+ // Silent
137
+ }
138
+ };
139
+
140
+ client.on('connectionStateChange', handleStateChange);
141
+ client.on('peerConnect', handlePeerUpdate);
142
+ client.on('peerDisconnect', handlePeerUpdate);
143
+
144
+ const hasPeers = directNodeAddrs.length > 0 || relayPeers.length > 0;
145
+
146
+ console.log('[ByteCaveProvider] Auto-connect check:', {
147
+ hasPeers,
148
+ directNodeAddrs: directNodeAddrs.length,
149
+ relayPeers: relayPeers.length,
150
+ connectCalled: connectCalledRef.current
151
+ });
152
+
153
+ if (hasPeers && !connectCalledRef.current) {
154
+ connectCalledRef.current = true;
155
+ console.log('[ByteCaveProvider] Auto-connecting in 100ms...');
156
+ setTimeout(() => {
157
+ console.log('[ByteCaveProvider] Calling connect()...');
158
+ connect();
159
+ }, 100);
160
+ } else if (!hasPeers) {
161
+ console.warn('[ByteCaveProvider] No relay peers or direct node addresses configured - cannot auto-connect');
162
+ }
163
+ };
164
+
165
+ initializeClient();
166
+
167
+ return () => {
168
+ if (globalClient) {
169
+ globalClient.off('connectionStateChange', () => {});
170
+ globalClient.off('peerConnect', () => {});
171
+ globalClient.off('peerDisconnect', () => {});
172
+ }
173
+ };
174
+ }, []); // Empty deps - only initialize once on mount
175
+
176
+ // Auto-reconnect when peers drop to 0
177
+ useEffect(() => {
178
+ const reconnectInterval = setInterval(async () => {
179
+ if (peers.length === 0 && connectionState === 'connected' && globalClient) {
180
+ console.log('[ByteCaveProvider] No peers connected, attempting reconnection...');
181
+ try {
182
+ await connect();
183
+ } catch (err) {
184
+ console.error('[ByteCaveProvider] Reconnection failed:', err);
185
+ }
186
+ }
187
+ }, 5000);
188
+
189
+ return () => clearInterval(reconnectInterval);
190
+ }, [peers.length, connectionState, connect]);
191
+
192
+ // Periodic peer directory refresh to rediscover nodes
193
+ useEffect(() => {
194
+ const refreshInterval = setInterval(async () => {
195
+ if (connectionState === 'connected' && globalClient) {
196
+ console.log('[ByteCaveProvider] Refreshing peer directory...');
197
+ try {
198
+ await globalClient.refreshPeerDirectory();
199
+ const updatedPeers = await globalClient.getPeers();
200
+ setPeers(updatedPeers);
201
+ } catch (err) {
202
+ console.error('[ByteCaveProvider] Peer directory refresh failed:', err);
203
+ }
204
+ }
205
+ }, 30000); // Refresh every 30 seconds
206
+
207
+ return () => clearInterval(refreshInterval);
208
+ }, [connectionState]);
209
+
210
+ const disconnect = async () => {
211
+ if (!globalClient) return;
212
+ try {
213
+ await globalClient.stop();
214
+ setPeers([]);
215
+ } catch (err: any) {
216
+ setError(err.message);
217
+ }
218
+ };
219
+
220
+ const store = async (data: Uint8Array, mimeType?: string, signer?: any): Promise<StoreResult> => {
221
+ if (!globalClient) {
222
+ return { success: false, error: 'Client not initialized' };
223
+ }
224
+ return (globalClient as any).store(data, mimeType, signer);
225
+ };
226
+
227
+ const retrieve = async (cid: string): Promise<RetrieveResult> => {
228
+ if (!globalClient) {
229
+ return { success: false, error: 'Client not initialized' };
230
+ }
231
+ return globalClient.retrieve(cid);
232
+ };
233
+
234
+ const getNodeHealth = async (peerId: string): Promise<NodeHealth | null> => {
235
+ if (!globalClient) {
236
+ return null;
237
+ }
238
+ return (globalClient as any).getNodeHealth(peerId);
239
+ };
240
+
241
+ const registerContent = async (cid: string, appId: string, signer: any): Promise<{ success: boolean; txHash?: string; error?: string }> => {
242
+ if (!globalClient) {
243
+ return { success: false, error: 'Client not initialized' };
244
+ }
245
+ return globalClient.registerContent(cid, appId, signer);
246
+ };
247
+
248
+ const value: ByteCaveContextValue = {
249
+ connectionState,
250
+ peers,
251
+ isConnected: connectionState === 'connected',
252
+ appId,
253
+ connect,
254
+ disconnect,
255
+ store,
256
+ retrieve,
257
+ registerContent,
258
+ getNodeHealth,
259
+ error
260
+ };
261
+
262
+ return (
263
+ <ByteCaveContext.Provider value={value}>
264
+ {children}
265
+ </ByteCaveContext.Provider>
266
+ );
267
+ }
268
+
269
+ export function useByteCaveContext() {
270
+ const context = useContext(ByteCaveContext);
271
+ if (!context) {
272
+ throw new Error('useByteCaveContext must be used within ByteCaveProvider');
273
+ }
274
+ return context;
275
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * React Components for HashD Protocol
3
+ *
4
+ * Drop-in components for loading content from ByteCave network
5
+ */
6
+
7
+ import React, { ImgHTMLAttributes, VideoHTMLAttributes, AudioHTMLAttributes } from 'react';
8
+ import { useHashdImage, useHashdMedia, type UseHashdContentOptions } from './hooks.js';
9
+
10
+ export interface HashdImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
11
+ src: string;
12
+ client: UseHashdContentOptions['client'];
13
+ placeholder?: string;
14
+ loadingComponent?: React.ReactNode;
15
+ errorComponent?: React.ReactNode;
16
+ onHashdLoad?: () => void;
17
+ onHashdError?: (error: Error) => void;
18
+ }
19
+
20
+ /**
21
+ * Drop-in replacement for <img> that loads from hashd:// URLs
22
+ *
23
+ * @example
24
+ * <HashdImage
25
+ * src="hashd://abc123..."
26
+ * client={byteCaveClient}
27
+ * alt="Profile picture"
28
+ * className="w-32 h-32 rounded-full"
29
+ * />
30
+ */
31
+ export function HashdImage({
32
+ src,
33
+ client,
34
+ placeholder,
35
+ loadingComponent,
36
+ errorComponent,
37
+ onHashdLoad,
38
+ onHashdError,
39
+ ...imgProps
40
+ }: HashdImageProps) {
41
+ const { src: blobUrl, loading, error } = useHashdImage(src, {
42
+ client,
43
+ placeholder,
44
+ onSuccess: onHashdLoad,
45
+ onError: onHashdError
46
+ });
47
+
48
+ if (loading && loadingComponent) {
49
+ return <>{loadingComponent}</>;
50
+ }
51
+
52
+ if (error && errorComponent) {
53
+ return <>{errorComponent}</>;
54
+ }
55
+
56
+ return <img {...imgProps} src={blobUrl} />;
57
+ }
58
+
59
+ export interface HashdVideoProps extends Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src'> {
60
+ src: string;
61
+ client: UseHashdContentOptions['client'];
62
+ loadingComponent?: React.ReactNode;
63
+ errorComponent?: React.ReactNode;
64
+ onHashdLoad?: () => void;
65
+ onHashdError?: (error: Error) => void;
66
+ }
67
+
68
+ /**
69
+ * Drop-in replacement for <video> that loads from hashd:// URLs
70
+ *
71
+ * @example
72
+ * <HashdVideo
73
+ * src="hashd://abc123..."
74
+ * client={byteCaveClient}
75
+ * controls
76
+ * className="w-full"
77
+ * />
78
+ */
79
+ export function HashdVideo({
80
+ src,
81
+ client,
82
+ loadingComponent,
83
+ errorComponent,
84
+ onHashdLoad,
85
+ onHashdError,
86
+ ...videoProps
87
+ }: HashdVideoProps) {
88
+ const { src: blobUrl, loading, error } = useHashdMedia(src, {
89
+ client,
90
+ onSuccess: onHashdLoad,
91
+ onError: onHashdError
92
+ });
93
+
94
+ if (loading && loadingComponent) {
95
+ return <>{loadingComponent}</>;
96
+ }
97
+
98
+ if (error && errorComponent) {
99
+ return <>{errorComponent}</>;
100
+ }
101
+
102
+ return <video {...videoProps} src={blobUrl} />;
103
+ }
104
+
105
+ export interface HashdAudioProps extends Omit<AudioHTMLAttributes<HTMLAudioElement>, 'src'> {
106
+ src: string;
107
+ client: UseHashdContentOptions['client'];
108
+ loadingComponent?: React.ReactNode;
109
+ errorComponent?: React.ReactNode;
110
+ onHashdLoad?: () => void;
111
+ onHashdError?: (error: Error) => void;
112
+ }
113
+
114
+ /**
115
+ * Drop-in replacement for <audio> that loads from hashd:// URLs
116
+ *
117
+ * @example
118
+ * <HashdAudio
119
+ * src="hashd://abc123..."
120
+ * client={byteCaveClient}
121
+ * controls
122
+ * />
123
+ */
124
+ export function HashdAudio({
125
+ src,
126
+ client,
127
+ loadingComponent,
128
+ errorComponent,
129
+ onHashdLoad,
130
+ onHashdError,
131
+ ...audioProps
132
+ }: HashdAudioProps) {
133
+ const { src: blobUrl, loading, error } = useHashdMedia(src, {
134
+ client,
135
+ onSuccess: onHashdLoad,
136
+ onError: onHashdError
137
+ });
138
+
139
+ if (loading && loadingComponent) {
140
+ return <>{loadingComponent}</>;
141
+ }
142
+
143
+ if (error && errorComponent) {
144
+ return <>{errorComponent}</>;
145
+ }
146
+
147
+ return <audio {...audioProps} src={blobUrl} />;
148
+ }
149
+
150
+ export interface HashdContentProps {
151
+ url: string;
152
+ client: UseHashdContentOptions['client'];
153
+ children: (props: {
154
+ blobUrl: string | null;
155
+ loading: boolean;
156
+ error: Error | null;
157
+ mimeType: string | null;
158
+ }) => React.ReactNode;
159
+ }
160
+
161
+ /**
162
+ * Render prop component for custom content rendering
163
+ *
164
+ * @example
165
+ * <HashdContent url="hashd://abc123..." client={client}>
166
+ * {({ blobUrl, loading, error }) => {
167
+ * if (loading) return <Spinner />;
168
+ * if (error) return <Error message={error.message} />;
169
+ * return <img src={blobUrl} />;
170
+ * }}
171
+ * </HashdContent>
172
+ */
173
+ export function HashdContent({ url, client, children }: HashdContentProps) {
174
+ const { blobUrl, loading, error, mimeType } = useHashdImage(url, { client });
175
+
176
+ return <>{children({ blobUrl, loading, error, mimeType })}</>;
177
+ }