Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
600301a
Add a versioned wallet UI-state cache file and Redux seeding action
j0ntz Jul 18, 2026
5105dcc
Emit currency wallets before their engines exist
j0ntz Jul 18, 2026
ed985f4
Add deterministic tests for the wallet UI-state cache
j0ntz Jul 18, 2026
c09ac2c
Schedule cached wallets' engine startup behind a limited-concurrency …
j0ntz Jul 19, 2026
0ad9cff
Keep the existing balanceMap when a balance report is unchanged
j0ntz Jul 19, 2026
4783de3
Add deterministic engine-scheduler tests
j0ntz Jul 19, 2026
ff930d2
Seed the account boot state from a local cache
j0ntz Jul 21, 2026
9d7718f
Drop the duplicate publicKey.json read in the queued engine block
j0ntz Jul 21, 2026
e849063
Add account-cache and bulk-seeding tests
j0ntz Jul 21, 2026
b89bb6c
Keep user changes over racing wallet file loads
j0ntz Jul 21, 2026
a857b19
Deliver account state to engines that start before it loads
j0ntz Jul 21, 2026
f4e14c5
Make storage sync wait for the storage wallet
j0ntz Jul 21, 2026
6e27cf5
Wait for builtin tokens before filtering enabled token ids
j0ntz Jul 21, 2026
5f3b860
Stop the deferred boot loads after a logout
j0ntz Jul 21, 2026
b4f6389
Reject a pending storage sync on logout or wallet deletion
j0ntz Jul 21, 2026
71beb97
Reject repo waiters after a terminal boot-load failure
j0ntz Jul 21, 2026
93ceb0c
Wait for plugin settings before writing them
j0ntz Jul 21, 2026
d7a0ed0
Reject a pending token toggle after a terminal boot failure
j0ntz Jul 21, 2026
dc1cb70
Wait for the wallet-state load before changing wallet states
j0ntz Jul 21, 2026
d8c48b2
Gate the account token saver on the custom-token load
j0ntz Jul 21, 2026
7a80922
Keep user token edits per token id when a load races them
j0ntz Jul 21, 2026
3bbc97f
Apply enabled-token changes as toggles over the loaded list
j0ntz Jul 21, 2026
e9027da
Track dirty plugin settings per plugin id
j0ntz Jul 21, 2026
987632c
Wait for the engine in the account-level engine methods
j0ntz Jul 21, 2026
9b4fe78
Watch currencyWalletIds in the account cache saver
j0ntz Jul 21, 2026
1e2d480
Accept hash-suffixed store routes in the fake sync server
j0ntz Jul 21, 2026
16ff1fe
Add write-path staleness tests
j0ntz Jul 21, 2026
2ea1c01
Serve cached addresses before the engine on stable-address chains
j0ntz Jul 22, 2026
e1f7c42
Expose otherMethods as permanent delegating stubs
j0ntz Jul 22, 2026
f5f0e64
Cache config otherMethod names
j0ntz Jul 22, 2026
b034623
Add address and otherMethods cache tests
j0ntz Jul 22, 2026
bb3a169
Key the address cache by tokenId and rebuild stubs on growth
j0ntz Jul 22, 2026
eeabe43
Keep the live plugin surface for config otherMethods
j0ntz Jul 22, 2026
bf2bf4b
Harden the boot-retry and scheduler-timeout edge paths
j0ntz Jul 22, 2026
e3fab42
Serialize repo syncs per storage wallet
j0ntz Jul 22, 2026
6f7c633
Add allowCached opt-in for provisional receive addresses
j0ntz Jul 23, 2026
c532ebe
Unwedge wallet pixies when the deferred account load fails
j0ntz Jul 23, 2026
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

## Unreleased

