Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

Self is an identity wallet that uses zk-SNARKs to generate privacy-preserving proofs from government-issued IDs (passports, ID cards). Users scan NFC chips to prove validity while revealing only specific attributes (age, nationality, humanity).

## Monorepo Structure

This is a Yarn workspaces monorepo with these main packages:

- **`app/`** - React Native mobile app (iOS/Android) with web support via Vite
- **`circuits/`** - Zero-knowledge proof circuits written in Circom
- **`contracts/`** - Solidity smart contracts for on-chain verification
- **`common/`** - Shared utilities, constants, and types
- **`sdk/`** - SDKs for integration (core, qrcode)

## Development Commands

### Root Level
- `yarn install` - Bootstrap dependencies and setup husky hooks
- `yarn build` - Build all workspaces in topological order
- `yarn lint` - Run linting across all workspaces
- `yarn format` - Format code across all workspaces
- `yarn types` - Run TypeScript checks across workspaces

### Mobile App (`app/`)
- `yarn start` - Start Metro bundler for React Native
- `yarn android` - Run on Android
- `yarn ios` - Run on iOS
- `yarn web` - Start Vite dev server for web
- `yarn test` - Run Jest tests
- `yarn types` - TypeScript type checking
- `yarn lint` / `yarn lint:fix` - ESLint
- `yarn fmt` - Check code formatting (Prettier)
- `yarn fmt:fix` - Fix code formatting issues
- `yarn setup` - Full setup including pods and dependencies
- `yarn reinstall` - Clean reinstall everything

### Circuits (`circuits/`)
- `yarn test` - Run all circuit tests with Mocha
- `yarn test-register` - Test registration circuits
- `yarn test-dsc` - Test DSC verification circuits
- `yarn test-disclose` - Test disclosure circuits
- `yarn build-all` - Build all circuit types
- `yarn download` - Download pre-built circuits from AWS

### Contracts (`contracts/`)
- `yarn build` - Compile Solidity contracts with Hardhat
- `yarn test` - Run contract tests
- `yarn test:local` - Run tests with local environment
- `yarn deploy:verifiers` - Deploy circuit verifiers
- `yarn deploy:registry` - Deploy identity registry
- `yarn deploy:hub:v2` - Deploy verification hub

### Common (`common/`)
- `yarn build` - Build both ESM and CJS distributions
- `yarn test` - Run utility tests

## Architecture Patterns

### Mobile App Architecture
- **State Management**: Zustand stores (`stores/`) for global state
- **Navigation**: React Navigation with route definitions in `navigation/`
- **Platform Separation**: `.web.ts`/`.tsx` files for web-specific implementations
- **Providers**: Context providers for auth, database, passport data
- **Proving Flow**: XState machines for complex proof generation workflows

### Circuit Architecture
- **Modular Design**: Separate circuits for register, disclose, DSC verification
- **Instance Generation**: Multiple algorithm-specific circuit instances
- **Test Coverage**: Comprehensive test suite for each circuit type
- **Utility Circuits**: Reusable components for cryptographic operations

### Contract Architecture
- **Upgradeable Contracts**: Using OpenZeppelin upgradeable pattern
- **Registry Pattern**: Central registry for identity commitments
- **Verification Hub**: Orchestrates different proof types
- **Circuit Verifiers**: Generated Solidity verifiers for each circuit

### Key Components
- **Passport Reading**: NFC scanning with platform-specific native modules
- **Proof Generation**: Browser/mobile-compatible proving using Web Workers
- **ZK Circuits**: Support for multiple hash algorithms and signature schemes
- **State Machines**: XState for complex async workflows like proving

## Testing

### Running Tests
- Mobile: `yarn test` (Jest with React Native Testing Library)
- Circuits: `yarn test` (Mocha with circom_tester)
- Contracts: `yarn test` (Hardhat with Chai)

