Complete API documentation for Uniquity smart contracts and frontend hooks.
Privacy-preserving proof of humanity using FHE-encrypted biometric storage.
Version Note: v1 (current) verifies users instantly. v2 will add uniqueness comparison via off-chain FHE computation.
uint64 public constant SIMILARITY_THRESHOLD = 400000; // Used in v2
uint256 public constant MAX_PROFILES = 10000;
uint8 public constant NUM_BUCKETS = 8;// Profile storage
mapping(uint256 => EncryptedProfile) public profiles;
uint256 public profileCount;
mapping(uint8 => uint256[]) public bucketProfiles;
// Verification tracking
mapping(address => bool) public isVerified;
mapping(address => uint256) public userProfileId;
// Pending requests (v2)
mapping(uint256 => VerificationRequest) public pendingRequests;
uint256 public requestCounter;
mapping(address => bool) public hasPendingRequest;Start the verification process with encrypted biometric data.
function proveHumanity(
externalEuint64 inChunk0,
externalEuint64 inChunk1,
externalEuint64 inChunk2,
externalEuint64 inChunk3,
bytes calldata inputProof,
uint8 bucketId
) externalParameters:
| Name | Type | Description |
|---|---|---|
inChunk0-3 |
externalEuint64 |
FHE-encrypted embedding chunks |
inputProof |
bytes |
Proof for encrypted input validation |
bucketId |
uint8 |
Bucket for comparison grouping (0-7) |
Behavior:
| Version | Behavior |
|---|---|
| v1 (Current) | User is verified immediately. Encrypted embedding stored for future comparison. |
| v2 (Planned) | Emits event for off-chain comparison service. User must complete decryption flow. |
Events:
// v1: Emitted immediately on successful verification
event VerificationComplete(address indexed user, bool isUnique, uint256 profileId);
// v1: Profile registered
event ProfileRegistered(uint256 indexed profileId, uint8 bucketId);
// v2: Emitted when embedding submitted (triggers off-chain comparison)
event EmbeddingSubmitted(
uint256 indexed verificationId,
address indexed user,
uint8 bucketId,
bytes32 chunk0Handle,
bytes32 chunk1Handle,
bytes32 chunk2Handle,
bytes32 chunk3Handle
);Errors:
AlreadyVerified()- User already has verificationMaxProfilesReached()- System at capacityInvalidBucketId()- bucketId >= NUM_BUCKETSVerificationPending()- User has pending request (v2)
Example:
const input = fhevmInstance.createEncryptedInput(coreAddress, userAddress);
input.add64(chunk0);
input.add64(chunk1);
input.add64(chunk2);
input.add64(chunk3);
const encrypted = await input.encrypt();
await coreContract.proveHumanity(
encrypted.handles[0],
encrypted.handles[1],
encrypted.handles[2],
encrypted.handles[3],
encrypted.inputProof,
bucketId
);
// v1: User is now verified!
// v2: User must wait for comparison service, then complete decryption flowv2 Feature: This function is part of the v2 uniqueness verification flow.
Enable public decryption of the computed similarity distance (Step 2 of v0.9 pattern).
function requestDecryption() externalRequirements:
- Caller must have pending verification request
- Comparison service must have submitted result
- Decryption not already enabled
Actions:
- Calls
FHE.makePubliclyDecryptable(minDistance)to enable off-chain decryption - Emits event with handle for client to use in Step 3
Events:
event DecryptionEnabled(
uint256 indexed requestId,
address indexed user,
bytes32 distanceHandle
);Errors:
NoPendingRequest()- No pending verificationComparisonNotSubmitted()- Waiting for comparison service (v2)DecryptionAlreadyEnabled()- Already triggered
Flow:
- User calls
requestDecryption()on-chain - Contract marks the encrypted distance as publicly decryptable
- Client receives event with handle to use in off-chain decryption
v2 Feature: This function is part of the v2 uniqueness verification flow.
Submit decrypted value with proof for on-chain verification (Step 4 of v0.9 pattern).
function finalizeVerification(
uint256 requestId,
uint64 clearDistance,
bytes calldata decryptionProof
) externalParameters:
| Name | Type | Description |
|---|---|---|
requestId |
uint256 |
The verification request ID |
clearDistance |
uint64 |
Decrypted distance from off-chain Step 3 |
decryptionProof |
bytes |
KMS signatures proving authentic decryption |
Actions:
- Verifies proof via
FHE.checkSignatures()(reverts if invalid) - If distance > threshold: registers profile, marks user as verified
- If distance ≤ threshold: rejects as duplicate
Events:
event VerificationComplete(address indexed user, bool isUnique, uint256 profileId);Errors:
NoPendingRequest()- No pending verificationDecryptionNotEnabled()- Step 2 not completedAlreadyFinalized()- Request already processed- Reverts if
FHE.checkSignatures()fails (invalid proof)
Cancel a pending verification request.
function cancelVerification() externalRequirements:
- Caller must have pending request
Errors:
NoPendingRequest()- No pending verification
// Check if user is verified
function isVerified(address user) external view returns (bool);
// Check if user has pending verification (v2)
function hasPendingVerification(address user) external view returns (bool);
// Get pending request details (v2)
function getPendingRequest(address user) external view returns (
uint256 requestId,
uint256 timestamp,
bool decryptionEnabled,
bytes32 distanceHandle
);
// Get bucket size
function getBucketSize(uint8 bucketId) external view returns (uint256);
// Alias for isVerified
function hasHumanCredential(address user) external view returns (bool);v2 Feature: This SDK is used in v2 for the uniqueness verification flow.
The v0.9 fhEVM uses a 3-step public decryption pattern. Step 3 happens off-chain using the Relayer SDK.
Decrypt ciphertexts that have been marked as publicly decryptable.
import { createInstance, FhevmInstance, PublicDecryptResults } from "fhevmjs";
const instance: FhevmInstance = await createInstance();
const results: PublicDecryptResults = await instance.publicDecrypt(handles);Parameters:
| Name | Type | Description |
|---|---|---|
handles |
(string | Uint8Array)[] |
Array of ciphertext handles (bytes32) to decrypt |
Returns: PublicDecryptResults
| Property | Type | Description |
|---|---|---|
clearValues |
Record<string, bigint | boolean> |
Map of handle → decrypted value |
abiEncodedClearValues |
0x${string} |
ABI-encoded cleartext (for checkSignatures) |
decryptionProof |
0x${string} |
KMS signatures (for checkSignatures) |
Example: Complete v2 Verification Flow
// Step 1: Submit embedding (proveHumanity)
// ... emits EmbeddingSubmitted event
// Step 2: Wait for comparison service
// ... service computes FHE distance off-chain
// ... service calls submitComparisonResult()
// ... emits ComparisonReady event with distanceHandle
// Step 3: Enable decryption (requestDecryption)
await contract.requestDecryption();
// ... emits DecryptionEnabled event
// Step 4: Off-chain decryption
const instance = await createInstance();
const results = await instance.publicDecrypt([distanceHandle]);
const clearDistance = results.clearValues[distanceHandle];
const proof = results.decryptionProof;
// Step 5: Submit proof on-chain (finalizeVerification)
const tx = await contract.finalizeVerification(requestId, clearDistance, proof);
await tx.wait();Important Notes:
- Handles must be marked as publicly decryptable via
FHE.makePubliclyDecryptable()first - The proof is cryptographically bound to the order of handles
- Proof for
[handleA, handleB]≠ proof for[handleB, handleA]
Campaign management and encrypted submission system.
IUniquityCore public immutable uniquityCore;
mapping(uint256 => Campaign) public campaigns;
uint256 public campaignCount;
mapping(uint256 => mapping(uint256 => Submission)) internal submissions;
mapping(uint256 => mapping(address => bool)) public hasSubmitted;
mapping(uint256 => mapping(address => uint256)) public userSubmissionIndex;
mapping(address => uint256[]) public adminCampaigns;Create a new campaign.
function createCampaign(
bytes32 metadataCid,
string calldata metadataCidString,
uint256 deadline,
uint256 editDeadline,
bool requiresVerification,
bool allowEdits,
uint64 maxSubmissions
) external onlyVerifiedHuman returns (uint256 campaignId)Parameters:
| Name | Type | Description |
|---|---|---|
metadataCid |
bytes32 |
keccak256 hash of metadata CID string |
metadataCidString |
string |
IPFS CID for campaign metadata |
deadline |
uint256 |
Unix timestamp for submission deadline |
editDeadline |
uint256 |
Unix timestamp for edit deadline (0 = same as deadline) |
requiresVerification |
bool |
Whether submitters must be verified |
allowEdits |
bool |
Whether submissions can be edited |
maxSubmissions |
uint64 |
Maximum submissions (0 = unlimited) |
Returns:
campaignId- The ID of the created campaign
Events:
event CampaignCreated(
uint256 indexed campaignId,
address indexed admin,
bytes32 metadataCid,
string metadataCidString,
uint256 deadline,
uint256 editDeadline,
bool requiresVerification,
bool allowEdits,
uint64 maxSubmissions
);Errors:
NotVerifiedHuman()- Caller not verifiedInvalidInput()- Empty CID or hash mismatchDeadlineMustBeFuture()- Deadline in the past
Example:
const metadataCid =
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
const metadataCidBytes32 = ethers.keccak256(ethers.toUtf8Bytes(metadataCid));
const deadline = Math.floor(Date.now() / 1000) + 86400; // 1 day from now
const tx = await submitContract.createCampaign(
metadataCidBytes32,
metadataCid,
deadline,
0, // editDeadline = deadline
true, // requiresVerification
true, // allowEdits
100 // maxSubmissions
);Submit encrypted data to a campaign.
function submit(
uint256 campaignId,
bytes32 ipfsCid,
string calldata ipfsCidString,
externalEuint256 inEncryptedAesKey,
bytes calldata inputProof
) externalParameters:
| Name | Type | Description |
|---|---|---|
campaignId |
uint256 |
Target campaign ID |
ipfsCid |
bytes32 |
keccak256 hash of submission CID string |
ipfsCidString |
string |
IPFS CID of encrypted submission |
inEncryptedAesKey |
externalEuint256 |
FHE-encrypted AES key |
inputProof |
bytes |
Proof for encrypted input |
Events:
event SubmissionReceived(
uint256 indexed campaignId,
uint256 indexed submissionIndex,
address indexed submitter,
bytes32 ipfsCid,
string ipfsCidString
);Errors:
CampaignNotFound()- Invalid campaign IDNotVerifiedHuman()- User not verified (if required)CampaignNotActive()- Campaign is closedCampaignExpired()- Past deadlineAlreadySubmitted()- User already submittedInvalidInput()- Empty CID or hash mismatchMaxSubmissionsReached()- At capacity
Edit an existing submission.
function editSubmission(
uint256 campaignId,
bytes32 newIpfsCid,
string calldata newIpfsCidString,
externalEuint256 inNewEncryptedAesKey,
bytes calldata inputProof
) externalParameters:
| Name | Type | Description |
|---|---|---|
campaignId |
uint256 |
Campaign ID |
newIpfsCid |
bytes32 |
New submission CID hash |
newIpfsCidString |
string |
New IPFS CID |
inNewEncryptedAesKey |
externalEuint256 |
New FHE-encrypted AES key |
inputProof |
bytes |
Proof for encrypted input |
Events:
event SubmissionEdited(
uint256 indexed campaignId,
uint256 indexed submissionIndex,
bytes32 newIpfsCid,
string newIpfsCidString
);Errors:
EditsNotAllowed()- Campaign doesn't allow editsEditDeadlinePassed()- Past edit deadlineSubmissionNotFound()- No submission to editNotSubmitter()- Caller isn't submitter
// Update campaign metadata
function updateMetadata(
uint256 campaignId,
bytes32 newMetadataCid,
string calldata newMetadataCidString
) external onlyCampaignAdmin;
// Update settings
function updateSettings(
uint256 campaignId,
bool allowEdits,
uint64 maxSubmissions
) external onlyCampaignAdmin;
// Extend deadline
function extendDeadline(
uint256 campaignId,
uint256 newDeadline
) external onlyCampaignAdmin;
// Extend edit deadline
function extendEditDeadline(
uint256 campaignId,
uint256 newEditDeadline
) external onlyCampaignAdmin;
// Close campaign
function closeCampaign(uint256 campaignId) external onlyCampaignAdmin;
// Reopen campaign
function reopenCampaign(uint256 campaignId) external onlyCampaignAdmin;
// Toggle edits
function setAllowEdits(uint256 campaignId, bool allowEdits) external onlyCampaignAdmin;// Admin: Get single key handle
function getSubmissionKeyHandle(
uint256 campaignId,
uint256 submissionIndex
) external view onlyCampaignAdmin returns (bytes32);
// Admin: Get multiple key handles
function getSubmissionKeyHandles(
uint256 campaignId,
uint256[] calldata submissionIndices
) external view onlyCampaignAdmin returns (bytes32[] memory);
// User: Get own key handle (for editing)
function getMySubmissionKeyHandle(
uint256 campaignId
) external view returns (bytes32);// Get campaign data
function getCampaign(uint256 campaignId) external view returns (
bytes32 metadataCid,
address admin,
uint256 deadline,
uint256 editDeadline,
uint256 createdAt,
bool active,
bool requiresVerification,
bool allowEdits,
uint64 maxSubmissions,
uint64 submissionCount
);
// Get submission info
function getSubmissionInfo(
uint256 campaignId,
uint256 submissionIndex
) external view returns (
address submitter,
bytes32 ipfsCid,
uint64 timestamp,
uint32 editCount,
bool exists
);
// Get all submission CIDs
function getAllSubmissionCids(uint256 campaignId) external view returns (bytes32[] memory);
// Check if user can submit
function canSubmit(uint256 campaignId, address user) external view returns (
bool canSubmitNow,
string memory reason
);
// Check if user can edit
function canEdit(uint256 campaignId, address user) external view returns (
bool canEditNow,
string memory reason
);
// Get campaigns by admin
function getCampaignsByAdmin(address admin) external view returns (uint256[] memory);
// Check admin status
function isAdmin(uint256 campaignId, address user) external view returns (bool);import { useUniquityCore } from "@/hooks/useUniquityCore";
const {
// State
isLoading,
error,
isVerified,
hasPendingVerification,
pendingVerification, // v2
// Actions
proveHumanity,
requestDecryption, // v2
finalizeVerification, // v2
cancelVerification,
// Queries
checkIsVerified,
checkHasPendingRequest,
getPendingRequest,
refreshStatus,
} = useUniquityCore();const proveHumanity = async (
embedding: FaceEmbedding // Contains chunks and bucketId
): Promise<TransactionResult>
// v1: Returns immediately with success
// v2: Returns with verificationId for trackingconst requestDecryption = async (): Promise<TransactionResult & {
distanceHandle?: `0x${string}`;
}>const finalizeVerification = async (
verificationId: bigint,
clearDistance: bigint,
decryptionProof: `0x${string}`
): Promise<TransactionResult>import { useUniquitySubmit } from "@/hooks/useUniquitySubmit";
const {
isLoading,
error,
// Campaign Management
createCampaign,
updateMetadata,
updateSettings,
extendDeadline,
extendEditDeadline,
closeCampaign,
reopenCampaign,
setAllowEdits,
// Submissions
submit,
editSubmission,
checkCanSubmit,
checkCanEdit,
// Decryption
getSubmissionKeyHandle,
getMySubmissionKeyHandle,
getSubmissionKeyHandles,
decryptSubmission,
batchDecryptSubmissions,
} = useUniquitySubmit();interface CreateCampaignParams {
metadata: CampaignMetadata;
deadline: number;
editDeadline?: number;
requiresVerification?: boolean;
allowEdits?: boolean;
maxSubmissions?: number;
}
const createCampaign = async (
params: CreateCampaignParams
): Promise<TransactionResult & { campaignId?: number }>const submit = async (
campaignId: number,
encryptedDataCid: string,
aesKey: Uint8Array
): Promise<TransactionResult>const decryptSubmission = async (
campaignId: number,
submissionIndex: number
): Promise<DecryptedSubmission | null>import {
useCampaigns,
useCampaign,
useSubmissions,
useMySubmissions,
useMyCampaigns,
useGlobalStats,
} from "@/hooks/useUniquitySubgraph";const {
campaigns,
loading,
error,
page,
setPage,
hasMore,
} = useCampaigns(activeFilter?: boolean);const {
campaign,
loading,
error,
refresh,
} = useCampaign(campaignId: string);query GetCampaigns($first: Int!, $skip: Int!, $active: Boolean) {
campaigns(
first: $first
skip: $skip
where: { active: $active }
orderBy: createdAt
orderDirection: desc
) {
id
admin {
id
}
metadataCidString
deadline
editDeadline
createdAt
active
requiresVerification
allowEdits
maxSubmissions
submissionCount
}
}query GetCampaignWithSubmissions($id: ID!, $first: Int!, $skip: Int!) {
campaign(id: $id) {
id
admin {
id
}
metadataCidString
deadline
editDeadline
active
submissionCount
submissions(first: $first, skip: $skip, orderBy: submittedAt) {
id
submitter {
id
}
submissionIndex
ipfsCidString
submittedAt
editCount
}
}
}query GetMySubmissions($submitter: String!, $first: Int!, $skip: Int!) {
submissions(
where: { submitter_: { id: $submitter } }
first: $first
skip: $skip
orderBy: submittedAt
orderDirection: desc
) {
id
submissionIndex
ipfsCidString
submittedAt
editCount
campaign {
id
metadataCidString
deadline
editDeadline
allowEdits
active
}
}
}interface CampaignMetadata {
name: string;
description: string;
tags?: string[];
fieldConfig: {
fields: FormField[];
};
organizationName?: string;
organizationLogo?: string;
externalUrl?: string;
version: number;
}
interface FormField {
id: string;
type: "text" | "textarea" | "email" | "url" | "number";
label: string;
placeholder?: string;
required: boolean;
}interface TransactionResult {
success: boolean;
txHash?: string;
error?: string;
}interface DecryptedSubmission {
submitter: string;
submissionIndex: number;
data: Record<string, any>;
submittedAt: Date;
editCount: number;
}interface FaceEmbedding {
chunks: [bigint, bigint, bigint, bigint]; // 4 × uint64
bucketId: number; // 0-7
full: Float32Array; // Original 128-D embedding
quantized: Int8Array; // Quantized version
reduced: Float32Array; // Reduced dimensions
}interface PendingVerification {
verificationId: bigint;
timestamp: bigint;
comparisonSubmitted: boolean; // Off-chain service completed
decryptionEnabled: boolean; // Ready for publicDecrypt
distanceHandle: `0x${string}`; // Handle for decryption
}interface DecryptionResult {
clearDistance: bigint;
decryptionProof: `0x${string}`;
}