DEV Community

UtkarshVarma
UtkarshVarma

Posted on

Building a Privacy-Preserving Custodian App with Selective Disclosure on Midnight

A comprehensive guide to building custodial asset management with selective disclosure using Midnight Network and zero-knowledge proofs


Table of Contents

  1. Introduction to Selective Disclosure
  2. Why Build Custodian Apps on Midnight?
  3. Understanding Selective Disclosure Architecture
  4. Prerequisites & Setup
  5. Project Structure
  6. Writing the Custodian Contract
  7. Implementing Selective Disclosure
  8. Frontend Integration
  9. Testing Your Contract
  10. Deployment Guide
  11. Advanced Patterns
  12. Compliance & Regulatory Considerations
  13. Next Steps & Resources

Introduction to Selective Disclosure

What is Selective Disclosure?

Selective Disclosure is a privacy-preserving technique that allows users to prove specific claims about their data without revealing the underlying information. Using zero-knowledge proofs, a user can demonstrate:

  • They have sufficient balance without revealing the exact amount
  • They are over 18 without revealing their birth date
  • They are an accredited investor without revealing net worth
  • They own specific assets without revealing their full portfolio
  • They passed KYC without revealing personal documents

The Custodian Model

A custodian is an entity that holds and manages assets on behalf of clients. Traditional custodians include:

  • Banks holding deposits
  • Brokerages holding securities
  • Crypto exchanges holding tokens
  • Asset managers holding investment portfolios

The Privacy Problem

Traditional custodians face a dilemma:

  1. Full Transparency: Reveals sensitive client information
  2. Full Opacity: Provides no verifiability or auditability

Midnight's Solution: Selective disclosure enables custodians to prove claims about client assets while keeping sensitive details private.


Why Build Custodian Apps on Midnight?

Real-World Use Cases

1. Proof of Reserves

Exchanges can prove they hold sufficient assets to cover all customer deposits without revealing individual account balances.

2. Accredited Investor Verification

Investment platforms can verify investor accreditation status without collecting and storing sensitive financial documents.

3. Credit Scoring

Lenders can verify creditworthiness without seeing complete financial history.

4. Regulatory Compliance

Demonstrate compliance with regulations (AML/KYC) while maintaining client privacy.

5. Asset Verification

Prove ownership of specific asset classes without revealing entire portfolio.

Comparison: Traditional vs. Midnight Custodian

Aspect Traditional Midnight
Balance Verification Reveal exact amounts Prove range/threshold
Identity Verification Share documents Prove attributes
Audit Trail Full transaction history Selective proof generation
Regulatory Reports Detailed disclosures Compliant proofs
Client Privacy Minimal Maximum

Why Zero-Knowledge Proofs?

Traditional Approach:
User → "My balance is $50,000" → Verifier sees exact amount

ZK Approach:
User → "I can prove balance > $10,000" → Verifier learns ONLY that balance exceeds threshold
Enter fullscreen mode Exit fullscreen mode

Understanding Selective Disclosure Architecture

System Architecture

+------------------------------------------------------------------+
|                        Client Application                         |
|    +------------------+  +------------------+  +----------------+ |
|    | Wallet Interface |  | Proof Generator  |  | Claim Builder  | |
|    +------------------+  +------------------+  +----------------+ |
+------------------------------------------------------------------+
                               |
                               v
+------------------------------------------------------------------+
|                     Midnight Network                              |
|  +--------------------+  +--------------------+  +--------------+ |
|  | Public Ledger      |  | Private State      |  | ZK Circuits  | |
|  | - Account exists   |  | - Actual balance   |  | - Proofs     | |
|  | - Proof validity   |  | - Personal data    |  | - Verifiers  | |
|  | - Custodian status |  | - Asset details    |  |              | |
|  +--------------------+  +--------------------+  +--------------+ |
+------------------------------------------------------------------+
                               |
                               v
+------------------------------------------------------------------+
|                        Verifier/Auditor                          |
|    Can verify proofs without accessing private data              |
+------------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Data Flow for Selective Disclosure

1. Client deposits assets with custodian
   └─→ Balance stored in private state

2. Client requests disclosure proof
   └─→ "Prove balance > $10,000"

3. ZK circuit generates proof
   └─→ Uses private balance, outputs boolean

4. Verifier checks proof
   └─→ Learns only: balance exceeds threshold

5. Private data never leaves client
   └─→ Zero knowledge transfer
Enter fullscreen mode Exit fullscreen mode

Key Components

1. Custodian Contract

Manages deposits, withdrawals, and account states.

2. Disclosure Circuits

ZK circuits that generate proofs about private data.

3. Verifier Functions

On-chain verification of disclosure proofs.

4. Credential Registry

Tracks verified attributes (KYC status, accreditation, etc.).


Prerequisites & Setup

Required Tools

# Check prerequisites
node -v   # Required: v23+
npm -v    # Required: v11+
docker -v # Required: Latest
Enter fullscreen mode Exit fullscreen mode

Step 1: Install Compact Compiler

# Install Compact tools
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh

# Install compiler version 0.27.0
compact update +0.27.0

# Verify installation
compact check
Enter fullscreen mode Exit fullscreen mode

Step 2: Clone the Starter Template

# Clone repository
git clone https://github.com/MeshJS/midnight-starter-template.git
cd midnight-starter-template

# Install dependencies
npm install
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Custodian Contract Structure

# Create new contract directory
mkdir -p custodian-contract/src
mkdir -p custodian-contract/src/test

# Create package.json for custodian contract
cat > custodian-contract/package.json << 'EOF'
{
  "name": "custodian-contract",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "compactc src/custodian.compact --output src/managed",
    "test": "vitest run"
  },
  "dependencies": {
    "@midnight-ntwrk/compact-runtime": "^0.27.0"
  },
  "devDependencies": {
    "vitest": "^1.0.0"
  }
}
EOF
Enter fullscreen mode Exit fullscreen mode

Project Structure

Directory Layout

midnight-starter-template/
├── custodian-contract/              # Smart contract code
│   ├── src/
│   │   ├── custodian.compact        # Main contract logic
│   │   ├── witnesses.ts             # Private state definitions
│   │   ├── managed/                 # Compiled artifacts
│   │   └── test/
│   │       ├── custodian.test.ts    # Unit tests
│   │       └── simulators/
│   │           └── simulator.ts     # Test simulator
│   └── package.json
│
├── custodian-cli/                   # CLI deployment tools
│   ├── src/
│   │   └── config.ts                # Network configuration
│   └── package.json
│
├── frontend-vite-react/             # React frontend
│   ├── src/
│   │   ├── pages/
│   │   │   └── custodian/           # Custodian UI pages
│   │   └── modules/midnight/
│   │       └── custodian-sdk/       # Contract SDK
│   └── package.json
│
└── package.json                     # Root workspace config
Enter fullscreen mode Exit fullscreen mode

Writing the Custodian Contract

The Complete Custodian Contract

Create custodian-contract/src/custodian.compact:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// ============================================
// CUSTODIAN CONTRACT WITH SELECTIVE DISCLOSURE
// ============================================

// Account status enum (as Uint<8>)
// 0 = Inactive
// 1 = Active
// 2 = Frozen
// 3 = Closed

// KYC Level enum (as Uint<8>)
// 0 = None
// 1 = Basic (email verified)
// 2 = Standard (ID verified)
// 3 = Enhanced (full verification)

// Accreditation Status (as Uint<8>)
// 0 = Not verified
// 1 = Accredited Investor
// 2 = Qualified Purchaser

// ============================================
// PUBLIC LEDGER STATE
// ============================================

// Total accounts registered
export ledger totalAccounts: Counter;

// Total assets under custody (aggregated, privacy-preserving)
export ledger totalAssetsUnderCustody: Counter;

// Account existence mapping (proves account exists without revealing balance)
export ledger accountExists: Map<Bytes<32>, Boolean>;

// Account status mapping
export ledger accountStatus: Map<Bytes<32>, Uint<8>>;

// KYC verification status (public proof that KYC was completed)
export ledger kycVerified: Map<Bytes<32>, Boolean>;

// KYC level (public)
export ledger kycLevel: Map<Bytes<32>, Uint<8>>;

// Accreditation status (public proof)
export ledger accreditationStatus: Map<Bytes<32>, Uint<8>>;

// Proof verification results (for auditors)
export ledger proofVerificationLog: Counter;

// Disclosure request nonces (prevent replay)
export ledger disclosureNonces: Map<Bytes<32>, Counter>;

// Custodian admin address
export ledger custodianAdmin: Bytes<32>;

// Contract initialized flag
export ledger initialized: Boolean;

// Minimum balance thresholds for different tiers
export ledger tierOneThreshold: Uint<64>;    // e.g., 10,000
export ledger tierTwoThreshold: Uint<64>;    // e.g., 100,000
export ledger tierThreeThreshold: Uint<64>;  // e.g., 1,000,000

// ============================================
// INITIALIZATION
// ============================================

