diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..54332edca --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,219 @@ +# 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. + +## Linting + +The codebase uses ESLint for code quality checks. Run linting before commits: + +### Root-level linting: +```bash +yarn lint # Lint all workspaces (warnings allowed, errors will fail CI) +``` + +**Note**: ESLint warnings are allowed and will not fail CI, but errors will. The lint output includes warnings for console statements and TypeScript `any` types which are acceptable in development code. + +## CI/CD Requirements + +The project uses GitHub Actions with multiple workflows for different components. All checks must pass for PRs to be merged. + +### CI Workflows Overview + +#### General Checks (`general-checks.yml`) +Runs on all pull requests: +- **Lint**: `yarn lint` - ESLint checks across all workspaces +- **Type Check**: `yarn types` - TypeScript type checking across workspaces +- **Common Tests**: `yarn workspace @selfxyz/common test` - Tests for shared utilities + +#### App CI (`app.yml`) +Runs when `app/` or `common/` files change: +- **Lint**: `yarn lint` in app workspace +- **Format**: `yarn fmt` in app workspace (Prettier check) +- **Test**: `yarn test` after building dependencies +- **Build**: Full iOS/Android build with Xcode 16.2 + +#### Circuits CI (`circuits.yml`) +Runs when `circuits/` or `common/` files change: +- **Lint**: `yarn workspace @selfxyz/circuits lint` +- **Test**: Circuit tests with Circom 2.1.9 and specialized dependencies + +#### Contracts CI (`contracts.yml`) +Runs when `contracts/` or `common/` files change: +- **Format**: `yarn prettier:check` in contracts workspace +- **Build**: `yarn build` after building common dependencies +- **Test**: Currently disabled (`if: false`) + +#### Security Scans +Run on all pull requests: +- **Gitleaks**: Secret detection using `.gitleaks.toml` configuration +- **GitGuardian**: Additional secret scanning service + +### Key CI Commands + +To ensure CI passes, run these commands before committing: + +```bash +# Root level - runs across all workspaces +yarn lint # ESLint (warnings OK, errors fail CI) +yarn types # TypeScript type checking +yarn format # Prettier formatting + +# App workspace specific +cd app +yarn lint # App-specific ESLint +yarn fmt # App-specific Prettier check +yarn test # Jest tests +yarn build:deps # Build dependencies + +# Circuits workspace +yarn workspace @selfxyz/circuits lint +yarn workspace @selfxyz/circuits test + +# Contracts workspace +cd contracts +yarn prettier:check +yarn build +``` + +### Pre-commit Setup + +The project uses Husky for pre-commit hooks: +- **Gitleaks**: `yarn gitleaks` - Prevents committing secrets +- Configured via `yarn prepare` during installation + +### Environment Requirements + +Different workspaces have specific environment needs: +- **Node.js**: 18.x for most workspaces, 20.x for contracts +- **Ruby**: 3.2 for mobile builds +- **Java**: 17 for Android builds +- **Xcode**: 16.2 for iOS builds +- **Circom**: 2.1.9 for circuit compilation \ No newline at end of file diff --git a/app/src/screens/home/ProofHistoryDetailScreen.tsx b/app/src/screens/home/ProofHistoryDetailScreen.tsx index d6738e3e9..81518b46e 100644 --- a/app/src/screens/home/ProofHistoryDetailScreen.tsx +++ b/app/src/screens/home/ProofHistoryDetailScreen.tsx @@ -113,22 +113,22 @@ const ProofHistoryDetailScreen: React.FC = ({ }, [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 ( diff --git a/app/src/screens/home/ProofHistoryScreen.tsx b/app/src/screens/home/ProofHistoryScreen.tsx index b408d8e6e..864c3ffe8 100644 --- a/app/src/screens/home/ProofHistoryScreen.tsx +++ b/app/src/screens/home/ProofHistoryScreen.tsx @@ -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; diff --git a/app/src/screens/prove/ProveScreen.tsx b/app/src/screens/prove/ProveScreen.tsx index 9c39f1d17..7f5813dde 100644 --- a/app/src/screens/prove/ProveScreen.tsx +++ b/app/src/screens/prove/ProveScreen.tsx @@ -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), }); } @@ -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) { diff --git a/app/src/stores/database.ts b/app/src/stores/database.ts index d771e5bcc..bdb82d4f7 100644 --- a/app/src/stores/database.ts +++ b/app/src/stores/database.ts @@ -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 ) `); @@ -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, diff --git a/app/src/stores/proof-types.ts b/app/src/stores/proof-types.ts index 95d9314f4..3471d3433 100644 --- a/app/src/stores/proof-types.ts +++ b/app/src/stores/proof-types.ts @@ -14,7 +14,7 @@ export interface ProofHistory { errorReason?: string; timestamp: number; disclosures: string; - logoBase64?: string; + logoUrl?: string; } export enum ProofStatus { diff --git a/app/src/stores/proofHistoryStore.ts b/app/src/stores/proofHistoryStore.ts index e9cdde35c..ddf437b31 100644 --- a/app/src/stores/proofHistoryStore.ts +++ b/app/src/stores/proofHistoryStore.ts @@ -169,7 +169,7 @@ export const useProofHistoryStore = create()((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 proofs.push({ id: row.id.toString(), sessionId: row.sessionId, @@ -180,7 +180,7 @@ export const useProofHistoryStore = create()((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, }); diff --git a/app/tests/src/stores/database.test.ts b/app/tests/src/stores/database.test.ts index a10f04a6c..e80fb7d99 100644 --- a/app/tests/src/stores/database.test.ts +++ b/app/tests/src/stores/database.test.ts @@ -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 = { @@ -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, @@ -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, diff --git a/app/tests/src/stores/proofHistoryStore.test.ts b/app/tests/src/stores/proofHistoryStore.test.ts index 667abf064..84c3c8022 100644 --- a/app/tests/src/stores/proofHistoryStore.test.ts +++ b/app/tests/src/stores/proofHistoryStore.test.ts @@ -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', }, @@ -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 = { @@ -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', }, diff --git a/app/tests/utils/proving/provingMachine.generatePayload.test.ts b/app/tests/utils/proving/provingMachine.generatePayload.test.ts index 80a333e21..80e45bfb2 100644 --- a/app/tests/utils/proving/provingMachine.generatePayload.test.ts +++ b/app/tests/utils/proving/provingMachine.generatePayload.test.ts @@ -91,7 +91,7 @@ describe('_generatePayload', () => { scope: 's', sessionId: '', appName: '', - logoBase64: '', + logoUrl: '', header: '', userIdType: 'uuid', devMode: false, diff --git a/common/src/utils/appType.ts b/common/src/utils/appType.ts index 100c98914..19b598049 100644 --- a/common/src/utils/appType.ts +++ b/common/src/utils/appType.ts @@ -10,7 +10,7 @@ import { formatEndpoint } from './scope.js'; export interface SelfApp { appName: string; - logoBase64: string; + logoUrl: string; endpointType: EndpointType; endpoint: string; header: string; @@ -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 { diff --git a/sdk/core/README.md b/sdk/core/README.md index 0530d4b56..db9033fdd 100644 --- a/sdk/core/README.md +++ b/sdk/core/README.md @@ -22,7 +22,7 @@ import { SelfBackendVerifier } from '@selfxyz/core'; const selfBackendVerifier = new SelfBackendVerifier( scope, // Your application's unique scope endpoint, // Your verification endpoint URL - mockPassport, // Whether to use testnet (true) or mainnet (false) + network, // Network to use ('mainnet' or 'testnet') allowedIds, // Map of allowed attestation IDs configStorage, // Configuration storage implementation userIdentifierType // Type of user identifier ('hex' or 'uuid') @@ -132,7 +132,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const selfBackendVerifier = new SelfBackendVerifier( 'my-application-scope', 'https://my-api.com/api/verify', - false, // Use mainnet + 'mainnet', // Network to use ('mainnet' or 'testnet') new Map([[1, true]]), // Allow passport attestation configStore, 'uuid' // User identifier type @@ -209,7 +209,7 @@ const selfApp = new SelfAppBuilder({ appName: 'My Application', scope: 'my-application-scope', endpoint: 'https://my-api.com/api/verify', // Your API using SelfBackendVerifier - logoBase64: myLogoBase64, + logoUrl: 'https://my-api.com/logo.png', userId, disclosures: { name: true, diff --git a/sdk/core/src/SelfBackendVerifier.ts b/sdk/core/src/SelfBackendVerifier.ts index bc3b975fd..8c2840da9 100644 --- a/sdk/core/src/SelfBackendVerifier.ts +++ b/sdk/core/src/SelfBackendVerifier.ts @@ -24,6 +24,8 @@ const CELO_TESTNET_RPC_URL = 'https://alfajores-forno.celo-testnet.org'; const IDENTITY_VERIFICATION_HUB_ADDRESS = '0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF'; const IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = '0x68c931C9a534D37aa78094877F46fE46a49F1A51'; +export type Network = 'mainnet' | 'testnet'; + export class SelfBackendVerifier { protected scope: string; protected identityVerificationHubContract: IdentityVerificationHubImpl; @@ -35,16 +37,17 @@ export class SelfBackendVerifier { constructor( scope: string, endpoint: string, - mockPassport: boolean = false, + network: Network = 'mainnet', allowedIds: Map, configStorage: IConfigStorage, userIdentifierType: UserIdType ) { - const rpcUrl = mockPassport ? CELO_TESTNET_RPC_URL : CELO_MAINNET_RPC_URL; + const rpcUrl = network === 'testnet' ? CELO_TESTNET_RPC_URL : CELO_MAINNET_RPC_URL; const provider = new ethers.JsonRpcProvider(rpcUrl); - const identityVerificationHubAddress = mockPassport - ? IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING - : IDENTITY_VERIFICATION_HUB_ADDRESS; + const identityVerificationHubAddress = + network === 'testnet' + ? IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING + : IDENTITY_VERIFICATION_HUB_ADDRESS; this.identityVerificationHubContract = IdentityVerificationHubImpl__factory.connect( identityVerificationHubAddress, provider diff --git a/sdk/core/src/index.ts b/sdk/core/src/index.ts index 3a5241a45..fc40fc959 100644 --- a/sdk/core/src/index.ts +++ b/sdk/core/src/index.ts @@ -1 +1 @@ -export { SelfBackendVerifier } from './SelfBackendVerifier.js'; +export { SelfBackendVerifier, type Network } from './SelfBackendVerifier.js'; diff --git a/sdk/qrcode/README.md b/sdk/qrcode/README.md index ecabf1262..1cfe5fdb9 100644 --- a/sdk/qrcode/README.md +++ b/sdk/qrcode/README.md @@ -30,7 +30,7 @@ const selfApp = new SelfAppBuilder({ appName: 'My App', scope: 'my-app-scope', endpoint: 'https://myapp.com/api/verify', - logoBase64: 'base64EncodedLogo', // Optional + logoUrl: 'https://myapp.com/logo.png', // Optional userId, // Optional disclosure requirements disclosures: { @@ -74,14 +74,14 @@ function MyComponent() { The `SelfAppBuilder` allows you to configure your application's verification requirements: -| Parameter | Type | Required | Description | -| ------------- | ------ | -------- | ---------------------------------------------- | -| `appName` | string | Yes | The name of your application | -| `scope` | string | Yes | A unique identifier for your application | -| `endpoint` | string | Yes | The endpoint that will verify the proof | -| `logoBase64` | string | No | Base64-encoded logo to display in the Self app | -| `userId` | string | Yes | Unique identifier for the user | -| `disclosures` | object | No | Disclosure and verification requirements | +| Parameter | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------------------ | +| `appName` | string | Yes | The name of your application | +| `scope` | string | Yes | A unique identifier for your application | +| `endpoint` | string | Yes | The endpoint that will verify the proof | +| `logoUrl` | string | No | URL to PNG logo to display in the Self app | +| `userId` | string | Yes | Unique identifier for the user | +| `disclosures` | object | No | Disclosure and verification requirements | ### Disclosure Options diff --git a/sdk/qrcode/utils/websocket.ts b/sdk/qrcode/utils/websocket.ts index 37c480b41..5cc65ad34 100644 --- a/sdk/qrcode/utils/websocket.ts +++ b/sdk/qrcode/utils/websocket.ts @@ -5,7 +5,7 @@ import { SelfApp } from '@selfxyz/common/utils/appType'; export interface WebAppInfo { appName: string; userId: string; - logoBase64: string; + logoUrl: string; } // Log once when this module loads