diff --git a/src/common/plugin/EngineEmitter.ts b/src/common/plugin/EngineEmitter.ts index 72d0c02e..b77351e0 100644 --- a/src/common/plugin/EngineEmitter.ts +++ b/src/common/plugin/EngineEmitter.ts @@ -92,7 +92,46 @@ export declare interface EngineEmitter { listener: (txids: EdgeTxidMap) => Promise | void ) => this) } -export class EngineEmitter extends EventEmitter {} +// Global throttle: max 1 emitAddressesChecked per 500ms; ratio=1 always passes. +let acLastEmitTime = 0 + +export class EngineEmitter extends EventEmitter { + private lastBlockHeight?: number + private lastWalletBalance?: string + private lastAddressesCheckedRatio?: number + + emitBlockHeightChanged(uri: string, blockHeight: number): boolean { + if (this.lastBlockHeight === blockHeight) return false + this.lastBlockHeight = blockHeight + return super.emit(EngineEvent.BLOCK_HEIGHT_CHANGED, uri, blockHeight) + } + + emitWalletBalanceChanged( + currencyCode: string, + nativeBalance: string + ): boolean { + if (this.lastWalletBalance === nativeBalance) return false + this.lastWalletBalance = nativeBalance + return super.emit( + EngineEvent.WALLET_BALANCE_CHANGED, + currencyCode, + nativeBalance + ) + } + + emitAddressesChecked(progressRatio: number): boolean { + if (this.lastAddressesCheckedRatio === progressRatio) return false + this.lastAddressesCheckedRatio = progressRatio + + if (progressRatio !== 1) { + const now = Date.now() + if (now - acLastEmitTime < 500) return false + acLastEmitTime = now + } + + return super.emit(EngineEvent.ADDRESSES_CHECKED, progressRatio) + } +} export enum EngineEvent { SEEN_TX_CHECKPOINT = 'seen:tx:checkpoint', @@ -116,12 +155,9 @@ export const makeEngineEmitter = ( const emitter = new EngineEmitter() emitter.on(EngineEvent.ADDRESSES_CHECKED, callbacks.onAddressesChecked) - emitter.on( - EngineEvent.BLOCK_HEIGHT_CHANGED, - (_uri: string, height: number) => { - callbacks.onBlockHeightChanged(height) - } - ) + // BLOCK_HEIGHT_CHANGED is used internally (e.g. to check unconfirmed txs) + // but is no longer forwarded to core-js. UTXO txs already set + // confirmations directly, so onBlockHeightChanged is a no-op in core-js. emitter.on(EngineEvent.SEEN_TX_CHECKPOINT, callbacks.onSeenTxCheckpoint) emitter.on(EngineEvent.TRANSACTIONS, callbacks.onTransactions) emitter.on(EngineEvent.TRANSACTIONS_CHANGED, callbacks.onTransactionsChanged) diff --git a/src/common/plugin/Metadata.ts b/src/common/plugin/Metadata.ts index bec6d8cf..3c74560e 100644 --- a/src/common/plugin/Metadata.ts +++ b/src/common/plugin/Metadata.ts @@ -46,11 +46,7 @@ export const makeMetadata = async ( ) cache.balance = cumulativeBalance await setMetadata(cache) - emitter.emit( - EngineEvent.WALLET_BALANCE_CHANGED, - currencyCode, - cumulativeBalance - ) + emitter.emitWalletBalanceChanged(currencyCode, cumulativeBalance) } emitter.on( diff --git a/src/common/utxobased/db/Models/TransactionData.ts b/src/common/utxobased/db/Models/TransactionData.ts index 19e17b13..e97cbbc2 100644 --- a/src/common/utxobased/db/Models/TransactionData.ts +++ b/src/common/utxobased/db/Models/TransactionData.ts @@ -1,7 +1,11 @@ import { Transaction } from 'altcoin-js' import { lt } from 'biggystring' import BN from 'bn.js' -import { EdgeTransaction, JsonObject } from 'edge-core-js/types' +import { + EdgeConfirmationState, + EdgeTransaction, + JsonObject +} from 'edge-core-js/types' import { PluginInfo } from '../../../plugin/types' import { UtxoTxOtherParams } from '../../engine/types' @@ -91,9 +95,16 @@ export const toEdgeTransaction = async ( } } catch (e) {} + // Calculate confirmations: for UTXO chains with requiredConfirmations = 1 (default), + // any transaction in a block is immediately confirmed + let confirmations: EdgeConfirmationState = tx.confirmations ?? 'unconfirmed' + if (confirmations === 'unconfirmed' || confirmations === undefined) { + confirmations = tx.blockHeight > 0 ? 'confirmed' : 'unconfirmed' + } + return { blockHeight: tx.blockHeight, - confirmations: tx.confirmations, + confirmations, currencyCode: currencyInfo.currencyCode, date: tx.date, feeRateUsed, diff --git a/src/common/utxobased/engine/ServerStates.ts b/src/common/utxobased/engine/ServerStates.ts index 18d289ab..ad936fb9 100644 --- a/src/common/utxobased/engine/ServerStates.ts +++ b/src/common/utxobased/engine/ServerStates.ts @@ -309,7 +309,7 @@ export function makeServerStates(config: ServerStateConfig): ServerStates { serverStatesCache[uri].blockHeight = blockHeight // Emit initial BLOCK_HEIGHT_CHANGED event - engineEmitter.emit(EngineEvent.BLOCK_HEIGHT_CHANGED, uri, blockHeight) + engineEmitter.emitBlockHeightChanged(uri, blockHeight) // Increment server score using response time const responseTime = Date.now() - startTime diff --git a/src/common/utxobased/engine/UtxoEngineProcessor.ts b/src/common/utxobased/engine/UtxoEngineProcessor.ts index 6f6b16fa..758a3ca2 100644 --- a/src/common/utxobased/engine/UtxoEngineProcessor.ts +++ b/src/common/utxobased/engine/UtxoEngineProcessor.ts @@ -157,7 +157,7 @@ export function makeUtxoEngineProcessor( **/ const processesPerAddress = 2 let processedCount = 0 - let processedPercent = 0 // last sync ratio emitted + let processedPercent = 0 // in-memory high-water-mark; resets to 0 on engine creation const updateProgressRatio = async (): Promise => { // Avoid re-sending sync ratios / sending ratios larger than 1 if (processedPercent >= 1) return @@ -174,13 +174,19 @@ export function makeUtxoEngineProcessor( if (expectedProcessCount === 0) throw new Error('No addresses to process') const percent = processedCount / expectedProcessCount + + // High-water-mark: never report progress going backwards. This can happen + // when setLookAhead discovers new addresses and inflates expectedProcessCount, + // or when start() is called again (pause/unpause) and processedCount resets. + if (percent <= processedPercent) return + if (percent - processedPercent > CACHE_THROTTLE || percent === 1) { log( `processed changed, percent: ${percent}, processedCount: ${processedCount}, totalCount: ${expectedProcessCount}` ) processedPercent = percent common.updateSeenTxCheckpoint() - emitter.emit(EngineEvent.ADDRESSES_CHECKED, percent) + emitter.emitAddressesChecked(percent) } } @@ -360,8 +366,11 @@ export function makeUtxoEngineProcessor( return { processedPercent, async start(): Promise { + // Reset the count so the address walk restarts, but keep processedPercent + // as an in-memory high-water-mark. This prevents the reported ratio from + // rolling backwards if start() is called again (pause/unpause) or if + // setLookAhead discovers new addresses and inflates the denominator. processedCount = 0 - processedPercent = 0 await run() serverStates.refillServers() @@ -1261,9 +1270,12 @@ async function* processAddressForTransactions( // The tx unconfirmed or confirmed after/at the last seenTxCheckpoint (tx.blockHeight === 0 || tx.blockHeight > seenTxBlockHeight) - common.emitter.emit(EngineEvent.TRANSACTIONS, [ - { isNew, transaction: edgeTx } - ]) + // Only emit if tx is new or changed (blockHeight changed) + if (existingTx == null || existingTx.blockHeight !== tx.blockHeight) { + common.emitter.emit(EngineEvent.TRANSACTIONS, [ + { isNew, transaction: edgeTx } + ]) + } if (edgeTx.blockHeight > common.maxSeenTxBlockHeight) { common.maxSeenTxBlockHeight = edgeTx.blockHeight