Skip to main content

The Contract

pragma language_version >= 0.17.0;

import CompactStandardLibrary;

// Initialization state
export ledger _isInitialized: Boolean;

// Balances per token ID per account
export ledger _balances: Map<Uint<128>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>;

// Operator approvals (all-or-nothing)
export ledger _operatorApprovals: Map<Either<ZswapCoinPublicKey, ContractAddress>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Boolean>>;

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

// Initialize the contract
export circuit initialize(baseUri: Opaque<"string">): [] {
    assert(!_isInitialized, "Already initialized");
    _isInitialized = disclose(true);
}

// Get balance of account for token ID
export circuit balanceOf(
    account: Either<ZswapCoinPublicKey, ContractAddress>,
    id: Uint<128>
): Uint<128> {
    assert(_isInitialized, "Not initialized");
    if (!_balances.member(disclose(id))) {
        return 0;
    }

    const accountBalances = _balances.lookup(disclose(id));
    if (!accountBalances.member(disclose(account))) {
        return 0;
    }

    return accountBalances.lookup(disclose(account));
}

// Set operator approval for all token types
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(
    account: Either<ZswapCoinPublicKey, ContractAddress>,
    operator: Either<ZswapCoinPublicKey, ContractAddress>
): Boolean {
    assert(_isInitialized, "Not initialized");
    if (_operatorApprovals.member(disclose(account)) &&
        _operatorApprovals.lookup(account).member(disclose(operator))) {
        return _operatorApprovals.lookup(account).lookup(disclose(operator));
    }
    return false;
}

// Transfer single token type
export circuit safeTransferFrom(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    id: Uint<128>,
    value: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    const caller = left<ZswapCoinPublicKey, ContractAddress>(ownPublicKey());
    assert(from == caller || isApprovedForAll(from, caller), "Not authorized");
    assert(!isKeyOrAddressZero(to), "Invalid receiver");

    _update(from, to, id, value);
}

// Get token URI
export circuit uri(tokenId: Uint<128>): Opaque<"string"> {
    assert(_isInitialized, "Not initialized");
    if (!_uris.member(disclose(tokenId))) {
        return ""; // Empty string
    }
    return _uris.lookup(disclose(tokenId));
}

// Mint tokens
export circuit _mint(
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    id: Uint<128>,
    value: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    assert(!isKeyOrAddressZero(to), "Invalid receiver");
    _update(burnAddress(), to, id, value);
}

// Burn tokens
export circuit _burn(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    id: Uint<128>,
    value: Uint<128>
): [] {
    assert(_isInitialized, "Not initialized");
    assert(!isKeyOrAddressZero(from), "Invalid sender");
    _update(from, burnAddress(), id, value);
}

// Set token URI
export circuit _setURI(id: Uint<128>, newuri: Opaque<"string">): [] {
    assert(_isInitialized, "Not initialized");
    _uris.insert(disclose(id), disclose(newuri));
}

// Internal functions

circuit _update(
    from: Either<ZswapCoinPublicKey, ContractAddress>,
    to: Either<ZswapCoinPublicKey, ContractAddress>,
    id: Uint<128>,
    value: Uint<128>
): [] {
    if (!isKeyOrAddressZero(disclose(from))) {
        const fromBalance = balanceOf(from, id);
        assert(fromBalance >= value, "Insufficient balance");

        const newBalance = fromBalance - value;
        if (!_balances.member(disclose(id))) {
            _balances.insert(disclose(id), default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>);
        }
        _balances.lookup(id).insert(disclose(from), disclose(newBalance));
    }

    if (!isKeyOrAddressZero(disclose(to))) {
        const toBalance = balanceOf(to, id);

        // Check overflow
        const MAX_UINT128 = 340282366920938463463374607431768211455 as Uint<128>;
        assert(toBalance <= MAX_UINT128 - value, "Balance overflow");

        const newBalance = toBalance + value;
        if (!_balances.member(disclose(id))) {
            _balances.insert(disclose(id), default<Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>);
        }
        _balances.lookup(id).insert(disclose(to), disclose(newBalance));
    }
}

