diff --git a/README.md b/README.md index 29ec068..e7acfa3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,165 @@ -https://eips.ethereum.org/EIPS/eip-721 +# Foundry NFT — Learning Project -forge init -forge install OpenZeppelin/openzeppelin-contracts --no-commit -forge install ChainAccelOrg/foundry-devops --no-commit +A hands-on **Solidity / Foundry** learning project for building and deploying ERC-721 NFTs. It walks through the full lifecycle — writing the contract, embedding an image fully on-chain, storing metadata on IPFS, deploying to a local Anvil node or Sepolia testnet, and minting/viewing the result in MetaMask. -// Local Anvil Test +> This is a **learning repo**, not production code. The contract and tests are deliberately kept small and heavily commented so each concept is easy to follow. See [Concepts covered](#concepts-covered) for the study notes. + +--- + +## What this project does + +It implements **two** NFT contracts and the scripts + tests to drive them: + +| Contract | Description | +|----------|-------------| +| `BasicNft.sol` | A minimal ERC-721 where `mintNft(tokenUri)` stores an arbitrary URI (usually an IPFS URI) per token. | +| `MoodNft.sol` | A "dynamic" / fully-on-chain SVG NFT. The artwork is encoded in the contract itself and can be *flipped* between Happy and Sad states by the owner. | + +Both inherit OpenZeppelin's `ERC721` — so most of the heavy lifting (transfer, approval, `safeMint`, `balanceOf`, `ownerOf`) comes for free. + +--- + +## Project structure + +``` +├── src/ +│ ├── BasicNft.sol # Minimal ERC-721 with per-token URI +│ └── MoodNft.sol # On-chain SVG NFT with flipMood() +├── script/ +│ ├── DeployBasicNft.s.sol # Deploys BasicNft +│ ├── DeployMoodNft.s.sol # Reads img/*.svg, base64-encodes them, deploys MoodNft +│ └── Interactions.s.sol # MintBasicNft / MintMoodNft (uses DevOpsTools to find latest deployment) +├── test/ +│ ├── unit/ # Isolated unit tests (DeployMoodNftTest, MoodNftTest) +│ └── interactions/ # Integration tests (BasicNftTest, MoodNftIntegrationTest) +├── img/ # Source SVG art + example metadata JSONs for IPFS +├── lib/ # Submodules: forge-std, openzeppelin-contracts, foundry-devops +├── foundry.toml # Foundry config (build, remappings, rpc/etherscan) +├── Makefile # Convenience targets (deploy, mint, test, anvil...) +├── sample1-deploy.txt # Reference log: Sepolia deploy of BasicNft +└── sample2-mint.txt # Reference log: Sepolia mint of BasicNft +``` + +--- + +## Getting started + +### 1. Install Foundry + +```bash +curl -L https://foundry.paradigm.xyz | bash +foundryup +``` + +### 2. Install dependencies (git submodules) + +```bash +git submodule update --init --recursive +``` + +### 3. Build & test + +```bash +forge build +forge test +``` + +The test suite runs 6 tests across 4 suites and should pass green. + +--- + +## Run against a local Anvil node + +```bash +# Terminal 1 — start a local EVM +make anvil + +# Terminal 2 — deploy and mint make deploy make mint +``` -// Sepolia Test +`make anvil` starts `anvil` with a known test mnemonic and 1-second block time. The default deployer key is Anvil's first account (`0xac09...`), already configured in the `Makefile`. + +--- + +## Deploy to Sepolia testnet + +Set up your `.env` (ignored by git): + +```bash +SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/ +ETHERSCAN_API_KEY= +SEPOLIA_PRIVATE_KEY= +``` + +Then: + +```bash make deploy ARGS="--network sepolia" make mint ARGS="--network sepolia" +``` -// MetaMask add deployed contract address with token id start with 0 +The `make deploy`/`make mint` targets auto-switch to the Sepolia RPC + auto-verify on Etherscan when `--network sepolia` is passed. See `sample1-deploy.txt` and `sample2-mint.txt` for what a successful run looks like. -base64 -i smile.svg -data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjAwIDIwMCIgd2lkdGg9IjQwMCIgIGhlaWdodD0iNDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxjaXJjbGUgY3g9IjEwMCIgY3k9IjEwMCIgZmlsbD0ieWVsbG93IiByPSI3OCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIi8+CiAgPGcgY2xhc3M9ImV5ZXMiPgogICAgPGNpcmNsZSBjeD0iNzAiIGN5PSI4MiIgcj0iMTIiLz4KICAgIDxjaXJjbGUgY3g9IjEyNyIgY3k9IjgyIiByPSIxNC41Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0xMzcgMTE2LjUzYy42OSAyNi4xNy02NC4xMSA0Mi04MS41Mi0uNzMiIHN0eWxlPSJmaWxsOm5vbmU7IHN0cm9rZTogYmxhY2s7IHN0cm9rZS13aWR0aDogMzsiLz4KPC9zdmc+ +> **Viewing the NFT in MetaMask:** add the deployed contract + token ID (starts at `0`) as a collectible. For `BasicNft` you'll see the IPFS image; for `MoodNft` the artwork is rendered from the on-chain base64 SVG. -base64 -i sad.svg -data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8c3ZnIHdpZHRoPSIxMDI0cHgiIGhlaWdodD0iMTAyNHB4IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGZpbGw9IiMzMzMiIGQ9Ik01MTIgNjRDMjY0LjYgNjQgNjQgMjY0LjYgNjQgNTEyczIwMC42IDQ0OCA0NDggNDQ4IDQ0OC0yMDAuNiA0NDgtNDQ4Uzc1OS40IDY0IDUxMiA2NHptMCA4MjBjLTIwNS40IDAtMzcyLTE2Ni42LTM3Mi0zNzJzMTY2LjYtMzcyIDM3Mi0zNzIgMzcyIDE2Ni42IDM3MiAzNzItMTY2LjYgMzcyLTM3MiAzNzJ6Ii8+CiAgPHBhdGggZmlsbD0iI0U2RTZFNiIgZD0iTTUxMiAxNDBjLTIwNS40IDAtMzcyIDE2Ni42LTM3MiAzNzJzMTY2LjYgMzcyIDM3MiAzNzIgMzcyLTE2Ni42IDM3Mi0zNzItMTY2LjYtMzcyLTM3Mi0zNzJ6TTI4OCA0MjFhNDguMDEgNDguMDEgMCAwIDEgOTYgMCA0OC4wMSA0OC4wMSAwIDAgMS05NiAwem0zNzYgMjcyaC00OC4xYy00LjIgMC03LjgtMy4yLTguMS03LjRDNjA0IDYzNi4xIDU2Mi41IDU5NyA1MTIgNTk3cy05Mi4xIDM5LjEtOTUuOCA4OC42Yy0uMyA0LjItMy45IDcuNC04LjEgNy40SDM2MGE4IDggMCAwIDEtOC04LjRjNC40LTg0LjMgNzQuNS0xNTEuNiAxNjAtMTUxLjZzMTU1LjYgNjcuMyAxNjAgMTUxLjZhOCA4IDAgMCAxLTggOC40em0yNC0yMjRhNDguMDEgNDguMDEgMCAwIDEgMC05NiA0OC4wMSA0OC4wMSAwIDAgMSAwIDk2eiIvPgogIDxwYXRoIGZpbGw9IiMzMzMiIGQ9Ik0yODggNDIxYTQ4IDQ4IDAgMSAwIDk2IDAgNDggNDggMCAxIDAtOTYgMHptMjI0IDExMmMtODUuNSAwLTE1NS42IDY3LjMtMTYwIDE1MS42YTggOCAwIDAgMCA4IDguNGg0OC4xYzQuMiAwIDcuOC0zLjIgOC4xLTcuNCAzLjctNDkuNSA0NS4zLTg4LjYgOTUuOC04OC42czkyIDM5LjEgOTUuOCA4OC42Yy4zIDQuMiAzLjkgNy40IDguMSA3LjRINjY0YTggOCAwIDAgMCA4LTguNEM2NjcuNiA2MDAuMyA1OTcuNSA1MzMgNTEyIDUzM3ptMTI4LTExMmE0OCA0OCAwIDEgMCA5NiAwIDQ4IDQ4IDAgMSAwLTk2IDB6Ii8+Cjwvc3ZnPg== +--- + +## Concepts covered + +### 1. ERC-721 essentials (OpenZeppelin) +- Inheriting `ERC721` gives you the full token standard (ERC-165, transfers, approvals, metadata). +- `_safeMint(to, tokenId)` — guarantees the recipient can actually receive ERC-721s (reverts for plain EOAs that would lose the token). +- Custom errors (`error BasicNft__TokenUriNotFound();`) — modern, gas-cheaper alternative to `require` with string messages. + +### 2. `tokenURI` and metadata +- `tokenURI(tokenId)` returns the location of the token's **metadata JSON** — not necessarily the raw image. +- The standard metadata shape: + ```json + { + "name": "...", + "description": "...", + "image": "", + "attributes": [ { "trait_type": "...", "value": "..." } ] + } + ``` + Store this JSON on IPFS, then point the contract at its `ipfs://` CID. Example metadata JSONs live in `img/*.json`. + +### 3. Fully on-chain SVG (`MoodNft`) +- Instead of hosting art on IPFS, the SVG is **base64-encoded and embedded in the contract**: + ``` + data:image/svg+xml;base64, + ``` +- `DeployMoodNft` uses `vm.readFile("img/smile.svg")` + `Base64.encode(...)` (OpenZeppelin) to build these image URIs at deploy time. +- `tokenURI` then builds a JSON `data:application/json;base64,...` URI on the fly, so the *entire NFT* is self-contained on-chain. +- `flipMood(tokenId)` mutates on-chain state (`s_tokenIdToMood`), so the NFT "changes expression" — a gentle intro to **dynamic NFTs**. + +### 4. Foundry testing patterns +- **Unit tests** (`test/unit/`) instantiate contracts directly with fixed inputs. +- **Integration tests** (`test/interactions/`) run the *actual deploy scripts* (`deployer.run()`) to exercise the full path. +- `vm.prank(USER)` — impersonate an address for the next call. +- `makeAddr("HOELEE")` — deterministic fake address from a label. +- **String comparison gotcha:** you can't `==` two `string`s in Solidity. Compare their hashes instead: + ```solidity + keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)) + ``` + +### 5. DevOpsTools (foundry-devops) +`DevOpsTools.get_most_recent_deployment("BasicNft", block.chainid)` reads the `broadcast/` logs to find the latest deployed contract — so the mint script automatically targets the last deployment on the current chain instead of a hardcoded address. + +--- + +## Notes & gotchas + +- **Line endings / SVG:** the deploy script base64-encodes whatever bytes `vm.readFile` returns. The repo pins `img/*.svg` to LF via `.gitattributes` so the on-chain bytes are **deterministic** regardless of platform (Windows/`core.autocrlf` would otherwise inject CRLF and silently change the encoded art). +- **`remappings`** in `foundry.toml` maps `@openzeppelin/contracts/...` → `lib/openzeppelin-contracts/contracts`. +- **IPFS pinning:** `Interactions.s.sol` contains both IPFS CIDs and Pinata gateway URLs as examples. Prefer the raw `ipfs://` form in production so the asset resolves independent of any single gateway. + +--- + +## Reference + +- [ERC-721 standard (EIP-721)](https://eips.ethereum.org/EIPS/eip-721) +- [Foundry book](https://book.getfoundry.sh/) +- [OpenZeppelin Contracts](https://github.com/OpenZeppelin/openzeppelin-contracts) diff --git a/notes/OLD-README-cheatsheet.md b/notes/OLD-README-cheatsheet.md new file mode 100644 index 0000000..193faa0 --- /dev/null +++ b/notes/OLD-README-cheatsheet.md @@ -0,0 +1,33 @@ +# OLD README — raw setup notes (archived) + +The original README was a scratch-pad of raw commands and base64 outputs. It is preserved here verbatim for reference. + +--- + +https://eips.ethereum.org/EIPS/eip-721 + +forge init +forge install OpenZeppelin/openzeppelin-contracts --no-commit +forge install ChainAccelOrg/foundry-devops --no-commit + +// Local Anvil Test +make deploy +make mint + +// Sepolia Test +make deploy ARGS="--network sepolia" +make mint ARGS="--network sepolia" + +// MetaMask add deployed contract address with token id start with 0 + +base64 -i smile.svg +data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjAwIDIwMCIgd2lkdGg9IjQwMCIgIGhlaWdodD0iNDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxjaXJjbGUgY3g9IjEwMCIgY3k9IjEwMCIgZmlsbD0ieWVsbG93IiByPSI3OCIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIzIi8+CiAgPGcgY2xhc3M9ImV5ZXMiPgogICAgPGNpcmNsZSBjeD0iNzAiIGN5PSI4MiIgcj0iMTIiLz4KICAgIDxjaXJjbGUgY3g9IjEyNyIgY3k9IjgyIiByPSIxNC41Ii8+CiAgPC9nPgogIDxwYXRoIGQ9Im0xMzcgMTE2LjUzYy42OSAyNi4xNy02NC4xMSA0Mi04MS41Mi0uNzMiIHN0eWxlPSJmaWxsOm5vbmU7IHN0cm9rZTogYmxhY2s7IHN0cm9rZS13aWR0aDogMzsiLz4KPC9zdmc+ + +base64 -i sad.svg +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8c3ZnIHdpZHRoPSIxMDI0cHgiIGhlaWdodD0iMTAyNHB4IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxwYXRoIGZpbGw9IiMzMzMiIGQ9Ik01MTIgNjRDMjY0LjYgNjQgNjQgMjY0LjYgNjQgNTEyczIwMC42IDQ0OCA0NDggNDQ4IDQ0OC0yMDAuNiA0NDgtNDQ4Uzc1OS40IDY0IDUxMiA2NHptMCA4MjBjLTIwNS40IDAtMzcyLTE2Ni42LTM3Mi0zNzJzMTY2LjYtMzcyIDM3Mi0zNzIgMzcyIDE2Ni42IDM3MiAzNzItMTY2LjYgMzcyLTM3MiAzNzJ6Ii8+CiAgPHBhdGggZmlsbD0iI0U2RTZFNiIgZD0iTTUxMiAxNDBjLTIwNS40IDAtMzcyIDE2Ni42LTM3MiAzNzJzMTY2LjYgMzcyIDM3MiAzNzIgMzcyLTE2Ni42IDM3Mi0zNzItMTY2LjYtMzcyLTM3Mi0zNzJ6TTI4OCA0MjFhNDguMDEgNDguMDEgMCAwIDEgOTYgMCA0OC4wMSA0OC4wMSAwIDAgMS05NiAwem0zNzYgMjcyaC00OC4xYy00LjIgMC03LjgtMy4yLTguMS03LjRDNjA0IDYzNi4xIDU2Mi41IDU5NyA1MTIgNTk3cy05Mi4xIDM5LjEtOTUuOCA4OC42Yy0uMyA0LjItMy45IDcuNC04LjEgNy40SDM2MGE4IDggMCAwIDEtOC04LjRjNC40LTg0LjMgNzQuNS0xNTEuNiAxNjAtMTUxLjZzMTU1LjYgNjcuMyAxNjAgMTUxLjZhOCA4IDAgMCAxLTggOC40em0yNC0yMjRhNDguMDEgNDguMDEgMCAwIDEgMC05NiA0OC4wMSA0OC4wMSAwIDAgMSAwIDk2eiIvPgogIDxwYXRoIGZpbGw9IiMzMzMiIGQ9Ik0yODggNDIxYTQ4IDQ4IDAgMSAwIDk2IDAgNDggNDggMCAxIDAtOTYgMHptMjI0IDExMmMtODUuNSAwLTE1NS42IDY3LjMtMTYwIDE1MS42YTggOCAwIDAgMCA4IDguNGg0OC4xYzQuMiAwIDcuOC0zLjIgOC4xLTcuNCAzLjctNDkuNSA0NS4zLTg4LjYgOTUuOC04OC42czkyIDM5LjEgOTUuOCA4OC42Yy4zIDQuMiAzLjkgNy40IDguMSA3LjRINjY0YTggOCAwIDAgMCA4LTguNEM2NjcuNiA2MDAuMyA1OTcuNSA1MzMgNTEyIDUzM3ptMTI4LTExMmE0OCA0OCAwIDEgMCA5NiAwIDQ4IDQ4IDAgMSAwLTk2IDB6Ii8+Cjwvc3ZnPg== + +--- + +## Archive note + +`img/base64Smile.txt` and `img/base64Sad.txt` also hold the base64-encoded SVG strings generated from the source SVGs. The deploy script (`script/DeployMoodNft.s.sol`) generates these at build/deploy time via `vm.readFile` + `Base64.encode`, so the `.txt` files are only kept as readable reference.