From e7ae64da27baab87d3f79dda05d60f7cab78707c Mon Sep 17 00:00:00 2001 From: aniket866 Date: Tue, 24 Feb 2026 19:17:44 +0530 Subject: [PATCH 1/3] fixing-minting-limit --- contracts/src/TNT.sol | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/src/TNT.sol b/contracts/src/TNT.sol index 1b78e48..d02bb5a 100644 --- a/contracts/src/TNT.sol +++ b/contracts/src/TNT.sol @@ -16,6 +16,7 @@ contract TNT is ERC721, AccessControl { error NotOwner(); error InvalidIndex(); error NonTransferable(); + error MaxSupplyReached(); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant REVOKER_ROLE = keccak256("REVOKER_ROLE"); @@ -28,6 +29,7 @@ contract TNT is ERC721, AccessControl { bool public immutable revokable; address public factoryContract; string public imageURL; + uint256 public maxSupply; struct TokenMetadata { uint256 issuedAt; @@ -54,8 +56,14 @@ contract TNT is ERC721, AccessControl { imageURL = _imageURL; } + function setMaxSupply(uint256 _maxSupply) external onlyRole(DEFAULT_ADMIN_ROLE) { + maxSupply = _maxSupply; + } + function issueToken(address user) public onlyRole(MINTER_ROLE) { if (user == address(0)) revert InvalidUser(); + + if (maxSupply > 0 && _nextTokenId >= maxSupply) revert MaxSupplyReached(); uint256 tokenId = _nextTokenId++; _safeMint(user, tokenId); From 29d6ad4aec66390725aa60669def10f1c9159c2d Mon Sep 17 00:00:00 2001 From: aniket866 Date: Sat, 21 Mar 2026 19:54:49 +0530 Subject: [PATCH 2/3] frontend-fix --- web/src/app/create/page.tsx | 103 +++++++++++++++-------- web/src/app/token-actions/page.tsx | 43 +++++++++- web/src/utils/contractsABI/TNT.ts | 23 +++++ web/src/utils/contractsABI/TNTFactory.ts | 5 ++ web/src/utils/roleManager.ts | 11 ++- web/tsconfig.json | 2 +- 6 files changed, 148 insertions(+), 39 deletions(-) diff --git a/web/src/app/create/page.tsx b/web/src/app/create/page.tsx index 86037a3..7546e40 100644 --- a/web/src/app/create/page.tsx +++ b/web/src/app/create/page.tsx @@ -14,14 +14,19 @@ import { TNTFactoryAbi } from "@/utils/contractsABI/TNTFactory"; import { Info } from "lucide-react"; import { useTheme } from "next-themes"; import { getPublicClient } from "@wagmi/core"; +import { decodeEventLog } from "viem"; interface DeployContractProps { tokenName: string; tokenSymbol: string; revokable: boolean; imageURL: string; + maxMintCap: string; } +const TNTCREATED_EVENT_NOT_FOUND = + "TNTCreated event not found in transaction logs"; + const fields = [ { id: "tokenName", @@ -44,6 +49,13 @@ const fields = [ placeholder: "https://example.com/image.png", description: "Image to associate with this TNT", }, + { + id: "maxMintCap", + label: "Max Mint Cap", + type: "number", + placeholder: "e.g. 1000", + description: "Maximum number of tokens that can ever be minted", + }, ]; export default function CreateTNT() { @@ -52,6 +64,7 @@ export default function CreateTNT() { tokenSymbol: "", revokable: false, imageURL: "", + maxMintCap: "", }); const [isDeploying, setIsDeploying] = useState(false); @@ -78,9 +91,21 @@ export default function CreateTNT() { }; const saveTransaction = (txDetails: object) => { - const history = getTransactionHistory(); - history.push(txDetails); - localStorage.setItem("transactionHistory", JSON.stringify(history)); + try { + const history = getTransactionHistory(); + history.push(txDetails); + localStorage.setItem("transactionHistory", JSON.stringify(history)); + } catch (error) { + // Handle QuotaExceededError or security exceptions (e.g., storage disabled) + console.error("Failed to save transaction to localStorage:", error); + + if (error instanceof DOMException && + (error.name === 'QuotaExceededError' || error.name === 'NS_ERROR_DOM_QUOTA_REACHED')) { + toast.error("Local storage is full. Transaction history might not be saved."); + } else { + toast.error("An error occurred while saving transaction data."); + } + } }; const deployContract = async () => { @@ -105,7 +130,7 @@ export default function CreateTNT() { return; } - const { tokenName, tokenSymbol, revokable, imageURL } = formData; + const { tokenName, tokenSymbol, revokable, imageURL, maxMintCap } = formData; // Validate inputs if (!tokenName.trim()) { @@ -118,6 +143,11 @@ export default function CreateTNT() { return; } + if (!maxMintCap || Number(maxMintCap) <= 0) { + toast.error("Max Mint Cap must be a positive number"); + return; + } + const factoryAddress = TNTVaultFactories[chainId]; const cleanImageURL = imageURL.trim() || ""; @@ -127,6 +157,7 @@ export default function CreateTNT() { symbol: tokenSymbol.trim(), revokable, imageURL: cleanImageURL, + maxMintCap, chainId, userAddress: address, }); @@ -137,7 +168,7 @@ export default function CreateTNT() { address: factoryAddress, abi: TNTFactoryAbi, functionName: "createTNT", - args: [tokenName.trim(), tokenSymbol.trim(), revokable, cleanImageURL], + args: [tokenName.trim(), tokenSymbol.trim(), revokable, cleanImageURL, BigInt(maxMintCap)], chainId: chainId, }); @@ -157,38 +188,33 @@ export default function CreateTNT() { console.log("Transaction confirmed! Receipt:", receipt); if (receipt.status === "success") { - // Extract the new TNT contract address from logs - let newTNTAddress = null; - - // Look for TNTCreated event in logs - try { - const publicClient = getPublicClient(config as any, { chainId }); - if (publicClient) { - // Decode logs to find the TNTCreated event - for (const log of receipt.logs) { - if ( - log.address.toLowerCase() === factoryAddress.toLowerCase() - ) { - // This is likely our TNTCreated event - // The first topic after the event signature should be the owner - // The data should contain the TNT address - if (log.topics.length > 1 && log.data) { - // Try to decode the TNT address from the log data - // For now, we'll use a placeholder - in a real implementation, - // you'd properly decode the event logs - console.log("TNT created successfully, log:", log); - newTNTAddress = log.data; // This is a simplified approach - } - } + // Extract the new TNT contract address from TNTCreated event + let newTNTAddress: `0x${string}` | null = null; + + for (const log of receipt.logs) { + if (log.address.toLowerCase() !== factoryAddress.toLowerCase()) + continue; + try { + const decoded = decodeEventLog({ + abi: TNTFactoryAbi, + data: log.data, + topics: log.topics, + }); + if ( + decoded.eventName === "TNTCreated" && + decoded.args && + "tntAddress" in decoded.args + ) { + newTNTAddress = decoded.args.tntAddress as `0x${string}`; + break; } + } catch { + continue; } - } catch (logError) { - console.warn("Could not decode TNT address from logs:", logError); } - // If we couldn't get the address from logs, create a placeholder if (!newTNTAddress) { - newTNTAddress = `0x${txHash.slice(2, 42)}`; // Use part of tx hash as placeholder + throw new Error(TNTCREATED_EVENT_NOT_FOUND); } const txDetails = { @@ -196,6 +222,7 @@ export default function CreateTNT() { tokenSymbol: tokenSymbol.trim(), revokable, imageURL: cleanImageURL, + maxMintCap, transactionHash: txHash, contractAddress: newTNTAddress, chainId, @@ -215,15 +242,21 @@ export default function CreateTNT() { throw new Error("Transaction failed"); } } catch (waitError) { + const isDecodingFailure = + waitError instanceof Error && + waitError.message === TNTCREATED_EVENT_NOT_FOUND; + if (isDecodingFailure) throw waitError; + console.error("Error waiting for transaction:", waitError); toast.error("Transaction confirmation failed. Please check manually."); - // Still save the transaction attempt + // Still save the transaction attempt (no contractAddress) const txDetails = { tokenName: tokenName.trim(), tokenSymbol: tokenSymbol.trim(), revokable, imageURL: cleanImageURL, + maxMintCap, transactionHash: txHash, chainId, factoryAddress, @@ -258,6 +291,10 @@ export default function CreateTNT() { toast.error( "Network error. Please check your connection and try again." ); + } else if (error?.message === TNTCREATED_EVENT_NOT_FOUND) { + toast.error( + "Transaction succeeded but the TNT address could not be read from the receipt. Check the block explorer for the contract address." + ); } else { toast.error( `Failed to deploy TNT: ${ diff --git a/web/src/app/token-actions/page.tsx b/web/src/app/token-actions/page.tsx index 61974d3..155b481 100644 --- a/web/src/app/token-actions/page.tsx +++ b/web/src/app/token-actions/page.tsx @@ -49,6 +49,10 @@ export default function TokenActionsPage() { }); const [isLoadingPermissions, setIsLoadingPermissions] = useState(true); + // mint cap state + const [mintCap, setMintCap] = useState(0n); + const [totalMinted, setTotalMinted] = useState(0n); + // Form states for each action const [issueRecipient, setIssueRecipient] = useState(""); const [revokeTokenId, setRevokeTokenId] = useState(""); @@ -112,6 +116,25 @@ export default function TokenActionsPage() { setPermissions(perms); setUserRoles(roles); setIsTokenRevokable(tokenInfo.revokable); + + // read mint cap and total minted from contract + const publicClient = getPublicClient(config as any, { chainId }); + if (publicClient) { + const [cap, minted] = await Promise.all([ + publicClient.readContract({ + address: contractAddress, + abi: TNTAbi, + functionName: "maxMintCap", + }) as Promise, + publicClient.readContract({ + address: contractAddress, + abi: TNTAbi, + functionName: "getAllParticipantsCount", + }) as Promise, + ]); + setMintCap(cap); + setTotalMinted(minted); + } } catch (error) { console.error("Error loading permissions:", error); } finally { @@ -257,6 +280,10 @@ export default function TokenActionsPage() { toast.error("You don't have permission to issue tokens"); return; } + if (mintCap > 0n && totalMinted >= mintCap) { + toast.error("Mint cap has been reached. No more tokens can be issued."); + return; + } try { setIsIssuing(true); const tx = await writeContract(config as any, { @@ -269,6 +296,7 @@ export default function TokenActionsPage() { toast.success("Token issued successfully!"); console.log("Issue token tx:", tx); setIssueRecipient(""); + setTotalMinted((prev) => prev + 1n); } catch (error) { console.error("Error issuing token:", error); toast.error("Failed to issue token"); @@ -568,6 +596,15 @@ export default function TokenActionsPage() { + {/* mint cap progress */} + {mintCap > 0n && ( +
+ Minted: {totalMinted.toString()} / {mintCap.toString()} + {mintCap > 0n && totalMinted >= mintCap && ( + Cap Reached! + )} +
+ )} 0n && totalMinted >= mintCap)} />