story-research-zapwall/lib/mempoolSpace.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

227 lines
6.3 KiB
TypeScript

import { calculateSponsoringSplit, PLATFORM_BITCOIN_ADDRESS } from './platformCommissions'
const MEMPOOL_API_BASE = 'https://mempool.space/api'
export interface MempoolTransaction {
txid: string
vout: Array<{
value: number // in sats
scriptpubkey_address: string
}>
status: {
confirmed: boolean
block_height?: number
block_hash?: string
}
}
export interface TransactionVerificationResult {
valid: boolean
confirmed: boolean
confirmations: number
authorOutput?: {
address: string
amount: number
}
platformOutput?: {
address: string
amount: number
}
error?: string
}
/**
* Mempool.space API service
* Used to verify Bitcoin mainnet transactions for sponsoring payments
*/
export class MempoolSpaceService {
/**
* Fetch transaction from mempool.space
*/
async getTransaction(txid: string): Promise<MempoolTransaction | null> {
try {
const response = await fetch(`${MEMPOOL_API_BASE}/tx/${txid}`)
if (!response.ok) {
if (response.status === 404) {
console.warn('Transaction not found on mempool.space', { txid })
return null
}
throw new Error(`Failed to fetch transaction: ${response.status} ${response.statusText}`)
}
const transaction = await response.json() as MempoolTransaction
return transaction
} catch (error) {
console.error('Error fetching transaction from mempool.space', {
txid,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
})
return null
}
}
/**
* Verify sponsoring payment transaction
* Checks that transaction has correct outputs for both author and platform
*/
async verifySponsoringTransaction(
txid: string,
authorMainnetAddress: string
): Promise<TransactionVerificationResult> {
try {
const transaction = await this.getTransaction(txid)
if (!transaction) {
return {
valid: false,
confirmed: false,
confirmations: 0,
error: 'Transaction not found',
}
}
const split = calculateSponsoringSplit()
const expectedAuthorAmount = split.authorSats
const expectedPlatformAmount = split.platformSats
// Find outputs matching expected addresses and amounts
const authorOutput = transaction.vout.find(
(output) =>
output.scriptpubkey_address === authorMainnetAddress &&
output.value === expectedAuthorAmount
)
const platformOutput = transaction.vout.find(
(output) =>
output.scriptpubkey_address === PLATFORM_BITCOIN_ADDRESS &&
output.value === expectedPlatformAmount
)
const valid = Boolean(authorOutput && platformOutput)
const confirmed = transaction.status.confirmed
const confirmations = confirmed && transaction.status.block_height
? await this.getConfirmations(transaction.status.block_height)
: 0
if (!valid) {
console.error('Transaction verification failed', {
txid,
authorAddress: authorMainnetAddress,
platformAddress: PLATFORM_BITCOIN_ADDRESS,
expectedAuthorAmount,
expectedPlatformAmount,
actualOutputs: transaction.vout.map((o) => ({
address: o.scriptpubkey_address,
amount: o.value,
})),
timestamp: new Date().toISOString(),
})
}
return {
valid,
confirmed,
confirmations,
authorOutput: authorOutput
? {
address: authorOutput.scriptpubkey_address,
amount: authorOutput.value,
}
: undefined,
platformOutput: platformOutput
? {
address: platformOutput.scriptpubkey_address,
amount: platformOutput.value,
}
: undefined,
error: valid ? undefined : 'Transaction outputs do not match expected split',
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error verifying sponsoring transaction', {
txid,
authorAddress: authorMainnetAddress,
error: errorMessage,
timestamp: new Date().toISOString(),
})
return {
valid: false,
confirmed: false,
confirmations: 0,
error: errorMessage,
}
}
}
/**
* Get current block height and calculate confirmations
*/
private async getConfirmations(blockHeight: number): Promise<number> {
try {
const response = await fetch(`${MEMPOOL_API_BASE}/blocks/tip/height`)
if (!response.ok) {
return 0
}
const currentHeight = await response.json() as number
return Math.max(0, currentHeight - blockHeight + 1)
} catch (error) {
console.error('Error getting current block height', {
error: error instanceof Error ? error.message : 'Unknown error',
})
return 0
}
}
/**
* Wait for transaction confirmation
* Polls mempool.space until transaction is confirmed or timeout
*/
async waitForConfirmation(
txid: string,
timeout: number = 600000, // 10 minutes
interval: number = 10000 // 10 seconds
): Promise<TransactionVerificationResult | null> {
const startTime = Date.now()
return new Promise((resolve) => {
const checkConfirmation = async () => {
if (Date.now() - startTime > timeout) {
resolve(null)
return
}
// Get author address from transaction (first output that's not platform)
const transaction = await this.getTransaction(txid)
if (!transaction) {
setTimeout(checkConfirmation, interval)
return
}
const authorOutput = transaction.vout.find(
(output) => output.scriptpubkey_address !== PLATFORM_BITCOIN_ADDRESS
)
if (!authorOutput) {
setTimeout(checkConfirmation, interval)
return
}
const result = await this.verifySponsoringTransaction(txid, authorOutput.scriptpubkey_address)
if (result.confirmed && result.valid) {
resolve(result)
} else {
setTimeout(checkConfirmation, interval)
}
}
checkConfirmation()
})
}
}
export const mempoolSpaceService = new MempoolSpaceService()