midnight-ledger 8.0.2

Provides the transaction format and semantics for Midnight.
Documentation
// Token Vault Contract
// This file is part of midnight-ledger.
// Copyright (C) 2025 Midnight Foundation
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Token Vault Contract
//!
//! **REFERENCE IMPLEMENTATION ONLY**
//! This code is provided for educational and testing purposes to demonstrate
//! Midnight ledger features. DO NOT use this code as-is in production without
//! proper security review, auditing, and hardening.
//!
//! A contract that demonstrates both shielded and unshielded token operations.
//! - Shielded tokens: Privacy-preserving using ZK proofs
//! - Unshielded tokens: Public UTXO-based transfers

export {
    depositShielded,
    withdrawShielded,
    depositUnshielded,
    withdrawUnshielded,
    getShieldedBalance,
    getUnshieldedBalance
}

import CompactStandardLibrary;

// ============================================================================
// Ledger State
// ============================================================================

// Shielded token storage (private tokens)
ledger shieldedVault: QualifiedShieldedCoinInfo;
ledger hasShieldedTokens: Boolean;

// Access control
ledger owner: Bytes<32>;
ledger authorized: Set<Bytes<32>>;

// Statistics
ledger totalShieldedDeposits: Counter;
ledger totalShieldedWithdrawals: Counter;
ledger totalUnshieldedDeposits: Counter;
ledger totalUnshieldedWithdrawals: Counter;

// ============================================================================
// Witness Functions
// ============================================================================

witness localSecretKey(): Bytes<32>;

// ============================================================================
// Helper Circuits
// ============================================================================

circuit publicKey(sk: Bytes<32>): Bytes<32> {
    return persistentCommit<Bytes<14>>("token:vault:pk", disclose(sk));
}

circuit isOwner(): Boolean {
    const sk = localSecretKey();
    const pk = publicKey(sk);
    return pk == owner;
}

circuit isAuthorized(): Boolean {
    const sk = localSecretKey();
    const pk = publicKey(sk);
    return authorized.member(pk) || pk == owner;
}

// ============================================================================
// Shielded Token Operations (Private Tokens)
// ============================================================================

/// Deposit shielded tokens into the vault
circuit depositShielded(coin: ShieldedCoinInfo): [] {
    assert(coin.value > 0, "Deposit amount must be positive");
    
    receiveShielded(disclose(coin));
    
    if (!hasShieldedTokens) {
        shieldedVault.writeCoin(
            disclose(coin),
            right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
        );
        hasShieldedTokens = true;
    } else {
        shieldedVault.writeCoin(
            mergeCoinImmediate(shieldedVault, disclose(coin)),
            right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
        );
    }
    
    totalShieldedDeposits.increment(1);
}

/// Withdraw shielded tokens from the vault
circuit withdrawShielded(amount: Uint<128>): ShieldedCoinInfo {
    assert(isAuthorized(), "Not authorized to withdraw");
    assert(hasShieldedTokens, "No shielded tokens in vault");
    assert(shieldedVault.value >= amount, "Insufficient shielded balance");
    
    const recipient = ownPublicKey();
    
    const result = sendShielded(
        shieldedVault,
        left<ZswapCoinPublicKey, ContractAddress>(recipient),
        disclose(amount)
    );
    
    if (result.change.is_some) {
        shieldedVault.writeCoin(
            result.change.value,
            right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
        );
    } else {
        hasShieldedTokens = false;
        shieldedVault = QualifiedShieldedCoinInfo {
            nonce: pad(32, ""),
            color: pad(32, ""),
            value: 0,
            mt_index: 0
        };
    }
    
    totalShieldedWithdrawals.increment(1);
    
    return result.sent;
}

circuit getShieldedBalance(): Uint<128> {
    if (hasShieldedTokens) {
        return shieldedVault.value;
    }
    return 0;
}

// ============================================================================
// Unshielded Token Operations (Public Tokens)
// ============================================================================

/// Deposit unshielded tokens into the vault
/// The caller must include the unshielded tokens as inputs to the transaction
/// This circuit declares the contract will receive the tokens
circuit depositUnshielded(color: Bytes<32>, amount: Uint<128>): [] {
    assert(amount > 0, "Deposit amount must be positive");
    
    // Declare that this contract is receiving unshielded tokens
    // The actual tokens come from the transaction inputs
    receiveUnshielded(disclose(color), disclose(amount));
    
    totalUnshieldedDeposits.increment(1);
}

/// Withdraw unshielded tokens from the vault
circuit withdrawUnshielded(color: Bytes<32>, amount: Uint<128>, recipient: Either<ContractAddress, UserAddress>): [] {
    assert(isAuthorized(), "Not authorized to withdraw");
    assert(unshieldedBalanceGte(disclose(color), disclose(amount)), "Insufficient unshielded balance");
    
    sendUnshielded(disclose(color), disclose(amount), disclose(recipient));
    
    totalUnshieldedWithdrawals.increment(1);
}

/// Get unshielded balance for a token type
/// Note: This returns the balance at the START of execution,
/// not reflecting any sends/receives in the current transaction
circuit getUnshieldedBalance(color: Bytes<32>): Uint<128> {
    return unshieldedBalance(disclose(color));
}

/// Check if balance is greater than a threshold
circuit hasUnshieldedBalanceAbove(color: Bytes<32>, threshold: Uint<128>): Boolean {
    return unshieldedBalanceGt(disclose(color), disclose(threshold));
}