import { Transaction } from './transaction.js'; import { binToHex, Output } from '@bitauth/libauth'; export interface UTXO extends Output { outpointTransactionHash: string; outpointIndex: number; } export class TransactionGraph { private transactions: Map; constructor() { this.transactions = new Map(); } addTransaction(tx: Transaction) { this.transactions.set(tx.toHashHex(), tx); } removeTransaction(txHash: string) { this.transactions.delete(txHash); } removeDependentTransactions(txHash: string): string[] { const removedTxids: string[] = []; const toRemove: Set = new Set([txHash]); const findDependents = (id: string) => { for (const [otherTxid, tx] of this.transactions) { if ( tx .getInputs() .some((input) => binToHex(input.outpointTransactionHash) === id) ) { toRemove.add(otherTxid); findDependents(otherTxid); } } }; findDependents(txHash); for (const id of toRemove) { if (this.transactions.has(id)) { this.transactions.delete(id); removedTxids.push(id); } } return removedTxids; } getTransaction(txHash: string): Transaction | undefined { return this.transactions.get(txHash); } getAllTransactions(): Transaction[] { return Array.from(this.transactions.values()); } deriveUtxoState(): UTXO[] { const utxoMap = new Map(); // First, add all outputs as potential UTXOs for (const tx of this.transactions.values()) { tx.getOutputs().forEach((output, outpointIndex) => { const txHashHex = tx.toHashHex(); const utxo: UTXO = { outpointTransactionHash: txHashHex, outpointIndex, valueSatoshis: output.valueSatoshis, lockingBytecode: output.lockingBytecode, }; utxoMap.set(`${txHashHex}:${outpointIndex}`, utxo); }); } // Then, remove all spent outputs for (const tx of this.transactions.values()) { tx.getInputs().forEach((input) => { const outpointTransactionHashHex = binToHex( input.outpointTransactionHash ); utxoMap.delete(`${outpointTransactionHashHex}:${input.outpointIndex}`); }); } return Array.from(utxoMap.values()); } isTransactionValid(tx: Transaction): boolean { const currentUtxos = this.deriveUtxoState(); const utxoMap = new Map( currentUtxos.map((utxo) => [ `${utxo.outpointTransactionHash}:${utxo.outpointIndex}`, utxo, ]) ); return tx.getInputs().every((input) => { const outpointTransactionHashHex = binToHex( input.outpointTransactionHash ); return utxoMap.has( `${outpointTransactionHashHex}:${input.outpointIndex}` ); }); } getDependentTransactions(txHash: string): Transaction[] { return this.getAllTransactions().filter((tx) => { tx.getInputs().some((input) => { const outpointTransactionHashHex = binToHex( input.outpointTransactionHash ); return outpointTransactionHashHex === txHash; }); }); } getBroadcastOrder(): Transaction[] { const visited = new Set(); const order: Transaction[] = []; const visit = (txid: string) => { if (visited.has(txid)) return; const tx = this.transactions.get(txid); if (!tx) return; visited.add(txid); // First, visit all transactions that this transaction depends on for (const input of tx.getInputs()) { const outpointTransactionHashHex = binToHex( input.outpointTransactionHash ); visit(outpointTransactionHashHex); } order.push(tx); }; // Start the topological sort from each transaction for (const tx of this.transactions.values()) { visit(tx.toHashHex()); } return order; } getNextTransactionToBroadcast(): Transaction | undefined { // This is pretty inefficient in the sense we recompile the graph each time. // But I'm not sure if that would really have much real-world performance impact. return this.getBroadcastOrder()[0] ?? undefined; } }