Skip to content

Commit

Permalink
Merge pull request #184 from scaffold-eth/challenge-7-svg-nft-init
Browse files Browse the repository at this point in the history
Challenge 7 svg nft init
  • Loading branch information
rin-st authored Jun 13, 2024
2 parents ffae39f + 29ddccb commit 8d37783
Show file tree
Hide file tree
Showing 18 changed files with 693 additions and 173 deletions.
104 changes: 85 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

---

Expand All @@ -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
```

Expand All @@ -37,40 +64,79 @@ yarn chain
> in a second terminal window, 🛰 deploy your contract (locally):
```sh
cd <challenge_folder_name>
cd challenge-7-svg-nft
yarn deploy
```

> in a third terminal window, start your 📱 frontend:
```sh
cd <challenge_folder_name>
cd challenge-7-svg-nft
yarn start
```

📱 Open http://localhost:3000 to see the app.

> 👩‍💻 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).
Expand Down
17 changes: 17 additions & 0 deletions packages/hardhat/contracts/HexStrings.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
15 changes: 15 additions & 0 deletions packages/hardhat/contracts/ToColor.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
199 changes: 199 additions & 0 deletions packages/hardhat/contracts/YourCollectible.sol
Original file line number Diff line number Diff line change
@@ -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(
'<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">',
renderTokenById(id),
"</svg>"
)
);

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(
'<g id="eye1">',
'<ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_1" cy="154.5" cx="181.5" stroke="#000" fill="#fff"/>',
'<ellipse ry="3.5" rx="2.5" id="svg_3" cy="154.5" cx="173.5" stroke-width="3" stroke="#000" fill="#000000"/>',
"</g>",
'<g id="head">',
'<ellipse fill="#',
color[id].toColor(),
'" stroke-width="3" cx="204.5" cy="211.80065" id="svg_5" rx="',
chubbiness[id].toString(),
'" ry="51.80065" stroke="#000"/>',
"</g>",
'<g id="eye2">',
'<ellipse stroke-width="3" ry="29.5" rx="29.5" id="svg_2" cy="168.5" cx="209.5" stroke="#000" fill="#fff"/>',
'<ellipse ry="3.5" rx="3" id="svg_4" cy="169.5" cx="208" stroke-width="3" fill="#000000" stroke="#000"/>',
"</g>"
'<g class="mouth" transform="translate(',
uint256((810 - 9 * chubbiness[id]) / 11).toString(),
',0)">',
'<path d="M 130 240 Q 165 250 ',
mouthLength[id].toString(),
' 235" stroke="black" stroke-width="3" fill="transparent"/>',
"</g>"
)
);

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);
}
}
Loading

0 comments on commit 8d37783

Please sign in to comment.