diff --git a/app/features/accounts/account-hooks.ts b/app/features/accounts/account-hooks.ts index 4ed4fb51..af518d28 100644 --- a/app/features/accounts/account-hooks.ts +++ b/app/features/accounts/account-hooks.ts @@ -9,6 +9,7 @@ import { import { useCallback, useMemo, useRef } from 'react'; import { type Currency, Money } from '~/lib/money'; import type { AgicashDbAccountWithProofs } from '../agicash-db/database'; +import { sparkDebugLog } from '../shared/spark'; import { useUser } from '../user/user-hooks'; import { type Account, @@ -55,14 +56,24 @@ export class AccountsCache { // TODO: Update when Spark bug is fixed and workaround is removed. updateSparkAccountIfBalanceOrWalletChanged(account: SparkAccount) { this.queryClient.setQueryData([AccountsCache.Key], (curr: Account[]) => - curr.map((x) => - x.id === account.id && - x.type === 'spark' && - account.version >= x.version && - this.hasDifferentBalanceOrWallet(x, account) - ? account - : x, - ), + curr.map((x) => { + if (x.id !== account.id || x.type !== 'spark') return x; + + const versionOk = account.version >= x.version; + const balanceChanged = this.hasDifferentBalanceOrWallet(x, account); + const willUpdate = versionOk && balanceChanged; + + sparkDebugLog('Cache update check', { + accountId: account.id, + cachedVersion: String(x.version), + incomingVersion: String(account.version), + versionOk: String(versionOk), + balanceChanged: String(balanceChanged), + willUpdate: String(willUpdate), + }); + + return willUpdate ? account : x; + }), ); } diff --git a/app/features/receive/spark-receive-quote-hooks.ts b/app/features/receive/spark-receive-quote-hooks.ts index 00f9e0ad..2cbc4f54 100644 --- a/app/features/receive/spark-receive-quote-hooks.ts +++ b/app/features/receive/spark-receive-quote-hooks.ts @@ -22,7 +22,7 @@ import { useSelectItemsWithOnlineAccount, } from '../accounts/account-hooks'; import type { AgicashDbSparkReceiveQuote } from '../agicash-db/database'; -import { sparkBalanceQueryKey } from '../shared/spark'; +import { sparkBalanceQueryKey, sparkDebugLog } from '../shared/spark'; import type { TransactionPurpose } from '../transactions/transaction-enums'; import { useTransactionsCache } from '../transactions/transaction-hooks'; import { useUser } from '../user/user-hooks'; @@ -420,6 +420,11 @@ export function useOnSparkReceiveStateChange({ 'Spark transfer ID is required when receive request has TRANSFER_COMPLETED status.', ); } + sparkDebugLog('Receive payment detected as completed', { + quoteId: quote.id, + accountId: quote.accountId, + sparkTransferId: receiveRequest.transfer.sparkId, + }); onCompletedRef.current(quote.id, { sparkTransferId: receiveRequest.transfer.sparkId, paymentPreimage: receiveRequest.paymentPreimage, @@ -502,6 +507,11 @@ export function useProcessSparkReceiveQuoteTasks() { throwOnError: true, onSuccess: (updatedQuote) => { if (updatedQuote) { + sparkDebugLog('Receive quote completed — invalidating balance', { + quoteId: updatedQuote.id, + accountId: updatedQuote.accountId, + transactionId: updatedQuote.transactionId, + }); // Updating the quote cache triggers navigation to the transaction details page. // Completing the quote also completes the transaction and if navigation to transaction // page happens before transaction udpated realtime notification is processed, the @@ -516,6 +526,12 @@ export function useProcessSparkReceiveQuoteTasks() { queryClient.invalidateQueries({ queryKey: sparkBalanceQueryKey(updatedQuote.accountId), }); + sparkDebugLog('Balance query invalidated', { + accountId: updatedQuote.accountId, + queryKey: JSON.stringify( + sparkBalanceQueryKey(updatedQuote.accountId), + ), + }); } }, onError: (error, { quoteId }) => { diff --git a/app/features/send/spark-send-quote-hooks.ts b/app/features/send/spark-send-quote-hooks.ts index bf5881bb..c02f2539 100644 --- a/app/features/send/spark-send-quote-hooks.ts +++ b/app/features/send/spark-send-quote-hooks.ts @@ -16,7 +16,7 @@ import { } from '../accounts/account-hooks'; import type { AgicashDbSparkSendQuote } from '../agicash-db/database'; import { DomainError } from '../shared/error'; -import { sparkBalanceQueryKey } from '../shared/spark'; +import { sparkBalanceQueryKey, sparkDebugLog } from '../shared/spark'; import { useUser } from '../user/user-hooks'; import type { SparkSendQuote } from './spark-send-quote'; import { useSparkSendQuoteRepository } from './spark-send-quote-repository'; @@ -206,6 +206,10 @@ export function useOnSparkSendStateChange({ lastTriggeredStateRef.current.set(quoteId, 'COMPLETED'); + sparkDebugLog('Send payment detected as completed', { + quoteId: quote.id, + accountId: quote.accountId, + }); onCompletedRef.current(quote, { paymentPreimage: sendRequest.paymentPreimage, }); @@ -463,11 +467,21 @@ export function useProcessSparkSendQuoteTasks() { throwOnError: true, onSuccess: (updatedQuote) => { if (updatedQuote) { + sparkDebugLog('Send quote completed — invalidating balance', { + quoteId: updatedQuote.id, + accountId: updatedQuote.accountId, + }); unresolvedQuotesCache.remove(updatedQuote); // Invalidate spark balance since we sent funds queryClient.invalidateQueries({ queryKey: sparkBalanceQueryKey(updatedQuote.accountId), }); + sparkDebugLog('Balance query invalidated', { + accountId: updatedQuote.accountId, + queryKey: JSON.stringify( + sparkBalanceQueryKey(updatedQuote.accountId), + ), + }); } }, onError: (error, { quote }) => { diff --git a/app/features/shared/spark.ts b/app/features/shared/spark.ts index 1b4e5b0f..4e42402e 100644 --- a/app/features/shared/spark.ts +++ b/app/features/shared/spark.ts @@ -20,6 +20,13 @@ import { import { getSeedPhraseDerivationPath } from '../accounts/account-cryptography'; import { useAccounts, useAccountsCache } from '../accounts/account-hooks'; import { getDefaultUnit } from './currencies'; +import { getFeatureFlag } from './feature-flags'; + +export function sparkDebugLog(message: string, data?: Record) { + if (getFeatureFlag('DEBUG_LOGGING_SPARK')) { + console.debug(`[Spark] ${message}`, data ?? ''); + } +} const seedDerivationPath = getSeedPhraseDerivationPath('spark', 12); @@ -134,15 +141,26 @@ export function useTrackAndUpdateSparkAccountBalances() { } if (!account.isOnline) { + sparkDebugLog('Skipping balance poll — account offline', { + accountId: account.id, + }); return null; } + sparkDebugLog('Polling balance', { accountId: account.id }); + const { satsBalance } = await measureOperation( 'SparkWallet.getBalance', () => account.wallet.getBalance(), { 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. @@ -210,19 +228,32 @@ export function useTrackAndUpdateSparkAccountBalances() { } // END WORKAROUND + const newOwnedBalance = new Money({ + amount: Number(effectiveOwnedBalance), + currency: account.currency as Currency, + unit: getDefaultUnit(account.currency), + }); + const newAvailableBalance = new Money({ + amount: Number(effectiveAvailableBalance), + currency: account.currency as Currency, + unit: getDefaultUnit(account.currency), + }); + + sparkDebugLog('Updating accounts cache', { + 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: new Money({ - amount: Number(effectiveOwnedBalance), - currency: account.currency as Currency, - unit: getDefaultUnit(account.currency), - }), - availableBalance: new Money({ - amount: Number(effectiveAvailableBalance), - currency: account.currency as Currency, - unit: getDefaultUnit(account.currency), - }), + ownedBalance: newOwnedBalance, + availableBalance: newAvailableBalance, }); return effectiveOwnedBalance;