diff --git a/docs/curriculum/Day11_NFT_Metadata.md b/docs/curriculum/Day11_NFT_Metadata.md index 8f02a0c..071f12d 100644 --- a/docs/curriculum/Day11_NFT_Metadata.md +++ b/docs/curriculum/Day11_NFT_Metadata.md @@ -4,7 +4,8 @@ ## 学習目的 - 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) の「共通の前提(動作確認済みバージョン含む)」を確認してから進める。 @@ -12,12 +13,13 @@ --- ## 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 @@ -29,7 +31,9 @@ NFT_ROYALTY_BPS=500 # 5% = 500 basis points ## 1. メタデータ設計(教科書) - metadata.json 必須キー:`name`, `description`, `image`。拡張:`attributes[]`, `animation_url`。 -- 画像は`ipfs:///1.png` のように**内容アドレス**で参照。HTTP Gateway(`https://ipfs.io/ipfs/`)はプレビュー用。 +- 画像は`ipfs:///1.png` のように**内容アドレス**で参照する。CIDはcontentと生成条件から決まる識別子であり、保存場所や永続提供を単独では保証しない。 +- pinningはCIDに対応するdataをnode/serviceが保持・提供する契約、Gatewayは`ipfs://`を直接扱えないHTTP client向けの取得経路である。この3つを同じものとして扱わない。 +- HTTP Gateway(例:`https://ipfs.io/ipfs/`)はプレビュー用の取得経路であり、providerやrate limitに依存する。on-chainの参照はprovider固有URLではなく`ipfs://`を維持する。 - `baseURI` を `ipfs:///` に固定し、`tokenURI(id)` を `baseURI + id + .json` とする。 - メタデータは**凍結**(フリーズ)方針を採用。差し替えが必要ならバージョンを変えて再発行。 @@ -40,9 +44,11 @@ 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` 雛形 @@ -50,17 +56,62 @@ ipfs/ { "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を``として記録する。 +3. `ipfs/metadata/1.json`の`image`を`ipfs:///1.png`へ置き換える。 +4. `ipfs/metadata/`をfolderとしてuploadし、root CIDを``として記録する。 +5. Files画面で両方がpinning対象として保持されていることを確認し、次の値を`.env`へ設定する。 + +```bash +NFT_BASE=ipfs:/// +``` + +PinataのUIやplanが変わって手順どおりに進まない場合は、IPFS公式のPinning quickstartから、その時点で新規利用可能なWeb UI、CLI、またはself-hosted nodeを選ぶ。providerを変更しても、image/metadataの相対path、2つのfolder root CID、`NFT_BASE=ipfs:///`という成果物は変えない。 + +### 2.4 CIDとpathをupload直後に検証する + +取得用Gatewayはpinning先と別の責務である。まずservice gateway、次に別のpublic gatewayで同じCID/pathを確認する。Gateway URLをcontractへ保存しない。 + +```bash +export METADATA_ROOT_CID='' +export 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:///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作成を前提とする手順を共有しない。 --- @@ -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を確認し、`/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)` 実行 | @@ -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:///`はprovider-neutralな参照として維持できる。 +- A3. client bundleとpublic requestは利用者が読めるため、admin keyを入れると第三者が権限を再利用できる。認証済みserverにkeyを保持し、browserへはscopeと有効時間を絞ったsigned upload URLだけを返す。 ### 確認コマンド(最小) ```bash @@ -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導入前に一次資料を再確認する。 diff --git a/docs/curriculum/index.md b/docs/curriculum/index.md index bebc39b..bf75d64 100644 --- a/docs/curriculum/index.md +++ b/docs/curriculum/index.md @@ -21,7 +21,7 @@ - テスト ETH を受け取れるウォレット(Sepolia / Optimism など) - MetaMask 等のウォレット拡張(Day09 以降) - The Graph のアカウント(Day10 を実際に試す場合) -- IPFS / NFT メタデータ配信用のサービスアカウント(Day11 を実際に試す場合) +- 新規登録可能なIPFS pinning serviceのアカウント、またはself-hosted IPFS node(Day11 を実際に試す場合) ### 安全運用の前提 - 学習にはテストネット用またはローカル開発用の秘密鍵だけを使う。Mainnet や実資産を扱う鍵は使わない。 @@ -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/` が再現できれば、本教材の主要手順は概ね追従できていると判断してよい。 diff --git a/tools/check-docs-consistency.mjs b/tools/check-docs-consistency.mjs index e2625e2..5311763 100644 --- a/tools/check-docs-consistency.mjs +++ b/tools/check-docs-consistency.mjs @@ -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'); @@ -217,6 +218,36 @@ check( 'The Graph appendix must preserve the dated upstream dependency risk boundary' ); +const ipfsContract = [ + 'CID、pinning、Gateway', + 'Pinata Public IPFS', + 'ipfs:///1.png', + 'NFT_BASE=ipfs:///', + '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}`); @@ -224,4 +255,4 @@ if (errors.length) { } 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.');