- Correction toutes erreurs TypeScript : - Variables non utilisées supprimées - Types optionnels corrigés (exactOptionalPropertyTypes) - Imports corrigés (PLATFORM_BITCOIN_ADDRESS depuis platformConfig) - Gestion correcte des propriétés optionnelles - Suppression fichiers obsolètes : - code-cleanup-summary.md (redondant) - todo-implementation*.md (todos obsolètes) - corrections-completed.md, fallbacks-found.md (corrections faites) - implementation-summary.md (redondant) - documentation-plan.md (plan, pas documentation) - Suppression scripts temporaires : - add-ssh-key.sh - add-ssh-key-plink.sh - Réorganisation documentation dans docs/ : - architecture.md (nouveau) - commissions.md (nouveau) - implementation-summary.md - remaining-tasks.md - split-and-transfer.md - commission-system.md - commission-implementation.md - content-delivery-verification.md Toutes erreurs TypeScript corrigées, documentation centralisée.
234 lines
6.4 KiB
TypeScript
234 lines
6.4 KiB
TypeScript
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<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(),
|
|
})
|
|
}
|
|
|
|
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<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()
|