import { finalizeEvent, nip19, SimplePool, type Event, type EventTemplate } 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 { return null } async updateProfile(updates: Partial): Promise { if (!this.privateKey || !this.publicKey) { throw new Error('Private key and public key must be set to update profile') } const existingProfile = await this.getProfile(this.publicKey) const currentProfile: NostrProfile = existingProfile ?? { pubkey: this.publicKey } const updatedProfile: NostrProfile = { ...currentProfile, ...updates, pubkey: this.publicKey } const profileEvent: EventTemplate = { kind: 0, created_at: Math.floor(Date.now() / 1000), tags: [], content: JSON.stringify({ name: updatedProfile.name, about: updatedProfile.about, picture: updatedProfile.picture, nip05: updatedProfile.nip05, lud16: updatedProfile.lud16, lud06: updatedProfile.lud06, }), } await this.publishEvent(profileEvent) } 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 } } export const nostrService = new NostrService()