291 lines
8.9 KiB
JavaScript
291 lines
8.9 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
// Blast relays - configure these once and they'll be used for publishing
|
|
// These are hidden from the UI and used to blast events to reliable relays
|
|
const BLAST_RELAYS = [
|
|
'wss://relay.primal.net',
|
|
'wss://relay.damus.io',
|
|
'wss://sendit.nosflare.com',
|
|
'wss://nostr.mom',
|
|
'wss://relay.ditto.pub',
|
|
'wss://nos.lol'
|
|
];
|
|
|
|
export const useNostr = () => {
|
|
const [pubkey, setPubkey] = useState(null);
|
|
const [isConnected, setIsConnected] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
const [extension, setExtension] = useState(null);
|
|
|
|
useEffect(() => {
|
|
const checkExtension = async () => {
|
|
try {
|
|
if (window.nostr) {
|
|
setExtension(window.nostr);
|
|
try {
|
|
const pk = await window.nostr.getPublicKey();
|
|
setPubkey(pk);
|
|
setIsConnected(true);
|
|
} catch (err) {
|
|
console.log('Extension available but not authorized');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError('No Nostr extension found');
|
|
}
|
|
};
|
|
checkExtension();
|
|
}, []);
|
|
|
|
const connect = useCallback(async () => {
|
|
try {
|
|
if (!window.nostr) {
|
|
throw new Error('Please install a Nostr extension (Alby, Nos2x, etc.)');
|
|
}
|
|
const pk = await window.nostr.getPublicKey();
|
|
setPubkey(pk);
|
|
setIsConnected(true);
|
|
setExtension(window.nostr);
|
|
setError(null);
|
|
return pk;
|
|
} catch (err) {
|
|
setError(err.message);
|
|
setIsConnected(false);
|
|
throw err;
|
|
}
|
|
}, []);
|
|
|
|
const signEvent = useCallback(async (event) => {
|
|
if (!extension) {
|
|
throw new Error('No Nostr extension connected');
|
|
}
|
|
return await extension.signEvent(event);
|
|
}, [extension]);
|
|
|
|
const publishToSingleRelay = useCallback((url, event) => {
|
|
return new Promise((resolve) => {
|
|
let ws = null;
|
|
let resolved = false;
|
|
let timeoutIds = [];
|
|
|
|
try {
|
|
ws = new WebSocket(url);
|
|
|
|
const connectTimeout = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
|
try { ws.close(); } catch (e) {}
|
|
}
|
|
resolve({ url, success: false, error: 'Connection timeout' });
|
|
}
|
|
}, 10000);
|
|
timeoutIds.push(connectTimeout);
|
|
|
|
ws.onopen = () => {
|
|
const message = JSON.stringify(['EVENT', event]);
|
|
ws.send(message);
|
|
|
|
const publishTimeout = setTimeout(() => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
console.log(`⏰ Publish timeout for ${url}`);
|
|
try { ws.close(); } catch (e) {}
|
|
resolve({ url, success: false, error: 'Publish timeout' });
|
|
}
|
|
}, 15000);
|
|
timeoutIds.push(publishTimeout);
|
|
|
|
ws.onmessage = (msg) => {
|
|
try {
|
|
const data = JSON.parse(msg.data);
|
|
if (Array.isArray(data) && data[0] === 'OK' && data[1] === event.id) {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
console.log(`✅ Published to ${url}`);
|
|
timeoutIds.forEach(id => clearTimeout(id));
|
|
try { ws.close(); } catch (e) {}
|
|
resolve({ url, success: true });
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
timeoutIds.forEach(id => clearTimeout(id));
|
|
try { ws.close(); } catch (e) {}
|
|
resolve({ url, success: false, error: 'WebSocket error' });
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
timeoutIds.forEach(id => clearTimeout(id));
|
|
resolve({ url, success: false, error: 'Connection closed' });
|
|
}
|
|
};
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
timeoutIds.forEach(id => clearTimeout(id));
|
|
resolve({ url, success: false, error: 'Connection error' });
|
|
}
|
|
};
|
|
|
|
} catch (err) {
|
|
if (!resolved) {
|
|
resolved = true;
|
|
timeoutIds.forEach(id => clearTimeout(id));
|
|
if (ws) {
|
|
try { ws.close(); } catch (e) {}
|
|
}
|
|
resolve({ url, success: false, error: err.message });
|
|
}
|
|
}
|
|
});
|
|
}, []);
|
|
|
|
/**
|
|
* Publish to multiple relays with detailed results
|
|
*/
|
|
const publishToRelays = useCallback(async (event, relayUrls, label = '') => {
|
|
console.log(`Publishing ${label} to ${relayUrls.length} relays...`);
|
|
|
|
const validUrls = relayUrls.filter(url => {
|
|
try {
|
|
new URL(url);
|
|
return url.startsWith('wss://') || url.startsWith('ws://');
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (validUrls.length === 0) {
|
|
return [{ url: 'none', success: false, error: 'No valid relay URLs' }];
|
|
}
|
|
|
|
const publishPromises = validUrls.map(url => publishToSingleRelay(url, event));
|
|
const results = await Promise.all(publishPromises);
|
|
|
|
const successCount = results.filter(r => r.success).length;
|
|
console.log(`Published ${label} to ${successCount}/${results.length} relays`);
|
|
|
|
return results;
|
|
}, [publishToSingleRelay]);
|
|
|
|
/**
|
|
* Generic publish function that publishes to BOTH blast relays AND user relays
|
|
*/
|
|
const publishKind = useCallback(async (kind, data, pubkey, userRelayUrls = []) => {
|
|
if (!extension) {
|
|
throw new Error('No Nostr extension connected');
|
|
}
|
|
|
|
const event = {
|
|
kind: kind,
|
|
pubkey: pubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: data.tags || [],
|
|
content: data.content || ''
|
|
};
|
|
|
|
console.log(`Signing kind ${kind} event...`);
|
|
const signedEvent = await extension.signEvent(event);
|
|
console.log(`Kind ${kind} event signed successfully`);
|
|
|
|
// Combine blast relays and user relays (deduplicated)
|
|
const allRelays = [...new Set([...BLAST_RELAYS, ...userRelayUrls])];
|
|
console.log(`Publishing kind ${kind} to ${allRelays.length} relays (${BLAST_RELAYS.length} blast + ${userRelayUrls.length} user)`);
|
|
|
|
// Publish to all relays
|
|
const results = await publishToRelays(signedEvent, allRelays, `kind ${kind}`);
|
|
|
|
// Separate results for better reporting
|
|
const blastResults = results.filter(r => BLAST_RELAYS.includes(r.url));
|
|
const userResults = results.filter(r => userRelayUrls.includes(r.url));
|
|
|
|
console.log(`Kind ${kind} results:`, {
|
|
blast: `${blastResults.filter(r => r.success).length}/${blastResults.length}`,
|
|
user: `${userResults.filter(r => r.success).length}/${userResults.length}`
|
|
});
|
|
|
|
return {
|
|
event: signedEvent,
|
|
results: results,
|
|
blastResults: blastResults,
|
|
userResults: userResults
|
|
};
|
|
}, [extension, publishToRelays]);
|
|
|
|
// Kind 10002 - Relays
|
|
const publishKind10002 = useCallback(async (relays, pubkey) => {
|
|
const validRelays = relays.filter(r => r.url && r.url.trim() !== '');
|
|
|
|
if (validRelays.length === 0) {
|
|
throw new Error('No valid relays to publish');
|
|
}
|
|
|
|
const tags = validRelays.map(relay => {
|
|
const params = [];
|
|
if (!relay.read) params.push('read');
|
|
if (!relay.write) params.push('write');
|
|
return ['r', relay.url, ...params];
|
|
});
|
|
|
|
// Extract user relay URLs for combined publishing
|
|
const userRelayUrls = validRelays.map(r => r.url);
|
|
|
|
return await publishKind(10002, { tags }, pubkey, userRelayUrls);
|
|
}, [publishKind]);
|
|
|
|
// Kind 10063 - Blossom Servers
|
|
const publishKind10063 = useCallback(async (servers, pubkey) => {
|
|
const validServers = servers.filter(s => s.url && s.url.trim() !== '');
|
|
|
|
if (validServers.length === 0) {
|
|
throw new Error('No valid blossom servers');
|
|
}
|
|
|
|
const tags = validServers.map(server => ['server', server.url]);
|
|
|
|
// For blossom servers, we want to publish to blast relays
|
|
// The user's main relays are used for kind 10002, not kind 10063
|
|
return await publishKind(10063, { tags }, pubkey, []);
|
|
}, [publishKind]);
|
|
|
|
// Kind 10050 - DM Relays
|
|
const publishKind10050 = useCallback(async (relays, pubkey) => {
|
|
const validRelays = relays.filter(r => r.url && r.url.trim() !== '');
|
|
|
|
if (validRelays.length === 0) {
|
|
throw new Error('No valid DM relays');
|
|
}
|
|
|
|
const tags = validRelays.map(relay => ['relay', relay.url]);
|
|
|
|
// For DM relays, we want to publish to blast relays
|
|
// The user's main relays are used for kind 10002, not kind 10050
|
|
return await publishKind(10050, { tags }, pubkey, []);
|
|
}, [publishKind]);
|
|
|
|
return {
|
|
pubkey,
|
|
isConnected,
|
|
error,
|
|
extension,
|
|
connect,
|
|
signEvent,
|
|
publishToRelays,
|
|
publishKind,
|
|
publishKind10002,
|
|
publishKind10063,
|
|
publishKind10050
|
|
};
|
|
};
|
|
|
|
export default useNostr;
|