Skip to main content

Building dApps on Specter

End-to-end guide for building a privacy-enabled dApp using React, viem, and snarkjs.

Project setup

npm create vite@latest my-specter-dapp -- --template react-ts
cd my-specter-dapp
npm install viem snarkjs circomlibjs @rainbow-me/rainbowkit wagmi

Connect wallet

// src/App.tsx
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider, ConnectButton } from '@rainbow-me/rainbowkit';
import { config } from './config';

function App() {
return (
<WagmiProvider config={config}>
<RainbowKitProvider>
<ConnectButton />
{/* Your app components */}
</RainbowKitProvider>
</WagmiProvider>
);
}

Read chain data

import { useReadContract } from 'wagmi';

function VaultStatus() {
const { data: totalCommitted } = useReadContract({
address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
abi: [{ name: 'totalCommitted', type: 'function', inputs: [{ type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' }],
functionName: 'totalCommitted',
args: ['0x0000000000000000000000000000000000000000'],
});

return <div>Total GHOST committed: {formatEther(totalCommitted || 0n)}</div>;
}

Commit tokens

import { useWriteContract } from 'wagmi';
import { parseEther } from 'viem';

function CommitForm() {
const { writeContract } = useWriteContract();

async function handleCommit(commitment: `0x${string}`, amount: string) {
writeContract({
address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
abi: [{ name: 'commitNative', type: 'function', inputs: [{ type: 'bytes32' }, { type: 'bytes32' }], stateMutability: 'payable' }],
functionName: 'commitNative',
args: [commitment, '0x' + '0'.repeat(64) as `0x${string}`],
value: parseEther(amount),
gasPrice: 1_000_000_000n,
});
}

return (/* form UI */);
}

Generate proof and reveal

async function handleReveal(secrets, recipientAddress) {
// 1. Generate proof (in Web Worker for best UX)
const { proof, publicInputs } = await generateProof(secrets, recipientAddress);

// 2. Submit reveal transaction
writeContract({
address: '0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
abi: revealAbi,
functionName: 'reveal',
args: [
'0x0000000000000000000000000000000000000000', // native GHOST
proof,
publicInputs,
secrets.commitment,
'0x', // no quantum proof
'0x' + '0'.repeat(64), // no quantum change
'0x', // no policy params
],
gasPrice: 1_000_000_000n,
});
}

Secret management

warning

The commitment secrets (secret, nullifierSecret, blinding) must be stored securely. If lost, committed tokens are permanently unrecoverable. Consider:

  • Browser storage: localStorage or IndexedDB (cleared if browser data is deleted)
  • Encrypted backup: Let users download an encrypted file containing their secrets
  • Key derivation: Derive secrets deterministically from a seed phrase using a KDF

Architecture recommendations

  1. Never store secrets on a server — keep commitment secrets client-side only
  2. Use Web Workers for proof generation — keeps the UI responsive during the 2–30 second proof generation
  3. Handle root staleness — if a proof fails with InvalidRoot, regenerate against the latest root
  4. Show commitment status — poll the CommitmentTree for root updates so users know when they can reveal
  5. Set gas price explicitly — always use gasPrice: 1_000_000_000n