From 06b10af22eea7aee6d1ebf741ece1d67bf2abada Mon Sep 17 00:00:00 2001 From: "rhys.bartelswaller@iohk.io" Date: Sun, 31 Aug 2025 01:04:40 +0100 Subject: [PATCH 1/4] feat: implement comprehensive rewards debugging system with browser view support - Add detailed console logging in useStakingRewards hook - Store debug information in global window objects - Fix browser view debugging by temporarily disabling multi-delegation - Create debug-extractor.js script for comprehensive data extraction - Update DEBUG_INSTRUCTIONS.md with complete debugging workflow - Ensure debugging works in both popup and browser tab views - Add ESLint disable comments to resolve linting issues --- .../DEBUG_INSTRUCTIONS.md | 99 +++++++++++++++ .../debug-extractor.js | 64 ++++++++++ .../src/hooks/useMultiDelegationEnabled.ts | 34 ++--- .../src/hooks/useStakingRewards.ts | 120 ++++++++++++++++-- .../features/staking/components/Staking.tsx | 23 ++-- .../components/StakingInfo/StakingInfo.tsx | 40 +++++- 6 files changed, 335 insertions(+), 45 deletions(-) create mode 100644 apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md create mode 100644 apps/browser-extension-wallet/debug-extractor.js diff --git a/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md new file mode 100644 index 0000000000..569873ed67 --- /dev/null +++ b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md @@ -0,0 +1,99 @@ +# Lace Wallet - Rewards Debugging Instructions + +## Overview +This version of Lace Wallet includes comprehensive debugging information for investigating rewards calculation discrepancies. The debugging information is accessible through the browser console and stored in global variables for easy inspection. + +## How to Use + +### 1. Install the Extension +1. Build the application: `DROP_CONSOLE_IN_PRODUCTION=false yarn build` +2. Load the `dist/` folder as an unpacked extension in Chrome/Firefox +3. Navigate to the Staking page + +### 2. View Debug Information + +#### Console Access (All Views) +Open the browser console (F12) and access: + +```javascript +// Total rewards calculation details +window.rewardsDebugInfo + +// Last reward calculation details +window.lastRewardDebugInfo +``` + +### 3. Use the Debug Extractor Script +For comprehensive debug data extraction, run the provided script in the console: + +```javascript +// Copy and paste the contents of debug-extractor.js into the console +// Or run it directly if you have it saved +``` + +### 4. What to Look For + +#### Rewards Calculation Flow +1. **Raw Rewards History**: Check `window.rewardsDebugInfo.rawRewardsHistoryLength` +2. **Epoch Filtering**: Current epoch and which epochs are excluded +3. **Confirmed Rewards**: Number of rewards after filtering +4. **BigNumber Conversion**: See the exact values being summed +5. **Final ADA Conversion**: Check the division by 1,000,000 + +#### Key Debug Points +- **Current Epoch**: What epoch the calculation is based on +- **Epochs Excluded**: Which epochs are being filtered out (LAST_STABLE_EPOCH = 2) +- **Rewards Array**: The exact string values being passed to BigNumber.sum +- **Precision**: Check if any precision is lost during string conversion +- **Type Consistency**: Ensure all values are the expected types + +### 5. Expected Behavior +- **Total Rewards**: Should match explorer values (accounting for 2-epoch delay) +- **Last Reward**: Should show the most recent confirmed reward +- **Epoch Filtering**: Should exclude current epoch and previous epoch + +### 6. Common Issues to Check +1. **String Conversion**: BigNumber.sum with string conversion may cause precision issues +2. **Epoch Timing**: 2-epoch delay might not account for all discrepancies +3. **Data Source**: Verify the rewards data coming from Blockfrost +4. **Type Mismatches**: Check for BigInt vs BigNumber vs number type issues + +### 7. Reporting Issues +When reporting discrepancies, include: +- Console output of `window.rewardsDebugInfo` +- Console output of `window.lastRewardDebugInfo` +- Expected vs actual values +- Wallet address (if safe to share) +- Time period of the discrepancy + +## Technical Details + +### Build Configuration +- Set `DROP_CONSOLE_IN_PRODUCTION=false` to preserve debugging code +- Uses production webpack configuration with debugging enabled +- Console logging and global variable storage preserved + +### Debug Implementation +- Debug info stored in `window.rewardsDebugInfo` and `window.lastRewardDebugInfo` +- Clean console logging in `useStakingRewards` hook (no noisy initialization logs) +- Console access available in both popup and browser views +- No visible UI debug elements - all debugging via console +- **Browser View Fix**: Temporarily disabled multi-delegation to ensure local Staking component is used + +### File Locations +- **Hook**: `src/hooks/useStakingRewards.ts` +- **Browser View**: `src/views/browser-view/features/staking/components/Staking.tsx` +- **Display**: `src/views/browser-view/features/staking/components/StakingInfo/StakingInfo.tsx` +- **Browser View Fix**: `src/hooks/useMultiDelegationEnabled.ts` (temporarily returns false) + +### Accessing Debug Information +- **Console Logs**: Essential rewards calculation details without noise +- **Window Objects**: Access debug data via `window.rewardsDebugInfo` and `window.lastRewardDebugInfo` +- **Debug Script**: Use `debug-extractor.js` for comprehensive data extraction +- **Clean Output**: Focused on rewards calculation details without cluttering the console +- **Both Views**: Debugging now works in popup AND browser tab view + +### Important Notes +- **Multi-Delegation Disabled**: For debugging purposes, the browser view has been configured to use the local Staking component instead of the `@lace/staking` package component +- **Temporary Change**: This change is in `useMultiDelegationEnabled.ts` and should be reverted after debugging is complete +- **Consistent Experience**: Both popup and browser views now provide the same debugging information diff --git a/apps/browser-extension-wallet/debug-extractor.js b/apps/browser-extension-wallet/debug-extractor.js new file mode 100644 index 0000000000..fd18dab8bb --- /dev/null +++ b/apps/browser-extension-wallet/debug-extractor.js @@ -0,0 +1,64 @@ +// Lace Wallet Debug Information Extractor +// Run this script in the browser console on the staking page +// +// NOTE: Debugging works in both popup AND browser tab view! +// The browser view has been configured to use the local Staking component +// with debugging enabled instead of the @lace/staking package component. + +console.log('šŸ” LACE WALLET REWARDS DEBUG EXTRACTOR'); +console.log('====================================='); +console.log('āœ… Debugging enabled for both popup and browser tab views'); + +// Extract rewards debug info +if (window.rewardsDebugInfo) { + console.log('\nšŸ“Š TOTAL REWARDS DEBUG INFO:'); + console.log('Current Epoch:', window.rewardsDebugInfo.currentEpoch); + console.log('Raw History Length:', window.rewardsDebugInfo.rawRewardsHistoryLength); + console.log('Confirmed Length:', window.rewardsDebugInfo.confirmedRewardsLength); + console.log('Rewards Array:', window.rewardsDebugInfo.rewardsArray); + console.log('Total BigNumber:', window.rewardsDebugInfo.totalBigNumber); + console.log('Total ADA:', window.rewardsDebugInfo.totalADA); +} else { + console.log('āŒ No rewards debug info available'); +} + +// Extract last reward debug info +if (window.lastRewardDebugInfo) { + console.log('\nšŸŽÆ LAST REWARD DEBUG INFO:'); + console.log('Raw Value:', window.lastRewardDebugInfo.lastRewardRaw); + console.log('BigNumber Value:', window.lastRewardDebugInfo.lastRewardBigNumber); + console.log('ADA Value:', window.lastRewardDebugInfo.lastRewardADA); + console.log('Epoch:', window.lastRewardDebugInfo.epoch); +} else { + console.log('āŒ No last reward debug info available'); +} + +// Check for any console errors or warnings +console.log('\nāš ļø CONSOLE STATUS:'); +console.log('Console logging enabled:', typeof console.log === 'function'); +console.log('Debug data available:', !!(window.rewardsDebugInfo || window.lastRewardDebugInfo)); + +// Export debug data for easy copying +const exportDebugData = () => { + const debugData = { + timestamp: new Date().toISOString(), + rewardsDebugInfo: window.rewardsDebugInfo || null, + lastRewardDebugInfo: window.lastRewardDebugInfo || null + }; + + console.log('\nšŸ“‹ EXPORTABLE DEBUG DATA:'); + console.log(JSON.stringify(debugData, null, 2)); + + // Copy to clipboard if available + if (navigator.clipboard) { + navigator.clipboard.writeText(JSON.stringify(debugData, null, 2)) + .then(() => console.log('āœ… Debug data copied to clipboard')) + .catch(err => console.log('āŒ Failed to copy to clipboard:', err)); + } +}; + +// Make export function available globally +window.exportLaceDebugData = exportDebugData; + +console.log('\nšŸ’” TIP: Run exportLaceDebugData() to export all debug data'); +console.log('====================================='); diff --git a/apps/browser-extension-wallet/src/hooks/useMultiDelegationEnabled.ts b/apps/browser-extension-wallet/src/hooks/useMultiDelegationEnabled.ts index 756243d9fe..25ee7ae69f 100644 --- a/apps/browser-extension-wallet/src/hooks/useMultiDelegationEnabled.ts +++ b/apps/browser-extension-wallet/src/hooks/useMultiDelegationEnabled.ts @@ -1,18 +1,22 @@ -import { WalletType } from '@cardano-sdk/web-extension'; -import { useWalletStore } from '@src/stores'; import { useMemo } from 'react'; -export const useMultiDelegationEnabled = (): boolean => { - const { walletType } = useWalletStore(); +export const useMultiDelegationEnabled = (): boolean => + // Temporarily disabled for debugging - no need to access wallet state + // const { walletType } = useWalletStore(); - return useMemo(() => { - switch (walletType) { - case WalletType.Ledger: - return process.env.USE_MULTI_DELEGATION_STAKING_LEDGER === 'true'; - case WalletType.Trezor: - return process.env.USE_MULTI_DELEGATION_STAKING_TREZOR === 'true'; - default: - return true; - } - }, [walletType]); -}; + useMemo( + () => + // TEMPORARILY DISABLED FOR DEBUGGING - Force use of local Staking component + false, + + // Original logic (commented out for debugging): + // switch (walletType) { + // case WalletType.Ledger: + // return process.env.USE_MULTI_DELEGATION_STAKING_LEDGER === 'true'; + // case WalletType.Trezor: + // return process.env.USE_MULTI_DELEGATION_STAKING_TREZOR === 'true'; + // default: + // return true; + // } + [] + ); // Removed walletType dependency since we're returning false diff --git a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts index 3a6717e840..242947c060 100644 --- a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts +++ b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts @@ -1,8 +1,9 @@ +/* eslint-disable sonarjs/cognitive-complexity, max-statements, complexity */ import { useWalletStore } from '@src/stores'; import BigNumber from 'bignumber.js'; import { useMemo } from 'react'; import { combineLatest, map } from 'rxjs'; -import { useObservable } from '@lace/common'; +import { useObservable, logger } from '@lace/common'; interface UseStakingRewardsReturns { totalRewards: BigInt | number; @@ -10,33 +11,126 @@ interface UseStakingRewardsReturns { } const LAST_STABLE_EPOCH = 2; +const LOVELACE_TO_ADA = 1_000_000; + +// Debug logging function +const logRewardsDebug = (message: string, data?: unknown) => { + logger.info(`šŸ” [REWARDS_DEBUG] ${message}`, data || ''); +}; export const useStakingRewards = (): UseStakingRewardsReturns => { const { inMemoryWallet } = useWalletStore(); + const rewardsSummary = useObservable( useMemo( () => combineLatest([inMemoryWallet.currentEpoch$, inMemoryWallet.delegation.rewardsHistory$]).pipe( map(([{ epochNo }, { all }]) => { - // rewards do not match the ones in the explorer because we aren't taking into account chain rollbacks + logRewardsDebug('=== REWARDS CALCULATION START ==='); + logRewardsDebug('Current epoch:', epochNo?.valueOf()); + logRewardsDebug('Total rewards history entries:', all?.length || 0); + logRewardsDebug('Raw rewards history:', all); + + // rewards do not become stable until 2 epochs after they are distributed const lastNonVolatileEpoch = epochNo.valueOf() - LAST_STABLE_EPOCH; - const confirmedRewardHistory = all.filter(({ epoch }) => epoch.valueOf() <= lastNonVolatileEpoch); + logRewardsDebug('Last non-volatile epoch:', lastNonVolatileEpoch); + logRewardsDebug('LAST_STABLE_EPOCH constant:', LAST_STABLE_EPOCH); + logRewardsDebug('Epochs being excluded:', [epochNo.valueOf(), epochNo.valueOf() - 1]); + + const confirmedRewardHistory = all?.filter(({ epoch }) => epoch.valueOf() <= lastNonVolatileEpoch) ?? []; + logRewardsDebug('Confirmed rewards history (filtered):', confirmedRewardHistory); + logRewardsDebug('Number of confirmed rewards:', confirmedRewardHistory.length); + + if (confirmedRewardHistory.length > 0) { + logRewardsDebug('First confirmed reward:', confirmedRewardHistory[0]); + logRewardsDebug('Last confirmed reward:', confirmedRewardHistory[confirmedRewardHistory.length - 1]); + + // Log each reward entry for detailed inspection + logRewardsDebug('=== DETAILED REWARDS BREAKDOWN ==='); + confirmedRewardHistory.forEach((reward, index) => { + logRewardsDebug(`Reward ${index + 1}:`, { + epoch: reward.epoch.valueOf(), + rewards: reward.rewards.toString(), + poolId: reward.poolId?.toString(), + rewardsInLovelace: reward.rewards.toString(), + // eslint-disable-next-line no-magic-numbers + rewardsInAda: (Number(reward.rewards) / LOVELACE_TO_ADA).toFixed(6) + }); + }); + } + + // ORIGINAL LOGIC: Use BigNumber.sum with string conversion + let totalRewards = 0; + let lastReward = 0; + let totalBigNumber: BigNumber | undefined; + + if (confirmedRewardHistory.length > 0) { + const rewardStrings = confirmedRewardHistory.map(({ rewards }) => rewards.toString()); + const total = BigNumber.sum.apply(undefined, rewardStrings ?? []); + totalBigNumber = total; + totalRewards = total.dividedBy(LOVELACE_TO_ADA).toNumber(); + + // Store debug info for UI display + const debugInfo = { + currentEpoch: epochNo?.valueOf(), + rawRewardsHistoryLength: all?.length || 0, + confirmedRewardsLength: confirmedRewardHistory?.length || 0, + rewardsArray: rewardStrings, + totalBigNumber: total.toString(), + totalADA: total.dividedBy(LOVELACE_TO_ADA).toString() + }; + + // Store debug info in a way that won't be stripped by minification + if (typeof window !== 'undefined') { + (window as unknown as Record).rewardsDebugInfo = debugInfo; + } + + // Calculate last reward + const last = confirmedRewardHistory[confirmedRewardHistory.length - 1]; + const lastRewardValue = new BigNumber(last.rewards.toString()); + lastReward = lastRewardValue.dividedBy(LOVELACE_TO_ADA).toNumber(); + + // Store debug info for UI display + const lastRewardDebugInfo = { + lastRewardRaw: last.rewards.toString(), + lastRewardBigNumber: lastRewardValue.toString(), + lastRewardADA: lastRewardValue.dividedBy(LOVELACE_TO_ADA).toString(), + epoch: last.epoch.valueOf() + }; + + // Store debug info in a way that won't be stripped by minification + if (typeof window !== 'undefined') { + (window as unknown as Record).lastRewardDebugInfo = lastRewardDebugInfo; + } + } + + logRewardsDebug('Total rewards calculation details:'); + logRewardsDebug(' - confirmedRewardHistory.length:', confirmedRewardHistory.length); + logRewardsDebug( + ' - rewards values:', + confirmedRewardHistory.map(({ rewards }) => rewards.toString()) + ); + logRewardsDebug(' - BigNumber.sum result:', totalBigNumber?.toString() || '0'); + logRewardsDebug(' - totalRewards type:', typeof totalRewards); + logRewardsDebug(' - totalRewards value:', totalRewards); + logRewardsDebug( + ' - totalRewards in ADA:', + totalBigNumber ? totalBigNumber.dividedBy(LOVELACE_TO_ADA).toString() : '0' + ); + + logRewardsDebug('Last reward:', lastReward); + logRewardsDebug('Last reward type:', typeof lastReward); + logRewardsDebug('=== REWARDS CALCULATION END ==='); return { - totalRewards: - confirmedRewardHistory?.length > 0 - ? // eslint-disable-next-line unicorn/no-null - BigNumber.sum.apply(null, confirmedRewardHistory.map(({ rewards }) => rewards.toString()) ?? []) - : 0, - lastReward: confirmedRewardHistory[confirmedRewardHistory.length - 1]?.rewards || 0 + totalRewards, + lastReward }; }) ), [inMemoryWallet.currentEpoch$, inMemoryWallet.delegation.rewardsHistory$] ) ); - return { - totalRewards: rewardsSummary?.totalRewards || 0, - lastReward: rewardsSummary?.lastReward || 0 - }; + + return rewardsSummary ?? { totalRewards: 0, lastReward: 0 }; }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx index 90afe2c433..8ed3cc0235 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx @@ -114,19 +114,14 @@ export const Staking = (): React.ReactElement => { ]); return ( - <> - {showRegisterAsDRepBanner && ( - - - - )} -
- +
+
+ {showRegisterAsDRepBanner && ( + + + + )} + {hasNoFunds && ( { onStake={onStake} />
- +
); }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingInfo/StakingInfo.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingInfo/StakingInfo.tsx index 6f22f780a4..06940e7163 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingInfo/StakingInfo.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingInfo/StakingInfo.tsx @@ -8,7 +8,7 @@ import styles from './StakingInfo.module.scss'; import { StakePoolInfo } from './StakePoolInfo'; import { Stats } from './Stats'; import { Tooltip } from './StatsTooltip'; -import { addEllipsis, getRandomIcon } from '@lace/common'; +import { addEllipsis, getRandomIcon, logger } from '@lace/common'; import { formatLocaleNumber } from '@utils/format-number'; const formatNumericValue = (val: number | string, suffix: number | string): React.ReactElement => ( @@ -58,6 +58,40 @@ export const StakingInfo = ({ const { t } = useTranslation(); const ref = useRef(); + // Debug logging for rewards display + logger.info('šŸ” [STAKING_INFO_DEBUG] === STAKING INFO COMPONENT DEBUG ==='); + logger.info('šŸ” [STAKING_INFO_DEBUG] Component props:', { + totalRewards, + lastReward, + totalRewardsType: typeof totalRewards, + lastRewardType: typeof lastReward, + coinBalance, + id: id?.toString() + }); + + // Log the rewards values being displayed + if (totalRewards) { + logger.info('šŸ” [STAKING_INFO_DEBUG] totalRewards details:', { + value: totalRewards, + type: typeof totalRewards, + stringValue: totalRewards.toString(), + isBigInt: typeof totalRewards === 'bigint', + isNumber: typeof totalRewards === 'number' + }); + } + + if (lastReward) { + logger.info('šŸ” [STAKING_INFO_DEBUG] lastReward details:', { + value: lastReward, + type: typeof lastReward, + stringValue: lastReward.toString(), + isBigInt: typeof lastReward === 'bigint', + isNumber: typeof lastReward === 'number' + }); + } + + logger.info('šŸ” [STAKING_INFO_DEBUG] === END STAKING INFO DEBUG ==='); + return (
+ {totalRewards} {cardanoCoin.symbol} @@ -133,7 +167,7 @@ export const StakingInfo = ({ + {lastReward} {cardanoCoin.symbol} From 5b36d6a8f8d6b9b999e2b54f21f88fffdf6f86bb Mon Sep 17 00:00:00 2001 From: "rhys.bartelswaller@iohk.io" Date: Sun, 31 Aug 2025 12:26:24 +0100 Subject: [PATCH 2/4] feat: improve 2-epoch offset description and variable naming in useStakingRewards --- .../src/hooks/useStakingRewards.ts | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts index 242947c060..504dc02aab 100644 --- a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts +++ b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts @@ -31,23 +31,33 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { logRewardsDebug('Total rewards history entries:', all?.length || 0); logRewardsDebug('Raw rewards history:', all); - // rewards do not become stable until 2 epochs after they are distributed - const lastNonVolatileEpoch = epochNo.valueOf() - LAST_STABLE_EPOCH; - logRewardsDebug('Last non-volatile epoch:', lastNonVolatileEpoch); + // Rewards are calculated and added to the database immediately when they are distributed + // However, they need 2 epochs to pass before they become available for withdrawal + // This is a Cardano protocol requirement, not a database limitation + const lastWithdrawableEpoch = epochNo.valueOf() - LAST_STABLE_EPOCH; + logRewardsDebug('Current epoch:', epochNo?.valueOf()); logRewardsDebug('LAST_STABLE_EPOCH constant:', LAST_STABLE_EPOCH); - logRewardsDebug('Epochs being excluded:', [epochNo.valueOf(), epochNo.valueOf() - 1]); - - const confirmedRewardHistory = all?.filter(({ epoch }) => epoch.valueOf() <= lastNonVolatileEpoch) ?? []; - logRewardsDebug('Confirmed rewards history (filtered):', confirmedRewardHistory); - logRewardsDebug('Number of confirmed rewards:', confirmedRewardHistory.length); - - if (confirmedRewardHistory.length > 0) { - logRewardsDebug('First confirmed reward:', confirmedRewardHistory[0]); - logRewardsDebug('Last confirmed reward:', confirmedRewardHistory[confirmedRewardHistory.length - 1]); + logRewardsDebug('Last epoch with withdrawable rewards:', lastWithdrawableEpoch); + logRewardsDebug('Epochs with non-withdrawable rewards (excluded):', [ + epochNo.valueOf(), + epochNo.valueOf() - 1 + ]); + + const withdrawableRewardHistory = + all?.filter(({ epoch }) => epoch.valueOf() <= lastWithdrawableEpoch) ?? []; + logRewardsDebug('Withdrawable rewards history (filtered):', withdrawableRewardHistory); + logRewardsDebug('Number of withdrawable rewards:', withdrawableRewardHistory.length); + + if (withdrawableRewardHistory.length > 0) { + logRewardsDebug('First withdrawable reward:', withdrawableRewardHistory[0]); + logRewardsDebug( + 'Last withdrawable reward:', + withdrawableRewardHistory[withdrawableRewardHistory.length - 1] + ); // Log each reward entry for detailed inspection logRewardsDebug('=== DETAILED REWARDS BREAKDOWN ==='); - confirmedRewardHistory.forEach((reward, index) => { + withdrawableRewardHistory.forEach((reward, index) => { logRewardsDebug(`Reward ${index + 1}:`, { epoch: reward.epoch.valueOf(), rewards: reward.rewards.toString(), @@ -64,8 +74,8 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { let lastReward = 0; let totalBigNumber: BigNumber | undefined; - if (confirmedRewardHistory.length > 0) { - const rewardStrings = confirmedRewardHistory.map(({ rewards }) => rewards.toString()); + if (withdrawableRewardHistory.length > 0) { + const rewardStrings = withdrawableRewardHistory.map(({ rewards }) => rewards.toString()); const total = BigNumber.sum.apply(undefined, rewardStrings ?? []); totalBigNumber = total; totalRewards = total.dividedBy(LOVELACE_TO_ADA).toNumber(); @@ -74,7 +84,7 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { const debugInfo = { currentEpoch: epochNo?.valueOf(), rawRewardsHistoryLength: all?.length || 0, - confirmedRewardsLength: confirmedRewardHistory?.length || 0, + withdrawableRewardsLength: withdrawableRewardHistory?.length || 0, rewardsArray: rewardStrings, totalBigNumber: total.toString(), totalADA: total.dividedBy(LOVELACE_TO_ADA).toString() @@ -86,7 +96,7 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { } // Calculate last reward - const last = confirmedRewardHistory[confirmedRewardHistory.length - 1]; + const last = withdrawableRewardHistory[withdrawableRewardHistory.length - 1]; const lastRewardValue = new BigNumber(last.rewards.toString()); lastReward = lastRewardValue.dividedBy(LOVELACE_TO_ADA).toNumber(); @@ -105,10 +115,10 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { } logRewardsDebug('Total rewards calculation details:'); - logRewardsDebug(' - confirmedRewardHistory.length:', confirmedRewardHistory.length); + logRewardsDebug(' - withdrawableRewardHistory.length:', withdrawableRewardHistory.length); logRewardsDebug( ' - rewards values:', - confirmedRewardHistory.map(({ rewards }) => rewards.toString()) + withdrawableRewardHistory.map(({ rewards }) => rewards.toString()) ); logRewardsDebug(' - BigNumber.sum result:', totalBigNumber?.toString() || '0'); logRewardsDebug(' - totalRewards type:', typeof totalRewards); From c49ba9b66b1bdfcb5ad83d2a63f9aed13e6c3598 Mon Sep 17 00:00:00 2001 From: "rhys.bartelswaller@iohk.io" Date: Sun, 31 Aug 2025 14:13:56 +0100 Subject: [PATCH 3/4] feat: enhance rewards debugging with raw vs filtered comparison and 2-epoch offset analysis --- .../DEBUG_INSTRUCTIONS.md | 11 +++- .../debug-extractor.js | 17 +++--- .../src/hooks/useStakingRewards.ts | 54 ++++++++++++++++++- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md index 569873ed67..e6c7f531ad 100644 --- a/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md +++ b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md @@ -36,16 +36,25 @@ For comprehensive debug data extraction, run the provided script in the console: #### Rewards Calculation Flow 1. **Raw Rewards History**: Check `window.rewardsDebugInfo.rawRewardsHistoryLength` 2. **Epoch Filtering**: Current epoch and which epochs are excluded -3. **Confirmed Rewards**: Number of rewards after filtering +3. **Withdrawable Rewards**: Number of rewards after filtering (2-epoch offset) 4. **BigNumber Conversion**: See the exact values being summed 5. **Final ADA Conversion**: Check the division by 1,000,000 +#### Enhanced Debug Information +6. **Raw Rewards Sum**: Total of all rewards without filtering (`window.rewardsDebugInfo.rawRewardsSum`) +7. **Raw Rewards Sum (ADA)**: Raw sum converted to ADA (`window.rewardsDebugInfo.rawRewardsSumADA`) +8. **Excluded Rewards Sum**: Sum of rewards excluded by 2-epoch offset (`window.rewardsDebugInfo.excludedRewardsSum`) +9. **Excluded Rewards Sum (ADA)**: Excluded sum converted to ADA (`window.rewardsDebugInfo.excludedRewardsSumADA`) + #### Key Debug Points - **Current Epoch**: What epoch the calculation is based on - **Epochs Excluded**: Which epochs are being filtered out (LAST_STABLE_EPOCH = 2) - **Rewards Array**: The exact string values being passed to BigNumber.sum - **Precision**: Check if any precision is lost during string conversion - **Type Consistency**: Ensure all values are the expected types +- **Raw vs Filtered**: Compare `rawRewardsSumADA` vs displayed Total Rewards +- **Excluded Amount**: Verify that `excludedRewardsSumADA` explains the difference +- **2-Epoch Offset**: Confirm that excluded rewards match the 2-epoch protocol requirement ### 5. Expected Behavior - **Total Rewards**: Should match explorer values (accounting for 2-epoch delay) diff --git a/apps/browser-extension-wallet/debug-extractor.js b/apps/browser-extension-wallet/debug-extractor.js index fd18dab8bb..44fda0c8f8 100644 --- a/apps/browser-extension-wallet/debug-extractor.js +++ b/apps/browser-extension-wallet/debug-extractor.js @@ -1,6 +1,6 @@ // Lace Wallet Debug Information Extractor // Run this script in the browser console on the staking page -// +// // NOTE: Debugging works in both popup AND browser tab view! // The browser view has been configured to use the local Staking component // with debugging enabled instead of the @lace/staking package component. @@ -14,10 +14,14 @@ if (window.rewardsDebugInfo) { console.log('\nšŸ“Š TOTAL REWARDS DEBUG INFO:'); console.log('Current Epoch:', window.rewardsDebugInfo.currentEpoch); console.log('Raw History Length:', window.rewardsDebugInfo.rawRewardsHistoryLength); - console.log('Confirmed Length:', window.rewardsDebugInfo.confirmedRewardsLength); + console.log('Withdrawable Length:', window.rewardsDebugInfo.withdrawableRewardsLength); console.log('Rewards Array:', window.rewardsDebugInfo.rewardsArray); console.log('Total BigNumber:', window.rewardsDebugInfo.totalBigNumber); console.log('Total ADA:', window.rewardsDebugInfo.totalADA); + console.log('Raw Rewards Sum (lovelace):', window.rewardsDebugInfo.rawRewardsSum); + console.log('Raw Rewards Sum (ADA):', window.rewardsDebugInfo.rawRewardsSumADA); + console.log('Excluded Rewards Sum (lovelace):', window.rewardsDebugInfo.excludedRewardsSum); + console.log('Excluded Rewards Sum (ADA):', window.rewardsDebugInfo.excludedRewardsSumADA); } else { console.log('āŒ No rewards debug info available'); } @@ -45,15 +49,16 @@ const exportDebugData = () => { rewardsDebugInfo: window.rewardsDebugInfo || null, lastRewardDebugInfo: window.lastRewardDebugInfo || null }; - + console.log('\nšŸ“‹ EXPORTABLE DEBUG DATA:'); console.log(JSON.stringify(debugData, null, 2)); - + // Copy to clipboard if available if (navigator.clipboard) { - navigator.clipboard.writeText(JSON.stringify(debugData, null, 2)) + navigator.clipboard + .writeText(JSON.stringify(debugData, null, 2)) .then(() => console.log('āœ… Debug data copied to clipboard')) - .catch(err => console.log('āŒ Failed to copy to clipboard:', err)); + .catch((err) => console.log('āŒ Failed to copy to clipboard:', err)); } }; diff --git a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts index 504dc02aab..b7590c2626 100644 --- a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts +++ b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts @@ -31,6 +31,16 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { logRewardsDebug('Total rewards history entries:', all?.length || 0); logRewardsDebug('Raw rewards history:', all); + // Calculate what the raw rewards_sum would be (sum of all rewards without filtering) + const rawRewardsSum = + all?.reduce((sum, reward) => sum + BigInt(reward.rewards.toString()), BigInt(0)) ?? BigInt(0); + const rawRewardsSumADA = Number(rawRewardsSum) / LOVELACE_TO_ADA; + logRewardsDebug('=== RAW REWARDS ANALYSIS ==='); + logRewardsDebug('Raw rewards_sum (all epochs):', rawRewardsSum.toString()); + // eslint-disable-next-line no-magic-numbers + logRewardsDebug('Raw rewards_sum in ADA:', rawRewardsSumADA.toFixed(6)); + logRewardsDebug('This should match Blockfrost rewards_sum field'); + // Rewards are calculated and added to the database immediately when they are distributed // However, they need 2 epochs to pass before they become available for withdrawal // This is a Cardano protocol requirement, not a database limitation @@ -48,6 +58,26 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { logRewardsDebug('Withdrawable rewards history (filtered):', withdrawableRewardHistory); logRewardsDebug('Number of withdrawable rewards:', withdrawableRewardHistory.length); + // Show which specific rewards are being excluded + const excludedRewards = all?.filter(({ epoch }) => epoch.valueOf() > lastWithdrawableEpoch) ?? []; + if (excludedRewards.length > 0) { + logRewardsDebug('=== EXCLUDED REWARDS (2-epoch offset) ==='); + logRewardsDebug('Number of excluded rewards:', excludedRewards.length); + const excludedSum = excludedRewards.reduce( + (sum, reward) => sum + BigInt(reward.rewards.toString()), + BigInt(0) + ); + const excludedSumADA = Number(excludedSum) / LOVELACE_TO_ADA; + logRewardsDebug('Excluded rewards sum in lovelace:', excludedSum.toString()); + // eslint-disable-next-line no-magic-numbers + logRewardsDebug('Excluded rewards sum in ADA:', excludedSumADA.toFixed(6)); + logRewardsDebug( + 'Excluded reward epochs:', + excludedRewards.map((r) => r.epoch.valueOf()) + ); + logRewardsDebug('This explains the difference between raw rewards_sum and displayed Total Rewards'); + } + if (withdrawableRewardHistory.length > 0) { logRewardsDebug('First withdrawable reward:', withdrawableRewardHistory[0]); logRewardsDebug( @@ -87,7 +117,18 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { withdrawableRewardsLength: withdrawableRewardHistory?.length || 0, rewardsArray: rewardStrings, totalBigNumber: total.toString(), - totalADA: total.dividedBy(LOVELACE_TO_ADA).toString() + totalADA: total.dividedBy(LOVELACE_TO_ADA).toString(), + rawRewardsSum: rawRewardsSum.toString(), + // eslint-disable-next-line no-magic-numbers + rawRewardsSumADA: rawRewardsSumADA.toFixed(6), + excludedRewardsSum: excludedRewards + .reduce((sum, reward) => sum + BigInt(reward.rewards.toString()), BigInt(0)) + .toString(), + // eslint-disable-next-line no-magic-numbers + excludedRewardsSumADA: ( + Number(excludedRewards.reduce((sum, reward) => sum + BigInt(reward.rewards.toString()), BigInt(0))) / + LOVELACE_TO_ADA + ).toFixed(6) // eslint-disable-line no-magic-numbers }; // Store debug info in a way that won't be stripped by minification @@ -130,6 +171,17 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { logRewardsDebug('Last reward:', lastReward); logRewardsDebug('Last reward type:', typeof lastReward); + + // Final summary comparison + logRewardsDebug('=== FINAL COMPARISON ==='); + // eslint-disable-next-line no-magic-numbers + logRewardsDebug('Raw rewards_sum (all epochs):', `${rawRewardsSumADA.toFixed(6)} ADA`); + // eslint-disable-next-line no-magic-numbers + logRewardsDebug('Displayed Total Rewards:', `${totalRewards.toFixed(6)} ADA`); + const difference = rawRewardsSumADA - totalRewards; + // eslint-disable-next-line no-magic-numbers + logRewardsDebug('Difference:', `${difference.toFixed(6)} ADA`); + logRewardsDebug('This difference should match the excluded rewards sum above'); logRewardsDebug('=== REWARDS CALCULATION END ==='); return { From c3f905b19e6187a7dbdae356f428c9658b844667 Mon Sep 17 00:00:00 2001 From: "rhys.bartelswaller@iohk.io" Date: Sun, 31 Aug 2025 16:51:05 +0100 Subject: [PATCH 4/4] feat: implement automated Blockfrost comparison hook for real-time data integrity validation - Import and use existing Blockfrost configuration from config() - Automatically use project_id from config if available - Add detailed logging for config usage and API key status - Integrate hook into Staking component for automated comparison - Support all network configurations (Mainnet, Preprod, Preview) --- .../DEBUG_INSTRUCTIONS.md | 32 ++- .../debug-extractor.js | 26 ++- .../src/hooks/useBlockfrostComparison.ts | 201 ++++++++++++++++++ .../src/hooks/useStakingRewards.ts | 3 + .../features/staking/components/Staking.tsx | 16 +- 5 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 apps/browser-extension-wallet/src/hooks/useBlockfrostComparison.ts diff --git a/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md index e6c7f531ad..95c92e7e94 100644 --- a/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md +++ b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md @@ -46,6 +46,12 @@ For comprehensive debug data extraction, run the provided script in the console: 8. **Excluded Rewards Sum**: Sum of rewards excluded by 2-epoch offset (`window.rewardsDebugInfo.excludedRewardsSum`) 9. **Excluded Rewards Sum (ADA)**: Excluded sum converted to ADA (`window.rewardsDebugInfo.excludedRewardsSumADA`) +#### Automated Blockfrost Comparison +10. **Real-time API Calls**: Automatic comparison with Blockfrost's `accounts/stake_address` endpoint +11. **Data Integrity Check**: Direct comparison between Lace's calculated sum and Blockfrost's `rewards_sum` +12. **Stake Address Detection**: Automatically gets stake address from wallet's reward accounts +13. **Instant Validation**: Detects data discrepancies in real-time during rewards calculation + #### Key Debug Points - **Current Epoch**: What epoch the calculation is based on - **Epochs Excluded**: Which epochs are being filtered out (LAST_STABLE_EPOCH = 2) @@ -55,6 +61,8 @@ For comprehensive debug data extraction, run the provided script in the console: - **Raw vs Filtered**: Compare `rawRewardsSumADA` vs displayed Total Rewards - **Excluded Amount**: Verify that `excludedRewardsSumADA` explains the difference - **2-Epoch Offset**: Confirm that excluded rewards match the 2-epoch protocol requirement +- **Blockfrost Comparison**: Check if Lace's calculated sum matches Blockfrost's `rewards_sum` +- **Data Integrity**: Verify that the discrepancy isn't due to missing reward records ### 5. Expected Behavior - **Total Rewards**: Should match explorer values (accounting for 2-epoch delay) @@ -90,18 +98,38 @@ When reporting discrepancies, include: - **Browser View Fix**: Temporarily disabled multi-delegation to ensure local Staking component is used ### File Locations -- **Hook**: `src/hooks/useStakingRewards.ts` +- **Rewards Hook**: `src/hooks/useStakingRewards.ts` +- **Blockfrost Comparison Hook**: `src/hooks/useBlockfrostComparison.ts` - **Browser View**: `src/views/browser-view/features/staking/components/Staking.tsx` - **Display**: `src/views/browser-view/features/staking/components/StakingInfo/StakingInfo.tsx` - **Browser View Fix**: `src/hooks/useMultiDelegationEnabled.ts` (temporarily returns false) ### Accessing Debug Information - **Console Logs**: Essential rewards calculation details without noise -- **Window Objects**: Access debug data via `window.rewardsDebugInfo` and `window.lastRewardDebugInfo` +- **Window Objects**: Access debug data via `window.rewardsDebugInfo`, `window.lastRewardDebugInfo`, and `window.blockfrostComparison` - **Debug Script**: Use `debug-extractor.js` for comprehensive data extraction - **Clean Output**: Focused on rewards calculation details without cluttering the console - **Both Views**: Debugging now works in popup AND browser tab view +### Using the Blockfrost Comparison Hook +To integrate automated Blockfrost comparison in your components: + +```typescript +import { useBlockfrostComparison } from '@src/hooks/useBlockfrostComparison'; + +const MyComponent = () => { + const { totalRewards } = useStakingRewards(); + const { comparisonData, isLoading, stakeAddress } = useBlockfrostComparison( + totalRewards.toString(), + Number(totalRewards) + ); + + // comparisonData will contain the full comparison results + // isLoading shows when the API call is in progress + // stakeAddress shows the detected stake address +}; +``` + ### Important Notes - **Multi-Delegation Disabled**: For debugging purposes, the browser view has been configured to use the local Staking component instead of the `@lace/staking` package component - **Temporary Change**: This change is in `useMultiDelegationEnabled.ts` and should be reverted after debugging is complete diff --git a/apps/browser-extension-wallet/debug-extractor.js b/apps/browser-extension-wallet/debug-extractor.js index 44fda0c8f8..3ce1a761de 100644 --- a/apps/browser-extension-wallet/debug-extractor.js +++ b/apps/browser-extension-wallet/debug-extractor.js @@ -37,6 +37,29 @@ if (window.lastRewardDebugInfo) { console.log('āŒ No last reward debug info available'); } +// Extract Blockfrost comparison data +if (window.blockfrostComparison) { + console.log('\n🌐 BLOCKFROST COMPARISON:'); + console.log('Status:', window.blockfrostComparison.status); + console.log('Stake Address:', window.blockfrostComparison.stakeAddress); + console.log('Blockfrost rewards_sum (lovelace):', window.blockfrostComparison.blockfrostRewardsSum); + console.log('Blockfrost rewards_sum (ADA):', window.blockfrostComparison.blockfrostRewardsSumADA); + console.log('Lace calculated rewards_sum (lovelace):', window.blockfrostComparison.laceRewardsSum); + console.log('Lace calculated rewards_sum (ADA):', window.blockfrostComparison.laceRewardsSumADA); + console.log('Difference:', window.blockfrostComparison.difference, 'ADA'); + console.log('Blockfrost controlled_amount (lovelace):', window.blockfrostComparison.blockfrostControlledAmount); + console.log('Blockfrost controlled_amount (ADA):', window.blockfrostComparison.blockfrostControlledAmountADA); + console.log('Timestamp:', window.blockfrostComparison.timestamp); + + if (window.blockfrostComparison.error) { + console.log('āŒ Error:', window.blockfrostComparison.error); + } +} else { + console.log('\n🌐 BLOCKFROST COMPARISON:'); + console.log('No comparison data available yet'); + console.log('Check console logs for Blockfrost API calls'); +} + // Check for any console errors or warnings console.log('\nāš ļø CONSOLE STATUS:'); console.log('Console logging enabled:', typeof console.log === 'function'); @@ -47,7 +70,8 @@ const exportDebugData = () => { const debugData = { timestamp: new Date().toISOString(), rewardsDebugInfo: window.rewardsDebugInfo || null, - lastRewardDebugInfo: window.lastRewardDebugInfo || null + lastRewardDebugInfo: window.lastRewardDebugInfo || null, + blockfrostComparison: window.blockfrostComparison || null }; console.log('\nšŸ“‹ EXPORTABLE DEBUG DATA:'); diff --git a/apps/browser-extension-wallet/src/hooks/useBlockfrostComparison.ts b/apps/browser-extension-wallet/src/hooks/useBlockfrostComparison.ts new file mode 100644 index 0000000000..38f5de5a9e --- /dev/null +++ b/apps/browser-extension-wallet/src/hooks/useBlockfrostComparison.ts @@ -0,0 +1,201 @@ +import { useEffect, useState } from 'react'; +import { useObservable, logger } from '@lace/common'; +import { useWalletStore } from '@src/stores'; +import { config } from '@src/config'; + +interface BlockfrostComparisonData { + stakeAddress: string; + blockfrostRewardsSum: string; + blockfrostRewardsSumADA: string; + laceRewardsSum: string; + laceRewardsSumADA: string; + difference: string; + blockfrostControlledAmount: string; + blockfrostControlledAmountADA: string; + timestamp: string; + status: 'idle' | 'loading' | 'success' | 'error'; + error?: string; +} + +const LOVELACE_TO_ADA = 1_000_000; +const DECIMAL_PLACES = 6; +const PERFECT_MATCH_THRESHOLD = 0.000_001; +const MINOR_DIFFERENCE_THRESHOLD = 0.001; +const HTTP_FORBIDDEN = 403; + +export const useBlockfrostComparison = ( + laceRewardsSum: string, + laceRewardsSumADA: number +): { + comparisonData: BlockfrostComparisonData | undefined; + isLoading: boolean; + stakeAddress: string | undefined; +} => { + const { inMemoryWallet } = useWalletStore(); + const rewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); + const [comparisonData, setComparisonData] = useState(); + const [isLoading, setIsLoading] = useState(false); + + // eslint-disable-next-line sonarjs/cognitive-complexity, max-statements + useEffect(() => { + // eslint-disable-next-line max-statements + const fetchBlockfrostComparison = async () => { + try { + // Get the stake address from the wallet's reward accounts + const stakeAddress = rewardAccounts?.[0]?.address; + if (!stakeAddress) { + logger.info('šŸ” [BLOCKFROST_DEBUG] No stake address available for comparison'); + return; + } + + setIsLoading(true); + logger.info('šŸ” [BLOCKFROST_DEBUG] Fetching Blockfrost data for stake address:', stakeAddress); + + // Use Blockfrost endpoint from config (supports all networks) + const blockfrostConfig = config().BLOCKFROST_CONFIGS.Mainnet; + const blockfrostUrl = `${blockfrostConfig.baseUrl}/api/v0/accounts/${stakeAddress}`; + + logger.info('šŸ” [BLOCKFROST_DEBUG] Using Blockfrost config:', { + baseUrl: blockfrostConfig.baseUrl, + hasProjectId: !!blockfrostConfig.projectId + }); + + // Use the project ID from config if available + const headers: Record = { + Accept: 'application/json' + }; + + if (blockfrostConfig.projectId) { + headers['project-id'] = blockfrostConfig.projectId; + logger.info('šŸ” [BLOCKFROST_DEBUG] Using Blockfrost project ID from config'); + } else { + logger.info('šŸ” [BLOCKFROST_DEBUG] No Blockfrost project ID in config - API calls may be rate-limited'); + } + + const response = await fetch(blockfrostUrl, { headers }); + + if (response.ok) { + const blockfrostData = await response.json(); + logger.info('šŸ” [BLOCKFROST_DEBUG] === BLOCKFROST COMPARISON ==='); + logger.info('šŸ” [BLOCKFROST_DEBUG] Blockfrost rewards_sum:', blockfrostData.rewards_sum); + logger.info( + 'šŸ” [BLOCKFROST_DEBUG] Blockfrost rewards_sum in ADA:', + `${(Number(blockfrostData.rewards_sum) / LOVELACE_TO_ADA).toFixed(DECIMAL_PLACES)}` + ); + logger.info('šŸ” [BLOCKFROST_DEBUG] Blockfrost controlled_amount:', blockfrostData.controlled_amount); + logger.info( + 'šŸ” [BLOCKFROST_DEBUG] Blockfrost controlled_amount in ADA:', + `${(Number(blockfrostData.controlled_amount) / LOVELACE_TO_ADA).toFixed(DECIMAL_PLACES)}` + ); + + // Compare Lace vs Blockfrost + const blockfrostRewardsADA = Number(blockfrostData.rewards_sum) / LOVELACE_TO_ADA; + const difference = Math.abs(blockfrostRewardsADA - laceRewardsSumADA); + + logger.info('šŸ” [BLOCKFROST_DEBUG] === COMPARISON RESULTS ==='); + logger.info( + 'šŸ” [BLOCKFROST_DEBUG] Lace calculated rewards_sum:', + `${laceRewardsSumADA.toFixed(DECIMAL_PLACES)} ADA` + ); + logger.info( + 'šŸ” [BLOCKFROST_DEBUG] Blockfrost rewards_sum:', + `${blockfrostRewardsADA.toFixed(DECIMAL_PLACES)} ADA` + ); + logger.info('šŸ” [BLOCKFROST_DEBUG] Difference:', `${difference.toFixed(DECIMAL_PLACES)} ADA`); + + if (difference < PERFECT_MATCH_THRESHOLD) { + logger.info('šŸ” [BLOCKFROST_DEBUG] āœ… PERFECT MATCH: Lace and Blockfrost rewards_sum are identical'); + } else if (difference < MINOR_DIFFERENCE_THRESHOLD) { + logger.info('šŸ” [BLOCKFROST_DEBUG] āš ļø MINOR DIFFERENCE: Small discrepancy (possibly rounding)'); + } else { + logger.info('šŸ” [BLOCKFROST_DEBUG] āŒ SIGNIFICANT DIFFERENCE: Data integrity issue detected!'); + logger.info( + 'šŸ” [BLOCKFROST_DEBUG] This suggests Lace is missing reward records or there is an API data issue' + ); + } + + // Store comparison data for UI access + const blockfrostComparisonData: BlockfrostComparisonData = { + stakeAddress, + blockfrostRewardsSum: blockfrostData.rewards_sum, + blockfrostRewardsSumADA: blockfrostRewardsADA.toFixed(DECIMAL_PLACES), + laceRewardsSum, + laceRewardsSumADA: laceRewardsSumADA.toFixed(DECIMAL_PLACES), + difference: difference.toFixed(DECIMAL_PLACES), + blockfrostControlledAmount: blockfrostData.controlled_amount, + blockfrostControlledAmountADA: (Number(blockfrostData.controlled_amount) / LOVELACE_TO_ADA).toFixed( + DECIMAL_PLACES + ), + timestamp: new Date().toISOString(), + status: 'success' + }; + + setComparisonData(blockfrostComparisonData); + + // Store in global window for debugging + if (typeof window !== 'undefined') { + (window as unknown as Record).blockfrostComparison = blockfrostComparisonData; + } + } else { + logger.error( + 'šŸ” [BLOCKFROST_DEBUG] āŒ Failed to fetch Blockfrost data:', + response.status, + response.statusText + ); + if (response.status === HTTP_FORBIDDEN) { + logger.info( + 'šŸ” [BLOCKFROST_DEBUG] šŸ’” Tip: Blockfrost API key required. Check environment variables for BLOCKFROST_PROJECT_ID_MAINNET' + ); + } + + const errorData: BlockfrostComparisonData = { + stakeAddress: '', + blockfrostRewardsSum: '', + blockfrostRewardsSumADA: '', + laceRewardsSum, + laceRewardsSumADA: laceRewardsSumADA.toFixed(DECIMAL_PLACES), + difference: '', + blockfrostControlledAmount: '', + blockfrostControlledAmountADA: '', + timestamp: new Date().toISOString(), + status: 'error', + error: `HTTP ${response.status}: ${response.statusText}` + }; + + setComparisonData(errorData); + } + } catch (error) { + logger.error('šŸ” [BLOCKFROST_DEBUG] āŒ Error fetching Blockfrost data:', error); + + const errorData: BlockfrostComparisonData = { + stakeAddress: '', + blockfrostRewardsSum: '', + blockfrostRewardsSumADA: '', + laceRewardsSum, + laceRewardsSumADA: laceRewardsSumADA.toFixed(DECIMAL_PLACES), + difference: '', + blockfrostControlledAmount: '', + blockfrostControlledAmountADA: '', + timestamp: new Date().toISOString(), + status: 'error', + error: error instanceof Error ? error.message : 'Unknown error' + }; + + setComparisonData(errorData); + } finally { + setIsLoading(false); + } + }; + + // Only fetch if we have both the stake address and Lace rewards data + if (rewardAccounts?.[0]?.address && laceRewardsSum && laceRewardsSumADA > 0) { + fetchBlockfrostComparison(); + } + }, [rewardAccounts, laceRewardsSum, laceRewardsSumADA]); + + return { + comparisonData, + isLoading, + stakeAddress: rewardAccounts?.[0]?.address + }; +}; diff --git a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts index b7590c2626..bbbccfbdbf 100644 --- a/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts +++ b/apps/browser-extension-wallet/src/hooks/useStakingRewards.ts @@ -41,6 +41,9 @@ export const useStakingRewards = (): UseStakingRewardsReturns => { logRewardsDebug('Raw rewards_sum in ADA:', rawRewardsSumADA.toFixed(6)); logRewardsDebug('This should match Blockfrost rewards_sum field'); + // Automated Blockfrost comparison will be triggered by useBlockfrostComparison hook + // The comparison data will be available in window.blockfrostComparison + // Rewards are calculated and added to the database immediately when they are distributed // However, they need 2 epochs to pass before they become available for withdrawal // This is a Cardano protocol requirement, not a database limitation diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx index 8ed3cc0235..9327ccaf3f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/Staking.tsx @@ -11,6 +11,7 @@ import { StakeFundsBanner } from './StakeFundsBanner'; import { FundWalletBanner } from '@src/views/browser-view/components'; import { useWalletStore } from '@stores'; import { useBalances, useDelegationDetails, useFetchCoinPrice, useStakingRewards } from '@src/hooks'; +import { useBlockfrostComparison } from '@src/hooks/useBlockfrostComparison'; import { useDelegationStore } from '@src/features/delegation/stores'; import { walletBalanceTransformer } from '@src/api/transformers'; import { StakePoolDetails } from './StakePoolDetails'; @@ -20,7 +21,7 @@ import { StakingInfo } from './StakingInfo'; import { useStakePoolDetails } from '../store'; import { SectionTitle } from '@components/Layout/SectionTitle'; import { LACE_APP_ID } from '@src/utils/constants'; -import { useObservable } from '@lace/common'; +import { useObservable, logger } from '@lace/common'; import { fetchPoolsInfo } from '../utils'; import { Box } from '@input-output-hk/lace-ui-toolkit'; import { useExternalLinkOpener } from '@providers'; @@ -50,6 +51,19 @@ export const Staking = (): React.ReactElement => { const protocolParameters = useObservable(inMemoryWallet?.protocolParameters$); const delegationDetails = useDelegationDetails(); const { totalRewards, lastReward } = useStakingRewards(); + + // Automated Blockfrost comparison for data integrity validation + // This hook runs automatically to compare Lace vs Blockfrost data + const blockfrostComparison = useBlockfrostComparison(totalRewards.toString(), Number(totalRewards)); + + // Log comparison status for debugging (satisfies TypeScript) + if (blockfrostComparison.comparisonData) { + logger.info('šŸ” [STAKING_DEBUG] Blockfrost comparison available:', { + stakeAddress: blockfrostComparison.stakeAddress, + status: blockfrostComparison.comparisonData.status + }); + } + const openExternalLink = useExternalLinkOpener(); const { coinBalance: minAda } = walletBalanceTransformer(protocolParameters?.stakeKeyDeposit.toString());