morphit/apps/indexer/scripts/stranger-fee-handler-smoke.ts

343 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Stranger-fee handler — tsx smoke runner.
*
* Exercises the layer-2 admission handler without vitest.
* Same style as block-handler-smoke.ts and chat-handler-smoke.ts.
*
* Usage (from apps/indexer):
* tsx scripts/stranger-fee-handler-smoke.ts
*/
import handler from '../src/indexer/handlers/strangerFee.ts';
import { makeCtx } from '../test/testutils/context.ts';
import { makeMockClient } from '../test/testutils/mockClient.ts';
let failures = 0;
let scenarios = 0;
function scenario(name: string, fn: () => void | Promise<void>): Promise<void> {
scenarios++;
return Promise.resolve()
.then(fn)
.then(
() => {
console.log(`${name}`);
},
(err) => {
failures++;
console.log(`${name}`);
console.log(` ${err instanceof Error ? err.message : String(err)}`);
}
);
}
function assertEqual(actual: unknown, expected: unknown, label: string): void {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
if (a !== e) {
throw new Error(`${label}: expected ${e}, got ${a}`);
}
}
/** Builds a realistic ctx for the stranger-fee handler.
* Callers override just the fields they're varying in a
* scenario. After the BLURT-native fee refactor, fees are
* denominated directly in BLURT — no priceSource on context,
* no USD fields in the payload. */
function ctxFor(override: {
signer?: string;
payload?: unknown;
transferAmountBlurt?: number | null;
transferTo?: string;
transferFrom?: string;
transferMemo?: string;
}) {
const signer = override.signer ?? 'alice';
const recipient = (override.payload as { recipient?: string } | undefined)?.recipient ?? 'bob';
const siblingOps: (readonly [string, Record<string, unknown>])[] = [];
if (override.transferAmountBlurt !== null && override.transferAmountBlurt !== undefined) {
siblingOps.push([
'transfer',
{
from: override.transferFrom ?? signer,
to: override.transferTo ?? 'morphit-fees',
amount: `${override.transferAmountBlurt.toFixed(3)} BLURT`,
memo: override.transferMemo ?? `morphit-stranger:${recipient}`
}
]);
}
return makeCtx({
signer,
payload: override.payload ?? {
v: 1,
recipient,
amount_blurt: 5
},
siblingOps,
// feeTolerance widened to 0.02 for test arithmetic ergonomics —
// scenarios use whole-BLURT amounts that can drift by a few
// thousandths under the strict 0.001 production default.
config: {
feeRecipient: 'morphit-fees',
feeTolerance: 0.02
} as unknown as NonNullable<Parameters<typeof makeCtx>[0]>['config']
});
}
console.log('\n── Stranger-fee handler ──────────────────────────────');
// ─── Validation ──────────────────────────────────────────────
await scenario('rejects non-object payload', async () => {
const mock = makeMockClient();
const r = await handler(ctxFor({ payload: 'nope' as unknown }), mock.client);
assertEqual(r, { ok: false, reason: 'payload_not_object' }, 'result');
});
await scenario('rejects invalid recipient', async () => {
const mock = makeMockClient();
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'X', amount_blurt: 5 }
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'recipient_invalid' }, 'result');
});
await scenario('rejects self-fee', async () => {
const mock = makeMockClient();
const r = await handler(
ctxFor({
signer: 'alice',
payload: { v: 1, recipient: 'alice', amount_blurt: 5 }
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'self_fee' }, 'result');
});
await scenario('rejects out-of-range amount_blurt (> 1.5× current quote)', async () => {
// Base quote is 5 BLURT; 1.5× = 7.5 BLURT. 10 BLURT exceeds.
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] }
]);
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'bob', amount_blurt: 100 }
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'amount_blurt_out_of_range' }, 'result');
});
// ─── Idempotency ─────────────────────────────────────────────
await scenario('accepts silently when row already exists', async () => {
const mock = makeMockClient([{ match: 'EXISTS', rows: [{ exists: true }] }]);
const r = await handler(ctxFor({ transferAmountBlurt: 5 }), mock.client);
assertEqual(r, { ok: true }, 'result');
if (mock.queries.length !== 1) {
throw new Error(`expected existence check only, got ${mock.queries.length} queries`);
}
});
await scenario('translates PK collision (23505) into ok:true', async () => {
const pgErr = Object.assign(new Error('dup'), { code: '23505' });
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] },
{ match: 'INSERT INTO stranger_fees', throwError: pgErr }
]);
const r = await handler(ctxFor({ transferAmountBlurt: 5 }), mock.client);
assertEqual(r, { ok: true }, 'result');
});
// ─── Fee verification ────────────────────────────────────────
await scenario('inserts when transfer matches and amount is sufficient', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] },
{ match: 'INSERT INTO stranger_fees' }
]);
const r = await handler(ctxFor({ transferAmountBlurt: 5 }), mock.client);
assertEqual(r, { ok: true }, 'result');
if (mock.queries.length !== 3) {
throw new Error(`expected 3 queries, got ${mock.queries.length}`);
}
});
await scenario('rejects fee_missing when no transfer sibling', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] }
]);
const r = await handler(ctxFor({ transferAmountBlurt: null }), mock.client);
assertEqual(r, { ok: false, reason: 'fee_missing' }, 'result');
});
await scenario('rejects fee_missing when memo targets different recipient', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] }
]);
const r = await handler(
ctxFor({
transferAmountBlurt: 5,
transferMemo: 'morphit-stranger:carol'
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'fee_missing' }, 'result');
});
await scenario('rejects fee_missing when transfer goes to wrong account', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] }
]);
const r = await handler(
ctxFor({ transferAmountBlurt: 5, transferTo: 'some-attacker' }),
mock.client
);
assertEqual(r, { ok: false, reason: 'fee_missing' }, 'result');
});
await scenario('rejects fee_missing when transfer is from a different signer', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] }
]);
const r = await handler(ctxFor({ transferAmountBlurt: 5, transferFrom: 'eve' }), mock.client);
assertEqual(r, { ok: false, reason: 'fee_missing' }, 'result');
});
await scenario('rejects fee_underpaid when amount is below tolerance', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] }
]);
// 3 BLURT vs 5 BLURT expected → clearly short.
const r = await handler(ctxFor({ transferAmountBlurt: 3 }), mock.client);
assertEqual(r, { ok: false, reason: 'fee_underpaid' }, 'result');
});
await scenario('accepts over-payment (user paid extra BLURT)', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '0' }] },
{ match: 'INSERT INTO stranger_fees' }
]);
const r = await handler(ctxFor({ transferAmountBlurt: 8 }), mock.client);
assertEqual(r, { ok: true }, 'result');
});
// ─── Escalation ──────────────────────────────────────────────
// The doubling cap kicks in when the sender has paid for prior
// strangers in the last 5 minutes. count=N → multiplier=2^N
// (capped at 128 = 640 BLURT).
await scenario('escalation: count=3 → multiplier 8 → accepts 40 BLURT claim', async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '3' }] },
{ match: 'INSERT INTO stranger_fees' }
]);
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'bob', amount_blurt: 40 },
transferAmountBlurt: 40
}),
mock.client
);
assertEqual(r, { ok: true }, 'result');
});
await scenario(
'escalation: count=3 but user claims base 5 BLURT → rejects amount_blurt_below_current_quote',
async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '3' }] }
]);
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'bob', amount_blurt: 5 },
transferAmountBlurt: 5
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'amount_blurt_below_current_quote' }, 'result');
}
);
await scenario(
'escalation: count=10 (capped) → multiplier 128 → accepts 640 BLURT claim',
async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '10' }] },
{ match: 'INSERT INTO stranger_fees' }
]);
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'bob', amount_blurt: 640 },
transferAmountBlurt: 640
}),
mock.client
);
assertEqual(r, { ok: true }, 'result');
}
);
await scenario(
'escalation: count=3 with claim 100 BLURT (>1.5× quote of 40) → rejects amount_blurt_out_of_range',
async () => {
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '3' }] }
]);
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'bob', amount_blurt: 100 },
transferAmountBlurt: 100
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'amount_blurt_out_of_range' }, 'result');
}
);
await scenario(
'escalation: count=3, claim 40 BLURT but transfer is base 5 BLURT → rejects fee_underpaid',
async () => {
// User claimed the right escalating BLURT but didn't pay the
// matching transfer — the indexer's transfer-amount check
// catches this independently of the claim.
const mock = makeMockClient([
{ match: 'EXISTS', rows: [{ exists: false }] },
{ match: 'paid_at >', rows: [{ count: '3' }] }
]);
const r = await handler(
ctxFor({
payload: { v: 1, recipient: 'bob', amount_blurt: 40 },
transferAmountBlurt: 5
}),
mock.client
);
assertEqual(r, { ok: false, reason: 'fee_underpaid' }, 'result');
}
);
console.log(`\n${'─'.repeat(54)}`);
if (failures === 0) {
console.log(`✓ all ${scenarios} scenarios passed`);
process.exit(0);
} else {
console.log(`${failures}/${scenarios} scenarios failed`);
process.exit(1);
}