- added: Account-level `accountCache.json` with the wallet states and custom tokens the account boot needs. A warm login seeds Redux from it and emits the account right after the currency plugins load, before the account repo syncs, and every cached wallet then seeds from a single bulk dispatch. The deferred file loads overwrite the seeded state authoritatively, and user changes made during the window win over the values those loads read. First login (no cache) boots exactly as before.
- added: Per-wallet `walletCache.json` with each wallet's name, fiat code, enabled token IDs, and last-known balances. Currency wallets now emit their API objects as soon as this cache loads, before their engines exist, so the GUI can render the wallet list immediately at login. Engine-backed methods wait for the engine internally and reject if it fails or the wallet is deleted. First login (no cache) behaves exactly as before.
- added: Cached wallets' engine startup is staggered through a limited-concurrency queue (8 at a time) instead of all racing at login. Asking for a wallet via `waitForCurrencyWallet`, calling an engine- or storage-backed method, or un-pausing it moves it to the front of the queue. Wallets without a cache skip the queue, so first login is unaffected.
- added: An optional `EdgeCurrencyInfo.hasStableAddresses` hint for chains whose receive addresses never rotate. When set, a wallet serves its cached addresses (keyed per token) before the engine loads; rotating and unflagged chains keep the engine wait, exactly as before.
- added: An `allowCached` option on `getAddresses` / `getReceiveAddress` that lets a caller opt in to a provisional cached address before the engine loads, even on a rotating-address chain. Programmatic callers leave it unset and stay engine-gated, so only the receive scene serves a provisional address that it reconciles once the engine confirms the fresh one.
- changed: `waitForCurrencyWallet` and `waitForAllWallets` now resolve when the wallet object exists, which can be before its engine loads.
- changed: `wallet.otherMethods` exposes delegating stubs: a method whose name is in the wallet's cache can be called before the engine exists (the call waits for the engine internally), the object keeps its identity when the cache already names every engine method, and a cached name the engine no longer implements rejects cleanly at call time. Pre-engine with no cache it is `{}`, as before.
- changed: `EdgeCurrencyWallet.balanceMap` keeps its object identity when an engine re-reports an unchanged balance.
Comment thread
j0ntz marked this conversation as resolved.
- changed: `getActivationAssets`, `activateWallet`, `getDisplayPrivateKey`, and `getDisplayPublicKey` wait for the wallet's engine instead of throwing when it has not loaded yet.
- fixed: The account's custom-token file is never written before its first load, which could permanently delete a custom token another device had synced to the account.
- fixed: File loads that race a user change during the boot window now merge per field (custom tokens per token id, enabled tokens per toggle, plugin settings per plugin id, wallet states per wallet id) instead of keeping or discarding whole maps, so changes synced from another device survive the window.
- fixed: `changeEnabledTokenIds` applies the caller's toggles to the current list, so a call built against a stale list no longer erases enablement changes synced from another device.
- fixed: The plugin-settings writers merge into the on-disk file instead of rebuilding it from memory, so settings another device synced to disk survive a local settings change.
- fixed: The fake sync server accepts the hash-suffixed store routes it hands out, so a repo's second sync inside `makeFakeEdgeWorld` no longer fails with a 404.

## 2.47.1 (2026-07-17)

