diff --git a/src/app/components/StampListSection.tsx b/src/app/components/StampListSection.tsx index 6e13dfe..38ee1b2 100644 --- a/src/app/components/StampListSection.tsx +++ b/src/app/components/StampListSection.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import styles from './css/StampListSection.module.css'; import { formatUnits } from 'viem'; import { UploadStep } from './types'; @@ -58,6 +58,8 @@ interface BatchEvent { batchTTL?: number; bucketDepth?: number; isPropagating?: boolean; // Flag to indicate stamp is still propagating on network + payer?: string; // The payer address (actual buyer if bought through proxy) + owner?: string; // The owner address (from the registry query) } interface StampInfo { @@ -180,7 +182,7 @@ const StampListSection: React.FC = ({ } }; - const markStampAsExpired = (batchId: string, stampTimestamp?: number) => { + const markStampAsExpired = useCallback((batchId: string, stampTimestamp?: number) => { try { const cache = getExpiredStampsCache(); const now = Date.now(); @@ -220,7 +222,7 @@ const StampListSection: React.FC = ({ } catch (error) { console.warn('Error updating expired stamps cache:', error); } - }; + }, []); useEffect(() => { const isStampKnownExpired = (batchId: string): boolean => { @@ -330,6 +332,7 @@ const StampListSection: React.FC = ({ const stampInfo = await fetchStampInfo(batchId, stampTimestamp); const depth = Number(contractBatch.depth); + const payer = contractBatch.payer?.toString(); // If no stamp info, determine if it's propagating or expired if (!stampInfo) { @@ -349,6 +352,8 @@ const StampListSection: React.FC = ({ batchTTL: 30 * 24 * 60 * 60, // Assume 30 days default bucketDepth: 16, // Standard bucket depth isPropagating: true, // Flag to show propagation message + payer, + owner: address, }; } @@ -366,6 +371,8 @@ const StampListSection: React.FC = ({ batchTTL: stampInfo.batchTTL, bucketDepth: stampInfo.bucketDepth, isPropagating: false, // Not propagating, we have real data + payer, + owner: address, }; }); @@ -392,7 +399,7 @@ const StampListSection: React.FC = ({ }; fetchStamps(); - }, [address, beeApiUrl]); // Only dependencies that actually need to trigger re-fetching + }, [address, beeApiUrl, markStampAsExpired]); // Only dependencies that actually need to trigger re-fetching // Function to refresh a specific stamp const refreshSingleStamp = async (stampToRefresh: BatchEvent) => { @@ -465,30 +472,93 @@ const StampListSection: React.FC = ({
No stamps found
) : ( <> - {stamps.map((stamp, index) => ( -
-
{ - const idToCopy = stamp.batchId.startsWith('0x') - ? stamp.batchId.slice(2) - : stamp.batchId; - navigator.clipboard.writeText(idToCopy); - // Show temporary "Copied!" message - const element = document.querySelector(`[data-stamp-id="${stamp.batchId}"]`); - if (element) { - element.setAttribute('data-copied', 'true'); - setTimeout(() => { - element.setAttribute('data-copied', 'false'); - }, 2000); - } - }} - data-stamp-id={stamp.batchId} - data-copied="false" - title="Click to copy stamp ID" - > - ID: {stamp.batchId.startsWith('0x') ? stamp.batchId.slice(2) : stamp.batchId} -
+ {stamps.map((stamp, index) => { + // Determine which address to show (payer if bought through proxy, otherwise owner) + const getOwnerToDisplay = () => { + if (!stamp.owner && !stamp.payer) return null; + // If owner and payer are different, it was bought through proxy - show payer + if ( + stamp.owner && + stamp.payer && + stamp.owner.toLowerCase() !== stamp.payer.toLowerCase() + ) { + return stamp.payer; + } + // Otherwise show owner (or payer if owner is not available) + return stamp.owner || stamp.payer || null; + }; + + // Determine the label for the owner field + const getOwnerLabel = () => { + if (!stamp.owner || !stamp.payer) return 'Owner'; + // If they're different, it was bought through proxy + if (stamp.owner.toLowerCase() !== stamp.payer.toLowerCase()) { + return 'Buyer'; + } + return 'Owner'; + }; + + // Format address with truncation (first6...last4) + const formatAddress = (address: string) => { + const cleanAddress = address.startsWith('0x') ? address.slice(2) : address; + if (cleanAddress.length <= 10) return cleanAddress; + return `${cleanAddress.slice(0, 6)}...${cleanAddress.slice(-4)}`; + }; + + const ownerToDisplay = getOwnerToDisplay(); + + return ( +
+
+
{ + const idToCopy = stamp.batchId.startsWith('0x') + ? stamp.batchId.slice(2) + : stamp.batchId; + navigator.clipboard.writeText(idToCopy); + // Show temporary "Copied!" message + const element = document.querySelector(`[data-stamp-id="${stamp.batchId}"]`); + if (element) { + element.setAttribute('data-copied', 'true'); + setTimeout(() => { + element.setAttribute('data-copied', 'false'); + }, 2000); + } + }} + data-stamp-id={stamp.batchId} + data-copied="false" + title="Click to copy stamp ID" + > + ID: {stamp.batchId.startsWith('0x') ? stamp.batchId.slice(2) : stamp.batchId} +
+ {ownerToDisplay && ( +
{ + const addressToCopy = ownerToDisplay.startsWith('0x') + ? ownerToDisplay + : `0x${ownerToDisplay}`; + navigator.clipboard.writeText(addressToCopy); + // Show temporary "Copied!" message + const element = document.querySelector( + `[data-owner-address="${stamp.batchId}"]` + ); + if (element) { + element.setAttribute('data-copied', 'true'); + setTimeout(() => { + element.setAttribute('data-copied', 'false'); + }, 2000); + } + }} + data-owner-address={stamp.batchId} + data-copied="false" + title="Click to copy owner address" + > + {getOwnerLabel()}: {formatAddress(ownerToDisplay)} +
+ )} +
Paid: {Number(stamp.totalAmount).toFixed(2)} BZZ Size: {stamp.size} @@ -608,7 +678,8 @@ const StampListSection: React.FC = ({
- ))} + ); + })} )} diff --git a/src/app/components/SwapComponent.tsx b/src/app/components/SwapComponent.tsx index 0e95985..ee58dbc 100644 --- a/src/app/components/SwapComponent.tsx +++ b/src/app/components/SwapComponent.tsx @@ -38,6 +38,7 @@ import SearchableChainDropdown from './SearchableChainDropdown'; import SearchableTokenDropdown from './SearchableTokenDropdown'; import StorageStampsDropdown from './StorageStampsDropdown'; import StorageDurationDropdown from './StorageDurationDropdown'; +import TTLDisplay from './TTLDisplay'; import { formatErrorMessage, @@ -50,6 +51,8 @@ import { // handleExchangeRateUpdate removed - was only used by LiFi fetchCurrentPriceFromOracle, fetchStampInfo, + fetchBatchInfoFromContract, + fetchBatchOwnerInfo, formatExpiryTime, isExpiringSoon, getStampUsage, @@ -227,6 +230,18 @@ const SwapComponent: React.FC = () => { // Add state for original stamp info (used in top-ups) const [originalStampInfo, setOriginalStampInfo] = useState(null); + // Add state for batch info from contract (TTL and remaining balance) + const [contractBatchInfo, setContractBatchInfo] = useState<{ + ttlSeconds: number; + remainingBalance: string; + depth: number; + owner?: string; + payer?: string; + } | null>(null); + + // Add state for TTL display flashing animation + const [isTTLFlashing, setIsTTLFlashing] = useState(false); + // Add a ref to track the current wallet client const currentWalletClientRef = useRef(walletClient); @@ -628,6 +643,18 @@ const SwapComponent: React.FC = () => { } }; + // Calculate amount for topping up an existing batch + const calculateTopUpAmount = useCallback((originalDepth: number) => { + if (currentPrice === null || !selectedDays) return 0n; + + // We use the original depth from the stamp, not the currently selected depth + const initialPaymentPerChunkPerDay = BigInt(currentPrice) * BigInt(17280); + const totalPricePerDuration = initialPaymentPerChunkPerDay * BigInt(selectedDays); + + // Calculate for the original batch depth + return totalPricePerDuration * BigInt(2 ** originalDepth); + }, [currentPrice, selectedDays]); + // Check approval status when relevant parameters change useEffect(() => { const checkApprovalStatus = async () => { @@ -673,6 +700,7 @@ const SwapComponent: React.FC = () => { originalStampInfo, selectedDepth, checkBzzApproval, + calculateTopUpAmount, ]); useEffect(() => { @@ -1554,6 +1582,7 @@ const SwapComponent: React.FC = () => { // Only fetch if we have a topUpBatchId and we're in top-up mode if (topUpBatchId && isTopUp) { const getStampInfo = async () => { + // Fetch from Bee API for depth info const stampInfo = await fetchStampInfoForComponent(topUpBatchId); if (stampInfo) { console.log('Fetched original stamp info:', stampInfo); @@ -1568,23 +1597,58 @@ const SwapComponent: React.FC = () => { swarmBatchDepth: stampInfo.depth.toString(), })); } + + // Fetch TTL and balance from contract + const batchInfo = await fetchBatchInfoFromContract(topUpBatchId); + if (batchInfo) { + // Fetch owner info from registry by querying events + const ownerInfo = await fetchBatchOwnerInfo(topUpBatchId); + + const combinedInfo = { + ...batchInfo, + owner: ownerInfo?.owner, + payer: ownerInfo?.payer, + }; + + setContractBatchInfo(combinedInfo); + } }; getStampInfo(); } }, [topUpBatchId, isTopUp, fetchStampInfoForComponent]); - // Calculate amount for topping up an existing batch - const calculateTopUpAmount = (originalDepth: number) => { - if (currentPrice === null || !selectedDays) return 0n; + // Add this effect to refresh stamp info periodically (every 60 seconds) + useEffect(() => { + if (!topUpBatchId || !isTopUp) return; + + const refreshStampInfo = async () => { + // Flash before refreshing + setIsTTLFlashing(true); + setTimeout(() => setIsTTLFlashing(false), 600); // Match flash animation duration + + // Fetch updated TTL and balance from contract + const batchInfo = await fetchBatchInfoFromContract(topUpBatchId); + if (batchInfo) { + // Fetch owner info from registry by querying events + const ownerInfo = await fetchBatchOwnerInfo(topUpBatchId); + + const combinedInfo = { + ...batchInfo, + owner: ownerInfo?.owner, + payer: ownerInfo?.payer, + }; - // We use the original depth from the stamp, not the currently selected depth - const initialPaymentPerChunkPerDay = BigInt(currentPrice) * BigInt(17280); - const totalPricePerDuration = initialPaymentPerChunkPerDay * BigInt(selectedDays); + setContractBatchInfo(combinedInfo); + } + }; - // Calculate for the original batch depth - return totalPricePerDuration * BigInt(2 ** originalDepth); - }; + // Set up interval to refresh every 60 seconds + const intervalId = setInterval(refreshStampInfo, 60000); + + // Clean up interval on unmount + return () => clearInterval(intervalId); + }, [topUpBatchId, isTopUp]); // Add useEffect to set hasMounted after component mounts useEffect(() => { @@ -1654,6 +1718,22 @@ const SwapComponent: React.FC = () => { {!showHelp && !showStampList && !showUploadHistory ? ( <> + {isTopUp && contractBatchInfo && ( +
+ + +
+ )} +