export circuit initialize(
  adminAddress: Bytes<32>,
  tier1: Uint<64>,
  tier2: Uint<64>,
  tier3: Uint<64>
): [] {
  assert(!initialized, "Contract already initialized");

  custodianAdmin = adminAddress;
  tierOneThreshold = tier1;
  tierTwoThreshold = tier2;
  tierThreeThreshold = tier3;
  initialized = true;
}

// ============================================
// ACCOUNT MANAGEMENT
// ============================================

// Register a new custodial account
// accountId is a hash of user's identity (privacy-preserving)
export circuit registerAccount(accountId: Bytes<32>): [] {
  assert(initialized, "Contract not initialized");
  assert(!accountExists[accountId], "Account already exists");

  accountExists[accountId] = true;
  accountStatus[accountId] = 1; // Active
  kycVerified[accountId] = false;
  kycLevel[accountId] = 0;
  accreditationStatus[accountId] = 0;

  totalAccounts.increment(1);
}

// Deposit assets (amount is added to private state, only aggregates are public)
export circuit deposit(
  accountId: Bytes<32>,
  amount: Uint<64>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(accountStatus[accountId] == 1, "Account not active");

  // Increment total custody (privacy: individual amounts hidden)
  totalAssetsUnderCustody.increment(amount);
}

// Withdraw assets
export circuit withdraw(
  accountId: Bytes<32>,
  amount: Uint<64>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(accountStatus[accountId] == 1, "Account not active");

  // Note: In a full implementation, balance check would be in private state
  // This decrements the public aggregate
  // The actual balance verification happens via ZK proof
}

// ============================================
// KYC VERIFICATION (Admin Functions)
// ============================================

// Admin verifies KYC for an account
export circuit verifyKYC(
  accountId: Bytes<32>,
  level: Uint<8>,
  adminSig: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(level >= 1, "Invalid KYC level");
  assert(level <= 3, "Invalid KYC level");

  // In production: verify adminSig matches custodianAdmin

  kycVerified[accountId] = true;
  kycLevel[accountId] = level;
}

// Admin verifies accreditation status
export circuit verifyAccreditation(
  accountId: Bytes<32>,
  status: Uint<8>,
  adminSig: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(kycVerified[accountId], "KYC required first");
  assert(status >= 1, "Invalid accreditation status");
  assert(status <= 2, "Invalid accreditation status");

  accreditationStatus[accountId] = status;
}

// ============================================
// SELECTIVE DISCLOSURE CIRCUITS
// ============================================

// Prove balance exceeds a threshold without revealing exact balance
// This circuit takes the actual balance as a private witness input
export circuit proveBalanceAboveThreshold(
  accountId: Bytes<32>,
  threshold: Uint<64>,
  actualBalance: Uint<64>,  // Private witness
  nonce: Bytes<32>          // Prevent replay
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(accountStatus[accountId] == 1, "Account not active");

  // The ZK magic: prove balance > threshold without revealing balance
  assert(actualBalance >= threshold, "Balance below threshold");

  // Log successful verification
  proofVerificationLog.increment(1);
  disclosureNonces[accountId].increment(1);
}

// Prove balance is within a range (e.g., $10k - $100k)
export circuit proveBalanceInRange(
  accountId: Bytes<32>,
  minBalance: Uint<64>,
  maxBalance: Uint<64>,
  actualBalance: Uint<64>,  // Private witness
  nonce: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(minBalance <= maxBalance, "Invalid range");

  // Prove balance is within range
  assert(actualBalance >= minBalance, "Balance below minimum");
  assert(actualBalance <= maxBalance, "Balance above maximum");

  proofVerificationLog.increment(1);
  disclosureNonces[accountId].increment(1);
}

// Prove account tier (based on balance thresholds)
// Returns tier without revealing exact balance
export circuit proveAccountTier(
  accountId: Bytes<32>,
  actualBalance: Uint<64>,  // Private witness
  claimedTier: Uint<8>,
  nonce: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(claimedTier >= 1, "Invalid tier");
  assert(claimedTier <= 3, "Invalid tier");

  // Verify claimed tier matches actual balance
  if (claimedTier == 1) {
    assert(actualBalance >= tierOneThreshold, "Insufficient balance for Tier 1");
  }
  if (claimedTier == 2) {
    assert(actualBalance >= tierTwoThreshold, "Insufficient balance for Tier 2");
  }
  if (claimedTier == 3) {
    assert(actualBalance >= tierThreeThreshold, "Insufficient balance for Tier 3");
  }

  proofVerificationLog.increment(1);
}

// Prove KYC status meets minimum level
export circuit proveKYCLevel(
  accountId: Bytes<32>,
  minimumLevel: Uint<8>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(kycVerified[accountId], "KYC not completed");
  assert(kycLevel[accountId] >= minimumLevel, "KYC level insufficient");

  proofVerificationLog.increment(1);
}

// Prove accredited investor status
export circuit proveAccreditedInvestor(accountId: Bytes<32>): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(accreditationStatus[accountId] >= 1, "Not accredited");

  proofVerificationLog.increment(1);
}

// Combined proof: KYC + Balance threshold (common for investments)
export circuit proveInvestmentEligibility(
  accountId: Bytes<32>,
  minKYCLevel: Uint<8>,
  minBalance: Uint<64>,
  actualBalance: Uint<64>,  // Private witness
  nonce: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(accountStatus[accountId] == 1, "Account not active");

  // Verify KYC
  assert(kycVerified[accountId], "KYC required");
  assert(kycLevel[accountId] >= minKYCLevel, "Insufficient KYC level");

  // Verify balance (private)
  assert(actualBalance >= minBalance, "Insufficient balance");

  proofVerificationLog.increment(1);
}

// ============================================
// ADMIN FUNCTIONS
// ============================================

// Freeze an account (compliance action)
export circuit freezeAccount(
  accountId: Bytes<32>,
  adminSig: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");

  accountStatus[accountId] = 2; // Frozen
}

// Unfreeze an account
export circuit unfreezeAccount(
  accountId: Bytes<32>,
  adminSig: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");
  assert(accountStatus[accountId] == 2, "Account not frozen");

  accountStatus[accountId] = 1; // Active
}

// Close an account
export circuit closeAccount(
  accountId: Bytes<32>,
  adminSig: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(accountExists[accountId], "Account does not exist");

  accountStatus[accountId] = 3; // Closed
}

// Update tier thresholds
export circuit updateTierThresholds(
  tier1: Uint<64>,
  tier2: Uint<64>,
  tier3: Uint<64>,
  adminSig: Bytes<32>
): [] {
  assert(initialized, "Contract not initialized");
  assert(tier1 < tier2, "Tier 1 must be less than Tier 2");
  assert(tier2 < tier3, "Tier 2 must be less than Tier 3");

  tierOneThreshold = tier1;
  tierTwoThreshold = tier2;
  tierThreeThreshold = tier3;
}
Enter fullscreen mode Exit fullscreen mode

Implementing Selective Disclosure

Private State Definition

Create custodian-contract/src/witnesses.ts:

// ============================================
// PRIVATE STATE (WITNESSES) FOR CUSTODIAN
// ============================================

export type AssetHolding = {
  assetType: string;       // "TOKEN", "NFT", "FIAT"
  assetId: string;         // Token address or asset identifier
  amount: bigint;          // Actual amount held
  acquisitionDate: number; // Unix timestamp
  acquisitionPrice: bigint; // Original purchase price
};

export type PersonalData = {
  firstName: string;
  lastName: string;
  dateOfBirth: number;     // Unix timestamp
  countryOfResidence: string;
  taxId: string;           // SSN, TIN, etc.
  email: string;
  phone: string;
};

export type KYCDocuments = {
  idType: string;          // "PASSPORT", "DRIVERS_LICENSE", "NATIONAL_ID"
  idNumber: string;
  idExpiry: number;
  proofOfAddressType: string;
  verificationDate: number;
  verifiedBy: string;      // Verification provider
};

export type AccreditationData = {
  verificationType: string; // "INCOME", "NET_WORTH", "PROFESSIONAL"
  verificationDate: number;
  expiryDate: number;
  certifyingEntity: string;
  supportingDocHash: string;
};

export type CustodianPrivateState = {
  // Account identification
  accountId: string;        // Hashed account ID (public reference)
  internalId: string;       // Internal account number

  // Financial data (PRIVATE - never disclosed directly)
  totalBalance: bigint;
  availableBalance: bigint;
  lockedBalance: bigint;
  holdings: AssetHolding[];

  // Personal data (PRIVATE - only attributes disclosed via ZK)
  personalData: PersonalData;

  // KYC data (PRIVATE - only verification status disclosed)
  kycDocuments: KYCDocuments | null;

  // Accreditation (PRIVATE - only status disclosed)
  accreditationData: AccreditationData | null;

  // Transaction history (PRIVATE)
  transactionCount: number;
  lastActivityDate: number;
};

