Skip to content

Commit 7d5f560

Browse files
Fix Feed Filter and Wallet Screen Performance Issues (#138)
* Fix wallet transaction fetch error by validating inkey before API calls - Add null check for wallet inkey in WalletTransactionLog component - Add conditional transaction fetch in WalletAllowanceComponent - Provide clear error messages when wallet not found - Prevent 'Failed to fetch' errors on wallet screens Fixes #135 Co-authored-by: akash2017sky <akash2017sky@users.noreply.github.com> * Updated the Allowance and Private wallet feedlist on the wallet screen * Modified the feedlist to individual wallet fetch * Fixed the Feed filtering issue * Fixed the broken wllet feed screen * Code review comments fixed * Code review from claude. Error handling, variable names and Module-Level State in Service File * Fixed the final code review changes. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: akash2017sky <akash2017sky@users.noreply.github.com>
1 parent 5c0fd71 commit 7d5f560

File tree

5 files changed

+385
-239
lines changed

5 files changed

+385
-239
lines changed

tabs/src/components/FeedList.tsx

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ interface ZapTransaction {
2020
const ITEMS_PER_PAGE = 10; // Items per page
2121
const MAX_RECORDS = 100; // Maximum records to display
2222

23+
// Helper function to parse transaction timestamp (handles both Unix seconds and ISO strings)
24+
const parseTransactionTime = (timestamp: number | string): Date | null => {
25+
if (typeof timestamp === 'number') {
26+
return new Date(timestamp * 1000);
27+
}
28+
if (typeof timestamp === 'string') {
29+
const date = new Date(timestamp);
30+
if (isNaN(date.getTime())) {
31+
console.warn(`Invalid timestamp: ${timestamp}`);
32+
return null;
33+
}
34+
return date;
35+
}
36+
return null;
37+
};
38+
2339
// Wallet type identifiers - these match the exact naming convention used by the backend
2440
// Backend creates wallets with names 'Allowance' and 'Private' (see functions/sendZap/index.ts)
2541
// NOTE: If wallet naming conventions change on the backend, these must be updated
@@ -74,16 +90,18 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
7490
return;
7591
}
7692

77-
// Step 2: Get wallets for each user
78-
const allWalletsData: { userId: string; wallets: Wallet[] }[] = [];
93+
// Step 2: Parallelize wallet fetches for all users
94+
const walletPromises = fetchedUsers.map(async (user) => {
95+
try {
96+
const userWallets = await getUserWallets(adminKey, user.id);
97+
return { userId: user.id, wallets: userWallets || [] };
98+
} catch (err) {
99+
// Log error but continue - don't fail entire feed for one user
100+
return { userId: user.id, wallets: [] };
101+
}
102+
});
103+
const allWalletsData = await Promise.all(walletPromises);
79104

80-
for (const user of fetchedUsers) {
81-
const userWallets = await getUserWallets(adminKey, user.id);
82-
allWalletsData.push({
83-
userId: user.id,
84-
wallets: userWallets || []
85-
});
86-
}
87105
// Step 3: Get payments from both Allowance and Private wallets
88106
// We need both to match sender (Allowance) with receiver (Private)
89107
const allowanceWalletIds = new Set<string>();
@@ -92,7 +110,7 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
92110
let failedWalletCount = 0;
93111

94112
for (const userData of allWalletsData) {
95-
// Filter to Allowance and Private wallets
113+
// Filter to Allowance and Private wallets using exact match
96114
const relevantWallets = userData.wallets.filter(wallet =>
97115
isAllowanceWallet(wallet.name) || isPrivateWallet(wallet.name)
98116
);
@@ -296,11 +314,25 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
296314
return 0;
297315
});
298316

317+
// Apply timestamp filter (7/30/60 days) - filter transactions by time
318+
// timestamp prop is in Unix seconds (e.g., 7 days ago)
319+
// transaction.time can be either a number (Unix seconds) or an ISO date string
320+
const filteredZaps = timestamp && timestamp > 0
321+
? sortedZaps.filter(zap => {
322+
const parsedDate = parseTransactionTime(zap.transaction.time);
323+
if (!parsedDate) {
324+
return false; // Exclude transactions with invalid/unknown time format
325+
}
326+
const txTimeSeconds = Math.floor(parsedDate.getTime() / 1000);
327+
return txTimeSeconds >= timestamp;
328+
})
329+
: sortedZaps;
330+
299331
// Calculate pagination variables
300-
const totalPages = Math.max(1, Math.ceil(sortedZaps.length / ITEMS_PER_PAGE));
332+
const totalPages = Math.max(1, Math.ceil(filteredZaps.length / ITEMS_PER_PAGE));
301333
const indexOfLastItem = currentPage * ITEMS_PER_PAGE;
302334
const indexOfFirstItem = indexOfLastItem - ITEMS_PER_PAGE;
303-
const currentItems = sortedZaps.slice(indexOfFirstItem, indexOfLastItem);
335+
const currentItems = filteredZaps.slice(indexOfFirstItem, indexOfLastItem);
304336

