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

## 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.
- changed: Opening any wallet-scoped scene asks the core to prioritize that wallet's engine startup in the post-login queue.

## 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
10 changes: 10 additions & 0 deletions src/components/hoc/withWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export function withWallet<Props extends { wallet: EdgeCurrencyWallet }>(
const currencyWallets = useWatch(account, 'currencyWallets')
const wallet = currencyWallets[route.params.walletId]

// Opening a wallet-scoped scene is the "the user wants this wallet"
// signal: waitForCurrencyWallet moves the wallet's engine startup to
// the front of the core's post-login queue. Rejections (a deleted or
// broken wallet) are already handled by the effect below:
const { walletId } = route.params
React.useEffect(() => {
if (account.waitForCurrencyWallet == null) return
account.waitForCurrencyWallet(walletId).catch(() => {})
}, [account, walletId])

React.useEffect(() => {
if (wallet == null) navigation.goBack()
}, [navigation, wallet])
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