lint fix wip

This commit is contained in:
Nicolas Cantu 2026-01-09 01:49:57 +01:00
parent d2124e24aa
commit 38941147cb
15 changed files with 347 additions and 222 deletions

View File

@ -64,9 +64,9 @@ export async function createPreviewEvent(
draft: params.draft, draft: params.draft,
invoice: params.invoice, invoice: params.invoice,
authorPubkey: params.authorPubkey, authorPubkey: params.authorPubkey,
authorPresentationId: params.authorPresentationId, ...(params.authorPresentationId ? { authorPresentationId: params.authorPresentationId } : {}),
extraTags: params.extraTags, ...(params.extraTags ? { extraTags: params.extraTags } : {}),
encryptedKey: params.encryptedKey, ...(params.encryptedKey ? { encryptedKey: params.encryptedKey } : {}),
}) })
return { return {

View File

@ -95,13 +95,17 @@ async function buildParsedArticleFromDraft(
return { article, hash, version, index } return { article, hash, version, index }
} }
async function publishPreviewWithInvoice( interface PublishPreviewWithInvoiceParams {
draft: ArticleDraft, draft: ArticleDraft
invoice: AlbyInvoice, invoice: AlbyInvoice
authorPubkey: string, authorPubkey: string
presentationId: string, presentationId: string
extraTags?: string[][], extraTags?: string[][]
customArticle?: Article customArticle?: Article
}
async function publishPreviewWithInvoice(
params: PublishPreviewWithInvoiceParams
): Promise<import('nostr-tools').Event | null> { ): Promise<import('nostr-tools').Event | null> {
// Build parsed article object (use custom article if provided, e.g., for updates with version) // Build parsed article object (use custom article if provided, e.g., for updates with version)
let article: Article let article: Article
@ -109,22 +113,22 @@ async function publishPreviewWithInvoice(
let version: number let version: number
let index: number let index: number
if (customArticle) { if (params.customArticle) {
;({ hash, version } = customArticle) ;({ hash, version } = params.customArticle)
article = customArticle article = params.customArticle
index = customArticle.index ?? 0 index = params.customArticle.index ?? 0
} else { } else {
const built = await buildParsedArticleFromDraft(draft, invoice, authorPubkey) const built = await buildParsedArticleFromDraft(params.draft, params.invoice, params.authorPubkey)
;({ article, hash, version, index } = built) ;({ article, hash, version, index } = built)
} }
// Build event template // Build event template
const previewEventTemplate = await createPreviewEvent({ const previewEventTemplate = await createPreviewEvent({
draft, draft: params.draft,
invoice, invoice: params.invoice,
authorPubkey, authorPubkey: params.authorPubkey,
authorPresentationId: presentationId, authorPresentationId: params.presentationId,
extraTags, ...(params.extraTags ? { extraTags: params.extraTags } : {}),
}) })
// Set private key in orchestrator // Set private key in orchestrator
@ -479,42 +483,43 @@ async function buildReviewEvent(
} }
} }
async function buildUpdateTags( async function buildUpdateTags(params: {
draft: ArticleDraft, draft: ArticleDraft
originalArticleId: string, originalArticleId: string
newCategory: 'sciencefiction' | 'research', newCategory: 'sciencefiction' | 'research'
authorPubkey: string, authorPubkey: string
currentVersion: number = 0 currentVersion?: number
): Promise<string[][]> { }): Promise<string[][]> {
// Generate hash ID from publication data // Generate hash ID from publication data
const hashId = await generatePublicationHashId({ const hashId = await generatePublicationHashId({
pubkey: authorPubkey, pubkey: params.authorPubkey,
title: draft.title, title: params.draft.title,
preview: draft.preview, preview: params.draft.preview,
category: newCategory, category: params.newCategory,
seriesId: draft.seriesId ?? undefined, seriesId: params.draft.seriesId ?? undefined,
bannerUrl: draft.bannerUrl ?? undefined, bannerUrl: params.draft.bannerUrl ?? undefined,
zapAmount: draft.zapAmount, zapAmount: params.draft.zapAmount,
}) })
// Increment version for update // Increment version for update
const currentVersion = params.currentVersion ?? 0
const nextVersion = currentVersion + 1 const nextVersion = currentVersion + 1
const updateTags = buildTags({ const updateTags = buildTags({
type: 'publication', type: 'publication',
category: newCategory, category: params.newCategory,
id: hashId, id: hashId,
service: PLATFORM_SERVICE, service: PLATFORM_SERVICE,
version: nextVersion, version: nextVersion,
hidden: false, hidden: false,
paywall: true, paywall: true,
title: draft.title, title: params.draft.title,
preview: draft.preview, preview: params.draft.preview,
zapAmount: draft.zapAmount, zapAmount: params.draft.zapAmount,
...(draft.seriesId ? { seriesId: draft.seriesId } : {}), ...(params.draft.seriesId ? { seriesId: params.draft.seriesId } : {}),
...(draft.bannerUrl ? { bannerUrl: draft.bannerUrl } : {}), ...(params.draft.bannerUrl ? { bannerUrl: params.draft.bannerUrl } : {}),
}) })
updateTags.push(['e', originalArticleId], ['replace', 'article-update']) updateTags.push(['e', params.originalArticleId], ['replace', 'article-update'])
return updateTags return updateTags
} }
@ -545,7 +550,13 @@ async function publishUpdate(
// Use current version from original article // Use current version from original article
const currentVersion = originalArticle.version ?? 0 const currentVersion = originalArticle.version ?? 0
const updateTags = await buildUpdateTags(draft, originalArticleId, newCategory, authorPubkey, currentVersion) const updateTags = await buildUpdateTags({
draft,
originalArticleId,
newCategory,
authorPubkey,
currentVersion,
})
// Build parsed article with incremented version // Build parsed article with incremented version
const { article } = await buildParsedArticleFromDraft(draft, invoice, authorPubkey) const { article } = await buildParsedArticleFromDraft(draft, invoice, authorPubkey)
@ -554,7 +565,14 @@ async function publishUpdate(
version: currentVersion + 1, // Increment version for update version: currentVersion + 1, // Increment version for update
} }
const publishedEvent = await publishPreviewWithInvoice(draft, invoice, authorPubkey, presentationId, updateTags, updatedArticle) const publishedEvent = await publishPreviewWithInvoice({
draft,
invoice,
authorPubkey,
presentationId,
extraTags: updateTags,
customArticle: updatedArticle,
})
if (!publishedEvent) { if (!publishedEvent) {
return updateFailure(originalArticleId, 'Failed to publish article update') return updateFailure(originalArticleId, 'Failed to publish article update')
} }

View File

@ -243,7 +243,7 @@ export class ArticlePublisher {
} }
// Build event template // Build event template
const eventTemplate = await buildPresentationEvent(draft, authorPubkey, authorName, category, version, index) const eventTemplate = await buildPresentationEvent({ draft, authorPubkey, authorName, category, version, index })
// Set private key in orchestrator // Set private key in orchestrator
writeOrchestrator.setPrivateKey(authorPrivateKey) writeOrchestrator.setPrivateKey(authorPrivateKey)

