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
54 changes: 30 additions & 24 deletions app/features/accounts/account-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,44 +54,50 @@ export class AccountsCache {
// This is used for a Spark bug workaround in useTrackAndUpdateSparkAccountBalances hook.
// Once the bug is resolved we can change this function to simply update the account balance if changed.
// TODO: Update when Spark bug is fixed and workaround is removed.
updateSparkAccountIfBalanceOrWalletChanged(account: SparkAccount) {
updateSparkAccountBalance({
accountId,
availableBalance,
ownedBalance,
}: {
accountId: string;
availableBalance: Money;
ownedBalance: Money;
}) {
this.queryClient.setQueryData([AccountsCache.Key], (curr: Account[]) =>
curr.map((x) => {
if (x.id !== account.id || x.type !== 'spark') return x;
if (x.id !== accountId || x.type !== 'spark') return x;

const versionOk = account.version >= x.version;
const balanceChanged = this.hasDifferentBalanceOrWallet(x, account);
const willUpdate = versionOk && balanceChanged;
const balanceChanged = this.hasDifferentBalance(x, {
availableBalance,
ownedBalance,
});

sparkDebugLog('Cache update check', {
accountId: account.id,
cachedVersion: String(x.version),
incomingVersion: String(account.version),
versionOk: String(versionOk),
balanceChanged: String(balanceChanged),
willUpdate: String(willUpdate),
accountId,
willUpdate: balanceChanged,
newAvailableBalance: availableBalance.toString(),
newOwnedBalance: ownedBalance.toString(),
});

return willUpdate ? account : x;
return balanceChanged ? { ...x, availableBalance, ownedBalance } : x;
}),
);
}

private hasDifferentBalanceOrWallet(
accountOne: SparkAccount,
accountTwo: SparkAccount,
private hasDifferentBalance(
account: SparkAccount,
balance: {
availableBalance: Money;
ownedBalance: Money;
},
) {
const oneOwned = accountOne.ownedBalance ?? Money.zero(accountOne.currency);
const twoOwned = accountTwo.ownedBalance ?? Money.zero(accountTwo.currency);
const oneAvailable =
accountOne.availableBalance ?? Money.zero(accountOne.currency);
const twoAvailable =
accountTwo.availableBalance ?? Money.zero(accountTwo.currency);
const accountOwned = account.ownedBalance ?? Money.zero(account.currency);
const accountAvailable =
account.availableBalance ?? Money.zero(account.currency);

return (
!oneOwned.equals(twoOwned) ||
!oneAvailable.equals(twoAvailable) ||
accountOne.wallet !== accountTwo.wallet
!accountOwned.equals(balance.ownedBalance) ||
!accountAvailable.equals(balance.availableBalance)
);
}

Expand Down
126 changes: 15 additions & 111 deletions app/features/shared/spark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {
type QueryClient,
queryOptions,
useQueries,
useQueryClient,
} from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { type Currency, Money } from '~/lib/money';
import { measureOperation } from '~/lib/performance';
import { computeSHA256 } from '~/lib/sha256';
Expand Down Expand Up @@ -108,27 +106,6 @@ export function sparkBalanceQueryKey(accountId: string) {
export function useTrackAndUpdateSparkAccountBalances() {
const { data: sparkAccounts } = useAccounts({ type: 'spark' });
const accountCache = useAccountsCache();
const queryClient = useQueryClient();

// Needed for workaround below.
// TODO: Remove when workaround is removed.
const verifiedZeroBalanceAccounts = useRef(new Set<string>());

useEffect(() => {
const clear = () => verifiedZeroBalanceAccounts.current.clear();

const onVisibilityChange = () => {
if (document.visibilityState === 'visible') clear();
};

document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('online', clear);
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('online', clear);
};
}, []);
// end workaround

useQueries({
queries: sparkAccounts.map((account) => ({
Expand All @@ -155,108 +132,35 @@ export function useTrackAndUpdateSparkAccountBalances() {
{ accountId: account.id },
);

sparkDebugLog('Balance fetched from Spark SDK', {
accountId: account.id,
owned: String(satsBalance.owned),
available: String(satsBalance.available),
});

// WORKAROUND: Spark SDK sometimes returns 0 for balance incorrectly.
// The bug seems to be resolved after the wallet is reinitialized.
// Reinitialize the wallet and re-check balance.
// TODO: Remove when Spark fixes the bug.
let effectiveOwnedBalance = satsBalance.owned;
let effectiveAvailableBalance = satsBalance.available;
let effectiveWallet = account.wallet;
if (Number(satsBalance.owned) === 0) {
if (!verifiedZeroBalanceAccounts.current.has(account.id)) {
try {
const {
ownedBalance: freshOwnedBalance,
availableBalance: freshAvailableBalance,
wallet: newWallet,
} = await measureOperation(
'SparkWallet.balanceRecovery',
async () => {
console.warn(
'[Spark] Balance returned 0, reinitializing wallet',
{
accountId: account.id,
network: account.network,
},
);

const mnemonic = await queryClient.fetchQuery(
sparkMnemonicQueryOptions(),
);
const newWallet = await queryClient.fetchQuery({
...sparkWalletQueryOptions({
network: account.network,
mnemonic,
}),
staleTime: 0, // Forces a refetch
});

const { satsBalance: freshSatsBalance } =
await newWallet.getBalance();
return {
ownedBalance: freshSatsBalance.owned,
availableBalance: freshSatsBalance.available,
wallet: newWallet,
};
},
{ accountId: account.id },
);

effectiveOwnedBalance = freshOwnedBalance;
effectiveAvailableBalance = freshAvailableBalance;
effectiveWallet = newWallet;

if (Number(freshOwnedBalance) === 0) {
verifiedZeroBalanceAccounts.current.add(account.id);
}
} catch (error) {
console.error('Failed to reinitialize Spark wallet', {
cause: error,
accountId: account.id,
});
return satsBalance.owned;
}
}
} else {
verifiedZeroBalanceAccounts.current.delete(account.id);
}
// END WORKAROUND

const newOwnedBalance = new Money({
amount: Number(effectiveOwnedBalance),
amount: Number(satsBalance.owned),
currency: account.currency as Currency,
unit: getDefaultUnit(account.currency),
});
const newAvailableBalance = new Money({
amount: Number(effectiveAvailableBalance),
amount: Number(satsBalance.available),
currency: account.currency as Currency,
unit: getDefaultUnit(account.currency),
});

sparkDebugLog('Updating accounts cache', {
sparkDebugLog(
'Balance fetched from Spark SDK. Will update accounts cache',
{
accountId: account.id,
prevOwned: account.ownedBalance?.toString() ?? 'null',
newOwned: newOwnedBalance.toString(),
prevAvailable: account.availableBalance?.toString() ?? 'null',
newAvailable: newAvailableBalance.toString(),
accountVersion: String(account.version),
},
);
accountCache.updateSparkAccountBalance({
accountId: account.id,
prevOwned: account.ownedBalance?.toString() ?? 'null',
newOwned: newOwnedBalance.toString(),
prevAvailable: account.availableBalance?.toString() ?? 'null',
newAvailable: newAvailableBalance.toString(),
walletChanged: String(effectiveWallet !== account.wallet),
accountVersion: String(account.version),
});

accountCache.updateSparkAccountIfBalanceOrWalletChanged({
...account,
wallet: effectiveWallet,
ownedBalance: newOwnedBalance,
availableBalance: newAvailableBalance,
});

return effectiveOwnedBalance;
return satsBalance.owned;
},
staleTime: Number.POSITIVE_INFINITY,
gcTime: Number.POSITIVE_INFINITY,
Expand Down
Loading