// Factory function to create initial private state
export const createPrivateState = (
  accountId: string,
  internalId: string
): CustodianPrivateState => {
  return {
    accountId,
    internalId,
    totalBalance: 0n,
    availableBalance: 0n,
    lockedBalance: 0n,
    holdings: [],
    personalData: {
      firstName: '',
      lastName: '',
      dateOfBirth: 0,
      countryOfResidence: '',
      taxId: '',
      email: '',
      phone: '',
    },
    kycDocuments: null,
    accreditationData: null,
    transactionCount: 0,
    lastActivityDate: 0,
  };
};

// ============================================
// WITNESS FUNCTIONS FOR SELECTIVE DISCLOSURE
// ============================================

export const witnesses = {
  // Witness for balance threshold proof
  getBalanceForThresholdProof: (
    state: CustodianPrivateState
  ): bigint => {
    return state.totalBalance;
  },

  // Witness for range proof
  getBalanceForRangeProof: (
    state: CustodianPrivateState
  ): bigint => {
    return state.availableBalance;
  },

  // Witness for tier proof
  getTierWitness: (
    state: CustodianPrivateState,
    tierThresholds: { tier1: bigint; tier2: bigint; tier3: bigint }
  ): { balance: bigint; tier: number } => {
    const balance = state.totalBalance;
    let tier = 0;
    if (balance >= tierThresholds.tier3) tier = 3;
    else if (balance >= tierThresholds.tier2) tier = 2;
    else if (balance >= tierThresholds.tier1) tier = 1;
    return { balance, tier };
  },

  // Witness for age verification
  getAgeWitness: (
    state: CustodianPrivateState,
    referenceDate: number
  ): { dateOfBirth: number; ageInYears: number } => {
    const dob = state.personalData.dateOfBirth;
    const ageMs = referenceDate - dob;
    const ageYears = Math.floor(ageMs / (365.25 * 24 * 60 * 60 * 1000));
    return { dateOfBirth: dob, ageInYears: ageYears };
  },

  // Witness for country verification
  getResidencyWitness: (
    state: CustodianPrivateState
  ): string => {
    return state.personalData.countryOfResidence;
  },

  // Witness for investment eligibility
  getInvestmentEligibilityWitness: (
    state: CustodianPrivateState
  ): {
    balance: bigint;
    kycComplete: boolean;
    isAccredited: boolean;
  } => {
    return {
      balance: state.availableBalance,
      kycComplete: state.kycDocuments !== null,
      isAccredited: state.accreditationData !== null,
    };
  },
};

// ============================================
// DISCLOSURE PROOF BUILDERS
// ============================================

export type DisclosureType =
  | 'BALANCE_THRESHOLD'
  | 'BALANCE_RANGE'
  | 'ACCOUNT_TIER'
  | 'KYC_LEVEL'
  | 'ACCREDITATION'
  | 'AGE_VERIFICATION'
  | 'RESIDENCY'
  | 'INVESTMENT_ELIGIBILITY';

export type DisclosureRequest = {
  type: DisclosureType;
  accountId: string;
  parameters: Record<string, unknown>;
  nonce: string;
  requestedBy: string;
  requestedAt: number;
  expiresAt: number;
};

export type DisclosureProof = {
  type: DisclosureType;
  accountId: string;
  claim: string;           // Human-readable claim
  proofData: Uint8Array;   // ZK proof bytes
  publicInputs: unknown[]; // Public inputs for verification
  generatedAt: number;
  expiresAt: number;
};

