Skip to content
74 changes: 50 additions & 24 deletions tabs/src/components/FeedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,16 +90,18 @@ const FeedList: React.FC<FeedListProps> = ({ 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<string>();
Expand All @@ -92,7 +110,7 @@ const FeedList: React.FC<FeedListProps> = ({ 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)
);
Expand Down Expand Up @@ -296,11 +314,25 @@ const FeedList: React.FC<FeedListProps> = ({ 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));
Expand Down Expand Up @@ -359,15 +391,9 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
<div className={styles.personDetails}>
<div className={styles.userName}>
{(() => {
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', {
Expand Down Expand Up @@ -421,7 +447,7 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
) : (
<div>No data available</div>
)}
{sortedZaps.length > ITEMS_PER_PAGE && (
{filteredZaps.length > ITEMS_PER_PAGE && (
<div className={styles.pagination}>
<button
onClick={firstPage}
Expand Down Expand Up @@ -454,9 +480,9 @@ const FeedList: React.FC<FeedListProps> = ({ timestamp }) => {
>
&#187; {/* Double right arrow */}
</button>
</div>
</div>
)}
</div>
);
};
export default FeedList;
export default FeedList;
33 changes: 21 additions & 12 deletions tabs/src/components/WalletAllowanceComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -62,18 +67,22 @@ const WalletAllowanceCard: React.FC<AllowanceCardProps> = () => {
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);
}
}
}
}
Expand Down
106 changes: 51 additions & 55 deletions tabs/src/components/WalletTransactionLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<WalletTransactionLogProps> = ({
activeTab,
activeWallet,
}) => {
const [allTransactions, setAllTransactions] = useState<Transaction[]>([]); // Cache all transactions
const [displayedTransactions, setDisplayedTransactions] = useState<Transaction[]>([]); // Filtered transactions to display
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentWallet, setCurrentWallet] = useState<string | undefined>(undefined); // Track which wallet data is cached for
Expand All @@ -34,11 +38,10 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({

// 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];

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

const currentUserLNbitDetails = await getUsers(adminKey, {
aadObjectId: account.localAccountId,
Expand All @@ -65,50 +65,48 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
// 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<string, User>();
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
Expand Down Expand Up @@ -214,13 +212,19 @@ const WalletTransactionLog: React.FC<WalletTransactionLogProps> = ({
}
};

// 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(() => {
Expand Down Expand Up @@ -255,14 +259,6 @@ const rewardsName = rewardNameContext.rewardName;
return <div>{error}</div>;
}

if (loading) {
return <div>Loading...</div>;
}

if (error) {
return <div>{error}</div>;
}



return (
Expand Down
Loading
Loading