docs: rewrite README as learning note; archive old cheat-sheet
CI / Foundry project (push) Has been cancelled

- README.md: comprehensive learning-oriented writeup (contracts, structure,
  setup, concepts, gotchas) matching the actual code/Makefile.
- notes/OLD-README-cheatsheet.md: verbatim archive of the original raw
  command scratch-pad + base64 outputs.
This commit is contained in:
2026-08-19 19:42:03 +08:00
parent 50bacbd40c
commit c8e59061de
2 changed files with 188 additions and 11 deletions
+155 -11
View File
@@ -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/<YOUR_KEY>
ETHERSCAN_API_KEY=<YOUR_KEY>
SEPOLIA_PRIVATE_KEY=<YOUR_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": "<ipfs or data: URI>",
"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,<encoded>
```
- `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://<CID>` 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)
+33
View File
@@ -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.