From 7987158c9e38bc14198f4b733d96037c9d66c05e Mon Sep 17 00:00:00 2001
From: Cryptskii <47649969+cryptskii@users.noreply.github.com>
Date: Sat, 26 Sep 2026 04:31:33 -0400
Subject: [PATCH 01/11] =?UTF-8?q?fix(wallet):=20one=20reload=20per=20annou?=
=?UTF-8?q?nced=20change=20=E2=80=94=20the=20listener=20drops=20nothing,?=
=?UTF-8?q?=20and=20the=20raw-event=20reloads=20beside=20it=20go?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The one reload path, `wallet.refresh` → `useWalletRefreshListener`, had a
"cooldown" that counted dropped events rather than frames: after any reload
the next 119 events from a low-priority source (`inbox.sync`,
`storage.sync`, `sofi`, an adoption, a dialog's completion) were discarded
outright, and only nine named sources bypassed it. Around that gate the
provider and the wallet screen subscribed to the raw `inbox.updated` and
`bilateral.transferComplete` events as well, and the provider reloaded again
on `wallet.bilateralCommitted`, so one inbox sync with new items reloaded
the projection and the screen three to four times when the gate was open,
and the raw subscriptions were the only reload when it was shut. The event
bridge also emitted a `wallet.refresh` claiming `bilateral.transfer_complete`
on one BLE prepare response in eight — a prepare response is not a wallet
change.
The listener now coalesces onto an animation frame, runs one reload at a
time and owes exactly one more for events that arrive mid-reload: nothing is
dropped, and there are no priority classes. The provider and the screen hook
reload on `wallet.refresh` alone; the raw subscriptions and the provider's
reload on `wallet.bilateralCommitted` are deleted (its toast stays). The BLE
prepare-response emit is deleted. The accept path's staggered re-reads stay
beside Rust's TRANSFER_COMPLETE announcement and name themselves
`bilateral.accept_followup`; whether the announcement alone suffices on a
device is undecided and recorded Open (§6.29). The provider's store and the
wallet screen's hook still each hold a copy of balances and history — a
store duplication, recorded, not a reload duplication.
Tests: useWalletRefreshListener (nothing dropped after a completed refresh;
events mid-refresh owe exactly one more; no owed refresh after unmount),
WalletCreditSound (the event bridge's own announcements, decoded from bytes,
reload the projection once each; the coin sound follows the credit),
EnhancedWalletScreen.events (one inbox sync reloads the wallet data once),
EventBridge.bilateral (a BLE prepare response announces no wallet change);
the five suites that pinned the old behaviour now assert the production pair
of events and the accept follow-up's own name. Mutation controls, each red
on its named test: the drop gate reintroduced; the provider's raw
inbox.updated reload re-added; the screen hook's raw inbox.updated reload
re-added; the BLE prepare-response emit restored. Verified: tsc, lint, jest
843/843, npm run build, and the purity, flow, scan, bridge, codegen, proto
and bridge-RPC-name gates.
---
.../EnhancedWalletScreen.events.test.tsx | 31 +++++
.../wallet/hooks/useWalletScreenData.ts | 35 +-----
.../frontend/src/contexts/WalletContext.tsx | 17 ++-
.../WalletContext.bilateralCommitted.test.tsx | 31 +++--
.../WalletContext.bilateralThrottle.test.tsx | 13 +-
.../__tests__/WalletCreditSound.test.tsx | 35 +++++-
dsm_client/frontend/src/dsm/EventBridge.ts | 20 +--
.../__tests__/EventBridge.bilateral.test.ts | 26 ++++
dsm_client/frontend/src/dsm/transactions.ts | 15 +--
.../useWalletRefreshListener.test.tsx | 56 +++++----
.../src/hooks/useWalletRefreshListener.ts | 115 ++++++------------
.../frontend/src/hooks/useWalletSync.ts | 6 +-
.../tests/E2E.bilateral.acceptFlow.test.tsx | 11 +-
.../src/tests/E2E.offlineBleExchange.test.ts | 17 ++-
.../src/tests/E2E.uiCoordination.test.tsx | 13 +-
specs/requirements/CONFORMANCE_GAPS.md | 4 +
16 files changed, 253 insertions(+), 192 deletions(-)
diff --git a/dsm_client/frontend/src/components/screens/__tests__/EnhancedWalletScreen.events.test.tsx b/dsm_client/frontend/src/components/screens/__tests__/EnhancedWalletScreen.events.test.tsx
index 0ce3ce540..3e31f749d 100644
--- a/dsm_client/frontend/src/components/screens/__tests__/EnhancedWalletScreen.events.test.tsx
+++ b/dsm_client/frontend/src/components/screens/__tests__/EnhancedWalletScreen.events.test.tsx
@@ -295,6 +295,37 @@ describe('EnhancedWalletScreen event-driven refresh', () => {
expect(button.className).toContain('has-items');
});
+ // One inbox sync with new items is one reload. The event bridge announces
+ // it as `inbox.updated` and one `wallet.refresh`; the screen used to reload
+ // on both, and on `bilateral.transferComplete` beside `wallet.refresh` too.
+ test('one inbox sync with new items reloads the wallet data once', async () => {
+ installStandardWalletMocks();
+ (dsmClient.getAllBalances as any) = jest
+ .fn()
+ .mockResolvedValue([{ tokenId: 'ERA', symbol: 'ERA', baseUnits: 100n, displayAmount: '100', decimals: 0, protocolDefined: true }]);
+ (dsmClient.getWalletHistory as any) = jest.fn().mockResolvedValue({ transactions: [] });
+
+ render(