diff --git a/src/servers/sidecar/wallet/backends/onchain.ts b/src/servers/sidecar/wallet/backends/onchain.ts index 8364eb7a..35fce413 100644 --- a/src/servers/sidecar/wallet/backends/onchain.ts +++ b/src/servers/sidecar/wallet/backends/onchain.ts @@ -313,6 +313,7 @@ export class OnchainBackend extends BaseBackend { syncedAt: this.current?.at ?? null, stale: this.current !== null && Date.now() - this.current.at >= SCAN_TTL_MS, lastError: this.lastError, + canRescan: this.chain.startRescan !== undefined, rescan: this.chain.rescanState?.() ?? null, }; } diff --git a/src/servers/sidecar/wallet/types.ts b/src/servers/sidecar/wallet/types.ts index eddac036..6ab9ed3d 100644 --- a/src/servers/sidecar/wallet/types.ts +++ b/src/servers/sidecar/wallet/types.ts @@ -321,6 +321,14 @@ export type SyncState = { stale: boolean; /** The last refresh failure, still reported while the previous good data is being served. */ lastError: string | null; + /** + * Whether this chain source can search the chain from scratch at all. False for Esplora, which asks + * about every address on every refresh and so has nothing to catch up on. + * + * Distinct from `rescan === null`, which is ALSO what a source that simply hasn't run one yet reports + * — conflating the two put a button that 501s in front of every Esplora wallet. + */ + canRescan: boolean; /** A deep rescan in flight, or the outcome of the last one. Null from a source that cannot rescan. */ rescan: RescanState | null; }; diff --git a/src/workspaces/officerdev/src/apps/Wallet/RescanCard.tsx b/src/workspaces/officerdev/src/apps/Wallet/RescanCard.tsx index 0a6cad7a..cb0ee806 100644 --- a/src/workspaces/officerdev/src/apps/Wallet/RescanCard.tsx +++ b/src/workspaces/officerdev/src/apps/Wallet/RescanCard.tsx @@ -23,10 +23,10 @@ export const RescanCard = ({ walletId }: RescanCardProps) => { const { sync } = useBalances(walletId); const { rescan } = useWalletOperations(walletId); - // `sync.rescan` is null from a source with no rescan endpoint AND from one that has simply never run - // one, so the card has to stay visible in the second case. `sync` itself being null is a node backend, - // which owns its own coins and has nothing to look for. - if (!sync) return null; + // `sync` null is a node backend, which owns its own coins and has nothing to look for. `canRescan` + // false is Esplora. Note this cannot key off `sync.rescan` being null — that is equally what NBXplorer + // reports before its first scan, which is exactly when the card is most needed. + if (!sync?.canRescan) return null; const state = sync.rescan; const running = state?.running === true; diff --git a/src/workspaces/officerdev/src/apps/Wallet/shared.ts b/src/workspaces/officerdev/src/apps/Wallet/shared.ts index 6d403975..868c2cdc 100644 --- a/src/workspaces/officerdev/src/apps/Wallet/shared.ts +++ b/src/workspaces/officerdev/src/apps/Wallet/shared.ts @@ -123,6 +123,12 @@ export type SyncState = { syncedAt: number | null; stale: boolean; lastError: string | null; + /** + * Whether the chain source can search the chain from scratch at all — false for Esplora, which reads + * every address on every refresh and has nothing to catch up on. Gate the control on this, NOT on + * `rescan === null`: that is also what a source reports before its first scan. + */ + canRescan: boolean; /** A deep rescan in flight, or the last one's outcome. Null from a chain source that cannot rescan. */ rescan: RescanState | null; };