import { calculateSponsoringSplit } from './platformCommissions' import { PLATFORM_BITCOIN_ADDRESS } from './platformConfig' 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 | undefined } /** * 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 { 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 { 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(), }) } const result: TransactionVerificationResult = { valid, confirmed, confirmations, } if (!valid) { result.error = 'Transaction outputs do not match expected split' } if (authorOutput) { result.authorOutput = { address: authorOutput.scriptpubkey_address, amount: authorOutput.value, } } if (platformOutput) { result.platformOutput = { address: platformOutput.scriptpubkey_address, amount: platformOutput.value, } } return result } 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 { 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 { 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()