Skip to main content

Components

This guide walks you through the key components that handle encryption, minting, and balance display.

EncryptedInput Component

This component handles:
  1. Taking user input
  2. Encrypting the value with Inco SDK
  3. Creating mint and token accounts if needed
  4. Minting confidential tokens

Full Implementation

"use client";

import { useState, useEffect, useRef } from "react";
import {
  useWallet,
  useConnection,
  useAnchorWallet,
} from "@solana/wallet-adapter-react";
import { encryptValue } from "@inco/solana-sdk/encryption";
import { hexToBuffer } from "@inco/solana-sdk/utils";
import {
  Keypair,
  PublicKey,
  SystemProgram,
  Transaction,
} from "@solana/web3.js";
import {
  fetchUserMint,
  fetchUserTokenAccount,
  getAllowancePda,
  extractHandle,
  getProgram,
} from "@/utils/constants";

export default function EncryptedInput() {
  const { publicKey, connected, sendTransaction } = useWallet();
  const { connection } = useConnection();
  const wallet = useAnchorWallet();

  // State
  const [mint, setMint] = useState<string | null>(null);
  const [account, setAccount] = useState<string | null>(null);
  const [value, setValue] = useState("");
  const [encrypted, setEncrypted] = useState("");
  const [txHash, setTxHash] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Fetch existing accounts when wallet connects
  useEffect(() => {
    if (!publicKey) {
      setMint(null);
      setAccount(null);
      return;
    }

    (async () => {
      const m = await fetchUserMint(connection, publicKey);
      if (m) {
        setMint(m.pubkey.toBase58());
        const a = await fetchUserTokenAccount(connection, publicKey, m.pubkey);
        setAccount(a?.pubkey.toBase58() ?? null);
      }
    })();
  }, [publicKey, connection]);

  // Encrypt the input value
  const handleEncrypt = async () => {
    if (!value) return;
    setLoading(true);
    try {
      // Convert to smallest unit (6 decimals)
      const amount = BigInt(Math.floor(parseFloat(value) * 1e6));
      const encryptedHex = await encryptValue(amount);
      setEncrypted(encryptedHex);
    } catch (e: any) {
      setError(e.message || "Encryption failed");
    }
    setLoading(false);
  };

  // Mint tokens with the encrypted amount
  const handleMint = async () => {
    if (!publicKey || !wallet || !encrypted) {
      setError("Missing required data");
      return;
    }
    
    setLoading(true);
    setError(null);

    try {
      const program = getProgram(connection, wallet);
      const ciphertext = hexToBuffer(encrypted);
      let m = mint;
      let a = account;

      // Create mint and account if they don't exist
      if (!m || !a) {
        const signers: Keypair[] = [];
        const tx = new Transaction();

        if (!m) {
          const mintKp = Keypair.generate();
          m = mintKp.publicKey.toBase58();
          signers.push(mintKp);
          
          tx.add(
            await program.methods
              .initializeMint(6, publicKey, publicKey)
              .accounts({
                mint: mintKp.publicKey,
                payer: publicKey,
                systemProgram: SystemProgram.programId,
              })
              .instruction()
          );
        }

        if (!a) {
          const accountKp = Keypair.generate();
          a = accountKp.publicKey.toBase58();
          signers.push(accountKp);
          
          tx.add(
            await program.methods
              .initializeAccount()
              .accounts({
                account: accountKp.publicKey,
                mint: m,
                owner: publicKey,
                payer: publicKey,
                systemProgram: SystemProgram.programId,
              })
              .instruction()
          );
        }

        // Send initialization transaction
        tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
        tx.feePayer = publicKey;
        tx.partialSign(...signers);
        
        await connection.confirmTransaction(
          await sendTransaction(tx, connection),
          "confirmed"
        );
        
        setMint(m);
        setAccount(a);
        
        // Wait for account to be available
        await new Promise((r) => setTimeout(r, 1000));
      }

      // Now mint the tokens
      const mintPk = new PublicKey(m);
      const accPk = new PublicKey(a);
      
      const accounts = {
        mint: mintPk,
        account: accPk,
        mintAuthority: publicKey,
        systemProgram: SystemProgram.programId,
      };

      // Simulate to get the handle
      const simTx = await program.methods
        .mintTo(ciphertext, 0)
        .accounts(accounts)
        .transaction();
      simTx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
      simTx.feePayer = publicKey;

      const sim = await connection.simulateTransaction(simTx, undefined, [accPk]);
      
      if (sim.value.err) {
        throw new Error(`Simulation failed: ${JSON.stringify(sim.value.err)}`);
      }

      // Extract handle from simulation
      const data = sim.value.accounts?.[0]?.data;
      if (!data) throw new Error("No simulation data");
      
      const handle = extractHandle(Buffer.from(data[0], "base64"));
      if (!handle) throw new Error("No handle found");

      // Derive allowance PDA
      const [allowancePda] = getAllowancePda(handle, publicKey);

      // Submit the actual transaction with allowance PDA
      const sig = await program.methods
        .mintTo(ciphertext, 0)
        .accounts(accounts)
        .remainingAccounts([
          { pubkey: allowancePda, isSigner: false, isWritable: true },
          { pubkey: publicKey, isSigner: false, isWritable: false },
        ])
        .rpc();

      setTxHash(sig);
      setValue("");
      setEncrypted("");
      
      // Notify other components
      window.dispatchEvent(new CustomEvent("token-minted"));
      
    } catch (e: any) {
      console.error(e);
      setError(e.message || "Minting failed");
    }
    
    setLoading(false);
  };

  return (
    <div className="mt-8 space-y-4">
      {/* Input Field */}
      <div>
        <label className="block text-sm font-medium mb-2">
          Amount to Mint
        </label>
        <div className="flex space-x-2">
          <input
            type="number"
            placeholder="Enter amount..."
            value={value}
            onChange={(e) => {
              setValue(e.target.value);
              setTxHash(null);
              setEncrypted("");
              setError(null);
            }}
            className="flex-1 p-3 border rounded-full"
          />
          <button
            onClick={handleEncrypt}
            disabled={loading || !value || !connected}
            className="px-6 py-3 bg-blue-600 text-white rounded-full 
                       hover:bg-blue-700 disabled:bg-gray-300"
          >
            {loading ? "..." : "Encrypt"}
          </button>
        </div>
      </div>

      {/* Encrypted Value Display */}
      {encrypted && (
        <>
          <div className="bg-gray-100 p-3 rounded-full flex items-center">
            <span className="text-sm font-mono truncate flex-1">
              {encrypted.slice(0, 20)}...
            </span>
            <button
              onClick={() => navigator.clipboard.writeText(encrypted)}
              className="ml-2 bg-blue-600 text-white px-3 py-1 rounded-full text-xs"
            >
              Copy
            </button>
          </div>
          
          <button
            onClick={handleMint}
            disabled={loading}
            className="w-full bg-blue-600 text-white py-2 rounded-full 
                       hover:bg-blue-700 disabled:bg-gray-300"
          >
            {loading ? "Processing..." : "Mint Tokens"}
          </button>
        </>
      )}

      {/* Error Display */}
      {error && (
        <p className="text-sm text-red-500 bg-red-50 p-3 rounded-xl">
          {error}
        </p>
      )}
      
      {/* Success Display */}
      {txHash && (
        <div className="bg-green-50 p-3 rounded-xl">
          <p className="text-sm text-green-800">
            ✅ Success!{" "}
            <a
              href={`https://explorer.solana.com/tx/${txHash}?cluster=devnet`}
              target="_blank"
              className="underline"
            >
              View transaction
            </a>
          </p>
        </div>
      )}
    </div>
  );
}

