diff --git a/README.md b/README.md index fac6319..730616f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,25 @@ -# 🔌 create-eth Extensions +# ERC-721 NFT Extension for Scaffold-ETH 2 -This repository holds all the BG curated extensions for [create-eth](https://github.com/scaffold-eth/create-eth), so you can extend the functionality of your Scaffold-ETH project. +This extension introduces an ERC-721 token contract and demonstrates how to use it, including getting the total supply and holder balance, listing all NFTs from the collection and NFTs from the connected address, and how to transfer NFTs. -## Usage +The ERC-721 Token Standard introduces a standard for Non-Fungible Tokens ([EIP-721](https://eips.ethereum.org/EIPS/eip-721)), in other words, each token is unique. -You can install any of the extensions in this repository by running the following command: +The ERC-721 token contract is implemented using the [ERC-721 token implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol) from OpenZeppelin. + +The ERC-721 token implementation uses the [ERC-721 Enumerable extension](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/extensions/ERC721Enumerable.sol) from OpenZeppelin to list all tokens from the collection and all the tokens owned by an address. You can remove this if you plan to use an indexer, like a Subgraph or Ponder ([extensions available](https://scaffoldeth.io/extensions)). + +## Installation ```bash -npx create-eth@latest -e +npx create-eth@latest -e erc-721 ``` -## Available Extensions - -- [subgraph](https://github.com/scaffold-eth/create-eth-extensions/tree/subgraph): This Scaffold-ETH 2 extension helps you build and test subgraphs locally for your contracts. It also enables interaction with the front-end and facilitates easy deployment to Subgraph Studio. -- [eip-712](https://github.com/scaffold-eth/create-eth-extensions/tree/eip-712): An implementation of EIP-712, allowing you to send, sign, and verify typed messages in a user-friendly manner. -- [ponder](https://github.com/scaffold-eth/create-eth-extensions/tree/ponder): This Scaffold-ETH 2 extension comes pre-configured with [ponder.sh](https://ponder.sh), providing an example to help you get started quickly. -- [onchainkit](https://github.com/scaffold-eth/create-eth-extensions/tree/onchainkit): This Scaffold-ETH 2 extension comes pre-configured with [onchainkit](https://onchainkit.xyz/), providing an example to help you get started quickly. -- [erc-20](https://github.com/scaffold-eth/create-eth-extensions/tree/erc-20): This extension introduces an ERC-20 token contract and demonstrates how to interact with it, including getting a holder balance and transferring tokens. -- [eip-5792](https://github.com/scaffold-eth/create-eth-extensions/tree/eip-5792): This extension shows how to use [EIP-5792](https://eips.ethereum.org/EIPS/eip-5792) wallet capabilities. It comes with an example frontend interaction with the `EIP5792_Example.sol` contract. The code demonstrates how to make a batched transaction that sets new greetings and increments the counter in a single transaction using Wagmi's experimental hooks and Coinbase smart wallet. +## 🚀 Setup extension -## Create your own extension +Deploy your contract running `yarn deploy` -You can extend Scaffold-ETH by creating your own extension. To do so, you need to create a new repository with the following structure: +## Interact with the NFT -`ToDo` +Start the front-end with `yarn start` and go to the _/erc721_ page to interact with your deployed ERC-721 token. -```bash -npx create-eth@latest -e your-github-username/your-extension-repository:branch-name # branch-name is optional -``` +You can check the code at `packages/nextjs/app/erc721`. diff --git a/extension/README.md.args.mjs b/extension/README.md.args.mjs new file mode 100644 index 0000000..ccefc2d --- /dev/null +++ b/extension/README.md.args.mjs @@ -0,0 +1,21 @@ +export const extraContents = `## 🚀 Setup ERC-721 NFT Extension + +This extension introduces an ERC-721 token contract and demonstrates how to use it, including getting the total supply and holder balance, listing all NFTs from the collection and NFTs from the connected address, and how to transfer NFTs. + +The ERC-721 Token Standard introduces a standard for Non-Fungible Tokens ([EIP-721](https://eips.ethereum.org/EIPS/eip-721)), in other words, each token is unique. + +The ERC-721 token contract is implemented using the [ERC-721 token implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol) from OpenZeppelin. + +The ERC-721 token implementation uses the [ERC-721 Enumerable extension](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/extensions/ERC721Enumerable.sol) from OpenZeppelin to list all tokens from the collection and all the tokens owned by an address. You can remove this if you plan to use an indexer, like a Subgraph or Ponder ([extensions available](https://scaffoldeth.io/extensions)). + +### Setup + +Deploy your contract running \`\`\`yarn deploy\`\`\` + +### Interact with the NFT + +Start the front-end with \`\`\`yarn start\`\`\` and go to the _/erc721_ page to interact with your deployed ERC-721 token. + +You can check the code at \`\`\`packages/nextjs/app/erc721\`\`\`. + +`; diff --git a/extension/packages/foundry/contracts/SE2NFT.sol b/extension/packages/foundry/contracts/SE2NFT.sol new file mode 100644 index 0000000..43f0c80 --- /dev/null +++ b/extension/packages/foundry/contracts/SE2NFT.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; + +contract SE2NFT is ERC721Enumerable { + uint256 public tokenIdCounter; + mapping(uint256 tokenId => string) public tokenURIs; + string[] public uris = [ + "QmVHi3c4qkZcH3cJynzDXRm5n7dzc9R9TUtUcfnWQvhdcw", // Zebra + "QmfVMAmNM1kDEBYrC2TPzQDoCRFH6F5tE1e9Mr4FkkR5Xr", // Buffalo + "QmcvcUaKf6JyCXhLD1by6hJXNruPQGs3kkLg2W1xr7nF1j" // Rhino + ]; + + constructor() ERC721("SE2-ERC721-NFT", "SE2NFT") { } + + function _baseURI() internal pure override returns (string memory) { + return "https://ipfs.io/ipfs/"; + } + + function mintItem( + address to + ) public returns (uint256) { + tokenIdCounter++; + _safeMint(to, tokenIdCounter); + + bytes32 predictableRandom = keccak256( + abi.encodePacked( + tokenIdCounter, blockhash(block.number - 1), msg.sender, address(this) + ) + ); + + tokenURIs[tokenIdCounter] = uris[uint256(predictableRandom) % uris.length]; + return tokenIdCounter; + } + + function tokenURI( + uint256 tokenId + ) public view override returns (string memory) { + _requireOwned(tokenId); + + string memory _tokenURI = tokenURIs[tokenId]; + string memory base = _baseURI(); + + return string.concat(base, _tokenURI); + } +} diff --git a/extension/packages/foundry/script/Deploy.s.sol.args.mjs b/extension/packages/foundry/script/Deploy.s.sol.args.mjs new file mode 100644 index 0000000..f74ab4b --- /dev/null +++ b/extension/packages/foundry/script/Deploy.s.sol.args.mjs @@ -0,0 +1,5 @@ +export const deploymentsScriptsImports = `import { DeploySE2Nft } from "./DeploySE2Nft.s.sol";`; +export const deploymentsLogic = ` + DeploySE2Nft deploySE2Nft = new DeploySE2Nft(); + deploySE2Nft.run(); +`; diff --git a/extension/packages/foundry/script/DeploySE2Nft.s.sol b/extension/packages/foundry/script/DeploySE2Nft.s.sol new file mode 100644 index 0000000..0f52a57 --- /dev/null +++ b/extension/packages/foundry/script/DeploySE2Nft.s.sol @@ -0,0 +1,14 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../contracts/SE2NFT.sol"; +import "./DeployHelpers.s.sol"; + +contract DeploySE2Nft is ScaffoldETHDeploy { + function run() external ScaffoldEthDeployerRunner { + SE2NFT se2Nft = new SE2NFT(); + console.logString( + string.concat("SE2NFT deployed at: ", vm.toString(address(se2Nft))) + ); + } +} diff --git a/extension/packages/hardhat/contracts/SE2NFT.sol b/extension/packages/hardhat/contracts/SE2NFT.sol new file mode 100644 index 0000000..4e32f4e --- /dev/null +++ b/extension/packages/hardhat/contracts/SE2NFT.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0 <0.9.0; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; + +contract SE2NFT is ERC721Enumerable { + uint256 public tokenIdCounter; + mapping(uint256 tokenId => string) public tokenURIs; + string[] public uris = [ + "QmVHi3c4qkZcH3cJynzDXRm5n7dzc9R9TUtUcfnWQvhdcw", // Zebra + "QmfVMAmNM1kDEBYrC2TPzQDoCRFH6F5tE1e9Mr4FkkR5Xr", // Buffalo + "QmcvcUaKf6JyCXhLD1by6hJXNruPQGs3kkLg2W1xr7nF1j" // Rhino + ]; + + constructor() ERC721("SE2-ERC721-NFT", "SE2NFT") {} + + function _baseURI() internal pure override returns (string memory) { + return "https://ipfs.io/ipfs/"; + } + + function mintItem(address to) public returns (uint256) { + tokenIdCounter++; + _safeMint(to, tokenIdCounter); + + bytes32 predictableRandom = keccak256( + abi.encodePacked( + tokenIdCounter, + blockhash(block.number - 1), + msg.sender, + address(this) + ) + ); + + tokenURIs[tokenIdCounter] = uris[ + uint256(predictableRandom) % uris.length + ]; + return tokenIdCounter; + } + + function tokenURI( + uint256 tokenId + ) public view override returns (string memory) { + _requireOwned(tokenId); + + string memory _tokenURI = tokenURIs[tokenId]; + string memory base = _baseURI(); + + return string.concat(base, _tokenURI); + } +} diff --git a/extension/packages/hardhat/deploy/01_deploy_se2_nft.ts b/extension/packages/hardhat/deploy/01_deploy_se2_nft.ts new file mode 100644 index 0000000..4533ed5 --- /dev/null +++ b/extension/packages/hardhat/deploy/01_deploy_se2_nft.ts @@ -0,0 +1,38 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { Contract, parseEther } from "ethers"; + +/** + * Deploys a contract named "YourContract" using the deployer account and + * constructor arguments set to the deployer address + * + * @param hre HardhatRuntimeEnvironment object. + */ +const deploySe2Nft: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + /* + On localhost, the deployer account is the one that comes with Hardhat, which is already funded. + + When deploying to live networks (e.g `yarn deploy --network sepolia`), the deployer account + should have sufficient balance to pay for the gas fees for contract creation. + + You can generate a random account with `yarn generate` which will fill DEPLOYER_PRIVATE_KEY + with a random private key in the .env file (then used on hardhat.config.ts) + You can run the `yarn account` command to check your balance in every network. + */ + const { deployer } = await hre.getNamedAccounts(); + const { deploy } = hre.deployments; + + await deploy("SE2NFT", { + from: deployer, + log: true, + // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by + // automatically mining the contract deployment transaction. There is no effect on live networks. + autoMine: true, + }); +}; + +export default deploySe2Nft; + +// Tags are useful if you have multiple deploy files and only want to run one of them. +// e.g. yarn deploy --tags SE2NFT +deploySe2Nft.tags = ["SE2NFT"]; diff --git a/extension/packages/nextjs/app/erc721/components/AllNfts.tsx b/extension/packages/nextjs/app/erc721/components/AllNfts.tsx new file mode 100644 index 0000000..bc9893e --- /dev/null +++ b/extension/packages/nextjs/app/erc721/components/AllNfts.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { NFTCard } from "./NFTCard"; +import { useScaffoldContract, useScaffoldReadContract } from "~~/hooks/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +export interface Collectible { + id: number; + uri: string; + owner: string; + image: string; + name: string; +} + +export const AllNfts = () => { + const [allNfts, setAllNfts] = useState([]); + const [loading, setLoading] = useState(false); + + const { data: se2NftContract } = useScaffoldContract({ + contractName: "SE2NFT", + }); + + const { data: totalSupply } = useScaffoldReadContract({ + contractName: "SE2NFT", + functionName: "totalSupply", + watch: true, + }); + + useEffect(() => { + const updateAllNfts = async (): Promise => { + if (totalSupply === undefined || se2NftContract === undefined) return; + + setLoading(true); + const collectibleUpdate: Collectible[] = []; + for (let tokenIndex = 0; tokenIndex < parseInt(totalSupply.toString()); tokenIndex++) { + try { + const tokenId = await se2NftContract.read.tokenByIndex([BigInt(tokenIndex)]); + + const tokenURI = await se2NftContract.read.tokenURI([tokenId]); + const owner = await se2NftContract.read.ownerOf([tokenId]); + + const tokenMetadata = await fetch(tokenURI); + const metadata = await tokenMetadata.json(); + + collectibleUpdate.push({ + id: parseInt(tokenId.toString()), + uri: tokenURI, + owner, + image: metadata.image, + name: metadata.name, + }); + } catch (e) { + notification.error("Error fetching NTFs"); + setLoading(false); + console.log(e); + } + } + collectibleUpdate.sort((a, b) => a.id - b.id); + setAllNfts(collectibleUpdate); + setLoading(false); + }; + + updateAllNfts(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [totalSupply]); + + if (loading) + return ( +
+ +
+ ); + + return ( + <> +
+

Total Supply:

+

{totalSupply ? totalSupply.toString() : 0} tokens

+
+ {allNfts.length > 0 && ( +
+ {allNfts.map(item => ( + + ))} +
+ )} + + ); +}; diff --git a/extension/packages/nextjs/app/erc721/components/MyNfts.tsx b/extension/packages/nextjs/app/erc721/components/MyNfts.tsx new file mode 100644 index 0000000..dce1925 --- /dev/null +++ b/extension/packages/nextjs/app/erc721/components/MyNfts.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { NFTCard } from "./NFTCard"; +import { useAccount } from "wagmi"; +import { useScaffoldContract, useScaffoldReadContract } from "~~/hooks/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +export interface Collectible { + id: number; + uri: string; + owner: string; + image: string; + name: string; +} + +export const MyNfts = () => { + const { address: connectedAddress } = useAccount(); + const [myNfts, setMyNfts] = useState([]); + const [loading, setLoading] = useState(false); + + const { data: se2NftContract } = useScaffoldContract({ + contractName: "SE2NFT", + }); + + const { data: balance } = useScaffoldReadContract({ + contractName: "SE2NFT", + functionName: "balanceOf", + args: [connectedAddress], + watch: true, + }); + + useEffect(() => { + const updateMyNfts = async (): Promise => { + if (balance === undefined || se2NftContract === undefined || connectedAddress === undefined) return; + + setLoading(true); + const collectibleUpdate: Collectible[] = []; + const totalBalance = parseInt(balance.toString()); + for (let tokenIndex = 0; tokenIndex < totalBalance; tokenIndex++) { + try { + const tokenId = await se2NftContract.read.tokenOfOwnerByIndex([connectedAddress, BigInt(tokenIndex)]); + + const tokenURI = await se2NftContract.read.tokenURI([tokenId]); + + const tokenMetadata = await fetch(tokenURI); + const metadata = await tokenMetadata.json(); + + collectibleUpdate.push({ + id: parseInt(tokenId.toString()), + uri: tokenURI, + owner: connectedAddress, + image: metadata.image, + name: metadata.name, + }); + } catch (e) { + notification.error("Error fetching your NFTs"); + setLoading(false); + console.log(e); + } + } + collectibleUpdate.sort((a, b) => a.id - b.id); + setMyNfts(collectibleUpdate); + setLoading(false); + }; + + updateMyNfts(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectedAddress, balance]); + + if (loading) + return ( +
+ +
+ ); + + return ( + <> +
+

Your Balance:

+

{balance ? balance.toString() : 0} tokens

+
+ {myNfts.length > 0 && ( +
+ {myNfts.map(item => ( + + ))} +
+ )} + + ); +}; diff --git a/extension/packages/nextjs/app/erc721/components/NFTCard.tsx b/extension/packages/nextjs/app/erc721/components/NFTCard.tsx new file mode 100644 index 0000000..bcc20e6 --- /dev/null +++ b/extension/packages/nextjs/app/erc721/components/NFTCard.tsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { Collectible } from "./MyNfts"; +import { Address, AddressInput } from "~~/components/scaffold-eth"; +import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; + +export const NFTCard = ({ nft, transfer }: { nft: Collectible; transfer?: boolean }) => { + const [transferToAddress, setTransferToAddress] = useState(""); + + const { writeContractAsync } = useScaffoldWriteContract("SE2NFT"); + + return ( +
+
+ {/* eslint-disable-next-line */} + NFT Image +
+ # {nft.id} +
+
+
+
+

{nft.name}

+
+
+ Owner : +
+
+ {transfer && ( + <> +
+ Transfer To: + setTransferToAddress(newValue)} + /> +
+
+ +
+ + )} +
+
+ ); +}; diff --git a/extension/packages/nextjs/app/erc721/page.tsx b/extension/packages/nextjs/app/erc721/page.tsx new file mode 100644 index 0000000..df3ee7c --- /dev/null +++ b/extension/packages/nextjs/app/erc721/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useState } from "react"; +import { AllNfts } from "./components/AllNfts"; +import { MyNfts } from "./components/MyNfts"; +import type { NextPage } from "next"; +import { useAccount } from "wagmi"; +import { AddressInput, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; +import { useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; + +const ERC721: NextPage = () => { + const { address: connectedAddress } = useAccount(); + + const [toAddress, setToAddress] = useState(""); + + const { writeContractAsync: writeSE2TokenAsync } = useScaffoldWriteContract("SE2NFT"); + + return ( + <> +
+
+

ERC-721 NFT

+
+

+ This extension introduces an ERC-721 token contract and demonstrates how to use it, including getting the + total supply and holder balance, listing all NFTs from the collection and NFTs from the connected address, and how to transfer NFTs. +

+

+ The ERC-721 Token Standard introduces a standard for Non-Fungible Tokens ( + + EIP-721 + + ), in other words, each token is unique. +

+

+ The ERC-721 token contract is implemented using the{" "} + + ERC-721 token implementation + {" "} + from OpenZeppelin. +

+

+ The ERC-721 token implementation uses the{" "} + + ERC-721 Enumerable extension + {" "} + from OpenZeppelin to list all tokens from the collection and all the tokens owned by an address. You can remove this if you + plan to use an indexer, like a Subgraph or Ponder ( + + extensions available + + ). +

+
+ +
+ +

Interact with the NFT

+ +
+

Below you can mint an NFT for yourself or to another address.

+

+ You can see your balance and your NFTs, and below that, you can see the total supply and all the NFTs + minted. +

+

+ Check the code under packages/nextjs/app/erc721 to learn more about how to interact with the + ERC721 contract. +

+
+
+ + {connectedAddress ? ( +
+
+ +
+
+

Mint token to another address

+
+
To:
+
+ +
+
+
+ +
+
+ + +
+ ) : ( +
+

Please connect your wallet to interact with the token.

+ +
+ )} +
+ + ); +}; + +export default ERC721; diff --git a/extension/packages/nextjs/components/Header.tsx.args.mjs b/extension/packages/nextjs/components/Header.tsx.args.mjs new file mode 100644 index 0000000..d0e0dd3 --- /dev/null +++ b/extension/packages/nextjs/components/Header.tsx.args.mjs @@ -0,0 +1,6 @@ +export const menuIconImports = `import { PhotoIcon } from "@heroicons/react/24/outline";`; +export const menuObjects = `{ + label: "ERC-721", + href: "/erc721", + icon: , +}`; \ No newline at end of file