diff --git a/tabs/src/components/FeedList.tsx b/tabs/src/components/FeedList.tsx index 8ebf9695a..04fc20761 100644 --- a/tabs/src/components/FeedList.tsx +++ b/tabs/src/components/FeedList.tsx @@ -20,6 +20,22 @@ interface ZapTransaction { const ITEMS_PER_PAGE = 10; // Items per page const MAX_RECORDS = 100; // Maximum records to display +// Helper function to parse transaction timestamp (handles both Unix seconds and ISO strings) +const parseTransactionTime = (timestamp: number | string): Date | null => { + if (typeof timestamp === 'number') { + return new Date(timestamp * 1000); + } + if (typeof timestamp === 'string') { + const date = new Date(timestamp); + if (isNaN(date.getTime())) { + console.warn(`Invalid timestamp: ${timestamp}`); + return null; + } + return date; + } + return null; +}; + // 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) // NOTE: If wallet naming conventions change on the backend, these must be updated @@ -74,16 +90,18 @@ const FeedList: React.FC = ({ timestamp }) => { return; } - // Step 2: Get wallets for each user - const allWalletsData: { userId: string; wallets: Wallet[] }[] = []; + // Step 2: Parallelize wallet fetches for all users + const walletPromises = fetchedUsers.map(async (user) => { + try { + const userWallets = await getUserWallets(adminKey, user.id); + return { userId: user.id, wallets: userWallets || [] }; + } catch (err) { + // Log error but continue - don't fail entire feed for one user + return { userId: user.id, wallets: [] }; + } + }); + const allWalletsData = await Promise.all(walletPromises); - for (const user of fetchedUsers) { - const userWallets = await getUserWallets(adminKey, user.id); - allWalletsData.push({ - userId: user.id, - wallets: userWallets || [] - }); - } // Step 3: Get payments from both Allowance and Private wallets // We need both to match sender (Allowance) with receiver (Private) const allowanceWalletIds = new Set(); @@ -92,7 +110,7 @@ const FeedList: React.FC = ({ timestamp }) => { let failedWalletCount = 0; for (const userData of allWalletsData) { - // Filter to Allowance and Private wallets + // Filter to Allowance and Private wallets using exact match const relevantWallets = userData.wallets.filter(wallet => isAllowanceWallet(wallet.name) || isPrivateWallet(wallet.name) ); @@ -296,11 +314,25 @@ const FeedList: React.FC = ({ timestamp }) => { 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 parsedDate = parseTransactionTime(zap.transaction.time); + if (!parsedDate) { + return false; // Exclude transactions with invalid/unknown time format + } + const txTimeSeconds = Math.floor(parsedDate.getTime() / 1000); + return txTimeSeconds >= timestamp; + }) + : sortedZaps; + // Calculate pagination variables - const totalPages = Math.max(1, Math.ceil(sortedZaps.length / ITEMS_PER_PAGE)); + const totalPages = Math.max(1, 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)); @@ -359,15 +391,9 @@ const FeedList: React.FC = ({ timestamp }) => {
{(() => { - const timestamp = zap.transaction.time; - // Try to parse as ISO string first, then Unix timestamp - let date = new Date(timestamp); - if (isNaN(date.getTime()) && typeof timestamp === 'number') { - // Try as Unix timestamp (seconds) - date = new Date(timestamp * 1000); - } - if (isNaN(date.getTime())) { - return `Invalid: ${timestamp}`; + const date = parseTransactionTime(zap.transaction.time); + if (!date) { + return `Invalid: ${zap.transaction.time}`; } // UK format: DD/MM/YYYY HH:MM (24-hour) return `${date.toLocaleDateString('en-GB')} ${date.toLocaleTimeString('en-GB', { @@ -421,7 +447,7 @@ const FeedList: React.FC = ({ timestamp }) => { ) : (
No data available
)} - {sortedZaps.length > ITEMS_PER_PAGE && ( + {filteredZaps.length > ITEMS_PER_PAGE && (
-
+
)}
); }; -export default FeedList; \ No newline at end of file +export default FeedList; diff --git a/tabs/src/components/WalletAllowanceComponent.tsx b/tabs/src/components/WalletAllowanceComponent.tsx index 7e3f3c040..c7a2e356d 100644 --- a/tabs/src/components/WalletAllowanceComponent.tsx +++ b/tabs/src/components/WalletAllowanceComponent.tsx @@ -10,6 +10,11 @@ import SendZapsPopup from './SendZapsPopup'; const adminKey = process.env.REACT_APP_LNBITS_ADMINKEY as string; +// Time constants +const SECONDS_PER_DAY = 86400; +const MS_PER_SECOND = 1000; +const TRANSACTION_HISTORY_DAYS = 30; + interface AllowanceCardProps { // Define the props here if there are any, for example: // someProp: string; @@ -62,18 +67,22 @@ 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 transactionHistoryStart = Date.now() / MS_PER_SECOND - TRANSACTION_HISTORY_DAYS * SECONDS_PER_DAY; + const transaction = await getWalletTransactionsSince( + allowanceWallet.inkey, + transactionHistoryStart, + {} + ); + + const spent = transaction + .filter(t => t.amount < 0) + .reduce((total, t) => total + Math.abs(t.amount), 0) / MS_PER_SECOND; + setSpentSats(spent); + } else { + setSpentSats(0); + } } } } diff --git a/tabs/src/components/WalletTransactionLog.tsx b/tabs/src/components/WalletTransactionLog.tsx index 3f12bcf77..43cedc2ee 100644 --- a/tabs/src/components/WalletTransactionLog.tsx +++ b/tabs/src/components/WalletTransactionLog.tsx @@ -19,13 +19,17 @@ interface WalletTransactionLogProps { const adminKey = process.env.REACT_APP_LNBITS_ADMINKEY as string; +// Time constants +const SECONDS_PER_DAY = 86400; +const MS_PER_SECOND = 1000; +const TRANSACTION_HISTORY_DAYS = 30; + const WalletTransactionLog: React.FC = ({ activeTab, activeWallet, }) => { const [allTransactions, setAllTransactions] = useState([]); // Cache all transactions const [displayedTransactions, setDisplayedTransactions] = useState([]); // Filtered transactions to display - const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [currentWallet, setCurrentWallet] = useState(undefined); // Track which wallet data is cached for @@ -34,11 +38,10 @@ 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; + // Calculate the timestamp for transaction history period + const transactionHistoryStart = Date.now() / MS_PER_SECOND - TRANSACTION_HISTORY_DAYS * SECONDS_PER_DAY; - // Use the provided timestamp or default to 7 days ago - const paymentsSinceTimestamp = sevenDaysAgo; + const paymentsSinceTimestamp = transactionHistoryStart; const account = accounts[0]; @@ -51,9 +54,6 @@ const WalletTransactionLog: React.FC = ({ try { // First, fetch all users const allUsers = await getUsers(adminKey, {}); - if (allUsers) { - setUsers(allUsers); - } const currentUserLNbitDetails = await getUsers(adminKey, { aadObjectId: account.localAccountId, @@ -65,50 +65,48 @@ const WalletTransactionLog: React.FC = ({ // Fetch user's wallets const userWallets = await getUserWallets(adminKey, user.id); - // Create a wallet ID to user mapping for ALL users + // Create a wallet ID to user mapping for ALL users - parallelized const walletToUserMap = new Map(); + let allPayments: Transaction[] = []; - // 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); - }); + // Parallelize wallet fetches for all users + const walletResults = await Promise.all( + allUsers.map(async (u) => { + try { + const wallets = await getUserWallets(adminKey, u.id); + return { user: u, wallets: wallets || [] }; + } catch (err) { + // Log error but continue - don't fail for one user + return { user: u, wallets: [] }; } - } catch (err) { - console.error(`Error fetching wallets for user ${u.id}:`, err); - } - } - } - - // Fetch ALL payments from ALL wallets to enable matching - const allPayments: Transaction[] = []; + }) + ); - if (allUsers) { - for (const u of allUsers) { - try { - const wallets = await getUserWallets(adminKey, u.id); - if (wallets) { - for (const wallet of wallets) { - try { - const payments = await getWalletTransactionsSince( - wallet.inkey, - paymentsSinceTimestamp, - null, - ); - allPayments.push(...payments); - } catch (err) { - console.error(`Error fetching payments for wallet ${wallet.id}:`, err); - } - } + // Build wallet to user mapping + walletResults.forEach(({ user, wallets }) => { + wallets.forEach(wallet => { + walletToUserMap.set(wallet.id, user); + }); + }); + + // Collect all wallets and parallelize payment fetches + const allWallets = walletResults.flatMap(r => r.wallets); + const paymentResults = await Promise.all( + allWallets.map(async (wallet) => { + try { + return await getWalletTransactionsSince( + wallet.inkey, + paymentsSinceTimestamp, + null, + ); + } catch (err) { + // Log error but continue - don't fail for one wallet + return []; } - } catch (err) { - console.error(`Error fetching wallets for user ${u.id}:`, err); - } - } + }) + ); + allPayments = paymentResults.flat(); } // Create a map of all payments by checking_id for internal transfer matching @@ -214,13 +212,19 @@ const WalletTransactionLog: React.FC = ({ } }; + // Early return if no accounts available yet + if (!accounts || accounts.length === 0) { + setLoading(false); + return; + } + // Only fetch if wallet changed or no data cached if (currentWallet !== activeWallet) { setAllTransactions([]); setDisplayedTransactions([]); fetchTransactions(); } - }, [activeWallet, accounts, currentWallet, users]); + }, [activeWallet, accounts, currentWallet]); // Separate effect to filter cached transactions when activeTab changes useEffect(() => { @@ -255,14 +259,6 @@ const rewardsName = rewardNameContext.rewardName; return
{error}
; } - if (loading) { - return
Loading...
; - } - - if (error) { - return
{error}
; - } - return ( diff --git a/tabs/src/hooks/useTeamsAuth.ts b/tabs/src/hooks/useTeamsAuth.ts index 740933808..af5238d30 100644 --- a/tabs/src/hooks/useTeamsAuth.ts +++ b/tabs/src/hooks/useTeamsAuth.ts @@ -3,6 +3,7 @@ import { useMsal } from '@azure/msal-react'; import { InteractionStatus } from '@azure/msal-browser'; import * as microsoftTeams from '@microsoft/teams-js'; import { toast } from 'react-toastify'; +import { clearApiCache } from '../services/lnbitsServiceLocal'; interface UseTeamsAuthReturn { isInTeams: boolean; @@ -34,11 +35,9 @@ export const useTeamsAuth = (): UseTeamsAuthReturn => { const context = await microsoftTeams.app.getContext(); if (context && mounted) { setIsInTeams(true); - console.log('Running inside Microsoft Teams'); } - } catch (error) { + } catch { // Not running in Teams context - this is expected for web browser - console.log('Not running in Teams context:', error instanceof Error ? error.message : String(error)); if (mounted) { setIsInTeams(false); } @@ -59,7 +58,6 @@ export const useTeamsAuth = (): UseTeamsAuthReturn => { const handleLogout = useCallback(async () => { // Check if MSAL has an interaction in progress if (inProgress !== InteractionStatus.None) { - console.log('MSAL interaction in progress, skipping logout'); return; } @@ -69,24 +67,20 @@ export const useTeamsAuth = (): UseTeamsAuthReturn => { setIsLoggingOut(true); try { - if (isInTeams) { - console.log('Logging out from Teams'); - } else { - console.log('Logging out from Web Browser'); - } + // Clear API cache before logout to prevent stale data on re-login + clearApiCache(); + await instance.logoutPopup({ postLogoutRedirectUri: window.location.origin + '/login', account: accounts[0] || null, }); - console.log('Successfully logged out from MSAL'); } catch (error) { - console.error('Error during logout:', error); toast.error('Failed to sign out. Please try again.'); } finally { logoutInProgressRef.current = false; setIsLoggingOut(false); } - }, [instance, accounts, isInTeams, inProgress]); + }, [instance, accounts, inProgress]); return { isInTeams, diff --git a/tabs/src/services/lnbitsServiceLocal.ts b/tabs/src/services/lnbitsServiceLocal.ts index d01aa5288..e9b42945d 100644 --- a/tabs/src/services/lnbitsServiceLocal.ts +++ b/tabs/src/services/lnbitsServiceLocal.ts @@ -15,6 +15,68 @@ const TOKEN_EXPIRY_HOURS = 24; const TOKEN_KEY = 'accessToken'; const TOKEN_TIMESTAMP_KEY = 'accessTokenTimestamp'; +// ============================================================================= +// Cache Configuration +// ============================================================================= +// TTLs optimized for data change frequency: +// - Users: 60 seconds (new users should appear within a minute) +// - Wallets: 15 seconds (balance updates need to be reasonably fresh) +const CACHE_DURATION_USERS_MS = 60000; // 1 minute for user list +const CACHE_DURATION_WALLETS_MS = 15000; // 15 seconds for wallet data +const MAX_WALLET_CACHE_SIZE = 100; // Limit cache size to prevent memory growth + +interface CacheEntry { + data: T; + timestamp: number; +} + +// Raw API user data type (before mapping to User) +// Used by getAllUsersFromAPI - mapping to User type is done in getUsers() +interface RawApiUser { + id: string; + username?: string; + external_id?: string; + extra?: Record | string; +} + +// Cache stores raw API data to avoid type mismatches +const apiCache: { + rawUsers?: CacheEntry; + userWallets: Map>; +} = { + userWallets: new Map(), +}; + +// Helper to check if cache is valid with configurable duration +const isCacheValid = (entry: CacheEntry | undefined, durationMs: number): entry is CacheEntry => { + if (!entry) return false; + return Date.now() - entry.timestamp < durationMs; +}; + +// Pending promises to prevent duplicate concurrent requests +let pendingUsersRequest: Promise | null = null; +const pendingWalletRequests: Map> = new Map(); + +// Clear cache function - call on logout or account switch +export const clearApiCache = () => { + apiCache.rawUsers = undefined; + apiCache.userWallets.clear(); + pendingUsersRequest = null; + pendingWalletRequests.clear(); + logger.debug('API cache cleared'); +}; + +// Invalidate wallet cache for a specific user (call after transactions) +export const invalidateWalletCache = (userId?: string) => { + if (userId) { + apiCache.userWallets.delete(userId); + logger.debug(`Wallet cache invalidated for user ${userId}`); + } else { + apiCache.userWallets.clear(); + logger.debug('All wallet caches invalidated'); + } +}; + // Get token from storage if valid, otherwise return null const getStoredToken = (): string | null => { const token = sessionStorage.getItem(TOKEN_KEY); @@ -154,7 +216,7 @@ const getWallets = async ( return filteredData; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -180,7 +242,7 @@ const getWalletDetails = async (inKey: string, walletId: string) => { return data; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -205,7 +267,7 @@ const getWalletBalance = async (inKey: string) => { return data.balance / 1000; // return in Sats (not millisatoshis) } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -214,51 +276,88 @@ const getUserWallets = async ( adminKey: string, userId: string, ): Promise => { + // Check cache first with wallet-specific TTL + const cachedEntry = apiCache.userWallets.get(userId); + if (isCacheValid(cachedEntry, CACHE_DURATION_WALLETS_MS)) { + logger.debug(`[Cache HIT] getUserWallets for user ${userId}`); + return cachedEntry.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 if there's already a pending request for this user + const pendingRequest = pendingWalletRequests.get(userId); + if (pendingRequest) { + logger.debug(`[Dedup] Reusing pending getUserWallets request for user ${userId}`); + return pendingRequest; + } + + // Create new request and store it to prevent duplicates + const requestPromise = (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}`, + //'X-Api-Key': adminKey, + }, }, - }, - ); + ); - if (!response.ok) { - throw new Error( - `Error getting users wallets response (status: ${response.status})`, + if (!response.ok) { + throw new Error( + `Error getting users wallets response (status: ${response.status})`, + ); + } + + 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 || '', // 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, + })); + + // Now remove the deleted wallets. + const filteredWallets = walletData.filter( + wallet => wallet.deleted !== true, ); - } - const data: Wallet[] = await response.json(); + // Limit cache size to prevent unbounded memory growth + if (apiCache.userWallets.size >= MAX_WALLET_CACHE_SIZE) { + // Remove oldest entry (first entry in Map) + const firstKey = apiCache.userWallets.keys().next().value; + if (firstKey) { + apiCache.userWallets.delete(firstKey); + } + } - // 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, - })); + // Cache the result + apiCache.userWallets.set(userId, { + data: filteredWallets, + timestamp: Date.now(), + }); - // Now remove the deleted wallets. - const filteredWallets = walletData.filter( - wallet => wallet.deleted !== true, - ); + return filteredWallets; + } catch (error) { + logger.error('Error fetching user wallets:', error); + throw error; + } finally { + // Remove from pending requests + pendingWalletRequests.delete(userId); + } + })(); - return filteredWallets; - } catch (error) { - console.error(error); - throw error; - } + pendingWalletRequests.set(userId, requestPromise); + return requestPromise; }; // Migrated to use LNbits v1+ core API @@ -287,7 +386,6 @@ const getUsers = async ( 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 @@ -319,42 +417,44 @@ const getUsers = async ( // Apply filter if provided if (filterByExtra && Object.keys(filterByExtra).length > 0) { - console.log('=== FILTERING USERS ==='); + logger.debug('=== 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); + logger.debug('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}`); + logger.debug(`User ${user.displayName}: external_id=${userRaw.external_id}, matches=${matches}`); return matches; }); - console.log(`Filtered to ${filteredUsers.length} users by external_id`); - console.log('===================='); + logger.debug(`Filtered to ${filteredUsers.length} users by external_id`); + logger.debug('===================='); return filteredUsers; } // Otherwise, filter by extra metadata fields - console.log('Filtering by extra metadata:', filterByExtra); + logger.debug('Filtering by extra metadata:', filterByExtra); const filteredUsers = users.filter(user => { - const userRaw = rawUsers.find((u: any) => u.id === user.id); + const userRaw = rawUsers.find(u => 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') { + let extraData: Record; + if (typeof userRaw.extra === 'string') { try { - extraData = JSON.parse(extraData); + extraData = JSON.parse(userRaw.extra); } catch (e) { return false; } + } else { + extraData = userRaw.extra; } return Object.keys(filterByExtra).every( @@ -362,15 +462,15 @@ const getUsers = async ( ); }); - console.log(`Filtered to ${filteredUsers.length} users by extra metadata`); - console.log('===================='); + logger.debug(`Filtered to ${filteredUsers.length} users by extra metadata`); + logger.debug('===================='); return filteredUsers; } - console.log('Returning all users'); + logger.debug('Returning all users'); return users; } catch (error) { - console.error('Error fetching users:', error); + logger.error('Error fetching users:', error); throw error; } }; @@ -430,7 +530,7 @@ const getUser = async ( allowanceWallet: allowanceWallet, }; } catch (error) { - console.error(`Error fetching user ${userId}:`, error); + logger.error(`Error fetching user ${userId}:`, error); throw error; } }; @@ -454,7 +554,7 @@ const getWalletName = async (inKey: string) => { return data.name; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -477,7 +577,7 @@ const getWalletPayments = async (inKey: string) => { const data = await response.json(); return data; } catch (error) { - console.error('Error:', error); + logger.error('Error:', error); return null; } }; @@ -497,7 +597,7 @@ const getWalletPayLinks = async (inKey: string, walletId: string) => { ); if (!response.ok) { - console.error( + logger.error( `Error getting paylinks for wallet (status: ${response.status})`, ); return null; @@ -507,7 +607,7 @@ const getWalletPayLinks = async (inKey: string, walletId: string) => { return data; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -525,7 +625,7 @@ const getWalletId = async (inKey: string) => { }); if (!response.ok) { - console.error(`Error getting wallet ID (status: ${response.status})`); + logger.error(`Error getting wallet ID (status: ${response.status})`); return null; } @@ -535,14 +635,14 @@ const getWalletId = async (inKey: string) => { const wallet = data.find((wallet: any) => wallet.inkey === inKey); if (!wallet) { - console.error('No wallet found for this inKey.'); + logger.error('No wallet found for this inKey.'); return null; } // Return the id of the wallet return wallet.id; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -568,7 +668,7 @@ const getInvoicePayment = async (lnKey: string, invoice: string) => { return data; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -588,8 +688,8 @@ const getAllWallets = async (lnKey: string) => { }); if (!response.ok) { - console.error('Response status:', response.status); - console.error('Response statusText:', response.statusText); + logger.error('Response status:', response.status); + logger.error('Response statusText:', response.statusText); throw new Error( `Error getting wallets (status: ${response.status})`, ); @@ -597,8 +697,8 @@ const getAllWallets = async (lnKey: string) => { const data: Wallet[] = await response.json(); - console.log('All Wallets returned:', data.length); - console.log('All Wallets: ', data); + logger.debug('All Wallets returned:', data.length); + logger.debug('All Wallets: ', data); // Map the wallets to match the Wallet interface let walletData: Wallet[] = data.map((wallet: any) => ({ @@ -617,10 +717,10 @@ const getAllWallets = async (lnKey: string) => { wallet => wallet.deleted !== true, ); - console.log('Filtered wallets count:', filteredWallets.length); + logger.debug('Filtered wallets count:', filteredWallets.length); return filteredWallets; } catch (error) { - console.error('Error in getAllWallets:', error); + logger.error('Error in getAllWallets:', error); throw error; } }; @@ -657,7 +757,7 @@ const getWalletTransactionsSince = async ( const data = await response.json(); - console.log("DATA",data); + logger.debug("DATA",data); // Show all payments (timestamp filter removed) const paymentsSince = data; @@ -673,7 +773,7 @@ const getWalletTransactionsSince = async ( }) : paymentsSince; - console.log("DATA2",filteredPayments); + logger.debug("DATA2",filteredPayments); // Map the payments to match the Zap interface const transactionData: Transaction[] = filteredPayments.map( @@ -690,11 +790,11 @@ const getWalletTransactionsSince = async ( }), ); - //console.log('Transactions:', transactionData); + //logger.debug('Transactions:', transactionData); return transactionData; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -735,7 +835,7 @@ const createInvoice = async ( return data.payment_request; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -795,7 +895,7 @@ const createWallet = async ( return data; } catch (error) { - console.error(error); + logger.error(error); throw error; } }; @@ -825,7 +925,7 @@ const getWalletIdByUserId = async (adminKey: string, userId: string) => { return data.id; } catch (error) { - console.error(error); + logger.error(error); return null; } }; @@ -853,19 +953,19 @@ const getNostrRewards = async ( // Check if the response is JSON const contentType = response.headers.get('content-type'); - console.log('Content-Type:', contentType); + logger.debug('Content-Type:', contentType); if (contentType && contentType.includes('application/json')) { const data: Reward[] = await response.json(); - console.log('Products:', data); + logger.debug('Products:', data); return data; } else { const text = await response.text(); // Capture non-JSON responses - console.log('Non-JSON response:', text); + logger.debug('Non-JSON response:', text); throw new Error(`Expected JSON, but got: ${text}`); } } catch (error) { - console.error('Error fetching rewards:', error); + logger.error('Error fetching rewards:', error); throw error; } }; @@ -892,7 +992,7 @@ const getUserWalletTransactions = async ( if (!response.ok) { const errorMessage = `Failed to fetch transactions for wallet ${walletId}: ${response.status} - ${response.statusText}`; - console.error(errorMessage); + logger.error(errorMessage); throw new Error(errorMessage); } @@ -909,13 +1009,13 @@ const getUserWalletTransactions = async ( }) : data; - /*console.log( + /*logger.debug( `Transactions fetched for wallet: ${walletId}`, filteredPayments, );*/ // Log fetched data return filteredPayments; // Assuming data is an array of transactions } catch (error) { - console.error(`Error fetching transactions for wallet ${walletId}:`, error); + logger.error(`Error fetching transactions for wallet ${walletId}:`, error); throw error; // Re-throw the error to handle it in the parent function } }; @@ -954,7 +1054,7 @@ const getAllowance = async ( }; return allowance; } catch (error) { - console.error(`Error fetching allowances for ${userId}:`, error); + logger.error(`Error fetching allowances for ${userId}:`, error); throw error; // Re-throw the error to handle it in the parent function } }; @@ -976,7 +1076,7 @@ const getAllPayments = async ( url.searchParams.append('sortby', sortby); url.searchParams.append('direction', direction); - console.log('Full URL:', url.toString()); + logger.debug('Full URL:', url.toString()); const response = await fetch(url.toString(), { method: 'GET', @@ -987,17 +1087,17 @@ const getAllPayments = async ( }); if (!response.ok) { - console.error('Response status:', response.status); - console.error('Response statusText:', response.statusText); + logger.error('Response status:', response.status); + logger.error('Response statusText:', response.statusText); throw new Error( `Error getting all payments (status: ${response.status})`, ); } const data = await response.json(); - console.log('Raw response data:', data); - console.log('Data type:', typeof data); - console.log('Is array:', Array.isArray(data)); + logger.debug('Raw response data:', data); + logger.debug('Data type:', typeof data); + logger.debug('Is array:', Array.isArray(data)); // The API might return an object with a 'data' or 'payments' property let payments = data; @@ -1013,53 +1113,74 @@ const getAllPayments = async ( } } - console.log('Total payments retrieved:', payments?.length || 0); - console.log('Sample payment:', payments?.[0]); - console.log('==========================='); + logger.debug('Total payments retrieved:', payments?.length || 0); + logger.debug('Sample payment:', payments?.[0]); + logger.debug('==========================='); return Array.isArray(payments) ? payments : []; } catch (error) { - console.error('Error in getAllPayments:', error); + logger.error('Error in getAllPayments:', error); throw error; } }; // NEW: Get all users from /users/api/v1/user endpoint -const getAllUsersFromAPI = async (): Promise => { - console.log('=== getAllUsersFromAPI ==='); - console.log('Fetching from:', `${nodeUrl}/users/api/v1/user`); +// Returns raw API data - mapping to User type is done in getUsers() +const getAllUsersFromAPI = async (): Promise => { + // Check cache first + if (isCacheValid(apiCache.rawUsers, CACHE_DURATION_USERS_MS)) { + logger.debug('[Cache HIT] getAllUsersFromAPI'); + return apiCache.rawUsers.data; + } - try { - const accessToken = await getAccessToken(`${userName}`, `${password}`); + // Check if there's already a pending request + if (pendingUsersRequest) { + logger.debug('[Dedup] Reusing pending getAllUsersFromAPI request'); + return pendingUsersRequest; + } - const response = await fetch(`${nodeUrl}/users/api/v1/user`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }); + logger.debug('[Cache MISS] Fetching users from API'); - 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})`, - ); - } + // Create new request and store it to prevent duplicates + pendingUsersRequest = (async (): Promise => { + try { + const accessToken = await getAccessToken(`${userName}`, `${password}`); - const responseData = await response.json(); - console.log('Total users retrieved:', responseData?.data?.length || 0); - console.log('All Users:', responseData); - console.log('==========================='); + const response = await fetch(`${nodeUrl}/users/api/v1/user`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }); - // 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; - } + if (!response.ok) { + logger.error(`getAllUsersFromAPI failed with status: ${response.status}`); + throw new Error('Failed to fetch users'); + } + + const responseData = await response.json(); + const users = responseData?.data || []; + const result: RawApiUser[] = Array.isArray(users) ? users : []; + + logger.debug(`Fetched ${result.length} users from API`); + + // Cache the result with proper type + apiCache.rawUsers = { + data: result, + timestamp: Date.now(), + }; + + return result; + } catch (error) { + logger.error('Error in getAllUsersFromAPI:', error); + throw error; + } finally { + pendingUsersRequest = null; + } + })(); + + return pendingUsersRequest; }; // NEW: Get wallets paginated for a specific user @@ -1077,7 +1198,7 @@ const getWalletsPaginated = async ( url.searchParams.append('offset', offset.toString()); url.searchParams.append('user_id', userId); - console.log('>>> Full URL with params:', url.toString()); + logger.debug('>>> Full URL with params:', url.toString()); const response = await fetch(url.toString(), { method: 'GET', @@ -1088,24 +1209,24 @@ const getWalletsPaginated = async ( }); if (!response.ok) { - console.error('Response status:', response.status); - console.error('Response statusText:', response); + logger.error('Response status:', response.status); + logger.error('Response statusText:', response); throw new Error( `Error getting wallets for user ${userId} (status: ${response.status})`, ); } const responseData = await response.json(); - console.log(`>>> Raw response for user ${userId}:`, responseData); + logger.debug(`>>> 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`); + logger.debug(`>>> 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}:`); + logger.debug(`>>> 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 ? '✓' : '✗'}`); + logger.debug(` 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 @@ -1129,13 +1250,13 @@ 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('==========================='); + logger.debug(`>>> Filtered wallets count for user ${userId}:`, filteredWallets.length); + logger.debug(`>>> Wallet IDs: [${filteredWallets.map(w => w.id).join(', ')}]`); + logger.debug('==========================='); return filteredWallets; } catch (error) { - console.error(`Error in getWalletsPaginated for user ${userId}:`, error); + logger.error(`Error in getWalletsPaginated for user ${userId}:`, error); throw error; } };