From 7a2f03e67ff0758b803dd87c9c4df69153de1187 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 31 May 2026 18:07:01 +0530 Subject: [PATCH] feat(folio-spec): predicate that gates balance check on TxnSubmit tap Replaces the row-sum predicate (which always held by construction since balance is derived from rows in Folio) with one that compares the typed amount to the actual balance delta after a tap on TxnSubmit. Catches the planted double-submit bug. --- examples/folio/sanderling/predicates.ts | 39 ++++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/examples/folio/sanderling/predicates.ts b/examples/folio/sanderling/predicates.ts index 04889b8..9ab381f 100644 --- a/examples/folio/sanderling/predicates.ts +++ b/examples/folio/sanderling/predicates.ts @@ -1,22 +1,19 @@ -export interface LedgerRow { - key: string; - signed: number; -} - -// When new ledger rows appear, the balance delta must equal the SUM of the -// new rows' signed amounts. Single-row sums and multi-row sums BOTH must -// match the delta; a double-submit landing two rows whose total is twice -// the balance change trips the sum check. -export function balanceMatchesAddedSum( - previousRows: readonly LedgerRow[], - currentRows: readonly LedgerRow[], - previousBalance: number, - currentBalance: number, -): boolean { - const previousKeys = new Set(previousRows.map(row => row.key)); - const added = currentRows.filter(row => !previousKeys.has(row.key)); - if (added.length === 0) return true; - const delta = currentBalance - previousBalance; - const addedSum = added.reduce((sum, row) => sum + row.signed, 0); - return addedSum === delta; +// When the last action is a tap (or double-tap) on the transaction Submit +// button, the absolute change in total balance must equal the amount the +// user typed. A double-submit lands two transactions and shifts the balance +// by 2x the typed amount, tripping this check. +export function submitChangesBalanceByTypedAmount(args: { + lastAction: { kind?: string; on?: string | object } | null; + typedAmount: number; + prevTotalBalance: number; + currTotalBalance: number; +}): boolean { + const { lastAction, typedAmount, prevTotalBalance, currTotalBalance } = args; + if (lastAction == null) return true; + if (lastAction.kind !== "Tap" && lastAction.kind !== "DoubleTap") return true; + const on = lastAction.on; + const onString = typeof on === "string" ? on : on != null ? JSON.stringify(on) : ""; + if (!onString.includes("TxnSubmit")) return true; + if (typedAmount === 0) return true; + return Math.abs(currTotalBalance - prevTotalBalance) === typedAmount; }