> ## Documentation Index
> Fetch the complete documentation index at: https://compact-by-example.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Non-Fungible Token (ERC721)

> ERC721-style NFT contract in Compact: ownership, approvals, operator approval, token URIs, and admin mint/burn with witness-derived authentication.

<Warning>
  **This is a transparent NFT contract.** Every mint, transfer, and approval
  publishes the involved keys, token IDs, and (where applicable) URIs to the
  public ledger. The full ownership graph and provenance history is
  observable.

  For NFTs where ownership must stay private, use Midnight's shielded
  primitives — they're built on UTXOs and ZK commitments and don't leak the
  ownership graph.
</Warning>

<Warning>
  **Authentication uses witness-derived keypairs**, not `ownPublicKey()`. See
  [Access Control](/basics/access-control) for the rationale.
</Warning>

## The Contract

<Accordion title="View Contract Code">
  ```compact theme={null}
  pragma language_version >= 0.23;

  import CompactStandardLibrary;

  // User keypair (witness-derived)
  struct UserSecretKey { bytes: Bytes<32>; }
  struct UserPublicKey { bytes: Bytes<32>; }
  witness getUserSecret(): UserSecretKey;

  // Admin keypair (witness-derived) — gates _mint and _burn
  struct AdminSecretKey { bytes: Bytes<32>; }
  struct AdminPublicKey { bytes: Bytes<32>; }
  witness getAdminSecret(): AdminSecretKey;

  // Metadata (sealed: settable only in the constructor)
  export sealed ledger _name: Opaque<"string">;
  export sealed ledger _symbol: Opaque<"string">;

  // Admin
  export ledger _admin: AdminPublicKey;

  // Ownership maps
  //   _owners[tokenId] = owner pubkey
  //   _balances[owner] = how many NFTs that owner holds
  export ledger _owners: Map<Uint<128>, UserPublicKey>;
  export ledger _balances: Map<UserPublicKey, Uint<128>>;

  // Approvals
  //   _tokenApprovals[tokenId] = pubkey approved for that one token
  //   _operatorApprovals[owner][operator] = operator can manage all of owner's NFTs
  export ledger _tokenApprovals: Map<Uint<128>, UserPublicKey>;
  export ledger _operatorApprovals: Map<UserPublicKey, Map<UserPublicKey, Boolean>>;

  // Per-token metadata URIs
  export ledger _tokenURIs: Map<Uint<128>, Opaque<"string">>;

  constructor(
      initialAdmin: AdminPublicKey,
      name_: Opaque<"string">,
      symbol_: Opaque<"string">
  ) {
      _admin = disclose(initialAdmin);
      _name = disclose(name_);
      _symbol = disclose(symbol_);
  }

  // Key derivation
  export circuit deriveUserPublicKey(sk: UserSecretKey): UserPublicKey {
      return UserPublicKey {
          bytes: persistentHash<Vector<2, Bytes<32>>>([
              pad(32, "erc721:user:v1"),
              sk.bytes
          ])
      };
  }

  export circuit deriveAdminPublicKey(sk: AdminSecretKey): AdminPublicKey {
      return AdminPublicKey {
          bytes: persistentHash<Vector<2, Bytes<32>>>([
              pad(32, "erc721:admin:v1"),
              sk.bytes
          ])
      };
  }

  // Metadata
  export circuit name(): Opaque<"string"> { return _name; }
  export circuit symbol(): Opaque<"string"> { return _symbol; }

  export circuit ownerOf(tokenId: Uint<128>): UserPublicKey {
      return _requireOwned(tokenId);
  }

  export circuit balanceOf(owner: UserPublicKey): Uint<128> {
      if (!_balances.member(disclose(owner))) { return 0; }
      return _balances.lookup(disclose(owner));
  }

  export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> {
      _requireOwned(tokenId);
      if (!_tokenURIs.member(disclose(tokenId))) { return default<Opaque<"string">>; }
      return _tokenURIs.lookup(disclose(tokenId));
  }

  // Approve a single token for transfer by `to`
  export circuit approve(to: UserPublicKey, tokenId: Uint<128>): [] {
      const auth = disclose(deriveUserPublicKey(getUserSecret()));
      _approve(to, tokenId, auth);
  }

  export circuit getApproved(tokenId: Uint<128>): UserPublicKey {
      _requireOwned(tokenId);
      return _getApproved(tokenId);
  }

  // Approve operator for all of caller's NFTs
  export circuit setApprovalForAll(operator: UserPublicKey, approved: Boolean): [] {
      const owner = disclose(deriveUserPublicKey(getUserSecret()));
      _setApprovalForAll(owner, operator, approved);
  }

  export circuit isApprovedForAll(owner: UserPublicKey, operator: UserPublicKey): Boolean {
      if (_operatorApprovals.member(disclose(owner)) &&
          _operatorApprovals.lookup(disclose(owner)).member(disclose(operator))) {
          return _operatorApprovals.lookup(disclose(owner)).lookup(disclose(operator));
      }
      return false;
  }

  // Transfer — caller must be owner, approved-for-token, or approved-for-all
  export circuit transferFrom(
      fromKey: UserPublicKey,
      to: UserPublicKey,
      tokenId: Uint<128>
  ): [] {
      const caller = disclose(deriveUserPublicKey(getUserSecret()));
      const previousOwner = _update(to, tokenId, caller);
      assert(previousOwner == fromKey, "Incorrect owner");
  }

  // Admin: mint a new NFT
  export circuit mint(to: UserPublicKey, tokenId: Uint<128>): [] {
      assertAdmin();
      assert(!_owners.member(disclose(tokenId)), "Token already minted");
      _balances.insert(disclose(to), disclose((balanceOf(to) + 1) as Uint<128>));
      _owners.insert(disclose(tokenId), disclose(to));
  }

  // Admin: burn an NFT
  export circuit adminBurn(tokenId: Uint<128>): [] {
      assertAdmin();
      const owner = _requireOwned(tokenId);
      // clear single-token approval
      if (_tokenApprovals.member(disclose(tokenId))) {
          _tokenApprovals.remove(disclose(tokenId));
      }
      _balances.insert(disclose(owner), disclose(_balances.lookup(disclose(owner)) - 1));
      _owners.remove(disclose(tokenId));
  }

  // Set the URI of a token you own
  export circuit setTokenURI(tokenId: Uint<128>, uri: Opaque<"string">): [] {
      const caller = disclose(deriveUserPublicKey(getUserSecret()));
      const owner = _requireOwned(tokenId);
      assert(owner == caller, "Only owner may set URI");
      _tokenURIs.insert(disclose(tokenId), disclose(uri));
  }

  // Internals

  circuit assertAdmin(): [] {
      assert(_admin == deriveAdminPublicKey(getAdminSecret()), "Not admin");
  }

  circuit _requireOwned(tokenId: Uint<128>): UserPublicKey {
      assert(_owners.member(disclose(tokenId)), "Token does not exist");
      return _owners.lookup(disclose(tokenId));
  }

  circuit _getApproved(tokenId: Uint<128>): UserPublicKey {
      if (!_tokenApprovals.member(disclose(tokenId))) {
          return UserPublicKey { bytes: pad(32, "") };
      }
      return _tokenApprovals.lookup(disclose(tokenId));
  }

  circuit _approve(to: UserPublicKey, tokenId: Uint<128>, auth: UserPublicKey): [] {
      const owner = _requireOwned(tokenId);
      assert(owner == auth || isApprovedForAll(owner, auth), "Not authorized");
      _tokenApprovals.insert(disclose(tokenId), disclose(to));
  }

  circuit _setApprovalForAll(
      owner: UserPublicKey,
      operator: UserPublicKey,
      approved: Boolean
  ): [] {
      if (!_operatorApprovals.member(disclose(owner))) {
          _operatorApprovals.insert(
              disclose(owner),
              default<Map<UserPublicKey, Boolean>>
          );
      }
      _operatorApprovals.lookup(disclose(owner)).insert(disclose(operator), disclose(approved));
  }

  circuit _update(to: UserPublicKey, tokenId: Uint<128>, auth: UserPublicKey): UserPublicKey {
      const fromKey = _requireOwned(tokenId);
      _checkAuthorized(fromKey, auth, tokenId);
      // clear single-token approval
      if (_tokenApprovals.member(disclose(tokenId))) {
          _tokenApprovals.remove(disclose(tokenId));
      }
      // adjust balances
      _balances.insert(
          disclose(fromKey),
          disclose(_balances.lookup(disclose(fromKey)) - 1)
      );
      if (!_balances.member(disclose(to))) {
          _balances.insert(disclose(to), 0);
      }
      _balances.insert(
          disclose(to),
          disclose((_balances.lookup(disclose(to)) + 1) as Uint<128>)
      );
      _owners.insert(disclose(tokenId), disclose(to));
      return fromKey;
  }

  circuit _checkAuthorized(owner: UserPublicKey, spender: UserPublicKey, tokenId: Uint<128>): [] {
      assert(
          owner == spender
              || isApprovedForAll(owner, spender)
              || _getApproved(tokenId) == spender,
          "Not authorized"
      );
  }
  ```
</Accordion>

## How It Works

This ERC721 implementation provides standard non-fungible token functionality
with witness-derived caller authentication.

### Dual ownership view

* `_owners`: `tokenId → owner` (who owns *this specific* NFT)
* `_balances`: `owner → count` (how many NFTs *that owner* holds)

Both maps are kept in sync inside `_update`, `mint`, and `adminBurn`. Keeping
the count denormalized lets `balanceOf` answer in `O(1)` without iterating
`_owners`.

### Authorization layers

Three ways to be authorized to move a token:

1. You are the owner: `owner == caller`.
2. The owner has approved you for this specific token: `_getApproved(tokenId) == caller`.
3. The owner has approved you as operator for all their NFTs:
   `isApprovedForAll(owner, caller)`.

`_checkAuthorized` evaluates all three; the union is the standard ERC721
authorization rule.

### Admin mint / burn

`mint` and `adminBurn` are admin-only — they wrap `assertAdmin()` around the
underlying state mutation. To delegate mint authority (e.g. to a separate
"minter" role), follow the role-based pattern from
[Access Control](/basics/access-control#role-based-access).

### Token URI

`setTokenURI(tokenId, uri)` is callable by the current owner. The URI is
typically a pointer to off-chain metadata (IPFS, Arweave, HTTPS) and is
stored as `Opaque<"string">` since the contract doesn't need to read its
bytes.

```json theme={null}
{
  "name": "Cool Cat #42",
  "image": "ipfs://QmX.../cat42.png",
  "attributes": [{ "trait_type": "Color", "value": "Blue" }]
}
```

### What's not here

* **`safeTransferFrom`** and **`onERC721Received`** — Compact has no
  contract-to-contract call yet, so the unsafe-vs-safe distinction is moot.
* **ERC721 events** — Compact has no event/log primitive yet (contract log
  events per MIP-0002 are arriving in newer toolchains). For now, off-chain
  indexers must read state directly.
* **Enumerable extension** — Compact has no iteration over Map keys; if you
  need `tokenOfOwnerByIndex`, track an auxiliary list explicitly.

## Try It Yourself

<Accordion title="Build and Deploy">
  **1. Create project structure:**

  ```bash theme={null}
  mkdir my-nft-collection && cd my-nft-collection
  mkdir -p contracts
  ```

  **2. Save the contract** at `contracts/my-nft.compact`.

  **3. Compile:**

  ```bash theme={null}
  compact compile contracts/my-nft.compact contracts/managed/my-nft
  ```

  **4. Deploy** with `(adminPublicKey, name, symbol)`. The DApp supplies
  `getAdminSecret()` and `getUserSecret()` witnesses.
</Accordion>

## What's Next

<CardGroup cols={2}>
  <Card title="ERC20 Token" icon="coins" href="/applications/erc20-token">
    Create fungible tokens
  </Card>

  <Card title="Multi-Token (ERC1155)" icon="layer-group" href="/applications/erc1155-multitoken">
    Manage multiple token types
  </Card>
</CardGroup>