Key Steps Explained

1. Encrypt the Value

import { encryptValue } from "@inco/solana-sdk/encryption";

const amount = BigInt(Math.floor(parseFloat(value) * 1e6));
const encryptedHex = await encryptValue(amount);

2. Create Accounts if Needed

First-time users need a mint and token account:
// Create mint
const mintKp = Keypair.generate();
tx.add(
  await program.methods
    .initializeMint(6, publicKey, publicKey)
    .accounts({
      mint: mintKp.publicKey,
      payer: publicKey,
      systemProgram: SystemProgram.programId,
    })
    .instruction()
);

// Create token account
const accountKp = Keypair.generate();
tx.add(
  await program.methods
    .initializeAccount()
    .accounts({
      account: accountKp.publicKey,
      mint: mintKp.publicKey,
      owner: publicKey,
      payer: publicKey,
      systemProgram: SystemProgram.programId,
    })
    .instruction()
);

3. Simulate to Get Handle

Before minting, simulate to extract the handle:
const sim = await connection.simulateTransaction(simTx, undefined, [accPk]);
const data = sim.value.accounts?.[0]?.data;
const handle = extractHandle(Buffer.from(data[0], "base64"));

4. Derive Allowance PDA and Mint

const [allowancePda] = getAllowancePda(handle, publicKey);

