From 0b4582cf84d0c118cf86ff5bd85142496dc07988 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:02:10 +0000 Subject: [PATCH 1/8] 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 --- .../components/WalletAllowanceComponent.tsx | 30 +++++++++++-------- tabs/src/components/WalletTransactionLog.tsx | 6 ++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/tabs/src/components/WalletAllowanceComponent.tsx b/tabs/src/components/WalletAllowanceComponent.tsx index de42c7a80..4d6a9d6d1 100644 --- a/tabs/src/components/WalletAllowanceComponent.tsx +++ b/tabs/src/components/WalletAllowanceComponent.tsx @@ -60,18 +60,24 @@ const WalletAllowanceCard: React.FC = () => { setAllowance(null); } - const sevenDaysAgo = Date.now() / 1000 - 30 * 24 * 60 * 60; - const encodedExtra = {}; - const transaction = await getWalletTransactionsSince( - allowanceWallet.inkey, - sevenDaysAgo, - encodedExtra - ); - - const spent = transaction - .filter(transaction => transaction.amount < 0) - .reduce((total, transaction) => total + Math.abs(transaction.amount), 0) / 1000; - setSpentSats(spent); + // Check if inkey exists before fetching transactions + if (allowanceWallet.inkey) { + const sevenDaysAgo = Date.now() / 1000 - 30 * 24 * 60 * 60; + const encodedExtra = {}; + const transaction = await getWalletTransactionsSince( + allowanceWallet.inkey, + sevenDaysAgo, + encodedExtra + ); + + const spent = transaction + .filter(transaction => transaction.amount < 0) + .reduce((total, transaction) => total + Math.abs(transaction.amount), 0) / 1000; + setSpentSats(spent); + } else { + console.error('Allowance wallet inkey not found'); + setSpentSats(0); + } } } } diff --git a/tabs/src/components/WalletTransactionLog.tsx b/tabs/src/components/WalletTransactionLog.tsx index 3f12bcf77..2b010eb08 100644 --- a/tabs/src/components/WalletTransactionLog.tsx +++ b/tabs/src/components/WalletTransactionLog.tsx @@ -136,6 +136,12 @@ const WalletTransactionLog: React.FC = ({ console.error('No wallets found for user'); } + // Check if inkey exists before fetching transactions + if (!inkey) { + const walletType = activeWallet === 'Private' ? 'Private' : 'Allowance'; + throw new Error(`${walletType} wallet not found for user`); + } + const transactions = await getWalletTransactionsSince( inkey, paymentsSinceTimestamp, From 199edfdd0639387bd3296913b85aa6fd2f74c94f Mon Sep 17 00:00:00 2001 From: Akash Jadhav Date: Tue, 9 Dec 2025 12:05:57 +0000 Subject: [PATCH 2/8] Updated the Allowance and Private wallet feedlist on the wallet screen --- tabs/src/components/WalletTransactionLog.tsx | 251 ++++++++-------- tabs/src/services/lnbitsServiceLocal.ts | 286 +++++++++---------- 2 files changed, 256 insertions(+), 281 deletions(-) diff --git a/tabs/src/components/WalletTransactionLog.tsx b/tabs/src/components/WalletTransactionLog.tsx index 2b010eb08..34ea29e0a 100644 --- a/tabs/src/components/WalletTransactionLog.tsx +++ b/tabs/src/components/WalletTransactionLog.tsx @@ -35,10 +35,8 @@ const WalletTransactionLog: React.FC = ({ // Effect to fetch data when wallet changes useEffect(() => { // Calculate the timestamp for 30 days ago - const sevenDaysAgo = Date.now() / 1000 - 30 * 24 * 60 * 60; - - // Use the provided timestamp or default to 7 days ago - const paymentsSinceTimestamp = sevenDaysAgo; + const thirtyDaysAgo = Date.now() / 1000 - 30 * 24 * 60 * 60; + const paymentsSinceTimestamp = thirtyDaysAgo; const account = accounts[0]; @@ -46,175 +44,162 @@ const WalletTransactionLog: React.FC = ({ setLoading(true); setError(null); - let fetchedTransactions: Transaction[] = []; - try { - // First, fetch all users + // Fetch all users once (cached in service layer) const allUsers = await getUsers(adminKey, {}); if (allUsers) { setUsers(allUsers); } + // Get current user's LNbits details const currentUserLNbitDetails = await getUsers(adminKey, { aadObjectId: account.localAccountId, }); - if (currentUserLNbitDetails && currentUserLNbitDetails.length > 0) { - const user = currentUserLNbitDetails[0]; + if (!currentUserLNbitDetails || currentUserLNbitDetails.length === 0) { + throw new Error('User not found in LNbits'); + } - // Fetch user's wallets - const userWallets = await getUserWallets(adminKey, user.id); + const user = currentUserLNbitDetails[0]; - // Create a wallet ID to user mapping for ALL users - const walletToUserMap = new Map(); + // Fetch only the current user's wallets + const userWallets = await getUserWallets(adminKey, user.id); - // For each user, fetch their wallets and create mapping - if (allUsers) { - for (const u of allUsers) { - try { - const wallets = await getUserWallets(adminKey, u.id); - if (wallets) { - wallets.forEach(wallet => { - walletToUserMap.set(wallet.id, u); - }); - } - } catch (err) { - console.error(`Error fetching wallets for user ${u.id}:`, err); - } - } - } + if (!userWallets || userWallets.length === 0) { + throw new Error('No wallets found for user'); + } + + // Find the selected wallet + let selectedWallet: Wallet | undefined; + if (activeWallet === 'Private') { + selectedWallet = userWallets.find(w => w.name.toLowerCase() === 'private') || + userWallets.find(w => w.name.toLowerCase().includes('private')); + } else { + selectedWallet = userWallets.find(w => w.name.toLowerCase() === 'allowance') || + userWallets.find(w => w.name.toLowerCase().includes('allowance')); + } - // Fetch ALL payments from ALL wallets to enable matching - const allPayments: Transaction[] = []; + if (!selectedWallet?.inkey) { + const walletType = activeWallet === 'Private' ? 'Private' : 'Allowance'; + throw new Error(`${walletType} wallet not found for user`); + } - if (allUsers) { - for (const u of allUsers) { - try { - const wallets = await getUserWallets(adminKey, u.id); - if (wallets) { - for (const wallet of wallets) { + // Fetch transactions only for the current user's selected wallet + const transactions = await getWalletTransactionsSince( + selectedWallet.inkey, + paymentsSinceTimestamp, + null, + ); + + // Build wallet-to-user mapping and fetch all transactions for matching + const walletToUserMap = new Map(); + const allPayments: Transaction[] = []; + + if (allUsers) { + // Fetch all wallets and their transactions in parallel for efficiency + const walletPromises = allUsers.map(async (u) => { + try { + const wallets = await getUserWallets(adminKey, u.id); + if (wallets) { + // Map wallets to user + wallets.forEach(wallet => { + walletToUserMap.set(wallet.id, u); + }); + + // Fetch transactions from Private and Allowance wallets only + for (const wallet of wallets) { + const walletName = wallet.name.toLowerCase(); + if (walletName === 'private' || walletName === 'allowance') { try { const payments = await getWalletTransactionsSince( wallet.inkey, paymentsSinceTimestamp, - null, + null ); allPayments.push(...payments); } catch (err) { - console.error(`Error fetching payments for wallet ${wallet.id}:`, err); + // Silently continue } } } - } catch (err) { - console.error(`Error fetching wallets for user ${u.id}:`, err); } - } - } - - // Create a map of all payments by checking_id for internal transfer matching - const paymentsByCheckingId = new Map(); - allPayments.forEach(payment => { - const cleanId = payment.checking_id?.replace('internal_', '') || ''; - if (cleanId) { - const existing = paymentsByCheckingId.get(cleanId) || []; - existing.push(payment); - paymentsByCheckingId.set(cleanId, existing); + } catch (err) { + // Silently continue if wallet fetch fails for a user } }); - let inkey: any = null; - - if (userWallets && userWallets.length > 0) { - if (activeWallet === 'Private') { - const privateWallet = userWallets.find(w => w.name.toLowerCase().includes('private')); - inkey = privateWallet?.inkey; - } else { - const allowanceWallet = userWallets.find(w => w.name.toLowerCase().includes('allowance')); - inkey = allowanceWallet?.inkey; - } - } else { - console.error('No wallets found for user'); - } + await Promise.all(walletPromises); + } - // Check if inkey exists before fetching transactions - if (!inkey) { - const walletType = activeWallet === 'Private' ? 'Private' : 'Allowance'; - throw new Error(`${walletType} wallet not found for user`); + // Create a map of all payments by checking_id for internal transfer matching + const paymentsByCheckingId = new Map(); + allPayments.forEach(payment => { + const cleanId = payment.checking_id?.replace('internal_', '') || ''; + if (cleanId) { + const existing = paymentsByCheckingId.get(cleanId) || []; + existing.push(payment); + paymentsByCheckingId.set(cleanId, existing); } + }); - const transactions = await getWalletTransactionsSince( - inkey, - paymentsSinceTimestamp, - null, - ); - - // Don't filter by tab here - we'll cache ALL transactions and filter later - for (const transaction of transactions) { - const walletOwner = walletToUserMap.get(transaction.wallet_id); - const isIncoming = transaction.amount > 0; - - // Initialize extra.from and extra.to - if (!transaction.extra) { - transaction.extra = {}; - } - - // Try to find matching internal payment (the other side of the transfer) - const cleanCheckingId = transaction.checking_id?.replace('internal_', '') || ''; - const matchingPayments = paymentsByCheckingId.get(cleanCheckingId) || []; - const matchingPayment = matchingPayments.find(p => p.wallet_id !== transaction.wallet_id); + // Process transactions to add from/to user info + for (const transaction of transactions) { + const isIncoming = transaction.amount > 0; + let otherUser: User | null = null; - let otherUser: User | null = null; + // Try to find matching internal payment (the other side of the transfer) + const cleanCheckingId = transaction.checking_id?.replace('internal_', '') || ''; + const matchingPayments = paymentsByCheckingId.get(cleanCheckingId) || []; + const matchingPayment = matchingPayments.find(p => p.wallet_id !== transaction.wallet_id); - // First try to find the other party via matching payment - if (matchingPayment) { - otherUser = walletToUserMap.get(matchingPayment.wallet_id) || null; - } + if (matchingPayment) { + otherUser = walletToUserMap.get(matchingPayment.wallet_id) || null; + } - // If no matching payment found, try to extract from memo - if (!otherUser && transaction.memo) { - // Try to find user by matching displayName or email in memo - const memo = transaction.memo.toLowerCase(); - const foundUser = allUsers?.find(u => { - const displayName = u.displayName?.toLowerCase(); - const email = u.email?.toLowerCase(); - const username = u.email?.split('@')[0]?.toLowerCase(); - - return ( - (displayName && memo.includes(displayName)) || - (email && memo.includes(email)) || - (username && memo.includes(username)) - ); - }); - - if (foundUser) { - otherUser = foundUser; - } - } + // Fall back to memo text matching if no match found + if (!otherUser && transaction.memo && allUsers) { + const memo = transaction.memo.toLowerCase(); + otherUser = allUsers.find(u => { + const displayName = u.displayName?.toLowerCase(); + const email = u.email?.toLowerCase(); + const username = u.email?.split('@')[0]?.toLowerCase(); + return ( + (displayName && memo.includes(displayName)) || + (email && memo.includes(email)) || + (username && memo.includes(username)) + ); + }) || null; + } - if (isIncoming) { - // For incoming: TO = current wallet owner, FROM = other party - transaction.extra.to = walletOwner || null; - transaction.extra.from = otherUser; - } else { - // For outgoing: FROM = current wallet owner, TO = other party - transaction.extra.from = walletOwner || null; - transaction.extra.to = otherUser; - } + // Set the from/to fields + if (!transaction.extra) { + transaction.extra = {}; } - fetchedTransactions = fetchedTransactions.concat(transactions); + if (isIncoming) { + transaction.extra.to = user; + transaction.extra.from = otherUser; + } else { + transaction.extra.from = user; + transaction.extra.to = otherUser; + } } - // Cache all transactions - setAllTransactions(fetchedTransactions); + setAllTransactions(transactions); setCurrentWallet(activeWallet); } catch (error) { + console.error('WalletTransactionLog fetch error:', error); if (error instanceof Error) { - setError(`Failed to fetch transactions: ${error.message}`); + if (error.message === 'Failed to fetch') { + setError('Unable to connect to the server. Please check your network connection and try again.'); + } else if (error.message.includes('wallet not found')) { + setError(`${activeWallet === 'Private' ? 'Private' : 'Allowance'} wallet is not available for your account.`); + } else { + setError(`Failed to load transactions: ${error.message}`); + } } else { - setError('An unknown error occurred while fetching transactions'); + setError('An unexpected error occurred. Please refresh and try again.'); } - console.error(error); } finally { setLoading(false); } @@ -226,7 +211,7 @@ const WalletTransactionLog: React.FC = ({ setDisplayedTransactions([]); fetchTransactions(); } - }, [activeWallet, accounts, currentWallet, users]); + }, [activeWallet, accounts, currentWallet]); // Separate effect to filter cached transactions when activeTab changes useEffect(() => { @@ -261,14 +246,6 @@ const rewardsName = rewardNameContext.rewardName; return
{error}
; } - if (loading) { - return
Loading...
; - } - - if (error) { - return
{error}
; - } - return ( diff --git a/tabs/src/services/lnbitsServiceLocal.ts b/tabs/src/services/lnbitsServiceLocal.ts index d01aa5288..352cdac50 100644 --- a/tabs/src/services/lnbitsServiceLocal.ts +++ b/tabs/src/services/lnbitsServiceLocal.ts @@ -15,6 +15,31 @@ const TOKEN_EXPIRY_HOURS = 24; const TOKEN_KEY = 'accessToken'; const TOKEN_TIMESTAMP_KEY = 'accessTokenTimestamp'; +// Cache for API responses to prevent redundant calls +const CACHE_DURATION_MS = 60000; // 1 minute cache +interface CacheEntry { + data: T; + timestamp: number; +} +const apiCache: { + allUsers?: CacheEntry; + userWallets: Map>; + walletTransactions: Map>; +} = { + userWallets: new Map(), + walletTransactions: new Map(), +}; + +// Helper to check if cache is valid +const isCacheValid = (entry: CacheEntry | undefined): entry is CacheEntry => { + if (!entry) return false; + return Date.now() - entry.timestamp < CACHE_DURATION_MS; +}; + +// Pending promises to prevent duplicate concurrent requests +let pendingUsersRequest: Promise | null = null; +const pendingWalletRequests: Map> = new Map(); + // Get token from storage if valid, otherwise return null const getStoredToken = (): string | null => { const token = sessionStorage.getItem(TOKEN_KEY); @@ -214,92 +239,98 @@ const getUserWallets = async ( adminKey: string, userId: string, ): Promise => { + // Check cache first + const cacheKey = userId; + const cached = apiCache.userWallets.get(cacheKey); + if (isCacheValid(cached)) { + return cached.data; + } - try { - const accessToken = await getAccessToken(`${userName}`, `${password}`); - const response = await fetch( - `${nodeUrl}/users/api/v1/user/${userId}/wallet`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - //'X-Api-Key': adminKey, - }, - }, - ); + // Check for pending request to prevent duplicate concurrent calls + const pending = pendingWalletRequests.get(cacheKey); + if (pending) { + return pending; + } - if (!response.ok) { - throw new Error( - `Error getting users wallets response (status: ${response.status})`, + // Create new request + const request = (async (): Promise => { + try { + const accessToken = await getAccessToken(`${userName}`, `${password}`); + const response = await fetch( + `${nodeUrl}/users/api/v1/user/${userId}/wallet`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, ); - } - const data: Wallet[] = await response.json(); + if (!response.ok) { + throw new Error( + `Error getting users wallets response (status: ${response.status})`, + ); + } - // Map the wallets to match the Wallet interface - let walletData: Wallet[] = data.map((wallet: any) => ({ - id: wallet.id, - admin: wallet.admin || '', // TODO: To be implemented. Ref: https://t.me/lnbits/90188 - name: wallet.name, - adminkey: wallet.adminkey, - user: wallet.user, - inkey: wallet.inkey, - balance_msat: wallet.balance_msat, // TODO: To be implemented. Ref: https://t.me/lnbits/90188 - deleted: wallet.deleted, - })); + const data: Wallet[] = await response.json(); + + // Map the wallets to match the Wallet interface + let walletData: Wallet[] = data.map((wallet: any) => ({ + id: wallet.id, + admin: wallet.admin || '', + name: wallet.name, + adminkey: wallet.adminkey, + user: wallet.user, + inkey: wallet.inkey, + balance_msat: wallet.balance_msat, + deleted: wallet.deleted, + })); + + // Now remove the deleted wallets. + const filteredWallets = walletData.filter( + wallet => wallet.deleted !== true, + ); - // Now remove the deleted wallets. - const filteredWallets = walletData.filter( - wallet => wallet.deleted !== true, - ); + // Cache the result + apiCache.userWallets.set(cacheKey, { + data: filteredWallets, + timestamp: Date.now(), + }); - return filteredWallets; - } catch (error) { - console.error(error); - throw error; - } + return filteredWallets; + } catch (error) { + console.error(`Error fetching wallets for user ${userId}:`, error); + throw error; + } finally { + pendingWalletRequests.delete(cacheKey); + } + })(); + + pendingWalletRequests.set(cacheKey, request); + return request; }; // Migrated to use LNbits v1+ core API // Gets all users from /users/api/v1/user endpoint const getUsers = async ( adminKey: string, - filterByExtra: { [key: string]: string } | null, // Pass the extra field as an object + filterByExtra: { [key: string]: string } | null, ): Promise => { - logger.debug('=== getUsers ==='); - logger.debug('Fetching users from /users/api/v1/user'); - logger.debug('Filter criteria:', filterByExtra); - try { - // Get all users directly from the Users API + // Get all users directly from the Users API (now cached) const rawUsers = await getAllUsersFromAPI(); if (!rawUsers || rawUsers.length === 0) { - logger.debug('No users found'); return []; } - logger.debug(`Found ${rawUsers.length} users`); - - // Debug: Log first user to see available fields - if (rawUsers.length > 0) { - logger.debug('=== SAMPLE RAW USER FROM API ==='); - logger.debug('Sample user data:', rawUsers[0]); - logger.debug('Available fields:', Object.keys(rawUsers[0])); - logger.debug('Sample user.external_id:', rawUsers[0].external_id); - } - // Map the raw user data to User objects - // Note: Wallets are NOT fetched here - use separate functions to get wallets when needed const users: User[] = rawUsers.map((user: any) => { - // Try to get a friendly display name from various fields let displayName = user.username || user.id; - // If username is an email, extract the name part if (displayName.includes('@')) { displayName = displayName.split('@')[0].replace('.', ' '); - // Capitalize first letter of each word displayName = displayName.split(' ').map((word: string) => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); @@ -308,46 +339,34 @@ const getUsers = async ( return { id: user.id, displayName: displayName, - profileImg: user.extra?.profileImg || '', // Get from extra metadata if available - aadObjectId: user.external_id || user.extra?.aadObjectId || '', // Get from external_id or extra metadata - email: user.email || user.extra?.email || user.username || '', // Get from user object or extra metadata - type: (user.extra?.type as UserType) || 'Teammate' as UserType, // Default type - privateWallet: null, // Wallets should be fetched separately when needed - allowanceWallet: null, // Wallets should be fetched separately when needed + profileImg: user.extra?.profileImg || '', + aadObjectId: user.external_id || user.extra?.aadObjectId || '', + email: user.email || user.extra?.email || user.username || '', + type: (user.extra?.type as UserType) || 'Teammate' as UserType, + privateWallet: null, + allowanceWallet: null, }; }); // Apply filter if provided if (filterByExtra && Object.keys(filterByExtra).length > 0) { - console.log('=== FILTERING USERS ==='); - // Check if filtering by aadObjectId (which is stored in external_id field) if (filterByExtra.aadObjectId) { - console.log('Filtering by aadObjectId (external_id):', filterByExtra.aadObjectId); - const filteredUsers = users.filter(user => { const userRaw = rawUsers.find((u: any) => u.id === user.id); if (!userRaw) return false; - - const matches = userRaw.external_id === filterByExtra.aadObjectId; - console.log(`User ${user.displayName}: external_id=${userRaw.external_id}, matches=${matches}`); - return matches; + return userRaw.external_id === filterByExtra.aadObjectId; }); - - console.log(`Filtered to ${filteredUsers.length} users by external_id`); - console.log('===================='); return filteredUsers; } // Otherwise, filter by extra metadata fields - console.log('Filtering by extra metadata:', filterByExtra); const filteredUsers = users.filter(user => { const userRaw = rawUsers.find((u: any) => u.id === user.id); if (!userRaw || !userRaw.extra) { return false; } - // If extra is a string, try to parse it let extraData = userRaw.extra; if (typeof extraData === 'string') { try { @@ -361,13 +380,9 @@ const getUsers = async ( key => extraData[key] === filterByExtra[key] ); }); - - console.log(`Filtered to ${filteredUsers.length} users by extra metadata`); - console.log('===================='); return filteredUsers; } - console.log('Returning all users'); return users; } catch (error) { console.error('Error fetching users:', error); @@ -597,9 +612,6 @@ const getAllWallets = async (lnKey: string) => { const data: Wallet[] = await response.json(); - console.log('All Wallets returned:', data.length); - console.log('All Wallets: ', data); - // Map the wallets to match the Wallet interface let walletData: Wallet[] = data.map((wallet: any) => ({ id: wallet.id, @@ -617,7 +629,6 @@ const getAllWallets = async (lnKey: string) => { wallet => wallet.deleted !== true, ); - console.log('Filtered wallets count:', filteredWallets.length); return filteredWallets; } catch (error) { console.error('Error in getAllWallets:', error); @@ -657,15 +668,12 @@ const getWalletTransactionsSince = async ( const data = await response.json(); - console.log("DATA",data); - // Show all payments (timestamp filter removed) const paymentsSince = data; // Further filter by the `extra` field (if provided) const filteredPayments = filterByExtra ? paymentsSince.filter((payment: any) => { - // Check if the payment's extra field matches the filterByExtra object const paymentExtra = payment.extra || {}; return Object.keys(filterByExtra).every( key => paymentExtra[key] === filterByExtra[key], @@ -673,8 +681,6 @@ const getWalletTransactionsSince = async ( }) : paymentsSince; - console.log("DATA2",filteredPayments); - // Map the payments to match the Zap interface const transactionData: Transaction[] = filteredPayments.map( (transaction: any) => ({ @@ -976,8 +982,6 @@ const getAllPayments = async ( url.searchParams.append('sortby', sortby); url.searchParams.append('direction', direction); - console.log('Full URL:', url.toString()); - const response = await fetch(url.toString(), { method: 'GET', headers: { @@ -995,9 +999,6 @@ const getAllPayments = async ( } const data = await response.json(); - console.log('Raw response data:', data); - console.log('Data type:', typeof data); - console.log('Is array:', Array.isArray(data)); // The API might return an object with a 'data' or 'payments' property let payments = data; @@ -1013,10 +1014,6 @@ const getAllPayments = async ( } } - console.log('Total payments retrieved:', payments?.length || 0); - console.log('Sample payment:', payments?.[0]); - console.log('==========================='); - return Array.isArray(payments) ? payments : []; } catch (error) { console.error('Error in getAllPayments:', error); @@ -1024,42 +1021,57 @@ const getAllPayments = async ( } }; -// NEW: Get all users from /users/api/v1/user endpoint +// NEW: Get all users from /users/api/v1/user endpoint (with caching) const getAllUsersFromAPI = async (): Promise => { - console.log('=== getAllUsersFromAPI ==='); - console.log('Fetching from:', `${nodeUrl}/users/api/v1/user`); + // Return cached data if valid + if (isCacheValid(apiCache.allUsers)) { + return apiCache.allUsers.data; + } - try { - const accessToken = await getAccessToken(`${userName}`, `${password}`); + // Return pending request if one exists (prevents duplicate concurrent calls) + if (pendingUsersRequest) { + return pendingUsersRequest; + } - const response = await fetch(`${nodeUrl}/users/api/v1/user`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }); + // Create new request + pendingUsersRequest = (async () => { + try { + const accessToken = await getAccessToken(`${userName}`, `${password}`); - if (!response.ok) { - console.error('Response status:', response.status); - console.error('Response statusText:', response.statusText); - throw new Error( - `Error getting all users (status: ${response.status})`, - ); - } + const response = await fetch(`${nodeUrl}/users/api/v1/user`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }); - const responseData = await response.json(); - console.log('Total users retrieved:', responseData?.data?.length || 0); - console.log('All Users:', responseData); - console.log('==========================='); + if (!response.ok) { + throw new Error( + `Error getting all users (status: ${response.status})`, + ); + } - // Extract the users array from the response - const users = responseData?.data || []; - return Array.isArray(users) ? users : []; - } catch (error) { - console.error('Error in getAllUsersFromAPI:', error); - throw error; - } + const responseData = await response.json(); + const users = responseData?.data || []; + const result = Array.isArray(users) ? users : []; + + // Cache the result + apiCache.allUsers = { + data: result, + timestamp: Date.now(), + }; + + return result; + } catch (error) { + console.error('Error in getAllUsersFromAPI:', error); + throw error; + } finally { + pendingUsersRequest = null; + } + })(); + + return pendingUsersRequest; }; // NEW: Get wallets paginated for a specific user @@ -1077,8 +1089,6 @@ const getWalletsPaginated = async ( url.searchParams.append('offset', offset.toString()); url.searchParams.append('user_id', userId); - console.log('>>> Full URL with params:', url.toString()); - const response = await fetch(url.toString(), { method: 'GET', headers: { @@ -1096,17 +1106,9 @@ const getWalletsPaginated = async ( } const responseData = await response.json(); - console.log(`>>> Raw response for user ${userId}:`, responseData); // Extract the wallets array from the response (API returns {data: [...], total: X}) const wallets = responseData?.data || []; - console.log(`>>> Extracted ${wallets.length} wallets from response`); - - // DEBUG: Show the wallet.user field for each wallet to verify they match the requested userId - console.log(`>>> WALLET USER IDs FOR REQUESTED USER ${userId}:`); - wallets.forEach((wallet: any, index: number) => { - console.log(` Wallet ${index + 1}: ID=${wallet.id}, Name="${wallet.name}", User ID=${wallet.user}, Matches=${wallet.user === userId ? '✓' : '✗'}`); - }); // Map ALL fields from the API response to match the Wallet interface const walletData: Wallet[] = wallets.map((wallet: any) => ({ @@ -1129,10 +1131,6 @@ const getWalletsPaginated = async ( wallet => wallet.deleted !== true, ); - console.log(`>>> Filtered wallets count for user ${userId}:`, filteredWallets.length); - console.log(`>>> Wallet IDs: [${filteredWallets.map(w => w.id).join(', ')}]`); - console.log('==========================='); - return filteredWallets; } catch (error) { console.error(`Error in getWalletsPaginated for user ${userId}:`, error); From f6a45b50c59db22797cd5046a6eadc065ca19cb9 Mon Sep 17 00:00:00 2001 From: Akash Jadhav Date: Tue, 9 Dec 2025 12:06:40 +0000 Subject: [PATCH 3/8] Modified the feedlist to individual wallet fetch --- tabs/src/components/FeedList.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tabs/src/components/FeedList.tsx b/tabs/src/components/FeedList.tsx index 1cd2eff62..009ca65b3 100644 --- a/tabs/src/components/FeedList.tsx +++ b/tabs/src/components/FeedList.tsx @@ -23,6 +23,18 @@ interface ZapTransaction { const ITEMS_PER_PAGE = 10; // Items per page const MAX_RECORDS = 100; // Maximum records to display +// Wallet type identifiers - these match the exact naming convention used by the backend +// Backend creates wallets with names 'Allowance' and 'Private' (see functions/sendZap/index.ts) +const WALLET_NAME_ALLOWANCE = 'Allowance'; +const WALLET_NAME_PRIVATE = 'Private'; + +// Helper functions to identify wallet types by name (exact match, case-insensitive) +const isAllowanceWallet = (walletName: string): boolean => + walletName.toLowerCase() === WALLET_NAME_ALLOWANCE.toLowerCase(); + +const isPrivateWallet = (walletName: string): boolean => + walletName.toLowerCase() === WALLET_NAME_PRIVATE.toLowerCase(); + const FeedList: React.FC = ({ timestamp, allZaps = [], From f9ab11a5773cc046d0494ab477088caa2607ce68 Mon Sep 17 00:00:00 2001 From: Akash Jadhav Date: Tue, 9 Dec 2025 13:55:20 +0000 Subject: [PATCH 4/8] Fixed the Feed filtering issue --- tabs/src/components/FeedList.tsx | 39 ++- tabs/src/components/WalletTransactionLog.tsx | 245 ++++++++-------- tabs/src/services/lnbitsServiceLocal.ts | 286 ++++++++++--------- 3 files changed, 299 insertions(+), 271 deletions(-) diff --git a/tabs/src/components/FeedList.tsx b/tabs/src/components/FeedList.tsx index 009ca65b3..c1fba0fd8 100644 --- a/tabs/src/components/FeedList.tsx +++ b/tabs/src/components/FeedList.tsx @@ -23,18 +23,6 @@ interface ZapTransaction { const ITEMS_PER_PAGE = 10; // Items per page const MAX_RECORDS = 100; // Maximum records to display -// Wallet type identifiers - these match the exact naming convention used by the backend -// Backend creates wallets with names 'Allowance' and 'Private' (see functions/sendZap/index.ts) -const WALLET_NAME_ALLOWANCE = 'Allowance'; -const WALLET_NAME_PRIVATE = 'Private'; - -// Helper functions to identify wallet types by name (exact match, case-insensitive) -const isAllowanceWallet = (walletName: string): boolean => - walletName.toLowerCase() === WALLET_NAME_ALLOWANCE.toLowerCase(); - -const isPrivateWallet = (walletName: string): boolean => - walletName.toLowerCase() === WALLET_NAME_PRIVATE.toLowerCase(); - const FeedList: React.FC = ({ timestamp, allZaps = [], @@ -277,11 +265,32 @@ const FeedList: React.FC = ({ return 0; }); + // Apply timestamp filter (7/30/60 days) - filter transactions by time + // timestamp prop is in Unix seconds (e.g., 7 days ago) + // transaction.time can be either a number (Unix seconds) or an ISO date string + const filteredZaps = timestamp && timestamp > 0 + ? sortedZaps.filter(zap => { + const txTimeRaw = zap.transaction.time; + let txTimeSeconds: number; + + if (typeof txTimeRaw === 'number') { + txTimeSeconds = txTimeRaw; + } else if (typeof txTimeRaw === 'string') { + // Parse ISO date string to Unix seconds + txTimeSeconds = Math.floor(new Date(txTimeRaw).getTime() / 1000); + } else { + txTimeSeconds = 0; + } + + return txTimeSeconds >= timestamp; + }) + : sortedZaps; + // Calculate pagination variables - const totalPages = Math.ceil(sortedZaps.length / ITEMS_PER_PAGE); + const totalPages = Math.ceil(filteredZaps.length / ITEMS_PER_PAGE); const indexOfLastItem = currentPage * ITEMS_PER_PAGE; const indexOfFirstItem = indexOfLastItem - ITEMS_PER_PAGE; - const currentItems = sortedZaps.slice(indexOfFirstItem, indexOfLastItem); + const currentItems = filteredZaps.slice(indexOfFirstItem, indexOfLastItem); const nextPage = () => setCurrentPage(prev => Math.min(prev + 1, totalPages)); const prevPage = () => setCurrentPage(prev => Math.max(prev - 1, 1)); @@ -401,7 +410,7 @@ const FeedList: React.FC = ({ ) : (
No data available
)} - {sortedZaps.length > ITEMS_PER_PAGE && ( + {filteredZaps.length > ITEMS_PER_PAGE && (