nft

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Scope This skill covers NFT development on Midnight Network. It includes the OpenZeppelin NonFungibleToken contract (ERC721-like implementation in Compact) and native Midnight token operations (shielded/unshielded). Covers minting, transfers, approvals, metadata URIs, and privacy patterns for NFTs.
Place files exactly as shown:
  • contracts/NonFungibleToken.compact
    → OpenZeppelin base contract
  • contracts/MyNFT.compact
    → your custom NFT contract
  • test/
    → contract unit tests
适用范围 本技能涵盖Midnight网络上的NFT开发内容,包括OpenZeppelin的NonFungibleToken合约(Compact中的类ERC721实现)以及Midnight原生代币操作(隐私保护/非隐私保护)。内容覆盖NFT的铸造、转账、授权、元数据URI以及隐私模式。
请严格按照以下结构放置文件:
  • contracts/NonFungibleToken.compact
    → OpenZeppelin基础合约
  • contracts/MyNFT.compact
    → 自定义NFT合约
  • test/
    → 合约单元测试

1) Installation

1) 安装

Prerequisites

前置条件

  • Node.js v22.15+
  • Compact devtools:
    curl --proto '=https' --tlsv1.2 -sSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
  • Add to PATH:
    source $HOME/.local/bin/env
  • 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

Add OpenZeppelin Contracts for Compact

为Compact添加OpenZeppelin合约

bash
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 ..
bash
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 Contract

2) OpenZeppelin NonFungibleToken合约

The OpenZeppelin
NonFungibleToken
module provides an ERC721-like implementation in Compact.
OpenZeppelin的
NonFungibleToken
模块提供了Compact中的类ERC721实现。

Key Features

核心功能

FeatureStatusNotes
Token ID type
Uint<128>
Uint256 not supported (circuit limits)
TransfersTo ECDSA public keys or contract addresses
ApprovalsPer-token and operator approvals
Metadata URIPer-token URI storage
PausableThrough Pausable module
OwnableThrough Ownable module
Contract-to-contractNot yet supported (use
_unsafeTransfer
)
功能状态说明
Token ID类型
Uint<128>
不支持Uint256(受电路限制)
转账支持转至ECDSA公钥或合约地址
授权支持单代币授权和操作员授权
元数据URI支持单代币URI存储
可暂停通过Pausable模块实现
可拥有通过Ownable模块实现
合约间交互暂不支持(使用
_unsafeTransfer

Import and Use

导入与使用

compact
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();
}
compact
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) Native Midnight NFT Patterns

3) Midnight原生NFT模式

Midnight's standard library provides native token functions for shielded/unshielded operations.
Midnight标准库提供了用于隐私保护/非隐私保护操作的原生代币函数。

Unshielded NFT (Public)

非隐私保护NFT(公开)

compact
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));
}
compact
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));
}

Shielded NFT (Private Ownership)

隐私保护NFT(私有所有权)

Use the Compact standard library's shielded token functions:
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);
}
使用Compact标准库的隐私保护代币函数:
compact
import CompactStandardLibrary;

// 铸造隐私保护NFT
export circuit mintShielded(
  to: ZswapCoinPublicKey,
  metadataHash: Field,
): Uint<128> {
  const tokenId = nextTokenId;
  nextTokenId += 1;

  // 存储所有权+元数据的承诺
  const commitment = persistentCommit<Uint<128>>(tokenId, freshNonce());
  shieldedCommitments.insert(disclose(to), commitment);

  return tokenId;
}

// 转账隐私保护NFT(无需公开tokenId即可证明所有权)
export circuit transferShielded(
  tokenId: Uint<128>,
  newOwner: ZswapCoinPublicKey,
): [] {
  // 使用ZK证明展示所有权
  // 所有者的隐私保护钱包签署转账
  const oldCommitment = shieldedCommitments.lookup(disclose(callerPublicKey()));
  assert(oldCommitment != default<Field>(), "Not owner");

  shieldedCommitments.remove(disclose(callerPublicKey()));
  shieldedCommitments.insert(disclose(newOwner), oldCommitment);
}

4) Compile the Contract

4) 编译合约

bash
cd contracts
compact compile MyNFT.compact artifacts/MyNFT
Expected output:
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
bash
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) Deploy and Interact (TypeScript)

5) 部署与交互(TypeScript)

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',
);
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(/* 参考midnight-js技能文档 */);
const deployed = await deployContract(providers, {
  compiledContract,
  privateStateId: 'nftPrivateState',
  initialPrivateState: {},
});

console.log('NFT合约地址:', deployed.deployTxData.public.contractAddress);

