Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 100 additions & 29 deletions src/app/components/StampListSection.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -180,7 +182,7 @@ const StampListSection: React.FC<StampListSectionProps> = ({
}
};

const markStampAsExpired = (batchId: string, stampTimestamp?: number) => {
const markStampAsExpired = useCallback((batchId: string, stampTimestamp?: number) => {
try {
const cache = getExpiredStampsCache();
const now = Date.now();
Expand Down Expand Up @@ -220,7 +222,7 @@ const StampListSection: React.FC<StampListSectionProps> = ({
} catch (error) {
console.warn('Error updating expired stamps cache:', error);
}
};
}, []);

useEffect(() => {
const isStampKnownExpired = (batchId: string): boolean => {
Expand Down Expand Up @@ -330,6 +332,7 @@ const StampListSection: React.FC<StampListSectionProps> = ({
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) {
Expand All @@ -349,6 +352,8 @@ const StampListSection: React.FC<StampListSectionProps> = ({
batchTTL: 30 * 24 * 60 * 60, // Assume 30 days default
bucketDepth: 16, // Standard bucket depth
isPropagating: true, // Flag to show propagation message
payer,
owner: address,
};
}

Expand All @@ -366,6 +371,8 @@ const StampListSection: React.FC<StampListSectionProps> = ({
batchTTL: stampInfo.batchTTL,
bucketDepth: stampInfo.bucketDepth,
isPropagating: false, // Not propagating, we have real data
payer,
owner: address,
};
});

Expand All @@ -392,7 +399,7 @@ const StampListSection: React.FC<StampListSectionProps> = ({
};

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) => {
Expand Down Expand Up @@ -465,30 +472,93 @@ const StampListSection: React.FC<StampListSectionProps> = ({
<div className={styles.stampListEmpty}>No stamps found</div>
) : (
<>
{stamps.map((stamp, index) => (
<div key={index} className={styles.stampListItem}>
<div
className={styles.stampListId}
onClick={() => {
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}
</div>
{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 (
<div key={index} className={styles.stampListItem}>
<div className={styles.stampIdRow}>
<div
className={styles.stampListId}
onClick={() => {
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}
</div>
{ownerToDisplay && (
<div
className={styles.stampOwner}
onClick={() => {
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)}
</div>
)}
</div>
<div className={styles.stampListDetails}>
<span>Paid: {Number(stamp.totalAmount).toFixed(2)} BZZ</span>
<span>Size: {stamp.size}</span>
Expand Down Expand Up @@ -608,7 +678,8 @@ const StampListSection: React.FC<StampListSectionProps> = ({
</button>
</div>
</div>
))}
);
})}
</>
)}

Expand Down
104 changes: 93 additions & 11 deletions src/app/components/SwapComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -50,6 +51,8 @@ import {
// handleExchangeRateUpdate removed - was only used by LiFi
fetchCurrentPriceFromOracle,
fetchStampInfo,
fetchBatchInfoFromContract,
fetchBatchOwnerInfo,
formatExpiryTime,
isExpiringSoon,
getStampUsage,
Expand Down Expand Up @@ -227,6 +230,18 @@ const SwapComponent: React.FC = () => {
// Add state for original stamp info (used in top-ups)
const [originalStampInfo, setOriginalStampInfo] = useState<StampInfo | null>(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);

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -673,6 +700,7 @@ const SwapComponent: React.FC = () => {
originalStampInfo,
selectedDepth,
checkBzzApproval,
calculateTopUpAmount,
]);

useEffect(() => {
Expand Down Expand Up @@ -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);
Expand All @@ -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(() => {
Expand Down Expand Up @@ -1654,6 +1718,22 @@ const SwapComponent: React.FC = () => {

{!showHelp && !showStampList && !showUploadHistory ? (
<>
{isTopUp && contractBatchInfo && (
<div className={styles.inputGroup}>
<label className={styles.label}>
Stamp expiry:
</label>
<TTLDisplay
ttlSeconds={contractBatchInfo.ttlSeconds}
stampValue={contractBatchInfo.remainingBalance}
stampId={topUpBatchId || undefined}
owner={contractBatchInfo.owner}
payer={contractBatchInfo.payer}
isFlashing={isTTLFlashing}
/>
</div>
)}

<div className={styles.inputGroup}>
<label className={styles.label} data-tooltip="Select chain with funds">
From chain
Expand Down Expand Up @@ -2563,8 +2643,10 @@ const SwapComponent: React.FC = () => {
if (typeof window !== 'undefined') {
const url = new URL(window.location.href);
if (url.searchParams.has('topup')) {
// Remove the topup parameter and navigate to clean URL
window.location.href = window.location.origin;
// Get the topup batch ID to preserve in URL
const topupParam = url.searchParams.get('topup');
// Navigate with topup parameter preserved
window.location.href = `${window.location.origin}/?topup=${topupParam}`;
}
}
}}
Expand Down
Loading