// Build a disclosure proof from private state
export const buildDisclosureProof = async (
  request: DisclosureRequest,
  privateState: CustodianPrivateState
): Promise<DisclosureProof> => {
  // This would integrate with the Midnight proof server
  // to generate actual ZK proofs

  const now = Date.now();

  switch (request.type) {
    case 'BALANCE_THRESHOLD':
      const threshold = request.parameters.threshold as bigint;
      const meetsThreshold = privateState.totalBalance >= threshold;
      return {
        type: 'BALANCE_THRESHOLD',
        accountId: request.accountId,
        claim: `Balance exceeds ${threshold}`,
        proofData: new Uint8Array(), // Actual proof would go here
        publicInputs: [request.accountId, threshold, meetsThreshold],
        generatedAt: now,
        expiresAt: request.expiresAt,
      };

    case 'ACCOUNT_TIER':
      const { tier } = witnesses.getTierWitness(privateState, {
        tier1: 10000n,
        tier2: 100000n,
        tier3: 1000000n,
      });
      return {
        type: 'ACCOUNT_TIER',
        accountId: request.accountId,
        claim: `Account is Tier ${tier}`,
        proofData: new Uint8Array(),
        publicInputs: [request.accountId, tier],
        generatedAt: now,
        expiresAt: request.expiresAt,
      };

    // Add more cases for other disclosure types...

    default:
      throw new Error(`Unsupported disclosure type: ${request.type}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

Contract Controller

Create frontend-vite-react/src/modules/midnight/custodian-sdk/api/contractController.ts:

import {
  deployContract,
  findDeployedContract,
  type DeployedContract,
  type ContractAddress,
} from '@midnight-ntwrk/midnight-js-contracts';
import * as Rx from 'rxjs';

// Import compiled contract
import { Custodian } from 'custodian-contract/src/managed/custodian';
import {
  type CustodianPrivateState,
  createPrivateState,
  witnesses,
  type DisclosureRequest,
  type DisclosureProof,
  buildDisclosureProof,
} from 'custodian-contract/src/witnesses';

export type CustodianProviders = {
  publicDataProvider: PublicDataProvider;
  privateStateProvider: PrivateStateProvider;
  zkConfigProvider: ZkConfigProvider;
  proofProvider: ProofProvider;
  midnightProvider: MidnightProvider;
  walletProvider: WalletProvider;
};

export type CustodianLedgerState = {
  totalAccounts: bigint;
  totalAssetsUnderCustody: bigint;
  initialized: boolean;
  tierOneThreshold: bigint;
  tierTwoThreshold: bigint;
  tierThreeThreshold: bigint;
};

export type AccountInfo = {
  accountId: string;
  exists: boolean;
  status: number;
  kycVerified: boolean;
  kycLevel: number;
  accreditationStatus: number;
};

export type DerivedState = {
  ledger: CustodianLedgerState;
  accounts: Map<string, AccountInfo>;
  privateState: CustodianPrivateState | null;
  pendingDisclosures: DisclosureRequest[];
  completedProofs: DisclosureProof[];
};

export interface CustodianControllerInterface {
  readonly deployedContractAddress: ContractAddress;
  readonly state$: Rx.Observable<DerivedState>;

  // Account management
  registerAccount(accountId: string): Promise<void>;
  deposit(accountId: string, amount: bigint): Promise<void>;
  withdraw(accountId: string, amount: bigint): Promise<void>;

  // KYC/Accreditation
  verifyKYC(accountId: string, level: number): Promise<void>;
  verifyAccreditation(accountId: string, status: number): Promise<void>;

  // Selective disclosure
  requestDisclosure(request: DisclosureRequest): Promise<void>;
  generateProof(request: DisclosureRequest): Promise<DisclosureProof>;

  // Verification circuits
  proveBalanceAboveThreshold(
    accountId: string,
    threshold: bigint
  ): Promise<void>;
  proveBalanceInRange(
    accountId: string,
    min: bigint,
    max: bigint
  ): Promise<void>;
  proveAccountTier(
    accountId: string,
    tier: number
  ): Promise<void>;
  proveInvestmentEligibility(
    accountId: string,
    minKYC: number,
    minBalance: bigint
  ): Promise<void>;
}

export class CustodianController implements CustodianControllerInterface {
  readonly deployedContractAddress: ContractAddress;
  readonly state$: Rx.Observable<DerivedState>;

  private readonly deployedContract: DeployedContract<Custodian>;
  private readonly providers: CustodianProviders;
  private privateState: CustodianPrivateState | null = null;
  private pendingDisclosures: DisclosureRequest[] = [];
  private completedProofs: DisclosureProof[] = [];

  private constructor(
    deployedContract: DeployedContract<Custodian>,
    providers: CustodianProviders
  ) {
    this.deployedContract = deployedContract;
    this.deployedContractAddress = deployedContract.deployTxData.public.contractAddress;
    this.providers = providers;

    // Set up reactive state
    this.state$ = this.createStateObservable();
  }

  // ============================================
  // FACTORY METHODS
  // ============================================

  static async deploy(
    providers: CustodianProviders,
    adminAddress: string,
    tiers: { tier1: bigint; tier2: bigint; tier3: bigint }
  ): Promise<CustodianController> {
    const deployedContract = await deployContract(providers, {
      contract: Custodian,
      initialPrivateState: createPrivateState('', ''),
    });

    const controller = new CustodianController(deployedContract, providers);

    // Initialize contract
    await controller.deployedContract.callTx.initialize(
      stringToBytes32(adminAddress),
      tiers.tier1,
      tiers.tier2,
      tiers.tier3
    );

    return controller;
  }

  static async join(
    contractAddress: ContractAddress,
    providers: CustodianProviders
  ): Promise<CustodianController> {
    const deployedContract = await findDeployedContract(providers, {
      contractAddress,
      contract: Custodian,
    });

    return new CustodianController(deployedContract, providers);
  }

  // ============================================
  // ACCOUNT MANAGEMENT
  // ============================================

  async registerAccount(accountId: string): Promise<void> {
    const accountIdBytes = stringToBytes32(accountId);
    await this.deployedContract.callTx.registerAccount(accountIdBytes);

    // Initialize local private state
    this.privateState = createPrivateState(accountId, generateInternalId());
  }

  async deposit(accountId: string, amount: bigint): Promise<void> {
    const accountIdBytes = stringToBytes32(accountId);
    await this.deployedContract.callTx.deposit(accountIdBytes, amount);

    // Update local private state
    if (this.privateState && this.privateState.accountId === accountId) {
      this.privateState.totalBalance += amount;
      this.privateState.availableBalance += amount;
      this.privateState.transactionCount++;
      this.privateState.lastActivityDate = Date.now();
    }
  }

  async withdraw(accountId: string, amount: bigint): Promise<void> {
    if (!this.privateState || this.privateState.accountId !== accountId) {
      throw new Error('Not authorized for this account');
    }

    if (this.privateState.availableBalance < amount) {
      throw new Error('Insufficient balance');
    }

    const accountIdBytes = stringToBytes32(accountId);
    await this.deployedContract.callTx.withdraw(accountIdBytes, amount);

    this.privateState.totalBalance -= amount;
    this.privateState.availableBalance -= amount;
    this.privateState.transactionCount++;
    this.privateState.lastActivityDate = Date.now();
  }

  // ============================================
  // KYC & ACCREDITATION
  // ============================================

  async verifyKYC(accountId: string, level: number): Promise<void> {
    const accountIdBytes = stringToBytes32(accountId);
    const adminSig = stringToBytes32('admin-signature'); // Simplified
    await this.deployedContract.callTx.verifyKYC(
      accountIdBytes,
      BigInt(level),
      adminSig
    );
  }

  async verifyAccreditation(accountId: string, status: number): Promise<void> {
    const accountIdBytes = stringToBytes32(accountId);
    const adminSig = stringToBytes32('admin-signature');
    await this.deployedContract.callTx.verifyAccreditation(
      accountIdBytes,
      BigInt(status),
      adminSig
    );
  }

  // ============================================
  // SELECTIVE DISCLOSURE
  // ============================================

  async requestDisclosure(request: DisclosureRequest): Promise<void> {
    this.pendingDisclosures.push(request);
  }

  async generateProof(request: DisclosureRequest): Promise<DisclosureProof> {
    if (!this.privateState) {
      throw new Error('No private state available');
    }

    const proof = await buildDisclosureProof(request, this.privateState);
    this.completedProofs.push(proof);

    // Remove from pending
    this.pendingDisclosures = this.pendingDisclosures.filter(
      (r) => r.nonce !== request.nonce
    );

    return proof;
  }

  async proveBalanceAboveThreshold(
    accountId: string,
    threshold: bigint
  ): Promise<void> {
    if (!this.privateState || this.privateState.accountId !== accountId) {
      throw new Error('Not authorized for this account');
    }

    const accountIdBytes = stringToBytes32(accountId);
    const nonce = stringToBytes32(generateNonce());

    await this.deployedContract.callTx.proveBalanceAboveThreshold(
      accountIdBytes,
      threshold,
      this.privateState.totalBalance, // Private witness
      nonce
    );
  }

  async proveBalanceInRange(
    accountId: string,
    min: bigint,
    max: bigint
  ): Promise<void> {
    if (!this.privateState || this.privateState.accountId !== accountId) {
      throw new Error('Not authorized for this account');
    }

    const accountIdBytes = stringToBytes32(accountId);
    const nonce = stringToBytes32(generateNonce());

    await this.deployedContract.callTx.proveBalanceInRange(
      accountIdBytes,
      min,
      max,
      this.privateState.availableBalance,
      nonce
    );
  }

  async proveAccountTier(accountId: string, tier: number): Promise<void> {
    if (!this.privateState || this.privateState.accountId !== accountId) {
      throw new Error('Not authorized for this account');
    }

    const accountIdBytes = stringToBytes32(accountId);
    const nonce = stringToBytes32(generateNonce());

    await this.deployedContract.callTx.proveAccountTier(
      accountIdBytes,
      this.privateState.totalBalance,
      BigInt(tier),
      nonce
    );
  }

  async proveInvestmentEligibility(
    accountId: string,
    minKYC: number,
    minBalance: bigint
  ): Promise<void> {
    if (!this.privateState || this.privateState.accountId !== accountId) {
      throw new Error('Not authorized for this account');
    }

    const accountIdBytes = stringToBytes32(accountId);
    const nonce = stringToBytes32(generateNonce());

    await this.deployedContract.callTx.proveInvestmentEligibility(
      accountIdBytes,
      BigInt(minKYC),
      minBalance,
      this.privateState.availableBalance,
      nonce
    );
  }

  // ============================================
  // PRIVATE HELPERS
  // ============================================

  private createStateObservable(): Rx.Observable<DerivedState> {
    return this.providers.publicDataProvider
      .contractStateObservable(this.deployedContractAddress, { type: 'all' })
      .pipe(
        Rx.map((state) => {
          const ledger = Custodian.ledger(state.data);
          return {
            ledger: {
              totalAccounts: ledger.totalAccounts,
              totalAssetsUnderCustody: ledger.totalAssetsUnderCustody,
              initialized: ledger.initialized,
              tierOneThreshold: ledger.tierOneThreshold,
              tierTwoThreshold: ledger.tierTwoThreshold,
              tierThreeThreshold: ledger.tierThreeThreshold,
            },
            accounts: new Map(),
            privateState: this.privateState,
            pendingDisclosures: this.pendingDisclosures,
            completedProofs: this.completedProofs,
          };
        })
      );
  }
}

// ============================================
// UTILITY FUNCTIONS
// ============================================

function stringToBytes32(str: string): Uint8Array {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(str);
  const result = new Uint8Array(32);
  result.set(bytes.slice(0, 32));
  return result;
}

function generateNonce(): string {
  return crypto.randomUUID();
}

function generateInternalId(): string {
  return `INT-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
Enter fullscreen mode Exit fullscreen mode

Frontend Integration

Main Custodian Dashboard

Create frontend-vite-react/src/pages/custodian/index.tsx:

import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
  Shield,
  Eye,
  EyeOff,
  CheckCircle,
  AlertTriangle,
  Lock,
  Unlock,
  FileCheck,
  DollarSign,
  Users,
  Activity,
} from 'lucide-react';

// Types
type AccountStatus = 'inactive' | 'active' | 'frozen' | 'closed';
type KYCLevel = 0 | 1 | 2 | 3;
type AccreditationStatus = 0 | 1 | 2;

type AccountInfo = {
  accountId: string;
  status: AccountStatus;
  kycLevel: KYCLevel;
  accreditationStatus: AccreditationStatus;
  // Private (local only)
  balance?: bigint;
  availableBalance?: bigint;
};

type DisclosureProof = {
  type: string;
  claim: string;
  verified: boolean;
  timestamp: number;
};

// Mock data for demonstration
const mockAccount: AccountInfo = {
  accountId: '0x1234...5678',
  status: 'active',
  kycLevel: 2,
  accreditationStatus: 1,
  balance: 150000n,
  availableBalance: 145000n,
};

export const CustodianDashboard = () => {
  const [account, setAccount] = useState<AccountInfo | null>(null);
  const [activeTab, setActiveTab] = useState('overview');
  const [showBalance, setShowBalance] = useState(false);
  const [proofs, setProofs] = useState<DisclosureProof[]>([]);
  const [loading, setLoading] = useState(false);

  // Disclosure form state
  const [disclosureType, setDisclosureType] = useState('');
  const [threshold, setThreshold] = useState('');

  useEffect(() => {
    // Load account data
    setAccount(mockAccount);
  }, []);

  const handleGenerateProof = async (type: string) => {
    setLoading(true);

    // Simulate proof generation
    await new Promise((resolve) => setTimeout(resolve, 2000));

    const newProof: DisclosureProof = {
      type,
      claim: getClaimText(type),
      verified: true,
      timestamp: Date.now(),
    };

    setProofs((prev) => [newProof, ...prev]);
    setLoading(false);
  };

  const getClaimText = (type: string): string => {
    switch (type) {
      case 'balance_threshold':
        return `Balance exceeds $${threshold}`;
      case 'tier':
        return 'Account is Tier 2 or higher';
      case 'kyc':
        return 'KYC Level 2 verified';
      case 'accredited':
        return 'Accredited Investor status verified';
      case 'investment_eligible':
        return 'Eligible for qualified investments';
      default:
        return 'Claim verified';
    }
  };

  const getStatusBadge = (status: AccountStatus) => {
    const variants: Record<AccountStatus, { color: string; icon: React.ReactNode }> = {
      active: { color: 'bg-green-500', icon: <CheckCircle className="w-3 h-3" /> },
      inactive: { color: 'bg-gray-500', icon: <AlertTriangle className="w-3 h-3" /> },
      frozen: { color: 'bg-blue-500', icon: <Lock className="w-3 h-3" /> },
      closed: { color: 'bg-red-500', icon: <AlertTriangle className="w-3 h-3" /> },
    };

    return (
      <Badge className={`${variants[status].color} text-white`}>
        {variants[status].icon}
        <span className="ml-1 capitalize">{status}</span>
      </Badge>
    );
  };

  const getKYCBadge = (level: KYCLevel) => {
    const labels = ['None', 'Basic', 'Standard', 'Enhanced'];
    const colors = ['bg-gray-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'];

    return (
      <Badge className={`${colors[level]} text-white`}>
        <FileCheck className="w-3 h-3 mr-1" />
        KYC: {labels[level]}
      </Badge>
    );
  };

  if (!account) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary" />
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-background py-8 px-4">
      <div className="max-w-6xl mx-auto space-y-6">
        {/* Header */}
        <div className="flex items-center justify-between">
          <div>
            <h1 className="text-3xl font-bold flex items-center gap-2">
              <Shield className="w-8 h-8 text-primary" />
              Custodian Dashboard
            </h1>
            <p className="text-muted-foreground mt-1">
              Privacy-preserving asset custody with selective disclosure
            </p>
          </div>
          <div className="flex gap-2">
            {getStatusBadge(account.status)}
            {getKYCBadge(account.kycLevel)}
          </div>
        </div>

        {/* Stats Overview */}
        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
          <Card>
            <CardContent className="pt-6">
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-muted-foreground">Total Balance</p>
                  <div className="flex items-center gap-2">
                    {showBalance ? (
                      <p className="text-2xl font-bold">
                        ${account.balance?.toLocaleString()}
                      </p>
                    ) : (
                      <p className="text-2xl font-bold">*****</p>
                    )}
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => setShowBalance(!showBalance)}
                    >
                      {showBalance ? (
                        <EyeOff className="w-4 h-4" />
                      ) : (
                        <Eye className="w-4 h-4" />
                      )}
                    </Button>
                  </div>
                </div>
                <DollarSign className="w-8 h-8 text-muted-foreground" />
              </div>
            </CardContent>
          </Card>

          <Card>
            <CardContent className="pt-6">
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-muted-foreground">Account Tier</p>
                  <p className="text-2xl font-bold">Tier 2</p>
                </div>
                <Activity className="w-8 h-8 text-muted-foreground" />
              </div>
            </CardContent>
          </Card>

          <Card>
            <CardContent className="pt-6">
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-muted-foreground">Proofs Generated</p>
                  <p className="text-2xl font-bold">{proofs.length}</p>
                </div>
                <Shield className="w-8 h-8 text-muted-foreground" />
              </div>
            </CardContent>
          </Card>

          <Card>
            <CardContent className="pt-6">
              <div className="flex items-center justify-between">
                <div>
                  <p className="text-sm text-muted-foreground">Accreditation</p>
                  <p className="text-2xl font-bold">
                    {account.accreditationStatus === 1 ? 'Verified' : 'Pending'}
                  </p>
                </div>
                <Users className="w-8 h-8 text-muted-foreground" />
              </div>
            </CardContent>
          </Card>
        </div>

        {/* Main Content Tabs */}
        <Tabs value={activeTab} onValueChange={setActiveTab}>
          <TabsList className="grid w-full grid-cols-4">
            <TabsTrigger value="overview">Overview</TabsTrigger>
            <TabsTrigger value="disclosure">Selective Disclosure</TabsTrigger>
            <TabsTrigger value="proofs">Proof History</TabsTrigger>
            <TabsTrigger value="settings">Settings</TabsTrigger>
          </TabsList>

          {/* Overview Tab */}
          <TabsContent value="overview" className="space-y-4">
            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
              <Card>
                <CardHeader>
                  <CardTitle>Account Information</CardTitle>
                  <CardDescription>
                    Your custodial account details
                  </CardDescription>
                </CardHeader>
                <CardContent className="space-y-4">
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Account ID</span>
                    <span className="font-mono">{account.accountId}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Status</span>
                    {getStatusBadge(account.status)}
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">KYC Level</span>
                    {getKYCBadge(account.kycLevel)}
                  </div>
                  <div className="flex justify-between">
                    <span className="text-muted-foreground">Accreditation</span>
                    <Badge variant={account.accreditationStatus ? 'default' : 'secondary'}>
                      {account.accreditationStatus === 1
                        ? 'Accredited Investor'
                        : account.accreditationStatus === 2
                        ? 'Qualified Purchaser'
                        : 'Not Verified'}
                    </Badge>
                  </div>
                </CardContent>
              </Card>

              <Card>
                <CardHeader>
                  <CardTitle>Privacy Status</CardTitle>
                  <CardDescription>
                    What information is public vs. private
                  </CardDescription>
                </CardHeader>
                <CardContent className="space-y-3">
                  <div className="flex items-center gap-2 text-green-600">
                    <Lock className="w-4 h-4" />
                    <span>Exact balance (Private)</span>
                  </div>
                  <div className="flex items-center gap-2 text-green-600">
                    <Lock className="w-4 h-4" />
                    <span>Personal information (Private)</span>
                  </div>
                  <div className="flex items-center gap-2 text-green-600">
                    <Lock className="w-4 h-4" />
                    <span>Transaction history (Private)</span>
                  </div>
                  <div className="flex items-center gap-2 text-blue-600">
                    <Unlock className="w-4 h-4" />
                    <span>Account exists (Public)</span>
                  </div>
                  <div className="flex items-center gap-2 text-blue-600">
                    <Unlock className="w-4 h-4" />
                    <span>KYC verified status (Public)</span>
                  </div>
                </CardContent>
              </Card>
            </div>

            <Alert>
              <Shield className="w-4 h-4" />
              <AlertDescription>
                Your sensitive financial data is encrypted and stored privately.
                Only you can generate proofs about your data, and you control
                exactly what information is disclosed.
              </AlertDescription>
            </Alert>
          </TabsContent>

          {/* Selective Disclosure Tab */}
          <TabsContent value="disclosure" className="space-y-4">
            <Card>
              <CardHeader>
                <CardTitle>Generate Disclosure Proofs</CardTitle>
                <CardDescription>
                  Prove claims about your account without revealing sensitive data
                </CardDescription>
              </CardHeader>
              <CardContent className="space-y-6">
                {/* Balance Threshold Proof */}
                <div className="border rounded-lg p-4 space-y-4">
                  <div className="flex items-center justify-between">
                    <div>
                      <h3 className="font-semibold">Balance Threshold Proof</h3>
                      <p className="text-sm text-muted-foreground">
                        Prove your balance exceeds a specific amount
                      </p>
                    </div>
                    <DollarSign className="w-6 h-6 text-muted-foreground" />
                  </div>
                  <div className="flex gap-4">
                    <div className="flex-1">
                      <Label htmlFor="threshold">Threshold Amount ($)</Label>
                      <Input
                        id="threshold"
                        type="number"
                        placeholder="10000"
                        value={threshold}
                        onChange={(e) => setThreshold(e.target.value)}
                      />
                    </div>
                    <Button
                      className="self-end"
                      onClick={() => handleGenerateProof('balance_threshold')}
                      disabled={loading || !threshold}
                    >
                      {loading ? 'Generating...' : 'Generate Proof'}
                    </Button>
                  </div>
                  <p className="text-xs text-muted-foreground">
                    This will prove: "Balance &ge; ${threshold || '0'}" without revealing exact amount
                  </p>
                </div>

                {/* Tier Proof */}
                <div className="border rounded-lg p-4 space-y-4">
                  <div className="flex items-center justify-between">
                    <div>
                      <h3 className="font-semibold">Account Tier Proof</h3>
                      <p className="text-sm text-muted-foreground">
                        Prove your account tier without revealing balance
                      </p>
                    </div>
                    <Activity className="w-6 h-6 text-muted-foreground" />
                  </div>
                  <div className="flex gap-2">
                    <Button
                      variant="outline"
                      onClick={() => handleGenerateProof('tier')}
                      disabled={loading}
                    >
                      Prove Tier 1+
                    </Button>
                    <Button
                      variant="outline"
                      onClick={() => handleGenerateProof('tier')}
                      disabled={loading}
                    >
                      Prove Tier 2+
                    </Button>
                    <Button
                      variant="outline"
                      onClick={() => handleGenerateProof('tier')}
                      disabled={loading}
                    >
                      Prove Tier 3
                    </Button>
                  </div>
                </div>

                {/* KYC Level Proof */}
                <div className="border rounded-lg p-4 space-y-4">
                  <div className="flex items-center justify-between">
                    <div>
                      <h3 className="font-semibold">KYC Verification Proof</h3>
                      <p className="text-sm text-muted-foreground">
                        Prove KYC completion without revealing documents
                      </p>
                    </div>
                    <FileCheck className="w-6 h-6 text-muted-foreground" />
                  </div>
                  <Button
                    onClick={() => handleGenerateProof('kyc')}
                    disabled={loading}
                  >
                    Prove KYC Level 2+
                  </Button>
                </div>

                {/* Accredited Investor Proof */}
                <div className="border rounded-lg p-4 space-y-4">
                  <div className="flex items-center justify-between">
                    <div>
                      <h3 className="font-semibold">Accredited Investor Proof</h3>
                      <p className="text-sm text-muted-foreground">
                        Prove accreditation without revealing financials
                      </p>
                    </div>
                    <Users className="w-6 h-6 text-muted-foreground" />
                  </div>
                  <Button
                    onClick={() => handleGenerateProof('accredited')}
                    disabled={loading || account.accreditationStatus === 0}
                  >
                    {account.accreditationStatus === 0
                      ? 'Accreditation Required'
                      : 'Prove Accredited Status'}
                  </Button>
                </div>

                {/* Combined Investment Eligibility */}
                <div className="border rounded-lg p-4 space-y-4 bg-primary/5">
                  <div className="flex items-center justify-between">
                    <div>
                      <h3 className="font-semibold">Investment Eligibility Proof</h3>
                      <p className="text-sm text-muted-foreground">
                        Combined proof: KYC + Balance + Accreditation
                      </p>
                    </div>
                    <Shield className="w-6 h-6 text-primary" />
                  </div>
                  <Button
                    onClick={() => handleGenerateProof('investment_eligible')}
                    disabled={loading}
                    className="w-full"
                  >
                    Generate Investment Eligibility Proof
                  </Button>
                  <p className="text-xs text-muted-foreground">
                    Proves you meet all requirements for qualified investments
                    in a single proof, without revealing specific details.
                  </p>
                </div>
              </CardContent>
            </Card>
          </TabsContent>

          {/* Proof History Tab */}
          <TabsContent value="proofs" className="space-y-4">
            <Card>
              <CardHeader>
                <CardTitle>Generated Proofs</CardTitle>
                <CardDescription>
                  History of your disclosure proofs
                </CardDescription>
              </CardHeader>
              <CardContent>
                {proofs.length === 0 ? (
                  <div className="text-center py-8 text-muted-foreground">
                    <Shield className="w-12 h-12 mx-auto mb-4 opacity-50" />
                    <p>No proofs generated yet</p>
                    <p className="text-sm">
                      Generate proofs from the Selective Disclosure tab
                    </p>
                  </div>
                ) : (
                  <div className="space-y-3">
                    {proofs.map((proof, index) => (
                      <div
                        key={index}
                        className="flex items-center justify-between p-4 border rounded-lg"
                      >
                        <div className="flex items-center gap-3">
                          {proof.verified ? (
                            <CheckCircle className="w-5 h-5 text-green-500" />
                          ) : (
                            <AlertTriangle className="w-5 h-5 text-yellow-500" />
                          )}
                          <div>
                            <p className="font-medium">{proof.claim}</p>
                            <p className="text-sm text-muted-foreground">
                              {new Date(proof.timestamp).toLocaleString()}
                            </p>
                          </div>
                        </div>
                        <Badge variant={proof.verified ? 'default' : 'secondary'}>
                          {proof.verified ? 'Verified' : 'Pending'}
                        </Badge>
                      </div>
                    ))}
                  </div>
                )}
              </CardContent>
            </Card>
          </TabsContent>

          {/* Settings Tab */}
          <TabsContent value="settings" className="space-y-4">
            <Card>
              <CardHeader>
                <CardTitle>Privacy Settings</CardTitle>
                <CardDescription>
                  Configure your disclosure preferences
                </CardDescription>
              </CardHeader>
              <CardContent className="space-y-4">
                <div className="flex items-center justify-between p-4 border rounded-lg">
                  <div>
                    <p className="font-medium">Auto-expire proofs</p>
                    <p className="text-sm text-muted-foreground">
                      Proofs automatically expire after 24 hours
                    </p>
                  </div>
                  <Button variant="outline">Configure</Button>
                </div>
                <div className="flex items-center justify-between p-4 border rounded-lg">
                  <div>
                    <p className="font-medium">Require confirmation</p>
                    <p className="text-sm text-muted-foreground">
                      Confirm before generating any proof
                    </p>
                  </div>
                  <Button variant="outline">Configure</Button>
                </div>
                <div className="flex items-center justify-between p-4 border rounded-lg">
                  <div>
                    <p className="font-medium">Audit log</p>
                    <p className="text-sm text-muted-foreground">
                      Keep record of all proof requests
                    </p>
                  </div>
                  <Button variant="outline">View Log</Button>
                </div>
              </CardContent>
            </Card>
          </TabsContent>
        </Tabs>
      </div>
    </div>
  );
};

