Refactor Nostr Client and Update Dependencies

- Replaced NDKPrivateKeySigner with PrivateKeySigner from applesauce-signers for improved signing functionality.
- Updated the Nostr client implementation to utilize nostr-tools for event management and connection pooling.
- Enhanced event fetching logic to support multiple versions of replaceable events based on limit parameters.
- Updated package dependencies in package.json and bun.lock, including the addition of applesauce-core and applesauce-signers.
- Refined event kind definitions and improved documentation for clarity and consistency with NIP specifications.
- Adjusted CSS styles in bundle.css for better visual consistency across components.
This commit is contained in:
2025-10-25 17:27:25 +01:00
parent badac55813
commit c5ff2c648c
13 changed files with 901 additions and 1436 deletions

View File

@@ -1,42 +1,41 @@
import NDK, { NDKPrivateKeySigner, NDKEvent } from '@nostr-dev-kit/ndk';
import { SimplePool } from 'nostr-tools/pool';
import { EventStore } from 'applesauce-core';
import { PrivateKeySigner } from 'applesauce-signers';
import { DEFAULT_RELAYS } from "./constants.js";
// NDK-based Nostr client wrapper
// Nostr client wrapper using nostr-tools
class NostrClient {
constructor() {
this.ndk = new NDK({
explicitRelayUrls: DEFAULT_RELAYS
});
this.pool = new SimplePool();
this.eventStore = new EventStore();
this.isConnected = false;
this.signer = null;
this.relays = [...DEFAULT_RELAYS];
}
async connect() {
console.log("Starting NDK connection to", DEFAULT_RELAYS.length, "relays...");
console.log("Starting connection to", this.relays.length, "relays...");
try {
await this.ndk.connect();
// SimplePool doesn't require explicit connect
this.isConnected = true;
console.log("✓ NDK successfully connected to relays");
console.log("✓ Successfully initialized relay pool");
// Wait a bit for connections to stabilize
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
console.error("✗ NDK connection failed:", error);
console.error("✗ Connection failed:", error);
throw error;
}
}
async connectToRelay(relayUrl) {
console.log(`Adding relay to NDK: ${relayUrl}`);
console.log(`Adding relay: ${relayUrl}`);
try {
// For now, just update the DEFAULT_RELAYS array and reconnect
// This is a simpler approach that avoids replacing the NDK instance
DEFAULT_RELAYS.push(relayUrl);
// Reconnect with the updated relay list
await this.connect();
if (!this.relays.includes(relayUrl)) {
this.relays.push(relayUrl);
}
console.log(`✓ Successfully added relay ${relayUrl}`);
return true;
} catch (error) {
@@ -46,69 +45,76 @@ class NostrClient {
}
subscribe(filters, callback) {
console.log("Creating NDK subscription with filters:", filters);
console.log("Creating subscription with filters:", filters);
const subscription = this.ndk.subscribe(filters, {
closeOnEose: true
});
const sub = this.pool.subscribeMany(
this.relays,
filters,
{
onevent(event) {
console.log("Event received:", event);
callback(event);
},
oneose() {
console.log("EOSE received");
window.dispatchEvent(new CustomEvent('nostr-eose', {
detail: { subscriptionId: sub.id }
}));
}
}
);
subscription.on('event', (event) => {
console.log("Event received via NDK:", event);
callback(event.rawEvent());
});
subscription.on('eose', () => {
console.log("EOSE received via NDK");
window.dispatchEvent(new CustomEvent('nostr-eose', {
detail: { subscriptionId: subscription.id }
}));
});
return subscription.id;
return sub;
}
unsubscribe(subscriptionId) {
console.log(`Closing NDK subscription: ${subscriptionId}`);
// NDK handles subscription cleanup automatically
unsubscribe(subscription) {
console.log(`Closing subscription`);
if (subscription && subscription.close) {
subscription.close();
}
}
disconnect() {
console.log("Disconnecting NDK");
// Note: NDK doesn't have a destroy method, just disconnect
if (this.ndk && typeof this.ndk.disconnect === 'function') {
this.ndk.disconnect();
console.log("Disconnecting relay pool");
if (this.pool) {
this.pool.close(this.relays);
}
this.isConnected = false;
}
// Publish an event using NDK
// Publish an event
async publish(event) {
console.log("Publishing event via NDK:", event);
console.log("Publishing event:", event);
try {
const ndkEvent = new NDKEvent(this.ndk, event);
await ndkEvent.publish();
console.log("✓ Event published successfully via NDK");
const promises = this.pool.publish(this.relays, event);
await Promise.allSettled(promises);
console.log("✓ Event published successfully");
return { success: true, okCount: 1, errorCount: 0 };
} catch (error) {
console.error("✗ Failed to publish event via NDK:", error);
console.error("✗ Failed to publish event:", error);
throw error;
}
}
// Get NDK instance for advanced usage
getNDK() {
return this.ndk;
// Get pool for advanced usage
getPool() {
return this.pool;
}
// Get signer from NDK
// Get event store
getEventStore() {
return this.eventStore;
}
// Get signer
getSigner() {
return this.ndk.signer;
return this.signer;
}
// Set signer for NDK
// Set signer
setSigner(signer) {
this.ndk.signer = signer;
this.signer = signer;
}
}
@@ -118,6 +124,54 @@ export const nostrClient = new NostrClient();
// Export the class for creating new instances
export { NostrClient };
// Export signer classes
export { PrivateKeySigner };
// Export NIP-07 helper
export class Nip07Signer {
async getPublicKey() {
if (window.nostr) {
return await window.nostr.getPublicKey();
}
throw new Error('NIP-07 extension not found');
}
async signEvent(event) {
if (window.nostr) {
return await window.nostr.signEvent(event);
}
throw new Error('NIP-07 extension not found');
}
async nip04Encrypt(pubkey, plaintext) {
if (window.nostr && window.nostr.nip04) {
return await window.nostr.nip04.encrypt(pubkey, plaintext);
}
throw new Error('NIP-07 extension does not support NIP-04');
}
async nip04Decrypt(pubkey, ciphertext) {
if (window.nostr && window.nostr.nip04) {
return await window.nostr.nip04.decrypt(pubkey, ciphertext);
}
throw new Error('NIP-07 extension does not support NIP-04');
}
async nip44Encrypt(pubkey, plaintext) {
if (window.nostr && window.nostr.nip44) {
return await window.nostr.nip44.encrypt(pubkey, plaintext);
}
throw new Error('NIP-07 extension does not support NIP-44');
}
async nip44Decrypt(pubkey, ciphertext) {
if (window.nostr && window.nostr.nip44) {
return await window.nostr.nip44.decrypt(pubkey, ciphertext);
}
throw new Error('NIP-07 extension does not support NIP-44');
}
}
// IndexedDB helpers for caching events (kind 0 profiles)
const DB_NAME = "nostrCache";
const DB_VERSION = 1;
@@ -209,7 +263,7 @@ function parseProfileFromEvent(event) {
}
}
// Fetch user profile metadata (kind 0) using NDK
// Fetch user profile metadata (kind 0)
export async function fetchUserProfile(pubkey) {
console.log(`Starting profile fetch for pubkey: ${pubkey}`);
@@ -225,29 +279,32 @@ export async function fetchUserProfile(pubkey) {
console.warn("Failed to load cached profile", e);
}
// 2) Fetch profile using NDK
// 2) Fetch profile from relays
try {
const ndk = nostrClient.getNDK();
const user = ndk.getUser({ hexpubkey: pubkey });
const filters = [{
kinds: [0],
authors: [pubkey],
limit: 1
}];
// Fetch the latest profile event
const profileEvent = await user.fetchProfile();
const events = await fetchEvents(filters, { timeout: 10000 });
if (profileEvent) {
console.log("Profile fetched via NDK:", profileEvent);
if (events.length > 0) {
const profileEvent = events[0];
console.log("Profile fetched:", profileEvent);
// Cache the event
await putEvent(profileEvent.rawEvent());
await putEvent(profileEvent);
// Parse profile data
const profile = parseProfileFromEvent(profileEvent.rawEvent());
const profile = parseProfileFromEvent(profileEvent);
// Notify listeners that an updated profile is available
try {
if (typeof window !== "undefined" && window.dispatchEvent) {
window.dispatchEvent(
new CustomEvent("profile-updated", {
detail: { pubkey, profile, event: profileEvent.rawEvent() },
detail: { pubkey, profile, event: profileEvent },
}),
);
}
@@ -260,49 +317,53 @@ export async function fetchUserProfile(pubkey) {
throw new Error("No profile found");
}
} catch (error) {
console.error("Failed to fetch profile via NDK:", error);
console.error("Failed to fetch profile:", error);
throw error;
}
}
// Fetch events using NDK
// Fetch events
export async function fetchEvents(filters, options = {}) {
console.log(`Starting event fetch with filters:`, filters);
const {
timeout = 30000,
limit = null
} = options;
try {
const ndk = nostrClient.getNDK();
// Add limit to filters if specified
const requestFilters = { ...filters };
if (limit) {
requestFilters.limit = limit;
return new Promise((resolve, reject) => {
const events = [];
const timeoutId = setTimeout(() => {
console.log(`Timeout reached after ${timeout}ms, returning ${events.length} events`);
sub.close();
resolve(events);
}, timeout);
try {
const sub = nostrClient.pool.subscribeMany(
nostrClient.relays,
filters,
{
onevent(event) {
console.log("Event received:", event);
events.push(event);
},
oneose() {
console.log(`EOSE received, got ${events.length} events`);
clearTimeout(timeoutId);
sub.close();
resolve(events);
}
}
);
} catch (error) {
clearTimeout(timeoutId);
console.error("Failed to fetch events:", error);
reject(error);
}
console.log('Fetching events via NDK with filters:', requestFilters);
// Use NDK's fetchEvents method
const events = await ndk.fetchEvents(requestFilters, {
timeout
});
console.log(`Fetched ${events.size} events via NDK`);
// Convert NDK events to raw events
const rawEvents = Array.from(events).map(event => event.rawEvent());
return rawEvents;
} catch (error) {
console.error("Failed to fetch events via NDK:", error);
throw error;
}
});
}
// Fetch all events with timestamp-based pagination using NDK (including delete events)
// Fetch all events with timestamp-based pagination (including delete events)
export async function fetchAllEvents(options = {}) {
const {
limit = 100,
@@ -310,28 +371,25 @@ export async function fetchAllEvents(options = {}) {
until = null,
authors = null,
kinds = null,
tags = null
...rest
} = options;
const filters = {};
const filters = [{ ...rest }];
if (since) filters.since = since;
if (until) filters.until = until;
if (authors) filters.authors = authors;
if (kinds) filters.kinds = kinds;
if (tags) filters.tags = tags;
// Don't specify kinds filter - this will include all events including delete events (kind 5)
if (since) filters[0].since = since;
if (until) filters[0].until = until;
if (authors) filters[0].authors = authors;
if (kinds) filters[0].kinds = kinds;
if (limit) filters[0].limit = limit;
const events = await fetchEvents(filters, {
limit: limit,
timeout: 30000
});
return events;
}
// Fetch user's events with timestamp-based pagination using NDK
// Fetch user's events with timestamp-based pagination
export async function fetchUserEvents(pubkey, options = {}) {
const {
limit = 100,
@@ -339,22 +397,22 @@ export async function fetchUserEvents(pubkey, options = {}) {
until = null
} = options;
const filters = {
const filters = [{
authors: [pubkey]
};
}];
if (since) filters.since = since;
if (until) filters.until = until;
if (since) filters[0].since = since;
if (until) filters[0].until = until;
if (limit) filters[0].limit = limit;
const events = await fetchEvents(filters, {
limit: limit,
timeout: 30000
});
return events;
}
// NIP-50 search function using NDK
// NIP-50 search function
export async function searchEvents(searchQuery, options = {}) {
const {
limit = 100,
@@ -363,16 +421,16 @@ export async function searchEvents(searchQuery, options = {}) {
kinds = null
} = options;
const filters = {
const filters = [{
search: searchQuery
};
}];
if (since) filters.since = since;
if (until) filters.until = until;
if (kinds) filters.kinds = kinds;
if (since) filters[0].since = since;
if (until) filters[0].until = until;
if (kinds) filters[0].kinds = kinds;
if (limit) filters[0].limit = limit;
const events = await fetchEvents(filters, {
limit: limit,
timeout: 30000
});
@@ -383,39 +441,30 @@ export async function searchEvents(searchQuery, options = {}) {
export async function fetchEventById(eventId, options = {}) {
const {
timeout = 10000,
relays = null
} = options;
console.log(`Fetching event by ID: ${eventId}`);
try {
const ndk = nostrClient.getNDK();
const filters = {
const filters = [{
ids: [eventId]
};
}];
console.log('Fetching event via NDK with filters:', filters);
console.log('Fetching event with filters:', filters);
// Use NDK's fetchEvents method
const events = await ndk.fetchEvents(filters, {
timeout
});
const events = await fetchEvents(filters, { timeout });
console.log(`Fetched ${events.size} events via NDK`);
// Convert NDK events to raw events
const rawEvents = Array.from(events).map(event => event.rawEvent());
console.log(`Fetched ${events.length} events`);
// Return the first event if found, null otherwise
return rawEvents.length > 0 ? rawEvents[0] : null;
return events.length > 0 ? events[0] : null;
} catch (error) {
console.error("Failed to fetch event by ID via NDK:", error);
console.error("Failed to fetch event by ID:", error);
throw error;
}
}
// Fetch delete events that target a specific event ID using Nostr
// Fetch delete events that target a specific event ID
export async function fetchDeleteEventsByTarget(eventId, options = {}) {
const {
timeout = 10000
@@ -424,33 +473,24 @@ export async function fetchDeleteEventsByTarget(eventId, options = {}) {
console.log(`Fetching delete events for target: ${eventId}`);
try {
const ndk = nostrClient.getNDK();
const filters = {
const filters = [{
kinds: [5], // Kind 5 is deletion
'#e': [eventId] // e-tag referencing the target event
};
}];
console.log('Fetching delete events via NDK with filters:', filters);
console.log('Fetching delete events with filters:', filters);
// Use NDK's fetchEvents method
const events = await ndk.fetchEvents(filters, {
timeout
});
const events = await fetchEvents(filters, { timeout });
console.log(`Fetched ${events.size} delete events via NDK`);
console.log(`Fetched ${events.length} delete events`);
// Convert NDK events to raw events
const rawEvents = Array.from(events).map(event => event.rawEvent());
return rawEvents;
return events;
} catch (error) {
console.error("Failed to fetch delete events via NDK:", error);
console.error("Failed to fetch delete events:", error);
throw error;
}
}
// Initialize client connection
export async function initializeNostrClient() {
await nostrClient.connect();