Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 87 additions & 15 deletions docs/curriculum/Day11_NFT_Metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@

## 学習目的
- ERC‑721の `tokenURI` 設計とIPFSメタデータのベストプラクティスを理解し、簡単に説明できるようになる。
- 画像→IPFS→`baseURI`→ミント→`tokenURI`/Gateway で表示確認までを一連で実行できるようになる。
- 画像→content addressing→pinning→`baseURI`→ミント→`tokenURI`/Gateway で表示確認までを一連で実行できるようになる。
- provider固有のupload手順と、CID・`ipfs://`・Gatewayというprovider-neutralな契約を分離できるようになる。
- EIP‑2981(ロイヤリティ)と固定価格販売の最小例を実装し、動作確認できるようになる。

> まず [`docs/curriculum/index.md`](./index.md) の「共通の前提(動作確認済みバージョン含む)」を確認してから進める。

---

## 0. 前提
- PinataまたはInfura IPFS(Project ID/Secret)を用意。
- IPFSへ公開してよい画像だけを用意する。public IPFSへpinしたdataは、CIDを知る第三者が取得できる前提で扱う。
- 実際にuploadする場合は、新規登録可能なpinning serviceのアカウント(本章の例はPinata Public IPFS)、またはself-hosted IPFS nodeを用意する。
- 画像ファイル(例:`assets/1.png`)。
- 先に読む付録:[`docs/appendix/glossary.md`](../appendix/glossary.md)(用語に迷ったとき)
- 触るファイル(主なもの):`contracts/MyNFT.sol` / `scripts/deploy-nft.ts` / `scripts/mint-nft.ts` / `contracts/FixedPriceMarket.sol` / `test/mynft.ts`
- 今回触らないこと:NFTマーケットの本格実装(まずはtokenURI/IPFSの流れを固める)
- 最短手順(迷ったらここ):2章でIPFSに配置 → 3章の `MyNFT` をデプロイ → 4章でミント → `tokenURI`/Gateway で表示確認
- 最短手順(迷ったらここ):2章で公開dataをpinしてfolder root CIDを得る → 3章の `MyNFT` をデプロイ → 4章でミント → `tokenURI`/Gateway で表示確認

`.env.example`(項目は同梱してあるので、`.env` に値を入れる):
```bash
Expand All @@ -29,7 +31,9 @@ NFT_ROYALTY_BPS=500 # 5% = 500 basis points

## 1. メタデータ設計(教科書)
- metadata.json 必須キー:`name`, `description`, `image`。拡張:`attributes[]`, `animation_url`。
- 画像は`ipfs://<CID>/1.png` のように**内容アドレス**で参照。HTTP Gateway(`https://ipfs.io/ipfs/<CID>`)はプレビュー用。
- 画像は`ipfs://<CID>/1.png` のように**内容アドレス**で参照する。CIDはcontentと生成条件から決まる識別子であり、保存場所や永続提供を単独では保証しない。
- pinningはCIDに対応するdataをnode/serviceが保持・提供する契約、Gatewayは`ipfs://`を直接扱えないHTTP client向けの取得経路である。この3つを同じものとして扱わない。
- HTTP Gateway(例:`https://ipfs.io/ipfs/<CID>`)はプレビュー用の取得経路であり、providerやrate limitに依存する。on-chainの参照はprovider固有URLではなく`ipfs://`を維持する。
- `baseURI` を `ipfs://<CID>/` に固定し、`tokenURI(id)` を `baseURI + id + .json` とする。
- メタデータは**凍結**(フリーズ)方針を採用。差し替えが必要ならバージョンを変えて再発行。

Expand All @@ -40,27 +44,74 @@ NFT_ROYALTY_BPS=500 # 5% = 500 basis points
### 2.1 ディレクトリ構成
```text
ipfs/
├── 1.png
├── 1.json
└── _metadata_schema.md
├── images/
│ └── 1.png
└── metadata/
├── 1.json
└── _metadata_schema.md
```

### 2.2 `1.json` 雛形
```json
{
"name": "Sample #1",
"description": "Demo NFT",
"image": "ipfs://REPLACE_IMAGE_CID/1.png",
"image": "ipfs://REPLACE_IMAGE_ROOT_CID/1.png",
"attributes": [
{ "trait_type": "tier", "value": "basic" },
{ "trait_type": "series", "value": 1 }
]
}
```
> 画像 CID とメタデータ CID は**異なる**可能性がある。Pinataでフォルダ単位アップロードするとルート CID が付く
画像folderを先にpinし、そのroot CIDを`1.json`へ書いてからmetadata folderをpinする。metadataが自身のroot CIDを含む自己参照は作れないため、image root CIDとmetadata root CIDは別に記録する

