Skip to content

Sam/batch change server #664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2025
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
121 changes: 96 additions & 25 deletions src/core/currency/currency-pixie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ export const currency: TamePixie<RootProps> = 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 ?? {}
Expand Down Expand Up @@ -125,18 +128,35 @@ export const currency: TamePixie<RootProps> = 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',
Expand Down Expand Up @@ -200,50 +220,101 @@ export const currency: TamePixie<RootProps> = 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<string, ChangeServiceSubscription[]> =
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: {
Expand Down
4 changes: 3 additions & 1 deletion src/core/currency/wallet/currency-wallet-pixie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export const walletPixie: TamePixie<CurrencyWalletProps> = combinePixies({
props =>
!props.state.paused &&
!props.walletState.paused &&
props.walletState.engineStarted &&
props.walletState.changeServiceSubscriptions.some(
subscription => subscription.status === 'syncing'
)
Expand Down Expand Up @@ -371,11 +372,12 @@ export const walletPixie: TamePixie<CurrencyWalletProps> = 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
Expand Down
1 change: 1 addition & 0 deletions src/core/currency/wallet/currency-wallet-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading