diff --git a/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md new file mode 100644 index 0000000000..95c92e7e94 --- /dev/null +++ b/apps/browser-extension-wallet/DEBUG_INSTRUCTIONS.md @@ -0,0 +1,136 @@ +# 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. **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`) + +#### 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) +- **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 +- **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) +- **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 +- **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`, `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 +- **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..3ce1a761de --- /dev/null +++ b/apps/browser-extension-wallet/debug-extractor.js @@ -0,0 +1,93 @@ +// 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('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'); +} + +// 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'); +} + +// 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'); +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, + blockfrostComparison: window.blockfrostComparison || 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/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/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..bbbccfbdbf 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,191 @@ 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 - const lastNonVolatileEpoch = epochNo.valueOf() - LAST_STABLE_EPOCH; - const confirmedRewardHistory = all.filter(({ epoch }) => epoch.valueOf() <= lastNonVolatileEpoch); + logRewardsDebug('=== REWARDS CALCULATION START ==='); + logRewardsDebug('Current epoch:', epochNo?.valueOf()); + 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'); + + // 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 + const lastWithdrawableEpoch = epochNo.valueOf() - LAST_STABLE_EPOCH; + logRewardsDebug('Current epoch:', epochNo?.valueOf()); + logRewardsDebug('LAST_STABLE_EPOCH constant:', LAST_STABLE_EPOCH); + 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); + + // 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( + 'Last withdrawable reward:', + withdrawableRewardHistory[withdrawableRewardHistory.length - 1] + ); + + // Log each reward entry for detailed inspection + logRewardsDebug('=== DETAILED REWARDS BREAKDOWN ==='); + withdrawableRewardHistory.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 (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(); + + // Store debug info for UI display + const debugInfo = { + currentEpoch: epochNo?.valueOf(), + rawRewardsHistoryLength: all?.length || 0, + withdrawableRewardsLength: withdrawableRewardHistory?.length || 0, + rewardsArray: rewardStrings, + totalBigNumber: total.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 + if (typeof window !== 'undefined') { + (window as unknown as Record).rewardsDebugInfo = debugInfo; + } + + // Calculate last reward + const last = withdrawableRewardHistory[withdrawableRewardHistory.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(' - withdrawableRewardHistory.length:', withdrawableRewardHistory.length); + logRewardsDebug( + ' - rewards values:', + withdrawableRewardHistory.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); + + // 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 { - 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..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()); @@ -114,19 +128,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}