305337
const nextPage = () => setCurrentPage(prev => Math.min(prev + 1, totalPages));
306338
const prevPage = () => setCurrentPage(prev => Math.max(prev - 1, 1));
@@ -359,15 +391,9 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
359391
<div className={styles.personDetails}>
360392
<div className={styles.userName}>
361393
{(() => {
362-
const timestamp = zap.transaction.time;
363-
// Try to parse as ISO string first, then Unix timestamp
364-
let date = new Date(timestamp);
365-
if (isNaN(date.getTime()) && typeof timestamp === 'number') {
366-
// Try as Unix timestamp (seconds)
367-
date = new Date(timestamp * 1000);
368-
}
369-
if (isNaN(date.getTime())) {
370-
return `Invalid: ${timestamp}`;
394+
const date = parseTransactionTime(zap.transaction.time);
395+
if (!date) {
396+
return `Invalid: ${zap.transaction.time}`;
371397
}
372398
// UK format: DD/MM/YYYY HH:MM (24-hour)
373399
return `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB', {
@@ -421,7 +447,7 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
421447
) : (
422448
<div>No data available</div>
423449
)}
424-
{sortedZaps.length > ITEMS_PER_PAGE && (
450+
{filteredZaps.length > ITEMS_PER_PAGE && (
425451
<div className={styles.pagination}>
426452
<button
427453
onClick={firstPage}
@@ -454,9 +480,9 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
454480
>
455481
&#187; {/* Double right arrow */}
456482
</button>
457-
</div>
483+
</div>
458484
)}
459485
</div>
460486
);
461487
};
462-
export default FeedList;
488+
export default FeedList;

tabs/src/components/WalletAllowanceComponent.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import SendZapsPopup from './SendZapsPopup';
1010

1111
const adminKey = process.env.REACT_APP_LNBITS_ADMINKEY as string;
1212

13+
// Time constants
14+
const SECONDS_PER_DAY = 86400;
15+
const MS_PER_SECOND = 1000;
16+
const TRANSACTION_HISTORY_DAYS = 30;
17+
1318
interface AllowanceCardProps {
1419
// Define the props here if there are any, for example:
1520
// someProp: string;
@@ -62,18 +67,22 @@ const WalletAllowanceCard: React.FC<AllowanceCardProps> = () => {
6267
setAllowance(null);
6368
}
6469

65-
const sevenDaysAgo = Date.now() / 1000 - 30 * 24 * 60 * 60;
66-
const encodedExtra = {};
67-
const transaction = await getWalletTransactionsSince(
68-
allowanceWallet.inkey,
69-
sevenDaysAgo,
70-
encodedExtra
71-
);
72-
73-
const spent = transaction
74-
.filter(transaction => transaction.amount < 0)
75-
.reduce((total, transaction) => total + Math.abs(transaction.amount), 0) / 1000;
76-
setSpentSats(spent);
70+
// Check if inkey exists before fetching transactions
71+
if (allowanceWallet.inkey) {
72+
const transactionHistoryStart = Date.now() / MS_PER_SECOND - TRANSACTION_HISTORY_DAYS * SECONDS_PER_DAY;
73+
const transaction = await getWalletTransactionsSince(
74+
allowanceWallet.inkey,
75+
transactionHistoryStart,
76+
{}
77+
);
78+
79+
const spent = transaction
80+
.filter(t => t.amount < 0)
81+
.reduce((total, t) => total + Math.abs(t.amount), 0) / MS_PER_SECOND;
82+
setSpentSats(spent);
83+
} else {
84+
setSpentSats(0);
85+
}
7786
}
7887
}
7988
}

tabs/src/components/WalletTransactionLog.tsx

Lines changed: 51 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ interface WalletTransactionLogProps {
1919

2020
const adminKey = process.env.REACT_APP_LNBITS_ADMINKEY as string;
2121

22+
// Time constants
23+
const SECONDS_PER_DAY = 86400;
24+
const MS_PER_SECOND = 1000;
25+
const TRANSACTION_HISTORY_DAYS = 30;
26+
2227
const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
2328
activeTab,
2429
activeWallet,
2530
}) => {
2631
const [allTransactions, setAllTransactions] = useState<Transaction[]>([]); // Cache all transactions
2732
const [displayedTransactions, setDisplayedTransactions] = useState<Transaction[]>([]); // Filtered transactions to display
28-
const [users, setUsers] = useState<User[]>([]);
2933
const [loading, setLoading] = useState(true);
3034
const [error, setError] = useState<string | null>(null);
3135
const [currentWallet, setCurrentWallet] = useState<string | undefined>(undefined); // Track which wallet data is cached for
@@ -34,11 +38,10 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
3438

3539
// Effect to fetch data when wallet changes
3640
useEffect(() => {
37-
// Calculate the timestamp for 30 days ago
38-
const sevenDaysAgo = Date.now() / 1000 - 30 * 24 * 60 * 60;
41+
// Calculate the timestamp for transaction history period
42+
const transactionHistoryStart = Date.now() / MS_PER_SECOND - TRANSACTION_HISTORY_DAYS * SECONDS_PER_DAY;
3943

40-
// Use the provided timestamp or default to 7 days ago
41-
const paymentsSinceTimestamp = sevenDaysAgo;
44+
const paymentsSinceTimestamp = transactionHistoryStart;
4245

4346
const account = accounts[0];
4447

@@ -51,9 +54,6 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
5154
try {
5255
// First, fetch all users
5356
const allUsers = await getUsers(adminKey, {});
54-
if (allUsers) {
55-
setUsers(allUsers);
56-
}
5757

5858
const currentUserLNbitDetails = await getUsers(adminKey, {
5959
aadObjectId: account.localAccountId,
@@ -65,50 +65,48 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
6565
// Fetch user's wallets
6666
const userWallets = await getUserWallets(adminKey, user.id);
6767

68-
// Create a wallet ID to user mapping for ALL users
68+
// Create a wallet ID to user mapping for ALL users - parallelized
6969
const walletToUserMap = new Map<string, User>();
70+
let allPayments: Transaction[] = [];
7071

71-
// For each user, fetch their wallets and create mapping
7272
if (allUsers) {
73-
for (const u of allUsers) {
74-
try {
75-
const wallets = await getUserWallets(adminKey, u.id);
76-
if (wallets) {
77-
wallets.forEach(wallet => {
78-
walletToUserMap.set(wallet.id, u);
79-
});
73+
// Parallelize wallet fetches for all users
74+
const walletResults = await Promise.all(
75+
allUsers.map(async (u) => {
76+
try {
77+
const wallets = await getUserWallets(adminKey, u.id);
78+
return { user: u, wallets: wallets || [] };
79+
} catch (err) {
80+
// Log error but continue - don't fail for one user
81+
return { user: u, wallets: [] };
8082
}
81-
} catch (err) {
82-
console.error(`Error fetching wallets for user ${u.id}:`, err);
83-
}
84-
}
85-
}
86-
87-
// Fetch ALL payments from ALL wallets to enable matching
88-
const allPayments: Transaction[] = [];
83+
})
84+
);
8985

90-
if (allUsers) {
91-
for (const u of allUsers) {
92-
try {
93-
const wallets = await getUserWallets(adminKey, u.id);
94-
if (wallets) {
95-
for (const wallet of wallets) {
96-
try {
97-
const payments = await getWalletTransactionsSince(
98-
wallet.inkey,
99-
paymentsSinceTimestamp,
100-
null,
101-
);
102-
allPayments.push(...payments);
103-
} catch (err) {
104-
console.error(`Error fetching payments for wallet ${wallet.id}:`, err);
105-
}
106-
}
86+
// Build wallet to user mapping
87+
walletResults.forEach(({ user, wallets }) => {
88+
wallets.forEach(wallet => {
89+
walletToUserMap.set(wallet.id, user);
90+
});
91+
});
92+
93+
// Collect all wallets and parallelize payment fetches
94+
const allWallets = walletResults.flatMap(r => r.wallets);
95+
const paymentResults = await Promise.all(
96+
allWallets.map(async (wallet) => {
97+
try {
98+
return await getWalletTransactionsSince(
99+
wallet.inkey,
100+
paymentsSinceTimestamp,
101+
null,
102+
);
103+
} catch (err) {
104+
// Log error but continue - don't fail for one wallet
105+
return [];
107106
}
108-
} catch (err) {
109-
console.error(`Error fetching wallets for user ${u.id}:`, err);
110-
}
111-
}
107+
})
108+
);
109+
allPayments = paymentResults.flat();
112110
}
113111

114112
// Create a map of all payments by checking_id for internal transfer matching
@@ -214,13 +212,19 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
214212
}
215213
};
216214

215+
// Early return if no accounts available yet
216+
if (!accounts || accounts.length === 0) {
217+
setLoading(false);
218+
return;
219+
}
220+
217221
// Only fetch if wallet changed or no data cached
218222
if (currentWallet !== activeWallet) {
219223
setAllTransactions([]);
220224
setDisplayedTransactions([]);
221225
fetchTransactions();
222226
}
223-
}, [activeWallet, accounts, currentWallet, users]);
227+
}, [activeWallet, accounts, currentWallet]);
224228

225229
// Separate effect to filter cached transactions when activeTab changes
226230
useEffect(() => {
@@ -255,14 +259,6 @@ const rewardsName = rewardNameContext.rewardName;
255259
return <div>{error}</div>;
256260
}
257261

258-
if (loading) {
259-
return <div>Loading...</div>;
260-
}
261-
262-
if (error) {
263-
return <div>{error}</div>;
264-
}
265-
266262

267263

268264
return (

0 commit comments

Comments
 (0)