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

## Unreleased (develop)

- 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.

## 4.50.0 (staging)

- added: Changelly swap provider
Expand Down
1 change: 0 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ export default [
'src/components/services/DeepLinkingManager.tsx',
'src/components/services/EdgeContextCallbackManager.tsx',

'src/components/services/FioService.ts',
'src/components/services/LoanManagerService.ts',
'src/components/services/NetworkActivity.ts',
'src/components/services/PasswordReminderService.ts',
Expand Down
73 changes: 45 additions & 28 deletions src/components/services/FioService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface Props {

type NameDates = Record<string, Date>

export const FioService = (props: Props) => {
export const FioService: React.FC<Props> = props => {
const { account, navigation } = props
const dispatch = useDispatch()

Expand Down Expand Up @@ -65,40 +65,54 @@ export const FioService = (props: Props) => {
}

if (expiredChecking.current) return
expiredChecking.current = true

const walletsToCheck: EdgeCurrencyWallet[] = []
for (const fioWallet of fioWallets.current) {
if (!walletsCheckedForExpired.current[fioWallet.id]) {
walletsToCheck.push(fioWallet)
// Wallet objects can exist before their engines do (wallet cache),
// and refreshFioNames calls engine-backed otherMethods.
// Skip pre-engine wallets and let the next cycle retry them:
const readyWallets = fioWallets.current.filter(
fioWallet => fioWallet.otherMethods.getFioAddresses != null
)
if (readyWallets.length === 0) return

expiredChecking.current = true
try {
const walletsToCheck: EdgeCurrencyWallet[] = []
for (const fioWallet of readyWallets) {
if (!walletsCheckedForExpired.current[fioWallet.id]) {
walletsToCheck.push(fioWallet)
}
}
}

const namesToCheck: FioDomain[] = []
const { fioDomains, fioWalletsById } = await refreshFioNames(walletsToCheck)
expiredLastChecks.current ??= await getFioExpiredCheckFromDisklet(disklet)
for (const fioDomain of fioDomains) {
if (needToCheckExpired(expiredLastChecks.current, fioDomain.name)) {
namesToCheck.push(fioDomain)
const namesToCheck: FioDomain[] = []
const { fioDomains, fioWalletsById } = await refreshFioNames(
walletsToCheck
)
expiredLastChecks.current ??= await getFioExpiredCheckFromDisklet(disklet)
for (const fioDomain of fioDomains) {
if (needToCheckExpired(expiredLastChecks.current, fioDomain.name)) {
namesToCheck.push(fioDomain)
}
}
}

if (namesToCheck.length !== 0) {
const expired: FioDomain[] = getExpiredSoonFioDomains(fioDomains)
if (expired.length > 0) {
const first: FioDomain = expired[0]
const fioWallet: EdgeCurrencyWallet = fioWalletsById[first.walletId]
await showFioExpiredModal(navigation, fioWallet, first)
expireReminderShown.current = true
if (namesToCheck.length !== 0) {
const expired: FioDomain[] = getExpiredSoonFioDomains(fioDomains)
if (expired.length > 0) {
const first: FioDomain = expired[0]
const fioWallet: EdgeCurrencyWallet = fioWalletsById[first.walletId]
await showFioExpiredModal(navigation, fioWallet, first)
expireReminderShown.current = true

expiredLastChecks.current[first.name] = new Date()
await setFioExpiredCheckToDisklet(expiredLastChecks.current, disklet)
}
expiredLastChecks.current[first.name] = new Date()
await setFioExpiredCheckToDisklet(expiredLastChecks.current, disklet)
}

for (const walletId in fioWalletsById) {
walletsCheckedForExpired.current[walletId] = true
for (const walletId in fioWalletsById) {
walletsCheckedForExpired.current[walletId] = true
}
}

} finally {
// Always release the latch, or a cycle with nothing to check
// (or an error) would disable this check for the whole session:
expiredChecking.current = false
}
})
Expand Down Expand Up @@ -130,7 +144,10 @@ export const FioService = (props: Props) => {
return null
}

function arraysEqual(arr1: EdgeCurrencyWallet[], arr2: EdgeCurrencyWallet[]) {
function arraysEqual(
arr1: EdgeCurrencyWallet[],
arr2: EdgeCurrencyWallet[]
): boolean {
if (arr1.length !== arr2.length) return false

arr1.sort((a, b) => a.id.localeCompare(b.id))
Expand Down
21 changes: 21 additions & 0 deletions src/components/services/Services.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
width
} from '../../util/scaling'
import { snooze } from '../../util/utils'
import { waitForWalletOtherMethods } from '../../util/waitForWalletOtherMethods'
import { AlertDropdown } from '../navigation/AlertDropdown'
import { AccountCallbackManager } from './AccountCallbackManager'
import { ActionQueueService } from './ActionQueueService'
Expand Down Expand Up @@ -88,6 +89,26 @@ export const Services: React.FC<Props> = props => {
console.warn('registerNotificationsV2 error:', error)
})

// Wallet objects can exist before their engines do (wallet cache),
// and the FIO refreshes below call engine-backed otherMethods,
// so wait for each FIO wallet's engine first:
const fioWallets = Object.values(account.currencyWallets).filter(
wallet => wallet.currencyInfo.pluginId === 'fio'
)
await Promise.all(
fioWallets.map(async wallet => {
// Per-wallet, so one broken wallet cannot reject the whole gate
// or hide which wallet timed out:
await waitForWalletOtherMethods(wallet).catch((error: unknown) => {
console.warn('waitForWalletOtherMethods error:', error)
})
})
)

Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
j0ntz marked this conversation as resolved.
// Bail out if the account logged out while we waited for engines,
// so the refreshes below never run against the next session:
if (!account.loggedIn) return

Comment thread
j0ntz marked this conversation as resolved.
await dispatch(refreshConnectedWallets).catch((error: unknown) => {
console.warn(error)
})
Expand Down
12 changes: 12 additions & 0 deletions src/controllers/action-queue/runtime/checkActionEffect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { gte, lte } from 'biggystring'

import { DONE_THRESHOLD } from '../../../constants/WalletAndCurrencyConstants'
import { filterNull } from '../../../util/safeFilters'
import { checkPushEvent } from '../push'
import type {
Expand Down Expand Up @@ -120,6 +121,17 @@ export async function checkActionEffect(
// TODO: Use effect.address when we can check address balances
const { aboveAmount, belowAmount, tokenId, walletId } = effect
const wallet = await account.waitForCurrencyWallet(walletId)

// The wallet object can exist before its engine loads (wallet cache),
// so don't evaluate the effect against cached, possibly stale
// balances. Report "not yet effective" until the engine has synced:
if (wallet.syncStatus.totalRatio < DONE_THRESHOLD) {
return {
delay: 15000,
isEffective: false
}
}
Comment thread
j0ntz marked this conversation as resolved.

const walletBalance = wallet.balanceMap.get(tokenId) ?? '0'

return {
Expand Down
42 changes: 42 additions & 0 deletions src/util/waitForWalletOtherMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { EdgeCurrencyWallet } from 'edge-core-js'

// Engine creation for a large account can take minutes on login,
// matching how long waitForAllWallets used to take before the wallet
// cache existed, so this is a safety valve rather than a deadline:
const OTHER_METHODS_TIMEOUT_MS = 10 * 60 * 1000

/**
* Waits for a wallet's engine-backed `otherMethods` to arrive.
*
* The core's wallet cache emits wallet objects before their engines
* exist, and `wallet.otherMethods` is guaranteed to be `{}` until the
* engine loads. Rejects after a timeout so a wallet whose engine never
* loads cannot hang callers forever.
*/
export async function waitForWalletOtherMethods(
wallet: EdgeCurrencyWallet,
timeoutMs: number = OTHER_METHODS_TIMEOUT_MS
): Promise<void> {
if (Object.keys(wallet.otherMethods).length > 0) return

await new Promise<void>((resolve, reject) => {
const handleReady = (): void => {
clearTimeout(timeout)
unsubscribe()
resolve()
}
const timeout = setTimeout(() => {
unsubscribe()
reject(
new Error(`Timed out waiting for wallet ${wallet.id} engine methods`)
)
}, timeoutMs)
const unsubscribe = wallet.watch('otherMethods', otherMethods => {
if (Object.keys(otherMethods).length > 0) handleReady()
})

// The methods may have arrived between the caller's check and the
// subscription above, in which case the watcher never fires:
if (Object.keys(wallet.otherMethods).length > 0) handleReady()
})
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
j0ntz marked this conversation as resolved.
}
Loading