From fc9acade678a3a87d96dafd359118f80ae224612 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 25 Feb 2026 08:22:47 -0800 Subject: [PATCH 1/5] Stop calling onBlockHeightChanged from accountbased engines Accountbased engines called onBlockHeightChanged on every new block (polygon ~1/sec, fantom ~1/sec, etc.), which forced core-js to iterate ALL transactions to recompute confirmations. By setting confirmations directly on transactions via updateConfirmations and emitting them through onTransactions, we eliminate ~130 expensive callbacks per 10s. --- src/common/CurrencyEngine.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/CurrencyEngine.ts b/src/common/CurrencyEngine.ts index 18487ae47..00df17f1e 100644 --- a/src/common/CurrencyEngine.ts +++ b/src/common/CurrencyEngine.ts @@ -717,10 +717,12 @@ export class CurrencyEngine< this.walletLocalData.blockHeight = blockHeight this.walletLocalDataDirty = true - this.currencyEngineCallbacks.onBlockHeightChanged(blockHeight) + // Update confirmations directly on all in-memory transactions and emit + // any that changed via onTransactions. Confirmations are owned by the engine; + // core-js learns of changes only when we send txs with updated confirmations + // via onTransactions (i.e. the deprecated onBlockHeightChanged is not called). const activeTokenIds = [null, ...this.enabledTokenIds] - for (const tokenId of activeTokenIds) { const txList = this.transactionList[tokenId ?? ''] ?? [] for (let i = 0; i < txList.length; i++) { From ab97b94f187c074938dba6096a5604810b26151b Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 25 Feb 2026 08:23:46 -0800 Subject: [PATCH 2/5] Gate onNewTokens with dedup via reportDetectedTokens in CurrencyEngine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EVM engines called onNewTokens(detectedTokenIds) on every balance-check cycle — ~25 times per 10 seconds — even when the token list hadn't changed. A sorted-array comparison against the last-reported set eliminates all redundant calls. Adds reportDetectedTokens() to the base class with a per-wallet detectedTokenIds cache (persisted to walletLocalData) and updates all engine callsites to use the new method. --- src/algorand/AlgorandEngine.ts | 2 +- src/common/CurrencyEngine.ts | 10 ++++++++++ src/common/types.ts | 2 ++ src/cosmos/engine/CosmosEngine.ts | 2 +- src/ethereum/EthereumNetwork.ts | 4 ++-- src/ripple/RippleEngine.ts | 2 +- src/solana/SolanaEngine.ts | 2 +- src/sui/SuiEngine.ts | 2 +- src/tron/TronEngine.ts | 2 +- src/zano/ZanoEngine.ts | 2 +- 10 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/algorand/AlgorandEngine.ts b/src/algorand/AlgorandEngine.ts index 4118e8fa0..8e367aedc 100644 --- a/src/algorand/AlgorandEngine.ts +++ b/src/algorand/AlgorandEngine.ts @@ -208,7 +208,7 @@ export class AlgorandEngine extends CurrencyEngine< } if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } this.updateBlockHeight(round) diff --git a/src/common/CurrencyEngine.ts b/src/common/CurrencyEngine.ts index 00df17f1e..0199adb89 100644 --- a/src/common/CurrencyEngine.ts +++ b/src/common/CurrencyEngine.ts @@ -196,6 +196,7 @@ export class CurrencyEngine< publicKey: '', totalBalances: {}, numTransactions: {}, + detectedTokenIds: {}, unactivatedTokenIds: [], otherData: undefined } @@ -655,6 +656,15 @@ export class CurrencyEngine< this.syncTracker.balanceComplete?.(tokenId) } + reportDetectedTokens(tokenIds: string[]): void { + const known = this.walletLocalData.detectedTokenIds + const newTokenIds = tokenIds.filter(id => known[id] == null) + if (newTokenIds.length === 0) return + for (const id of newTokenIds) known[id] = true + this.walletLocalDataDirty = true + this.currencyEngineCallbacks.onNewTokens(Object.keys(known)) + } + updateConfirmations(tx: EdgeTransaction): boolean { // No update needed for these status switch (tx.confirmations) { diff --git a/src/common/types.ts b/src/common/types.ts index 6972edb93..f73e68c23 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,5 +1,6 @@ import { asArray, + asBoolean, asCodec, asEither, asMaybe, @@ -60,6 +61,7 @@ export const asWalletLocalData = asObject({ asObject(asNumber), () => ({}) ), + detectedTokenIds: asMaybe(asObject(asBoolean), () => ({})), unactivatedTokenIds: asMaybe(asArray(asString), () => []), otherData: asOptional(asUnknown, () => ({})) }) diff --git a/src/cosmos/engine/CosmosEngine.ts b/src/cosmos/engine/CosmosEngine.ts index fb787e183..a132a65cc 100644 --- a/src/cosmos/engine/CosmosEngine.ts +++ b/src/cosmos/engine/CosmosEngine.ts @@ -637,7 +637,7 @@ export class CosmosEngine extends CurrencyEngine< }) if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } if (this.stakingSupported) { diff --git a/src/ethereum/EthereumNetwork.ts b/src/ethereum/EthereumNetwork.ts index 182616763..4bf26eddf 100644 --- a/src/ethereum/EthereumNetwork.ts +++ b/src/ethereum/EthereumNetwork.ts @@ -456,7 +456,7 @@ export class EthereumNetwork { for (const [tokenId, bal] of tokenBal) { this.ethEngine.updateBalance(tokenId, bal) } - this.ethEngine.currencyEngineCallbacks.onNewTokens( + this.ethEngine.reportDetectedTokens( ethereumNetworkUpdate.detectedTokenIds ?? [] ) } @@ -723,7 +723,7 @@ function makeThrottledFunction( fn: (...args: Args) => Promise ): () => Promise { let lastTime = 0 - let lastTimeout: NodeJS.Timeout | undefined + let lastTimeout: ReturnType | undefined return async (...args: Args) => { return await new Promise((resolve, reject) => { const timeSinceLast = Date.now() - lastTime diff --git a/src/ripple/RippleEngine.ts b/src/ripple/RippleEngine.ts index b0f614de7..ead24cdeb 100644 --- a/src/ripple/RippleEngine.ts +++ b/src/ripple/RippleEngine.ts @@ -705,7 +705,7 @@ export class XrpEngine extends CurrencyEngine< }) if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } // If get here, we've checked balances for all possible tokens the user diff --git a/src/solana/SolanaEngine.ts b/src/solana/SolanaEngine.ts index be6c8bd6c..b24a56c17 100644 --- a/src/solana/SolanaEngine.ts +++ b/src/solana/SolanaEngine.ts @@ -217,7 +217,7 @@ export class SolanaEngine extends CurrencyEngine< } if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } } catch (e: any) { // Nodes will return 0 for uninitiated accounts so thrown errors should be logged diff --git a/src/sui/SuiEngine.ts b/src/sui/SuiEngine.ts index f8727e87e..e354324e2 100644 --- a/src/sui/SuiEngine.ts +++ b/src/sui/SuiEngine.ts @@ -157,7 +157,7 @@ export class SuiEngine extends CurrencyEngine< } if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } this.syncTracker.setBalanceRatios([null, ...this.enabledTokenIds], 1) diff --git a/src/tron/TronEngine.ts b/src/tron/TronEngine.ts index 0a73d323a..1c0601b52 100644 --- a/src/tron/TronEngine.ts +++ b/src/tron/TronEngine.ts @@ -256,7 +256,7 @@ export class TronEngine extends CurrencyEngine< } if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } } catch (e) { this.log.error('checkTokenBalances error', e) diff --git a/src/zano/ZanoEngine.ts b/src/zano/ZanoEngine.ts index 612955c55..6f7bd4c17 100644 --- a/src/zano/ZanoEngine.ts +++ b/src/zano/ZanoEngine.ts @@ -192,7 +192,7 @@ export class ZanoEngine extends CurrencyEngine< } if (detectedTokenIds.length > 0) { - this.currencyEngineCallbacks.onNewTokens(detectedTokenIds) + this.reportDetectedTokens(detectedTokenIds) } this.syncTracker.updateBalanceRatio(1) From 2679dbb593a0ed8090e5cfe2063f0e5447a1268a Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 25 Feb 2026 08:24:27 -0800 Subject: [PATCH 3/5] Gate onStakingStatusChanged to only fire on change Tron and FIO engines called onStakingStatusChanged on every poll cycle (1-3 per 10s) with identical staking data. A JSON.stringify comparison against the previous status in reportStakingStatus() eliminates redundant dispatches. --- src/common/CurrencyEngine.ts | 10 ++++++++++ src/cosmos/engine/CosmosEngine.ts | 2 +- src/fio/FioEngine.ts | 8 ++------ src/tron/TronEngine.ts | 4 +--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/common/CurrencyEngine.ts b/src/common/CurrencyEngine.ts index 0199adb89..e2d5e9b85 100644 --- a/src/common/CurrencyEngine.ts +++ b/src/common/CurrencyEngine.ts @@ -15,6 +15,7 @@ import { EdgeLog, EdgeMetaToken, EdgeSpendInfo, + EdgeStakingStatus, EdgeSubscribedAddress, EdgeSyncStatus, EdgeToken, @@ -656,6 +657,15 @@ export class CurrencyEngine< this.syncTracker.balanceComplete?.(tokenId) } + private lastStakingStatusJson: string = '' + + protected reportStakingStatus(status: EdgeStakingStatus): void { + const json = JSON.stringify(status) + if (json === this.lastStakingStatusJson) return + this.lastStakingStatusJson = json + this.currencyEngineCallbacks.onStakingStatusChanged(status) + } + reportDetectedTokens(tokenIds: string[]): void { const known = this.walletLocalData.detectedTokenIds const newTokenIds = tokenIds.filter(id => known[id] == null) diff --git a/src/cosmos/engine/CosmosEngine.ts b/src/cosmos/engine/CosmosEngine.ts index a132a65cc..8a4e9be4b 100644 --- a/src/cosmos/engine/CosmosEngine.ts +++ b/src/cosmos/engine/CosmosEngine.ts @@ -656,7 +656,7 @@ export class CosmosEngine extends CurrencyEngine< } ] } - this.currencyEngineCallbacks.onStakingStatusChanged(stakingStatus) + this.reportStakingStatus(stakingStatus) this.stakedBalanceCache = stakedBalance.amount } } catch (e) { diff --git a/src/fio/FioEngine.ts b/src/fio/FioEngine.ts index a2691b75e..e8a580fbc 100644 --- a/src/fio/FioEngine.ts +++ b/src/fio/FioEngine.ts @@ -318,9 +318,7 @@ export class FioEngine extends CurrencyEngine< super.doInitialBalanceCallback() try { - this.currencyEngineCallbacks.onStakingStatusChanged({ - ...this.otherData.stakingStatus - }) + this.reportStakingStatus({ ...this.otherData.stakingStatus }) } catch (e: any) { this.error(`doInitialBalanceCallback onStakingStatusChanged`, e) } @@ -428,9 +426,7 @@ export class FioEngine extends CurrencyEngine< } this.localDataDirty() try { - this.currencyEngineCallbacks.onStakingStatusChanged({ - ...this.otherData.stakingStatus - }) + this.reportStakingStatus({ ...this.otherData.stakingStatus }) } catch (e: any) { this.error('onStakingStatusChanged error') } diff --git a/src/tron/TronEngine.ts b/src/tron/TronEngine.ts index 1c0601b52..53bb4aff0 100644 --- a/src/tron/TronEngine.ts +++ b/src/tron/TronEngine.ts @@ -345,9 +345,7 @@ export class TronEngine extends CurrencyEngine< } this.stakingStatus = { stakedAmounts } - this.currencyEngineCallbacks.onStakingStatusChanged({ - ...this.stakingStatus - }) + this.reportStakingStatus({ ...this.stakingStatus }) } catch (e: any) { this.log.error('Error checking TRX address balance: ', e) } From 6f96cb43aa3f2158909b3f8f3caa68b40b079eaa Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 25 Feb 2026 08:24:54 -0800 Subject: [PATCH 4/5] Gate SyncTracker to only fire sendSyncStatus on ratio change Engines that were already fully synced (ratio=1.0) kept firing onSyncStatusChanged hundreds of times. Solana alone produced 129 calls per 10 seconds at steady state. sendSyncStatusIfChanged suppresses callbacks when the ratio hasn't changed since the last emission. --- src/common/SyncTracker.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/common/SyncTracker.ts b/src/common/SyncTracker.ts index 7429976ba..9b4407c0e 100644 --- a/src/common/SyncTracker.ts +++ b/src/common/SyncTracker.ts @@ -43,6 +43,7 @@ export function makeTokenSyncTracker(engine: SyncEngine): TokenSyncTracker { // Each tokenId can be a 0-1 value: const balanceRatios = new Map() const historyRatios = new Map() + let lastSyncStatus: EdgeSyncStatus | undefined function getSyncStatus(): EdgeSyncStatus { const activeTokenIds = [null, ...engine.enabledTokenIds] @@ -66,10 +67,22 @@ export function makeTokenSyncTracker(engine: SyncEngine): TokenSyncTracker { return { totalRatio } } + function sendSyncStatusIfChanged(): void { + const currentStatus = getSyncStatus() + if ( + lastSyncStatus == null || + lastSyncStatus.totalRatio !== currentStatus.totalRatio + ) { + lastSyncStatus = currentStatus + engine.sendSyncStatus(currentStatus) + } + } + const out: TokenSyncTracker = { resetSync() { balanceRatios.clear() historyRatios.clear() + lastSyncStatus = undefined }, balanceComplete(tokenId) { @@ -78,12 +91,12 @@ export function makeTokenSyncTracker(engine: SyncEngine): TokenSyncTracker { setBalanceRatios(tokenIds, ratio) { for (const tokenId of tokenIds) balanceRatios.set(tokenId, ratio) - engine.sendSyncStatus(getSyncStatus()) + sendSyncStatusIfChanged() }, setHistoryRatios(tokenIds, ratio) { for (const tokenId of tokenIds) historyRatios.set(tokenId, ratio) - engine.sendSyncStatus(getSyncStatus()) + sendSyncStatusIfChanged() }, updateBalanceRatio(tokenId, ratio) { @@ -94,7 +107,7 @@ export function makeTokenSyncTracker(engine: SyncEngine): TokenSyncTracker { if (ratio <= lastRatio) return balanceRatios.set(tokenId, ratio) - engine.sendSyncStatus(getSyncStatus()) + sendSyncStatusIfChanged() }, updateHistoryRatio(tokenId, ratio, minStep) { @@ -108,7 +121,7 @@ export function makeTokenSyncTracker(engine: SyncEngine): TokenSyncTracker { if (minStep != null && ratio - lastRatio < minStep && ratio < 1) return historyRatios.set(tokenId, ratio) - engine.sendSyncStatus(getSyncStatus()) + sendSyncStatusIfChanged() } } From 93692e0e676a502839c4973c5e915da2e0952358 Mon Sep 17 00:00:00 2001 From: Paul V Puey Date: Wed, 25 Feb 2026 08:25:10 -0800 Subject: [PATCH 5/5] Add global throttle on accountbased sendSyncStatus (500ms) Even with per-engine dedup, 30+ accountbased engines syncing simultaneously produced ~42 onSyncStatusChanged calls per 10 seconds. A global rate limiter (max 1 per 500ms, totalRatio=1 always passes) limits aggregate volume during the post-login sync burst. --- src/common/SyncTracker.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/common/SyncTracker.ts b/src/common/SyncTracker.ts index 9b4407c0e..f55abedc6 100644 --- a/src/common/SyncTracker.ts +++ b/src/common/SyncTracker.ts @@ -1,5 +1,8 @@ import type { EdgeSyncStatus, EdgeTokenId } from 'edge-core-js/types' +// Global throttle: max 1 sendSyncStatus per 500ms; totalRatio=1 always passes. +let ssLastEmitTime = 0 + /** * Abstracts the ability to return a sync status, * since different chains track their sync status in different ways. @@ -74,6 +77,13 @@ export function makeTokenSyncTracker(engine: SyncEngine): TokenSyncTracker { lastSyncStatus.totalRatio !== currentStatus.totalRatio ) { lastSyncStatus = currentStatus + + if (currentStatus.totalRatio !== 1) { + const now = Date.now() + if (now - ssLastEmitTime < 500) return + ssLastEmitTime = now + } + engine.sendSyncStatus(currentStatus) } }