export default CustodianDashboard;
Enter fullscreen mode Exit fullscreen mode

Verifier Component

Create frontend-vite-react/src/pages/custodian/verifier.tsx:

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
  Shield,
  CheckCircle,
  XCircle,
  Upload,
  Search,
} from 'lucide-react';

type VerificationResult = {
  valid: boolean;
  claim: string;
  accountId: string;
  timestamp: number;
  expiresAt: number;
};

export const ProofVerifier = () => {
  const [proofInput, setProofInput] = useState('');
  const [verifying, setVerifying] = useState(false);
  const [result, setResult] = useState<VerificationResult | null>(null);

  const handleVerify = async () => {
    setVerifying(true);

    // Simulate verification
    await new Promise((resolve) => setTimeout(resolve, 1500));

    // Mock result
    setResult({
      valid: true,
      claim: 'Balance exceeds $10,000',
      accountId: '0x1234...5678',
      timestamp: Date.now() - 3600000,
      expiresAt: Date.now() + 86400000,
    });

    setVerifying(false);
  };

  return (
    <div className="min-h-screen bg-background py-8 px-4">
      <div className="max-w-2xl mx-auto space-y-6">
        <div className="text-center">
          <h1 className="text-3xl font-bold flex items-center justify-center gap-2">
            <Shield className="w-8 h-8 text-primary" />
            Proof Verifier
          </h1>
          <p className="text-muted-foreground mt-2">
            Verify selective disclosure proofs without accessing private data
          </p>
        </div>

        <Card>
          <CardHeader>
            <CardTitle>Verify a Proof</CardTitle>
            <CardDescription>
              Paste a proof or upload a proof file to verify its validity
            </CardDescription>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="space-y-2">
              <Label htmlFor="proof">Proof Data</Label>
              <Input
                id="proof"
                placeholder="Paste proof data here..."
                value={proofInput}
                onChange={(e) => setProofInput(e.target.value)}
              />
            </div>

            <div className="flex gap-2">
              <Button
                onClick={handleVerify}
                disabled={verifying || !proofInput}
                className="flex-1"
              >
                {verifying ? (
                  <>Verifying...</>
                ) : (
                  <>
                    <Search className="w-4 h-4 mr-2" />
                    Verify Proof
                  </>
                )}
              </Button>
              <Button variant="outline">
                <Upload className="w-4 h-4 mr-2" />
                Upload File
              </Button>
            </div>
          </CardContent>
        </Card>

        {result && (
          <Card className={result.valid ? 'border-green-500' : 'border-red-500'}>
            <CardHeader>
              <CardTitle className="flex items-center gap-2">
                {result.valid ? (
                  <>
                    <CheckCircle className="w-6 h-6 text-green-500" />
                    Proof Valid
                  </>
                ) : (
                  <>
                    <XCircle className="w-6 h-6 text-red-500" />
                    Proof Invalid
                  </>
                )}
              </CardTitle>
            </CardHeader>
            <CardContent className="space-y-4">
              <div className="grid grid-cols-2 gap-4">
                <div>
                  <p className="text-sm text-muted-foreground">Claim</p>
                  <p className="font-medium">{result.claim}</p>
                </div>
                <div>
                  <p className="text-sm text-muted-foreground">Account</p>
                  <p className="font-mono">{result.accountId}</p>
                </div>
                <div>
                  <p className="text-sm text-muted-foreground">Generated</p>
                  <p>{new Date(result.timestamp).toLocaleString()}</p>
                </div>
                <div>
                  <p className="text-sm text-muted-foreground">Expires</p>
                  <p>{new Date(result.expiresAt).toLocaleString()}</p>
                </div>
              </div>

              <Alert>
                <Shield className="w-4 h-4" />
                <AlertDescription>
                  This proof was verified using zero-knowledge cryptography.
                  The verifier learned ONLY that the claim is true, without
                  accessing any private data.
                </AlertDescription>
              </Alert>
            </CardContent>
          </Card>
        )}

        <Card>
          <CardHeader>
            <CardTitle>What Can Be Verified</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="grid grid-cols-2 gap-4">
              <div className="flex items-center gap-2">
                <Badge>Balance</Badge>
                <span className="text-sm">Exceeds threshold</span>
              </div>
              <div className="flex items-center gap-2">
                <Badge>Tier</Badge>
                <span className="text-sm">Account tier level</span>
              </div>
              <div className="flex items-center gap-2">
                <Badge>KYC</Badge>
                <span className="text-sm">Verification level</span>
              </div>
              <div className="flex items-center gap-2">
                <Badge>Accreditation</Badge>
                <span className="text-sm">Investor status</span>
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  );
};

