diff --git a/README.md b/README.md index 77d8e78f..6ea45fc6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,40 @@ -# 🚩 Challenge {challengeNum}: {challengeEmoji} {challengeTitle} +# 🚩 Challenge 7: 🎁 SVG NFT -{challengeHeroImage} +![readme-7](https://github.com/scaffold-eth/se-2-challenges/assets/25638585/94178d41-f7ce-4d0f-af9a-488a224d301f) -A {challengeDescription}. +🎨 Creating on-chain SVG NFTs is an exciting way to leverage the power of smart contracts for generating unique digital art. This challenge will have you build a contract that generates dynamic SVG images directly on the blockchain. Users will be able to mint their own unique NFTs with customizable SVG graphics and metadata. -🌟 The final deliverable is an app that {challengeDeliverable}. -Deploy your contracts to a testnet then build and upload your app to a public web server. Submit the url on [SpeedRunEthereum.com](https://speedrunethereum.com)! +πŸ”— Your contract will handle the creation and storage of the SVG code, ensuring each minted NFT is unique and stored entirely on-chain. This approach keeps the artwork decentralized and immutable. -πŸ’¬ Meet other builders working on this challenge and get help in the {challengeTelegramLink} +πŸ’Ž The objective is to develop an app that allows users to mint their own dynamic SVG NFTs. Customize your SVG generation logic and make the minting process interactive and engaging. + +πŸš€ Once your project is live, share the minting URL so others can see and mint their unique SVG NFTs! + +🌟 Use Loogies NFT as an example to guide your project. This will provide a solid foundation and inspiration for creating your own dynamic SVG NFTs. + +> πŸ’¬ Meet other builders working on this challenge and get help in the [🎁 SVG NFT 🎫 Building Cohort](https://t.me/+mUeITJ5u7Ig0ZWJh)! + +--- + +## πŸ“œ Quest Journal 🧭 + +This challenge is brimming with creative freedom, giving you the opportunity to explore various approaches! + +🌟 To help guide your efforts, consider the following goals. Additionally, the current branch includes an example of SVG NFTs, the Loogies. Feel free to use it as inspiration or start your project entirely from scratch! πŸš€ + +### πŸ₯… Goals: + +- [ ] Design and implement SVG generation logic within the contract +- [ ] Add metadata generation functionality to the smart contract +- [ ] Make sure metadata is stored and retrievable on-chain +- [ ] Ensure each minted NFT is unique and customizable +- [ ] Create UI for minting and interaction with your smart contracts + +### βš”οΈ Side Quests: + +- [ ] Leave the minting funds in the contract, so the minter does not pay extra gas to send the funds to the recipient address. Create a `Withdraw()` function to allow the owner to withdraw the funds. +- [ ] Explore other [pricing models for minting NFTs](https://docs.artblocks.io/creator-docs/minter-suite/minting-philosophy/), such as dutch auctions (with or without settlement) +- [ ] Set different phases for minting, such as a discount for early adopters (allowlisted). Manage the allowlist and the functions to switch between phases. --- @@ -22,9 +49,9 @@ Before you begin, you need to install the following tools: Then download the challenge to your computer and install dependencies by running: ```sh -git clone https://github.com/scaffold-eth/se-2-challenges.git {challengeName} -cd {challengeName} -git checkout {challengeName} +git clone https://github.com/scaffold-eth/se-2-challenges.git challenge-7-svg-nft +cd challenge-7-svg-nft +git checkout challenge-7-svg-nft yarn install ``` @@ -37,14 +64,14 @@ yarn chain > in a second terminal window, πŸ›° deploy your contract (locally): ```sh -cd +cd challenge-7-svg-nft yarn deploy ``` > in a third terminal window, start your πŸ“± frontend: ```sh -cd +cd challenge-7-svg-nft yarn start ``` @@ -52,25 +79,64 @@ yarn start > πŸ‘©β€πŸ’» Rerun `yarn deploy --reset` whenever you want to deploy new contracts to the frontend, update your current contracts with changes, or re-deploy it to get a fresh contract address. -πŸ” Now you are ready to edit your smart contract `{mainContractName.sol}` in `packages/hardhat/contracts` +πŸ” Now you are ready to edit your smart contracts `YourCollectible.sol` in `packages/hardhat/contracts` --- -_Other commonly used Checkpoints (check one Challenge and adapt the texts for your own):_ +## Checkpoint 1: πŸ’Ύ Deploy your contracts! πŸ›° + +πŸ“‘ Edit the `defaultNetwork` to [your choice of public EVM networks](https://ethereum.org/en/developers/docs/networks/) in `packages/hardhat/hardhat.config.ts` -## Checkpoint {num}: πŸ’Ύ Deploy your contract! πŸ›° +πŸ” You will need to generate a **deployer address** using `yarn generate` This creates a mnemonic and saves it locally. -## Checkpoint {num}: 🚒 Ship your frontend! 🚁 +πŸ‘©β€πŸš€ Use `yarn account` to view your deployer account balances. -## Checkpoint {num}: πŸ“œ Contract Verification +⛽️ You will need to send ETH to your **deployer address** with your wallet, or get it from a public faucet of your chosen network. + +πŸš€ Run `yarn deploy` to deploy your smart contract to a public network (selected in `hardhat.config.ts`) + +> πŸ’¬ Hint: You can set the `defaultNetwork` in `hardhat.config.ts` to `sepolia` **OR** you can `yarn deploy --network sepolia`. --- -_Create all the required Checkpoints for the Challenge, can also add Side Quests you think may be interesting to complete. Check other Challenges for inspiration._ +## Checkpoint 2: 🚒 Ship your frontend! 🚁 + +✏️ Edit your frontend config in `packages/nextjs/scaffold.config.ts` to change the `targetNetwork` to `chains.sepolia` or any other public network. + +πŸ’» View your frontend at http://localhost:3000 and verify you see the correct network. -### βš”οΈ Side Quests +πŸ“‘ When you are ready to ship the frontend app... + +πŸ“¦ Run `yarn vercel` to package up your frontend and deploy. + +> Follow the steps to deploy to Vercel. Once you log in (email, github, etc), the default options should work. It'll give you a public URL. + +> If you want to redeploy to the same production URL you can run `yarn vercel --prod`. If you omit the `--prod` flag it will deploy it to a preview/test URL. + +> 🦊 Since we have deployed to a public testnet, you will now need to connect using a wallet you own or use a burner wallet. By default πŸ”₯ `burner wallets` are only available on `hardhat` . You can enable them on every chain by setting `onlyLocalBurnerWallet: false` in your frontend config (`scaffold.config.ts` in `packages/nextjs/`) + +#### Configuration of Third-Party Services for Production-Grade Apps. + +By default, πŸ— Scaffold-ETH 2 provides predefined API keys for popular services such as Alchemy and Etherscan. This allows you to begin developing and testing your applications more easily, avoiding the need to register for these services. +This is great to complete your **SpeedRunEthereum**. + +For production-grade applications, it's recommended to obtain your own API keys (to prevent rate limiting issues). You can configure these at: + +- πŸ”·`ALCHEMY_API_KEY` variable in `packages/hardhat/.env` and `packages/nextjs/.env.local`. You can create API keys from the [Alchemy dashboard](https://dashboard.alchemy.com/). + +- πŸ“ƒ`ETHERSCAN_API_KEY` variable in `packages/hardhat/.env` with your generated API key. You can get your key [here](https://etherscan.io/myapikey). + +> πŸ’¬ Hint: It's recommended to store env's for nextjs in Vercel/system env config for live apps and use .env.local for local testing. + +--- + +## Checkpoint 3: πŸ“œ Contract Verification + +Run the `yarn verify --network your_network` command to verify your contracts on etherscan πŸ›° + +--- -_To finish your README, can add these links_ +> πŸ‘©β€β€οΈβ€πŸ‘¨ Share your public url with friends, showcase your art on-chain, and enjoy the minting experience togetherπŸŽ‰!! > πŸƒ Head to your next challenge [here](https://speedrunethereum.com). diff --git a/packages/hardhat/contracts/HexStrings.sol b/packages/hardhat/contracts/HexStrings.sol new file mode 100644 index 00000000..eeeeb07c --- /dev/null +++ b/packages/hardhat/contracts/HexStrings.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library HexStrings { + bytes16 internal constant ALPHABET = '0123456789abcdef'; + + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = '0'; + buffer[1] = 'x'; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = ALPHABET[value & 0xf]; + value >>= 4; + } + return string(buffer); + } +} diff --git a/packages/hardhat/contracts/ToColor.sol b/packages/hardhat/contracts/ToColor.sol new file mode 100644 index 00000000..8c0b8f7f --- /dev/null +++ b/packages/hardhat/contracts/ToColor.sol @@ -0,0 +1,15 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library ToColor { + bytes16 internal constant ALPHABET = "0123456789abcdef"; + + function toColor(bytes3 value) internal pure returns (string memory) { + bytes memory buffer = new bytes(6); + for (uint256 i = 0; i < 3; i++) { + buffer[i * 2 + 1] = ALPHABET[uint8(value[i]) & 0xf]; + buffer[i * 2] = ALPHABET[uint8(value[i] >> 4) & 0xf]; + } + return string(buffer); + } +} diff --git a/packages/hardhat/contracts/YourCollectible.sol b/packages/hardhat/contracts/YourCollectible.sol new file mode 100644 index 00000000..2ce55143 --- /dev/null +++ b/packages/hardhat/contracts/YourCollectible.sol @@ -0,0 +1,199 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "base64-sol/base64.sol"; + +import "./HexStrings.sol"; +import "./ToColor.sol"; + +//learn more: https://docs.openzeppelin.com/contracts/3.x/erc721 + +// GET LISTED ON OPENSEA: https://testnets.opensea.io/get-listed/step-two + +contract YourCollectible is ERC721Enumerable, Ownable { + using Strings for uint256; + using HexStrings for uint160; + using ToColor for bytes3; + using Counters for Counters.Counter; + Counters.Counter private _tokenIds; + + // all funds go to buidlguidl.eth + address payable public constant recipient = + payable(0xa81a6a910FeD20374361B35C451a4a44F86CeD46); + + uint256 public constant limit = 3728; + uint256 public constant curve = 1002; // price increase 0,4% with each purchase + uint256 public price = 0.001 ether; + // the 1154th optimistic loogies cost 0.01 ETH, the 2306th cost 0.1ETH, the 3459th cost 1 ETH and the last ones cost 1.7 ETH + + mapping(uint256 => bytes3) public color; + mapping(uint256 => uint256) public chubbiness; + mapping(uint256 => uint256) public mouthLength; + + constructor() ERC721("OptimisticLoogies", "OPLOOG") { + // RELEASE THE OPTIMISTIC LOOGIES! + } + + function mintItem() public payable returns (uint256) { + require(_tokenIds.current() < limit, "DONE MINTING"); + require(msg.value >= price, "NOT ENOUGH"); + + price = (price * curve) / 1000; + + _tokenIds.increment(); + + uint256 id = _tokenIds.current(); + _mint(msg.sender, id); + + bytes32 predictableRandom = keccak256( + abi.encodePacked( + id, + blockhash(block.number - 1), + msg.sender, + address(this) + ) + ); + color[id] = + bytes2(predictableRandom[0]) | + (bytes2(predictableRandom[1]) >> 8) | + (bytes3(predictableRandom[2]) >> 16); + chubbiness[id] = + 35 + + ((55 * uint256(uint8(predictableRandom[3]))) / 255); + // small chubiness loogies have small mouth + mouthLength[id] = + 180 + + ((uint256(chubbiness[id] / 4) * + uint256(uint8(predictableRandom[4]))) / 255); + + (bool success, ) = recipient.call{ value: msg.value }(""); + require(success, "could not send"); + + return id; + } + + function tokenURI(uint256 id) public view override returns (string memory) { + require(_exists(id), "not exist"); + string memory name = string( + abi.encodePacked("Loogie #", id.toString()) + ); + string memory description = string( + abi.encodePacked( + "This Loogie is the color #", + color[id].toColor(), + " with a chubbiness of ", + uint2str(chubbiness[id]), + " and mouth length of ", + uint2str(mouthLength[id]), + "!!!" + ) + ); + string memory image = Base64.encode(bytes(generateSVGofTokenById(id))); + + return + string( + abi.encodePacked( + "data:application/json;base64,", + Base64.encode( + bytes( + abi.encodePacked( + '{"name":"', + name, + '", "description":"', + description, + '", "external_url":"https://burnyboys.com/token/', + id.toString(), + '", "attributes": [{"trait_type": "color", "value": "#', + color[id].toColor(), + '"},{"trait_type": "chubbiness", "value": ', + uint2str(chubbiness[id]), + '},{"trait_type": "mouthLength", "value": ', + uint2str(mouthLength[id]), + '}], "owner":"', + (uint160(ownerOf(id))).toHexString(20), + '", "image": "', + "data:image/svg+xml;base64,", + image, + '"}' + ) + ) + ) + ) + ); + } + + function generateSVGofTokenById( + uint256 id + ) internal view returns (string memory) { + string memory svg = string( + abi.encodePacked( + '', + renderTokenById(id), + "" + ) + ); + + return svg; + } + + // Visibility is `public` to enable it being called by other contracts for composition. + function renderTokenById(uint256 id) public view returns (string memory) { + // the translate function for the mouth is based on the curve y = 810/11 - 9x/11 + string memory render = string( + abi.encodePacked( + '', + '', + '', + "", + '', + '', + "", + '', + '', + '', + "" + '', + '', + "" + ) + ); + + return render; + } + + function uint2str( + uint _i + ) internal pure returns (string memory _uintAsString) { + if (_i == 0) { + return "0"; + } + uint j = _i; + uint len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (48 + uint8(_i - (_i / 10) * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } +} diff --git a/packages/hardhat/contracts/YourContract.sol b/packages/hardhat/contracts/YourContract.sol deleted file mode 100644 index 3d364a0e..00000000 --- a/packages/hardhat/contracts/YourContract.sol +++ /dev/null @@ -1,87 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <0.9.0; - -// Useful for debugging. Remove when deploying to a live network. -import "hardhat/console.sol"; - -// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc) -// import "@openzeppelin/contracts/access/Ownable.sol"; - -/** - * A smart contract that allows changing a state variable of the contract and tracking the changes - * It also allows the owner to withdraw the Ether in the contract - * @author BuidlGuidl - */ -contract YourContract { - // State Variables - address public immutable owner; - string public greeting = "Building Unstoppable Apps!!!"; - bool public premium = false; - uint256 public totalCounter = 0; - mapping(address => uint) public userGreetingCounter; - - // Events: a way to emit log statements from smart contract that can be listened to by external parties - event GreetingChange( - address indexed greetingSetter, - string newGreeting, - bool premium, - uint256 value - ); - - // Constructor: Called once on contract deployment - // Check packages/hardhat/deploy/00_deploy_your_contract.ts - constructor(address _owner) { - owner = _owner; - } - - // Modifier: used to define a set of rules that must be met before or after a function is executed - // Check the withdraw() function - modifier isOwner() { - // msg.sender: predefined variable that represents address of the account that called the current function - require(msg.sender == owner, "Not the Owner"); - _; - } - - /** - * Function that allows anyone to change the state variable "greeting" of the contract and increase the counters - * - * @param _newGreeting (string memory) - new greeting to save on the contract - */ - function setGreeting(string memory _newGreeting) public payable { - // Print data to the hardhat chain console. Remove when deploying to a live network. - console.log( - "Setting new greeting '%s' from %s", - _newGreeting, - msg.sender - ); - - // Change state variables - greeting = _newGreeting; - totalCounter += 1; - userGreetingCounter[msg.sender] += 1; - - // msg.value: built-in global variable that represents the amount of ether sent with the transaction - if (msg.value > 0) { - premium = true; - } else { - premium = false; - } - - // emit: keyword used to trigger an event - emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value); - } - - /** - * Function that allows the owner to withdraw all the Ether in the contract - * The function can only be called by the owner of the contract as defined by the isOwner modifier - */ - function withdraw() public isOwner { - (bool success, ) = owner.call{ value: address(this).balance }(""); - require(success, "Failed to send Ether"); - } - - /** - * Function that allows the contract to receive ETH - */ - receive() external payable {} -} diff --git a/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/00_deploy_your_collectible.ts similarity index 73% rename from packages/hardhat/deploy/00_deploy_your_contract.ts rename to packages/hardhat/deploy/00_deploy_your_collectible.ts index 716fec79..57d05127 100644 --- a/packages/hardhat/deploy/00_deploy_your_contract.ts +++ b/packages/hardhat/deploy/00_deploy_your_collectible.ts @@ -1,14 +1,13 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DeployFunction } from "hardhat-deploy/types"; -import { Contract } from "ethers"; /** - * Deploys a contract named "YourContract" using the deployer account and + * Deploys a contract named "YourCollectible" using the deployer account and * constructor arguments set to the deployer address * * @param hre HardhatRuntimeEnvironment object. */ -const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { +const deployYourCollectible: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { /* On localhost, the deployer account is the one that comes with Hardhat, which is already funded. @@ -22,10 +21,10 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn const { deployer } = await hre.getNamedAccounts(); const { deploy } = hre.deployments; - await deploy("YourContract", { + await deploy("YourCollectible", { from: deployer, // Contract constructor arguments - args: [deployer], + args: [], 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. @@ -33,12 +32,11 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn }); // Get the deployed contract to interact with it after deploying. - const yourContract = await hre.ethers.getContract("YourContract", deployer); - console.log("πŸ‘‹ Initial greeting:", await yourContract.greeting()); + // const yourContract = await hre.ethers.getContract("YourCollectible", deployer); }; -export default deployYourContract; +export default deployYourCollectible; // Tags are useful if you have multiple deploy files and only want to run one of them. // e.g. yarn deploy --tags YourContract -deployYourContract.tags = ["YourContract"]; +deployYourCollectible.tags = ["YourCollectible"]; diff --git a/packages/hardhat/hardhat.config.ts b/packages/hardhat/hardhat.config.ts index d165b65b..ca241e03 100644 --- a/packages/hardhat/hardhat.config.ts +++ b/packages/hardhat/hardhat.config.ts @@ -24,7 +24,7 @@ const config: HardhatUserConfig = { version: "0.8.17", settings: { optimizer: { - enabled: true, + enabled: false, // https://docs.soliditylang.org/en/latest/using-the-compiler.html#optimizer-options runs: 200, }, diff --git a/packages/hardhat/package.json b/packages/hardhat/package.json index 0b619c59..ff5d51e8 100644 --- a/packages/hardhat/package.json +++ b/packages/hardhat/package.json @@ -49,6 +49,7 @@ "dependencies": { "@openzeppelin/contracts": "^4.8.1", "@typechain/ethers-v6": "^0.5.1", + "base64-sol": "^1.1.0", "dotenv": "^16.0.3", "envfile": "^6.18.0", "qrcode": "^1.5.1" diff --git a/packages/hardhat/test/ChallengeN.ts b/packages/hardhat/test/ChallengeN.ts deleted file mode 100644 index b91b96b7..00000000 --- a/packages/hardhat/test/ChallengeN.ts +++ /dev/null @@ -1,46 +0,0 @@ -// -// This script executes when you run 'yarn test' -// - -import { ethers } from "hardhat"; -import { YourContract } from "../typechain-types/contracts/YourContract"; -import { expect } from "chai"; - -describe("🚩 Challenge N: Description", function () { - // Change to name and type of your contract - let yourContract: YourContract; - - describe("Deployment", function () { - const contractAddress = process.env.CONTRACT_ADDRESS; - - // Don't change contractArtifact creation - let contractArtifact: string; - if (contractAddress) { - // For the autograder. - contractArtifact = `contracts/download-${contractAddress}.sol:YourContract`; - } else { - contractArtifact = "contracts/YourContract.sol:YourContract"; - } - - it("Should deploy the contract", async function () { - const [owner] = await ethers.getSigners(); - const yourContractFactory = await ethers.getContractFactory(contractArtifact); - yourContract = (await yourContractFactory.deploy(owner.address)) as YourContract; - console.log("\t", " πŸ›° Contract deployed on", await yourContract.getAddress()); - }); - }); - - // Test group example - describe("Initialization and change of greeting", function () { - it("Should have the right message on deploy", async function () { - expect(await yourContract.greeting()).to.equal("Building Unstoppable Apps!!!"); - }); - - it("Should allow setting a new message", async function () { - const newGreeting = "Learn Scaffold-ETH 2! :)"; - - await yourContract.setGreeting(newGreeting); - expect(await yourContract.greeting()).to.equal(newGreeting); - }); - }); -}); diff --git a/packages/nextjs/app/layout.tsx b/packages/nextjs/app/layout.tsx index 66afa609..3e7d7140 100644 --- a/packages/nextjs/app/layout.tsx +++ b/packages/nextjs/app/layout.tsx @@ -9,7 +9,7 @@ const baseUrl = process.env.VERCEL_URL : `http://localhost:${process.env.PORT || 3000}`; const imageUrl = `${baseUrl}/thumbnail.jpg`; -const title = "SpeedRunEthereum"; +const title = "Challenge #7 | SpeedRunEthereum"; const titleTemplate = "%s | SpeedRunEthereum"; const description = "Built with πŸ— Scaffold-ETH 2"; diff --git a/packages/nextjs/app/loogies/page.tsx b/packages/nextjs/app/loogies/page.tsx new file mode 100644 index 00000000..36313a5a --- /dev/null +++ b/packages/nextjs/app/loogies/page.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import type { NextPage } from "next"; +import { formatEther } from "viem"; +import { useAccount } from "wagmi"; +import { Address } from "~~/components/scaffold-eth"; +import { useScaffoldContract, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; + +const Loogies: NextPage = () => { + const { address: connectedAddress } = useAccount(); + const [allLoogies, setAllLoogies] = useState(); + const [page, setPage] = useState(1n); + const [loadingLoogies, setLoadingLoogies] = useState(true); + const perPage = 12n; + + const { data: price } = useScaffoldReadContract({ + contractName: "YourCollectible", + functionName: "price", + }); + + const { data: totalSupply } = useScaffoldReadContract({ + contractName: "YourCollectible", + functionName: "totalSupply", + }); + + const { writeContractAsync } = useScaffoldWriteContract("YourCollectible"); + + const { data: contract } = useScaffoldContract({ + contractName: "YourCollectible", + }); + + useEffect(() => { + const updateAllLoogies = async () => { + setLoadingLoogies(true); + if (contract && totalSupply) { + const collectibleUpdate = []; + const startIndex = totalSupply - 1n - perPage * (page - 1n); + for (let tokenIndex = startIndex; tokenIndex > startIndex - perPage && tokenIndex >= 0; tokenIndex--) { + try { + const tokenId = await contract.read.tokenByIndex([tokenIndex]); + const tokenURI = await contract.read.tokenURI([tokenId]); + const jsonManifestString = atob(tokenURI.substring(29)); + + try { + const jsonManifest = JSON.parse(jsonManifestString); + collectibleUpdate.push({ id: tokenId, uri: tokenURI, ...jsonManifest }); + } catch (e) { + console.log(e); + } + } catch (e) { + console.log(e); + } + } + console.log("Collectible Update: ", collectibleUpdate); + setAllLoogies(collectibleUpdate); + } + setLoadingLoogies(false); + }; + updateAllLoogies(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [totalSupply, page, perPage, Boolean(contract)]); + + return ( + <> +
+
+ Loogie +
+
+

