From e7bb259a7a6a82480d4d2d4ac32769ac292d3dd7 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Wed, 18 Mar 2026 21:51:40 -0700 Subject: [PATCH] feat: add change-server support for bitcoin, bitcoincash, dogecoin, litecoin Integrate the change-server protocol for UTXO engines following the contract in edge-core-js PR #718. Key changes: - Upgrade edge-core-js to ^2.43.0 for change-server types - Wire onSubscribeAddresses callback through EngineEmitter - Add syncNetwork() to UtxoEngine - Gate block-height confirmation checks behind usesChangeServer - Set usesChangeServer for bitcoin, bitcoincash, dogecoin, litecoin --- package.json | 2 +- src/common/plugin/EngineEmitter.ts | 19 +++++++-- src/common/utxobased/engine/UtxoEngine.ts | 41 +++++++++++++++++++ .../utxobased/engine/UtxoEngineProcessor.ts | 33 +++++++++++++++ src/common/utxobased/info/bitcoin.ts | 1 + src/common/utxobased/info/bitcoincash.ts | 1 + src/common/utxobased/info/dogecoin.ts | 1 + src/common/utxobased/info/litecoin.ts | 1 + test/common/utxobased/engine/engine.spec.ts | 2 + yarn.lock | 24 +++++++---- 10 files changed, 111 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 7bb6dc3d..a4528243 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "base-x": "^4.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "edge-core-js": "^2.24.0", + "edge-core-js": "^2.43.0", "esbuild-loader": "^4.1.0", "eslint": "^7.14.0", "eslint-config-standard-kit": "0.15.1", diff --git a/src/common/plugin/EngineEmitter.ts b/src/common/plugin/EngineEmitter.ts index 72d0c02e..c5fe1b09 100644 --- a/src/common/plugin/EngineEmitter.ts +++ b/src/common/plugin/EngineEmitter.ts @@ -1,5 +1,6 @@ import { EdgeCurrencyEngineCallbacks, + EdgeSubscribedAddress, EdgeTransaction, EdgeTransactionEvent, EdgeTxidMap @@ -10,9 +11,13 @@ import { SubscribeAddressResponse } from '../utxobased/network/blockbookApi' export declare interface EngineEmitter { emit: (( - event: EngineEvent.SEEN_TX_CHECKPOINT, - checkpoint: string + event: EngineEvent.SUBSCRIBE_ADDRESSES, + addresses: EdgeSubscribedAddress[] ) => boolean) & + (( + event: EngineEvent.SEEN_TX_CHECKPOINT, + checkpoint: string + ) => boolean) & (( event: EngineEvent.TRANSACTIONS, transactionEvents: EdgeTransactionEvent[] @@ -45,9 +50,13 @@ export declare interface EngineEmitter { ((event: EngineEvent.TXIDS_CHANGED, txids: EdgeTxidMap) => boolean) on: (( - event: EngineEvent.SEEN_TX_CHECKPOINT, - listener: (checkpoint: string) => Promise | void + event: EngineEvent.SUBSCRIBE_ADDRESSES, + listener: (addresses: EdgeSubscribedAddress[]) => Promise | void ) => this) & + (( + event: EngineEvent.SEEN_TX_CHECKPOINT, + listener: (checkpoint: string) => Promise | void + ) => this) & (( event: EngineEvent.TRANSACTIONS, listener: ( @@ -95,6 +104,7 @@ export declare interface EngineEmitter { export class EngineEmitter extends EventEmitter {} export enum EngineEvent { + SUBSCRIBE_ADDRESSES = 'subscribe:addresses', SEEN_TX_CHECKPOINT = 'seen:tx:checkpoint', TRANSACTIONS = 'transactions', /** @deprecated Use TRANSACTIONS */ @@ -123,6 +133,7 @@ export const makeEngineEmitter = ( } ) emitter.on(EngineEvent.SEEN_TX_CHECKPOINT, callbacks.onSeenTxCheckpoint) + emitter.on(EngineEvent.SUBSCRIBE_ADDRESSES, callbacks.onSubscribeAddresses) emitter.on(EngineEvent.TRANSACTIONS, callbacks.onTransactions) emitter.on(EngineEvent.TRANSACTIONS_CHANGED, callbacks.onTransactionsChanged) emitter.on(EngineEvent.TXIDS_CHANGED, callbacks.onTxidsChanged) diff --git a/src/common/utxobased/engine/UtxoEngine.ts b/src/common/utxobased/engine/UtxoEngine.ts index cc4ff6bd..ee7ba19b 100644 --- a/src/common/utxobased/engine/UtxoEngine.ts +++ b/src/common/utxobased/engine/UtxoEngine.ts @@ -5,6 +5,7 @@ import { DustSpendError, EdgeCurrencyEngine, EdgeDataDump, + EdgeEngineSyncNetworkOptions, EdgeFreshAddress, EdgeGetReceiveAddressOptions, EdgeGetTransactionsOptions, @@ -61,6 +62,7 @@ import { pathToPurposeType, sumUtxos } from './utils' +import { SubscribeAddressResponse } from '../network/blockbookApi' import { makeUtxoEngineProcessor } from './UtxoEngineProcessor' import { makeUtxoWalletTools } from './UtxoWalletTools' @@ -1029,6 +1031,45 @@ export async function makeUtxoEngine( return await sweepTxPromise }, + async syncNetwork(opts: EdgeEngineSyncNetworkOptions): Promise { + const syncIntervalMs = 10000 + + // If this currency doesn't use the change-server, behave as a normal + // polling engine: just return the polling interval. + if (currencyInfo.usesChangeServer !== true) { + return syncIntervalMs + } + + const { subscribeParam } = opts + + if (subscribeParam == null) { + // Ordinary polling wakeup — no change-server address activity. + // The existing Blockbook websocket subscriptions handle real-time + // updates; no extra work needed here. + return syncIntervalMs + } + + if (subscribeParam.needsSync === false) { + // Core says no real sync is needed for this address. + // Mark addresses-checked at 100% to signal fully synced. + emitter.emit(EngineEvent.ADDRESSES_CHECKED, 1) + return syncIntervalMs + } + + // Change-server reported activity for a specific address. + // Feed it into the existing address-scoped processing pipeline by + // emitting NEW_ADDRESS_TRANSACTION — the same event used for direct + // Blockbook address wakeups. The handler only uses response.address, + // so we cast the partial object to satisfy the type signature. + emitter.emit( + EngineEvent.NEW_ADDRESS_TRANSACTION, + 'syncNetwork', + { address: subscribeParam.address } as SubscribeAddressResponse + ) + + return syncIntervalMs + }, + otherMethods: {} } diff --git a/src/common/utxobased/engine/UtxoEngineProcessor.ts b/src/common/utxobased/engine/UtxoEngineProcessor.ts index 6f6b16fa..fdb982e9 100644 --- a/src/common/utxobased/engine/UtxoEngineProcessor.ts +++ b/src/common/utxobased/engine/UtxoEngineProcessor.ts @@ -103,6 +103,9 @@ export function makeUtxoEngineProcessor( } = config const { log } = engineOptions + const { currencyInfo } = pluginInfo + const usesChangeServer = currencyInfo.usesChangeServer === true + const { walletFormats } = walletInfo.keys const taskCache: TaskCache = { @@ -245,12 +248,42 @@ export function makeUtxoEngineProcessor( running = true await initializeAddressSubscriptions() + + // After initialization, notify core which addresses to subscribe on the + // change-server. Only do this when the currency opts in. + if (usesChangeServer) { + const { subscribedAddresses: restoredAddresses = [] } = + engineOptions + // Build a map of restored checkpoints for fast lookup + const checkpointByAddress = new Map() + for (const entry of restoredAddresses) { + checkpointByAddress.set(entry.address, entry.checkpoint) + } + + // Collect all current wallet addresses from the subscribe cache + const addressesToSubscribe = Object.keys( + taskCache.addressSubscribeCache + ).map(address => ({ + address, + checkpoint: checkpointByAddress.get(address) + })) + + if (addressesToSubscribe.length > 0) { + emitter.emit(EngineEvent.SUBSCRIBE_ADDRESSES, addressesToSubscribe) + } + } + await setLookAhead(common) } emitter.on( EngineEvent.BLOCK_HEIGHT_CHANGED, async (_uri: string, _blockHeight: number): Promise => { + // When change-server is enabled, unconfirmed→confirmed transitions are + // discovered via address wakeups instead of per-block network queries. + // Skip populating transactionUpdateCache to avoid redundant fetchTx calls. + if (usesChangeServer) return + // Add all unconfirmed transactions to the cache to check if these // transactions have been confirmed: const txs = await dataLayer.fetchTransactions({ diff --git a/src/common/utxobased/info/bitcoin.ts b/src/common/utxobased/info/bitcoin.ts index abc5cde0..1012c238 100644 --- a/src/common/utxobased/info/bitcoin.ts +++ b/src/common/utxobased/info/bitcoin.ts @@ -16,6 +16,7 @@ const currencyInfo: EdgeCurrencyInfo = { customFeeTemplate: utxoCustomFeeTemplate, memoOptions: utxoMemoOptions, pluginId: 'bitcoin', + usesChangeServer: true, walletType: 'wallet:bitcoin', // Explorers: diff --git a/src/common/utxobased/info/bitcoincash.ts b/src/common/utxobased/info/bitcoincash.ts index 339e88d0..549b0701 100644 --- a/src/common/utxobased/info/bitcoincash.ts +++ b/src/common/utxobased/info/bitcoincash.ts @@ -18,6 +18,7 @@ const currencyInfo: EdgeCurrencyInfo = { customFeeTemplate: utxoCustomFeeTemplate, memoOptions: utxoMemoOptions, pluginId: 'bitcoincash', + usesChangeServer: true, walletType: 'wallet:bitcoincash', // Explorers: diff --git a/src/common/utxobased/info/dogecoin.ts b/src/common/utxobased/info/dogecoin.ts index 156f22a0..fb588fcb 100644 --- a/src/common/utxobased/info/dogecoin.ts +++ b/src/common/utxobased/info/dogecoin.ts @@ -16,6 +16,7 @@ const currencyInfo: EdgeCurrencyInfo = { customFeeTemplate: utxoCustomFeeTemplate, memoOptions: utxoMemoOptions, pluginId: 'dogecoin', + usesChangeServer: true, walletType: 'wallet:dogecoin', // Explorers: diff --git a/src/common/utxobased/info/litecoin.ts b/src/common/utxobased/info/litecoin.ts index 0832d92e..04c31d9a 100644 --- a/src/common/utxobased/info/litecoin.ts +++ b/src/common/utxobased/info/litecoin.ts @@ -16,6 +16,7 @@ export const currencyInfo: EdgeCurrencyInfo = { customFeeTemplate: utxoCustomFeeTemplate, memoOptions: utxoMemoOptions, pluginId: 'litecoin', + usesChangeServer: true, walletType: 'wallet:litecoin', // Explorers: diff --git a/test/common/utxobased/engine/engine.spec.ts b/test/common/utxobased/engine/engine.spec.ts index f10d0f19..7e82cf08 100644 --- a/test/common/utxobased/engine/engine.spec.ts +++ b/test/common/utxobased/engine/engine.spec.ts @@ -98,6 +98,8 @@ describe('engine.spec', function () { fakeLogger.info('onTransactionsChanged:', transactionList) emitter.emit('onTransactionsChanged', transactionList) }, + onSubscribeAddresses() {}, + onSyncStatusChanged() {}, onTxidsChanged() {}, onUnactivatedTokenIdsChanged() {}, onWcNewContractCall() {} diff --git a/yarn.lock b/yarn.lock index 2164980b..65032020 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1145,6 +1145,11 @@ "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" +"@nymproject/mix-fetch@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@nymproject/mix-fetch/-/mix-fetch-1.4.3.tgz#bee246da3f29db6be48fba4e48656f3b1da9cf8b" + integrity sha512-+Q8W2p5JP6T7gk/GSynPabbDHe0hJ+FADTT0VSDQG7rstNSpTvMRz2RjKFuv5+Ufb4ufoEHYhYd8r1l6rrZ/sQ== + "@parcel/fs@^1.11.0": version "1.11.0" resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-1.11.0.tgz#fb8a2be038c454ad46a50dc0554c1805f13535cd" @@ -3925,11 +3930,12 @@ ecc-jsbn@~0.1.1: typeforce "^1.18.0" wif "^2.0.6" -edge-core-js@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.24.0.tgz#dde491266313562090862510984a85cd5d6659bb" - integrity sha512-MD7U1xrvDeU87xheqfsQOhqovcmBzyNp3j7RAJYtVeG4RDQI/YXXmFlX7QpVooGXJdlbxYsYZiyLVlfnQY9IYQ== +edge-core-js@^2.43.0: + version "2.43.4" + resolved "https://registry.yarnpkg.com/edge-core-js/-/edge-core-js-2.43.4.tgz#8486dce6cf4445934b17c5eaf6954b2425046fd2" + integrity sha512-P+nt44Ijv+gtN3U75bKEG4ylLAT5AE14oaSeoIwid8NLMPM/775UYEzz2FxyIiweJgxzNm6ujBdK4zZURjufzA== dependencies: + "@nymproject/mix-fetch" "^1.4.3" aes-js "^3.1.0" base-x "^4.0.0" biggystring "^4.2.3" @@ -3948,7 +3954,7 @@ edge-core-js@^2.24.0: scrypt-js "^2.0.3" serverlet "^0.1.2" yaob "^0.3.12" - yavent "^0.1.3" + yavent "^0.1.5" edge-sync-client@^0.2.7, edge-sync-client@^0.2.8: version "0.2.8" @@ -10735,10 +10741,10 @@ yargs@^17.2.1: y18n "^5.0.5" yargs-parser "^21.0.0" -yavent@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/yavent/-/yavent-0.1.3.tgz#c86a5e1d013721eda9c732e820fb7d6aaf2ab4db" - integrity sha512-O6ZngnMCkiM1Bg/zP2XKgprjXUuHS0tSQjHxWhMc53avgNu6AT5zcmDm+oYxQLRXQ9ajlEQwQ+Rf7CghKt1u1A== +yavent@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/yavent/-/yavent-0.1.5.tgz#caf32af73c59a18142533fddfefd2c0fc8f99b0f" + integrity sha512-VgHZ8Q19exDqdg+sQ4ax3cKfsUvCmeua/nGGPsHeQTfqefQfAMfEk4qwnLE4ABVJhlJOQCfGhxWA3llW6eFk1w== yocto-queue@^0.1.0: version "0.1.0"