nft
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseScope
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:
- → OpenZeppelin base contract
contracts/NonFungibleToken.compact - → your custom NFT contract
contracts/MyNFT.compact - → contract unit tests
test/
适用范围
本技能涵盖Midnight网络上的NFT开发内容,包括OpenZeppelin的NonFungibleToken合约(Compact中的类ERC721实现)以及Midnight原生代币操作(隐私保护/非隐私保护)。内容覆盖NFT的铸造、转账、授权、元数据URI以及隐私模式。
请严格按照以下结构放置文件:
- → OpenZeppelin基础合约
contracts/NonFungibleToken.compact - → 自定义NFT合约
contracts/MyNFT.compact - → 合约单元测试
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 module provides an ERC721-like implementation in Compact.
NonFungibleTokenOpenZeppelin的模块提供了Compact中的类ERC721实现。
NonFungibleTokenKey Features
核心功能
| Feature | Status | Notes |
|---|---|---|
| Token ID type | | Uint256 not supported (circuit limits) |
| Transfers | ✅ | To ECDSA public keys or contract addresses |
| Approvals | ✅ | Per-token and operator approvals |
| Metadata URI | ✅ | Per-token URI storage |
| Pausable | ✅ | Through Pausable module |
| Ownable | ✅ | Through Ownable module |
| Contract-to-contract | ❌ | Not yet supported (use |
| 功能 | 状态 | 说明 |
|---|---|---|
| Token ID类型 | | 不支持Uint256(受电路限制) |
| 转账 | ✅ | 支持转至ECDSA公钥或合约地址 |
| 授权 | ✅ | 支持单代币授权和操作员授权 |
| 元数据URI | ✅ | 支持单代币URI存储 |
| 可暂停 | ✅ | 通过Pausable模块实现 |
| 可拥有 | ✅ | 通过Ownable模块实现 |
| 合约间交互 | ❌ | 暂不支持(使用 |
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/MyNFTExpected 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/5bash
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/55) 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
| Aspect | Unshielded (Public) | Shielded (Private) |
|---|---|---|
| Ownership | Public on-chain | Private, only owner knows |
| Transfers | Visible to all | Hidden, ZK-proven |
| Metadata | Public URI on-chain | Can be committed/hashed |
| Gas/user cost | DUST fees | DUST fees (1AM sponsors on preview) |
| Use case | Public art, collectibles | Confidential assets, private collectibles |
| Token ID | Visible | Can 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) 常见陷阱
| Issue | Solution |
|---|---|
| Not supported. Use |
| Contract-to-contract transfers | Not yet supported. Use |
| String concatenation for base URI | Not supported in Compact. Store full URI per token. |
| Use |
Missing | Metadata URIs are public. Use |
| Shielded transfer without proof | Owner must sign transfers. Verify with |
| 问题 | 解决方案 |
|---|---|
| 不支持,使用最大 |
| 合约间转账 | 暂不支持,谨慎使用 |
| 拼接Base URI生成完整URI | Compact不支持,为每个代币存储完整URI |
| 铸造前使用 |
URI未使用 | 元数据URI是公开的,存储时需使用 |
| 隐私保护转账无证明 | 所有者必须签署转账,通过 |
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) 有用链接
- OpenZeppelin Contracts for Compact — Library documentation
- Compact Language Guide — Smart contract language reference
- Midnight Token Functions — Native token operations
- 1AM Wallet Integration — Connect wallet for NFT interactions
- Compact Skill — Language fundamentals
- OpenZeppelin Contracts for Compact — 库文档
- Compact语言指南 — 智能合约语言参考
- Midnight代币函数 — 原生代币操作
- 1AM钱包集成 — 连接钱包进行NFT交互
- Compact技能文档 — 语言基础 ",