+ OptimisticLoogies + Loogies with a smile :) +

+
+
Only 3728 Optimistic Loogies available on a price curve increasing 0.2% with each new mint.
+
+ Double the supply of the{" "} + + Original Ethereum Mainnet Loogies + +
+
+
+ +

{Number(3728n - (totalSupply || 0n))} Loogies left

+
+
+ +
+
+ {loadingLoogies ? ( +

Loading...

+ ) : !allLoogies?.length ? ( +

No loogies minted

+ ) : ( +
+
+ {allLoogies.map(loogie => { + return ( +
+

{loogie.name}

+ {loogie.name} +

{loogie.description}

+
+
+ ); + })} +
+
+
+ {page > 1n && ( + + )} + + {totalSupply !== undefined && totalSupply > page * perPage && ( + + )} +
+
+
+ )} +
+
+
+ + ); +}; + +export default Loogies; diff --git a/packages/nextjs/app/page.tsx b/packages/nextjs/app/page.tsx index 6231ccbf..1ced72fc 100644 --- a/packages/nextjs/app/page.tsx +++ b/packages/nextjs/app/page.tsx @@ -1,3 +1,5 @@ +import Image from "next/image"; +import Link from "next/link"; import type { NextPage } from "next"; const Home: NextPage = () => { @@ -6,16 +8,39 @@ const Home: NextPage = () => {

SpeedRunEthereum - Challenge #X: Challenge Title + Challenge #7: 🎁 SVG NFT

-

- Get started by editing{" "} - packages/nextjs/page/app.tsx -

-

- Edit your smart contract YourContract.sol in{" "} - packages/hardhat/contracts -

+
+ challenge banner +
+

+ 🎨 Creating on-chain SVG NFTs is an exciting way to leverage the power of smart contracts for generating + unique digital art. This challenge will have you build a contract that generates dynamic SVG images + directly on the blockchain. Users will be able to mint their own unique NFTs with customizable SVG + graphics and metadata. +

+

+ 🌟 Use{" "} + + Loogies + {" "} + as an example to guide your project. This will provide a solid foundation and inspiration for creating + your own dynamic SVG NFTs. +

+

+ πŸ’¬ Meet other builders working on this challenge and get help in the{" "} + + 🎁 SVG NFT 🎫 Building Cohort + +

+
+
); diff --git a/packages/nextjs/app/your-loogies/page.tsx b/packages/nextjs/app/your-loogies/page.tsx new file mode 100644 index 00000000..0e86b24e --- /dev/null +++ b/packages/nextjs/app/your-loogies/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import type { NextPage } from "next"; +import { formatEther } from "viem"; +import { useAccount } from "wagmi"; +import { useScaffoldContract, useScaffoldReadContract, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; + +const YourLoogies: NextPage = () => { + const { address: connectedAddress } = useAccount(); + const [yourLoogies, setYourLoogies] = useState(); + const [loadingLoogies, setLoadingLoogies] = useState(true); + + const { data: price } = useScaffoldReadContract({ + contractName: "YourCollectible", + functionName: "price", + }); + + const { data: totalSupply } = useScaffoldReadContract({ + contractName: "YourCollectible", + functionName: "totalSupply", + }); + + const { data: balance } = useScaffoldReadContract({ + contractName: "YourCollectible", + functionName: "balanceOf", + args: [connectedAddress], + }); + + const { writeContractAsync } = useScaffoldWriteContract("YourCollectible"); + + const { data: contract } = useScaffoldContract({ + contractName: "YourCollectible", + }); + + useEffect(() => { + const updateAllLoogies = async () => { + setLoadingLoogies(true); + if (contract && balance && connectedAddress) { + const collectibleUpdate = []; + for (let tokenIndex = 0n; tokenIndex < balance; tokenIndex++) { + try { + const tokenId = await contract.read.tokenOfOwnerByIndex([connectedAddress, tokenIndex]); + const tokenURI = await contract.read.tokenURI([tokenId]); + const jsonManifestString = atob(tokenURI.substring(29)); + + try { + const jsonManifest = JSON.parse(jsonManifestString); + collectibleUpdate.push({ id: tokenId, uri: tokenURI, ...jsonManifest }); + } catch (e) { + console.log(e); + } + } catch (e) { + console.log(e); + } + } + console.log("Collectible Update: ", collectibleUpdate); + setYourLoogies(collectibleUpdate); + } + setLoadingLoogies(false); + }; + updateAllLoogies(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [balance, connectedAddress, Boolean(contract)]); + + return ( + <> +
+
+ Loogie +
+
+

+ Your Loogies +

+
+ +

{Number(3728n - (totalSupply || 0n))} Loogies left

+
+
+ +
+
+ {loadingLoogies ? ( +

Loading...

+ ) : !yourLoogies?.length ? ( +

No loogies minted

+ ) : ( +
+
+ {yourLoogies.map(loogie => { + return ( +
+

{loogie.name}

+ {loogie.name} +

{loogie.description}

+
+ ); + })} +
+
+ )} +
+
+
+ + ); +}; + +export default YourLoogies; diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx index 955b8f35..79200cbc 100644 --- a/packages/nextjs/components/Header.tsx +++ b/packages/nextjs/components/Header.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useRef, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline"; +import { Bars3Icon, BriefcaseIcon, BugAntIcon, FaceSmileIcon } from "@heroicons/react/24/outline"; import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; import { useOutsideClick } from "~~/hooks/scaffold-eth"; @@ -19,6 +19,16 @@ export const menuLinks: HeaderMenuLink[] = [ label: "Home", href: "/", }, + { + label: "All Loogies", + href: "/loogies", + icon: , + }, + { + label: "Your Loogies", + href: "/your-loogies", + icon: , + }, { label: "Debug Contracts", href: "/debug", diff --git a/packages/nextjs/public/hero.png b/packages/nextjs/public/hero.png new file mode 100644 index 00000000..6a866b41 Binary files /dev/null and b/packages/nextjs/public/hero.png differ diff --git a/packages/nextjs/public/loogie.svg b/packages/nextjs/public/loogie.svg new file mode 100644 index 00000000..53d30dc5 --- /dev/null +++ b/packages/nextjs/public/loogie.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/packages/nextjs/public/your-loogie.svg b/packages/nextjs/public/your-loogie.svg new file mode 100644 index 00000000..777578f6 --- /dev/null +++ b/packages/nextjs/public/your-loogie.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 91ad73fb..1a3f763f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2519,6 +2519,7 @@ __metadata: "@types/qrcode": ^1 "@typescript-eslint/eslint-plugin": latest "@typescript-eslint/parser": latest + base64-sol: ^1.1.0 chai: ^4.3.6 dotenv: ^16.0.3 envfile: ^6.18.0 @@ -5386,6 +5387,13 @@ __metadata: languageName: node linkType: hard +"base64-sol@npm:^1.1.0": + version: 1.1.0 + resolution: "base64-sol@npm:1.1.0" + checksum: 9bea828af71e6adcf7c841cf0001e03bd1eae8cc1aef86daedb36f04117b2a626ac23631f700d56a24515f88c2021d7ebdbee4608369f66c53e51c0b4b39ee47 + languageName: node + linkType: hard + "bcrypt-pbkdf@npm:^1.0.0": version: 1.0.2 resolution: "bcrypt-pbkdf@npm:1.0.2"