- 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.
239 lines
7.3 KiB
TypeScript
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()
|