diff --git a/packages/foundry/contracts/Marketplace.sol b/packages/foundry/contracts/Marketplace.sol index 309ebcd..3973e50 100644 --- a/packages/foundry/contracts/Marketplace.sol +++ b/packages/foundry/contracts/Marketplace.sol @@ -7,251 +7,308 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract Marketplace is ReentrancyGuard, Ownable { - // * State - uint256 private listingIds; // Replaces the Counters.Counter - - enum Currency { - NativeToken, - USDCToken - } - - mapping(uint256 => Listing) public listings; - mapping(address => Royalties) public royalties; // Royalty information by collection address. - - uint256 private constant BPS = 10000; - address public immutable USDC; - - // * Structs - // Single NFT listing - struct Listing { - address nftContract; - uint256 nftId; - address payable seller; - uint256 price; - Currency payableCurrency; - bool isAuction; - uint256 date; - address highestBidder; - } - - // Royalty Info - struct Royalties { - address _nftContract; - address payoutAccount; - uint256 royaltyAmount; - bool exists; + // * State + uint256 private listingIds; // Replaces the Counters.Counter + + enum Currency { + NativeToken, + USDCToken + } + + mapping(uint256 => Listing) public listings; + mapping(address => Royalties) public royalties; // Royalty information by collection address. + + uint256 private constant BPS = 10000; + address public immutable USDC; + + // * Structs + // Single NFT listing + struct Listing { + address nftContract; + uint256 nftId; + address payable seller; + uint256 price; + Currency payableCurrency; + bool isAuction; + uint256 date; + address highestBidder; + } + + // Royalty Info + struct Royalties { + address _nftContract; + address payoutAccount; + uint256 royaltyAmount; + bool exists; + } + + // * Events + event ListingCreated( + uint256 listingId, + address indexed nftContract, + uint256 indexed nftId, + address seller, + uint256 price, + Currency payableCurrency, + bool isAuction, + uint256 date, + address highestBidder + ); + event Purchase( + uint256 indexed itemId, address buyer, Currency currency, uint256 price + ); + event UpdatedPrice(uint256 indexed itemId, address owner, uint256 price); + event NewBid(address buyer, Listing listing, uint256 newBid); + + // * Modifiers + modifier Owned(uint256 listingId) { + Listing memory item = listings[listingId]; + address ownerOfToken = IERC721(item.nftContract).ownerOf(item.nftId); + + require(ownerOfToken == msg.sender, "You don't own this NFT!"); + _; + } + + // * Constructor + constructor(address _usdcAddress) Ownable(msg.sender) { + USDC = _usdcAddress; + } + + // * Create Listing + function createListing( + address nftContract, + uint256 nftId, + uint256 price, + Currency payableCurrency, + bool isAuction, + uint256 biddingTime + ) public nonReentrant { + require(price > 0, "Price must be greater than 0."); + address nftOwner = IERC721(nftContract).ownerOf(nftId); + require(nftOwner == msg.sender, "You can't sell an NFT you don't own!"); + if (isAuction) { + require(biddingTime > 0, "Auctions must have a valid date"); } - // * Events - event ListingCreated( - uint256 listingId, - address indexed nftContract, - uint256 indexed nftId, - address seller, - uint256 price, - Currency payableCurrency, - bool isAuction, - uint256 date, - address highestBidder + listingIds++; // Increment listing ID + uint256 itemId = listingIds; + uint256 auctionDate = block.timestamp + biddingTime; + + listings[itemId] = Listing( + nftContract, + nftId, + payable(msg.sender), + price, + payableCurrency, + isAuction, + auctionDate, + payable(address(0)) ); - event Purchase(uint256 indexed itemId, address buyer, Currency currency, uint256 price); - event UpdatedPrice(uint256 indexed itemId, address owner, uint256 price); - event NewBid(address buyer, Listing listing, uint256 newBid); - - // * Modifiers - modifier Owned(uint256 listingId) { - Listing memory item = listings[listingId]; - address ownerOfToken = IERC721(item.nftContract).ownerOf(item.nftId); - - require(ownerOfToken == msg.sender, "You don't own this NFT!"); - _; - } - - // * Constructor - constructor(address _usdcAddress) Ownable(msg.sender) { - USDC = _usdcAddress; - } - - // * Create Listing - function createListing( - address nftContract, - uint256 nftId, - uint256 price, - Currency payableCurrency, - bool isAuction, - uint256 biddingTime - ) public nonReentrant { - require(price > 0, "Price must be greater than 0."); - address nftOwner = IERC721(nftContract).ownerOf(nftId); - require(nftOwner == msg.sender, "You can't sell an NFT you don't own!"); - if (isAuction) { - require(biddingTime > 0, "Auctions must have a valid date"); - } - - listingIds++; // Increment listing ID - uint256 itemId = listingIds; - uint256 auctionDate = block.timestamp + biddingTime; - - listings[itemId] = Listing( - nftContract, nftId, payable(msg.sender), price, payableCurrency, isAuction, auctionDate, payable(address(0)) - ); - emit ListingCreated( - itemId, nftContract, nftId, msg.sender, price, payableCurrency, isAuction, auctionDate, address(0) - ); - } - - // IERC20 buy function - function buy(uint256 listingId) public payable nonReentrant { - Listing memory item = listings[listingId]; - address contractAddress = item.nftContract; - address nftSeller = item.seller; - uint256 nftId = item.nftId; - uint256 price = item.price; - Currency payableCurrency = item.payableCurrency; - uint256 baseRoyalty = royalties[contractAddress].royaltyAmount; - address royaltyReceiver = royalties[contractAddress].payoutAccount; - require(msg.sender != nftSeller, "You cannot buy an NFT you are selling."); - require(contractAddress != address(0), "Listing does not exist."); - require(!item.isAuction, "This listing is designated for auction."); - - require(msg.value >= price, "Please submit asking price to complete purchase"); - - // Transfer NFT to the buyer - IERC721(contractAddress).safeTransferFrom(nftSeller, msg.sender, nftId); - // Transfer sale proceeds to seller, minus royalty - if (payableCurrency == Currency.USDCToken) { - bool usdcSuccess = IERC20(USDC).transferFrom(msg.sender, nftSeller, price - ((price * baseRoyalty) / BPS)); // price - royalty - // Transfer royalty to the royalty receiver - bool usdcRoyaltiesSuccess = - IERC20(USDC).transferFrom(msg.sender, royaltyReceiver, (price * baseRoyalty) / BPS); // just royalty - require(usdcSuccess && usdcRoyaltiesSuccess, "Failed to transfer USDC"); - } else { - (bool ethSuccess,) = payable(nftSeller).call{value: price - ((price * baseRoyalty) / BPS)}(""); - (bool ethRoyaltiesSuccess,) = payable(royaltyReceiver).call{value: (price * baseRoyalty) / BPS}(""); - require(ethSuccess && ethRoyaltiesSuccess, "Failed to transfer ETH"); - } - // Mixed payment - // Implement AggregatorV3Interface to convert the USD value to the native token equivalent value - - emit Purchase(listingId, msg.sender, item.payableCurrency, price); - } - - // * Get current listing price - function getPrice(uint256 listingId) public view returns (uint256) { - return listings[listingId].price; - } - - // * Update listing price, listing cannot be on auction. - function updatePrice(uint256 listingId, uint256 newPrice) public Owned(listingId) { - require(!listings[listingId].isAuction, "Auction starting prices cannot be updated"); - - // Set new price - listings[listingId].price = newPrice; - - emit UpdatedPrice(listingId, msg.sender, newPrice); - } - - // * New bid and return funds to previous bidder - function bid(uint256 listingId, uint256 amount) public payable nonReentrant { - Listing storage listing = listings[listingId]; - uint256 currentBid = listing.price; - address highestBidder = listing.highestBidder; - Currency payableCurrency = listing.payableCurrency; - require(msg.sender != listing.seller, "You cannot bid on an NFT you are selling."); - require(listing.isAuction, "This listing is not an auction"); - require(block.timestamp < listing.date, "Auction has already ended"); - require(amount > listing.price, "Bid must be greater than the previous bid."); - - if (payableCurrency == Currency.USDCToken) { - // Escrow payment in the marketplace contract - IERC20(USDC).transferFrom(msg.sender, address(this), amount); - // Transfer current bid back to previous bidder if applicable - if (listing.highestBidder != address(0)) { - IERC20(USDC).transfer(highestBidder, currentBid); - } - } else { - // Implement AggregatorV3Interface to convert the USD value to the native token equivalent value - } - // Update to new bid values - listing.highestBidder = msg.sender; - listing.price = amount; - - emit NewBid(msg.sender, listing, amount); + emit ListingCreated( + itemId, + nftContract, + nftId, + msg.sender, + price, + payableCurrency, + isAuction, + auctionDate, + address(0) + ); + } + + // IERC20 buy function + function buy(uint256 listingId) public payable nonReentrant { + Listing memory item = listings[listingId]; + address contractAddress = item.nftContract; + address nftSeller = item.seller; + uint256 nftId = item.nftId; + uint256 price = item.price; + Currency payableCurrency = item.payableCurrency; + // uint256 baseRoyalty = royalties[contractAddress].royaltyAmount; + // address royaltyReceiver = royalties[contractAddress].payoutAccount; + require(msg.sender != nftSeller, "You cannot buy an NFT you are selling."); + require(contractAddress != address(0), "Listing does not exist."); + require(!item.isAuction, "This listing is designated for auction."); + + // Mixed payment + // Implement AggregatorV3Interface to convert the USD value to the native token equivalent value + + // Transfer sale proceeds to seller, minus royalty + if (payableCurrency == Currency.USDCToken) { + // bool usdcSuccess = IERC20(USDC).transferFrom( + // msg.sender, nftSeller, price - ((price * baseRoyalty) / BPS) + // ); // price - royalty + bool usdcSuccess = IERC20(USDC).transferFrom(msg.sender, nftSeller, price); // price - royalty + // Transfer royalty to the royalty receiver + // bool usdcRoyaltiesSuccess = IERC20(USDC).transferFrom( + // msg.sender, royaltyReceiver, (price * baseRoyalty) / BPS + // ); // just royalty + // require(usdcSuccess && usdcRoyaltiesSuccess, "Failed to transfer USDC"); + require(usdcSuccess, "Failed to transfer USDC"); + } else { + require( + msg.value >= price, "Please submit asking price to complete purchase" + ); + // (bool ethSuccess,) = payable(nftSeller).call{ + // value: price - ((price * baseRoyalty) / BPS) + // }(""); + (bool ethSuccess,) = payable(nftSeller).call{ value: price }(""); + // (bool ethRoyaltiesSuccess,) = + // payable(royaltyReceiver).call{ value: (price * baseRoyalty) / BPS }(""); + // require(ethSuccess && ethRoyaltiesSuccess, "Failed to transfer ETH"); + require(ethSuccess, "Failed to transfer ETH"); } - // * Withdraw - function withdraw(uint256 listingId) public nonReentrant { - Listing storage listing = listings[listingId]; - address contractAddress = listing.nftContract; - uint256 nftId = listing.nftId; - address nftSeller = listing.seller; - address highestBidder = listing.highestBidder; - uint256 endBid = listing.price; - uint256 baseRoyalty = royalties[contractAddress].royaltyAmount; - address royaltyReceiver = royalties[contractAddress].payoutAccount; - Currency payableCurrency = listing.payableCurrency; - uint256 sellerAmount = endBid - ((endBid * baseRoyalty) / BPS); - uint256 royaltyRecAmount = (endBid * baseRoyalty) / BPS; - require( - msg.sender == nftSeller || msg.sender == highestBidder, "Only the seller or highest bidder can withdraw." - ); - - IERC721(contractAddress).safeTransferFrom(nftSeller, highestBidder, nftId); - if (payableCurrency == Currency.USDCToken) { - IERC20(USDC).transfer(nftSeller, sellerAmount); - IERC20(USDC).transfer(royaltyReceiver, royaltyRecAmount); - } else { - // Implement AggregatorV3Interface to convert the USD value to the native token equivalent value - } - - // Update listing status - listing.isAuction = false; - - emit Purchase(listingId, highestBidder, listing.payableCurrency, endBid); - } + // Transfer NFT to the buyer + IERC721(contractAddress).safeTransferFrom(nftSeller, msg.sender, nftId); + + emit Purchase(listingId, msg.sender, item.payableCurrency, price); + } + + // * Get current listing price + function getPrice(uint256 listingId) public view returns (uint256) { + return listings[listingId].price; + } + + // * Update listing price, listing cannot be on auction. + function updatePrice( + uint256 listingId, + uint256 newPrice + ) public Owned(listingId) { + require( + !listings[listingId].isAuction, + "Auction starting prices cannot be updated" + ); - // * Auction Cancel - function auctionCancel(uint256 listingId) public Owned(listingId) nonReentrant { - Listing storage listing = listings[listingId]; - require(block.timestamp < listing.date, "Auction has already ended"); - // If there is a highestBidder, transfer their bid back - if (listing.highestBidder != address(0)) { - payable(listing.highestBidder).transfer(listing.price); - } + // Set new price + listings[listingId].price = newPrice; - delete listings[listingId]; - } + emit UpdatedPrice(listingId, msg.sender, newPrice); + } - // * Remove Listing - function removeListing(uint256 listingId) public Owned(listingId) { - Listing storage listing = listings[listingId]; - require(listing.seller == msg.sender, "You do not own this listing."); - require(!listing.isAuction, "You cannot remove an active auction"); + // * New bid and return funds to previous bidder + function bid(uint256 listingId, uint256 amount) public payable nonReentrant { + Listing storage listing = listings[listingId]; + uint256 currentBid = listing.price; + address highestBidder = listing.highestBidder; + Currency payableCurrency = listing.payableCurrency; + require( + msg.sender != listing.seller, "You cannot bid on an NFT you are selling." + ); + require(listing.isAuction, "This listing is not an auction"); + require(block.timestamp < listing.date, "Auction has already ended"); + require( + amount > listing.price, "Bid must be greater than the previous bid." + ); - delete listings[listingId]; + if (payableCurrency == Currency.USDCToken) { + // Escrow payment in the marketplace contract + IERC20(USDC).transferFrom(msg.sender, address(this), amount); + // Transfer current bid back to previous bidder if applicable + if (listing.highestBidder != address(0)) { + IERC20(USDC).transfer(highestBidder, currentBid); + } + } else { + // Implement AggregatorV3Interface to convert the USD value to the native token equivalent value } + // Update to new bid values + listing.highestBidder = msg.sender; + listing.price = amount; + + emit NewBid(msg.sender, listing, amount); + } + + // * Withdraw + function withdraw(uint256 listingId) public nonReentrant { + Listing storage listing = listings[listingId]; + address contractAddress = listing.nftContract; + uint256 nftId = listing.nftId; + address nftSeller = listing.seller; + address highestBidder = listing.highestBidder; + uint256 endBid = listing.price; + uint256 baseRoyalty = royalties[contractAddress].royaltyAmount; + address royaltyReceiver = royalties[contractAddress].payoutAccount; + Currency payableCurrency = listing.payableCurrency; + uint256 sellerAmount = endBid - ((endBid * baseRoyalty) / BPS); + uint256 royaltyRecAmount = (endBid * baseRoyalty) / BPS; + require( + msg.sender == nftSeller || msg.sender == highestBidder, + "Only the seller or highest bidder can withdraw." + ); - // ============ UTILITIES ============== - - // * Set Royalty info - function setNFTCollectionRoyalty(address contractAddress, address payoutAccount, uint256 royaltyAmount) - public - returns (bool) - { - require(!royalties[contractAddress].exists, "Collection already has royalty info set."); - require(royaltyAmount > 0 && royaltyAmount <= 5000, "Please set a royalty amount between 0.01% and 50%"); - require(payoutAccount != address(0), "Royalties should not be burned."); - // Set royalties - royalties[contractAddress] = Royalties(contractAddress, payoutAccount, royaltyAmount, true); - - return true; + IERC721(contractAddress).safeTransferFrom(nftSeller, highestBidder, nftId); + if (payableCurrency == Currency.USDCToken) { + IERC20(USDC).transfer(nftSeller, sellerAmount); + IERC20(USDC).transfer(royaltyReceiver, royaltyRecAmount); + } else { + // Implement AggregatorV3Interface to convert the USD value to the native token equivalent value } - // * Remove ERC20 from contract - function removeERC20Stuck(address to, IERC20 currency, uint256 amount) public onlyOwner { - IERC20(currency).transfer(to, amount); + // Update listing status + listing.isAuction = false; + + emit Purchase(listingId, highestBidder, listing.payableCurrency, endBid); + } + + // * Auction Cancel + function auctionCancel(uint256 listingId) + public + Owned(listingId) + nonReentrant + { + Listing storage listing = listings[listingId]; + require(block.timestamp < listing.date, "Auction has already ended"); + // If there is a highestBidder, transfer their bid back + if (listing.highestBidder != address(0)) { + payable(listing.highestBidder).transfer(listing.price); } - // ========= Receive ============= - receive() external payable {} + delete listings[listingId]; + } + + // * Remove Listing + function removeListing(uint256 listingId) public Owned(listingId) { + Listing storage listing = listings[listingId]; + require(listing.seller == msg.sender, "You do not own this listing."); + require(!listing.isAuction, "You cannot remove an active auction"); + + delete listings[listingId]; + } + + // ============ UTILITIES ============== + + // * Set Royalty info + function setNFTCollectionRoyalty( + address contractAddress, + address payoutAccount, + uint256 royaltyAmount + ) public returns (bool) { + require( + !royalties[contractAddress].exists, + "Collection already has royalty info set." + ); + require( + royaltyAmount > 0 && royaltyAmount <= 5000, + "Please set a royalty amount between 0.01% and 50%" + ); + require(payoutAccount != address(0), "Royalties should not be burned."); + // Set royalties + royalties[contractAddress] = + Royalties(contractAddress, payoutAccount, royaltyAmount, true); + + return true; + } + + // * Remove ERC20 from contract + function removeERC20Stuck( + address to, + IERC20 currency, + uint256 amount + ) public onlyOwner { + IERC20(currency).transfer(to, amount); + } + + // ========= Receive ============= + receive() external payable { } } diff --git a/packages/foundry/contracts/MockUSDC.sol b/packages/foundry/contracts/MockUSDC.sol index 166598f..4da41ec 100644 --- a/packages/foundry/contracts/MockUSDC.sol +++ b/packages/foundry/contracts/MockUSDC.sol @@ -8,7 +8,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; contract MockUSDC is ERC20, ERC20Burnable, Ownable { constructor() ERC20("USDC", "USDC") Ownable(msg.sender) { } - function mint(address to, uint256 amount) public onlyOwner { + function mint(address to, uint256 amount) public { _mint(to, amount); } diff --git a/packages/nextjs/app/marketplace/_components/Marketplace.tsx b/packages/nextjs/app/marketplace/_components/Marketplace.tsx index 6975b7b..2797054 100644 --- a/packages/nextjs/app/marketplace/_components/Marketplace.tsx +++ b/packages/nextjs/app/marketplace/_components/Marketplace.tsx @@ -24,7 +24,6 @@ export interface Collectible extends Partial { export const Marketplace = () => { const { address: isConnected, isConnecting } = useAccount(); - const [listedCollectibles, setListedCollectibles] = useState([]); // Fetch the collectible contract @@ -44,6 +43,18 @@ export const Marketplace = () => { watch: true, }); + // Fetch Marketplace Purchase events + const { + data: purchaseEvents, + isLoading: purchaseIsLoadingEvents, + error: purchaseErrorReadingEvents, + } = useScaffoldEventHistory({ + contractName: "Marketplace", + eventName: "Purchase", + fromBlock: 0n, + watch: true, + }); + // Fetch SimpleMint CollectionStarted events const { data: simpleMintEvents, @@ -102,30 +113,25 @@ export const Marketplace = () => { for (const event of simpleMintEvents || []) { try { const { args } = event; - // This could be used to interact with the SimpleMinted contract - // const nftAddress = args?.nft; const artist = args?.artist; const tokenURI = args?.tokenURI; const usdPrice = args?.usdPrice; const maxTokenId = args?.maxTokenId; // Ensure tokenURI is defined - if (!tokenURI) { - console.warn("Skipping event because tokenURI is undefined"); - continue; - } + if (!tokenURI) continue; const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); const nftMetadata: NFTMetaData = await getMetadataFromIPFS(ipfsHash); // Add the NFT collection to the collectibles array collectiblesUpdate.push({ - listingId: undefined, // Not applicable for SimpleMint NFTs + listingId: undefined, uri: tokenURI, owner: artist || "", - price: usdPrice ? usdPrice.toString() : undefined, // Set price as USD price from the event - payableCurrency: usdPrice ? "USDC" : undefined, // Set payableCurrency to USDC if usdPrice is present - maxTokenId: maxTokenId ? Number(maxTokenId) : undefined, // Add maxTokenId + price: usdPrice ? usdPrice.toString() : undefined, + payableCurrency: usdPrice ? "USDC" : undefined, + maxTokenId: maxTokenId ? Number(maxTokenId) : undefined, ...nftMetadata, }); } catch (e) { @@ -134,14 +140,33 @@ export const Marketplace = () => { } } - // Update state with merged collectibles - setListedCollectibles(collectiblesUpdate); + // Debugging: Log the Purchase Events + console.log("Purchase Events:", purchaseEvents); + + // Debugging: Log the listed collectibles before filtering + console.log("Listed Collectibles before filtering:", collectiblesUpdate); + + // Filter out NFTs that have been purchased + const updatedCollectibles = collectiblesUpdate.filter(collectible => { + const hasBeenPurchased = purchaseEvents?.some(purchase => { + const purchaseItemId = Number(purchase.args.itemId); // Convert itemId from Purchase to number + return purchaseItemId === collectible.listingId; // Ensure both are numbers for comparison + }); + console.log(`Collectible ${collectible.listingId} has been purchased:`, hasBeenPurchased); + return !hasBeenPurchased; + }); + + // Debugging: Log the filtered collectibles + console.log("Filtered Collectibles:", updatedCollectibles); + + // Update state with filtered collectibles + setListedCollectibles(updatedCollectibles); }; fetchListedNFTs(); - }, [events, simpleMintEvents, yourCollectibleContract]); + }, [events, simpleMintEvents, purchaseEvents, yourCollectibleContract]); - if (isLoadingEvents || simpleMintIsLoadingEvents) { + if (isLoadingEvents || simpleMintIsLoadingEvents || purchaseIsLoadingEvents) { return (
@@ -149,8 +174,8 @@ export const Marketplace = () => { ); } - if (errorReadingEvents || simpleMintErrorReadingEvents) { - return
Error fetching events: {errorReadingEvents?.message || simpleMintErrorReadingEvents?.message}
; + if (errorReadingEvents || simpleMintErrorReadingEvents || purchaseErrorReadingEvents) { + return
Error fetching events: {errorReadingEvents?.message || purchaseErrorReadingEvents?.message}
; } return ( diff --git a/packages/nextjs/app/marketplace/_components/NFTCard.tsx b/packages/nextjs/app/marketplace/_components/NFTCard.tsx index ea984ee..c90bd38 100644 --- a/packages/nextjs/app/marketplace/_components/NFTCard.tsx +++ b/packages/nextjs/app/marketplace/_components/NFTCard.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import { formatEther } from "viem"; +import { useAccount } from "wagmi"; import { Address } from "~~/components/scaffold-eth"; -import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; +import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; import { NFTMetaData } from "~~/utils/simpleNFT/nftsMetadata"; export interface Collectible extends Partial { @@ -25,7 +26,21 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => { } const [activeTab, setActiveTab] = useState(initialActiveTab); + const { address: connectedAddress } = useAccount(); + const { writeContractAsync: MarketplaceWriteContractAsync } = useScaffoldWriteContract("Marketplace"); + const { writeContractAsync: USDCWriteContractAsync } = useScaffoldWriteContract("MockUSDC"); + + const { data: marketplaceData } = useDeployedContractInfo("Marketplace"); + + const { data: usdcAllowance } = useScaffoldReadContract({ + contractName: "MockUSDC", + functionName: "allowance", + args: [connectedAddress, marketplaceData?.address], + watch: true, + }); + + console.log("usdcAllowance", usdcAllowance); const handleBuyNFT = async () => { if (!nft.listingId || !nft.price || !nft.payableCurrency) return; // Skip if required data is missing @@ -49,7 +64,22 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => { } }; - // Convert and format price for display (handle if price is undefined) + const handleApproveUSDC = async () => { + if (!nft.listingId || !nft.price || !nft.payableCurrency) return; // Skip if required data is missing + + try { + // let value; + + await USDCWriteContractAsync({ + functionName: "approve", + args: [marketplaceData?.address, BigInt(nft.price)], + }); + } catch (err) { + console.error("Error calling buy function", err); + } + }; + + // Convert and format price for display const formattedPrice = nft.price && nft.payableCurrency === "ETH" ? formatEther(BigInt(nft.price)) // Format from wei to ETH @@ -57,6 +87,14 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => { ? (parseInt(nft.price) / 1e6).toFixed(2) // Format USDC (assuming 6 decimal places) : "N/A"; // If price is undefined + const usdcPriceInUnits = nft.price ? BigInt(nft.price) : BigInt(0); // Ensure USDC price is handled as BigInt + + // Check if approval is required (USDC) + const requiresApproval = + nft.payableCurrency === "USDC" && + usdcAllowance !== undefined && // Ensure that allowance is defined + usdcPriceInUnits > BigInt(usdcAllowance.toString()); // Ensure comparison is accurate + return (
{/* Tabs navigation */} @@ -112,15 +150,26 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => {
-
- -
+ {/* Conditionally render the Allow button */} + {requiresApproval ? ( +
+ +
+ ) : ( +
+ +
+ )} )} @@ -141,22 +190,39 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => { {/* Display price and buy button only if price is available */} {nft.price && nft.payableCurrency && ( -
+
+
+ Max Supply: + {nft.maxTokenId} +
{formattedPrice} {nft.payableCurrency}
-
- -
+ + {/* Conditionally render the Allow button */} + {requiresApproval ? ( +
+ +
+ ) : ( +
+ +
+ )}
)}
diff --git a/packages/nextjs/app/myProfile/_components/MyHoldings.tsx b/packages/nextjs/app/myProfile/_components/MyHoldings.tsx index 1ea166b..38a3bb0 100644 --- a/packages/nextjs/app/myProfile/_components/MyHoldings.tsx +++ b/packages/nextjs/app/myProfile/_components/MyHoldings.tsx @@ -46,9 +46,9 @@ export const MyHoldings = () => { const tokenURI = await yourCollectibleContract.read.tokenURI([tokenId]); const owner = await yourCollectibleContract.read.ownerOf([tokenId]); - // if (showOnlyMyNFTs && owner.toLowerCase() !== connectedAddress.toLowerCase()) { - // continue; - // } + if (owner.toLowerCase() !== connectedAddress.toLowerCase()) { + continue; + } const ipfsHash = tokenURI.replace("https://ipfs.io/ipfs/", ""); @@ -73,7 +73,6 @@ export const MyHoldings = () => { }; updateMyCollectibles(); - // }, [connectedAddress, showOnlyMyNFTs, myTotalBalance]); // Watching balance to update NFTs }, [connectedAddress, myTotalBalance]); // Watching balance to update NFTs if (allCollectiblesLoading) diff --git a/packages/nextjs/app/myProfile/_components/MyProfile.tsx b/packages/nextjs/app/myProfile/_components/MyProfile.tsx new file mode 100644 index 0000000..1066ed3 --- /dev/null +++ b/packages/nextjs/app/myProfile/_components/MyProfile.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useState } from "react"; +import { MyHoldings } from "./"; +import { NextPage } from "next"; +import { useAccount } from "wagmi"; +import { Address, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; +import { useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; +import { addToIPFS } from "~~/utils/simpleNFT/ipfs-fetch"; +import nftsMetadata from "~~/utils/simpleNFT/nftsMetadata"; + +export const MyProfile: NextPage = () => { + const { address: connectedAddress, isConnected, isConnecting } = useAccount(); + const { writeContractAsync: nftWriteAsync } = useScaffoldWriteContract("MockNFT"); + const { writeContractAsync: usdcWriteAsync } = useScaffoldWriteContract("MockUSDC"); + + const { data: tokenIdCounter } = useScaffoldReadContract({ + contractName: "MockNFT", + functionName: "tokenIdCounter", + watch: true, + }); + + const { data: usdcBalance } = useScaffoldReadContract({ + contractName: "MockUSDC", + functionName: "balanceOf", + args: [connectedAddress], + watch: true, + }); + + // Tab management state + const [activeTab, setActiveTab] = useState("your-nfts"); + + const handleMintUSDC = async () => { + try { + await usdcWriteAsync({ + functionName: "mint", + args: [connectedAddress, BigInt(100e6)], // Mint 1 USDC + }); + + notification.success("USDC Minted Successfully"); + } catch (error) { + console.error("Error during minting:", error); + + // Log the error and notify the user + notification.error("Minting failed, please try again."); + } + }; + + const handleMintItem = async () => { + if (tokenIdCounter === undefined) { + notification.error("Token ID Counter not found."); + return; + } + + const tokenIdCounterNumber = Number(tokenIdCounter); + const currentTokenMetaData = nftsMetadata[tokenIdCounterNumber % nftsMetadata.length]; + const notificationId = notification.loading("Uploading to IPFS"); + try { + const uploadedItem = await addToIPFS(currentTokenMetaData); + + notification.remove(notificationId); + notification.success("Metadata uploaded to IPFS"); + + // Log IPFS path before sending to contract + console.log("IPFS Path:", uploadedItem.path); + + // Mint the NFT + await nftWriteAsync({ + functionName: "mintItem", + args: [uploadedItem.path], + }); + + notification.success("NFT Minted Successfully"); + } catch (error) { + notification.remove(notificationId); + console.error("Error during minting:", error); + + // Log the error and notify the user + notification.error("Minting failed, please try again."); + } + }; + + return ( +
+ {/* User Profile Section */} +
+ {/* Profile Picture */} +
+
+ Profile +
+
+ + {/* User Bio */} +
+

-unregistered user-

+
+ +

-no bio available-

+
+ + {/* USDC Balance and Logo */} +
+ USDC Logo +

{usdcBalance ? Number(usdcBalance) / 1e6 : 0}

+
+
+ + {/* Tabs Section */} +
+
+ setActiveTab("your-nfts")} + > + Your NFTs + + setActiveTab("nfts-on-sale")} + > + NFTs on Sale + + setActiveTab("past-sales")} + > + Past Sales + +
+
+ {/* Content Based on Active Tab */} +
+ {activeTab === "your-nfts" && ( + <> +
+ {!isConnected || isConnecting ? ( + + ) : ( +
+ + +
+ )} +
+ + + )} + + {activeTab === "nfts-on-sale" && ( +
+

You currently have no NFTs listed for sale.

+
+ )} + + {activeTab === "past-sales" && ( +
+

You have no past sales yet.

+
+ )} +
+
+ ); +}; + +// export default MyNFTs; diff --git a/packages/nextjs/app/myProfile/_components/NFTCard.tsx b/packages/nextjs/app/myProfile/_components/NFTCard.tsx index a57ec49..8323621 100644 --- a/packages/nextjs/app/myProfile/_components/NFTCard.tsx +++ b/packages/nextjs/app/myProfile/_components/NFTCard.tsx @@ -2,14 +2,14 @@ import { useState } from "react"; import { Collectible } from "./MyHoldings"; import { parseEther } from "viem"; import { useAccount } from "wagmi"; -import { Address, AddressInput, InputBase } from "~~/components/scaffold-eth"; -import { useDeployedContractInfo, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; +import { AddressInput, InputBase } from "~~/components/scaffold-eth"; +import { useDeployedContractInfo, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; // For ETH conversion export const NFTCard = ({ nft }: { nft: Collectible }) => { const [transferToAddress, setTransferToAddress] = useState(""); - const [activeTab, setActiveTab] = useState("details"); + const [activeTab, setActiveTab] = useState("artwork"); const [NFTPrice, setNFTPrice] = useState(0); const [payableCurrency, setPayableCurrency] = useState("0"); // "0" for ETH, "1" for USDC const [isAuction, setIsAuction] = useState(false); @@ -23,7 +23,24 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => { const { writeContractAsync: MockERC721WriteContractAsync } = useScaffoldWriteContract("MockNFT"); const { writeContractAsync: MarketplaceWriteContractAsync } = useScaffoldWriteContract("Marketplace"); const { data: mockERC721Data } = useDeployedContractInfo("MockNFT"); - // const { data: marketplaceData } = useDeployedContractInfo("Marketplace"); + const { data: marketplaceData } = useDeployedContractInfo("Marketplace"); + + const { data: isApproved } = useScaffoldReadContract({ + contractName: "MockNFT", + functionName: "getApproved", + args: [BigInt(nft.id.toString())], + }); + + const handleApprove = async () => { + try { + await MockERC721WriteContractAsync({ + functionName: "approve", + args: [marketplaceData?.address, BigInt(nft.id.toString())], + }); + } catch (err) { + console.error("Error calling transferFrom function", err); + } + }; const handleTransfer = async () => { try { @@ -74,6 +91,12 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => {
{/* Tabs navigation */}
+ setActiveTab("artwork")} + > + Artwork + setActiveTab("details")} @@ -91,16 +114,26 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => {
{/* Tab Content */} - {activeTab === "details" && ( + {activeTab === "artwork" && ( // Render all combined content (artwork, info, actions) here
{/* eslint-disable-next-line */} NFT Image -
- # {nft.id} -
+
+ {nft.animation_url && ( + + )} +
+
+ )} + + {activeTab === "details" && ( + // Render all combined content (artwork, info, actions) here +

{nft.name}

@@ -112,18 +145,16 @@ export const NFTCard = ({ nft }: { nft: Collectible }) => { ))}
+

{nft.description}

- {nft.animation_url && ( - - )} -
- Owner : -
+
+ + Id: {nft.id} +
+
Transfer To: {
{/* Payable Currency Toggle */}
-

Create Listing

+

List NFT for sale

Currency
)} diff --git a/packages/nextjs/app/myProfile/page.tsx b/packages/nextjs/app/myProfile/page.tsx index 477dbc8..8830b9c 100644 --- a/packages/nextjs/app/myProfile/page.tsx +++ b/packages/nextjs/app/myProfile/page.tsx @@ -1,136 +1,18 @@ -"use client"; +import { MyProfile } from "./_components/MyProfile"; +import type { NextPage } from "next"; +import { getMetadata } from "~~/utils/scaffold-eth/getMetadata"; -import { useState } from "react"; -import { MyHoldings } from "./_components"; -import { NextPage } from "next"; -import { useAccount } from "wagmi"; -import { RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; -import { useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; -import { notification } from "~~/utils/scaffold-eth"; -import { addToIPFS } from "~~/utils/simpleNFT/ipfs-fetch"; -import nftsMetadata from "~~/utils/simpleNFT/nftsMetadata"; - -const MyNFTs: NextPage = () => { - const { address: isConnected, isConnecting } = useAccount(); - const { writeContractAsync } = useScaffoldWriteContract("MockNFT"); - - const { data: tokenIdCounter } = useScaffoldReadContract({ - contractName: "MockNFT", - functionName: "tokenIdCounter", - watch: true, - }); - - // Tab management state - const [activeTab, setActiveTab] = useState("your-nfts"); - - const handleMintItem = async () => { - if (tokenIdCounter === undefined) { - notification.error("Token ID Counter not found."); - return; - } - - const tokenIdCounterNumber = Number(tokenIdCounter); - const currentTokenMetaData = nftsMetadata[tokenIdCounterNumber % nftsMetadata.length]; - const notificationId = notification.loading("Uploading to IPFS"); - try { - const uploadedItem = await addToIPFS(currentTokenMetaData); - - notification.remove(notificationId); - notification.success("Metadata uploaded to IPFS"); - - // Log IPFS path before sending to contract - console.log("IPFS Path:", uploadedItem.path); - - // Mint the NFT - await writeContractAsync({ - functionName: "mintItem", - args: [uploadedItem.path], - }); - - notification.success("NFT Minted Successfully"); - } catch (error) { - notification.remove(notificationId); - console.error("Error during minting:", error); - - // Log the error and notify the user - notification.error("Minting failed, please try again."); - } - }; +export const metadata = getMetadata({ + title: "My Profile", + description: "Built with 🏗 Scaffold-ETH 2", +}); +const SimpleMintPage: NextPage = () => { return ( -
- {/* User Profile Section */} -
- {/* Profile Picture */} -
-
- Profile -
-
- {/* User Bio */} -
-

John Doe

-

- NFT collector and digital artist. Passionate about blockchain and decentralized art. -

-
-
- - {/* Tabs Section */} -
- - - {/* Content Based on Active Tab */} -
- {activeTab === "your-nfts" && ( - <> -
- {!isConnected || isConnecting ? ( - - ) : ( - - )} -
- - - )} - - {activeTab === "nfts-on-sale" && ( -
-

You currently have no NFTs listed for sale.

-
- )} - - {activeTab === "past-sales" && ( -
-

You have no past sales yet.

-
- )} -
-
-
+ <> + + ); }; -export default MyNFTs; +export default SimpleMintPage; diff --git a/packages/nextjs/app/simpleMint/_components/TextAreaBase.tsx b/packages/nextjs/app/simpleMint/_components/TextAreaBase.tsx index 95ef08d..58e374a 100644 --- a/packages/nextjs/app/simpleMint/_components/TextAreaBase.tsx +++ b/packages/nextjs/app/simpleMint/_components/TextAreaBase.tsx @@ -49,7 +49,7 @@ export const TextAreaBase = string } | undefined =
{prefix}