### 2.3 CLI例(Pinata)
Web UIで`ipfs/`フォルダをアップロード→取得したルート CID を`NFT_BASE`に設定。
### 2.3 primary path:Public IPFSへfolderをpinする

本章では、2026-07-23(Asia/Tokyo)時点で新規accountから利用できるPinata Appをservice固有例にする。provider-neutralな学習目標は「同じdirectory構造を保持してpublic IPFSへpinし、folder root CIDを得る」ことであり、UI名や料金planの暗記ではない。

1. Pinata AppのFiles画面で、networkが**Public IPFS**であることを確認する。NFT metadataを公開する演習なのでPrivate IPFSを選ばない。
2. `ipfs/images/`をfolderとしてuploadし、root CIDを`<IMAGE_ROOT_CID>`として記録する。
3. `ipfs/metadata/1.json`の`image`を`ipfs://<IMAGE_ROOT_CID>/1.png`へ置き換える。
4. `ipfs/metadata/`をfolderとしてuploadし、root CIDを`<METADATA_ROOT_CID>`として記録する。
5. Files画面で両方がpinning対象として保持されていることを確認し、次の値を`.env`へ設定する。

```bash
NFT_BASE=ipfs://<METADATA_ROOT_CID>/
```

PinataのUIやplanが変わって手順どおりに進まない場合は、IPFS公式のPinning quickstartから、その時点で新規利用可能なWeb UI、CLI、またはself-hosted nodeを選ぶ。providerを変更しても、image/metadataの相対path、2つのfolder root CID、`NFT_BASE=ipfs://<CID>/`という成果物は変えない。

### 2.4 CIDとpathをupload直後に検証する

取得用Gatewayはpinning先と別の責務である。まずservice gateway、次に別のpublic gatewayで同じCID/pathを確認する。Gateway URLをcontractへ保存しない。

```bash
export METADATA_ROOT_CID='<METADATA_ROOT_CID>'
export IMAGE_ROOT_CID='<IMAGE_ROOT_CID>'

curl --fail --show-error --location \
"https://gateway.pinata.cloud/ipfs/${METADATA_ROOT_CID}/1.json"
curl --fail --show-error --location \
"https://dweb.link/ipfs/${METADATA_ROOT_CID}/1.json"
curl --fail --show-error --location \
"https://dweb.link/ipfs/${IMAGE_ROOT_CID}/1.png" \
--output /dev/null
```

返されたJSONの`image`が`ipfs://<CID>/1.png`を指し、そのCID/pathも取得できることを確認する。公開Gatewayはrate limit、denylist、cache、障害の影響を受けるため、1つのGatewayが失敗しただけでCIDが無効とは判断しない。

### 2.5 credential境界とAPI利用

Web Appだけを使う本章のprimary pathでは、API keyやJWTをrepositoryへ追加しない。自動uploadへ発展させる場合も、次を必須とする。

- admin JWT/API secretはserver-sideのsecret store、またはgitignore済みのserver-only環境変数だけに置く。tracked file、`.env.example`、`dapp/`の`VITE_*`、browser bundle、public log、screenshot、shell historyへ出さない。
- keyはuploadに必要な最小scopeと利用回数に制限し、演習後にrevoke/rotateする。
- browserからuploadする場合は、認証済みserverが短命のsigned upload URLを発行し、admin JWTをclientへ渡さない。
- metadataや画像にwallet seed、private key、個人情報、非公開dataを含めない。public IPFSからの削除を前提にしない。

### 2.6 Infura IPFSを使っていた読者へ

Infura公式quickstartは、新規IPFS key作成を全userで停止し、2024年後半にactiveだったkeyだけが継続accessできると明記している。そのためInfura IPFSは本章の新規onboarding routeではない。既存legacy keyを持つ場合だけ公式quickstartとaccount状態を確認して利用し、他の読者へ新規key作成を前提とする手順を共有しない。

---

