Skip to content

Latest commit

 

History

History
975 lines (762 loc) · 21.6 KB

File metadata and controls

975 lines (762 loc) · 21.6 KB

📚 API Reference

Complete API documentation for Uniquity smart contracts and frontend hooks.


Table of Contents

  1. Smart Contracts
  2. Off-Chain SDK (v2)
  3. Frontend Hooks
  4. GraphQL Queries
  5. Types & Interfaces

Smart Contracts

UniquityCore

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.

Constants

uint64 public constant SIMILARITY_THRESHOLD = 400000;  // Used in v2
uint256 public constant MAX_PROFILES = 10000;
uint8 public constant NUM_BUCKETS = 8;

State Variables

// 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;

proveHumanity

Start the verification process with encrypted biometric data.

function proveHumanity(
    externalEuint64 inChunk0,
    externalEuint64 inChunk1,
    externalEuint64 inChunk2,
    externalEuint64 inChunk3,
    bytes calldata inputProof,
    uint8 bucketId
) external

Parameters:

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 verification
  • MaxProfilesReached() - System at capacity
  • InvalidBucketId() - bucketId >= NUM_BUCKETS
  • VerificationPending() - 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 flow

requestDecryption (v2)

v2 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() external

Requirements:

  • 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 verification
  • ComparisonNotSubmitted() - Waiting for comparison service (v2)
  • DecryptionAlreadyEnabled() - Already triggered

Flow:

  1. User calls requestDecryption() on-chain
  2. Contract marks the encrypted distance as publicly decryptable
  3. Client receives event with handle to use in off-chain decryption

finalizeVerification (v2)

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
) external

Parameters:

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 verification
  • DecryptionNotEnabled() - Step 2 not completed
  • AlreadyFinalized() - Request already processed
  • Reverts if FHE.checkSignatures() fails (invalid proof)

cancelVerification

Cancel a pending verification request.

function cancelVerification() external

Requirements:

  • Caller must have pending request

Errors:

  • NoPendingRequest() - No pending verification

View Functions

// 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);

Off-Chain SDK (v2: Public Decryption)

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.

publicDecrypt

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]

UniquitySubmit

Campaign management and encrypted submission system.

State Variables

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;

createCampaign

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 verified
  • InvalidInput() - Empty CID or hash mismatch
  • DeadlineMustBeFuture() - 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

Submit encrypted data to a campaign.

function submit(
    uint256 campaignId,
    bytes32 ipfsCid,
    string calldata ipfsCidString,
    externalEuint256 inEncryptedAesKey,
    bytes calldata inputProof
) external

Parameters:

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 ID
  • NotVerifiedHuman() - User not verified (if required)
  • CampaignNotActive() - Campaign is closed
  • CampaignExpired() - Past deadline
  • AlreadySubmitted() - User already submitted
  • InvalidInput() - Empty CID or hash mismatch
  • MaxSubmissionsReached() - At capacity

editSubmission

Edit an existing submission.

function editSubmission(
    uint256 campaignId,
    bytes32 newIpfsCid,
    string calldata newIpfsCidString,
    externalEuint256 inNewEncryptedAesKey,
    bytes calldata inputProof
) external

Parameters:

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 edits
  • EditDeadlinePassed() - Past edit deadline
  • SubmissionNotFound() - No submission to edit
  • NotSubmitter() - Caller isn't submitter

Admin Functions

// 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;

Key Handle Functions

// 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);

View Functions

// 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);

Frontend Hooks

useUniquityCore

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();

proveHumanity

const proveHumanity = async (
  embedding: FaceEmbedding  // Contains chunks and bucketId
): Promise<TransactionResult>

// v1: Returns immediately with success
// v2: Returns with verificationId for tracking

requestDecryption (v2)

const requestDecryption = async (): Promise<TransactionResult & {
  distanceHandle?: `0x${string}`;
}>

finalizeVerification (v2)

const finalizeVerification = async (
  verificationId: bigint,
  clearDistance: bigint,
  decryptionProof: `0x${string}`
): Promise<TransactionResult>

useUniquitySubmit

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();

createCampaign

interface CreateCampaignParams {
  metadata: CampaignMetadata;
  deadline: number;
  editDeadline?: number;
  requiresVerification?: boolean;
  allowEdits?: boolean;
  maxSubmissions?: number;
}

const createCampaign = async (
  params: CreateCampaignParams
): Promise<TransactionResult & { campaignId?: number }>

submit

const submit = async (
  campaignId: number,
  encryptedDataCid: string,
  aesKey: Uint8Array
): Promise<TransactionResult>

decryptSubmission

const decryptSubmission = async (
  campaignId: number,
  submissionIndex: number
): Promise<DecryptedSubmission | null>

useUniquitySubgraph

import {
  useCampaigns,
  useCampaign,
  useSubmissions,
  useMySubmissions,
  useMyCampaigns,
  useGlobalStats,
} from "@/hooks/useUniquitySubgraph";

useCampaigns

const {
  campaigns,
  loading,
  error,
  page,
  setPage,
  hasMore,
} = useCampaigns(activeFilter?: boolean);

useCampaign

const {
  campaign,
  loading,
  error,
  refresh,
} = useCampaign(campaignId: string);

GraphQL Queries

Get Campaigns

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
  }
}

Get Campaign with Submissions

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
    }
  }
}

Get My Submissions

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
    }
  }
}

Types & Interfaces

Campaign Metadata

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;
}

Transaction Result

interface TransactionResult {
  success: boolean;
  txHash?: string;
  error?: string;
}

Decrypted Submission

interface DecryptedSubmission {
  submitter: string;
  submissionIndex: number;
  data: Record<string, any>;
  submittedAt: Date;
  editCount: number;
}

Face Embedding (v1 & v2)

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
}

Pending Verification (v2)

interface PendingVerification {
  verificationId: bigint;
  timestamp: bigint;
  comparisonSubmitted: boolean; // Off-chain service completed
  decryptionEnabled: boolean; // Ready for publicDecrypt
  distanceHandle: `0x${string}`; // Handle for decryption
}

Decryption Result (v2)

interface DecryptionResult {
  clearDistance: bigint;
  decryptionProof: `0x${string}`;
}