// 铸造NFT
const mintResult = await deployed.callTx.mint(
  { type: 'left', value: callerPublicKey() },
  1n, // tokenId
  'https://my-nft.com/metadata/1.json',
);

6) Shielded vs Unshielded NFTs

6) 隐私保护型vs非隐私保护型NFT

AspectUnshielded (Public)Shielded (Private)
OwnershipPublic on-chainPrivate, only owner knows
TransfersVisible to allHidden, ZK-proven
MetadataPublic URI on-chainCan be committed/hashed
Gas/user costDUST feesDUST fees (1AM sponsors on preview)
Use casePublic art, collectiblesConfidential assets, private collectibles
Token IDVisibleCan be hidden with commitments
维度非隐私保护型(公开)隐私保护型(私有)
所有权链上公开私有,仅所有者知晓
转账全网可见隐藏,基于ZK证明
元数据链上公开URI可提交哈希值
Gas/用户成本DUST费用DUST费用(预览版由1AM赞助)
使用场景公开艺术作品、收藏品保密资产、私有收藏品
Token ID可见可通过承诺隐藏

When to Use Shielded NFTs

何时使用隐私保护型NFT

  • Confidential ownership: When who owns the NFT should remain private
  • Private collectibles: High-value items where ownership is sensitive
  • Compliance-by-design: Only reveal ownership when required
  • 保密所有权: 需要隐藏NFT所有者身份时
  • 私有收藏品: 高价值物品,所有权需保密时
  • 合规设计: 仅在必要时披露所有权

When to Use Unshielded NFTs

何时使用非隐私保护型NFT

  • Public art: Ownership is part of the value proposition
  • Transparent DAOs: Governance tokens as NFTs
  • Standard collectibles: Where public provenance adds value
  • 公开艺术: 所有权是价值的一部分时
  • 透明DAO: 治理代币以NFT形式存在时
  • 标准收藏品: 公开来源能提升价值时

7) Metadata Handling

7) 元数据处理

Public Metadata (Unshielded)

公开元数据(非隐私保护型)

compact
export ledger tokenURIs: Map<Uint<128>, Opaque<"string">;

export circuit setTokenURI(
  tokenId: Uint<128>,
  uri: Opaque<"string">,
): [] {
  tokenURIs.insert(disclose(tokenId), disclose(uri));
}
compact
export ledger tokenURIs: Map<Uint<128>, Opaque<"string">;

export circuit setTokenURI(
  tokenId: Uint<128>,
  uri: Opaque<"string">,
): [] {
  tokenURIs.insert(disclose(tokenId), disclose(uri));
}

Private Metadata (Shielded)

私有元数据(隐私保护型)

compact
// 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);
}
compact
// 仅在链上存储承诺
export ledger metadataCommitments: Map<Uint<128>, Field>;

export circuit setPrivateMetadata(
  tokenId: Uint<128>,
  metadataHash: Field, // 链上元数据的哈希值
): [] {
  const commitment = persistentCommit<Uint<128>>(tokenId, freshNonce());
  metadataCommitments.insert(disclose(tokenId), commitment);
}

8) Common Pitfalls

8) 常见陷阱

IssueSolution
Uint<256>
token IDs
Not supported. Use
Uint<128>
max (circuit constraint).
Contract-to-contract transfersNot yet supported. Use
_unsafeTransfer
with caution.
String concatenation for base URINot supported in Compact. Store full URI per token.
tokenId
overflow
Use
assert(tokenId < MAX_ID, "ID overflow")
before minting.
Missing
disclose()
on URI
Metadata URIs are public. Use
disclose()
when storing.
Shielded transfer without proofOwner must sign transfers. Verify with
callerPublicKey()
.
问题解决方案
Uint<256>
代币ID
不支持,使用最大
Uint<128>
(受电路约束)
合约间转账暂不支持,谨慎使用
_unsafeTransfer
拼接Base URI生成完整URICompact不支持,为每个代币存储完整URI
tokenId
溢出
铸造前使用
assert(tokenId < MAX_ID, "ID overflow")
检查
URI未使用
disclose()
元数据URI是公开的,存储时需使用
disclose()
隐私保护转账无证明所有者必须签署转账,通过
callerPublicKey()
验证

9) Testing NFT Contracts

9) NFT合约测试

compact
// 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");
}
compact
// 在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, "第一个代币ID应为1");
}

export circuit testTransfer(): [] {
  MyNFT_transferFrom(
    callerPublicKey(),
    someOtherKey(),
    1,
  );
  const newOwner = MyNFT_ownerOf(1);
  assert(newOwner == someOtherKey(), "转账失败");
}

10) Package.json Scripts

10) Package.json脚本

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"
  }
}
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) Useful Links

11) 有用链接