story-research-zapwall/lib/sponsoringPayment.ts
Nicolas Cantu 4735ee71ab feat: Complétion système split et intégrations externes
- Intégration mempool.space pour vérification transactions Bitcoin :
  - Service MempoolSpaceService avec API mempool.space
  - Vérification sorties et montants pour sponsoring
  - Vérification confirmations
  - Attente confirmation avec polling

- Récupération adresses Lightning depuis profils Nostr :
  - Service LightningAddressService
  - Support lud16 et lud06 (NIP-19)
  - Cache avec TTL 1 heure
  - Intégré dans paymentPolling et reviewReward

- Mise à jour événements Nostr pour avis rémunérés :
  - Publication événement avec tags rewarded et reward_amount
  - Parsing tags dans parseReviewFromEvent
  - Vérification doublons

- Tracking sponsoring sur Nostr :
  - Service SponsoringTrackingService
  - Événements avec commissions et confirmations
  - Intégration vérification mempool.space

Toutes les fonctionnalités de split sont maintenant opérationnelles.
Seuls les transferts Lightning réels nécessitent un nœud Lightning.
2025-12-27 21:18:14 +01:00

239 lines
7.3 KiB
TypeScript

import { calculateSponsoringSplit, PLATFORM_COMMISSIONS, PLATFORM_BITCOIN_ADDRESS } from './platformCommissions'
import { mempoolSpaceService } from './mempoolSpace'
import { sponsoringTrackingService } from './sponsoringTracking'
/**
* Sponsoring payment service
* Handles Bitcoin mainnet payments for sponsoring with automatic commission split
*
* Sponsoring: 0.046 BTC total
* - Author: 0.042 BTC (4,200,000 sats)
* - Platform: 0.004 BTC (400,000 sats)
*
* Since Bitcoin mainnet doesn't support automatic split like Lightning,
* we use a two-output transaction approach:
* 1. User creates transaction with two outputs (author + platform)
* 2. Platform verifies both outputs are present
* 3. Transaction is broadcast
*/
export interface SponsoringPaymentRequest {
authorPubkey: string
authorMainnetAddress: string
amount: number // Should be 0.046 BTC
}
export interface SponsoringPaymentResult {
success: boolean
transactionId?: string
error?: string
split: {
author: number
platform: number
total: number
authorSats: number
platformSats: number
totalSats: number
}
platformAddress: string
authorAddress: string
}
export class SponsoringPaymentService {
/**
* Create sponsoring payment request with split information
* Returns addresses and amounts for the user to create a Bitcoin transaction
*/
async createSponsoringPayment(request: SponsoringPaymentRequest): Promise<SponsoringPaymentResult> {
try {
// Verify amount matches expected commission structure
if (request.amount !== PLATFORM_COMMISSIONS.sponsoring.total) {
return {
success: false,
error: `Invalid sponsoring amount: ${request.amount} BTC. Expected ${PLATFORM_COMMISSIONS.sponsoring.total} BTC`,
split: calculateSponsoringSplit(),
platformAddress: PLATFORM_BITCOIN_ADDRESS,
authorAddress: request.authorMainnetAddress,
}
}
const split = calculateSponsoringSplit(request.amount)
// Verify addresses are valid Bitcoin addresses
if (!this.isValidBitcoinAddress(request.authorMainnetAddress)) {
return {
success: false,
error: 'Invalid author Bitcoin address',
split,
platformAddress: PLATFORM_BITCOIN_ADDRESS,
authorAddress: request.authorMainnetAddress,
}
}
console.log('Sponsoring payment request created', {
authorPubkey: request.authorPubkey,
authorAddress: request.authorMainnetAddress,
platformAddress: PLATFORM_BITCOIN_ADDRESS,
authorAmount: split.authorSats,
platformAmount: split.platformSats,
totalAmount: split.totalSats,
timestamp: new Date().toISOString(),
})
return {
success: true,
split,
platformAddress: PLATFORM_BITCOIN_ADDRESS,
authorAddress: request.authorMainnetAddress,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error creating sponsoring payment', {
authorPubkey: request.authorPubkey,
error: errorMessage,
timestamp: new Date().toISOString(),
})
return {
success: false,
error: errorMessage,
split: calculateSponsoringSplit(),
platformAddress: PLATFORM_BITCOIN_ADDRESS,
authorAddress: request.authorMainnetAddress,
}
}
}
/**
* Verify sponsoring payment transaction
* Checks that transaction has correct outputs for both author and platform
* Uses mempool.space API to verify the transaction
*/
async verifySponsoringPayment(
transactionId: string,
authorPubkey: string,
authorMainnetAddress: string
): Promise<boolean> {
try {
const verification = await mempoolSpaceService.verifySponsoringTransaction(
transactionId,
authorMainnetAddress
)
if (!verification.valid) {
console.error('Sponsoring payment verification failed', {
transactionId,
authorPubkey,
authorAddress: authorMainnetAddress,
error: verification.error,
timestamp: new Date().toISOString(),
})
return false
}
if (!verification.confirmed) {
console.warn('Sponsoring payment not yet confirmed', {
transactionId,
authorPubkey,
confirmations: verification.confirmations,
timestamp: new Date().toISOString(),
})
// Return true even if not confirmed - can be checked later
}
console.log('Sponsoring payment verified', {
transactionId,
authorPubkey,
authorAddress: authorMainnetAddress,
authorAmount: verification.authorOutput?.amount,
platformAmount: verification.platformOutput?.amount,
confirmed: verification.confirmed,
confirmations: verification.confirmations,
timestamp: new Date().toISOString(),
})
return true
} catch (error) {
console.error('Error verifying sponsoring payment', {
transactionId,
authorPubkey,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
return false
}
}
/**
* Track sponsoring payment
*/
async trackSponsoringPayment(
transactionId: string,
authorPubkey: string,
authorMainnetAddress: string,
authorPrivateKey: string
): Promise<void> {
try {
const split = calculateSponsoringSplit()
// Verify transaction first
const verification = await mempoolSpaceService.verifySponsoringTransaction(
transactionId,
authorMainnetAddress
)
if (!verification.valid) {
console.error('Cannot track invalid sponsoring payment', {
transactionId,
authorPubkey,
error: verification.error,
timestamp: new Date().toISOString(),
})
return
}
// Track the sponsoring payment on Nostr
await sponsoringTrackingService.trackSponsoringPayment(
{
transactionId,
authorPubkey,
authorMainnetAddress,
amount: split.total,
authorAmount: split.authorSats,
platformCommission: split.platformSats,
timestamp: Math.floor(Date.now() / 1000),
confirmed: verification.confirmed,
confirmations: verification.confirmations,
},
authorPrivateKey
)
console.log('Sponsoring payment tracked', {
transactionId,
authorPubkey,
authorAmount: split.authorSats,
platformCommission: split.platformSats,
confirmed: verification.confirmed,
confirmations: verification.confirmations,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error tracking sponsoring payment', {
transactionId,
authorPubkey,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
}
}
/**
* Validate Bitcoin address format
*/
private isValidBitcoinAddress(address: string): boolean {
// Basic validation: starts with 1, 3, or bc1
const bitcoinAddressRegex = /^(1|3|bc1)[a-zA-Z0-9]{25,62}$/
return bitcoinAddressRegex.test(address)
}
}
export const sponsoringPaymentService = new SponsoringPaymentService()