feat: wallet-based transaction filtering with persistent caching#22
feat: wallet-based transaction filtering with persistent caching#22Jitmisra wants to merge 1 commit intoDjedAlliance:mainfrom
Conversation
Implements Issue DjedAlliance#6 requirements: - Filter transactions by merchant wallet address (receiver) - Persist merchant address on wallet connect - Persistent localStorage cache (indefinite, no expiry) - Track lastSyncedBlock for incremental updates - Incremental fetching (only new transactions after last sync) - Deduplication by transaction hash - SSR-safe localStorage access - Clear cache on wallet disconnect - Helpful UI messages when no transactions found
WalkthroughThe PR implements wallet-based transaction filtering and persistent caching. It removes mock data from dashboard pages, adds wallet connectivity checks, introduces localStorage-backed merchant-scoped caching with incremental fetching support, and refactors UI components to reflect wallet connection status and real transaction data. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Dashboard as Dashboard UI
participant WalletHook as useWallet Hook
participant TxnHook as useTransactions Hook
participant TxnService as TransactionService
participant Blockchain as Blockchain
participant Storage as localStorage
User->>Dashboard: Connect Wallet
Dashboard->>WalletHook: connectWallet()
WalletHook->>Storage: Store merchant address
WalletHook-->>Dashboard: isConnected=true, address
User->>Dashboard: Fetch Transactions
Dashboard->>TxnHook: fetchTransactions()
TxnHook->>Storage: Load cached transactions & lastSyncedBlock
alt Cached data exists
Storage-->>TxnHook: Return cached transactions
TxnHook-->>Dashboard: Display cached data
else First fetch or manual refresh
TxnHook->>TxnService: fetchStableCoinPurchases(merchantAddress, startFromBlock=null)
TxnService->>Blockchain: Query events (full range or MAX_BLOCK_RANGE windows)
Blockchain-->>TxnService: Transaction events for merchant
TxnService->>TxnService: Deduplicate & map events
TxnService-->>TxnHook: TransactionEvent[] with amounts & block info
TxnHook->>TxnHook: Deduplicate against cache, merge results
TxnHook->>Storage: Save merged transactions & lastSyncedBlock
TxnHook-->>Dashboard: Display updated transactions
end
User->>Dashboard: Fetch Fresh Data
Dashboard->>TxnHook: fetchFreshData()
TxnHook->>TxnService: fetchStableCoinPurchases(merchantAddress, startFromBlock=lastSyncedBlock)
TxnService->>Blockchain: Query events from lastSyncedBlock onwards
Blockchain-->>TxnService: New transaction events
TxnService-->>TxnHook: Incremental TransactionEvent[]
TxnHook->>TxnHook: Append new, deduplicate, update lastSyncedBlock
TxnHook->>Storage: Persist merged cache with new lastSyncedBlock
TxnHook-->>Dashboard: Display updated full transaction list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
team name bigbull |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (6)
dashboard/hooks/use-wallet.ts (2)
23-35: Edge case:isConnectedtrue butaddressundefined could cause stale state.When
isConnectedis true butaddressis not yet available (theelsebranch at line 30), the code restores from localStorage. However, if wagmi eventually provides a different address than what's stored (e.g., user switched accounts), the UI could briefly show stale data until the effect re-runs.Consider adding a safeguard to clear stale state when the address becomes available and differs from localStorage:
} else { // Restore from localStorage if connected but address not yet available const stored = getStoredMerchantAddress(); - if (stored) setWalletAddress(stored); + if (stored && !address) setWalletAddress(stored); }This is a minor edge case since the effect will re-run when
addresschanges.
41-45: localStorage cleared beforedisconnect()completes.If
disconnect()throws or fails, the localStorage entry will already be removed, potentially causing state inconsistency. Consider clearing localStorage after successful disconnect, or within a try-catch that handles partial cleanup.const disconnectWallet = () => { - // Clear merchant address on disconnect - localStorage.removeItem(MERCHANT_ADDRESS_KEY); disconnect(); + // Clear merchant address on disconnect + localStorage.removeItem(MERCHANT_ADDRESS_KEY); };Note: Since
disconnect()from wagmi typically doesn't throw synchronously, this is low risk but worth considering for defensive coding.dashboard/lib/transaction-service.ts (1)
16-17: Magic numbers for block constants.Consider documenting why
DEFAULT_START_BLOCK = 6000000was chosen (e.g., contract deployment block) and whyMAX_BLOCK_RANGE = 49999(likely RPC provider limit). A brief comment helps future maintainers:-const DEFAULT_START_BLOCK = BigInt(6000000); -const MAX_BLOCK_RANGE = BigInt(49999); +// Contract deployment block on Sepolia +const DEFAULT_START_BLOCK = BigInt(6000000); +// Max block range per RPC request (publicnode limit is 50k) +const MAX_BLOCK_RANGE = BigInt(49999);dashboard/app/transactions/page.tsx (2)
14-21: Consider extracting shared utilities.The
formatAddresshelper duplicates logic that could be useful elsewhere. Similarly,getRiskLevelthresholds (50, 100) are magic numbers that could benefit from named constants:const RISK_THRESHOLDS = { HIGH: 100, MEDIUM: 50 } as const; const getRiskLevel = (amount: string) => { const num = parseFloat(amount); if (num > RISK_THRESHOLDS.HIGH) return "high"; if (num > RISK_THRESHOLDS.MEDIUM) return "medium"; return "low"; };
68-70: Guard against nullwalletAddressin display.When
isConnectedis true butwalletAddressis still null (briefly during initialization),formatAddress(walletAddress || '')would display"0x...which isn't helpful. Consider a fallback:- {isConnected ? `Filtering by: ${formatAddress(walletAddress || '')}` : 'Connect wallet to filter transactions'} + {isConnected && walletAddress + ? `Filtering by: ${formatAddress(walletAddress)}` + : isConnected + ? 'Loading wallet address...' + : 'Connect wallet to filter transactions'}dashboard/hooks/use-transactions.ts (1)
70-78: Consider handling other storage errors.The
QuotaExceededErrorcheck usese.name, but some browsers throwDOMExceptionwith a different structure. A more defensive approach:try { localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); } catch (e) { - // Handle quota exceeded - if (e instanceof Error && e.name === 'QuotaExceededError') { + // Handle quota exceeded or other storage errors + if (e instanceof DOMException || (e instanceof Error && e.name === 'QuotaExceededError')) { localStorage.removeItem(CACHE_KEY); } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
dashboard/app/page.tsx(4 hunks)dashboard/app/transactions/page.tsx(2 hunks)dashboard/hooks/use-transactions.ts(1 hunks)dashboard/hooks/use-wallet.ts(3 hunks)dashboard/lib/transaction-service.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
dashboard/app/transactions/page.tsx (2)
dashboard/hooks/use-transactions.ts (1)
useTransactions(24-147)dashboard/hooks/use-wallet.ts (1)
useWallet(16-53)
dashboard/app/page.tsx (1)
dashboard/hooks/use-wallet.ts (1)
useWallet(16-53)
dashboard/lib/transaction-service.ts (1)
dashboard/lib/config.ts (1)
getCurrentContractAddress(75-77)
dashboard/hooks/use-transactions.ts (2)
dashboard/lib/transaction-service.ts (2)
TransactionEvent(6-14)transactionService(87-87)dashboard/hooks/use-wallet.ts (1)
getStoredMerchantAddress(11-14)
🪛 ast-grep (0.40.0)
dashboard/hooks/use-wallet.ts
[warning] 25-25: Sensitive information detected in localStorage. Storing sensitive data like emails, usernames, or personal information in localStorage exposes it to malicious scripts and XSS attacks. Use secure storage alternatives or avoid storing sensitive data client-side.
Context: localStorage.setItem(MERCHANT_ADDRESS_KEY, address)
Note: [CWE-312] Cleartext Storage of Sensitive Information [REFERENCES]
- https://cwe.mitre.org/data/definitions/312.html
- https://owasp.org/www-community/vulnerabilities/HTML5_Local_Storage_Security
(local-storage-sensitive-information)
🔇 Additional comments (7)
dashboard/hooks/use-wallet.ts (1)
8-14: SSR-safe helper looks good.The
typeof windowcheck properly guards against SSR issues. This pattern is correct for Next.js applications.dashboard/app/page.tsx (2)
23-24: Success rate calculation is simplified.The current logic returns 100% if any transactions exist, 0% otherwise. This assumes all fetched transactions are successful (which aligns with only fetching completed events from the blockchain). If the intent is to track actual success/failure rates in the future, consider adding a TODO comment.
74-89: Good UX guidance for users.The conditional notices for wallet connection and empty transactions provide clear guidance. The link to mordor.djed.one helps users understand how to create test transactions.
dashboard/app/transactions/page.tsx (1)
162-260: Well-structured table with proper loading/error/empty states.The table implementation handles all states correctly:
- Loading state with spinner indication
- Error display with message
- Empty state with contextual guidance based on connection/fetch status
- Click handler with modal for details
The use of
stopPropagationon the external link button correctly prevents row click from firing.dashboard/hooks/use-transactions.ts (3)
40-52: Robust cache validation with case-insensitive address comparison.Good implementation:
- Case-insensitive comparison handles checksum vs non-checksum addresses
- Stale cache is cleared when merchant changes
- Error handling removes corrupted cache
80-125: Stale closure risk infetchTransactions.The callback depends on
transactionsandlastSyncedBlockstate, which are correctly included in the dependency array. However, if multiple rapid calls occur, there could be race conditions wheretransactionshasn't updated yet.For critical production use, consider using a ref to track in-flight requests or using
useReducerfor atomic state updates. For this dashboard use case, the current implementation should be acceptable.
143-144: Public API exposes two fetch methods with different behaviors.The return object exposes:
fetchTransactions: () => fetchTransactions(false)- full fetchfetchFreshData- incremental fetchThis is a clean API design that hides the internal
incrementalflag from consumers. The wrapper functions ensure consistent behavior.
| { | ||
| label: "REVENUE GENERATED", | ||
| value: "T/A", | ||
| description: "Fetch transactions for data", | ||
| icon: "proccesor" as keyof typeof iconMap, | ||
| intent: "positive" as const, | ||
| direction: "up" as const, | ||
| }, |
There was a problem hiding this comment.
Typo: "T/A" should likely be "N/A".
The value "T/A" appears to be a typo for "N/A" (Not Available). This same typo appears in other stats as well (lines 29, 45).
- value: "T/A",
+ value: "N/A",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| label: "REVENUE GENERATED", | |
| value: "T/A", | |
| description: "Fetch transactions for data", | |
| icon: "proccesor" as keyof typeof iconMap, | |
| intent: "positive" as const, | |
| direction: "up" as const, | |
| }, | |
| { | |
| label: "REVENUE GENERATED", | |
| value: "N/A", | |
| description: "Fetch transactions for data", | |
| icon: "proccesor" as keyof typeof iconMap, | |
| intent: "positive" as const, | |
| direction: "up" as const, | |
| }, |
🤖 Prompt for AI Agents
In dashboard/app/page.tsx around lines 35 to 42 (and also check similar entries
at lines ~29 and ~45), the value string "T/A" is a typo and should be "N/A";
update the value fields for the affected stat objects to "N/A" (and scan the
file for any other occurrences of "T/A" in stats to replace them) so the
displayed notation correctly reads "N/A".
| const [selectedTransaction, setSelectedTransaction] = useState<any>(null) | ||
| const [isModalOpen, setIsModalOpen] = useState(false) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Avoid any type for selectedTransaction.
Using any loses type safety. Since selectedTransaction comes from transactions, use the proper type:
-const [selectedTransaction, setSelectedTransaction] = useState<any>(null)
+const [selectedTransaction, setSelectedTransaction] = useState<TransactionEvent | null>(null)You'll need to import TransactionEvent from the transaction service:
+import { TransactionEvent } from "@/lib/transaction-service"Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In dashboard/app/transactions/page.tsx around lines 26-27, selectedTransaction
is typed as any which loses type safety; replace the any with the proper
TransactionEvent | null type, import TransactionEvent from the transaction
service at the top of the file, and update the useState generic to
useState<TransactionEvent | null>(null) (and adjust any handlers/props that set
or read selectedTransaction to expect TransactionEvent or null accordingly).
| }; | ||
| }, [transactions, lastSyncedBlock, saveToCache]); | ||
|
|
||
| const fetchFreshData = useCallback(() => fetchTransactions(true), [fetchTransactions]); |
There was a problem hiding this comment.
Confusing naming: fetchFreshData performs incremental fetch, not full refresh.
fetchFreshData calls fetchTransactions(true) where true means incremental. The name "Fresh Data" suggests a full refresh, but it actually fetches only new blocks. Consider renaming for clarity:
-const fetchFreshData = useCallback(() => fetchTransactions(true), [fetchTransactions]);
+const fetchNewTransactions = useCallback(() => fetchTransactions(true), [fetchTransactions]);Or update the UI label from "Fetch Fresh Data" to "Fetch New Transactions" to match the incremental behavior.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In dashboard/hooks/use-transactions.ts at line 127, the identifier
fetchFreshData is misleading because it calls fetchTransactions(true) which
performs an incremental fetch; rename the variable to a clearer name such as
fetchNewTransactions or fetchIncrementalTransactions and update all references
(callback declaration, dependency arrays, and any exports) accordingly, and also
change the corresponding UI label from "Fetch Fresh Data" to "Fetch New
Transactions" (or similar) so the name and UI reflect incremental behavior while
keeping the useCallback dependency on fetchTransactions intact.
| const purchaseEvents = await this.publicClient.getLogs({ | ||
| address: getCurrentContractAddress() as `0x${string}`, | ||
| event: parseAbiItem('event BoughtStableCoins(address indexed buyer, address indexed receiver, uint256 amountSC, uint256 amountBC)'), | ||
| args: merchantAddress ? { | ||
| receiver: merchantAddress | ||
| }as any:undefined, | ||
| receiver: merchantAddress as `0x${string}` | ||
| } : undefined, | ||
| fromBlock, | ||
| toBlock | ||
| }); | ||
| allEvents = [...allEvents, ...purchaseEvents]; | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
viem getLogs decoded event args parseAbiItem
💡 Result:
Use parseAbiItem for the event when calling getLogs — viem will decode the event args into each Log's .args (use strict: true to guarantee .args is defined). Example:
import { publicClient, parseAbiItem } from 'viem'
const logs = await publicClient.getLogs({
address: '0xContractAddress',
event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
fromBlock: 16330000n,
toBlock: 16330050n,
strict: true // optional — only return logs that conform to the ABI so args is always present
})
for (const log of logs) {
console.log(log.args) // { from: '0x...', to: '0x...', value: 123n }
}
If you need to decode manually from topics/data, use decodeEventLog with the parsed ABI. [1][2][3]
Sources:
[1] viem getLogs docs.
[2] viem v1 getLogs (args & strict mode).
[3] viem decodeEventLog docs.
🏁 Script executed:
# First, let's find and examine the transaction-service.ts file
fd -t f "transaction-service.ts" | head -5Repository: DjedAlliance/StablePay-MerchantDashboard
Length of output: 120
🏁 Script executed:
# Read the transaction-service.ts file to see the full context around lines 53-61
cat -n dashboard/lib/transaction-service.ts | sed -n '40,75p'Repository: DjedAlliance/StablePay-MerchantDashboard
Length of output: 2047
Leverage viem's decoded event args to eliminate manual hex parsing.
viem's getLogs returns decoded args when parseAbiItem is used with strict typing. Replace the manual parsing on lines 67-69 with decoded values:
// Current: manual slicing and parsing of event.data
const rawData = event.data.slice(2);
const amountSCHex = '0x' + rawData.slice(0, 64);
const amountBCHex = '0x' + rawData.slice(64);
// Instead, use decoded args from viem:
// event.args.amountSC and event.args.amountBC are already bigints
// event.args.buyer and event.args.receiver are already checksummed addressesThis eliminates slicing errors and reduces risk by using viem's built-in decoding.
🤖 Prompt for AI Agents
In dashboard/lib/transaction-service.ts around lines 53 to 61 (and the
subsequent manual parsing at ~67-69), you are manually slicing event.data to
extract amountSC and amountBC; instead, use viem's decoded event args provided
on the returned log object when parseAbiItem is used with proper typing. Replace
the manual hex-slicing logic with direct reads from event.args.amountSC and
event.args.amountBC (they are bigints) and use event.args.buyer and
event.args.receiver for addresses; ensure any downstream code converts the
bigints to the expected form (string/number) as needed.
| return allEvents.map(event => { | ||
| const rawData = event.data.slice(2); | ||
| const amountSCHex = '0x' + rawData.slice(0, 64); | ||
| const amountBCHex = '0x' + rawData.slice(64); | ||
|
|
||
| return { | ||
| buyer: this.formatAddress(event.topics[1]), | ||
| receiver: this.formatAddress(event.topics[2]), | ||
| amountSC: (parseInt(amountSCHex, 16) / 1000000).toString(), // Convert to SC | ||
| amountBC: formatUnits(BigInt(amountBCHex), 18), // Convert to ETH | ||
| amountSC: (parseInt(amountSCHex, 16) / 1000000).toString(), | ||
| amountBC: formatUnits(BigInt(amountBCHex), 18), | ||
| blockNumber: event.blockNumber, | ||
| transactionHash: event.transactionHash | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Potential precision loss with parseInt for large amounts.
parseInt(amountSCHex, 16) converts to a JavaScript number, which loses precision for values exceeding Number.MAX_SAFE_INTEGER (~9 quadrillion). For blockchain amounts, use BigInt throughout:
- amountSC: (parseInt(amountSCHex, 16) / 1000000).toString(),
+ amountSC: (BigInt(amountSCHex) / BigInt(1000000)).toString(),Additionally, consider extracting 1000000 as a named constant (e.g., STABLECOIN_DECIMALS = 6) for clarity.
🤖 Prompt for AI Agents
In dashboard/lib/transaction-service.ts around lines 66 to 79, the code uses
parseInt(amountSCHex, 16) which converts large token amounts to a JS Number and
can lose precision; replace that with BigInt(amountSCHex) and format the
stablecoin value using decimals (e.g., use a helper like
formatUnits(BigInt(amountSCHex), STABLECOIN_DECIMALS) or compute a string by
dividing BigInt and inserting a decimal), and extract 1000000 into a named
constant STABLECOIN_DECIMALS = 6 at the top of the file; update the returned
amountSC to use the BigInt-based formatting to preserve precision.
PR Title
PR Description
🎯 Fixes #6
Summary
Implements wallet-based transaction filtering and persistent caching to improve dashboard performance and reduce unnecessary blockchain queries.
🏗️ Architecture
✨ Key Features
1. Transaction Filtering
TransactionServicefor blockchain-level filtering2. Persistent Caching
3. Incremental Fetching
lastSyncedBlockfor each merchant4. SSR Safety
getStoredMerchantAddress()checks for browser environment📁 Files Changed
hooks/use-wallet.tshooks/use-transactions.tslib/transaction-service.tsstartFromBlockparameterapp/transactions/page.tsxapp/page.tsxTotal: 5 files, ~350 lines of focused changes
🖥️ UI Improvements
✅ Acceptance Criteria
lastSyncedBlock🧪 Testing Checklist
🔄 Cache Flow
📊 Impact
Monosnap.screencast.2025-12-14.02-41-23.mp4
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.