Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions src/common/plugin/EngineEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,46 @@ export declare interface EngineEmitter {
listener: (txids: EdgeTxidMap) => Promise<void> | 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)
}
Copy link

Choose a reason for hiding this comment

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

Throttled progress ratios pollute dedup cache prematurely

Low Severity

emitAddressesChecked sets lastAddressesCheckedRatio before the global 500ms throttle check. When the throttle suppresses an emission, the dedup cache records a value that was never actually delivered to listeners. This makes the "last-value tracking" semantically incorrect — it tracks the last input rather than the last emitted value. In the current single-call-site usage with monotonically increasing ratios, this has no observable effect, but it introduces a latent correctness issue if the method is ever reused elsewhere.

Fix in Cursor Fix in Web

}

export enum EngineEvent {
SEEN_TX_CHECKPOINT = 'seen:tx:checkpoint',
Expand All @@ -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)
Expand Down
6 changes: 1 addition & 5 deletions src/common/plugin/Metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 13 additions & 2 deletions src/common/utxobased/db/Models/TransactionData.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/common/utxobased/engine/ServerStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 18 additions & 6 deletions src/common/utxobased/engine/UtxoEngineProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
// Avoid re-sending sync ratios / sending ratios larger than 1
if (processedPercent >= 1) return
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -360,8 +366,11 @@ export function makeUtxoEngineProcessor(
return {
processedPercent,
async start(): Promise<void> {
// 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()
Expand Down Expand Up @@ -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 }
])
}
Copy link

Choose a reason for hiding this comment

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

Transaction emission skipped when ourAmount changes but blockHeight unchanged

Medium Severity

The new dedup guard only emits when existingTx is null or blockHeight changed, but saveTransaction can also change ourOuts, ourIns, and ourAmount when a new scriptPubkey is discovered for an already-known transaction. During initial sync, when setLookAhead discovers a new address that is an output of an existing confirmed transaction, the merged edgeTx (with updated nativeAmount) is saved to the DataLayer but never emitted to core-js. The transaction list then displays a stale nativeAmount indefinitely for that transaction.

Fix in Cursor Fix in Web


if (edgeTx.blockHeight > common.maxSeenTxBlockHeight) {
common.maxSeenTxBlockHeight = edgeTx.blockHeight
Expand Down
Loading