Skip to content

Commit 0a089ca

Browse files
committed
Gate balance effects and FIO refresh on engine readiness
With the core's wallet cache (wallet cache v2 phase 1), wallet objects exist before their engines load, and waitForAllWallets resolves in that window. Three login-path surfaces consumed engine state immediately: - The action queue's address-balance effect read balanceMap right after awaiting the wallet, which could evaluate a balance effect against cached, possibly stale balances. It now reports not-yet-effective until the engine has fully synced, matching the conservatism the loan flow already applies. - The FIO address refresh called otherMethods on pre-engine wallets, which is {} in that window. Services now waits for each FIO wallet's engine-backed otherMethods (bounded by a generous safety-valve timeout) before refreshing. - FioService's periodic expired-domain check called otherMethods.getFioAddresses the same way (caught live on the sim). It now skips pre-engine wallets and lets the next 30s cycle retry, which also avoids wedging its one-shot expiredChecking latch.
1 parent d76796f commit 0a089ca

6 files changed

Lines changed: 122 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased (develop)
44

5+
- changed: Gate action-queue balance-effect checks and the login FIO address refresh on engine readiness, so wallets emitted from the core's new wallet cache (before their engines load) cannot mis-evaluate balance effects or crash the FIO refresh.
6+
57
## 4.50.0 (staging)
68

79
- added: Changelly swap provider

