morphit/apps/web/scripts/order-card-identity-first-paint-smoke.ts

184 lines
7.8 KiB
TypeScript

#!/usr/bin/env tsx
/**
* order-card-identity-first-paint — v1.8.13 (Ken).
*
* KEN'S REQUIREMENT, VERBATIM: "i should NEVER see the default username and
* identicon if a custom display name and custom avatar have been set."
*
* THE BUG. The orderbook returned `posting_pubkey` inline but not the profile,
* so the browser fetched names and avatars in a SECOND round-trip. Cards
* painted `@account` + identicon and swapped to the real identity seconds later
* — ~7s on morphit.io.
*
* WHY THAT IS A TRUST DEFECT, NOT A PERFORMANCE ONE. Ken: "if i were an
* interested user in that order, i would think twice because it looks like i
* might get scammed when that user's ordercard seems like it can just change
* itself on the fly like that." On a marketplace where the counterparty's
* identity IS the product, an identity that visibly rewrites itself is
* indistinguishable from a swap attack. Fixing it by making the swap FASTER
* would not fix it; the swap has to not happen.
*
* THE FIX. `profiles` is LEFT JOINed into the orderbook query, so a row arrives
* carrying its own display name and avatar and the card is correct on first
* paint. The client still prefers the hydrated map when present (it is fresher,
* e.g. a profile edited after page load), and an older indexer that omits the
* inline fields degrades to exactly the previous behaviour.
*
* Tamper tests (each must turn this red):
* - Drop the profiles join or the selected columns from the query.
* - Stop the card consulting the inline profile.
* - Remove the fields from OrderRecord.
*/
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '..', '..', '..');
const read = (p: string): string => readFileSync(join(REPO, p), 'utf8');
const orderbookApi = read('apps/indexer/src/api/orderbook.ts');
const joins = read('apps/indexer/src/api/reputationJoin.ts');
const client = read('packages/indexer-client/src/index.ts');
const page = read('apps/web/src/routes/[lang]/orderbook/+page.svelte');
let passed = 0;
let failed = 0;
const check = (name: string, cond: boolean, detail = ''): void => {
if (cond) {
console.log(`${name}`);
passed++;
} else {
console.log(`${name}${detail ? `: ${detail}` : ''}`);
failed++;
}
};
console.log('\n── order-card-identity-first-paint (v1.8.13) ─────────\n');
// v1.8.14 (Ken) — EVERY query that builds an order row must join profiles, not
// just the one I happened to fix. v1.8.13 added it to the REST orderbook query
// only; `orderbookStream.ts` (the LIVE feed) and `featuredOrderbook.ts` were
// missed, so orders arriving or refreshing through those paths still painted
// @account + identicon and swapped. Ken: "it STILL takes 5-6 seconds... not all
// of the time, but half of the time or so" — the intermittency WAS the tell:
// different rows arrived by different paths.
// `accountsJoin` marks a query that builds order rows, so it is the anchor:
// wherever it appears, `profileJoin` must too.
const ORDER_ROW_QUERIES = [
'apps/indexer/src/api/orderbook.ts',
'apps/indexer/src/api/orderbookStream.ts',
'apps/indexer/src/api/featuredOrderbook.ts'
];
for (const q of ORDER_ROW_QUERIES) {
const qs = readFileSync(join(REPO, q), 'utf8');
const name = q.split('/').pop()!;
check(
`${name}: joins profiles wherever it joins accounts`,
!/accountsJoin\(/.test(qs) || /profileJoin\(/.test(qs),
'an order row without inline identity paints @account + identicon, then swaps'
);
check(
`${name}: selects the display name and avatar metadata`,
!/accountsJoin\(/.test(qs) || (/pr\.display_name/.test(qs) && /profile_json_metadata/.test(qs)),
'joined but unselected is the same as not joined'
);
}
check(
'a profiles join helper exists',
/export function profileJoin\(/.test(joins),
'without it the identity needs a second round-trip'
);
check(
'the orderbook query JOINs profiles',
/\$\{profileJoin\('o', 'pr'\)\}/.test(orderbookApi),
'the row cannot carry an identity it never selected'
);
check(
'…and selects the display name',
/pr\.display_name/.test(orderbookApi),
'the card would fall back to @account'
);
check(
'…and the profile metadata that holds the avatar',
/pr\.json_metadata AS profile_json_metadata/.test(orderbookApi),
'the card would fall back to the identicon'
);
check(
'both are returned in the response row',
/display_name: r\.display_name \?\? null/.test(orderbookApi) &&
/profile_json_metadata: r\.profile_json_metadata \?\? null/.test(orderbookApi),
'selected but not returned is the same as not selected'
);
check(
'OrderRecord carries them, so the client can use them',
/readonly display_name\?: string \| null;/.test(client) &&
/readonly profile_json_metadata\?: unknown;/.test(client),
'an untyped field will not be read'
);
check(
'the card renders from the inline profile when the hydrated map is empty',
/profileMap\[o\.account\] \?\? inlineProfileOf\(o\)/.test(page),
'THE WHOLE POINT: without this the first paint is still @account + identicon'
);
check(
'the hydrated map still takes precedence when present',
/profileMap\[o\.account\] \?\? /.test(page),
'a profile edited after page load must still win over the row snapshot'
);
check(
'the inline adapter degrades safely on an older indexer',
/if \(o\.display_name === undefined && o\.profile_json_metadata === undefined\) return null;/.test(
page
),
'an instance that omits the fields must fall back, not render blanks'
);
// v1.8.16 (Ken) — the SELECT-vs-EMIT gap, now closed for the FEATURED path too.
// The block above checks all three endpoints SELECT the identity columns, but it
// only checked that orderbook.ts EMITS them. featuredOrderbook.ts has its OWN
// wire mapping (a literal spreading reputationFieldsFromRow, NOT rowToWire) and
// SELECTed the columns while dropping them from the payload — so the homepage
// featured cards carried no inline identity and swapped @account+identicon for
// the real name/avatar a beat later (kentest3's "delayed" avatar). "Selected but
// not returned is the same as not selected" applies PER MAPPING, not once.
const featuredApi = read('apps/indexer/src/api/featuredOrderbook.ts');
const featuredCard = read('apps/web/src/lib/components/FeaturedOrders.svelte');
const streamApi = read('apps/indexer/src/api/orderbookStream.ts');
check(
'featuredOrderbook TYPES the identity fields on its row',
/display_name: string \| null;/.test(featuredApi) &&
/profile_json_metadata: unknown;/.test(featuredApi),
'an untyped SELECTed column is silently dropped by the mapping'
);
check(
'featuredOrderbook RETURNS the identity fields in its wire mapping',
/display_name: r\.display_name \?\? null/.test(featuredApi) &&
/profile_json_metadata: r\.profile_json_metadata \?\? null/.test(featuredApi),
'featured selected them but dropped them from the payload — the homepage card swapped'
);
check(
'orderbookStream reuses rowToWire, so its emission tracks orderbook.ts',
/rowToWire/.test(streamApi),
'a divergent stream mapping could drop the fields the snapshot emits'
);
check(
'the featured card renders from the inline profile when the hydrated map is empty',
/profileMap\[o\.account\] \?\? inlineProfileOf\(o\)/.test(featuredCard),
'without this the homepage first paint is still @account + identicon'
);
check(
'the featured card has the same safe inline adapter',
/if \(o\.display_name === undefined && o\.profile_json_metadata === undefined\) return null;/.test(
featuredCard
),
'an instance that omits the fields must fall back, not render blanks'
);
console.log(
`\n${passed} passed, ${failed} failed\n${failed === 0 ? `✓ all ${passed} order-card-identity-first-paint checks passed` : '✗ order-card-identity-first-paint FAILED'}`
);
process.exit(failed === 0 ? 0 : 1);