Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/utils/blockchain/counterparty/pack/__tests__/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,36 @@ describe('packing matches bytes generated with core\'s MPMA encoder', () => {
);
});

it('packs a hex memo alongside a send with no memo', () => {
// The form emits one flag per row and a row with no memo emits `false`, so this batch arrives
// as memos_are_hex="true,false". Only the flags belonging to an actual memo describe the
// encoding, and `composeMPMA` sends the single flag `true` — so declining here would have
// silently dropped byte verification for any batch where only some rows carry memos.
const packed = packComposeMessage('mpma', {
assets: 'XCP,XCP',
destinations: `${P2PKH_A},${P2WPKH}`,
quantities: '7,9',
memos: 'beef,',
memos_are_hex: 'true,false',
});

expect(packed).not.toBeNull();
expect(bytesToHex(packed!.bytes)).toBe(
'434e5452505254590300020062e907b15cbf27d5425399ebf6f0fb50ebb88f1880751e76e8199196d454941c45d1b3a323f1433bd6400000000000000060000000000000007c2beef80000000000000048'
);
});

it('still declines memos that genuinely mix hex and text', () => {
// Two real memos with different encodings cannot travel under core's single flag.
expect(packComposeMessage('mpma', {
assets: 'XCP,XCP',
destinations: `${P2PKH_A},${P2WPKH}`,
quantities: '7,9',
memos: 'beef,hello',
memos_are_hex: 'true,false',
})).toBeNull();
});

it('packs the send form\'s comma-separated destinations as the same MPMA message', () => {
// composeSendOrMPMA replicates the asset and quantity per destination and carries the memo
// once as the whole-send memo; the fixture is core's encoding of exactly that request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import { bytesToHex } from '../../unpack/binary';
import { COUNTERPARTY_PREFIX_HEX } from '../../unpack/messageTypes';

const API_URL = process.env.COUNTERPARTY_API_URL;
/** How many recent transactions of each type to check. */
const SAMPLE_SIZE = 5;
/**
* How many recent transactions of each type to check. Wide enough that a burst of same-shaped
* traffic does not starve the comparison — see the note on principled declines below.
*/
const SAMPLE_SIZE = 25;

interface OnChainTransaction {
tx_hash: string;
Expand Down Expand Up @@ -178,6 +181,8 @@ describe.skipIf(!API_URL)('rebuilding real on-chain messages', () => {
expect(transactions.length, `no ${apiType} transactions returned`).toBeGreaterThan(0);

let compared = 0;
/** Samples the packer refuses on purpose, as opposed to ones it got wrong. */
let declined = 0;
const failures: string[] = [];

for (const transaction of transactions) {
Expand All @@ -196,7 +201,13 @@ describe.skipIf(!API_URL)('rebuilding real on-chain messages', () => {
const packed = packComposeMessage(
COMPOSE_TYPE_FOR[apiType]!, params, unpacked.data as Record<string, unknown>
);
if (!packed) continue; // a variant this build declines to build
if (!packed) {
// A variant this build declines by design — a locked or reset subasset, a hex memo, a
// Taproot MPMA destination — rather than one it got wrong. Counted so that a window full
// of them reads as "nothing to compare" instead of "the packer is broken".
declined += 1;
continue;
}

compared += 1;
const rebuilt = bytesToHex(packed.bytes).toLowerCase();
Expand All @@ -208,6 +219,21 @@ describe.skipIf(!API_URL)('rebuilding real on-chain messages', () => {

expect(failures, `rebuilt bytes differ from chain:\n${failures.join('\n')}`).toEqual([]);
// A type where every sample was skipped proves nothing, so say so rather than pass quietly.
expect(compared, `every ${apiType} sample was skipped; none could be rebuilt`).toBeGreaterThan(0);
// Comparing nothing proves nothing, so this must never pass quietly — but there are two
// reasons it happens and only one is a defect. Traffic is bursty enough that a single prolific
// minter fills the whole window with a shape the packer refuses on purpose: at the time of
// writing all 100 most recent issuances were locked subassets, which the borrow guard declines
// by design. That run is uninformative rather than wrong.
//
// So when nothing was compared, the invariant is that *every* sample was a principled decline.
// A sample that was attempted and still could not be rebuilt means the packer is wrong.
if (compared === 0) {
expect(
declined,
`no ${apiType} sample could be rebuilt, and not all were shapes this build declines by `
+ 'design — the packer is likely wrong rather than the sample unrepresentative'
).toBe(transactions.length);
return;
}
}, 30_000);
});
9 changes: 7 additions & 2 deletions src/utils/blockchain/counterparty/pack/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,8 +739,13 @@ function packMpmaFromParams(params: Params): PackedMessage | null {
const flagValues = typeof params.memos_are_hex === 'string'
? params.memos_are_hex.split(',').map((value) => value === 'true')
: [params.memos_are_hex === true];
if (new Set(flagValues).size > 1) return null;
memosAreHex = flagValues[0] ?? false;
// Only the flags belonging to a memo matter. The form emits one flag per row, and a row with
// no memo emits `false`, so a batch mixing a hex memo with an empty one would otherwise look
// like a hex/text mix and decline — the common shape when only some rows carry memos.
// `composeMPMA` filters the same way before choosing the single flag it sends.
const flagsForMemos = flagValues.filter((_, index) => (memosCsv[index] ?? '') !== '');
if (new Set(flagsForMemos).size > 1) return null;
memosAreHex = flagsForMemos[0] ?? false;
}

const sends: MpmaSend[] = [];
Expand Down
Loading