diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e234729..3fc4fa16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## Unreleased +- added: Support for zero-sync (aka silent-sync) enabled by the change-server protocol. +- changed: Batch change-server subscriptions for better reliability and efficiency. - fixed: Allow pin change while device is in duress mode and user is logged into a non-duress account. +- fixed: Change conditions for syncNetwork pixies to avoid unnecessary sync and support for zero-sync (aka silent-sync) enabled by the change-server protocol. ## 2.30.2 (2025-05-30) diff --git a/src/core/currency/currency-pixie.ts b/src/core/currency/currency-pixie.ts index f8096fcd..70141f5d 100644 --- a/src/core/currency/currency-pixie.ts +++ b/src/core/currency/currency-pixie.ts @@ -76,8 +76,11 @@ export const currency: TamePixie = combinePixies({ const { wallets } = input.props.state.currency // Memoize this function using the wallet state: - if (wallets === lastWallets) return - else lastWallets = wallets + if (wallets === lastWallets) { + return + } else { + lastWallets = wallets + } let { changeService, changeServiceConnected = false } = input.props.output.currency.changeServiceManager ?? {} @@ -125,18 +128,35 @@ export const currency: TamePixie = combinePixies({ } }, handleConnect() { + const wallets = Object.entries(input.props.state.currency.wallets) + // Reset to subscribing status for all reconnecting wallets: + for (const [walletId, wallet] of wallets) { + const subscriptions = wallet.changeServiceSubscriptions + .filter(subscription => subscription.status === 'reconnecting') + .map(subscription => ({ + ...subscription, + status: 'subscribing' as const + })) + input.props.dispatch({ + type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS', + payload: { + walletId, + subscriptions + } + }) + } // Start subscribing for all supported wallets: input.onOutput({ changeService, changeServiceConnected: true }) }, handleDisconnect() { const wallets = Object.entries(input.props.state.currency.wallets) - // Reset to subscribing status for all supported wallets: + // Reset to reconnecting status for all supported wallets: for (const [walletId, wallet] of wallets) { const subscriptions = wallet.changeServiceSubscriptions .filter(subscription => subscription.status !== 'avoiding') .map(subscription => ({ ...subscription, - status: 'subscribing' as const + status: 'reconnecting' as const })) input.props.dispatch({ type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS', @@ -200,50 +220,101 @@ export const currency: TamePixie = combinePixies({ subscription => subscription.status === 'subscribing' ) ) + const indexToWalletId: Array<{ + walletId: string + wallet: CurrencyWalletState + }> = [] + const batches: SubscribeParams[][] = [] for (const [walletId, wallet] of filteredWallets) { + if (wallet.paused) continue + // Build the subscribe parameters: - const subscribeParams: SubscribeParams[] = - wallet.changeServiceSubscriptions.map(subscription => [ + const params = wallet.changeServiceSubscriptions.map( + (subscription): SubscribeParams => [ wallet.currencyInfo.pluginId, subscription.address, subscription.checkpoint - ]) - if (subscribeParams.length === 0) continue + ] + ) + if (params.length === 0) continue + let subscribeParams = batches[batches.length - 1] + if ( + subscribeParams == null || + subscribeParams.length + params.length > 100 + ) { + batches.push([]) + subscribeParams = batches[batches.length - 1] + } + subscribeParams.push(...params) + for (let i = 0; i < params.length; i++) { + indexToWalletId[indexToWalletId.length] = { walletId, wallet } + } + } - // Subscribe to the change service: - const results = await changeService + // Subscribe to the change service: + const results: SubscribeResult[] = [] + for (const subscribeParams of batches) { + const r: SubscribeResult[] = await changeService .subscribe(subscribeParams) .catch(err => { input.props.log(`Failed to subscribe: ${String(err)}`) return [0] as SubscribeResult[] }) + results.push(...r) + } + + const subscriptionUpdates: Map = + new Map() + for (let i = 0; i < results.length; i++) { + const result = results[i] + const { walletId, wallet } = indexToWalletId[i] // Determine the new status of the subscription to all addresses // for the wallet: - let status: ChangeServiceSubscriptionStatus = 'listening' - // If any of the subscriptions not supported: - if (results.some(result => result === -1)) { - // The engine wants ECS, but the server does not support it, so - // avoid the change service: - status = 'avoiding' - } - // If any of the subscriptions failed: - if (results.some(result => result === 0)) { - // The engine wants ECS, but the server failed, so try again later: - status = 'subscribing' + let status: ChangeServiceSubscriptionStatus + switch (result) { + // Change server does not support this wallet plugin: + case -1: + // Avoid the change service: + status = 'avoiding' + break + // Change server does support this wallet plugin, but failed to + // subscribe to the address: + case 0: + // Try subscribing again later: + status = 'subscribing' + break + // Change server does support this wallet plugin, and there are no + // changes for the address: + case 1: + // Start syncing the wallet once for initial syncNetwork call: + status = 'syncing' + break + // Change server does support this wallet plugin, and there are + // changes for the address: + case 2: + // Start syncing the wallet: + status = 'syncing' + break } - // If any of the subscriptions have changes present: - if (results.some(result => result === 2)) { - // Start syncing the wallet: - status = 'syncing' + + // The status for the subscription is already set to subscribing, so + // we don't need to update it: + if (status === 'subscribing') { + continue } + // Update the status for the subscription: const subscriptions = wallet.changeServiceSubscriptions .filter(subscription => subscription.status === 'subscribing') .map(subscription => ({ ...subscription, status })) + subscriptionUpdates.set(walletId, subscriptions) + } + + for (const [walletId, subscriptions] of subscriptionUpdates.entries()) { input.props.dispatch({ type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS', payload: { diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts index d15833ea..f0d382bd 100644 --- a/src/core/currency/wallet/currency-wallet-pixie.ts +++ b/src/core/currency/wallet/currency-wallet-pixie.ts @@ -312,6 +312,7 @@ export const walletPixie: TamePixie = combinePixies({ props => !props.state.paused && !props.walletState.paused && + props.walletState.engineStarted && props.walletState.changeServiceSubscriptions.some( subscription => subscription.status === 'syncing' ) @@ -371,11 +372,12 @@ export const walletPixie: TamePixie = combinePixies({ props => !props.state.paused && !props.walletState.paused && + props.walletState.engineStarted && (props.walletState.changeServiceSubscriptions.length === 0 || props.walletState.changeServiceSubscriptions.some( subscription => subscription.status === 'avoiding' || - subscription.status === 'subscribing' + subscription.status === 'reconnecting' )) ? props : undefined diff --git a/src/core/currency/wallet/currency-wallet-reducer.ts b/src/core/currency/wallet/currency-wallet-reducer.ts index a798fe98..9bc18bd9 100644 --- a/src/core/currency/wallet/currency-wallet-reducer.ts +++ b/src/core/currency/wallet/currency-wallet-reducer.ts @@ -103,6 +103,7 @@ export interface ChangeServiceSubscription { export type ChangeServiceSubscriptionStatus = | 'avoiding' // The wallet is avoiding the change service (unsupported) | 'listening' // The wallet is connected and listening for changes + | 'reconnecting' // The wallet is reconnecting to the change service while its not available | 'subscribing' // The wallet is in the process of subscribing (supported) | 'syncing' // The wallet is syncing historical data