export default ProofVerifier;
Enter fullscreen mode Exit fullscreen mode

Testing Your Contract

Test Suite

Create custodian-contract/src/test/custodian.test.ts:

import { describe, it, expect, beforeEach } from 'vitest';
import { CustodianSimulator } from './simulators/simulator';

describe('Custodian Contract with Selective Disclosure', () => {
  let simulator: CustodianSimulator;

  beforeEach(() => {
    simulator = CustodianSimulator.deploy({
      adminAddress: 'admin-001',
      tier1: 10000n,
      tier2: 100000n,
      tier3: 1000000n,
    });
  });

  describe('Account Management', () => {
    it('registers a new account', () => {
      simulator.registerAccount('user-001');

      const ledger = simulator.getLedger();
      expect(ledger.totalAccounts).toBe(1n);
      expect(simulator.accountExists('user-001')).toBe(true);
    });

    it('prevents duplicate account registration', () => {
      simulator.registerAccount('user-001');

      expect(() => {
        simulator.registerAccount('user-001');
      }).toThrow('Account already exists');
    });

    it('handles deposits correctly', () => {
      simulator.registerAccount('user-001');
      simulator.deposit('user-001', 50000n);

      const ledger = simulator.getLedger();
      expect(ledger.totalAssetsUnderCustody).toBe(50000n);
    });
  });

  describe('KYC Verification', () => {
    it('verifies KYC for an account', () => {
      simulator.registerAccount('user-001');
      simulator.verifyKYC('user-001', 2);

      expect(simulator.getKYCLevel('user-001')).toBe(2);
      expect(simulator.isKYCVerified('user-001')).toBe(true);
    });

    it('requires valid KYC level', () => {
      simulator.registerAccount('user-001');

      expect(() => {
        simulator.verifyKYC('user-001', 5);
      }).toThrow('Invalid KYC level');
    });
  });

  describe('Selective Disclosure', () => {
    beforeEach(() => {
      simulator.registerAccount('user-001');
      simulator.deposit('user-001', 150000n);
      simulator.verifyKYC('user-001', 2);
    });

    it('proves balance above threshold', () => {
      // This should succeed - balance 150000 > 100000
      expect(() => {
        simulator.proveBalanceAboveThreshold('user-001', 100000n, 150000n);
      }).not.toThrow();
    });

    it('fails proof when balance is insufficient', () => {
      // This should fail - balance 150000 < 200000
      expect(() => {
        simulator.proveBalanceAboveThreshold('user-001', 200000n, 150000n);
      }).toThrow('Balance below threshold');
    });

    it('proves balance in range', () => {
      expect(() => {
        simulator.proveBalanceInRange('user-001', 100000n, 200000n, 150000n);
      }).not.toThrow();
    });

    it('fails range proof when outside range', () => {
      expect(() => {
        simulator.proveBalanceInRange('user-001', 200000n, 300000n, 150000n);
      }).toThrow('Balance below minimum');
    });

    it('proves account tier', () => {
      // Balance 150000 qualifies for Tier 2 (100000 threshold)
      expect(() => {
        simulator.proveAccountTier('user-001', 150000n, 2);
      }).not.toThrow();
    });

    it('fails tier proof for higher tier', () => {
      // Balance 150000 does not qualify for Tier 3 (1000000 threshold)
      expect(() => {
        simulator.proveAccountTier('user-001', 150000n, 3);
      }).toThrow('Insufficient balance for Tier 3');
    });

    it('proves KYC level', () => {
      expect(() => {
        simulator.proveKYCLevel('user-001', 2);
      }).not.toThrow();
    });

    it('proves investment eligibility', () => {
      simulator.verifyAccreditation('user-001', 1);

      expect(() => {
        simulator.proveInvestmentEligibility('user-001', 2, 100000n, 150000n);
      }).not.toThrow();
    });
  });

  describe('Admin Functions', () => {
    it('freezes an account', () => {
      simulator.registerAccount('user-001');
      simulator.freezeAccount('user-001');

      expect(simulator.getAccountStatus('user-001')).toBe(2); // Frozen
    });

    it('unfreezes an account', () => {
      simulator.registerAccount('user-001');
      simulator.freezeAccount('user-001');
      simulator.unfreezeAccount('user-001');

      expect(simulator.getAccountStatus('user-001')).toBe(1); // Active
    });

    it('prevents operations on frozen accounts', () => {
      simulator.registerAccount('user-001');
      simulator.freezeAccount('user-001');

      expect(() => {
        simulator.deposit('user-001', 10000n);
      }).toThrow('Account not active');
    });
  });

  describe('Privacy Guarantees', () => {
    it('does not expose actual balance in ledger', () => {
      simulator.registerAccount('user-001');
      simulator.deposit('user-001', 150000n);

      const ledger = simulator.getLedger();
      // Ledger should only show aggregate, not individual balance
      expect(ledger.totalAssetsUnderCustody).toBe(150000n);
      // Individual balance should not be accessible from ledger
    });

    it('increments proof counter without exposing proof details', () => {
      simulator.registerAccount('user-001');
      simulator.deposit('user-001', 150000n);

      const before = simulator.getLedger().proofVerificationLog;
      simulator.proveBalanceAboveThreshold('user-001', 100000n, 150000n);
      const after = simulator.getLedger().proofVerificationLog;

      expect(after).toBe(before + 1n);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Test Simulator

Create custodian-contract/src/test/simulators/simulator.ts:

type LedgerState = {
  totalAccounts: bigint;
  totalAssetsUnderCustody: bigint;
  initialized: boolean;
  tierOneThreshold: bigint;
  tierTwoThreshold: bigint;
  tierThreeThreshold: bigint;
  proofVerificationLog: bigint;
};

type AccountState = {
  exists: boolean;
  status: number;
  kycVerified: boolean;
  kycLevel: number;
  accreditationStatus: number;
};

type InitConfig = {
  adminAddress: string;
  tier1: bigint;
  tier2: bigint;
  tier3: bigint;
};

export class CustodianSimulator {
  private ledger: LedgerState;
  private accounts: Map<string, AccountState>;
  private balances: Map<string, bigint>;

  private constructor(config: InitConfig) {
    this.ledger = {
      totalAccounts: 0n,
      totalAssetsUnderCustody: 0n,
      initialized: true,
      tierOneThreshold: config.tier1,
      tierTwoThreshold: config.tier2,
      tierThreeThreshold: config.tier3,
      proofVerificationLog: 0n,
    };
    this.accounts = new Map();
    this.balances = new Map();
  }

  static deploy(config: InitConfig): CustodianSimulator {
    return new CustodianSimulator(config);
  }

  getLedger(): LedgerState {
    return { ...this.ledger };
  }

  accountExists(accountId: string): boolean {
    return this.accounts.has(accountId);
  }

  getAccountStatus(accountId: string): number {
    return this.accounts.get(accountId)?.status ?? 0;
  }

  getKYCLevel(accountId: string): number {
    return this.accounts.get(accountId)?.kycLevel ?? 0;
  }

  isKYCVerified(accountId: string): boolean {
    return this.accounts.get(accountId)?.kycVerified ?? false;
  }

  registerAccount(accountId: string): void {
    if (this.accounts.has(accountId)) {
      throw new Error('Account already exists');
    }

    this.accounts.set(accountId, {
      exists: true,
      status: 1,
      kycVerified: false,
      kycLevel: 0,
      accreditationStatus: 0,
    });
    this.balances.set(accountId, 0n);
    this.ledger.totalAccounts += 1n;
  }

  deposit(accountId: string, amount: bigint): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (account.status !== 1) throw new Error('Account not active');

    this.balances.set(accountId, (this.balances.get(accountId) ?? 0n) + amount);
    this.ledger.totalAssetsUnderCustody += amount;
  }

  verifyKYC(accountId: string, level: number): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (level < 1 || level > 3) throw new Error('Invalid KYC level');

    account.kycVerified = true;
    account.kycLevel = level;
  }

  verifyAccreditation(accountId: string, status: number): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (!account.kycVerified) throw new Error('KYC required first');

    account.accreditationStatus = status;
  }

  proveBalanceAboveThreshold(
    accountId: string,
    threshold: bigint,
    actualBalance: bigint
  ): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (actualBalance < threshold) throw new Error('Balance below threshold');

    this.ledger.proofVerificationLog += 1n;
  }

  proveBalanceInRange(
    accountId: string,
    min: bigint,
    max: bigint,
    actualBalance: bigint
  ): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (actualBalance < min) throw new Error('Balance below minimum');
    if (actualBalance > max) throw new Error('Balance above maximum');

    this.ledger.proofVerificationLog += 1n;
  }

  proveAccountTier(
    accountId: string,
    actualBalance: bigint,
    claimedTier: number
  ): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');

    if (claimedTier === 1 && actualBalance < this.ledger.tierOneThreshold) {
      throw new Error('Insufficient balance for Tier 1');
    }
    if (claimedTier === 2 && actualBalance < this.ledger.tierTwoThreshold) {
      throw new Error('Insufficient balance for Tier 2');
    }
    if (claimedTier === 3 && actualBalance < this.ledger.tierThreeThreshold) {
      throw new Error('Insufficient balance for Tier 3');
    }

    this.ledger.proofVerificationLog += 1n;
  }

  proveKYCLevel(accountId: string, minimumLevel: number): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (!account.kycVerified) throw new Error('KYC not completed');
    if (account.kycLevel < minimumLevel) throw new Error('KYC level insufficient');

    this.ledger.proofVerificationLog += 1n;
  }

  proveInvestmentEligibility(
    accountId: string,
    minKYC: number,
    minBalance: bigint,
    actualBalance: bigint
  ): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (!account.kycVerified) throw new Error('KYC required');
    if (account.kycLevel < minKYC) throw new Error('Insufficient KYC level');
    if (actualBalance < minBalance) throw new Error('Insufficient balance');

    this.ledger.proofVerificationLog += 1n;
  }

  freezeAccount(accountId: string): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    account.status = 2;
  }

  unfreezeAccount(accountId: string): void {
    const account = this.accounts.get(accountId);
    if (!account) throw new Error('Account does not exist');
    if (account.status !== 2) throw new Error('Account not frozen');
    account.status = 1;
  }
}
Enter fullscreen mode Exit fullscreen mode

