story-research-zapwall/lib/paymentPolling.ts
Nicolas Cantu f7bd7faa73 fix: Correction erreurs TypeScript, nettoyage et réorganisation documentation
- Correction toutes erreurs TypeScript :
  - Variables non utilisées supprimées
  - Types optionnels corrigés (exactOptionalPropertyTypes)
  - Imports corrigés (PLATFORM_BITCOIN_ADDRESS depuis platformConfig)
  - Gestion correcte des propriétés optionnelles

- Suppression fichiers obsolètes :
  - code-cleanup-summary.md (redondant)
  - todo-implementation*.md (todos obsolètes)
  - corrections-completed.md, fallbacks-found.md (corrections faites)
  - implementation-summary.md (redondant)
  - documentation-plan.md (plan, pas documentation)

- Suppression scripts temporaires :
  - add-ssh-key.sh
  - add-ssh-key-plink.sh

- Réorganisation documentation dans docs/ :
  - architecture.md (nouveau)
  - commissions.md (nouveau)
  - implementation-summary.md
  - remaining-tasks.md
  - split-and-transfer.md
  - commission-system.md
  - commission-implementation.md
  - content-delivery-verification.md

Toutes erreurs TypeScript corrigées, documentation centralisée.
2025-12-27 21:25:19 +01:00

266 lines
8.0 KiB
TypeScript

