Components
This guide walks you through the key components that handle encryption, minting, and balance display.EncryptedInput Component
This component handles:- Taking user input
- Encrypting the value with Inco SDK
- Creating mint and token accounts if needed
- Minting confidential tokens
Full Implementation
Copy
"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
Copy
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:Copy
// 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:Copy
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
Copy
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
Copy
"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
Copy
const mint = await fetchUserMint(connection, publicKey);
const acc = await fetchUserTokenAccount(connection, publicKey, mint.pubkey);
2. Extract Handle
Copy
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
Copy
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:Copy
// 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);
}, []);
Summary
| Component | Purpose | SDK Functions Used |
|---|---|---|
EncryptedInput | Encrypt values and mint tokens | encryptValue, hexToBuffer |
Balance | Display and decrypt balance | decrypt |
Header | Wallet connection UI | Wallet adapter hooks |