diff --git a/floethereum/index.html b/floethereum/index.html
index 44a909a..f655047 100644
--- a/floethereum/index.html
+++ b/floethereum/index.html
@@ -822,56 +822,71 @@
loadTransactionsPage(ethAddress, floAddress, currentPage);
}
+ let allTransactionsCache = [];
+ let currentBalances = { eth: '0', usdc: '0', usdt: '0' };
+
async function loadTransactionsPage(ethAddress, floAddress, page) {
buttonLoader('check_balance_button', true);
try {
- const results = await Promise.allSettled([
- ethOperator.getBalance(ethAddress),
- ethOperator.getTokenBalance(ethAddress, 'usdc'),
- ethOperator.getTokenBalance(ethAddress, 'usdt'),
- ethOperator.getTransactionHistory(ethAddress, {
- page: page,
- offset: TRANSACTIONS_PER_PAGE,
- sort: 'desc'
+ let transactions = [];
+ let etherBalance = '0', usdcBalance = '0', usdtBalance = '0';
+
+ // Fetch new data if it's the first page (Search or Refresh)
+ if (page === 1) {
+ const results = await Promise.allSettled([
+ ethOperator.getBalance(ethAddress),
+ ethOperator.getTokenBalance(ethAddress, 'usdc'),
+ ethOperator.getTokenBalance(ethAddress, 'usdt'),
+ ethOperator.getTransactionHistory(ethAddress, {
+ page: 1, // Always Page 1 of API
+ offset: 500, // Batch size 500
+ sort: 'desc'
+ })
+ ]);
+
+ etherBalance = results[0].status === 'fulfilled' ? results[0].value : '0';
+ usdcBalance = results[1].status === 'fulfilled' ? results[1].value : '0';
+ usdtBalance = results[2].status === 'fulfilled' ? results[2].value : '0';
+ transactions = results[3].status === 'fulfilled' ? results[3].value : [];
+
+ // Update Cache
+ allTransactionsCache = transactions;
+ currentBalances = { eth: etherBalance, usdc: usdcBalance, usdt: usdtBalance };
+
+ // Log warnings
+ if (results[0].status === 'rejected') console.warn('Failed to fetch ETH balance:', results[0].reason);
+ if (results[1].status === 'rejected') console.warn('Failed to fetch USDC balance:', results[1].reason);
+ if (results[2].status === 'rejected') console.warn('Failed to fetch USDT balance:', results[2].reason);
+ if (results[3].status === 'rejected') console.warn('Failed to fetch transaction history:', results[3].reason);
+
+ } else {
+ // Use Cache for pagination
+ transactions = allTransactionsCache;
+ ({ eth: etherBalance, usdc: usdcBalance, usdt: usdtBalance } = currentBalances);
+ }
+
+ // Local Pagination / Slicing
+ const startIndex = (page - 1) * TRANSACTIONS_PER_PAGE;
+ const endIndex = startIndex + TRANSACTIONS_PER_PAGE;
+ const paginatedTransactions = transactions.slice(startIndex, endIndex);
+ const hasNextPage = transactions.length > endIndex;
+
+ // Sync Contacts (only on fresh load really, but safe here)
+ if (page === 1) {
+ compactIDB.readData('contacts', floAddress || ethAddress).then(result => {
+ if (result) return
+ compactIDB.addData('contacts', {
+ ethAddress,
+ }, floAddress || ethAddress).then(() => {
+ renderSearchedAddressList()
+ }).catch((error) => {
+ console.error(error)
+ })
})
- ]);
-
- // Extract balance and transaction data, using defaults if any request failed
- const etherBalance = results[0].status === 'fulfilled' ? results[0].value : '0';
- const usdcBalance = results[1].status === 'fulfilled' ? results[1].value : '0';
- const usdtBalance = results[2].status === 'fulfilled' ? results[2].value : '0';
- const transactions = results[3].status === 'fulfilled' ? results[3].value : [];
-
- // Store transactions for filtering
- allTransactions = transactions;
-
- // Log warnings if any API requests failed
- if (results[0].status === 'rejected') {
- console.warn('Failed to fetch ETH balance:', results[0].reason);
- }
- if (results[1].status === 'rejected') {
- console.warn('Failed to fetch USDC balance:', results[1].reason);
- }
- if (results[2].status === 'rejected') {
- console.warn('Failed to fetch USDT balance:', results[2].reason);
- }
- if (results[3].status === 'rejected') {
- console.warn('Failed to fetch transaction history:', results[3].reason);
}
- compactIDB.readData('contacts', floAddress || ethAddress).then(result => {
- if (result) return
- compactIDB.addData('contacts', {
- ethAddress,
- }, floAddress || ethAddress).then(() => {
- renderSearchedAddressList()
- }).catch((error) => {
- console.error(error)
- })
- })
-
- renderBalanceAndTransactions(ethAddress, floAddress, etherBalance, usdcBalance, usdtBalance, transactions, page);
+ renderBalanceAndTransactions(ethAddress, floAddress, etherBalance, usdcBalance, usdtBalance, paginatedTransactions, page, hasNextPage);
} catch (error) {
notify(error.message || error, 'error');
@@ -880,7 +895,7 @@
}
}
- function renderBalanceAndTransactions(ethAddress, floAddress, etherBalance, usdcBalance, usdtBalance, transactions, page) {
+ function renderBalanceAndTransactions(ethAddress, floAddress, etherBalance, usdcBalance, usdtBalance, transactions, page, hasNextPage) {
// Update URL to reflect the current address being viewed
const url = new URL(window.location);
url.searchParams.set('address', ethAddress);
@@ -890,7 +905,7 @@
window.history.pushState({}, '', url.pathname + url.search + url.hash);
// Determine if pagination buttons should be enabled
- const hasNextPage = transactions.length >= TRANSACTIONS_PER_PAGE;
+ // hasNextPage is now passed in
const hasPrevPage = page > 1;
renderElem(getRef('eth_balance_wrapper'), html`
@@ -1124,14 +1139,32 @@
- ${txDetails.tokenTransfer ? html`
+ ${txDetails.tokenTransfers && txDetails.tokenTransfers.length > 0 ? html`
-
Token Transfer
-
${txDetails.tokenTransfer.value} ${txDetails.tokenTransfer.symbol}
+
Token Transfers
+
+ ${txDetails.tokenTransfers.map((transfer, index) => html`
+
+
+ ${transfer.value} ${transfer.symbol}
+
+
+
+ From
+
+
+
+ To
+
+
+
+
+ `)}
+
` : html`
diff --git a/floethereum/scripts/ethOperator.js b/floethereum/scripts/ethOperator.js
index 6c2302e..f633539 100644
--- a/floethereum/scripts/ethOperator.js
+++ b/floethereum/scripts/ethOperator.js
@@ -257,7 +257,7 @@
try {
if (!address || !isValidAddress(address))
return new Error('Invalid address');
-
+
// Use read-only provider (public RPC) for balance checks
const provider = getProvider(true);
const balanceWei = await provider.getBalance(address);
@@ -274,7 +274,7 @@
return new Error("Token not specified");
if (!CONTRACT_ADDRESSES[token] && contractAddress)
return new Error('Contract address of token not available')
-
+
// Use read-only provider (public RPC) for token balance checks
const provider = getProvider(true);
const tokenAddress = CONTRACT_ADDRESSES[token] || contractAddress;
@@ -307,17 +307,17 @@
const provider = getProvider();
const signer = new ethers.Wallet(privateKey, provider);
const limit = await estimateGas({ privateKey, receiver, amount })
-
+
// Get current fee data from the network
const feeData = await provider.getFeeData();
-
+
// Calculate priority fee (tip to miners) - use 1.5 gwei or the network's suggested priority fee, whichever is higher
const priorityFee = feeData.maxPriorityFeePerGas || ethers.utils.parseUnits("1.5", "gwei");
-
+
// Calculate max fee per gas (base fee + priority fee)
// Use the network's suggested maxFeePerGas or calculate it manually
let maxFee = feeData.maxFeePerGas;
-
+
// If maxFeePerGas is not available or is less than priority fee, calculate it
if (!maxFee || maxFee.lt(priorityFee)) {
// Get the base fee from the latest block and add our priority fee
@@ -326,13 +326,13 @@
// maxFee = (baseFee * 2) + priorityFee to account for potential base fee increases
maxFee = baseFee.mul(2).add(priorityFee);
}
-
+
// Ensure maxFee is at least 1.5x the priority fee for safety
const minMaxFee = priorityFee.mul(15).div(10); // 1.5x priority fee
if (maxFee.lt(minMaxFee)) {
maxFee = minMaxFee;
}
-
+
// Creating and sending the transaction object
return signer.sendTransaction({
to: receiver,
@@ -365,8 +365,8 @@
return tokenContract.transfer(receiver, amountWei)
}
-
- const ETHERSCAN_API_KEY = 'M3YBAHI21FVE7VS2FEKU6ZFGRA128WUVQK';
+
+ const ETHERSCAN_API_KEY = 'M3YBAHI21FVE7VS2FEKU6ZFGRA128WUVQK';
/**
* Get transaction history for an Ethereum address
@@ -390,7 +390,7 @@
// Fetch normal transactions using V2 API
const normalTxUrl = `https://api.etherscan.io/v2/api?chainid=1&module=account&action=txlist&address=${address}&startblock=${startBlock}&endblock=${endBlock}&page=${page}&offset=${offset}&sort=${sort}&apikey=${ETHERSCAN_API_KEY}`;
-
+
const normalTxResponse = await fetch(normalTxUrl);
const normalTxData = await normalTxResponse.json();
@@ -410,15 +410,25 @@
// Fetch ERC20 token transfers using V2 API
const tokenTxUrl = `https://api.etherscan.io/v2/api?chainid=1&module=account&action=tokentx&address=${address}&startblock=${startBlock}&endblock=${endBlock}&page=${page}&offset=${offset}&sort=${sort}&apikey=${ETHERSCAN_API_KEY}`;
-
+
const tokenTxResponse = await fetch(tokenTxUrl);
const tokenTxData = await tokenTxResponse.json();
const tokenTransfers = tokenTxData.status === '1' ? tokenTxData.result : [];
// Combine and sort transactions
- const allTransactions = [...normalTxData.result, ...tokenTransfers];
-
+ // Filter out normal transactions that are already present in token transfers (duplicate hash) AND have 0 value
+ // This prevents showing "0 ETH to Contract" alongside the actual "Token Transfer"
+ const tokenTxHashes = new Set(tokenTransfers.map(tx => tx.hash));
+ const uniqueNormalTxs = normalTxData.result.filter(tx => {
+ if (tokenTxHashes.has(tx.hash) && tx.value === '0') {
+ return false;
+ }
+ return true;
+ });
+
+ const allTransactions = [...uniqueNormalTxs, ...tokenTransfers];
+
// Sort by timestamp (descending)
allTransactions.sort((a, b) => parseInt(b.timeStamp) - parseInt(a.timeStamp));
@@ -427,9 +437,9 @@
return allTransactions.map(tx => {
const isTokenTransfer = tx.tokenSymbol !== undefined;
const isReceived = tx.to.toLowerCase() === address.toLowerCase();
-
+
let value, symbol, decimals;
-
+
if (isTokenTransfer) {
decimals = parseInt(tx.tokenDecimal) || 18;
value = parseFloat(ethers.utils.formatUnits(tx.value, decimals));
@@ -480,57 +490,75 @@
// Use read-only provider for fetching transaction details
const provider = getProvider(true);
-
+
// Get transaction details
const tx = await provider.getTransaction(txHash);
-
+
if (!tx) {
throw new Error('Transaction not found');
}
// Get transaction receipt for status and gas used
const receipt = await provider.getTransactionReceipt(txHash);
-
+
// Get current block number for confirmations
const currentBlock = await provider.getBlockNumber();
-
+
// Get block details for timestamp
const block = await provider.getBlock(tx.blockNumber);
// Calculate gas fee
const gasUsed = receipt ? receipt.gasUsed : null;
const effectiveGasPrice = receipt ? receipt.effectiveGasPrice : tx.gasPrice;
- const gasFee = gasUsed && effectiveGasPrice ?
+ const gasFee = gasUsed && effectiveGasPrice ?
parseFloat(ethers.utils.formatEther(gasUsed.mul(effectiveGasPrice))) : null;
// Check if it's a token transfer by examining logs
- let tokenTransfer = null;
+ let tokenTransfers = [];
+ // Simple in-memory cache for token metadata to avoid rate limiting
+ const TOKEN_METADATA_CACHE = ethOperator.TOKEN_METADATA_CACHE || {};
+ ethOperator.TOKEN_METADATA_CACHE = TOKEN_METADATA_CACHE;
+
if (receipt && receipt.logs.length > 0) {
// Try to decode ERC20 Transfer event
const transferEventSignature = ethers.utils.id('Transfer(address,address,uint256)');
- const transferLog = receipt.logs.find(log => log.topics[0] === transferEventSignature);
-
- if (transferLog) {
+ const transferLogs = receipt.logs.filter(log => log.topics[0] === transferEventSignature);
+
+ if (transferLogs.length > 0) {
try {
- const tokenContract = new ethers.Contract(transferLog.address, ERC20ABI, provider);
- const [symbol, decimals] = await Promise.all([
- tokenContract.symbol().catch(() => 'TOKEN'),
- tokenContract.decimals().catch(() => 18)
- ]);
-
- const from = ethers.utils.getAddress('0x' + transferLog.topics[1].slice(26));
- const to = ethers.utils.getAddress('0x' + transferLog.topics[2].slice(26));
- const value = parseFloat(ethers.utils.formatUnits(transferLog.data, decimals));
-
- tokenTransfer = {
- from,
- to,
- value,
- symbol,
- contractAddress: transferLog.address
- };
+ // Process all transfer logs
+ tokenTransfers = await Promise.all(transferLogs.map(async (transferLog) => {
+ const contractAddress = transferLog.address;
+ let symbol, decimals;
+
+ // Check cache first
+ if (TOKEN_METADATA_CACHE[contractAddress]) {
+ ({ symbol, decimals } = TOKEN_METADATA_CACHE[contractAddress]);
+ } else {
+ // Fetch from network if not cached
+ const tokenContract = new ethers.Contract(contractAddress, ERC20ABI, provider);
+ [symbol, decimals] = await Promise.all([
+ tokenContract.symbol().catch(() => 'TOKEN'),
+ tokenContract.decimals().catch(() => 18)
+ ]);
+ // Store in cache
+ TOKEN_METADATA_CACHE[contractAddress] = { symbol, decimals };
+ }
+
+ const from = ethers.utils.getAddress('0x' + transferLog.topics[1].slice(26));
+ const to = ethers.utils.getAddress('0x' + transferLog.topics[2].slice(26));
+ const value = parseFloat(ethers.utils.formatUnits(transferLog.data, decimals));
+
+ return {
+ from,
+ to,
+ value,
+ symbol,
+ contractAddress
+ };
+ }));
} catch (e) {
- console.warn('Could not decode token transfer:', e);
+ console.warn('Could not decode token transfers:', e);
}
}
}
@@ -552,7 +580,7 @@
input: tx.data,
status: receipt ? (receipt.status === 1 ? 'success' : 'failed') : 'pending',
isError: receipt ? receipt.status !== 1 : false,
- tokenTransfer: tokenTransfer,
+ tokenTransfers: tokenTransfers,
logs: receipt ? receipt.logs : [],
type: tx.type
};