ModulesTokens

NonFungibleToken

NonFungibleToken is a specification for non-fungible tokens, a type of token where all the units are unique and distinct from each other. This module is an approximation of EIP-721 written in the Compact programming language for the Midnight network.

ERC721 Compatibility

Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. Some features and behaviors are either not possible, not possible yet, or changed because of the vastly different tech stack and Compact language constraints.

Notable changes

  • Uint<128> tokenIds - Since 256-bit unsigned integers are not supported, the library uses the Compact type Uint<128>.
  • No _baseURI() support - Native strings and string operations are not supported within the Compact language, so concatenating a base URI + token ID is not possible like in other NFT implementations. Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. It's up to the implementation to decide on how to handle this.

Features and specifications NOT supported

  • Events - Midnight does not currently support events, but this is planned on being supported in the future.
  • Uint256 type - There's ongoing research on ways to support uint256 in the future.
  • Interface - Compact currently does not have a way to define a contract interface. This library offers modules of contracts with free floating circuits; nevertheless, there are no means of enforcing that all circuits are provided.
  • ERC-165 Standard - Since Compact doesn't provide a way to define a contract interface, it's not possible to implement an ERC-165 like interface standard at this time.
  • Safe Transfers - It's not possible to implement safe transfers without an ERC-165-like interface standard at this time.

Contract-to-contract calls

Contract-to-contract calls are currently not supported in the Compact language. Due to this limitation, the current iteration of NonFungibleToken disallows transfers and mints to the ContractAddress type. Transferring tokens to a contract may result in those tokens being locked forever. The NonFungibleToken module, however, does provide unsafe circuit variants for users who wish to experiment with sending tokens to contracts.

The unsafe circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning _transfer, _mint, etc. are planned to eventually allow the recipients to be of the ContractAddress type.

Identity Model

NonFungibleToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key by injecting it through the wit_NonFungibleTokenSK witness. The module computes the caller's account identifier as persistentHash(secretKey), a commitment that hides the key while providing a stable, pseudonymous on-chain identity. That identifier is the Bytes<32> (left) branch of the Either<Bytes<32>, ContractAddress> account type and is what approve, setApprovalForAll, and transferFrom check ownership and operator rights against.

What is public on the ledger:

  • _owners, keyed by token id, and _balances, keyed by account identifier (or contract address).
  • _tokenApprovals, keyed by token id, and _operatorApprovals, keyed by owner and then operator.
  • _tokenURIs, _name, and _symbol.

What the caller holds privately:

  • The 32-byte secretKey. It never reaches the ledger; only its hash does.

A transfer discloses the token id, the previous owner, and the recipient, because the owner and balance maps are updated with those values. A single-token approval discloses the token id and the approved account; an operator approval discloses the caller's identifier, the operator, and the flag.

Because the identifier is H(secretKey) with no per-deployment salt, the same key produces the same identity in every contract. Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer.

To derive an account identifier off-chain (for example, to pass as the recipient of a mint), use computeAccountId from the Utils module:

import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"
  prefix Utils_;

export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> {
  return Utils_computeAccountId(secretKey);
}

A contract that re-exports it this way exposes pureCircuits.computeAccountId(secretKey) to its TypeScript side. The Utils guide shows the equivalent computation with the Compact runtime.

The TypeScript witness implementation lives with the DApp, not with the library. The witnesses in the compact-contracts repository are test-only and are not published; consumers author their own wit_NonFungibleTokenSK that returns the caller's 32-byte secret key.

The secretKey must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, and key exposure allows impersonation. Use crypto.getRandomValues() or equivalent to generate keys.

Usage

Import the NonFungibleToken module into the implementing contract. It's recommended to prefix the module with NonFungibleToken_ to avoid circuit signature clashes.

pragma language_version >= 0.26.0;

import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"
  prefix NonFungibleToken_;

constructor(name: Opaque<"string">, symbol: Opaque<"string">) {
  NonFungibleToken_initialize(name, symbol);
}

Next, expose the circuits that users may call in the contract.

This library enables extensibility by following the rules of the Module/Contract Pattern. Note that circuits with a preceding underscore (_likeThis) are meant to be building blocks for implementing contracts. Exposing _mint without some sort of access control, for example, would allow ANYONE to mint tokens.

export circuit name(): Opaque<"string"> {
  return NonFungibleToken_name();
}

export circuit symbol(): Opaque<"string"> {
  return NonFungibleToken_symbol();
}

(...)

The following example is a simple non-fungible token contract that mints an NFT to the passed recipient upon construction.

// SimpleNonFungibleToken.compact

pragma language_version >= 0.26.0;

import CompactStandardLibrary;
import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"
  prefix NonFungibleToken_;

constructor(
  name: Opaque<"string">,
  symbol: Opaque<"string">,
  recipient: Either<Bytes<32>, ContractAddress>,
  tokenURI: Opaque<"string">,
) {
  const tokenId = 1 as Uint<128>;
  NonFungibleToken_initialize(name, symbol);
  NonFungibleToken__mint(recipient, tokenId);
  NonFungibleToken__setTokenURI(tokenId, tokenURI);
}

export circuit balanceOf(owner: Either<Bytes<32>, ContractAddress>): Uint<128> {
  return NonFungibleToken_balanceOf(owner);
}

export circuit ownerOf(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
  return NonFungibleToken_ownerOf(tokenId);
}

export circuit name(): Opaque<"string"> {
  return NonFungibleToken_name();
}

export circuit symbol(): Opaque<"string"> {
  return NonFungibleToken_symbol();
}

export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> {
  return NonFungibleToken_tokenURI(tokenId);
}

export circuit approve(to: Either<Bytes<32>, ContractAddress>, tokenId: Uint<128>): [] {
  NonFungibleToken_approve(to, tokenId);
}

export circuit getApproved(tokenId: Uint<128>): Either<Bytes<32>, ContractAddress> {
  return NonFungibleToken_getApproved(tokenId);
}

export circuit setApprovalForAll(
  operator: Either<Bytes<32>, ContractAddress>,
  approved: Boolean
): [] {
  NonFungibleToken_setApprovalForAll(operator, approved);
}

export circuit isApprovedForAll(
  owner: Either<Bytes<32>, ContractAddress>,
  operator: Either<Bytes<32>, ContractAddress>
): Boolean {
  return NonFungibleToken_isApprovedForAll(owner, operator);
}

export circuit transferFrom(
  fromAddress: Either<Bytes<32>, ContractAddress>,
  to: Either<Bytes<32>, ContractAddress>,
  tokenId: Uint<128>
): [] {
  NonFungibleToken_transferFrom(fromAddress, to, tokenId);
}