const sig = await program.methods
  .mintTo(ciphertext, 0)
  .accounts(accounts)
  .remainingAccounts([
    { pubkey: allowancePda, isSigner: false, isWritable: true },
    { pubkey: publicKey, isSigner: false, isWritable: false },
  ])
  .rpc();

Balance Component

This component displays and decrypts the user’s confidential token balance.

Full Implementation

"use client";

import { useState, useEffect } from "react";
import { useWallet, useConnection } from "@solana/wallet-adapter-react";
import { decrypt } from "@inco/solana-sdk/attested-decrypt";
import {
  fetchUserMint,
  fetchUserTokenAccount,
  extractHandle,
} from "@/utils/constants";

export default function Balance() {
  const { publicKey, connected, signMessage } = useWallet();
  const { connection } = useConnection();
  
  const [balance, setBalance] = useState<string>();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Read and decrypt balance
  const handleReadBalance = async () => {
    if (!connected || !publicKey || !signMessage) return;
    
    setLoading(true);
    setError(null);

    try {
      // Find user's mint
      const mint = await fetchUserMint(connection, publicKey);
      if (!mint) {
        setBalance("No mint");
        return;
      }

      // Find user's token account
      const acc = await fetchUserTokenAccount(
        connection,
        publicKey,
        mint.pubkey
      );
      if (!acc) {
        setBalance("No account");
        return;
      }

      // Extract the encrypted handle
      const handle = extractHandle(acc.data);
      
      // Handle zero balance
      if (handle === BigInt(0)) {
        setBalance("0");
        return;
      }

      // Decrypt the balance
      const result = await decrypt([handle.toString()], {
        address: publicKey,
        signMessage,
      });

      // Convert from smallest unit (6 decimals)
      const rawBalance = BigInt(result.plaintexts?.[0] ?? "0");
      setBalance((Number(rawBalance) / 1e6).toString());
      
    } catch (e) {
      console.error(e);
      setError(e instanceof Error ? e.message : "Failed to read balance");
    } finally {
      setLoading(false);
    }
  };

  // Reset on wallet change
  useEffect(() => {
    setBalance(undefined);
    setError(null);
  }, [publicKey]);

  // Listen for mint events
  useEffect(() => {
    const onMint = () => setBalance(undefined);
    window.addEventListener("token-minted", onMint);
    return () => window.removeEventListener("token-minted", onMint);
  }, []);

  return (
    <div className="mt-8 space-y-4">
      <div className="flex items-center justify-between">
        <div>
          <span className="text-sm font-medium">Balance:</span>
          <span className="ml-2 font-mono">
            {balance ?? "****"}
          </span>
        </div>
        
        <button
          onClick={handleReadBalance}
          disabled={loading || !connected}
          className="bg-gray-600 text-white py-2 px-4 rounded-full 
                     hover:bg-gray-700 disabled:bg-gray-300"
        >
          {loading ? "Loading..." : "Reveal"}
        </button>
      </div>
      
      {error && (
        <p className="text-sm text-red-500">{error}</p>
      )}
    </div>
  );
}

Key Steps Explained

1. Fetch Account Data

const mint = await fetchUserMint(connection, publicKey);
const acc = await fetchUserTokenAccount(connection, publicKey, mint.pubkey);

2. Extract Handle

const handle = extractHandle(acc.data);
The offset in extractHandle is specific to the Inco Token program. Adjust based on your program’s account layout.

3. Decrypt with Wallet Signature

import { decrypt } from "@inco/solana-sdk/attested-decrypt";

const result = await decrypt([handle.toString()], {
  address: publicKey,
  signMessage,  // From useWallet hook
});

const balance = result.plaintexts[0];
The signMessage function is required for attested reveal. The wallet will prompt the user to sign a message that proves ownership of the address.

Component Communication

Components communicate via custom events:
// In EncryptedInput - emit after minting
window.dispatchEvent(new CustomEvent("token-minted"));

// In Balance - listen for mint events
useEffect(() => {
  const onMint = () => setBalance(undefined);
  window.addEventListener("token-minted", onMint);
  return () => window.removeEventListener("token-minted", onMint);
}, []);
This ensures the balance display updates after minting new tokens.

Summary

ComponentPurposeSDK Functions Used
EncryptedInputEncrypt values and mint tokensencryptValue, hexToBuffer
BalanceDisplay and decrypt balancedecrypt
HeaderWallet connection UIWallet adapter hooks
You now have a complete understanding of how to build a confidential token dApp with Next.js!