Running Tests

# Navigate to contract directory
cd custodian-contract

# Run tests
npm run test

# Run with coverage
npm run test -- --coverage
Enter fullscreen mode Exit fullscreen mode

Deployment Guide

Local Development

# Terminal 1: Start local Midnight infrastructure
npm run setup-standalone

# Terminal 2: Start frontend
npm run dev:frontend
Enter fullscreen mode Exit fullscreen mode

Preview Network Deployment

  1. Configure Environment
# custodian-cli/.env
MY_PREVIEW_MNEMONIC="your twelve word mnemonic phrase"
ADMIN_ADDRESS="your-admin-address"
Enter fullscreen mode Exit fullscreen mode
  1. Deploy Contract
cd custodian-cli
npm run deploy
Enter fullscreen mode Exit fullscreen mode
  1. Initialize Contract
// Initialize with tier thresholds
await contract.initialize(
  adminAddress,
  10000n,    // Tier 1: $10,000
  100000n,   // Tier 2: $100,000
  1000000n   // Tier 3: $1,000,000
);
Enter fullscreen mode Exit fullscreen mode

Deployment Checklist

  • [ ] Contract compiled successfully
  • [ ] All tests passing
  • [ ] Environment variables configured
  • [ ] Admin wallet funded with tSTAR
  • [ ] Tier thresholds defined
  • [ ] Contract deployed
  • [ ] Contract initialized
  • [ ] Frontend configured with contract address

