nft について
Midnight Network の開発者向けに、OpenZeppelin の Compact 非代替性トークンコントラクトとネイティブトークン操作を扱うスキルです。公開・非公開の所有権パターンに加え、ミント、転送、承認、メタデータ管理を説明します。
主な機能
- 01ERC721 相当の Compact コントラクト
- 02ミント、転送、承認の説明
- 03公開・非公開の所有権パターン
- 04トークン URI とメタデータコミットメント
- 05コンパイル、デプロイ、テストの説明
活用シーン
- 01公開コレクティブル NFT の開発
- 02所有権を秘匿する NFT の構築
- 03トークンのメタデータ URI 設定
- 04Compact NFT コントラクトのテスト
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ kali-decoder/ midnight-skills --skill nftインストール後、Claude Code で/nftを入力して読み込まれたか確認します。
詳細
対象範囲 このスキルでは、Midnight Network での NFT 開発を扱います。OpenZeppelin の NonFungibleToken コントラクト(Compact での ERC721 風実装)と、ネイティブな Midnight トークン操作(シールド済み/シールドなし)が含まれます。NFT のミント、転送、承認、メタデータ URI、およびプライバシーパターンを扱います。
以下のとおり、ファイルを正確に配置してください。
contracts/NonFungibleToken.compact→ OpenZeppelin のベースコントラクトcontracts/MyNFT.compact→ カスタム NFT コントラクトtest/→ コントラクトの単体テスト
1) インストール
前提条件
- Node.js v22.15+
- Compact 開発ツール:
curl --proto '=https' --tlsv1.2 -sSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh - PATH に追加:
source $HOME/.local/bin/env
Compact 用 OpenZeppelin Contracts を追加する
mkdir my-nft-project && cd my-nft-project
git init
git submodule add https://github.com/OpenZeppelin/contracts-compact.git
cd contracts-compact && SKIP_ZK=true yarn && cd ..
2) OpenZeppelin NonFungibleToken コントラクト
OpenZeppelin の NonFungibleToken モジュールは、Compact で ERC721 風の実装を提供します。
主な機能
| 機能 | 状況 | 備考 |
|---|---|---|
| トークン ID の型 | Uint<128> | Uint256 はサポートされていません(回路の制限) |
| 転送 | ✅ | ECDSA 公開鍵またはコントラクトアドレス宛て |
| 承認 | ✅ | トークン単位およびオペレーターの承認 |
| メタデータ URI | ✅ | トークンごとの URI 保存 |
| 一時停止可能 | ✅ | Pausable モジュールを通じて利用可能 |
| 所有可能 | ✅ | Ownable モジュールを通じて利用可能 |
| コントラクト間 | ❌ | まだサポートされていません(_unsafeTransfer を使用) |
インポートと使用
pragma language_version >= 0.21.0;
import CompactStandardLibrary;
import "./contracts-compact/node_modules/@openzeppelin-compact/contracts/src/access/Ownable"
prefix Ownable_;
import "./contracts-compact/node_modules/@openzeppelin-compact/contracts/src/security/Pausable"
prefix Pausable_;
import "./contracts-compact/node_modules/@openzeppelin-compact/contracts/src/token/NonFungibleToken"
prefix NonFungibleToken_;
constructor(
_name: Opaque<"string">,
_symbol: Opaque<"string">,
_initOwner: Either<ZswapCoinPublicKey, ContractAddress>,
) {
Ownable_initialize(_initOwner);
NonFungibleToken_initialize(_name, _symbol);
}
export circuit mint(
to: Either<ZswapCoinPublicKey, ContractAddress>,
tokenId: Uint<128>,
uri: Opaque<"string">,
): [] {
Ownable_assertOnlyOwner();
NonFungibleToken__mint(to, tokenId);
NonFungibleToken__setTokenURI(tokenId, uri);
}
export circuit transferFrom(
from: Either<ZswapCoinPublicKey, ContractAddress>,
to: Either<ZswapCoinPublicKey, ContractAddress>,
tokenId: Uint<128>,
): [] {
Pausable_assertNotPaused();
NonFungibleToken_transferFrom(from, to, tokenId);
}
export circuit approve(
to: Either<ZswapCoinPublicKey, ContractAddress>,
tokenId: Uint<128>,
): [] {
NonFungibleToken_approve(to, tokenId);
}
export circuit setApprovalForAll(
operator: Either<ZswapCoinPublicKey, ContractAddress>,
approved: Boolean,
): [] {
Ownable_assertOnlyOwner();
NonFungibleToken_setApprovalForAll(operator, approved);
}
export circuit pause(): [] {
Ownable_assertOnlyOwner();
Pausable__pause();
}
export circuit unpause(): [] {
Ownable_assertOnlyOwner();
Pausable__unpause();
}
3) ネイティブな Midnight NFT パターン
Midnight の標準ライブラリは、シールド済み/シールドなしの操作に対応したネイティブトークン関数を提供します。
シールドなし NFT(公開)
import CompactStandardLibrary;
export ledger nextTokenId: Uint<128>;
export ledger owners: Map<Uint<128>, Either<ZswapCoinPublicKey, ContractAddress>>;
export ledger tokenURIs: Map<Uint<128>, Opaque<"string">>;
export circuit mint(
to: Either<ZswapCoinPublicKey, ContractAddress>,
uri: Opaque<"string">,
): Uint<128> {
const tokenId = nextTokenId;
nextTokenId += 1;
owners.insert(disclose(tokenId), disclose(to));
tokenURIs.insert(disclose(tokenId), disclose(uri));
return tokenId;
}
export circuit transfer(
from: Either<ZswapCoinPublicKey, ContractAddress>,
to: Either<ZswapCoinPublicKey, ContractAddress>,
tokenId: Uint<128>,
): [] {
const owner = owners.lookup(disclose(tokenId));
assert(owner == disclose(from), "Not owner");
owners.insert(disclose(tokenId), disclose(to));
}
export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> {
return tokenURIs.lookup(disclose(tokenId));
}
シールド済み NFT(非公開の所有権)
Compact 標準ライブラリのシールド済みトークン関数を使用します。
import CompactStandardLibrary;
// Mint a shielded NFT
export circuit mintShielded(
to: ZswapCoinPublicKey,
metadataHash: Field,
): Uint<128> {
const tokenId = nextTokenId;
nextTokenId += 1;
// Store commitment to ownership + metadata
const commitment = persistentCommit<Uint<128>>(tokenId, freshNonce());
shieldedCommitments.insert(disclose(to), commitment);
return tokenId;
}
// Transfer shielded NFT (prove ownership without revealing tokenId publicly)
export circuit transferShielded(
tokenId: Uint<128>,
newOwner: ZswapCoinPublicKey,
): [] {
// Use ZK proof to demonstrate ownership
// Owner's shielded wallet signs the transfer
const oldCommitment = shieldedCommitments.lookup(disclose(callerPublicKey()));
assert(oldCommitment != default<Field>(), "Not owner");
shieldedCommitments.remove(disclose(callerPublicKey()));
shieldedCommitments.insert(disclose(newOwner), oldCommitment);
}
4) コントラクトをコンパイルする
cd contracts
compact compile MyNFT.compact artifacts/MyNFT
予想される出力:
Compiling 5 circuits:
circuit "mint" (k=11, rows=1180)
circuit "transferFrom" (k=11, rows=1966)
circuit "approve" (k=10, rows=966)
circuit "pause" (k=10, rows=125)
circuit "unpause" (k=10, rows=121)
Overall progress [====================] 5/5
5) デプロイと操作(TypeScript)
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';
import { Contract } from './contracts/managed/MyNFT';
const compiledContract = CompiledContract.make('MyNFT', Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets('./contracts/managed/MyNFT'),
);
const providers = await buildProviders(/* see midnight-js skill */);
const deployed = await deployContract(providers, {
compiledContract,
privateStateId: 'nftPrivateState',
initialPrivateState: {},
});
console.log('NFT Contract:', deployed.deployTxData.public.contractAddress);
// Mint an NFT
const mintResult = await deployed.callTx.mint(
{ type: 'left', value: callerPublicKey() },
1n, // tokenId
'https://my-nft.com/metadata/1.json',
);
6) シールド済み NFT とシールドなし NFT
| 項目 | シールドなし(公開) | シールド済み(非公開) |
|---|---|---|
| 所有権 | オンチェーンで公開 | 非公開で、所有者のみが把握 |
| 転送 | 誰でも確認可能 | 非公開、ZK 証明で証明 |
| メタデータ | オンチェーンで公開 URI | コミット/ハッシュ化が可能 |
| ガス/ユーザーコスト | DUST 手数料 | DUST 手数料(プレビューでは 1AM がスポンサー) |
| ユースケース | パブリックアート、コレクティブル | 機密資産、非公開のコレクティブル |
| トークン ID | 可視 | コミットメントで非表示にできる |
シールド済み NFT を使用する場合
- 所有権の機密性:NFT の所有者を非公開にする必要がある場合
- 非公開のコレクティブル:所有権が機密情報となる高価値アイテム
- コンプライアンス・バイ・デザイン:必要な場合にのみ所有権を開示
シールドなし NFT を使用する場合
- パブリックアート:所有権が価値提案の一部となる場合
- 透明性の高い DAO:NFT としてのガバナンストークン
- 標準的なコレクティブル:公開された来歴が価値を高める場合
7) メタデータの処理
公開メタデータ(シールドなし)
export ledger tokenURIs: Map<Uint<128>, Opaque<"string">;
export circuit setTokenURI(
tokenId: Uint<128>,
uri: Opaque<"string">,
): [] {
tokenURIs.insert(disclose(tokenId), disclose(uri));
}
非公開メタデータ(シールド済み)
// Store only a commitment on-chain
export ledger metadataCommitments: Map<Uint<128>, Field>;
export circuit setPrivateMetadata(
tokenId: Uint<128>,
metadataHash: Field, // hash of off-chain metadata
): [] {
const commitment = persistentCommit<Uint<128>>(tokenId, freshNonce());
metadataCommitments.insert(disclose(tokenId), commitment);
}
8) よくある落とし穴
| 問題 | 解決策 |
|---|---|
Uint<256> のトークン ID | サポートされていません。最大で Uint<128> を使用してください(回路の制約)。 |
| コントラクト間の転送 | まだサポートされていません。_unsafeTransfer は注意して使用してください。 |
| ベース URI の文字列連結 | Compact ではサポートされていません。トークンごとに URI 全体を保存してください。 |
tokenId のオーバーフロー | ミント前に assert(tokenId < MAX_ID, "ID overflow") を使用してください。 |
URI に対する disclose() の欠落 | メタデータ URI は公開情報です。保存時に disclose() を使用してください。 |
| 証明のないシールド済み転送 | 所有者が転送に署名する必要があります。callerPublicKey() で確認してください。 |
9) NFT コントラクトのテスト
// In contracts/src/test/MyNFT.test.compact
pragma language_version >= 0.21.0;
import MyNFT;
import CompactStandardLibrary;
constructor() {
MyNFT_initialize("MyNFT", "MNFT");
}
export circuit testMint(): [] {
const tokenId = MyNFT_mint(disclose(callerPublicKey()), "uri1");
assert(tokenId == 1, "First token should be 1");
}
export circuit testTransfer(): [] {
MyNFT_transferFrom(
callerPublicKey(),
someOtherKey(),
1,
);
const newOwner = MyNFT_ownerOf(1);
assert(newOwner == someOtherKey(), "Transfer failed");
}
10) package.json スクリプト
{
"scripts": {
"compact": "cd contracts && compact compile MyNFT.compact artifacts/MyNFT",
"build": "tsc && npm run compact",
"test": "cd contracts && compact compile MyNFT.test.compact artifacts/MyNFT.test"
}
}
11) 便利なリンク
- Compact 向け OpenZeppelin Contracts — ライブラリのドキュメント
- Compact 言語ガイド — スマートコントラクト言語のリファレンス
- Midnight トークン関数 — ネイティブトークンの操作
- 1AM ウォレットの統合 — NFT とのやり取り用にウォレットを接続
- Compact スキル — 言語の基礎
コメント
Markdown 対応