> ## 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.

# Multi-Token Standard (ERC1155)

> ERC1155-style multi-token contract in Compact: per-id balances with nested maps, operator approvals, and admin-gated minting on Midnight.

<Warning>
  **All balances and transfers are transparent.** Each `_balances[id][owner]`
  entry and every transfer write reveals (token id, sender, recipient,
  amount). Anyone reading the chain can reconstruct holdings of every token
  type. Use this for game items and other transparent use cases — not for
  private finance.
</Warning>

<Warning>
  **Authentication uses witness-derived keypairs.** See
  [Access Control](/basics/access-control) for why `ownPublicKey()` cannot
  be used here.
</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;

  export ledger _admin: AdminPublicKey;

  // Balances: tokenId → (owner → amount)
  export ledger _balances: Map<Uint<128>, Map<UserPublicKey, Uint<128>>>;

  // Operator approvals (all-or-nothing across every token type)
  export ledger _operatorApprovals: Map<UserPublicKey, Map<UserPublicKey, Boolean>>;

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

  constructor(initialAdmin: AdminPublicKey) {
      _admin = disclose(initialAdmin);
  }

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

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

  // Balance of `account` for token id `id`
  export circuit balanceOf(account: UserPublicKey, id: Uint<128>): Uint<128> {
      if (!_balances.member(disclose(id))) { return 0; }
      if (!_balances.lookup(disclose(id)).member(disclose(account))) { return 0; }
      return _balances.lookup(disclose(id)).lookup(disclose(account));
  }

  // Operator approval — caller authenticates as `owner`
  export circuit setApprovalForAll(operator: UserPublicKey, approved: Boolean): [] {
      const owner = disclose(deriveUserPublicKey(getUserSecret()));
      _setApprovalForAll(owner, operator, approved);
  }

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

  // Transfer — caller must be `fromKey` itself, or an approved operator
  export circuit transfer(
      fromKey: UserPublicKey,
      to: UserPublicKey,
      id: Uint<128>,
      value: Uint<128>
  ): [] {
      // Disclose both sides of the comparison up front. `fromKey` will be
      // disclosed anyway by `_debit` below, so this changes no privacy
      // properties — but it lets the compiler see that the boolean branch
      // is on public values.
      const fromD = disclose(fromKey);
      const callerD = disclose(deriveUserPublicKey(getUserSecret()));
      assert(fromD == callerD || isApprovedForAll(fromKey, callerD), "Not authorized");
      _update(fromKey, to, id, value);
  }

  // Read token URI
  export circuit uri(tokenId: Uint<128>): Opaque<"string"> {
      if (!_uris.member(disclose(tokenId))) { return default<Opaque<"string">>; }
      return _uris.lookup(disclose(tokenId));
  }

  // Admin: set the URI for a token id
  export circuit setURI(id: Uint<128>, newuri: Opaque<"string">): [] {
      assertAdmin();
      _uris.insert(disclose(id), disclose(newuri));
  }

  // Admin: mint
  export circuit mint(to: UserPublicKey, id: Uint<128>, value: Uint<128>): [] {
      assertAdmin();
      _credit(to, id, value);
  }

  // Admin: burn (forceful — admin can burn any holder's tokens)
  export circuit adminBurn(fromKey: UserPublicKey, id: Uint<128>, value: Uint<128>): [] {
      assertAdmin();
      _debit(fromKey, id, value);
  }

  // Self-burn — caller authenticates as the holder
  export circuit burn(id: Uint<128>, value: Uint<128>): [] {
      const account = disclose(deriveUserPublicKey(getUserSecret()));
      _debit(account, id, value);
  }

  // Internals

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

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

  circuit _update(
      fromKey: UserPublicKey,
      to: UserPublicKey,
      id: Uint<128>,
      value: Uint<128>
  ): [] {
      _debit(fromKey, id, value);
      _credit(to, id, value);
  }

  circuit _credit(account: UserPublicKey, id: Uint<128>, value: Uint<128>): [] {
      const MAX_UINT128: Uint<128> = 340282366920938463463374607431768211455 as Uint<128>;
      const current = balanceOf(account, id);
      assert(current <= MAX_UINT128 - value, "Balance overflow");

      if (!_balances.member(disclose(id))) {
          _balances.insert(disclose(id), default<Map<UserPublicKey, Uint<128>>>);
      }
      _balances.lookup(disclose(id)).insert(
          disclose(account),
          disclose((current + value) as Uint<128>)
      );
  }

  circuit _debit(account: UserPublicKey, id: Uint<128>, value: Uint<128>): [] {
      const current = balanceOf(account, id);
      assert(current >= value, "Insufficient balance");
      _balances.lookup(disclose(id)).insert(disclose(account), disclose(current - value));
  }
  ```
</Accordion>

## How It Works

ERC1155 supports many token types in one contract. Each token id has its own
balance Map.

### Storage layout

```compact theme={null}
_balances: Map<Uint<128>, Map<UserPublicKey, Uint<128>>>
```

* Outer key: token id (e.g. `1` = Gold, `100` = Legendary Sword).
* Inner map: per-holder balance for that token id.

This makes lookups `O(1)` and lets balances of different token types be
managed independently.

### Mixed token types

* **Fungible**: `mint(alice, 1, 1000)` — 1000 copies of token id 1.
* **Non-Fungible**: `mint(bob, 100, 1)` — single edition of token id 100.
* **Semi-Fungible**: `mint(carol, 50, 10)` — 10 of token id 50.

A common convention is to reserve id ranges (e.g. `1..999` for currency,
`1000..9999` for items, `10000+` for legendaries).

### Authorization

`transfer(fromKey, to, id, value)`:

* Either the caller authenticates as `fromKey` directly, or
* The caller authenticates as an operator that `fromKey` has approved via
  `setApprovalForAll`.

Note `from` is a Compact reserved keyword, so the parameter is named
`fromKey`.

### Admin mint / burn / URI

`mint`, `adminBurn`, and `setURI` are admin-only. Wrap them in your own
business logic for richer policies (per-id supply caps, mint windows,
delegated minter roles).

### Batch operations

Compact does not yet support dynamic arrays, so ERC1155 batch operations
(`balanceOfBatch`, `safeBatchTransferFrom`) are not implementable verbatim.
For now, callers must invoke single-id operations sequentially.

### Self-approval guard

```compact theme={null}
assert(owner != operator, "Cannot approve self");
```

Self-approval is rejected — it's a no-op (the owner can already move their
own tokens) and usually indicates a UI bug.

## Try It Yourself

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

  ```bash theme={null}
  mkdir my-game-items && cd my-game-items
  mkdir -p contracts
  ```

  **2. Save the contract** at `contracts/game-items.compact`.

  **3. Define your token types and mint policy** in a wrapper contract or your
  DApp. The admin pubkey is the constructor argument.

  **4. Compile:**

  ```bash theme={null}
  compact compile contracts/game-items.compact contracts/managed/game-items
  ```
</Accordion>

## What's Next

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

  <Card title="ERC721 NFT" icon="image" href="/applications/erc721-nft">
    Build non-fungible tokens
  </Card>
</CardGroup>
