Skip to content

Commit 6596573

Browse files
ditto-agentclaude
andcommitted
feat: add debug logging for Spark balance update flow
Adds feature-flag-gated debug logs (DEBUG_LOGGING_SPARK) to trace the full balance update chain after Spark sends and receives: payment detection → quote completion → balance invalidation → SDK fetch → cache update. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 38e682f commit 6596573

4 files changed

Lines changed: 92 additions & 20 deletions

File tree

app/features/accounts/account-hooks.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { useCallback, useMemo, useRef } from 'react';
1010
import { type Currency, Money } from '~/lib/money';
1111
import type { AgicashDbAccountWithProofs } from '../agicash-db/database';
12+
import { sparkDebugLog } from '../shared/spark';
1213
import { useUser } from '../user/user-hooks';
1314
import {
1415
type Account,
@@ -55,14 +56,24 @@ export class AccountsCache {
5556
// TODO: Update when Spark bug is fixed and workaround is removed.
5657
updateSparkAccountIfBalanceOrWalletChanged(account: SparkAccount) {
5758
this.queryClient.setQueryData([AccountsCache.Key], (curr: Account[]) =>
58-
curr.map((x) =>
59-
x.id === account.id &&
60-
x.type === 'spark' &&
61-
account.version >= x.version &&
62-
this.hasDifferentBalanceOrWallet(x, account)
63-
? account
64-
: x,
65-
),
59+
curr.map((x) => {
60+
if (x.id !== account.id || x.type !== 'spark') return x;
61+
62+
const versionOk = account.version >= x.version;
63+
const balanceChanged = this.hasDifferentBalanceOrWallet(x, account);
64+
const willUpdate = versionOk && balanceChanged;
65+
66+
sparkDebugLog('Cache update check', {
67+
accountId: account.id,
68+
cachedVersion: String(x.version),
69+
incomingVersion: String(account.version),
70+
versionOk: String(versionOk),
71+
balanceChanged: String(balanceChanged),
72+
willUpdate: String(willUpdate),
73+
});
74+
75+
return willUpdate ? account : x;
76+
}),
6677
);
6778
}
6879

app/features/receive/spark-receive-quote-hooks.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
useSelectItemsWithOnlineAccount,
2323
} from '../accounts/account-hooks';
2424
import type { AgicashDbSparkReceiveQuote } from '../agicash-db/database';
25-
import { sparkBalanceQueryKey } from '../shared/spark';
25+
import { sparkBalanceQueryKey, sparkDebugLog } from '../shared/spark';
2626
import type { TransactionPurpose } from '../transactions/transaction-enums';
2727
import { useTransactionsCache } from '../transactions/transaction-hooks';
2828
import { useUser } from '../user/user-hooks';
@@ -420,6 +420,11 @@ export function useOnSparkReceiveStateChange({
420420
'Spark transfer ID is required when receive request has TRANSFER_COMPLETED status.',
421421
);
422422
}
423+
sparkDebugLog('Receive payment detected as completed', {
424+
quoteId: quote.id,
425+
accountId: quote.accountId,
426+
sparkTransferId: receiveRequest.transfer.sparkId,
427+
});
423428
onCompletedRef.current(quote.id, {
424429
sparkTransferId: receiveRequest.transfer.sparkId,
425430
paymentPreimage: receiveRequest.paymentPreimage,
@@ -502,6 +507,11 @@ export function useProcessSparkReceiveQuoteTasks() {
502507
throwOnError: true,
503508
onSuccess: (updatedQuote) => {
504509
if (updatedQuote) {
510+
sparkDebugLog('Receive quote completed — invalidating balance', {
511+
quoteId: updatedQuote.id,
512+
accountId: updatedQuote.accountId,
513+
transactionId: updatedQuote.transactionId,
514+
});
505515
// Updating the quote cache triggers navigation to the transaction details page.
506516
// Completing the quote also completes the transaction and if navigation to transaction
507517
// page happens before transaction udpated realtime notification is processed, the
@@ -516,6 +526,12 @@ export function useProcessSparkReceiveQuoteTasks() {
516526
queryClient.invalidateQueries({
517527
queryKey: sparkBalanceQueryKey(updatedQuote.accountId),
518528
});
529+
sparkDebugLog('Balance query invalidated', {
530+
accountId: updatedQuote.accountId,
531+
queryKey: JSON.stringify(
532+
sparkBalanceQueryKey(updatedQuote.accountId),
533+
),
534+
});
519535
}
520536
},
521537
onError: (error, { quoteId }) => {

app/features/send/spark-send-quote-hooks.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '../accounts/account-hooks';
1717
import type { AgicashDbSparkSendQuote } from '../agicash-db/database';
1818
import { DomainError } from '../shared/error';
19-
import { sparkBalanceQueryKey } from '../shared/spark';
19+
import { sparkBalanceQueryKey, sparkDebugLog } from '../shared/spark';
2020
import { useUser } from '../user/user-hooks';
2121
import type { SparkSendQuote } from './spark-send-quote';
2222
import { useSparkSendQuoteRepository } from './spark-send-quote-repository';
@@ -206,6 +206,10 @@ export function useOnSparkSendStateChange({
206206

207207
lastTriggeredStateRef.current.set(quoteId, 'COMPLETED');
208208

209+
sparkDebugLog('Send payment detected as completed', {
210+
quoteId: quote.id,
211+
accountId: quote.accountId,
212+
});
209213
onCompletedRef.current(quote, {
210214
paymentPreimage: sendRequest.paymentPreimage,
211215
});
@@ -463,11 +467,21 @@ export function useProcessSparkSendQuoteTasks() {
463467
throwOnError: true,
464468
onSuccess: (updatedQuote) => {
465469
if (updatedQuote) {
470+
sparkDebugLog('Send quote completed — invalidating balance', {
471+
quoteId: updatedQuote.id,
472+
accountId: updatedQuote.accountId,
473+
});
466474
unresolvedQuotesCache.remove(updatedQuote);
467475
// Invalidate spark balance since we sent funds
468476
queryClient.invalidateQueries({
469477
queryKey: sparkBalanceQueryKey(updatedQuote.accountId),
470478
});
479+
sparkDebugLog('Balance query invalidated', {
480+
accountId: updatedQuote.accountId,
481+
queryKey: JSON.stringify(
482+
sparkBalanceQueryKey(updatedQuote.accountId),
483+
),
484+
});
471485
}
472486
},
473487
onError: (error, { quote }) => {

app/features/shared/spark.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import {
2020
import { getSeedPhraseDerivationPath } from '../accounts/account-cryptography';
2121
import { useAccounts, useAccountsCache } from '../accounts/account-hooks';
2222
import { getDefaultUnit } from './currencies';
23+
import { getFeatureFlag } from './feature-flags';
24+
25+
export function sparkDebugLog(message: string, data?: Record<string, unknown>) {
26+
if (getFeatureFlag('DEBUG_LOGGING_SPARK')) {
27+
console.debug(`[Spark] ${message}`, data ?? '');
28+
}
29+
}
2330

2431
const seedDerivationPath = getSeedPhraseDerivationPath('spark', 12);
2532

@@ -134,15 +141,26 @@ export function useTrackAndUpdateSparkAccountBalances() {
134141
}
135142

136143
if (!account.isOnline) {
144+
sparkDebugLog('Skipping balance poll — account offline', {
145+
accountId: account.id,
146+
});
137147
return null;
138148
}
139149

150+
sparkDebugLog('Polling balance', { accountId: account.id });
151+
140152
const { satsBalance } = await measureOperation(
141153
'SparkWallet.getBalance',
142154
() => account.wallet.getBalance(),
143155
{ accountId: account.id },
144156
);
145157

158+
sparkDebugLog('Balance fetched from Spark SDK', {
159+
accountId: account.id,
160+
owned: String(satsBalance.owned),
161+
available: String(satsBalance.available),
162+
});
163+
146164
// WORKAROUND: Spark SDK sometimes returns 0 for balance incorrectly.
147165
// The bug seems to be resolved after the wallet is reinitialized.
148166
// Reinitialize the wallet and re-check balance.
@@ -210,19 +228,32 @@ export function useTrackAndUpdateSparkAccountBalances() {
210228
}
211229
// END WORKAROUND
212230

231+
const newOwnedBalance = new Money({
232+
amount: Number(effectiveOwnedBalance),
233+
currency: account.currency as Currency,
234+
unit: getDefaultUnit(account.currency),
235+
});
236+
const newAvailableBalance = new Money({
237+
amount: Number(effectiveAvailableBalance),
238+
currency: account.currency as Currency,
239+
unit: getDefaultUnit(account.currency),
240+
});
241+
242+
sparkDebugLog('Updating accounts cache', {
243+
accountId: account.id,
244+
prevOwned: account.ownedBalance?.toString() ?? 'null',
245+
newOwned: newOwnedBalance.toString(),
246+
prevAvailable: account.availableBalance?.toString() ?? 'null',
247+
newAvailable: newAvailableBalance.toString(),
248+
walletChanged: String(effectiveWallet !== account.wallet),
249+
accountVersion: String(account.version),
250+
});
251+
213252
accountCache.updateSparkAccountIfBalanceOrWalletChanged({
214253
...account,
215254
wallet: effectiveWallet,
216-
ownedBalance: new Money({
217-
amount: Number(effectiveOwnedBalance),
218-
currency: account.currency as Currency,
219-
unit: getDefaultUnit(account.currency),
220-
}),
221-
availableBalance: new Money({
222-
amount: Number(effectiveAvailableBalance),
223-
currency: account.currency as Currency,
224-
unit: getDefaultUnit(account.currency),
225-
}),
255+
ownedBalance: newOwnedBalance,
256+
availableBalance: newAvailableBalance,
226257
});
227258

228259
return effectiveOwnedBalance;

0 commit comments

Comments
 (0)