# Frontend architecture · Build with zero-knowledge proofs on Chromia · Learn Chromia

> This frontend is a Next.js application that gives users a full interface for running zero-knowledge proof (ZKP) operations on Chromia. The sections below…

Source: https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-explore/

Build with zero-knowledge proofs on ChromiaLesson 18 of 19

1. [01Introduction to the course](https://chromia.com/learn/zero-knowledge-proof/introduction/)
2. [02Zero-knowledge proof](https://chromia.com/learn/zero-knowledge-proof/zero-knowledge-proof/)
3. [03Project setup](https://chromia.com/learn/zero-knowledge-proof/setup/)
4. [04Architecture overview](https://chromia.com/learn/zero-knowledge-proof/architecture-overview/)

Module 1: Writing Circom circuits

1. [05Overview](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/)
2. [06Circom circuits: introduction](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-introduction/)
3. [07Circom files overview](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-project/)
4. [08Circom circuits: compile](https://chromia.com/learn/zero-knowledge-proof/circom-circuits/circom-circuits-compile/)

Module 2 – Dapp

1. [09Overview](https://chromia.com/learn/zero-knowledge-proof/dapp/)
2. [10Dapp: setup and run](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-setup-run/)
3. [11Dapp overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-overview/)
4. [12Dapp entities](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-entities/)
5. [13PLONK verification](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-verification/)
6. [14Dapp operations overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-operations/)
7. [15Dapp queries overview](https://chromia.com/learn/zero-knowledge-proof/dapp/dapp-queries/)

Module 3 – Frontend

1. [16Overview](https://chromia.com/learn/zero-knowledge-proof/frontend/)
2. [17Frontend: setup and run](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-setup-run/)
3. [18Frontend architecture](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-explore/)
4. [19Frontend: test](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-test/)

[Learn](https://chromia.com/learn/)[Build with zero-knowledge proofs on Chromia](https://chromia.com/learn/zero-knowledge-proof/)

18 / 19Module 3 – FrontendAdvanced

# Frontend architecture

This frontend is a Next.js application that gives users a full interface for running zero-knowledge proof (ZKP) operations on Chromia. The sections below walk through how it is built.

## Key dependencies

ZKP operations in the frontend depend on the following libraries:

client/package.json

```
{
  "dependencies": {
    "@chromia/ft4": "^2.0.0",          // FT4 token standard
    "circomlibjs": "^0.1.7",           // Circom library for ZK circuits
    "crypto-js": "^4.1.1",             // Cryptographic operations
    "ethers": "^5.7.2",                // Ethereum wallet integration
    "postchain-client": "^2.0.0",      // Chromia blockchain client
    "snarkjs": "^0.7.0"                // zk-SNARK proof generation
  }
}
```

## Architecture overview

The code splits into these main parts:

* **SecureNoteManager**: the core ZKP logic that manages private notes
* **ZKP utilities**: create and verify proofs
* **Crypto utilities**: hashing and other cryptographic operations
* **FT4Client**: the layer that talks to the blockchain
* **React components**: the UI through which users run ZKP operations

## SecureNoteManager - core ZKP engine

At the center of the ZKP system sits the `SecureNoteManager` class. It holds the cryptographic keys and manages the encrypted notes:

client/src/services/secureNoteManager.js

```
class SecureNoteManager {
    constructor() {
        this.poseidon = null;           // Poseidon hash function
        this.masterSeed = null;         // Master seed for key derivation
        this.spendingKey = null;        // ZK spending key
        this.viewingKey = null;         // ZK viewing key
        this.encryptionPrivateKey = null; // Asymmetric encryption key
        this.encryptionPublicKey = null;  // Asymmetric public key
        this.notes = new Map();         // commitment -> note data
        this.nullifiers = new Set();    // Used nullifiers
        this.isUnlocked = false;        // Security state
    }

    // Generate secure deterministic master seed from EVM signature + password
    async generateSecureMasterSeed(password, accountId, evmWallet) {
        const message = `ZKP_WALLET_MASTER_SEED_${password}_${accountId}`;
        
        // Get signature from EVM wallet (MetaMask)
        const signature = await evmWallet.signMessage(message);
        
        // Derive deterministic master seed from signature
        const signatureSeed = ethers.utils.keccak256(signature);
        const masterSeed = ethers.utils.keccak256(
            ethers.utils.concat([
                ethers.utils.arrayify(signatureSeed),
                ethers.utils.toUtf8Bytes(password),
                ethers.utils.toUtf8Bytes(accountId)
            ])
        );
        
        return ethers.utils.arrayify(masterSeed);
    }

    // Derive ZK keys for zero-knowledge proofs
    async deriveZKKeys() {
        const masterKey = ethers.utils.keccak256(this.masterSeed);

        // Derive spending key (for generating nullifiers and proving ownership)
        this.spendingKey = this.poseidon.F.toString(
            this.poseidon([BigInt(masterKey)])
        );

        // Derive viewing key (for ZK proofs)
        this.viewingKey = this.poseidon.F.toString(
            this.poseidon([BigInt(this.spendingKey), BigInt(1)])
        );
    }
}
```

## ZKP utilities - proof generation

When you make a private transfer, these utilities produce the required zero-knowledge proofs:

client/src/services/ft4Client.js

```
// Generate a proof for a private transfer
export const generateProof = async ({
  privateSpendKey,
  amount_input,
  privateAddress_input,
  blindingFactor_input,
  transfer_amount,
  blindingFactor_output_sender,
  blindingFactor_output_recipient,
  recipient_privateAddress
}) => {
  // Initialize crypto if needed
  await initCrypto();
  
  // Calculate input commitment (for verification)
  const commitment_input = hashValues([
    amount_input,
    privateAddress_input,
    blindingFactor_input
  ]);
  
  // Calculate sender's change amount
  const sender_change = BigInt(amount_input) - BigInt(transfer_amount);
  
  // Calculate output commitments
  const commitment_output_sender = hashValues([
    sender_change,
    privateAddress_input,
    blindingFactor_output_sender
  ]);
  
  const commitment_output_recipient = hashValues([
    transfer_amount,
    recipient_privateAddress,
    blindingFactor_output_recipient
  ]);
  
  // Calculate nullifier (prevents double-spending)
  const nullifier = hashValues([privateSpendKey, commitment_input]);
  
  // Prepare inputs for the circuit
  const inputs = {
    // Private inputs (hidden from verifier)
    privateSpendKey: privateSpendKey.toString(),
    amount_input: amount_input.toString(),
    privateAddress_input: privateAddress_input.toString(),
    blindingFactor_input: blindingFactor_input.toString(),
    transfer_amount: transfer_amount.toString(),
    blindingFactor_output_sender: blindingFactor_output_sender.toString(),
    blindingFactor_output_recipient: blindingFactor_output_recipient.toString(),
    
    // Public inputs (visible to verifier)
    commitment_input: commitment_input.toString(),
    commitment_output_sender: commitment_output_sender.toString(),
    commitment_output_recipient: commitment_output_recipient.toString(),
    recipient_privateAddress: recipient_privateAddress.toString(),
    nullifier: nullifier.toString()
  };
  
  try {
    // Generate the ZK proof using snarkjs
    const { proof, publicSignals } = await snarkjs.plonk.fullProve(
      inputs,
      '/private_transfer.wasm',  // Circuit WASM file
      '/circuit.zkey'           // Proving key
    );
    
    // Format the proof for Postchain
    const proofData = Object.values(proof).flat().map(value => value.toString());
    
    return {
      proof: proofData,
      publicSignals: publicSignals.map(signal => signal.toString()),
      commitment_input,
      commitment_output_sender, 
      commitment_output_recipient,
      recipient_privateAddress,
      nullifier,
      sender_change
    };
  } catch (error) {
    console.error('Error generating proof:', error);
    throw error;
  }
};
```

## Crypto utilities - cryptographic operations

These utilities supply the basic cryptographic functions the rest of the app depends on:

client/src/utils/crypto.js

```
import CryptoJS from 'crypto-js';

// Initialize circomlib for Poseidon hashing
let poseidon;

export const initCrypto = async () => {
  if (!poseidon) {
    const circomlibjs = await import('circomlibjs');
    poseidon = await circomlibjs.buildPoseidon();
  }
  return { poseidon };
};

// Generate a random blinding factor for commitments
export const generateBlindingFactor = () => {
  const buf = new Uint8Array(32);
  window.crypto.getRandomValues(buf);
  return BigInt('0x' + Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join(''));
};

// Derive private address from private spend key
export const derivePrivateAddress = async (privateSpendKey) => {
  await initCrypto();
  return hashValues([privateSpendKey]);
};

// Hash values using Poseidon (simplified version)
export function hashValues(values) {
  const combined = values.map(v => v.toString()).join(',');
  return BigInt('0x' + CryptoJS.SHA256(combined).toString().slice(0, 16));
}

// Encrypt note for recipient
export function encryptNote(note, privateAddress) {
  // Convert private address to encryption key
  const keyHex = BigInt(privateAddress).toString(16).padStart(64, '0');
  const key = keyHex.slice(0, 64);
  
  // Encrypt using AES
  const noteJson = JSON.stringify(note);
  const encrypted = CryptoJS.AES.encrypt(noteJson, key).toString();
  
  return Buffer.from(encrypted, 'utf8');
}

// Decrypt note if intended for this address
export function decryptNote(encryptedNote, privateAddress) {
  try {
    const keyHex = BigInt(privateAddress).toString(16).padStart(64, '0');
    const key = keyHex.slice(0, 64);
    
    const encryptedString = Buffer.isBuffer(encryptedNote) 
      ? encryptedNote.toString('utf8') 
      : encryptedNote;
    
    const decrypted = CryptoJS.AES.decrypt(encryptedString, key);
    const decryptedString = decrypted.toString(CryptoJS.enc.Utf8);
    
    return decryptedString ? JSON.parse(decryptedString) : null;
  } catch (error) {
    return null; // Note not intended for this address
  }
}
```

## Frontend user interface

The UI for ZKP operations lives in the main React component:

client/src/pages/index.js

```
export default function Home() {
  // State management for ZKP operations
  const [ft4Client, setFt4Client] = useState(null);
  const [isWalletUnlocked, setIsWalletUnlocked] = useState(false);
  const [publicBalance, setPublicBalance] = useState('0');
  const [privateBalance, setPrivateBalance] = useState('0');
  const [showPasswordModal, setShowPasswordModal] = useState(false);
  
  // Initialize FT4 Client with ZKP support
  useEffect(() => {
    const initializeClient = async () => {
      const client = new FT4Client();
      await client.initialize();
      setFt4Client(client);
      
      // Check for existing sessions
      const existingSession = await client.checkExistingSession();
      if (existingSession) {
        // Restore session and sync private notes
        if (client.isWalletUnlocked()) {
          await client.syncPrivateNotes();
        }
      }
    };
    
    initializeClient();
  }, []);

  // Handle shielding tokens (public -> private)
  const handleShield = async () => {
    const performShield = async () => {
      const loadingToast = toast.loading('Shielding tokens... Generating ZK proof...');
      
      try {
        // Convert to blockchain format
        const shieldAmountFormatted = formatAmount(shieldAmount);
        
        // Generate ZK proof and shield tokens
        await ft4Client.shieldTokens(BigInt(shieldAmountFormatted));
        
        // Refresh balances
        await refreshBalances();
        
        toast.success(`Successfully shielded ${shieldAmount} tokens!`, { id: loadingToast });
      } catch (error) {
        toast.error('Failed to shield tokens', { id: loadingToast });
      }
    };

    // Prompt for password if wallet is locked
    if (!isWalletUnlocked) {
      promptForPassword('shield', performShield);
    } else {
      await performShield();
    }
  };

  // Handle private transfers
  const handleTransfer = async () => {
    const performTransfer = async () => {
      const loadingToast = toast.loading('Processing private transfer... Generating ZK proof...');
      
      try {
        const transferAmountFormatted = formatAmount(transferAmount);
        
        // Generate ZK proof and perform private transfer
        await ft4Client.privateTransfer(
          BigInt(transferAmountFormatted), 
          transferRecipient
        );
        
        await refreshBalances();
        
        toast.success(`Successfully transferred ${transferAmount} tokens privately!`, { id: loadingToast });
      } catch (error) {
        toast.error('Failed to complete private transfer', { id: loadingToast });
      }
    };

    if (!isWalletUnlocked) {
      promptForPassword('transfer', performTransfer);
    } else {
      await performTransfer();
    }
  };

  // Handle unshielding tokens (private -> public)
  const handleUnshield = async () => {
    const performUnshield = async () => {
      const loadingToast = toast.loading('Unshielding tokens... Generating ZK proof...');
      
      try {
        const unshieldAmountFormatted = formatAmount(unshieldAmount);
        
        // Generate ZK proof and unshield tokens
        await ft4Client.unshieldTokens(BigInt(unshieldAmountFormatted));
        
        await refreshBalances();
        
        toast.success(`Successfully unshielded ${unshieldAmount} tokens!`, { id: loadingToast });
      } catch (error) {
        toast.error('Failed to unshield tokens', { id: loadingToast });
      }
    };

    if (!isWalletUnlocked) {
      promptForPassword('unshield', performUnshield);
    } else {
      await performUnshield();
    }
  };
}
```

## Note management and privacy

Private notes are stored and handled under strong encryption:

client/src/services/secureNoteManager.js

```
// Create a new private note
async createNote(amount, recipientPrivateAddress = null) {
    this.requireUnlocked();

    const privateAddress = recipientPrivateAddress || this.getPrivateAddress();
    const blindingFactor = this.generateBlindingFactor();
    
    // Create commitment: hash(amount, privateAddress, blindingFactor)
    const commitment = this.createCommitment(amount, privateAddress, blindingFactor);

    const noteData = {
        amount: amount.toString(),
        privateAddress: privateAddress.toString(),
        blindingFactor,
        commitment,
        isOwned: privateAddress.toString() === this.getPrivateAddress().toString(),
        createdAt: Date.now()
    };

    // Store note locally if we own it
    if (noteData.isOwned) {
        this.notes.set(commitment, noteData);
        await this.saveNotesToStorage();
    }

    return noteData;
}

// Save notes with asymmetric encryption
async saveNotesToStorage() {
    const notesData = {
        notes: Array.from(this.notes.entries()),
        nullifiers: Array.from(this.nullifiers),
        version: '2.0',
        keypairId: this.keypairSeed.slice(0, 8),
        publicEncryptionKey: this.encryptionPublicKey
    };

    const publicKeyBase64 = this.getPublicEncryptionKey();
    const encrypted = await this.encryptDataAsymmetric(
        this.safeStringify(notesData), 
        publicKeyBase64
    );
    
    const encryptedBase64 = btoa(String.fromCharCode(...encrypted));
    localStorage.setItem(this.getStorageKey('encrypted_notes'), encryptedBase64);
}
```

## ZKP operation flow

The frontend covers every step of the ZKP workflow:

### 1. Shield operation (public → private)

* You enter the amount you want to shield
* The frontend creates a ZK proof showing you own the public tokens
* The proof is sent to the blockchain
* Your public balance goes down and a new private note is created

### 2. Private transfer

* You choose a recipient and an amount
* The frontend selects private notes that can cover the transfer
* A ZK proof is created that demonstrates:
  + You own the input notes
  + The amounts add up correctly
  + The nullifiers are valid, which blocks double-spending
* New private notes are created: one for the recipient and one for your change

### 3. Unshield operation (private → public)

* You enter the amount you want to unshield
* The frontend creates a ZK proof showing you own the private notes
* Those private notes are nullified
* Your public balance goes up

## Security features

Several protections are built into the frontend:

1. **Password-Protected Wallet**: the master seed comes from combining an EVM signature with a password
2. **Automatic Lock**: after 30 minutes without activity, the wallet locks itself
3. **Asymmetric Encryption**: notes are encrypted using derived keypairs
4. **Session Management**: sessions are handled securely through MetaMask integration
5. **Note Validation**: nullifier tracking stops notes from being spent twice

Taken together, this design gives you a secure and usable interface for zero-knowledge proof operations on Chromia, and it shows that standard web tooling can put complex cryptography within reach of ordinary users.

Written for this site from Chromia’s public course. Code samples are the course’s own.

[← Previous**Frontend: setup and run**](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-setup-run/)[Next →**Frontend: test**](https://chromia.com/learn/zero-knowledge-proof/frontend/frontend-test/)

On this page

* [Key dependencies](#key-dependencies)
* [Architecture overview](#architecture-overview)
* [SecureNoteManager - core ZKP engine](#securenotemanager---core-zkp-engine)
* [ZKP utilities - proof generation](#zkp-utilities---proof-generation)
* [Crypto utilities - cryptographic operations](#crypto-utilities---cryptographic-operations)
* [Frontend user interface](#frontend-user-interface)
* [Note management and privacy](#note-management-and-privacy)
* [ZKP operation flow](#zkp-operation-flow)
* [Security features](#security-features)
