story-research-zapwall/lib/mempoolSpaceVerification.ts

170 lines
5.0 KiB
TypeScript

import { calculateSponsoringSplit } from './platformCommissions'
import { PLATFORM_BITCOIN_ADDRESS } from './platformConfig'
import type { MempoolTransaction, TransactionVerificationResult } from './mempoolSpaceTypes'
import { getTransaction, getConfirmations } from './mempoolSpaceApi'
export function findTransactionOutputs(
transaction: MempoolTransaction,
authorMainnetAddress: string,
expectedAuthorAmount: number,
expectedPlatformAmount: number
): { authorOutput?: MempoolTransaction['vout'][0]; platformOutput?: MempoolTransaction['vout'][0] } {
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 result: { authorOutput?: MempoolTransaction['vout'][0]; platformOutput?: MempoolTransaction['vout'][0] } = {}
if (authorOutput) {
result.authorOutput = authorOutput
}
if (platformOutput) {
result.platformOutput = platformOutput
}
return result
}
export function buildVerificationResult(
valid: boolean,
confirmed: boolean,
confirmations: number,
authorOutput?: MempoolTransaction['vout'][0],
platformOutput?: MempoolTransaction['vout'][0]
): TransactionVerificationResult {
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
}
export async function validateTransactionOutputs(
transaction: MempoolTransaction,
authorMainnetAddress: string,
split: { authorSats: number; platformSats: number }
): Promise<{ valid: boolean; confirmed: boolean; confirmations: number; authorOutput?: MempoolTransaction['vout'][0]; platformOutput?: MempoolTransaction['vout'][0] }> {
const { authorOutput, platformOutput } = findTransactionOutputs(
transaction,
authorMainnetAddress,
split.authorSats,
split.platformSats
)
const valid = Boolean(authorOutput && platformOutput)
const confirmed = transaction.status.confirmed
const confirmations = confirmed && transaction.status.block_height
? await getConfirmations(transaction.status.block_height)
: 0
const result: { valid: boolean; confirmed: boolean; confirmations: number; authorOutput?: MempoolTransaction['vout'][0]; platformOutput?: MempoolTransaction['vout'][0] } = {
valid,
confirmed,
confirmations,
}
if (authorOutput) {
result.authorOutput = authorOutput
}
if (platformOutput) {
result.platformOutput = platformOutput
}
return result
}
export function logVerificationFailure(
txid: string,
authorMainnetAddress: string,
split: { authorSats: number; platformSats: number },
transaction: MempoolTransaction
): void {
console.error('Transaction verification failed', {
txid,
authorAddress: authorMainnetAddress,
platformAddress: PLATFORM_BITCOIN_ADDRESS,
expectedAuthorAmount: split.authorSats,
expectedPlatformAmount: split.platformSats,
actualOutputs: transaction.vout.map((o) => ({
address: o.scriptpubkey_address,
amount: o.value,
})),
timestamp: new Date().toISOString(),
})
}
export function handleVerificationError(txid: string, authorMainnetAddress: string, error: unknown): TransactionVerificationResult {
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,
}
}
export async function verifySponsoringTransaction(
txid: string,
authorMainnetAddress: string
): Promise<TransactionVerificationResult> {
try {
const transaction = await getTransaction(txid)
if (!transaction) {
return {
valid: false,
confirmed: false,
confirmations: 0,
error: 'Transaction not found',
}
}
const split = calculateSponsoringSplit()
const validation = await validateTransactionOutputs(transaction, authorMainnetAddress, split)
if (!validation.valid) {
logVerificationFailure(txid, authorMainnetAddress, split, transaction)
}
return buildVerificationResult(
validation.valid,
validation.confirmed,
validation.confirmations,
validation.authorOutput,
validation.platformOutput
)
} catch (error) {
return handleVerificationError(txid, authorMainnetAddress, error)
}
}