Expand Down Expand Up @@ -278,6 +329,9 @@ describe('MyNFT', () => {
| 症状 | 原因 | 対応 |
|---|---|---|
| Gateway で開けない | Gateway 側の障害/レート制限、またはURIのパス不一致 | 別 Gateway で再確認。`tokenURI` とファイル名(`1.json` 等)が一致しているか確認 |
| metadata root CIDの直下に`1.json`がない | file単体でuploadした、または余分な親directoryを含めた | pinning serviceのfile treeでpathを確認し、`<CID>/1.json`になる`metadata/`をfolderとしてuploadし直す |
| upload済みなのに取得できない | pinが未完了、provider側の処理中、public/private networkの選択違い | pin statusとPublic IPFSを確認し、時間を置いて複数Gatewayで再確認する |
| API credentialがbrowser codeに必要になった | admin keyをclientへ渡す設計になっている | upload APIをserver-sideへ移し、認証済みendpointから短命signed upload URLだけを返す |
| 画像が表示されない | `image`がHTTP/HTTPSや拡張子誤り | `ipfs://CID/...png` を再確認 |
| Verify失敗 | コンストラクタ引数不一致 | 引数順序・型・設定を確認。詰まったら [`docs/appendix/verify.md`](../appendix/verify.md) |
| `safeTransferFrom`失敗 | `approve`不足 | `setApprovalForAll` または `approve(id)` 実行 |
Expand All @@ -286,19 +340,21 @@ describe('MyNFT', () => {

## 9. まとめ
- `tokenURI` とIPFSメタデータの設計(CID/パス/凍結方針)を、実装と表示確認の流れで整理した。
- CID、pinning、Gatewayを分離し、serviceを変更しても`ipfs://`とfolder内pathを維持する契約を確認した。
- public IPFSの公開範囲と、upload credentialをserver-sideへ閉じ込める境界を確認した。
- デプロイ→ミント→Gateway で表示確認までをつなぎ、`tokenURI` とファイル名の一致が重要だと分かった。
- EIP‑2981はロイヤリティ情報をsignalするinterfaceであり、支払いはマーケット側の任意実装である。receiverとBPSはconstructorで検証する。
- 固定価格マーケットの最小例を通して、実運用で必要な防御(再入対策等)を明確化した。

### 理解チェック(3問)
- Q1. NFTの `tokenURI` が指しているものは何か?オンチェーン/オフチェーンで分けて説明してみる。
- Q2. IPFS の CID と Gateway の URLは、どちらが「安定」しやすいか?理由も添える
- Q3. 固定価格マーケットで「購入できる状態」にするために、最低限必要な手順を2つ挙げる。
- Q2. CID、pinning、Gatewayはそれぞれ何を担当するか?provider固有要素も分けて説明してみる
- Q3. browserからuploadするとき、admin API keyをclient bundleへ入れてはいけない理由と代替手段は何か?

### 解答例(短く)
- A1. `tokenURI` はメタデータ(JSON等)への参照だ。オンチェーンでは参照先(文字列)を返し、オフチェーンでその参照先から名前/画像などを取得して表示する。
- A2. CID の方が内容に紐づく識別子で安定しやすい。Gateway は提供元やパスで変わり得るため、複数候補を持つと事故が減る
- A3. 例:NFTをミントする、マーケットに移転/出品できるよう `approve`(または `setApprovalForAll`)する、価格を指定してlistする
- A2. CIDはcontent-addressed identifier、pinningはdataを保持・提供し続ける契約、GatewayはHTTPで取得する経路だ。pinning serviceとGateway hostは変更できるが、`ipfs://<CID>/<path>`はprovider-neutralな参照として維持できる
- A3. client bundleとpublic requestは利用者が読めるため、admin keyを入れると第三者が権限を再利用できる。認証済みserverにkeyを保持し、browserへはscopeと有効時間を絞ったsigned upload URLだけを返す

### 確認コマンド(最小)
```bash
Expand All @@ -317,3 +373,19 @@ NFT=0x... npx hardhat run scripts/mint-nft.ts --network sepolia

## 11. 実行例
- 実行ログ例:[`docs/reports/Day11.md`](../reports/Day11.md)

## 12. Source Notes(外部service鮮度)

確認日:**2026-07-23(Asia/Tokyo)**

| 対象 | 一次資料 | この章で固定する事実 |
|---|---|---|
| IPFS content addressing | https://docs.ipfs.tech/concepts/content-addressing/ | CIDはcontentと生成条件から決まるが、保存場所を示さない |
| IPFS lifecycle / pinning | https://docs.ipfs.tech/concepts/lifecycle/ | content addressing、providing/pinning、retrievalは別段階 |
| IPFS Pinning quickstart | https://docs.ipfs.tech/quickstart/pin/ | Web UI、self-hosted node、複数pinning serviceが選択肢。public IPFS dataは公開前提 |
| IPFS Gateway | https://docs.ipfs.tech/concepts/ipfs-gateway/ | GatewayはHTTP取得経路。path/subdomain、trusted/trustlessなどのmodeがある |
| Pinata upload | https://docs.pinata.cloud/files/uploading-files | Public IPFS upload、folder upload、Web App、server-side signed upload URLが提供される |
| Pinata API key | https://docs.pinata.cloud/account-management/api-keys | keyはscope/利用回数を制限でき、revoke可能。secret/JWTは再表示されない |
| Infura IPFS quickstart | https://docs.infura.io/reference/ipfs/quickstart/ | 新規IPFS key作成は停止。2024年後半にactiveだったkeyだけが継続access |

再確認条件:pinning serviceの新規登録可否、Public IPFS/folder uploadのUI・API、plan/rate limit、Gateway host、key scope/signed URL仕様、Infuraのrestricted access表示が変わったとき。または本章の版更新前と、credentialを使うupload automation導入前に一次資料を再確認する。
4 changes: 2 additions & 2 deletions docs/curriculum/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
- テスト ETH を受け取れるウォレット(Sepolia / Optimism など)
- MetaMask 等のウォレット拡張(Day09 以降)
- The Graph のアカウント(Day10 を実際に試す場合)
- IPFS / NFT メタデータ配信用のサービスアカウント(Day11 を実際に試す場合)
- 新規登録可能なIPFS pinning serviceのアカウント、またはself-hosted IPFS node(Day11 を実際に試す場合)

### 安全運用の前提
- 学習にはテストネット用またはローカル開発用の秘密鍵だけを使う。Mainnet や実資産を扱う鍵は使わない。
Expand Down Expand Up @@ -60,7 +60,7 @@

### 2.2 確認時点と再確認ポイント
- このカリキュラムは、`package.json` / lock file / `docs/reports/` を **2026-07-22(Asia/Tokyo)時点**で確認した内容を基準としている。
- 特に変わりやすいのは、Hardhat 3のminor versionとNode.jsサポート、Solidity最新リリース、OpenZeppelin Contracts 5.x、RPC提供者のUI/APIキー取得手順、ExplorerのVerify画面、GitHub Actionsの画面導線、The Graphの管理画面である。
- 特に変わりやすいのは、Hardhat 3のminor versionとNode.jsサポート、Solidity最新リリース、OpenZeppelin Contracts 5.x、RPC提供者のUI/APIキー取得手順、IPFS pinning serviceの新規登録・upload・Gateway、ExplorerのVerify画面、GitHub Actionsの画面導線、The Graphの管理画面である。
- 本文どおりに進まない場合は、まず `npm run install:reviewed` と `npm test` が通ることを確認し、そのうえで付録の切り分け手順と各サービスの公式ドキュメントを参照する。
- 章末の「確認コマンド」と `docs/reports/` が再現できれば、本教材の主要手順は概ね追従できていると判断してよい。

Expand Down
33 changes: 32 additions & 1 deletion tools/check-docs-consistency.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const navWorkflow = read('.github/workflows/nav-link-check.yml');
const day10 = read('docs/curriculum/Day10_Events_TheGraph.md');
const subgraphReadme = read('docs/subgraph/README.md');
const graphAppendix = read('docs/appendix/the-graph.md');
const day11 = read('docs/curriculum/Day11_NFT_Metadata.md');
check(day08.includes('ethereum-roadmap-reviewed-2026-07-11'), 'Day08 current-review marker is missing');
check(changelog.includes('## 2026.07'), 'CHANGELOG latest 2026.07 section is missing');
check(home.includes('version: "2026.07"'), 'Home front matter version must be 2026.07');
Expand Down Expand Up @@ -217,11 +218,41 @@ check(
'The Graph appendix must preserve the dated upstream dependency risk boundary'
);

const ipfsContract = [
'CID、pinning、Gateway',
'Pinata Public IPFS',
'ipfs://<IMAGE_ROOT_CID>/1.png',
'NFT_BASE=ipfs://<METADATA_ROOT_CID>/',
'https://gateway.pinata.cloud/ipfs/${METADATA_ROOT_CID}/1.json',
'https://dweb.link/ipfs/${METADATA_ROOT_CID}/1.json',
'短命のsigned upload URL',
'新規IPFS key作成を全userで停止',
'2026-07-23(Asia/Tokyo)',
'https://docs.ipfs.tech/quickstart/pin/',
'https://docs.pinata.cloud/files/uploading-files',
'https://docs.infura.io/reference/ipfs/quickstart/'
];
for (const marker of ipfsContract) {
check(day11.includes(marker), `Day11 current IPFS onboarding contract is missing ${marker}`);
}
check(
!day11.includes('PinataまたはInfura IPFS(Project ID/Secret)を用意'),
'Day11 must not present Infura legacy IPFS access as a new-reader prerequisite'
);
check(
day11.includes('admin JWT/API secretはserver-sideのsecret store') &&
day11.includes('`.env.example`') &&
day11.includes('`VITE_*`') &&
day11.includes('browser bundle') &&
day11.includes('shell history'),
'Day11 must keep the upload credential exposure boundary'
);

if (errors.length) {
console.error('Documentation consistency check failed:');
for (const error of errors) console.error(`- ${error}`);
process.exit(1);
}

console.log('Documentation consistency check passed.');
console.log('Checked numbered headings, publication markers, and The Graph CLI 0.98.1 contract.');
console.log('Checked numbered headings, publication markers, The Graph CLI, and IPFS onboarding contracts.');