- 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.
227 lines
6.3 KiB
TypeScript
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()
|
|
|