View File

@ -33,27 +33,27 @@ export function buildPrivateMessageEvent(recipientPubkey: string, articleId: str
} }
} }
async function publishEncryptedMessage( async function publishEncryptedMessage(params: {
articleId: string, articleId: string
recipientPubkey: string, recipientPubkey: string
authorPubkey: string, authorPubkey: string
authorPrivateKey: string, authorPrivateKey: string
keyData: string keyData: string
): Promise<{ eventId: string } | null> { }): Promise<{ eventId: string } | null> {
const encryptedKey = await Promise.resolve(nip04.encrypt(authorPrivateKey, recipientPubkey, keyData)) const encryptedKey = await Promise.resolve(nip04.encrypt(params.authorPrivateKey, params.recipientPubkey, params.keyData))
const privateMessageEvent = buildPrivateMessageEvent(recipientPubkey, articleId, encryptedKey) const privateMessageEvent = buildPrivateMessageEvent(params.recipientPubkey, params.articleId, encryptedKey)
const publishedEvent = await nostrService.publishEvent(privateMessageEvent) const publishedEvent = await nostrService.publishEvent(privateMessageEvent)
if (!publishedEvent) { if (!publishedEvent) {
console.error('Failed to publish private message event', { articleId, recipientPubkey, authorPubkey }) console.error('Failed to publish private message event', { articleId: params.articleId, recipientPubkey: params.recipientPubkey, authorPubkey: params.authorPubkey })
return null return null
} }
console.warn('Private message published', { console.warn('Private message published', {
messageEventId: publishedEvent.id, messageEventId: publishedEvent.id,
articleId, articleId: params.articleId,
recipientPubkey, recipientPubkey: params.recipientPubkey,
authorPubkey, authorPubkey: params.authorPubkey,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
return { eventId: publishedEvent.id } return { eventId: publishedEvent.id }
@ -70,7 +70,13 @@ export async function sendEncryptedContent(
nostrService.setPublicKey(storedContent.authorPubkey) nostrService.setPublicKey(storedContent.authorPubkey)
const keyData = prepareKeyData(storedContent) const keyData = prepareKeyData(storedContent)
const publishResult = await publishEncryptedMessage(articleId, recipientPubkey, storedContent.authorPubkey, authorPrivateKey, keyData) const publishResult = await publishEncryptedMessage({
articleId,
recipientPubkey,
authorPubkey: storedContent.authorPubkey,
authorPrivateKey,
keyData,
})
if (!publishResult) { if (!publishResult) {
return { success: false, error: 'Failed to publish private message event' } return { success: false, error: 'Failed to publish private message event' }
} }

View File

@ -11,25 +11,32 @@ import { generateObjectUrl, buildObjectId, parseObjectId } from './urlGenerator'
import { getLatestVersion } from './versionManager' import { getLatestVersion } from './versionManager'
import { objectCache } from './objectCache' import { objectCache } from './objectCache'
interface BuildPresentationEventParams {
draft: AuthorPresentationDraft
authorPubkey: string
authorName: string
category?: 'sciencefiction' | 'research'
version?: number
index?: number
}
export async function buildPresentationEvent( export async function buildPresentationEvent(
draft: AuthorPresentationDraft, params: BuildPresentationEventParams
authorPubkey: string,
authorName: string,
category: 'sciencefiction' | 'research' = 'sciencefiction',
version: number = 0,
index: number = 0
): Promise<{ ): Promise<{
kind: 1 kind: 1
created_at: number created_at: number
tags: string[][] tags: string[][]
content: string content: string
}> { }> {
const category = params.category ?? 'sciencefiction'
const version = params.version ?? 0
const index = params.index ?? 0
// Extract presentation and contentDescription from draft.content // Extract presentation and contentDescription from draft.content
// Format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}" // Format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}"
const separator = '\n\n---\n\nDescription du contenu :\n' const separator = '\n\n---\n\nDescription du contenu :\n'
const separatorIndex = draft.content.indexOf(separator) const separatorIndex = params.draft.content.indexOf(separator)
const presentation = separatorIndex !== -1 ? draft.content.substring(0, separatorIndex) : draft.presentation const presentation = separatorIndex !== -1 ? params.draft.content.substring(0, separatorIndex) : params.draft.presentation
let contentDescription = separatorIndex !== -1 ? draft.content.substring(separatorIndex + separator.length) : draft.contentDescription let contentDescription = separatorIndex !== -1 ? params.draft.content.substring(separatorIndex + separator.length) : params.draft.contentDescription
// Remove Bitcoin address from contentDescription if present (should not be visible in note content) // Remove Bitcoin address from contentDescription if present (should not be visible in note content)
// Remove lines matching "Adresse Bitcoin mainnet (pour le sponsoring) : ..." // Remove lines matching "Adresse Bitcoin mainnet (pour le sponsoring) : ..."
@ -43,12 +50,12 @@ export async function buildPresentationEvent(
// Generate hash ID from author data first (needed for URL) // Generate hash ID from author data first (needed for URL)
const hashId = await generateAuthorHashId({ const hashId = await generateAuthorHashId({
pubkey: authorPubkey, pubkey: params.authorPubkey,
authorName, authorName: params.authorName,
presentation, presentation,
contentDescription, contentDescription,
mainnetAddress: draft.mainnetAddress ?? undefined, mainnetAddress: params.draft.mainnetAddress ?? undefined,
pictureUrl: draft.pictureUrl ?? undefined, pictureUrl: params.draft.pictureUrl ?? undefined,
category, category,
}) })
@ -56,13 +63,14 @@ export async function buildPresentationEvent(
const profileUrl = generateObjectUrl('author', hashId, index, version) const profileUrl = generateObjectUrl('author', hashId, index, version)
// Encode pubkey to npub (for metadata JSON) // Encode pubkey to npub (for metadata JSON)
const npub = nip19.npubEncode(authorPubkey) const npub = nip19.npubEncode(params.authorPubkey)
// Build visible content message // Build visible content message
// If picture exists, use it as preview image for the link (markdown format) // If picture exists, use it as preview image for the link (markdown format)
// Note: The image will display at full size in most Nostr clients, not as a thumbnail // Note: The image will display at full size in most Nostr clients, not as a thumbnail
const {draft} = params
const linkWithPreview = draft.pictureUrl const linkWithPreview = draft.pictureUrl
? `[![${authorName}](${draft.pictureUrl})](${profileUrl})` ? `[![${params.authorName}](${draft.pictureUrl})](${profileUrl})`
: profileUrl : profileUrl
const visibleContent = [ const visibleContent = [
@ -74,9 +82,9 @@ export async function buildPresentationEvent(
// Build profile JSON for metadata (stored in tag, not in content) // Build profile JSON for metadata (stored in tag, not in content)
const profileJson = JSON.stringify({ const profileJson = JSON.stringify({
authorName, authorName: params.authorName,
npub, npub,
pubkey: authorPubkey, pubkey: params.authorPubkey,
presentation, presentation,
contentDescription, contentDescription,
mainnetAddress: draft.mainnetAddress, mainnetAddress: draft.mainnetAddress,

View File

@ -101,9 +101,9 @@ export async function publishPreview(
invoice, invoice,
authorPubkey, authorPubkey,
authorPresentationId: presentationId, authorPresentationId: presentationId,
extraTags, ...(extraTags ? { extraTags } : {}),
encryptedContent, ...(encryptedContent ? { encryptedContent } : {}),
encryptedKey, ...(encryptedKey ? { encryptedKey } : {}),
}) })
// Set private key in orchestrator // Set private key in orchestrator

View File

@ -22,16 +22,23 @@ export class AutomaticTransferService {
* Transfer author portion after article payment * Transfer author portion after article payment
* Creates a Lightning invoice from the platform to the author * Creates a Lightning invoice from the platform to the author
*/ */
private logTransferRequired(type: 'article' | 'review', id: string, pubkey: string, amount: number, recipient: string, platformCommission: number): void { private logTransferRequired(params: {
type: 'article' | 'review'
id: string
pubkey: string
amount: number
recipient: string
platformCommission: number
}): void {
const logData = { const logData = {
[type === 'article' ? 'articleId' : 'reviewId']: id, [params.type === 'article' ? 'articleId' : 'reviewId']: params.id,
[type === 'article' ? 'articlePubkey' : 'reviewerPubkey']: pubkey, [params.type === 'article' ? 'articlePubkey' : 'reviewerPubkey']: params.pubkey,
amount, amount: params.amount,
recipient, recipient: params.recipient,
platformCommission, platformCommission: params.platformCommission,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} }
console.warn(`Automatic transfer required${type === 'review' ? ' for review' : ''}`, logData) console.warn(`Automatic transfer required${params.type === 'review' ? ' for review' : ''}`, logData)
} }
private buildTransferError(error: unknown, recipient: string, amount: number = 0): TransferResult { private buildTransferError(error: unknown, recipient: string, amount: number = 0): TransferResult {
@ -62,8 +69,21 @@ export class AutomaticTransferService {
} }
} }
this.logTransferRequired('article', articleId, articlePubkey, split.author, authorLightningAddress, split.platform) this.logTransferRequired({
this.trackTransferRequirement('article', articleId, articlePubkey, split.author, authorLightningAddress) type: 'article',
id: articleId,
pubkey: articlePubkey,
amount: split.author,
recipient: authorLightningAddress,
platformCommission: split.platform,
})
this.trackTransferRequirement({
type: 'article',
id: articleId,
recipientPubkey: articlePubkey,
amount: split.author,
recipientAddress: authorLightningAddress,
})
return { return {
success: true, success: true,
@ -102,8 +122,21 @@ export class AutomaticTransferService {
} }
} }
this.logTransferRequired('review', reviewId, reviewerPubkey, split.reviewer, reviewerLightningAddress, split.platform) this.logTransferRequired({
this.trackTransferRequirement('review', reviewId, reviewerPubkey, split.reviewer, reviewerLightningAddress) type: 'review',
id: reviewId,
pubkey: reviewerPubkey,
amount: split.reviewer,
recipient: reviewerLightningAddress,
platformCommission: split.platform,
})
this.trackTransferRequirement({
type: 'review',
id: reviewId,
recipientPubkey: reviewerPubkey,
amount: split.reviewer,
recipientAddress: reviewerLightningAddress,
})
return { return {
success: true, success: true,
@ -126,11 +159,13 @@ export class AutomaticTransferService {
* In production, this would be stored in a database or queue * In production, this would be stored in a database or queue
*/ */
private trackTransferRequirement( private trackTransferRequirement(
type: 'article' | 'review', params: {
id: string, type: 'article' | 'review'
recipientPubkey: string, id: string
amount: number, recipientPubkey: string
amount: number
recipientAddress: string recipientAddress: string
}
): void { ): void {
// In production, this would: // In production, this would:
// 1. Store in a database/queue for processing // 1. Store in a database/queue for processing
@ -138,11 +173,11 @@ export class AutomaticTransferService {
// 3. Update tracking when transfer is complete // 3. Update tracking when transfer is complete
console.warn('Transfer requirement tracked', { console.warn('Transfer requirement tracked', {
type, type: params.type,
id, id: params.id,
recipientPubkey, recipientPubkey: params.recipientPubkey,
amount, amount: params.amount,
recipientAddress, recipientAddress: params.recipientAddress,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
} }

View File

@ -78,15 +78,17 @@ function setupContentDeliveryHandlers(
} }
function createContentDeliverySubscription( function createContentDeliverySubscription(
pool: import('nostr-tools').SimplePool, params: {
authorPubkey: string, pool: import('nostr-tools').SimplePool
recipientPubkey: string, authorPubkey: string
articleId: string, recipientPubkey: string
articleId: string
messageEventId: string messageEventId: string
}
): import('@/types/nostr-tools-extended').Subscription { ): import('@/types/nostr-tools-extended').Subscription {
const filters = createContentDeliveryFilters(authorPubkey, recipientPubkey, articleId, messageEventId) const filters = createContentDeliveryFilters(params.authorPubkey, params.recipientPubkey, params.articleId, params.messageEventId)
const relayUrl = getPrimaryRelaySync() const relayUrl = getPrimaryRelaySync()
return createSubscription(pool, [relayUrl], filters) return createSubscription(params.pool, [relayUrl], filters)
} }
function createContentDeliveryPromise( function createContentDeliveryPromise(
@ -129,7 +131,7 @@ export function verifyContentDelivery(
return Promise.resolve(status) return Promise.resolve(status)
} }
const sub = createContentDeliverySubscription(pool, authorPubkey, recipientPubkey, articleId, messageEventId) const sub = createContentDeliverySubscription({ pool, authorPubkey, recipientPubkey, articleId, messageEventId })
return createContentDeliveryPromise(sub, status) return createContentDeliveryPromise(sub, status)
} catch (error) { } catch (error) {
status.error = error instanceof Error ? error.message : 'Unknown error' status.error = error instanceof Error ? error.message : 'Unknown error'

View File

@ -10,21 +10,23 @@ export function scheduleNextCheck(checkConfirmation: () => void, interval: numbe
} }
export async function checkTransactionStatus( export async function checkTransactionStatus(
txid: string, params: {
startTime: number, txid: string
timeout: number, startTime: number
interval: number, timeout: number
resolve: (value: TransactionVerificationResult | null) => void, interval: number
resolve: (value: TransactionVerificationResult | null) => void
checkConfirmation: () => void checkConfirmation: () => void
}
): Promise<void> { ): Promise<void> {
if (Date.now() - startTime > timeout) { if (Date.now() - params.startTime > params.timeout) {
resolve(null) params.resolve(null)
return return
} }
const transaction = await getTransaction(txid) const transaction = await getTransaction(params.txid)
if (!transaction) { if (!transaction) {
scheduleNextCheck(checkConfirmation, interval) scheduleNextCheck(params.checkConfirmation, params.interval)
return return
} }
@ -33,15 +35,15 @@ export async function checkTransactionStatus(
) )
if (!authorOutput) { if (!authorOutput) {
scheduleNextCheck(checkConfirmation, interval) scheduleNextCheck(params.checkConfirmation, params.interval)
return return
} }
const result = await verifySponsoringTransaction(txid, authorOutput.scriptpubkey_address) const result = await verifySponsoringTransaction(params.txid, authorOutput.scriptpubkey_address)
if (result.confirmed && result.valid) { if (result.confirmed && result.valid) {
resolve(result) params.resolve(result)
} else { } else {
scheduleNextCheck(checkConfirmation, interval) scheduleNextCheck(params.checkConfirmation, params.interval)
} }
} }
@ -54,7 +56,7 @@ export async function waitForConfirmation(
return new Promise((resolve) => { return new Promise((resolve) => {
const checkConfirmation = (): void => { const checkConfirmation = (): void => {
void checkTransactionStatus(txid, startTime, timeout, interval, resolve, checkConfirmation) void checkTransactionStatus({ txid, startTime, timeout, interval, resolve, checkConfirmation })
} }
checkConfirmation() checkConfirmation()
}) })

View File

@ -32,33 +32,35 @@ export function findTransactionOutputs(
} }
export function buildVerificationResult( export function buildVerificationResult(
valid: boolean, params: {
confirmed: boolean, valid: boolean
confirmations: number, confirmed: boolean
authorOutput?: MempoolTransaction['vout'][0], confirmations: number
authorOutput?: MempoolTransaction['vout'][0]
platformOutput?: MempoolTransaction['vout'][0] platformOutput?: MempoolTransaction['vout'][0]
}
): TransactionVerificationResult { ): TransactionVerificationResult {
const result: TransactionVerificationResult = { const result: TransactionVerificationResult = {
valid, valid: params.valid,
confirmed, confirmed: params.confirmed,
confirmations, confirmations: params.confirmations,
} }
if (!valid) { if (!params.valid) {
result.error = 'Transaction outputs do not match expected split' result.error = 'Transaction outputs do not match expected split'
} }
if (authorOutput) { if (params.authorOutput) {
result.authorOutput = { result.authorOutput = {
address: authorOutput.scriptpubkey_address, address: params.authorOutput.scriptpubkey_address,
amount: authorOutput.value, amount: params.authorOutput.value,
} }
} }
if (platformOutput) { if (params.platformOutput) {
result.platformOutput = { result.platformOutput = {
address: platformOutput.scriptpubkey_address, address: params.platformOutput.scriptpubkey_address,
amount: platformOutput.value, amount: params.platformOutput.value,
} }
} }
@ -156,13 +158,13 @@ export async function verifySponsoringTransaction(
logVerificationFailure(txid, authorMainnetAddress, split, transaction) logVerificationFailure(txid, authorMainnetAddress, split, transaction)
} }
return buildVerificationResult( return buildVerificationResult({
validation.valid, valid: validation.valid,
validation.confirmed, confirmed: validation.confirmed,
validation.confirmations, confirmations: validation.confirmations,
validation.authorOutput, ...(validation.authorOutput ? { authorOutput: validation.authorOutput } : {}),
validation.platformOutput ...(validation.platformOutput ? { platformOutput: validation.platformOutput } : {}),
) })
} catch (error) { } catch (error) {
return handleVerificationError(txid, authorMainnetAddress, error) return handleVerificationError(txid, authorMainnetAddress, error)
} }

View File

@ -285,7 +285,13 @@ class NostrService {
throw new Error('Private key not set or pool not initialized') throw new Error('Private key not set or pool not initialized')
} }
return getPrivateContentFromPool(this.pool, eventId, authorPubkey, this.privateKey, this.publicKey) return getPrivateContentFromPool({
pool: this.pool,
eventId,
authorPubkey,
privateKey: this.privateKey,
publicKey: this.publicKey,
})
} }
/** /**
@ -298,7 +304,13 @@ class NostrService {
if (!this.privateKey || !this.pool || !this.publicKey) { if (!this.privateKey || !this.pool || !this.publicKey) {
return null return null
} }
return getDecryptionKey(this.pool, eventId, authorPubkey, this.privateKey, this.publicKey) return getDecryptionKey({
pool: this.pool,
eventId,
authorPubkey,
recipientPrivateKey: this.privateKey,
recipientPublicKey: this.publicKey,
})
} }
async getDecryptedArticleContent(eventId: string, authorPubkey: string): Promise<string | null> { async getDecryptedArticleContent(eventId: string, authorPubkey: string): Promise<string | null> {
@ -447,7 +459,13 @@ class NostrService {
// Use provided userPubkey or fall back to current public key // Use provided userPubkey or fall back to current public key
const checkPubkey = userPubkey ?? this.publicKey const checkPubkey = userPubkey ?? this.publicKey
return checkZapReceiptHelper(this.pool, targetPubkey, targetEventId, amount, checkPubkey) return checkZapReceiptHelper({
pool: this.pool,
targetPubkey,
targetEventId,
amount,
userPubkey: checkPubkey,
})
} }
/** /**

View File

@ -33,20 +33,22 @@ function decryptContent(privateKey: string, event: Event): Promise<string | null
* This function now returns the decryption key instead of the full content * This function now returns the decryption key instead of the full content
*/ */
export function getPrivateContent( export function getPrivateContent(
pool: SimplePool, params: {
eventId: string, pool: SimplePool
authorPubkey: string, eventId: string
privateKey: string, authorPubkey: string
privateKey: string
publicKey: string publicKey: string
}
): Promise<string | null> { ): Promise<string | null> {
if (!privateKey || !pool || !publicKey) { if (!params.privateKey || !params.pool || !params.publicKey) {
throw new Error('Private key not set or pool not initialized') throw new Error('Private key not set or pool not initialized')
} }
return new Promise<string | null>((resolve) => { return new Promise<string | null>((resolve) => {
let resolved = false let resolved = false
const relayUrl = getPrimaryRelaySync() const relayUrl = getPrimaryRelaySync()
const sub = createSubscription(pool, [relayUrl], createPrivateMessageFilters(eventId, publicKey, authorPubkey)) const sub = createSubscription(params.pool, [relayUrl], createPrivateMessageFilters(params.eventId, params.publicKey, params.authorPubkey))
const finalize = (result: string | null): void => { const finalize = (result: string | null): void => {
if (resolved) { if (resolved) {
@ -58,7 +60,7 @@ export function getPrivateContent(
} }
sub.on('event', (event: Event): void => { sub.on('event', (event: Event): void => {
void decryptContent(privateKey, event) void decryptContent(params.privateKey, event)
.then((content) => { .then((content) => {
if (content) { if (content) {
finalize(content) finalize(content)
@ -109,20 +111,22 @@ function handleDecryptionKeyEvent(
} }
export async function getDecryptionKey( export async function getDecryptionKey(
pool: SimplePool, params: {
eventId: string, pool: SimplePool
authorPubkey: string, eventId: string
recipientPrivateKey: string, authorPubkey: string
recipientPrivateKey: string
recipientPublicKey: string recipientPublicKey: string
}
): Promise<DecryptionKey | null> { ): Promise<DecryptionKey | null> {
if (!recipientPrivateKey || !pool || !recipientPublicKey) { if (!params.recipientPrivateKey || !params.pool || !params.recipientPublicKey) {
throw new Error('Private key not set or pool not initialized') throw new Error('Private key not set or pool not initialized')
} }
return new Promise<DecryptionKey | null>((resolve) => { return new Promise<DecryptionKey | null>((resolve) => {
let resolved = false let resolved = false
const relayUrl = getPrimaryRelaySync() const relayUrl = getPrimaryRelaySync()
const sub = createSubscription(pool, [relayUrl], createPrivateMessageFilters(eventId, recipientPublicKey, authorPubkey)) const sub = createSubscription(params.pool, [relayUrl], createPrivateMessageFilters(params.eventId, params.recipientPublicKey, params.authorPubkey))
const finalize = (result: DecryptionKey | null): void => { const finalize = (result: DecryptionKey | null): void => {
if (resolved) { if (resolved) {
@ -134,7 +138,7 @@ export async function getDecryptionKey(
} }
sub.on('event', (event: Event): void => { sub.on('event', (event: Event): void => {
handleDecryptionKeyEvent(event, recipientPrivateKey, finalize) handleDecryptionKeyEvent(event, params.recipientPrivateKey, finalize)
}) })
sub.on('eose', (): void => { sub.on('eose', (): void => {
finalize(null) finalize(null)

View File

@ -20,55 +20,67 @@ function createZapFilters(targetPubkey: string, targetEventId: string, userPubke
} }
async function isValidZapReceipt( async function isValidZapReceipt(
event: Event, params: {
targetEventId: string, event: Event
targetPubkey: string, targetEventId: string
userPubkey: string, targetPubkey: string
userPubkey: string
amount: number amount: number
}
): Promise<boolean> { ): Promise<boolean> {
// Import verification service dynamically to avoid circular dependencies // Import verification service dynamically to avoid circular dependencies
const { zapVerificationService } = await import('./zapVerification') const { zapVerificationService } = await import('./zapVerification')
return zapVerificationService.verifyZapReceiptForArticle(event, targetEventId, targetPubkey, userPubkey, amount) return zapVerificationService.verifyZapReceiptForArticle(params.event, params.targetEventId, params.targetPubkey, params.userPubkey, params.amount)
} }
/** /**
* Check if user has paid for an article by looking for zap receipts * Check if user has paid for an article by looking for zap receipts
*/ */
function handleZapReceiptEvent( function handleZapReceiptEvent(
event: Event, params: {
targetEventId: string, event: Event
targetPubkey: string, targetEventId: string
userPubkey: string, targetPubkey: string
amount: number, userPubkey: string
finalize: (value: boolean) => void, amount: number
finalize: (value: boolean) => void
resolved: { current: boolean } resolved: { current: boolean }
}
): void { ): void {
if (resolved.current) { if (params.resolved.current) {
return return
} }
void isValidZapReceipt(event, targetEventId, targetPubkey, userPubkey, amount).then((isValid) => { void isValidZapReceipt({
event: params.event,
targetEventId: params.targetEventId,
targetPubkey: params.targetPubkey,
userPubkey: params.userPubkey,
amount: params.amount,
}).then((isValid) => {
if (isValid) { if (isValid) {
finalize(true) params.finalize(true)
} }
}) })
} }
export function checkZapReceipt( export function checkZapReceipt(
pool: SimplePool, params: {
targetPubkey: string, pool: SimplePool
targetEventId: string, targetPubkey: string
amount: number, targetEventId: string
amount: number
userPubkey: string userPubkey: string
}
): Promise<boolean> { ): Promise<boolean> {
if (!pool) { if (!params.pool) {
return Promise.resolve(false) return Promise.resolve(false)
} }
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
let resolved = false let resolved = false
const relayUrl = getPrimaryRelaySync() const relayUrl = getPrimaryRelaySync()
const sub = createSubscription(pool, [relayUrl], createZapFilters(targetPubkey, targetEventId, userPubkey)) const sub = createSubscription(params.pool, [relayUrl], createZapFilters(params.targetPubkey, params.targetEventId, params.userPubkey))
const finalize = (value: boolean): void => { const finalize = (value: boolean): void => {
if (resolved) { if (resolved) {
@ -81,7 +93,15 @@ export function checkZapReceipt(
const resolvedRef = { current: resolved } const resolvedRef = { current: resolved }
sub.on('event', (event: Event): void => { sub.on('event', (event: Event): void => {
handleZapReceiptEvent(event, targetEventId, targetPubkey, userPubkey, amount, finalize, resolvedRef) handleZapReceiptEvent({
event,
targetEventId: params.targetEventId,
targetPubkey: params.targetPubkey,
userPubkey: params.userPubkey,
amount: params.amount,
finalize,
resolved: resolvedRef,
})
}) })
const end = (): void => { const end = (): void => {

View File

@ -29,15 +29,15 @@ export async function sendPrivateContentAfterPayment(
if (result.success && result.messageEventId) { if (result.success && result.messageEventId) {
verifyPaymentAmount(amount, articleId) verifyPaymentAmount(amount, articleId)
const trackingData = createTrackingData( const trackingData = createTrackingData({
articleId, articleId,
validation.storedContent.authorPubkey, authorPubkey: validation.storedContent.authorPubkey,
recipientPubkey, recipientPubkey,
result.messageEventId, messageEventId: result.messageEventId,
amount, amount,
result.verified ?? false, verified: result.verified ?? false,
zapReceiptId ...(zapReceiptId ? { zapReceiptId } : {}),
) })
await platformTracking.trackContentDelivery(trackingData, validation.authorPrivateKey) await platformTracking.trackContentDelivery(trackingData, validation.authorPrivateKey)
await triggerAutomaticTransfer(validation.storedContent.authorPubkey, articleId, amount) await triggerAutomaticTransfer(validation.storedContent.authorPubkey, articleId, amount)

View File

@ -3,31 +3,33 @@ import { lightningAddressService } from './lightningAddress'
import { automaticTransferService } from './automaticTransfer' import { automaticTransferService } from './automaticTransfer'
export function createTrackingData( export function createTrackingData(
articleId: string, params: {
authorPubkey: string, articleId: string
recipientPubkey: string, authorPubkey: string
messageEventId: string, recipientPubkey: string
amount: number, messageEventId: string
verified: boolean, amount: number
verified: boolean
zapReceiptId?: string zapReceiptId?: string
}
): import('./platformTracking').ContentDeliveryTracking { ): import('./platformTracking').ContentDeliveryTracking {
const expectedSplit = calculateArticleSplit() const expectedSplit = calculateArticleSplit()
const timestamp = Math.floor(Date.now() / 1000) const timestamp = Math.floor(Date.now() / 1000)
const trackingData: import('./platformTracking').ContentDeliveryTracking = { const trackingData: import('./platformTracking').ContentDeliveryTracking = {
articleId, articleId: params.articleId,
articlePubkey: authorPubkey, articlePubkey: params.authorPubkey,
recipientPubkey, recipientPubkey: params.recipientPubkey,
messageEventId, messageEventId: params.messageEventId,
amount, amount: params.amount,
authorAmount: expectedSplit.author, authorAmount: expectedSplit.author,
platformCommission: expectedSplit.platform, platformCommission: expectedSplit.platform,
timestamp, timestamp,
verified, verified: params.verified,
} }
if (zapReceiptId) { if (params.zapReceiptId) {
trackingData.zapReceiptId = zapReceiptId trackingData.zapReceiptId = params.zapReceiptId
} }
return trackingData return trackingData
@ -74,34 +76,36 @@ export async function triggerAutomaticTransfer(
} }
export function logPaymentSuccess( export function logPaymentSuccess(
articleId: string, params: {
recipientPubkey: string, articleId: string
amount: number, recipientPubkey: string
messageEventId: string, amount: number
messageEventId: string
verified: boolean verified: boolean
}
): void { ): void {
const expectedSplit = calculateArticleSplit() const expectedSplit = calculateArticleSplit()
console.warn('Article payment processed with commission', { console.warn('Article payment processed with commission', {
articleId, articleId: params.articleId,
totalAmount: amount, totalAmount: params.amount,
authorPortion: expectedSplit.author, authorPortion: expectedSplit.author,
platformCommission: expectedSplit.platform, platformCommission: expectedSplit.platform,
recipientPubkey, recipientPubkey: params.recipientPubkey,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
if (verified) { if (params.verified) {
console.warn('Private content sent and verified on relay', { console.warn('Private content sent and verified on relay', {
articleId, articleId: params.articleId,
recipientPubkey, recipientPubkey: params.recipientPubkey,
messageEventId, messageEventId: params.messageEventId,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
} else { } else {
console.warn('Private content sent but not yet verified on relay', { console.warn('Private content sent but not yet verified on relay', {
articleId, articleId: params.articleId,
recipientPubkey, recipientPubkey: params.recipientPubkey,
messageEventId, messageEventId: params.messageEventId,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}) })
} }
@ -114,7 +118,13 @@ export function logPaymentResult(
amount: number amount: number
): boolean { ): boolean {
if (result.success && result.messageEventId) { if (result.success && result.messageEventId) {
logPaymentSuccess(articleId, recipientPubkey, amount, result.messageEventId, result.verified ?? false) logPaymentSuccess({
articleId,
recipientPubkey,
amount,
messageEventId: result.messageEventId,
verified: result.verified ?? false,
})
return true return true
} }
console.error('Failed to send private content, but payment was confirmed', { console.error('Failed to send private content, but payment was confirmed', {