import { nostrService } from './nostr'
import { articlePublisher } from './articlePublisher'
import { getStoredPrivateContent } from './articleStorage'
import { platformTracking } from './platformTracking'
import { calculateArticleSplit } from './platformCommissions'
import { automaticTransferService } from './automaticTransfer'
import { lightningAddressService } from './lightningAddress'
const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io'
/**
* Poll for payment completion via zap receipt verification
* After payment is confirmed, sends private content to the user
*/
async function pollPaymentUntilDeadline(
articleId: string,
articlePubkey: string,
amount: number,
recipientPubkey: string,
interval: number,
deadline: number
): Promise<boolean> {
try {
const zapReceiptExists = await nostrService.checkZapReceipt(articlePubkey, articleId, amount, recipientPubkey)
if (zapReceiptExists) {
const zapReceiptId = await getZapReceiptId(articlePubkey, articleId, amount, recipientPubkey)
await sendPrivateContentAfterPayment(articleId, recipientPubkey, amount, zapReceiptId)
return true
}
} catch (error) {
console.error('Error checking zap receipt:', error)
}
if (Date.now() > deadline) {
return false
}
return new Promise<boolean>((resolve) => {
setTimeout(() => {
void pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline)
.then(resolve)
.catch(() => resolve(false))
}, interval)
})
}
export async function waitForArticlePayment(
_paymentHash: string,
articleId: string,
articlePubkey: string,
amount: number,
recipientPubkey: string,
timeout: number = 300000 // 5 minutes
): Promise<boolean> {
const interval = 2000
const deadline = Date.now() + timeout
try {
return await pollPaymentUntilDeadline(articleId, articlePubkey, amount, recipientPubkey, interval, deadline)
} catch (error) {
console.error('Wait for payment error:', error)
return false
}
}
async function getZapReceiptId(
articlePubkey: string,
articleId: string,
amount: number,
recipientPubkey: string
): Promise<string | undefined> {
try {
const pool = nostrService.getPool()
if (!pool) {
return undefined
}
const filters = [
{
kinds: [9735],
'#p': [articlePubkey],
'#e': [articleId],
limit: 1,
},
]
return new Promise((resolve) => {
let resolved = false
const poolWithSub = pool as import('@/types/nostr-tools-extended').SimplePoolWithSub
const sub = poolWithSub.sub([RELAY_URL], filters)
const finalize = (value: string | undefined) => {
if (resolved) {
return
}
resolved = true
sub.unsub()
resolve(value)
}
sub.on('event', (event) => {
const amountTag = event.tags.find((tag) => tag[0] === 'amount')?.[1]
const amountInSats = amountTag ? Math.floor(parseInt(amountTag, 10) / 1000) : 0
if (amountInSats === amount && event.pubkey === recipientPubkey) {
finalize(event.id)
}
})
sub.on('eose', () => finalize(undefined))
setTimeout(() => finalize(undefined), 3000)
})
} catch (error) {
console.error('Error getting zap receipt ID', {
articleId,
recipientPubkey,
error: error instanceof Error ? error.message : 'Unknown error',
})
return undefined
}
}
/**
* Send private content to user after payment confirmation
* Returns true if content was successfully sent and verified
*/
async function sendPrivateContentAfterPayment(
articleId: string,
recipientPubkey: string,
amount: number,
zapReceiptId?: string
): Promise<boolean> {
const storedContent = await getStoredPrivateContent(articleId)
if (!storedContent) {
console.error('Stored private content not found for article', {
articleId,
recipientPubkey,
timestamp: new Date().toISOString(),
})
return false
}
const authorPrivateKey = nostrService.getPrivateKey()
if (!authorPrivateKey) {
console.error('Author private key not available, cannot send private content automatically', {
articleId,
recipientPubkey,
authorPubkey: storedContent.authorPubkey,
timestamp: new Date().toISOString(),
})
return false
}
const result = await articlePublisher.sendPrivateContent(articleId, recipientPubkey, authorPrivateKey)
if (result.success && result.messageEventId) {
const timestamp = Math.floor(Date.now() / 1000)
// Verify payment amount matches expected commission structure
const expectedSplit = calculateArticleSplit()
if (amount !== expectedSplit.total) {
console.error('Payment amount does not match expected commission structure', {
articleId,
paidAmount: amount,
expectedTotal: expectedSplit.total,
expectedAuthor: expectedSplit.author,
expectedPlatform: expectedSplit.platform,
timestamp: new Date().toISOString(),
})
}
// Track content delivery with commission information
const trackingData: import('./platformTracking').ContentDeliveryTracking = {
articleId,
articlePubkey: storedContent.authorPubkey,
recipientPubkey,
messageEventId: result.messageEventId,
amount,
authorAmount: expectedSplit.author,
platformCommission: expectedSplit.platform,
timestamp,
verified: result.verified ?? false,
}
if (zapReceiptId) {
trackingData.zapReceiptId = zapReceiptId
}
await platformTracking.trackContentDelivery(trackingData, authorPrivateKey)
// Log commission information for platform tracking
console.log('Article payment processed with commission', {
articleId,
totalAmount: amount,
authorPortion: expectedSplit.author,
platformCommission: expectedSplit.platform,
recipientPubkey,
timestamp: new Date().toISOString(),
})
// Trigger automatic transfer of author portion
try {
// Get author's Lightning address from profile
const authorLightningAddress = await lightningAddressService.getLightningAddress(storedContent.authorPubkey)
if (authorLightningAddress) {
const transferResult = await automaticTransferService.transferAuthorPortion(
authorLightningAddress,
articleId,
storedContent.authorPubkey,
amount
)
if (!transferResult.success) {
console.warn('Automatic transfer failed, will be retried later', {
articleId,
authorPubkey: storedContent.authorPubkey,
error: transferResult.error,
timestamp: new Date().toISOString(),
})
}
} else {
console.warn('Author Lightning address not available for automatic transfer', {
articleId,
authorPubkey: storedContent.authorPubkey,
timestamp: new Date().toISOString(),
})
// Transfer will need to be done manually later
}
} catch (error) {
console.error('Error triggering automatic transfer', {
articleId,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
// Don't fail the payment process if transfer fails
}
if (result.verified) {
console.log('Private content sent and verified on relay', {
articleId,
recipientPubkey,
messageEventId: result.messageEventId,
timestamp: new Date().toISOString(),
})
return true
} else {
console.warn('Private content sent but not yet verified on relay', {
articleId,
recipientPubkey,
messageEventId: result.messageEventId,
timestamp: new Date().toISOString(),
})
return true
}
} else {
console.error('Failed to send private content, but payment was confirmed', {
articleId,
recipientPubkey,
error: result.error,
timestamp: new Date().toISOString(),
})
return false
}
}