Skip to main content
This page uses the witness-derived keypair pattern from Access Control. The caller authenticates by proving knowledge of a witness secret whose hash matches the stored UserPublicKey. ownPublicKey() is not used for authentication.

The Contract

pragma language_version >= 0.23;

import CompactStandardLibrary;

struct UserSecretKey { bytes: Bytes<32>; }
struct UserPublicKey { bytes: Bytes<32>; }
witness getUserSecret(): UserSecretKey;

// Balances
export ledger _balances: Map<UserPublicKey, Uint<128>>;

// Allowances: owner → (spender → amount)
export ledger _allowances: Map<UserPublicKey, Map<UserPublicKey, Uint<128>>>;

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

// Approve spender to spend up to `amount` of the caller's tokens
export circuit approve(spender: UserPublicKey, amount: Uint<128>): Boolean {
    const owner = disclose(deriveUserPublicKey(getUserSecret()));
    _approve(owner, spender, amount);
    return true;
}

// Read remaining allowance
export circuit allowance(
    owner: UserPublicKey,
    spender: UserPublicKey
): Uint<128> {
    if (!_allowances.member(disclose(owner))) {
        return 0;
    }
    if (!_allowances.lookup(disclose(owner)).member(disclose(spender))) {
        return 0;
    }
    return _allowances.lookup(disclose(owner)).lookup(disclose(spender));
}

// Internal: write the allowance entry
circuit _approve(
    owner: UserPublicKey,
    spender: UserPublicKey,
    amount: Uint<128>
): [] {
    if (!_allowances.member(disclose(owner))) {
        _allowances.insert(
            disclose(owner),
            default<Map<UserPublicKey, Uint<128>>>
        );
    }
    _allowances.lookup(disclose(owner)).insert(disclose(spender), disclose(amount));
}

How It Works

Authentication

export circuit approve(spender: UserPublicKey, amount: Uint<128>): Boolean {
    const owner = disclose(deriveUserPublicKey(getUserSecret()));
    _approve(owner, spender, amount);
    return true;
}
The caller proves knowledge of a witness secret whose hash equals their UserPublicKey. The contract then writes an allowance entry at _allowances[owner][spender] = amount.

Nested Map structure

export ledger _allowances: Map<UserPublicKey, Map<UserPublicKey, Uint<128>>>;
A two-level map: the outer key is the owner’s authenticated public key; the inner map records how much each spender may spend. Reading the nested map must be done inline — you cannot bind the inner map to a const:
// ✅ inline lookup is allowed
_allowances.lookup(disclose(owner)).lookup(disclose(spender));

// ❌ binding the inner Map to a const fails to compile
// const inner = _allowances.lookup(disclose(owner));  // ADT type error

Initializing the inner Map

if (!_allowances.member(disclose(owner))) {
    _allowances.insert(disclose(owner), default<Map<UserPublicKey, Uint<128>>>);
}
default<Map<K, V>> produces an empty Map, used to initialize the slot for a new owner before the first allowance is recorded.

Privacy Note

Every approve call reveals the owner, the spender, and the amount on the public ledger. This is appropriate for transparent tokens (game items, public registries) but unsuitable for private finance. Midnight’s native shielded primitives provide UTXO-based privacy that hides participants and amounts.

What’s Next

Allowance & Spending

Learn how to spend approved tokens

ERC20 Token

Complete token with approvals