### Test Organization
- Integration tests in `test/integration/`
- Unit tests alongside source files or in `test/unit/`
- Mock data and utilities in `test/utils/` or `__setup__/`

## Key Files to Understand

- `app/src/stores/selfAppStore.tsx` - WebSocket communication with verification backend
- `app/src/utils/proving/provingMachine.ts` - XState machine for proof generation
- `circuits/circuits/register/register.circom` - Core registration circuit
- `contracts/contracts/IdentityVerificationHubImplV2.sol` - Main verification contract
- `common/src/utils/passportData.ts` - Passport data parsing utilities

## Development Notes

- Use `yarn` (v4.6.0) as package manager
- Gitleaks runs on commit to prevent secrets
- Platform-specific code uses `.web.ts` suffix for web implementations
- Circuit compilation requires significant memory (8GB+ recommended)
- Mobile development requires iOS/Android SDKs and simulators/devices

## Code Formatting & CI

**IMPORTANT**: CI will fail if code formatting is incorrect. Always run formatting before committing:

### App-specific formatting:
```bash
cd app && yarn fmt:fix # Fix formatting in app workspace
cd app && yarn fmt # Check formatting passes
```

### Root-level formatting:
```bash
yarn format # Format all workspaces
```

**Note**: The app workspace uses `yarn fmt`/`yarn fmt:fix` while root uses `yarn format`. Both must pass for CI to succeed.
16 changes: 8 additions & 8 deletions app/src/screens/home/ProofHistoryDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,22 @@ const ProofHistoryDetailScreen: React.FC<ProofHistoryDetailScreenProps> = ({
}, [data.status]);

const logoSource = useMemo(() => {
if (!data.logoBase64) {
if (!data.logoUrl) {
return null;
}

if (
data.logoBase64.startsWith('http://') ||
data.logoBase64.startsWith('https://')
data.logoUrl.startsWith('http://') ||
data.logoUrl.startsWith('https://')
) {
return { uri: data.logoBase64 };
return { uri: data.logoUrl };
}

const base64String = data.logoBase64.startsWith('data:image')
? data.logoBase64
: `data:image/png;base64,${data.logoBase64}`;
const base64String = data.logoUrl.startsWith('data:image')
? data.logoUrl
: `data:image/png;base64,${data.logoUrl}`;
return { uri: base64String };
}, [data.logoBase64]);
}, [data.logoUrl]);

const isEthereumAddress = useMemo(() => {
return (
Expand Down
10 changes: 5 additions & 5 deletions app/src/screens/home/ProofHistoryScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,13 @@ const ProofHistoryScreen: React.FC = () => {
}) => {
try {
const disclosures = JSON.parse(item.disclosures);
const logoSource = item.logoBase64
const logoSource = item.logoUrl
? {
uri:
item.logoBase64.startsWith('data:') ||
item.logoBase64.startsWith('http')
? item.logoBase64
: `data:image/png;base64,${item.logoBase64}`,
item.logoUrl.startsWith('data:') ||
item.logoUrl.startsWith('http')
? item.logoUrl
: `data:image/png;base64,${item.logoUrl}`,
}
: null;

Expand Down
27 changes: 14 additions & 13 deletions app/src/screens/prove/ProveScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ProveScreen: React.FC = () => {
userIdType: selectedApp.userIdType,
endpointType: selectedApp.endpointType,
status: ProofStatus.PENDING,
logoBase64: selectedApp.logoBase64,
logoUrl: selectedApp.logoUrl,
disclosures: JSON.stringify(selectedApp.disclosures),
});
}
Expand Down Expand Up @@ -104,26 +104,27 @@ const ProveScreen: React.FC = () => {
return (selectedApp?.disclosures as SelfAppDisclosureConfig) || [];
}, [selectedApp?.disclosures]);

// Format the logo source based on whether it's a URL or base64 string
// Format the logo source - now only supports URLs
const logoSource = useMemo(() => {
if (!selectedApp?.logoBase64) {
if (!selectedApp?.logoUrl) {
return null;
}

// Check if the logo is already a URL
// Check if the logo is a valid URL
if (
selectedApp.logoBase64.startsWith('http://') ||
selectedApp.logoBase64.startsWith('https://')
selectedApp.logoUrl.startsWith('http://') ||
selectedApp.logoUrl.startsWith('https://')
) {
return { uri: selectedApp.logoBase64 };
return { uri: selectedApp.logoUrl };
}

// Otherwise handle as base64 as before
const base64String = selectedApp.logoBase64.startsWith('data:image')
? selectedApp.logoBase64
: `data:image/png;base64,${selectedApp.logoBase64}`;
return { uri: base64String };
}, [selectedApp?.logoBase64]);
// If not a URL, warn and return null
console.warn(
'logoUrl should be a valid HTTP/HTTPS URL:',
selectedApp.logoUrl,
);
return null;
}, [selectedApp?.logoUrl]);

