Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

- added: Account-level `accountCache.json` with the wallet states, custom tokens, and plugin settings 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.
- changed: `waitForCurrencyWallet` and `waitForAllWallets` now resolve when the wallet object exists, which can be before its engine loads.
- changed: `wallet.otherMethods` is `{}` until the wallet's engine loads, then switches to the engine's methods.
- changed: `EdgeCurrencyWallet.balanceMap` keeps its object identity when an engine re-reports an unchanged balance.
Comment thread
j0ntz marked this conversation as resolved.

## 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
5 changes: 5 additions & 0 deletions src/core/account/account-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '../../types/types'
import { makeEdgeResult } from '../../util/edgeResult'
import { base58 } from '../../util/encoding'
import { bumpEngineQueue } 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 @@ -724,6 +725,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
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
}
52 changes: 50 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,42 @@ 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
}

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)
})
29 changes: 28 additions & 1 deletion src/core/account/account-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ function different(a: any, b: any): boolean {
return false
}

/**
* Waits until the account's storage wallet exists. A cache-seeded
* login emits the account API object before `addStorageWallet` runs,
* so methods that touch the synced repo pend briefly instead of
* throwing during that window. Rejects if the account logs out.
*/
export function waitForAccountRepo(
ai: ApiInput,
accountId: string
): Promise<unknown> {
return ai.waitFor(props => {
const accountState = props.state.accounts[accountId]
if (accountState == null) {
throw new Error('The account was logged out')
}
const { accountWalletInfo } = accountState
if (props.state.storageWallets[accountWalletInfo.id] != null) return true
Comment thread
j0ntz marked this conversation as resolved.
})
}

/**
* Loads the legacy wallet list from the account folder.
*/
Expand Down Expand Up @@ -148,6 +168,7 @@ export async function changeWalletStates(
accountId: string,
newStates: EdgeWalletStates
): Promise<void> {
await waitForAccountRepo(ai, accountId)
const { accountWalletInfo, walletStates } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)

Expand Down Expand Up @@ -189,7 +210,11 @@ export async function changeWalletStates(

ai.props.dispatch({
type: 'ACCOUNT_CHANGED_WALLET_STATES',
payload: { accountId, walletStates: { ...walletStates, ...toWrite } }
payload: {
accountId,
walletStates: { ...walletStates, ...toWrite },
changedIds: walletIds
}
})
}

Expand All @@ -202,6 +227,7 @@ export async function changePluginUserSettings(
pluginId: string,
userSettings: object
): Promise<void> {
await waitForAccountRepo(ai, accountId)
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
const { accountWalletInfo } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)

Expand Down Expand Up @@ -237,6 +263,7 @@ export async function changeSwapSettings(
pluginId: string,
swapSettings: SwapSettings
): Promise<void> {
await waitForAccountRepo(ai, accountId)
const { accountWalletInfo } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)

Expand Down
Loading
Loading