- fixed: Revert `@nymproject/mix-fetch` to v1 (1.4.4), restoring the pinned gateway and network requester. The v2 stack shipped in 2.47.0 fails to complete small HTTPS JSON-RPC requests through most exit nodes and its exit-node auto-discovery rarely converges, which left wallets with NYM privacy enabled unable to sync or send.
Expand Down
51 changes: 34 additions & 17 deletions src/core/account/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import {
} from '../../types/types'
import { makeEdgeResult } from '../../util/edgeResult'
import { base58 } from '../../util/encoding'
import {
bumpEngineQueue,
waitForCurrencyEngine
} from '../currency/currency-selectors'
import { saveWalletSettings } from '../currency/wallet/currency-wallet-files'
import { getPublicWalletInfo } from '../currency/wallet/currency-wallet-pixie'
import {
Expand Down Expand Up @@ -124,7 +128,18 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {

// Specialty API's:
const dataStore = makeDataStoreApi(ai, accountId)
const storageWalletApi = makeStorageWalletApi(ai, accountWalletInfo)
const storageWalletApi = makeStorageWalletApi(
ai,
accountWalletInfo,
props => {
const accountState = props.state.accounts[accountId]
if (accountState == null) {
throw new Error('The account was logged out')
}
// A terminal boot failure means the repo is never coming:
if (accountState.loadFailure != null) throw accountState.loadFailure
}
)

function lockdown(): void {
if (ai.props.state.hideKeys) {
Expand Down Expand Up @@ -583,9 +598,9 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
return await tools.getDisplayPrivateKey(info)
}

const { engine } = ai.props.output.currency.wallets[walletId]
if (engine == null || engine.getDisplayPrivateSeed == null) {
throw new Error('Wallet has not yet loaded')
const engine = await waitForCurrencyEngine(ai, walletId)
if (engine.getDisplayPrivateSeed == null) {
throw new Error(`getDisplayPrivateKey unsupported by ${info.type}`)
}
const out = await engine.getDisplayPrivateSeed(info.keys)
if (out == null) throw new Error('The engine failed to return a key')
Expand All @@ -605,9 +620,9 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
return await tools.getDisplayPublicKey(publicInfo)
}

const { engine } = ai.props.output.currency.wallets[walletId]
if (engine == null || engine.getDisplayPublicSeed == null) {
throw new Error('Wallet has not yet loaded')
const engine = await waitForCurrencyEngine(ai, walletId)
if (engine.getDisplayPublicSeed == null) {
throw new Error(`getDisplayPublicKey unsupported by ${info.type}`)
}
const out = await engine.getDisplayPublicSeed()
if (out == null) throw new Error('The engine failed to return a key')
Expand Down Expand Up @@ -724,6 +739,10 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
},

async waitForCurrencyWallet(walletId: string): Promise<EdgeCurrencyWallet> {
// Asking for a wallet is the "the user wants this one" signal,
// so move its engine startup to the front of the queue:
bumpEngineQueue(ai, walletId)

return await new Promise((resolve, reject) => {
const check = (): void => {
const wallet = this.currencyWallets[walletId]
Expand Down Expand Up @@ -783,12 +802,11 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
activateWalletId,
activateTokenIds
}: EdgeGetActivationAssetsOptions): Promise<EdgeGetActivationAssetsResults> {
const { currencyWallets } = ai.props.output.accounts[accountId]
const walletOutput = ai.props.output.currency.wallets[activateWalletId]
const { engine } = walletOutput
const engine = await waitForCurrencyEngine(ai, activateWalletId)

if (engine == null)
throw new Error(`Invalid wallet: ${activateWalletId} not found`)
// Read the wallet list after the wait, so wallets that
// finished loading while the engine started are included:
const { currencyWallets } = ai.props.output.accounts[accountId]

if (engine.engineGetActivationAssets == null)
throw new Error(
Expand All @@ -809,12 +827,11 @@ export function makeAccountApi(ai: ApiInput, accountId: string): EdgeAccount {
opts: EdgeActivationOptions
): Promise<EdgeActivationQuote> {
const { activateWalletId, activateTokenIds, paymentInfo } = opts
const { currencyWallets } = ai.props.output.accounts[accountId]
const walletOutput = ai.props.output.currency.wallets[activateWalletId]
const { engine } = walletOutput
const engine = await waitForCurrencyEngine(ai, activateWalletId)

if (engine == null)
throw new Error(`Invalid wallet: ${activateWalletId} not found`)
// Read the wallet list after the wait, so wallets that
// finished loading while the engine started are included:
const { currencyWallets } = ai.props.output.accounts[accountId]

if (engine.engineActivateWallet == null)
throw new Error(
Expand Down
17 changes: 17 additions & 0 deletions src/core/account/account-cache-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { makeJsonFile } from '../../util/file-helpers'
import { asAccountCacheFile } from './account-cleaners'

/**
* Cached account boot state, stored on the account's local disklet.
* See `asAccountCacheFile` for the schema.
*/
export const ACCOUNT_CACHE_FILE = 'accountCache.json'
export const accountCacheFile = makeJsonFile(asAccountCacheFile)

/**
* Tuning for the account boot-state cache saver.
* Tests override the throttle to run quickly.
*/
export const accountCacheSaverConfig = {
throttleMs: 5000
}
58 changes: 56 additions & 2 deletions src/core/account/account-cleaners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ import {
asNumber,
asObject,
asOptional,
asString
asString,
asValue,
Cleaner
} from 'cleaners'

import { asBase16 } from '../../types/server-cleaners'
import { EdgeDenomination, EdgeToken } from '../../types/types'
import {
EdgeDenomination,
EdgePluginMap,
EdgeToken,
EdgeTokenMap,
EdgeWalletState,
EdgeWalletStates
} from '../../types/types'
import { asJsonObject } from '../../util/file-helpers'
import { SwapSettings } from './account-types'

Expand Down Expand Up @@ -92,3 +101,48 @@ export const asGuiSettingsFile = asObject({
export const asCustomTokensFile = asObject({
customTokens: asObject(asObject(asEdgeToken))
})

/**
* Cached account boot state, stored on the account's local disklet.
* This is what the deferred account file loads would produce,
* so wallet pixies can start before the account repo syncs.
* Values are last-known and explicitly allowed to be stale;
* the authoritative loads overwrite them within seconds.
* Never contains private key material: wallet keys stay in the
* encrypted login stash, which is already in memory at login.
* Plugin settings are deliberately excluded: unlike wallet states
* and token definitions, they can hold credentials (custom node
* auth, API keys), which must never leave the encrypted repo.
*/
export interface AccountCacheFile {
version: 1
customTokens: EdgePluginMap<EdgeTokenMap>
/**
* True when the account has legacy Airbitz-repo wallets. Their
* wallet infos cannot be cached (they contain private keys), so
* such accounts boot cold rather than briefly hiding wallets.
*/
legacyWallets: boolean
walletStates: EdgeWalletStates
/**
* Each plugin's `otherMethods` names, so `CurrencyConfig` can
* expose delegating stubs even if the plugin has not loaded yet.
*/
configOtherMethodNames: EdgePluginMap<string[]>
}

const asEdgeWalletState = asObject<EdgeWalletState>({
archived: asOptional(asBoolean),
deleted: asOptional(asBoolean),
hidden: asOptional(asBoolean),
migratedFromWalletId: asOptional(asString),
sortIndex: asOptional(asNumber)
})

export const asAccountCacheFile: Cleaner<AccountCacheFile> = asObject({
version: asValue(1),
customTokens: asObject(asObject(asEdgeToken)),
legacyWallets: asOptional(asBoolean, false),
walletStates: asObject(asEdgeWalletState),
configOtherMethodNames: asOptional(asObject(asArray(asString)), () => ({}))
})
Loading
Loading