const url = useMemo(() => {
if (!selectedApp?.endpoint) {
Expand Down
4 changes: 2 additions & 2 deletions app/src/stores/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const database: ProofDB = {
errorReason TEXT,
timestamp INTEGER NOT NULL,
disclosures TEXT NOT NULL,
logoBase64 TEXT
logoBase64 TEXT -- Note: stores logoUrl for backward compatibility
)
`);

Expand All @@ -128,7 +128,7 @@ export const database: ProofDB = {
proof.errorReason || null,
timestamp,
proof.disclosures,
proof.logoBase64 || null,
proof.logoUrl || null, // logoUrl stored in logoBase64 column for compatibility
proof.userId,
proof.userIdType,
proof.sessionId,
Expand Down
2 changes: 1 addition & 1 deletion app/src/stores/proof-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface ProofHistory {
errorReason?: string;
timestamp: number;
disclosures: string;
logoBase64?: string;
logoUrl?: string;
}

export enum ProofStatus {
Expand Down
4 changes: 2 additions & 2 deletions app/src/stores/proofHistoryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => {
const proofs: ProofHistory[] = [];
const totalCount = results.total_count || 0;
for (let i = 0; i < results.rows.length; i++) {
const row = results.rows[i];
const row = results.rows[i] as any; // Database row has logoBase64 column
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety for database row mapping.

The any cast reduces type safety. Consider creating a proper interface for the database row structure to maintain type checking while acknowledging the column name mismatch.

+interface DatabaseRow {
+  id: string;
+  sessionId: string;
+  appName: string;
+  endpointType: string;
+  status: ProofStatus;
+  errorCode?: string;
+  errorReason?: string;
+  timestamp: number;
+  disclosures: string;
+  logoBase64?: string; // Stores logoUrl for backward compatibility
+  userId: string;
+  userIdType: string;
+}

-          const row = results.rows[i] as any; // Database row has logoBase64 column
+          const row = results.rows[i] as DatabaseRow;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const row = results.rows[i] as any; // Database row has logoBase64 column
// At the top of the file (e.g. after imports), add:
interface DatabaseRow {
id: string;
sessionId: string;
appName: string;
endpointType: string;
status: ProofStatus;
errorCode?: string;
errorReason?: string;
timestamp: number;
disclosures: string;
logoBase64?: string; // Stores logoUrl for backward compatibility
userId: string;
userIdType: string;
}
// …later in the code, replace the `any` cast:
- const row = results.rows[i] as any; // Database row has logoBase64 column
+ const row = results.rows[i] as DatabaseRow;
🤖 Prompt for AI Agents
In app/src/stores/proofHistoryStore.ts at line 172, the database row is cast to
'any', which reduces type safety. Define a TypeScript interface representing the
expected structure of the database row, including the 'logoBase64' column, and
use this interface instead of 'any' for the row variable to improve type
checking and maintain clarity about the data shape.

proofs.push({
id: row.id.toString(),
sessionId: row.sessionId,
Expand All @@ -180,7 +180,7 @@ export const useProofHistoryStore = create<ProofHistoryState>()((set, get) => {
errorReason: row.errorReason,
timestamp: row.timestamp,
disclosures: row.disclosures,
logoBase64: row.logoBase64,
logoUrl: row.logoBase64, // logoBase64 column stores logoUrl
userId: row.userId,
userIdType: row.userIdType,
});
Expand Down
6 changes: 3 additions & 3 deletions app/tests/src/stores/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('database (SQLite)', () => {
endpointType: 'https' as const,
status: ProofStatus.PENDING,
disclosures: '{"test": "data"}',
logoBase64: 'base64-logo',
logoUrl: 'https://example.com/logo.png',
};

const mockInsertResult = {
Expand All @@ -95,7 +95,7 @@ describe('database (SQLite)', () => {
null, // errorReason
expect.any(Number), // timestamp
mockProof.disclosures,
mockProof.logoBase64,
mockProof.logoUrl,
mockProof.userId,
mockProof.userIdType,
mockProof.sessionId,
Expand Down Expand Up @@ -141,7 +141,7 @@ describe('database (SQLite)', () => {
mockProof.errorReason,
expect.any(Number),
mockProof.disclosures,
null, // logoBase64
null, // logoUrl (stored in logoBase64 column)
mockProof.userId,
mockProof.userIdType,
mockProof.sessionId,
Expand Down
6 changes: 3 additions & 3 deletions app/tests/src/stores/proofHistoryStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('proofHistoryStore', () => {
errorReason: null,
timestamp: Date.now(),
disclosures: '{"test": "data"}',
logoBase64: 'base64-logo',
logoUrl: 'https://example.com/logo.png',
userId: 'user-456',
userIdType: 'uuid',
},
Expand Down Expand Up @@ -106,7 +106,7 @@ describe('proofHistoryStore', () => {
endpointType: 'celo',
status: ProofStatus.PENDING,
disclosures: '{"test": "data"}',
logoBase64: 'base64-logo',
logoUrl: 'https://example.com/logo.png',
} as const;

const mockInsertResult = {
Expand Down Expand Up @@ -243,7 +243,7 @@ describe('proofHistoryStore', () => {
errorReason: null,
timestamp: Date.now(),
disclosures: '{"test": "data1"}',
logoBase64: 'base64-logo1',
logoUrl: 'https://example.com/logo1.png',
userId: 'user-1',
userIdType: 'uuid',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('_generatePayload', () => {
scope: 's',
sessionId: '',
appName: '',
logoBase64: '',
logoUrl: '',
header: '',
userIdType: 'uuid',
devMode: false,
Expand Down
25 changes: 14 additions & 11 deletions common/src/utils/appType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { formatEndpoint } from './scope.js';

export interface SelfApp {
appName: string;
logoBase64: string;
logoUrl: string;
endpointType: EndpointType;
endpoint: string;
header: string;
Expand Down Expand Up @@ -89,18 +89,21 @@ export class SelfAppBuilder {
}

this.config = {
appName: config.appName,
logoUrl: config.logoUrl || '',
endpointType: config.endpointType || 'https',
endpoint: config.endpoint,
header: config.header || '',
scope: config.scope,
sessionId: v4(),
userIdType: 'uuid',
devMode: false,
endpointType: 'https',
header: '',
logoBase64: '',
disclosures: {},
userId: config.userId,
userIdType: config.userIdType || 'uuid',
devMode: config.devMode || false,
disclosures: config.disclosures || {},
version: 2,
chainID: config.endpointType === 'staging_celo' ? 44787 : 42220,
version: config.version ?? 2,
userDefinedData: '',
...config,
} as SelfApp;
userDefinedData: config.userDefinedData || '',
};
}

build(): SelfApp {
Expand Down
Loading
Loading