|
| 1 | +import { OrdpoolTransactionFlags } from 'ordpool-parser'; |
| 2 | + |
| 3 | +import { getTransactionFlags } from './transaction.utils'; |
| 4 | +import { COUNTERPARTY_MPMA_TX, PLAIN_P2PKH_TX } from './transaction.utils.fixtures'; |
| 5 | + |
| 6 | +/** |
| 7 | + * Regression test for the "frontend tx-detail page never invoked the |
| 8 | + * ordpool parser" bug. The contract: getTransactionFlags MUST OR ordpool |
| 9 | + * artifact bits into the returned flags when given a tx that the parser |
| 10 | + * recognises -- mirroring the backend's common.ts::Common.getTransactionFlags |
| 11 | + * which calls `analyseTransaction(tx, flags)` at the end of its computation. |
| 12 | + * |
| 13 | + * Background (so this test isn't deleted later "as redundant"): |
| 14 | + * |
| 15 | + * The first implementation of frontend/backend integration used a side-channel |
| 16 | + * pattern: the parser mutated tx._ordpoolFlags as a side effect, and |
| 17 | + * getTransactionFlags read that field back. The architecture was later |
| 18 | + * refactored to a clean functional contract -- analyseTransaction(tx, flags) |
| 19 | + * takes input flags, returns merged flags, no side-channel needed. The |
| 20 | + * backend was updated. The frontend's getTransactionFlags was NOT updated |
| 21 | + * for ~a month, so any tx that arrived without server-pre-classified flags |
| 22 | + * (i.e. anything fetched via the tx-detail Esplora proxy at /api/tx/<id>) |
| 23 | + * had its ordpool artifact bits silently dropped. The Counterparty mpma |
| 24 | + * tx 4a412b0a...4788e is the canonical regression case. |
| 25 | + */ |
| 26 | +// jsdom test environment doesn't expose structuredClone; this works fine |
| 27 | +// for plain-data fixtures (no Date / Map / Set / Uint8Array inside). |
| 28 | +function deepClone<T>(v: T): T { |
| 29 | + return JSON.parse(JSON.stringify(v)); |
| 30 | +} |
| 31 | + |
| 32 | +describe('getTransactionFlags (ordpool integration)', () => { |
| 33 | + |
| 34 | + it('ORs ordpool_counterparty into flags for a real Counterparty mpma tx', async () => { |
| 35 | + // Clone the fixture so we don't accidentally mutate it across test |
| 36 | + // re-runs (the parser internally sets tx._ordpoolFlags as a side effect |
| 37 | + // for the OTS pre-enrichment path, and we want each test to start fresh). |
| 38 | + const tx = deepClone(COUNTERPARTY_MPMA_TX); |
| 39 | + |
| 40 | + const flags = await getTransactionFlags(tx, null, null, tx.status.block_height, 'mainnet'); |
| 41 | + |
| 42 | + expect(flags & OrdpoolTransactionFlags.ordpool_counterparty).toBe(OrdpoolTransactionFlags.ordpool_counterparty); |
| 43 | + }); |
| 44 | + |
| 45 | + it('returns no ordpool bits for a plain p2pkh tx', async () => { |
| 46 | + const tx = deepClone(PLAIN_P2PKH_TX); |
| 47 | + |
| 48 | + const flags = await getTransactionFlags(tx, null, null, tx.status.block_height, 'mainnet'); |
| 49 | + |
| 50 | + // Mask: every ordpool bit lives at position 48 or above. A plain tx must |
| 51 | + // not light any of them up. |
| 52 | + const ORDPOOL_BIT_MASK = ((1n << 32n) - 1n) << 48n; |
| 53 | + expect(flags & ORDPOOL_BIT_MASK).toBe(0n); |
| 54 | + }); |
| 55 | + |
| 56 | + it('returns the same value across two calls (no parser-mutation leaking across calls)', async () => { |
| 57 | + // The parser DOES still mutate tx._ordpoolFlags internally (used by the |
| 58 | + // OTS pre-enrichment path). Calling getTransactionFlags twice on the |
| 59 | + // SAME tx object must still produce the same flags both times -- not |
| 60 | + // double-OR something, not pick up stale leftovers from the previous run. |
| 61 | + const tx = deepClone(COUNTERPARTY_MPMA_TX); |
| 62 | + |
| 63 | + const first = await getTransactionFlags(tx, null, null, tx.status.block_height, 'mainnet'); |
| 64 | + const second = await getTransactionFlags(tx, null, null, tx.status.block_height, 'mainnet'); |
| 65 | + |
| 66 | + expect(second).toBe(first); |
| 67 | + }); |
| 68 | + |
| 69 | + it('does not read or rely on tx._ordpoolFlags side-channel', async () => { |
| 70 | + // The OLD bug was: getTransactionFlags only inspected tx._ordpoolFlags, |
| 71 | + // which was populated by the backend's side-effect pattern. On the |
| 72 | + // tx-detail page the field was never set, so the chip never fired. |
| 73 | + // |
| 74 | + // Guard: explicitly clear _ordpoolFlags BEFORE calling the function. |
| 75 | + // If the function only reads the side-channel (the old broken code), |
| 76 | + // this test would return 0 for the counterparty bit. The new code calls |
| 77 | + // the parser directly and computes the flag from the witness. |
| 78 | + const tx = deepClone(COUNTERPARTY_MPMA_TX); |
| 79 | + (tx as any)._ordpoolFlags = undefined; |
| 80 | + |
| 81 | + const flags = await getTransactionFlags(tx, null, null, tx.status.block_height, 'mainnet'); |
| 82 | + |
| 83 | + expect(flags & OrdpoolTransactionFlags.ordpool_counterparty).toBe(OrdpoolTransactionFlags.ordpool_counterparty); |
| 84 | + }); |
| 85 | +}); |
0 commit comments