eslint.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ export default [
330330
'src/components/services/DeepLinkingManager.tsx',
331331
'src/components/services/EdgeContextCallbackManager.tsx',
332332

333-
'src/components/services/FioService.ts',
334333
'src/components/services/LoanManagerService.ts',
335334
'src/components/services/NetworkActivity.ts',
336335
'src/components/services/PasswordReminderService.ts',

src/components/services/FioService.ts

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ interface Props {
2828

2929
type NameDates = Record<string, Date>
3030

31-
export const FioService = (props: Props) => {
31+
export const FioService: React.FC<Props> = props => {
3232
const { account, navigation } = props
3333
const dispatch = useDispatch()
3434

@@ -65,40 +65,54 @@ export const FioService = (props: Props) => {
6565
}
6666

6767
if (expiredChecking.current) return
68-
expiredChecking.current = true
6968

70-
const walletsToCheck: EdgeCurrencyWallet[] = []
71-
for (const fioWallet of fioWallets.current) {
72-
if (!walletsCheckedForExpired.current[fioWallet.id]) {
73-
walletsToCheck.push(fioWallet)
69+
// Wallet objects can exist before their engines do (wallet cache),
70+
// and refreshFioNames calls engine-backed otherMethods.
71+
// Skip pre-engine wallets and let the next cycle retry them:
72+
const readyWallets = fioWallets.current.filter(
73+
fioWallet => fioWallet.otherMethods.getFioAddresses != null
74+
)
75+
if (readyWallets.length === 0) return
76+
77+
expiredChecking.current = true
78+
try {
79+
const walletsToCheck: EdgeCurrencyWallet[] = []
80+
for (const fioWallet of readyWallets) {
81+
if (!walletsCheckedForExpired.current[fioWallet.id]) {
82+
walletsToCheck.push(fioWallet)
83+
}
7484
}
75-
}
7685

77-
const namesToCheck: FioDomain[] = []
78-
const { fioDomains, fioWalletsById } = await refreshFioNames(walletsToCheck)
79-
expiredLastChecks.current ??= await getFioExpiredCheckFromDisklet(disklet)
80-
for (const fioDomain of fioDomains) {
81-
if (needToCheckExpired(expiredLastChecks.current, fioDomain.name)) {
82-
namesToCheck.push(fioDomain)
86+
const namesToCheck: FioDomain[] = []
87+
const { fioDomains, fioWalletsById } = await refreshFioNames(
88+
walletsToCheck
89+
)
90+
expiredLastChecks.current ??= await getFioExpiredCheckFromDisklet(disklet)
91+
for (const fioDomain of fioDomains) {
92+
if (needToCheckExpired(expiredLastChecks.current, fioDomain.name)) {
93+
namesToCheck.push(fioDomain)
94+
}
8395
}
84-
}
8596

86-
if (namesToCheck.length !== 0) {
87-
const expired: FioDomain[] = getExpiredSoonFioDomains(fioDomains)
88-
if (expired.length > 0) {
89-
const first: FioDomain = expired[0]
90-
const fioWallet: EdgeCurrencyWallet = fioWalletsById[first.walletId]
91-
await showFioExpiredModal(navigation, fioWallet, first)
92-
expireReminderShown.current = true
97+
if (namesToCheck.length !== 0) {
98+
const expired: FioDomain[] = getExpiredSoonFioDomains(fioDomains)
99+
if (expired.length > 0) {
100+
const first: FioDomain = expired[0]
101+
const fioWallet: EdgeCurrencyWallet = fioWalletsById[first.walletId]
102+
await showFioExpiredModal(navigation, fioWallet, first)
103+
expireReminderShown.current = true
93104

94-
expiredLastChecks.current[first.name] = new Date()
95-
await setFioExpiredCheckToDisklet(expiredLastChecks.current, disklet)
96-
}
105+
expiredLastChecks.current[first.name] = new Date()
106+
await setFioExpiredCheckToDisklet(expiredLastChecks.current, disklet)
107+
}
97108

98-
for (const walletId in fioWalletsById) {
99-
walletsCheckedForExpired.current[walletId] = true
109+
for (const walletId in fioWalletsById) {
110+
walletsCheckedForExpired.current[walletId] = true
111+
}
100112
}
101-
113+
} finally {
114+
// Always release the latch, or a cycle with nothing to check
115+
// (or an error) would disable this check for the whole session:
102116
expiredChecking.current = false
103117
}
104118
})
@@ -130,7 +144,10 @@ export const FioService = (props: Props) => {
130144
return null
131145
}
132146

133-
function arraysEqual(arr1: EdgeCurrencyWallet[], arr2: EdgeCurrencyWallet[]) {
147+
function arraysEqual(
148+
arr1: EdgeCurrencyWallet[],
149+
arr2: EdgeCurrencyWallet[]
150+
): boolean {
134151
if (arr1.length !== arr2.length) return false
135152

136153
arr1.sort((a, b) => a.id.localeCompare(b.id))

src/components/services/Services.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
width
2828
} from '../../util/scaling'
2929
import { snooze } from '../../util/utils'
30+
import { waitForWalletOtherMethods } from '../../util/waitForWalletOtherMethods'
3031
import { AlertDropdown } from '../navigation/AlertDropdown'
3132
import { AccountCallbackManager } from './AccountCallbackManager'
3233
import { ActionQueueService } from './ActionQueueService'
@@ -88,6 +89,26 @@ export const Services: React.FC<Props> = props => {
8889
console.warn('registerNotificationsV2 error:', error)
8990
})
9091

92+
// Wallet objects can exist before their engines do (wallet cache),
93+
// and the FIO refreshes below call engine-backed otherMethods,
94+
// so wait for each FIO wallet's engine first:
95+
const fioWallets = Object.values(account.currencyWallets).filter(
96+
wallet => wallet.currencyInfo.pluginId === 'fio'
97+
)
98+
await Promise.all(
99+
fioWallets.map(async wallet => {
100+
// Per-wallet, so one broken wallet cannot reject the whole gate
101+
// or hide which wallet timed out:
102+
await waitForWalletOtherMethods(wallet).catch((error: unknown) => {
103+
console.warn('waitForWalletOtherMethods error:', error)
104+
})
105+
})
106+
)
107+
108+
// Bail out if the account logged out while we waited for engines,
109+
// so the refreshes below never run against the next session:
110+
if (!account.loggedIn) return
111+
91112
await dispatch(refreshConnectedWallets).catch((error: unknown) => {
92113
console.warn(error)
93114
})

src/controllers/action-queue/runtime/checkActionEffect.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { gte, lte } from 'biggystring'
22

3+
import { DONE_THRESHOLD } from '../../../constants/WalletAndCurrencyConstants'
34
import { filterNull } from '../../../util/safeFilters'
45
import { checkPushEvent } from '../push'
56
import type {
@@ -120,6 +121,17 @@ export async function checkActionEffect(
120121
// TODO: Use effect.address when we can check address balances
121122
const { aboveAmount, belowAmount, tokenId, walletId } = effect
122123
const wallet = await account.waitForCurrencyWallet(walletId)
124+
125+
// The wallet object can exist before its engine loads (wallet cache),
126+
// so don't evaluate the effect against cached, possibly stale
127+
// balances. Report "not yet effective" until the engine has synced:
128+
if (wallet.syncStatus.totalRatio < DONE_THRESHOLD) {
129+
return {
130+
delay: 15000,
131+
isEffective: false
132+
}
133+
}
134+
123135
const walletBalance = wallet.balanceMap.get(tokenId) ?? '0'
124136

125137
return {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { EdgeCurrencyWallet } from 'edge-core-js'
2+
3+
// Engine creation for a large account can take minutes on login,
4+
// matching how long waitForAllWallets used to take before the wallet
5+
// cache existed, so this is a safety valve rather than a deadline:
6+
const OTHER_METHODS_TIMEOUT_MS = 10 * 60 * 1000
7+
8+
/**
9+
* Waits for a wallet's engine-backed `otherMethods` to arrive.
10+
*
11+
* The core's wallet cache emits wallet objects before their engines
12+
* exist, and `wallet.otherMethods` is guaranteed to be `{}` until the
13+
* engine loads. Rejects after a timeout so a wallet whose engine never
14+
* loads cannot hang callers forever.
15+
*/
16+
export async function waitForWalletOtherMethods(
17+
wallet: EdgeCurrencyWallet,
18+
timeoutMs: number = OTHER_METHODS_TIMEOUT_MS
19+
): Promise<void> {
20+
if (Object.keys(wallet.otherMethods).length > 0) return
21+
22+
await new Promise<void>((resolve, reject) => {
23+
const handleReady = (): void => {
24+
clearTimeout(timeout)
25+
unsubscribe()
26+
resolve()
27+
}
28+
const timeout = setTimeout(() => {
29+
unsubscribe()
30+
reject(
31+
new Error(`Timed out waiting for wallet ${wallet.id} engine methods`)
32+
)
33+
}, timeoutMs)
34+
const unsubscribe = wallet.watch('otherMethods', otherMethods => {
35+
if (Object.keys(otherMethods).length > 0) handleReady()
36+
})
37+
38+
// The methods may have arrived between the caller's check and the
39+
// subscription above, in which case the watcher never fires:
40+
if (Object.keys(wallet.otherMethods).length > 0) handleReady()
41+
})
42+
}

0 commit comments

Comments
 (0)