circuit _setApprovalForAll(
    owner: Either<ZswapCoinPublicKey, ContractAddress>,
    operator: Either<ZswapCoinPublicKey, ContractAddress>,
    approved: Boolean
): [] {
    assert(owner != operator, "Cannot approve self");

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

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

// 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 ERC1155 implementation enables multiple token types in a single contract. For detailed explanations of core concepts, see the linked basics tutorials.

Initialization Protection

Uses the same initialization guard pattern as ERC20/ERC721 to ensure initialize() is called once to set the base URI.

Nested Map Balance Structure

Key Data Structure:
_balances: Map<Uint<128>, Map<Either<ZswapCoinPublicKey, ContractAddress>, Uint<128>>>
  • Outer Map: Token ID → Inner Map
  • Inner Map: Account → Balance
This allows one contract to manage multiple independent token types (ID 1: Gold, ID 2: Silver, ID 100: Legendary Sword). See Mappings tutorial for nested Map patterns.

Mixed Token Types

Fungible: _mint(alice, 1, 1000) - 1000 copies of token ID 1 (Gold Coins)
Non-Fungible: _mint(bob, 100, 1) - 1 unique token ID 100 (Legendary Sword)
Semi-Fungible: _mint(carol, 50, 10) - 10 limited copies of token ID 50 (Shield)
All coexist in the same contract. Organization tip: Use ID ranges (1-999: currency, 1000-9999: items, 10000+: legendaries).

ERC1155-Specific Features

Operator-Only Approval:
  • Only setApprovalForAll() (no single-token approval like ERC721)
  • Operator can transfer ANY token type you own (all-or-nothing)
  • Simpler but less granular than ERC721
Transfer with Value Parameter:
  • Transfer partial amounts: safeTransferFrom(alice, bob, 1, 500) - Transfer 500 of token ID 1
  • Transfer singles: safeTransferFrom(alice, bob, 100, 1) - Transfer unique NFT
  • Authorization: owner or approved operator only
Batch Operations Limitation:
Compact doesn’t support dynamic arrays yet, so standard ERC1155 batch operations (balanceOfBatch, safeBatchTransferFrom) are not possible. Must transfer one token ID at a time.
Metadata URIs:
  • Each token ID has one URI describing that token type (not instance)
  • Example: All “Gold Coin” tokens (ID 1) share the same metadata
  • Unlike ERC721 where each token is unique with its own metadata
For approval mechanics, see Approval tutorial and Allowance tutorial
For minting/burning patterns, see Minting tutorial and Burning 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 against default values. Used to prevent minting/transferring to invalid addresses.
  • isContractAddress: Determines if an address is a ContractAddress type. Used to prevent unsafe transfers to contracts until contract-to-contract calls are supported.
  • burnAddress: Returns the burn address for minting (from burn) and burning (to burn) 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-game-items && cd my-game-items
mkdir -p contracts
2. Save the contract:Create contracts/game-items.compact with the code above.3. Define your token types:
// Token IDs
const GOLD_COINS = 1;
const SILVER_COINS = 2;
const HEALTH_POTION = 10;
const MANA_POTION = 11;
const LEGENDARY_SWORD = 1000;

circuit constructor(): [] {
    // Set up metadata
    _setURI(GOLD_COINS, "ipfs://QmX.../gold.json");
    _setURI(SILVER_COINS, "ipfs://QmX.../silver.json");
    _setURI(HEALTH_POTION, "ipfs://QmX.../health-potion.json");
    _setURI(LEGENDARY_SWORD, "ipfs://QmX.../legendary-sword.json");
}
4. Mint different token types:
export circuit mintCurrency(to: Either<ZswapCoinPublicKey, ContractAddress>, amount: Uint<128>): [] {
    _mint(to, GOLD_COINS, amount);
}

export circuit mintItem(to: Either<ZswapCoinPublicKey, ContractAddress>, itemId: Uint<128>, quantity: Uint<128>): [] {
    _mint(to, itemId, quantity);
}

export circuit mintLegendary(to: Either<ZswapCoinPublicKey, ContractAddress>): [] {
    // Only 1 legendary sword exists
    assert(balanceOf(to, LEGENDARY_SWORD) == 0, "Already owns legendary");
    _mint(to, LEGENDARY_SWORD, 1);
}
5. Compile:
compact compile contracts/game-items.compact contracts/managed/game-items

What’s Next