This guide covers trait discovery, Groth16 proof generation, simulation, and submission to OpaqueReputationVerifier.
Prerequisites
- WASM module (
wasmModuleSpecifier)
- Native announcement rows with V2 PSR attestation metadata
snarkjs (pulled in by @opaquecash/psr-prover)
- Circuit artifacts (default: hosted on opaque.cash
/circuits/...)
Step 1: Discover traits
const chain = "ethereum" as const;
const rows = await client.fetchAnnouncementRows(chain);
const traits = await client.discoverTraitsV2(rows, { chain });
if (traits.length === 0) {
throw new Error("No V2 reputation traits found in inbox");
}
const trait = traits[0];
discoverTraitsV2 validates each 0xB2 marker against the chain’s schema registry and
returns only traits issued by an authorized schema authority or delegate. Fetch rows from
the native chain, not UAB, because relayed payloads cannot carry the full V2 metadata.
Step 2: Reconstruct the stealth signing key
const stealthPrivKeyBytes = client.getStealthSignerPrivateKeyForReputationTrait(trait);
// Or from a scan output:
// client.getStealthSignerPrivateKey(output)
Step 3: Build the action scope
import { buildActionScope, externalNullifierFromScope } from "@opaquecash/opaque";
const scope = buildActionScope({
chainId: 11155111,
module: "my-app",
actionId: "premium-gate",
});
const externalNullifier = externalNullifierFromScope(scope).toString();
Or use the equivalent static helpers on the client
(OpaqueClient.buildReputationActionScope / reputationExternalNullifierFromScope).
Step 4: Generate the proof
const proofData = await client.generateReputationProof({
trait,
stealthPrivKeyBytes,
externalNullifier,
onProgress: (stage, pct) => console.log(stage, pct),
// artifacts: { wasmPath, zkeyPath } // override default hosted paths
});
// proofData: { proof, publicSignals, nullifier, attestationId }
When trait comes from discoverTraitsV2, the SDK uses the trait’s V2 Merkle leaf
preimage fields automatically. You only need to pass issuerPkX, traitDataHash, or
nonce yourself for custom witness construction.
Step 5: Fetch a valid Merkle root
const merkleRoot = await client.fetchLatestValidReputationRoot();
const valid = await client.isReputationRootValid(merkleRoot);
const history = await client.fetchReputationRootHistory();
Step 6: Simulate (optional)
await client.simulateReputationVerification(walletClient, {
proofData,
merkleRoot,
externalNullifier,
});
Step 7: Verify view (read-only)
const ok = await client.verifyReputationProofView({
proofData,
merkleRoot,
externalNullifier,
});
Step 8: Submit on-chain
Consumes the nullifier on success:
const { txHash } = await client.submitReputationVerification("ethereum", {
proofData,
merkleRoot,
externalNullifier,
});
Solana:
await client.submitReputationVerification("solana", {
proofData,
merkleRoot,
externalNullifier,
});
Full flow diagram
Each (trait, externalNullifier) pair can only be verified once. Choose scopes carefully per action you gate.