import { finalizeEvent, nip19, SimplePool, type Event, type EventTemplate, type Filter } from 'nostr-tools' import { hexToBytes } from 'nostr-tools/utils' import type { Article, NostrProfile } from '@/types/nostr' import type { PublishResult } from '../publishResult' import { checkZapReceipt as checkZapReceiptHelper } from '../nostrZapVerification' import { createArticleSubscription, parseArticleOrPresentationFromEvent } from './articles' import { getCachedArticleById } from './cache' import { getDecryptedArticleContent, getPrivateContent } from './decryption' import { buildImmediatePublishStatuses, buildUnsignedEventTemplate, publishEventNonBlocking } from './publish' 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(): void { this.pool = new SimplePool() } setPrivateKey(privateKey: string): void { this.privateKey = privateKey try { const decoded = nip19.decode(privateKey) if (decoded.type === 'nsec' && typeof decoded.data === 'string') { this.privateKey = decoded.data } } catch { // Assume it's already a hex string } } getPrivateKey(): string | null { return this.privateKey } getPublicKey(): string | null { return this.publicKey } setPublicKey(publicKey: string): void { this.publicKey = publicKey try { const decoded = nip19.decode(publicKey) if (decoded.type === 'npub' && typeof decoded.data === 'string') { this.publicKey = decoded.data } } catch { // Assume it's already a hex string } } async publishEvent(eventTemplate: EventTemplate): Promise async publishEvent(eventTemplate: EventTemplate, returnStatus: false): Promise async publishEvent(eventTemplate: EventTemplate, returnStatus: true): Promise async publishEvent(eventTemplate: EventTemplate, returnStatus: boolean = false): Promise { if (!this.privateKey || !this.pool) { throw new Error('Private key not set or pool not initialized') } const unsignedEvent = buildUnsignedEventTemplate(eventTemplate) const event = finalizeEvent(unsignedEvent, hexToBytes(this.privateKey)) const { relaySessionManager } = await import('../relaySessionManager') const activeRelays = await relaySessionManager.getActiveRelays() void publishEventNonBlocking({ pool: this.pool, event, activeRelays, relaySessionManager }) if (returnStatus) { const relayStatuses = await buildImmediatePublishStatuses(activeRelays) return { event, relayStatuses } } return event } 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 sub = createArticleSubscription({ pool: this.pool, limit }) sub.on('event', (event: Event): void => { void (async (): Promise => { const parsed = await parseArticleOrPresentationFromEvent(event) if (parsed) { callback(parsed) } })() }) return (): void => sub.unsub() } async getArticleById(eventId: string): Promise
{ return getCachedArticleById(eventId) } 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 getPrivateContent({ pool: this.pool, eventId, authorPubkey, privateKey: this.privateKey, publicKey: this.publicKey }) } async getDecryptedArticleContent(eventId: string, authorPubkey: string): Promise { if (!this.privateKey || !this.pool || !this.publicKey) { throw new Error('Private key not set or pool not initialized') } return getDecryptedArticleContent({ pool: this.pool, eventId, authorPubkey, privateKey: this.privateKey, publicKey: this.publicKey }) } async getProfile(pubkey: string): Promise { if (!this.pool) { return null } const { getPrimaryRelaySync } = await import('../config') const { createSubscription } = await import('@/types/nostr-tools-extended') const relayUrl = getPrimaryRelaySync() const filters: Filter[] = [{ kinds: [0], authors: [pubkey] }] const sub = createSubscription(this.pool, [relayUrl], filters) return subscribeToProfile({ sub, pubkey }) } async createZapRequest(targetPubkey: string, targetEventId: string, amount: number, extraTags: string[][] = []): Promise { if (!this.privateKey) { throw new Error('Private key not set') } const { getPrimaryRelay } = await import('../config') const relayUrl = await getPrimaryRelay() const zapRequest: EventTemplate = { kind: 9734, created_at: Math.floor(Date.now() / 1000), tags: [['p', targetPubkey], ['e', targetEventId], ['amount', amount.toString()], ['relays', relayUrl], ...extraTags], content: '', } const event = await this.publishEvent(zapRequest) if (!event) { throw new Error('Failed to create zap request') } return event } checkZapReceipt(targetPubkey: string, targetEventId: string, amount: number, userPubkey?: string): Promise { if (!this.publicKey || !this.pool) { return Promise.resolve(false) } return checkZapReceiptHelper({ pool: this.pool, targetPubkey, targetEventId, amount, userPubkey: userPubkey ?? this.publicKey }) } getPool(): SimplePool | null { return this.pool } } async function subscribeToProfile(params: { sub: ReturnType; pubkey: string }): Promise { return new Promise((resolve) => { const resolved = { value: false } let timeoutId: ReturnType | null = null const cleanup = (): void => { if (timeoutId) { clearTimeout(timeoutId) } params.sub.unsub() } const resolveOnce = (value: NostrProfile | null): void => { if (resolved.value) { return } resolved.value = true cleanup() resolve(value) } params.sub.on('event', (event: Event): void => { try { const parsed = parseProfileEvent(event, params.pubkey) if (parsed) { resolveOnce(parsed) } } catch (e) { console.error('Error parsing profile event:', e) } }) params.sub.on('eose', (): void => { resolveOnce(null) }) timeoutId = setTimeout(() => { resolveOnce(null) }, 5000) }) } function parseProfileEvent(event: Event, pubkey: string): NostrProfile | null { if (event.kind !== 0) { return null } if (event.pubkey !== pubkey) { return null } try { const parsed = JSON.parse(event.content) as Record const profile: NostrProfile = { pubkey } if (typeof parsed.name === 'string') { profile.name = parsed.name } if (typeof parsed.about === 'string') { profile.about = parsed.about } if (typeof parsed.picture === 'string') { profile.picture = parsed.picture } if (typeof parsed.nip05 === 'string') { profile.nip05 = parsed.nip05 } if (typeof parsed.lud16 === 'string') { profile.lud16 = parsed.lud16 } if (typeof parsed.lud06 === 'string') { profile.lud06 = parsed.lud06 } return profile } catch (e) { console.error('Error parsing profile JSON:', e) return null } } export const nostrService = new NostrService()