import { Event, EventTemplate, getEventHash, signEvent, nip19, SimplePool } from 'nostr-tools' import type { Article, NostrProfile } from '@/types/nostr' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import { parseArticleFromEvent } from './nostrEventParsing' import { getPrivateContent as getPrivateContentFromPool } from './nostrPrivateMessages' import { checkZapReceipt as checkZapReceiptHelper } from './nostrZapVerification' import { subscribeWithTimeout } from './nostrSubscription' const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io' class NostrService { private pool: SimplePool | null = null private privateKey: string | null = null private publicKey: string | null = null constructor() { if (typeof window !== 'undefined') { this.initializePool() } } private initializePool() { this.pool = new SimplePool() } setPrivateKey(privateKey: string) { this.privateKey = privateKey try { const decoded = nip19.decode(privateKey) if (decoded.type === 'nsec' && typeof decoded.data === 'string') { this.privateKey = decoded.data } } catch (_e) { // Assume it's already a hex string } } getPrivateKey(): string | null { return this.privateKey } getPublicKey(): string | null { return this.publicKey } setPublicKey(publicKey: string) { this.publicKey = publicKey try { const decoded = nip19.decode(publicKey) if (decoded.type === 'npub' && typeof decoded.data === 'string') { this.publicKey = decoded.data } } catch (_e) { // Assume it's already a hex string } } async publishEvent(eventTemplate: EventTemplate): Promise { if (!this.privateKey || !this.pool) { throw new Error('Private key not set or pool not initialized') } const unsignedEvent = { pubkey: this.publicKey ?? '', ...eventTemplate, created_at: eventTemplate.created_at ?? Math.floor(Date.now() / 1000), } const event = { ...unsignedEvent, id: getEventHash(unsignedEvent), sig: signEvent(unsignedEvent, this.privateKey), } as Event try { const pubs = this.pool.publish([RELAY_URL], event) await Promise.all(pubs) return event } catch (e) { throw new Error(`Publish failed: ${e}`) } } subscribeToArticles(callback: (article: Article) => void, limit: number = 100): () => void { if (typeof window === 'undefined') { throw new Error('Cannot subscribe on server side') } if (!this.pool) { this.initializePool() } if (!this.pool) { throw new Error('Pool not initialized') } const filters = [ { kinds: [1], // Text notes (includes both articles and presentation articles) limit, }, ] const pool = this.pool as SimplePoolWithSub const sub = pool.sub([RELAY_URL], filters) sub.on('event', (event: Event) => { try { const article = parseArticleFromEvent(event) if (article) { callback(article) } } catch (e) { console.error('Error parsing article:', e) } }) return () => { sub.unsub() } } getArticleById(eventId: string): Promise
{ if (!this.pool) { throw new Error('Pool not initialized') } const filters = [{ ids: [eventId], kinds: [1] }] return subscribeWithTimeout(this.pool, filters, parseArticleFromEvent, 5000) } getPrivateContent(eventId: string, authorPubkey: string): Promise { if (!this.privateKey || !this.pool || !this.publicKey) { throw new Error('Private key not set or pool not initialized') } return getPrivateContentFromPool(this.pool, eventId, authorPubkey, this.privateKey, this.publicKey) } getProfile(pubkey: string): Promise { if (!this.pool) { throw new Error('Pool not initialized') } const filters = [ { kinds: [0], authors: [pubkey], limit: 1, }, ] const parseProfile = (event: Event) => { try { const profile = JSON.parse(event.content) as NostrProfile return { ...profile, pubkey } } catch (error) { console.error('Error parsing profile:', error) return null } } return subscribeWithTimeout(this.pool, filters, parseProfile, 5000) } async createZapRequest(targetPubkey: string, targetEventId: string, amount: number): Promise { if (!this.privateKey) { throw new Error('Private key not set') } const zapRequest: EventTemplate = { kind: 9734, // Zap request created_at: Math.floor(Date.now() / 1000), tags: [ ['p', targetPubkey], ['e', targetEventId], ['amount', amount.toString()], ['relays', RELAY_URL], ], content: '', } const event = await this.publishEvent(zapRequest) if (!event) { throw new Error('Failed to create zap request') } return event } // Check if user has paid for an article by looking for zap receipts checkZapReceipt( targetPubkey: string, targetEventId: string, amount: number, userPubkey?: string ): Promise { if (!this.publicKey || !this.pool) { return Promise.resolve(false) } // Use provided userPubkey or fall back to current public key const checkPubkey = userPubkey ?? this.publicKey return checkZapReceiptHelper(this.pool, targetPubkey, targetEventId, amount, checkPubkey) } /** * Get the pool instance (for use by other services) */ getPool(): SimplePool | null { return this.pool } } export const nostrService = new NostrService()