Advanced Patterns

Pattern 1: Multi-Asset Custody

// Support multiple asset types
export ledger assetTypes: Map<Bytes<32>, Boolean>;
export ledger assetTotals: Map<Bytes<32>, Counter>;

export circuit depositAsset(
  accountId: Bytes<32>,
  assetId: Bytes<32>,
  amount: Uint<64>
): [] {
  assert(accountExists[accountId], "Account does not exist");
  assert(assetTypes[assetId], "Asset type not supported");

  assetTotals[assetId].increment(amount);
}

// Prove holdings in specific asset
export circuit proveAssetHolding(
  accountId: Bytes<32>,
  assetId: Bytes<32>,
  actualHolding: Uint<64>,  // Private witness
  minHolding: Uint<64>
): [] {
  assert(actualHolding >= minHolding, "Insufficient holding");
  proofVerificationLog.increment(1);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Time-Bound Proofs

export ledger proofValidityPeriod: Uint<64>;  // In seconds

export circuit proveBalanceWithExpiry(
  accountId: Bytes<32>,
  threshold: Uint<64>,
  actualBalance: Uint<64>,
  currentTime: Uint<64>,
  expiryTime: Uint<64>
): [] {
  assert(expiryTime > currentTime, "Proof already expired");
  assert(expiryTime <= currentTime + proofValidityPeriod, "Expiry too far");
  assert(actualBalance >= threshold, "Balance below threshold");

  proofVerificationLog.increment(1);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Delegated Proofs

// Allow authorized parties to generate proofs
export ledger authorizedDelegates: Map<Bytes<32>, Map<Bytes<32>, Boolean>>;

export circuit authorizeDelegateForProofs(
  accountId: Bytes<32>,
  delegateId: Bytes<32>
): [] {
  assert(accountExists[accountId], "Account does not exist");
  // Caller must be account owner (verified via signature)
  authorizedDelegates[accountId][delegateId] = true;
}

export circuit delegatedBalanceProof(
  accountId: Bytes<32>,
  delegateId: Bytes<32>,
  threshold: Uint<64>,
  actualBalance: Uint<64>
): [] {
  assert(authorizedDelegates[accountId][delegateId], "Not authorized");
  assert(actualBalance >= threshold, "Balance below threshold");

  proofVerificationLog.increment(1);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Compliance Reporting

// Generate aggregate compliance reports without individual disclosure
export ledger complianceReportCount: Counter;

export circuit generateComplianceReport(
  reportType: Uint<8>,
  aggregateData: Uint<64>,  // Private aggregate
  threshold: Uint<64>
): [] {
  // Prove aggregate metrics meet compliance requirements
  // without revealing individual account data
  assert(aggregateData >= threshold, "Compliance threshold not met");

  complianceReportCount.increment(1);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Proof Revocation

export ledger revokedProofs: Map<Bytes<32>, Boolean>;

export circuit revokeProof(
  proofId: Bytes<32>,
  adminSig: Bytes<32>
): [] {
  // Admin can revoke proofs if account status changes
  revokedProofs[proofId] = true;
}

// Verifier checks revocation status
pure function isProofValid(proofId: Bytes<32>): Boolean {
  return !revokedProofs[proofId];
}
Enter fullscreen mode Exit fullscreen mode

Compliance & Regulatory Considerations

KYC/AML Integration

Selective disclosure enables compliance without data exposure:

Requirement Traditional Approach Selective Disclosure
Identity Verification Share ID documents Prove "ID verified by provider X"
Address Verification Share utility bills Prove "Residency verified"
Source of Funds Bank statements Prove "Funds from verified source"
Sanctions Check Full name/DOB Prove "Cleared sanctions check"

Regulatory Benefits

  1. GDPR Compliance: Minimize data collection/exposure
  2. Data Breach Mitigation: Less sensitive data to protect
  3. Right to be Forgotten: Easier to delete (less data stored)
  4. Audit Trails: Cryptographic proof of compliance

Auditor Access

Auditors can verify:

  • Total assets under custody (aggregate)
  • Number of KYC-verified accounts
  • Compliance proof counts
  • System health metrics

Without accessing:

  • Individual balances
  • Personal information
  • Transaction details

Next Steps & Resources

Learning Path

  1. Beginner: Implement basic account management
  2. Intermediate: Add selective disclosure circuits
  3. Advanced: Multi-asset custody with compliance reporting

Use Case Extensions

  • Decentralized Exchange: Prove sufficient balance for trades
  • Lending Platform: Prove collateral without revealing portfolio
  • Insurance: Prove coverage without revealing policy details
  • Healthcare: Prove insurance status without medical records

Official Resources


Quick Reference

Compact Disclosure Patterns

// Threshold proof
export circuit proveThreshold(
  actual: Uint<64>,    // Private witness
  threshold: Uint<64>  // Public parameter
): [] {
  assert(actual >= threshold, "Below threshold");
}

// Range proof
export circuit proveRange(
  actual: Uint<64>,
  min: Uint<64>,
  max: Uint<64>
): [] {
  assert(actual >= min, "Below minimum");
  assert(actual <= max, "Above maximum");
}

// Set membership proof
export circuit proveMembership(
  value: Uint<8>,
  validSet: Vector<Uint<8>, 10>
): [] {
  // Prove value is in validSet
}
Enter fullscreen mode Exit fullscreen mode

Disclosure Types Summary

Type Reveals Hides
Balance Threshold "Balance > X" Exact balance
Balance Range "X < Balance < Y" Exact balance
Account Tier Tier level Balance that determines tier
KYC Level Verification level Documents, personal data
Accreditation Status Financial details
Age Verification "Age > X" Birth date

Conclusion

This tutorial demonstrated how to build a privacy-preserving custodian application with selective disclosure on Midnight. Key takeaways:

  1. Zero-Knowledge Proofs enable proving claims without revealing underlying data
  2. Selective Disclosure gives users control over what information they share
  3. Compliance is achievable while maximizing privacy
  4. Auditable systems don't require full transparency

The custodian pattern applies to:

  • Financial services
  • Healthcare records
  • Identity management
  • Credential verification
  • Any scenario requiring verified claims with privacy

Build trust through cryptographic proof, not data exposure.


Built with the Midnight Starter Template

Webisoft Development Labs

Top comments (0)