- 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.
266 lines
8.0 KiB
TypeScript
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
|
|
}
|
|
}
|