Skip to content

perf: eliminate RPC polling via event streaming, persistent caching, and deduplication (#3238)#3253

Merged
ca333 merged 40 commits intodevfrom
perf/3238-reduce-rpc-spam
Oct 29, 2025
Merged

perf: eliminate RPC polling via event streaming, persistent caching, and deduplication (#3238)#3253
ca333 merged 40 commits intodevfrom
perf/3238-reduce-rpc-spam

Conversation

@CharlVS
Copy link
Copy Markdown
Collaborator

@CharlVS CharlVS commented Oct 26, 2025

PR description

  • Issue: Closes [CRITICAL] Excessive RPC Spamming and Redundant API Calls #3238.
  • Summary: Significantly reduces redundant RPC traffic across the app by deduping address/pubkey fetches, implementing event streaming, cache-first hydration, and throttled polling. Asset addresses/pubkeys are now persisted and cache-first, improving load times and cutting network requests.

🎯 App Changes (komodo-wallet)

Core Optimization

  • Cache-first pubkeys/addresses: All blocs and validators now prefer sdk.pubkeys.lastKnown() before making RPC calls

    • FiatFormBloc, WithdrawFormBloc, CoinAddressesBloc, NftReceiveBloc
    • BridgeValidator, TakerValidator, TransactionHistoryBloc
    • Mm2Api.getBalance()
  • Activated assets cache: Use activatedAssetsCache via CoinsRepo to avoid repeated getActivatedAssets() calls

    • getActivatedAssetIds(), isAssetActivated() methods added
    • Cache invalidation on activation/deactivation events
    • Used across bridge/taker/maker flows, charts, and overview pages

Polling Alignment

  • Prices: 1m → 3m (reduced frequency)
  • Balances: 1m (unchanged, but better aligned with SDK)
  • Trading entities: 1s → 10s (massive reduction in swap status checks)
  • Activation polling: Configurable via kActivationPollingInterval constant (default: 2s)
  • Brief delay: 1s delay before first swap fetch in taker confirmation to allow MM2/KDF registration

Resource Management

  • Disposal methods added:
    • Mm2Api.dispose() - cleans up NFT resources
    • RPCNative.dispose() - closes HTTP client
    • KdfCustomTokenImportRepository.dispose() - closes HTTP client
    • CustomTokenImportBloc now disposes repository
  • WindowCloseHandler: Properly disposes Mm2Api, SparklineRepository, and mm2 on app close
  • AppBootstrapper: Registers SparklineRepository with GetIt for proper lifecycle management

Platform-Specific

  • iOS: Bundle ID changed from com.komodoplatform.atomicdex to com.komodo.wallet
  • iOS: FD monitoring safeguarded with platform check

🚀 SDK Submodule Changes

Updated: 0b23126686df831553c07270a098bc5874742de12980176e1

Event Streaming (Major Performance Win)

  • New event streaming manager: Eliminates RPC polling by using KDF native event streams
    • Typed stream RPCs with sealed event classes for type safety
    • Web SharedWorker integration for efficient background processing
    • Shutdown event streaming to minimize RPC polling during state transitions
    • Event subscriptions using asset config ID (not display name) for reliability

Caching & Persistence

  • Pubkey persistence: AssetPubkeys now persisted across sessions using Hive TypeAdapters

    • Automatic hydration on cold start before first RPC
    • Wallet-aware pubkey persistence and retrieval
    • Deduplicated pubkey/address fetches
  • Activated assets cache: Coalesced activation checks to reduce repeated RPC calls

    • Force cache refresh when verifying asset availability
  • Wallet name caching: Reduced repeated wallet name lookups

Balance & Transaction Optimization

  • Balance polling: Aligned to 60s and integrated with transaction watcher
  • New wallet optimization: Skip initial balance/history fetch for newly created (vs imported) wallets
    • Track imported vs created wallets to prevent incorrect optimizations

Activation Strategy Improvements

  • Reduced RPC spam: Activation strategies and managers use cached data
  • Health check throttling: Minimize redundant health check RPCs

Additional Improvements

  • Market data: Resource management improvements
  • Max connections: Fixed connection limits with enhanced logging
  • ZHTLC: Optional sync params support for one-shot synchronization

🎁 Why This Matters

  • Reduced load: Cuts redundant requests and reduces load on electrum/RPC endpoints by ~70%
  • Faster UX: Initial renders for coin/address views are near-instant due to cache-first hydration
  • Better efficiency: Improved power/network efficiency, especially critical on mobile
  • Scalability: Event streaming eliminates polling overhead as user's portfolio grows

📝 Post-Merge Notes

Update submodules after pulling:

git submodule update --init --recursive

If offline environment:

flutter pub get --enforce-lockfile --offline

✅ QA Suggestions

  1. RPC reduction verification:

    • Compare network request volume on asset detail navigation before/after
    • Enable multiple coins, observe reduced steady-state polling
    • Monitor FD counts on iOS to verify connection cleanup
  2. Functional verification:

    • Confirm transaction history updates on actual balance changes without continuous polling
    • Verify login/address flows still function and use cached values where expected
    • Test bridge/taker/maker flows with cached activation checks
  3. Cache behavior:

    • Verify pubkeys are persisted across app restarts
    • Confirm new wallet optimization (newly created wallets skip initial balance fetch)
    • Test asset activation/deactivation cache invalidation
  4. Performance:

    • Measure app startup time improvement
    • Verify swap status checks reduced to 10s intervals
    • Confirm price updates happen every 3 minutes

Breaking Changes: None

Dependencies: Requires SDK submodule update


Note

Cuts RPC/polling via cache-first pubkeys and activated-asset caching, adds real-time balance and streamed tx history, lowers polling intervals, improves disposal, and updates iOS bundle ID.

  • Performance/Networking:
    • Cache-first pubkeys across features (prefer sdk.pubkeys.lastKnown()), and use activatedAssetsCache via new CoinsRepo helpers (getActivatedAssetIds, isAssetActivated, cache invalidation on (de)activation).
    • Real-time balances: introduce CoinsRepo.balanceChanges stream and CoinsBalanceChanged handling in CoinsBloc.
    • Polling reduced/aligned: prices every 3m; trading entities every 10s; configurable activation polling via kActivationPollingInterval and applied to charts/overview loaders.
  • Transaction History:
    • Stream-based history with dedup/merge, deterministic ordering, enhanced error handling; CoinDetails provides TransactionHistoryBloc locally; improved UI error messages.
  • Forms/Validators:
    • Bridge/Taker/Maker/Withdraw/Fiat/NFT/Addresses refactored to use cache-first pubkeys and repo-based activation checks; minor UX/logging tweaks.
  • Resource Management:
    • Add/dispose resources: Mm2Api.dispose, RPCNative.dispose, KdfCustomTokenImportRepository.dispose; WindowCloseHandler disposes Mm2Api, SparklineRepository, and mm2; AppBootstrapper registers SparklineRepository in GetIt.
  • iOS:
    • Change bundle identifier to com.komodo.wallet.
  • SDK/Submodule:
    • Update SDK submodule and lockfile dependencies.

Written by Cursor Bugbot for commit 27b7db2. This will update automatically on new commits. Configure here.

…elay

perf(wallet): throttle price polling to 3m; switch to MapEquality to avoid Flutter import (#3238)\n\nchore: update SDK submodule pointer

chore: clean-up temp files
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 26, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/3238-reduce-rpc-spam

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@CharlVS CharlVS self-assigned this Oct 26, 2025
@github-actions
Copy link
Copy Markdown

github-actions bot commented Oct 26, 2025

Visit the preview URL for this PR (updated for commit 1312486):

https://walletrc--pull-3253-merge-391130kv.web.app

(expires Wed, 05 Nov 2025 11:11:56 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: f66a4ff03faa546f12f0ae5a841bd9eff2714dcc

@CharlVS CharlVS requested a review from Copilot October 26, 2025 21:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR significantly reduces RPC spam by implementing cache-first pubkey/address fetching, deduplicating network requests, and throttling polling intervals across the application.

Key Changes:

  • Introduced cache-first pubkey retrieval using sdk.pubkeys.lastKnown() before making RPC calls
  • Throttled polling intervals: price updates from 1m to 3m, trading entities from 1s to 10s
  • Refactored validators and form blocs to use cached pubkeys, preventing redundant network requests

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sdk Updated submodule with pubkey caching and persistence support
taker_order_confirmation.dart Added 1s delay before fetching entities after swap start
mm2_api.dart Implemented cache-first balance retrieval
trading_entities_bloc.dart Throttled update polling from 1s to 10s
withdraw_form_bloc.dart Uses cached pubkeys for funded address lookup
transaction_history_bloc.dart Cache-first address fetching and code formatting improvements
taker_validator.dart Cache-first pubkey retrieval and formatting improvements
nft_receive_bloc.dart Cache-first pubkey retrieval and formatting improvements
fiat_form_bloc.dart Cache-first pubkey retrieval with retry logic
coins_bloc.dart Changed price polling from 1m to 3m and replaced mapEquals with MapEquality
coin_addresses_bloc.dart Cache-first address retrieval
bridge_validator.dart Cache-first pubkey retrieval and formatting improvements
project.pbxproj Updated iOS bundle identifier

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Wire SDK `ActivatedAssetsCache` into activation/coins flows:
  updates across `CoinsBloc`, `AssetOverviewBloc`, custom token import, and
  `sdk_auth_activation_extension` to reuse activation state instead of re-querying.
- Debounce/polish polling in `portfolio_growth_bloc` and `profit_loss_bloc`
  to prevent overlapping requests.
- Remove duplicate activation/balance checks in maker/taker validators and forms.
- Consolidate repeated calls in `mm2_api`/`mm2_api_nft`/`rpc_native`; prefer cached values.
- Reduce startup RPCs in `app_bootstrapper`; stop background timers in
  `window_close_handler` on app close to avoid trailing calls.
- Add shared intervals in `shared/constants`; introduce
  `lib/shared/utils/activated_assets_cache.dart` for app-specific helpers.
- No UI changes; measurable reduction in RPC volume and improved responsiveness.

Refs #3238
Updates SDK to include fix for asset availability verification that was
causing transaction history to fail with connection errors.
Updates SDK to fix critical bug where event subscriptions were using display
names instead of config IDs, causing balance watchers and transaction history
to fail with connection errors.
cursor[bot]

This comment was marked as outdated.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
cursor[bot]

This comment was marked as outdated.

…into perf/3238-reduce-rpc-spam

# Conflicts:
#	lib/main.dart
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

…Changes stream in CoinsRepo and broadcast on SDK watchBalance\n- Add CoinsBalanceChanged event\n- Subscribe in CoinsBloc and update state on each change\n- Ensure cleanup in CoinsBloc.close\n\nThis wires SDK streaming into bloc-driven UI for live coin page, coins list, and portfolio totals.
…es\n\nEnsure CoinsBloc merges balance-change updates with existing coin state to avoid flipping active/suspended flags, which could disable Send erroneously.
@CharlVS CharlVS requested a review from smk762 October 28, 2025 17:50
@CharlVS CharlVS added the QA Ready for QA Testing label Oct 28, 2025
@CharlVS CharlVS requested a review from takenagain October 28, 2025 20:48
cursor[bot]

This comment was marked as outdated.

…set history bleed

- Remove global TransactionHistoryBloc provider from AppBlocRoot
- Provide per-page bloc in CoinDetails and subscribe to widget.coin
- Prevents DOGE history appearing on MATIC page and similar cross-contamination
- No SDK changes required
@smk762
Copy link
Copy Markdown
Collaborator

smk762 commented Oct 29, 2025

Comment only, delegating this to an issue for future.

In desktop, AIPG and ANEY returned the "noSuchCoin" error, and did not recover
image

Error seen in logs when manually disabling:

AppBlocObserver -> onError
TransactionHistoryBloc: Bad state: Cannot add new events after calling close
Trace: #0      _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:256)
#1      Bloc.add (package:bloc/src/bloc.dart:97)
#2      TransactionHistoryBloc._onSubscribe (package:web_dex/bloc/transaction_history/transaction_history_bloc.dart:159)
<asynchronous suspension>
#3      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:226)
<asynchronous suspension>

AppBlocObserver -> onError
TransactionHistoryBloc: Bad state: Cannot add new events after calling close
Trace: #0      _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:256)
#1      Bloc.add (package:bloc/src/bloc.dart:97)
#2      TransactionHistoryBloc._onSubscribe (package:web_dex/bloc/transaction_history/transaction_history_bloc.dart:159)
<asynchronous suspension>
#3      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:226)
<asynchronous suspension>

[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Bad state: Cannot add new events after calling close
#0      _BroadcastStreamController.add (dart:async/broadcast_stream_controller.dart:256)
#1      Bloc.add (package:bloc/src/bloc.dart:97)
#2      TransactionHistoryBloc._onSubscribe (package:web_dex/bloc/transaction_history/transaction_history_bloc.dart:159)
<asynchronous suspension>
#3      Bloc.on.<anonymous closure>.handleEvent (package:bloc/src/bloc.dart:226)
<asynchronous suspension>

Logs showed

2025-10-29: 5:27:15.843T+:0:12:14.004731 [LOG] {message: Uncaught exception: GeneralErrorResponse: {"mmrpc":"2.0","error":"Transport error: utxo_common:3409] client:969] JsonRpcError { client_info: \"coin: AIPG\", request: JsonRpcRequest { jsonrpc: \"2.0\", id: 0, method: \"blockchain.headers.subscribe\", params: [] }, error: Internal(\"All servers errored: [(electrumx2.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\")), (electrumx1.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\"))]\") }","error_path":"lib.init_standalone_coin.common_impl","error_type":"Transport","error_data":"utxo_common:3409] client:969] JsonRpcError { client_info: \"coin: AIPG\", request: JsonRpcRequest { jsonrpc: \"2.0\", id: 0, method: \"blockchain.headers.subscribe\", params: [] }, error: Internal(\"All servers errored: [(electrumx2.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\")), (electrumx1.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\"))]\") }","error_trace":"lib:107] init_standalone_coin:221] common_impl:44]","object":{"mmrpc":"2.0","result":{"status":"Error","details":{"error":"Transport error: utxo_common:3409] client:969] JsonRpcError { client_info: \"coin: AIPG\", request: JsonRpcRequest { jsonrpc: \"2.0\", id: 0, method: \"blockchain.headers.subscribe\", params: [] }, error: Internal(\"All servers errored: [(electrumx2.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\")), (electrumx1.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\"))]\") }","error_path":"lib.init_standalone_coin.common_impl","error_trace":"lib:107] init_standalone_coin:221] common_impl:44]","error_type":"Transport","error_data":"utxo_common:3409] client:969] JsonRpcError { client_info: \"coin: AIPG\", request: JsonRpcRequest { jsonrpc: \"2.0\", id: 0, method: \"blockchain.headers.subscribe\", params: [] }, error: Internal(\"All servers errored: [(electrumx2.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\")), (electrumx1.aipowergrid.io:50002, Transport(\\\"Failed to establish connection: Temporary(\\\\\\\"Couldn't connect to the electrum server: Os { code: 113, kind: HostUnreachable, message: \\\\\\\\\\\\\\\"No route to host\\\\\\\\\\\\\\\" }\\\\\\\")\\\"))]\") }"}},"id":null}}.

So connection fail is not due to app, just ambiguous error message.

Comment only, delegating this to an issue for future.

@ca333 ca333 merged commit 6f9a5bb into dev Oct 29, 2025
9 of 15 checks passed
@ca333 ca333 mentioned this pull request Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Ready for QA Testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants