Skip to main content

The Contract

pragma language_version >= 0.17.0;

import CompactStandardLibrary;

// Initialization state
export ledger _isInitialized: Boolean;

// Token metadata
export sealed ledger _name: Opaque<"string">;
export sealed ledger _symbol: Opaque<"string">;

// NFT ownership and approvals
export ledger _owners: Map<Uint<128>, Either<ZswapCoinPublicKey, ContractAddress>>;
export ledger _balances: Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>;
export ledger _tokenApprovals: Map<Uint<128>, Either<ZswapCoinPublicKey, ContractAddress>>;
export ledger _operatorApprovals: Map<Either<ZswapCoinPublicKey, ContractAddress>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>>;

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

// Initialize the NFT collection (can only be called once)
export circuit initialize(name_: Opaque<"string">, symbol_: Opaque<"string">): [] {
    assert(!_isInitialized, "Already initialized");
    _isInitialized = disclose(true);

    _name = disclose(name_);
    _symbol = disclose(symbol_);
}

// Get collection name
export circuit name(): Opaque<"string"> {
    assert(_isInitialized, "Not initialized");
    return _name;
}

// Get collection symbol
export circuit symbol(): Opaque<"string"> {
    assert(_isInitialized, "Not initialized");
    return _symbol;
}

// Get token owner
export circuit ownerOf(tokenId: Uint<128>): Either<ZswapCoinPublicKey, ContractAddress> {
    assert(_isInitialized, "Not initialized");
    return _requireOwned(tokenId);
}

// Get owner's token count
export circuit balanceOf(owner: Either<ZswapCoinPublicKey, ContractAddress>): Uint<128> {
    assert(_isInitialized, "Not initialized");
    if (!_balances.member(disclose(owner))) {
        return 0;
    }
    return _balances.lookup(disclose(owner));
}

// Get token metadata URI
export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> {
    assert(_isInitialized, "Not initialized");
    _requireOwned(tokenId);

    if (!_tokenURIs.member(disclose(tokenId))) {
        return ""; // Empty string
    }
    return _tokenURIs.lookup(disclose(tokenId));
}

// Approve address to transfer token
export circuit approve(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    const auth = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _approve(to, tokenId, auth);
}

// Get approved address for token
export circuit getApproved(tokenId: Uint<128>): Either<ZswapCoinPublicKey, ContractAddress> {
    assert(_isInitialized, "Not initialized");
    _requireOwned(tokenId);
    return _getApproved(tokenId);
}

// Set operator approval for all tokens
export circuit setApprovalForAll(
    operator: Either<ZswapCoinPublicKey, ContractAddress>,
    approved: Boolean
): [] {
    assert(_isInitialized, "Not initialized");
    const owner = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    _setApprovalForAll(owner, operator, approved);
}

// Check if operator is approved
export circuit isApprovedForAll(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    operator: Either<ZswapCoinPublicKey, ContractAddress>
): Boolean {
    assert(_isInitialized, "Not initialized");
    if (_operatorApprovals.member(disclose(owner)) &&
        _operatorApprovals.lookup(owner).member(disclose(operator))) {
        return _operatorApprovals.lookup(owner).lookup(disclose(operator));
    }
    return false;
}

// Transfer token
export circuit transferFrom(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    assert(!isKeyOrAddressZero(to), "Invalid receiver");

    const caller = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    const previousOwner = _update(to, tokenId, caller);

    assert(previousOwner == from, "Incorrect owner");
}

// Mint new NFT
export circuit _mint(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    assert(!isKeyOrAddressZero(to), "Invalid receiver");

    const previousOwner = _update(to, tokenId, burnAddress());
    assert(isKeyOrAddressZero(previousOwner), "Token already minted");
}

// Burn NFT
export circuit _burn(tokenId: Uint<128>): [] {
    assert(_isInitialized, "Not initialized");
    const previousOwner = _update(burnAddress(), tokenId, burnAddress());
    assert(!isKeyOrAddressZero(previousOwner), "Token does not exist");
}

// Set token URI
export circuit _setTokenURI(tokenId: Uint<128>, uri: Opaque<"string">): [] {
    assert(_isInitialized, "Not initialized");
    _requireOwned(tokenId);
    _tokenURIs.insert(disclose(tokenId), disclose(uri));
}

// Internal functions

circuit _requireOwned(tokenId: Uint<128>): Either<ZswapCoinPublicKey, ContractAddress> {
    const owner = _ownerOf(tokenId);
    assert(!isKeyOrAddressZero(owner), "Token does not exist");
    return owner;
}

circuit _ownerOf(tokenId: Uint<128>): Either<ZswapCoinPublicKey, ContractAddress> {
    if (!_owners.member(disclose(tokenId))) {
        return burnAddress();
    }
    return _owners.lookup(disclose(tokenId));
}

circuit _getApproved(tokenId: Uint<128>): Either<ZswapCoinPublicKey, ContractAddress> {
    if (!_tokenApprovals.member(disclose(tokenId))) {
        return burnAddress();
    }
    return _tokenApprovals.lookup(disclose(tokenId));
}

circuit _approve(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>,
    auth: Either<ZswapCoinPublicKey, ContractAddress>
): [] {
    if (!isKeyOrAddressZero(disclose(auth))) {
        const owner = _requireOwned(tokenId);
        assert((owner == disclose(auth) || isApprovedForAll(owner, auth)), "Not authorized");
    }

    _tokenApprovals.insert(disclose(tokenId), disclose(to));
}

circuit _setApprovalForAll(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    operator: Either<ZswapCoinPublicKey, ContractAddress>,
    approved: Boolean
): [] {
    assert(!isKeyOrAddressZero(operator), "Invalid operator");

    if (!_operatorApprovals.member(disclose(owner))) {
        _operatorApprovals.insert(disclose(owner), default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>>);
    }

    _operatorApprovals.lookup(owner).insert(disclose(operator), disclose(approved));
}

circuit _update(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>,
    auth: Either<ZswapCoinPublicKey, ContractAddress>
): Either<ZswapCoinPublicKey, ContractAddress> {
    const from = _ownerOf(tokenId);

    // Check authorization
    if (!isKeyOrAddressZero(disclose(auth))) {
        _checkAuthorized(from, auth, tokenId);
    }

    // Update balances and ownership
    if (!isKeyOrAddressZero(disclose(from))) {
        _approve(burnAddress(), tokenId, burnAddress());
        const newBalance = _balances.lookup(disclose(from)) - 1 as Uint<128>;
        _balances.insert(disclose(from), disclose(newBalance));
    }

    if (!isKeyOrAddressZero(disclose(to))) {
        if (!_balances.member(disclose(to))) {
            _balances.insert(disclose(to), 0);
        }
        const newBalance = _balances.lookup(disclose(to)) + 1 as Uint<128>;
        _balances.insert(disclose(to), disclose(newBalance));
    }

    _owners.insert(disclose(tokenId), disclose(to));

    return from;
}

circuit _checkAuthorized(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>
): [] {
    if (!_isAuthorized(owner, spender, tokenId)) {
        assert(!isKeyOrAddressZero(owner), "Token does not exist");
        assert(false, "Not authorized");
    }
}

circuit _isAuthorized(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    spender: Either<ZswapCoinPublicKey, ContractAddress>,
    tokenId: Uint<128>
): Boolean {
    return (
        !isKeyOrAddressZero(disclose(spender)) &&
        (disclose(owner) == disclose(spender) ||
         isApprovedForAll(owner, spender) ||
         _getApproved(tokenId) == disclose(spender))
    );
}

// Helper Functions
// These would typically be imported from OpenZeppelin's Utils module
circuit isKeyOrAddressZero(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return isContractAddress(keyOrAddress)
        ? default<ContractAddress> == keyOrAddress.right
        : default<ZswapCoinPublicKey> == keyOrAddress.left;
}

circuit isContractAddress(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return !keyOrAddress.is_left;
}

circuit burnAddress(): Either<ZswapCoinPublicKey, ContractAddress> {
    return left<ZswapCoinPublicKey, ContractAddress>(default<ZswapCoinPublicKey>);
}

How It Works

This ERC721 NFT implementation provides standard non-fungible token functionality. For detailed explanations of core concepts, see the linked basics tutorials.

Initialization Protection

Uses an initialization guard (same pattern as ERC20) to ensure initialize() can only be called once with the collection name and symbol.

Ownership Structure

Dual Map System:
  • _owners: Maps tokenId → owner address (who owns which specific NFT)
  • _balances: Maps owner → count (how many NFTs they own total)
Each token has a unique ID (Uint<128>). Unlike fungible tokens where all units are identical, each NFT is distinct. See Mappings tutorial for Map storage basics.

Token Metadata

Each NFT has a URI pointing to off-chain metadata (name, description, image, attributes). Typically stored on IPFS/Arweave:
{
  "name": "Cool Cat #42",
  "image": "ipfs://QmX.../cat42.png",
  "attributes": [{ "trait_type": "Color", "value": "Blue" }]
}

ERC721-Specific Features

Dual Approval System:
  • approve(address, tokenId) - Approve transfer of one specific token
  • setApprovalForAll(operator, bool) - Approve operator for ALL tokens
Transfer Mechanics:
  • Authorization check (owner/approved/operator)
  • Clear single-token approval (operator approvals persist)
  • Update ownership maps (decrease sender balance, increase receiver balance)
  • Atomic updates (all-or-nothing)
Minting/Burning:
  • Each tokenId can only be minted once (no duplicates)
  • Minting “from” burn address, burning “to” burn address
  • Uses internal _update() function for all ownership changes
For transfer authorization patterns, see the Transfer tutorial

Helper Functions

The contract uses utility functions for address validation and operations:
circuit isKeyOrAddressZero(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return isContractAddress(keyOrAddress)
        ? default<ContractAddress> == keyOrAddress.right
        : default<ZswapCoinPublicKey> == keyOrAddress.left;
}

circuit isContractAddress(keyOrAddress: Either<ZswapCoinPublicKey, ContractAddress>): Boolean {
    return !keyOrAddress.is_left;
}

circuit burnAddress(): Either<ZswapCoinPublicKey, ContractAddress> {
    return left<ZswapCoinPublicKey, ContractAddress>(default<ZswapCoinPublicKey>);
}
  • isKeyOrAddressZero: Validates addresses by checking if they match the zero/default value. Used to prevent minting/transferring to invalid addresses.
  • isContractAddress: Checks if an address is a ContractAddress type (not a ZswapCoinPublicKey). Used to prevent unsafe transfers to contracts until contract-to-contract calls are supported.
  • burnAddress: Returns the burn address for minting (from burn address) and burning (to burn address) operations.
In production OpenZeppelin contracts, these utilities are imported from the Utils module. The implementations shown here match the OpenZeppelin Compact Contracts v0.0.1-alpha.0 specification.

Try It Yourself

1. Create project structure:
mkdir my-nft-collection && cd my-nft-collection
mkdir -p contracts
2. Save the contract:Create contracts/my-nft.compact with the code above.3. Initialize your collection:In your contract constructor:
circuit constructor(): [] {
    initialize("My Cool NFTs", "COOL");
}
4. Mint your first NFT:
export circuit mintNFT(to: Either<ZswapCoinPublicKey, ContractAddress>, tokenId: Uint<128>, uri: Opaque<"string">): [] {
    _mint(to, tokenId);
    _setTokenURI(tokenId, uri);
}
5. Compile:
compact compile contracts/my-nft.compact contracts/managed/my-nft

What’s Next