fix(notification-services-controller): source wallet-activity addresses from the keyring - #9985
Open
oilnam wants to merge 3 commits into
Open
fix(notification-services-controller): source wallet-activity addresses from the keyring#9985oilnam wants to merge 3 commits into
oilnam wants to merge 3 commits into
Conversation
…es from the keyring Authenticated User Storage is keyed by canonical profile ID, which profile pairing shares across every SRP belonging to the same user. Storing the wallet-activity address list there therefore pooled the addresses of unrelated SRPs and delivered each installation the union of all of them, so users received notifications for addresses they do not hold. Addresses are keyring-scoped and cannot live in profile-scoped storage. Candidate addresses now come from the keyring and the per-address enabled bit from the Trigger API, so an installation can only ever ask about addresses it holds. enableAccounts/disableAccounts write subscriptions back to the Trigger API, whose per-address upsert means two installations on the same profile no longer clobber each other. createOnChainTriggers subscribes the keyring only on genuine first-time setup, gated on the absence of a preferences blob rather than the absence of subscriptions: it also runs on the daily re-subscribe, so the latter condition would re-enable every account each day for a user who had turned them all off. The user-level walletActivity in-app and push toggles are still read from Authenticated User Storage. They contain no addresses, so they remain correct to share across a paired profile. An unreadable blob now counts as both toggles enabled, so a storage outage no longer silently empties the notification list. Also unregister the device from push when no account has notifications enabled. The push API rejects a registration with no addresses, and that request is what performs the delete-and-reinsert of the device's links, so the rejection left the previous links in place and push kept arriving after a user disabled every account. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 424c450. Configure here.
cursor
Bot
requested review from
Prithpal-Sooriya,
baptiste-marchand and
zelkibuilds
August 27, 2026 12:26
…igger API config A failed Trigger API config query returned the same empty list as "no address is subscribed", so a transient outage could re-subscribe accounts the user had turned off, unregister the device from push, or report every account as disabled. Failure is now distinguishable from emptiness and each caller leaves the existing state alone instead of guessing. Subscription writes also treated a 4xx/5xx as success and cached the intended state, so the settings UI, the config cache and the push links could diverge from the server for the full cache TTL. They now reject. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Explanation
Users are receiving notifications for addresses they do not own.
Since
notification-services-controllerv24, the wallet-activity address list has been stored in Authenticated User Storage, whose notification-preferences blob is keyed by canonical profile ID. Profile pairing shares that ID across every SRP belonging to the same human, so the stored list pools the addresses of unrelated SRPs and every installation reads back the union of all of them. Addresses are keyring-scoped and cannot live in profile-scoped storage.This restores the pre-v24 arrangement: the keyring is the source of truth for which addresses exist, and the Trigger API for which of them are enabled.
What changed
checkAccountsPresence,fetchAndUpdateMetamaskNotificationsandenablePushNotificationstake the candidate addresses from the keyring and read the per-address enabled bit from the Trigger API. An installation can therefore only ever ask about addresses it actually holds.enableAccounts/disableAccountswrite subscriptions through the reintroducedupdateOnChainNotifications. The endpoint is a per-address upsert rather than a full replace, so two installations authenticating as the same profile no longer clobber each other's subscriptions.createOnChainTriggerswriteswalletActivity.accounts: []and subscribes the keyring's accounts only on genuine first-time setup.Two decisions worth reviewing
First-time subscribe is gated on the absence of a preferences blob, not the absence of subscriptions.
createOnChainTriggersalso runs on the daily re-subscribe, so the pre-v24 condition ("no subscriptions yet, enable everything") would re-enable every account each day for a user who had turned them all off. Even at first-time setup, existing subscriptions win, so a user upgrading from a client that never wrote a preferences blob keeps whichever accounts they had already disabled.The two user-level
walletActivitychannel toggles are still read from Authenticated User Storage. They contain no addresses, so sharing them across a paired profile is correct, and they need no follow-up removal. An unreadable blob now counts as both toggles enabled, matching the API's "every toggle is on unless stated otherwise" default — previously a storage outage produced an empty address list and silently emptied the notification list.Drive-by fix
The device is now unregistered from push when no account has notifications enabled. The push API rejects a registration with no addresses, and that request is what performs the delete-and-reinsert of the device's links, so the rejection left the previous links in place and push kept arriving after a user disabled every account. This was reachable before this PR too, via an all-disabled list in storage.
References
walletActivity.accountsremoved locally and are ready to raise.Checklist
Testing
292 tests pass across the package's 18 suites with coverage thresholds met. New coverage for: the Trigger API write path including address lower-casing, the daily re-subscribe not re-enabling disabled accounts, existing subscriptions surviving first-time setup, both push-unregister paths, the wallet-activity in-app toggle gating the on-chain fetch without affecting other notification types, and the fetch surviving an unavailable preferences blob.
Note
yarn lintcurrently fails on cleanmainin my environment with 89 parsing errors, all in gitignored.tsc-lint-cachefiles across unrelated packages:lint:tscgenerates those caches andbuild:only-cleandoesn't remove them before eslint runs.yarn eslintscoped to this package passes clean. Flagging in case it isn't just my machine.Made with Cursor