diff --git a/app/README.md b/app/README.md index 26e4b8ec8..842e1aa5e 100644 --- a/app/README.md +++ b/app/README.md @@ -12,12 +12,12 @@ ### Android -| Requirement | Version | Installation Guide | -| --------------------------- | ------------- | ------------------------------------------------------------------------------------- | -| Java | 17 | [Install Java](https://www.oracle.com/java/technologies/javase-jdk17-downloads.html) | -| Android Studio (Optional)* | Latest | [Install Android Studio](https://developer.android.com/studio) | -| Android SDK | Latest | See instructions for Android below | -| Android NDK | 27.0.11718014 | See instructions for Android below | +| Requirement | Version | Installation Guide | +| --------------------------- | ------------- | ------------------------------------------------------------------------------------ | +| Java | 17 | [Install Java](https://www.oracle.com/java/technologies/javase-jdk17-downloads.html) | +| Android Studio (Optional)\* | Latest | [Install Android Studio](https://developer.android.com/studio) | +| Android SDK | Latest | See instructions for Android below | +| Android NDK | 27.0.11718014 | See instructions for Android below | \* To facilitate the installation of the SDK and the NDK, and to pair with development devices with a conventient QR code, you can use Android Studio. @@ -62,39 +62,40 @@ Under **SDK Platforms**, install the platform with the highest API number Under **SDK Tools**, check the **Show Package Details** checkbox, expand **NDK (Side by side)**, select version **27.0.11718014** and install. - #### Using sdkmanager via CLI Create a directory for the Android SDK. For example `~/android_sdk`. Define the environment variable `ANDROID_HOME` to point that directory. Install sdkmanager under `ANDROID_HOME` according to the instructions on https://developer.android.com/tools/sdkmanager - - List available SDK platforms + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --list | grep platforms ``` -In the list of platforms, find the latest version and install it. (Replace *NN* with the latest version number) +In the list of platforms, find the latest version and install it. (Replace _NN_ with the latest version number) + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-NN" ``` Install the NDK + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014" ``` + Define the environment variable `ANDROID_NDK_VERSION` to `27.0.11718014` and `ANDROID_NDK` to `$ANDROID_HOME/ndk/27.0.11718014` Install Platform Tools, needed for the `adb` tool + ```bash $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install platform-tools ``` Add `$ANDROID_HOME/platform-tools` to your `$PATH` variable - ## Run the app ### Android @@ -108,11 +109,13 @@ In Android Studio, use Device Manager to pair with and connect to your phone. ##### Using adb In your phone's developer settings, select **Wireless debugging** > **Pair the device using a pairing code**. Using the displayed information, run + ``` adb pair PHONE_IP:PAIRING_PORT PAIRING_CODE ``` To connect to the device, find the IP number and port (different port than in the pairing step) directly under Wireless debugging, and run + ``` adb connect PHONE_IP:DEVELOPMENT_PORT ``` @@ -126,11 +129,11 @@ sdk.dir=/path/to/your/android/sdk ``` or create it with + ```bash echo sdk.dir=$ANDROID_HOME > android/local.properties ``` - Launch the React Native server: ```bash @@ -160,6 +163,7 @@ pod install And run the app in Xcode. #### Simulator Build + > **Note:** iOS Simulator on Apple Silicon Macs requires Rosetta (x86_64) mode due to simulator architecture compatibility. If you're using a Silicon Mac (M1/M2/M3/M4), you may find that the Rosetta simulator build option is not available by default in Xcode. To enable it, open Xcode and go to **Product > Destination > Show All Run Destinations**. This will unlock the ability to select the Rosetta build simulator, allowing you to run the app in the iOS Simulator. @@ -235,6 +239,7 @@ Deployments happen automatically when you merge PRs: 2. **Merge to `main`** → Deploys to production To control versions with PR labels: + - `version:major` - Major version bump - `version:minor` - Minor version bump - `version:patch` - Patch version bump (default for main) @@ -257,6 +262,7 @@ git push && git push --tags ``` The release script will: + - Check for uncommitted changes - Bump the version in package.json - Update iOS and Android native versions @@ -310,7 +316,9 @@ bundle exec fastlane ios build_local ### Troubleshooting Deployments #### Version Already Exists + The build system auto-increments build numbers. If you get version conflicts: + ```bash # Check current versions node scripts/version.cjs status @@ -321,6 +329,7 @@ node scripts/version.cjs bump-build android ``` #### Certificate Issues (iOS) + ```bash # Check certificate validity bundle exec fastlane ios check_certs @@ -332,18 +341,22 @@ bundle exec fastlane ios check_certs ``` #### Play Store Upload Issues + If automated upload fails, the AAB is saved locally: + - Location: `android/app/build/outputs/bundle/release/app-release.aab` - Upload manually via Play Console ### Build Optimization The CI/CD pipeline uses extensive caching: + - **iOS builds**: ~15 minutes (with cache) - **Android builds**: ~10 minutes (with cache) - **First build**: ~25 minutes (no cache) To speed up local builds: + ```bash # Clean only what's necessary yarn clean:build # Clean build artifacts only diff --git a/app/package.json b/app/package.json index a5347efe2..4a7d99631 100644 --- a/app/package.json +++ b/app/package.json @@ -24,8 +24,8 @@ "clean:xcode": "rm -rf ~/Library/Developer/Xcode/DerivedData", "clean:xcode-env-local": "rm -f ios/.xcode.env.local", "find:type-imports": "node scripts/find-type-import-issues.mjs", - "fmt": "prettier --check .", - "fmt:fix": "prettier --write .", + "fmt": "yarn prettier --check .", + "fmt:fix": "yarn prettier --write .", "format": "yarn nice", "ia": "yarn install-app", "imports:fix": "node ./scripts/alias-imports.cjs", diff --git a/app/src/components/NavBar/HomeNavBar.tsx b/app/src/components/NavBar/HomeNavBar.tsx index 057189e76..0adc5030f 100644 --- a/app/src/components/NavBar/HomeNavBar.tsx +++ b/app/src/components/NavBar/HomeNavBar.tsx @@ -10,12 +10,12 @@ import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; import { Clipboard as ClipboardIcon } from '@tamagui/lucide-icons'; import type { SelfApp } from '@selfxyz/common/utils/appType'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import { NavBar } from '@/components/NavBar/BaseNavBar'; import ActivityIcon from '@/images/icons/activity.svg'; import ScanIcon from '@/images/icons/qr_scan.svg'; import SettingsIcon from '@/images/icons/settings.svg'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, charcoal, neutral400, slate50, white } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; import { buttonTap } from '@/utils/haptic'; diff --git a/app/src/screens/prove/ConfirmBelongingScreen.tsx b/app/src/screens/prove/ConfirmBelongingScreen.tsx index 68b56f62f..360d64736 100644 --- a/app/src/screens/prove/ConfirmBelongingScreen.tsx +++ b/app/src/screens/prove/ConfirmBelongingScreen.tsx @@ -60,13 +60,13 @@ const ConfirmBelongingScreen: React.FC = () => { if (permissionGranted) { const token = await getFCMToken(); if (token) { - setFcmToken(token); + setFcmToken(token, selfClient); trackEvent(ProofEvents.FCM_TOKEN_STORED); } } // Mark as user confirmed - proving will start automatically when ready - setUserConfirmed(); + setUserConfirmed(selfClient); // Navigate to loading screen navigate(); diff --git a/app/src/screens/prove/ProofRequestStatusScreen.tsx b/app/src/screens/prove/ProofRequestStatusScreen.tsx index df9f91e81..c1e6819b8 100644 --- a/app/src/screens/prove/ProofRequestStatusScreen.tsx +++ b/app/src/screens/prove/ProofRequestStatusScreen.tsx @@ -11,6 +11,7 @@ import { useIsFocused } from '@react-navigation/native'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import loadingAnimation from '@/assets/animations/loading/misc.json'; import failAnimation from '@/assets/animations/proof_failed.json'; @@ -24,7 +25,6 @@ import useHapticNavigation from '@/hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { ProofStatus } from '@/stores/proof-types'; import { useProofHistoryStore } from '@/stores/proofHistoryStore'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, white } from '@/utils/colors'; import { buttonTap, diff --git a/app/src/screens/prove/ProveScreen.tsx b/app/src/screens/prove/ProveScreen.tsx index 1d505f98d..c2e9d06b1 100644 --- a/app/src/screens/prove/ProveScreen.tsx +++ b/app/src/screens/prove/ProveScreen.tsx @@ -24,6 +24,7 @@ import type { SelfAppDisclosureConfig } from '@selfxyz/common/utils/appType'; import { formatEndpoint } from '@selfxyz/common/utils/scope'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import miscAnimation from '@/assets/animations/loading/misc.json'; import { HeldPrimaryButtonProveScreen } from '@/components/buttons/HeldPrimaryButtonProveScreen'; @@ -37,7 +38,6 @@ import { } from '@/providers/passportDataProvider'; import { ProofStatus } from '@/stores/proof-types'; import { useProofHistoryStore } from '@/stores/proofHistoryStore'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, slate300, white } from '@/utils/colors'; import { formatUserId } from '@/utils/formatUserId'; import { buttonTap } from '@/utils/haptic'; @@ -149,7 +149,7 @@ const ProveScreen: React.FC = () => { ); function onVerify() { - provingStore.setUserConfirmed(); + provingStore.setUserConfirmed(selfClient); buttonTap(); trackEvent(ProofEvents.PROOF_VERIFY_CONFIRMATION_ACCEPTED, { appName: selectedApp?.appName, diff --git a/app/src/screens/prove/QRCodeViewFinderScreen.tsx b/app/src/screens/prove/QRCodeViewFinderScreen.tsx index 766ae2de8..5f9168ed4 100644 --- a/app/src/screens/prove/QRCodeViewFinderScreen.tsx +++ b/app/src/screens/prove/QRCodeViewFinderScreen.tsx @@ -14,6 +14,7 @@ import { import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { ProofEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import qrScanAnimation from '@/assets/animations/qr_scan.json'; import { SecondaryButton } from '@/components/buttons/SecondaryButton'; @@ -26,7 +27,6 @@ import useConnectionModal from '@/hooks/useConnectionModal'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import QRScan from '@/images/icons/qr_code.svg'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { black, slate800, white } from '@/utils/colors'; import { parseAndValidateUrlParams } from '@/utils/deeplinks'; diff --git a/app/src/utils/deeplinks.ts b/app/src/utils/deeplinks.ts index 79e70a775..1ded995d2 100644 --- a/app/src/utils/deeplinks.ts +++ b/app/src/utils/deeplinks.ts @@ -7,9 +7,9 @@ import { Linking, Platform } from 'react-native'; import { countries } from '@selfxyz/common/constants/countries'; import type { IdDocInput } from '@selfxyz/common/utils'; +import { useSelfAppStore } from '@selfxyz/mobile-sdk-alpha/stores'; import { navigationRef } from '@/navigation'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import useUserStore from '@/stores/userStore'; // Validation patterns for each expected parameter diff --git a/app/src/utils/proving/provingInputs.ts b/app/src/utils/proving/provingInputs.ts deleted file mode 100644 index 028a8561a..000000000 --- a/app/src/utils/proving/provingInputs.ts +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import type { DocumentCategory, PassportData } from '@selfxyz/common/types'; -import type { SelfApp } from '@selfxyz/common/utils'; -import { generateTEEInputsDiscloseStateless } from '@selfxyz/common/utils/circuits/registerInputs'; -import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; - -export function generateTEEInputsDisclose( - secret: string, - passportData: PassportData, - selfApp: SelfApp, -) { - return generateTEEInputsDiscloseStateless( - secret, - passportData, - selfApp, - (document: DocumentCategory, tree) => { - const protocolStore = useProtocolStore.getState(); - switch (tree) { - case 'ofac': - return protocolStore[document].ofac_trees; - case 'commitment': - return protocolStore[document].commitment_tree; - default: - throw new Error('Unknown tree type'); - } - }, - ); -} diff --git a/app/src/utils/proving/provingMachine.ts b/app/src/utils/proving/provingMachine.ts index f7df25b51..463678f22 100644 --- a/app/src/utils/proving/provingMachine.ts +++ b/app/src/utils/proving/provingMachine.ts @@ -38,6 +38,7 @@ import { } from '@selfxyz/common/utils/proving'; import { clearPassportData, + generateTEEInputsDisclose, hasAnyValidRegisteredDocument, loadSelectedDocument, markCurrentDocumentAsRegistered, @@ -49,13 +50,10 @@ import { PassportEvents, ProofEvents, } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; -import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; - -import { useSelfAppStore } from '@/stores/selfAppStore'; -import analytics from '@/utils/analytics'; -import { generateTEEInputsDisclose } from '@/utils/proving/provingInputs'; - -const { trackEvent } = analytics(); +import { + useProtocolStore, + useSelfAppStore, +} from '@selfxyz/mobile-sdk-alpha/stores'; export type ProvingStateType = // Initial states @@ -163,12 +161,20 @@ export const getPostVerificationRoute = () => { // return cloudBackupEnabled ? 'AccountVerifiedSuccess' : 'SaveRecoveryPhrase'; }; +type WsHandlers = { + message: (event: MessageEvent) => void; + open: () => void; + error: (error: Event) => void; + close: (event: CloseEvent) => void; +}; + interface ProvingState { currentState: ProvingStateType; attestation: number[] | null; serverPublicKey: string | null; sharedKey: Buffer | null; wsConnection: WebSocket | null; + wsHandlers: WsHandlers | null; socketConnection: Socket | null; uuid: string | null; userConfirmed: boolean; @@ -180,30 +186,43 @@ interface ProvingState { endpointType: EndpointType | null; fcmToken: string | null; env: 'prod' | 'stg' | null; - selfClient: SelfClient | null; - setFcmToken: (token: string) => void; + setFcmToken: (token: string, selfClient: SelfClient) => void; init: ( selfClient: SelfClient, circuitType: 'dsc' | 'disclose' | 'register', userConfirmed?: boolean, ) => Promise; - startFetchingData: () => Promise; + startFetchingData: (selfClient: SelfClient) => Promise; validatingDocument: (selfClient: SelfClient) => Promise; - initTeeConnection: () => Promise; - startProving: () => Promise; + initTeeConnection: (selfClient: SelfClient) => Promise; + startProving: (selfClient: SelfClient) => Promise; postProving: (selfClient: SelfClient) => void; - setUserConfirmed: () => void; - _closeConnections: () => void; - _generatePayload: () => Promise; - _handleWebSocketMessage: (event: MessageEvent) => Promise; + setUserConfirmed: (selfClient: SelfClient) => void; + _closeConnections: (selfClient: SelfClient) => void; + _generatePayload: (selfClient: SelfClient) => Promise<{ + jsonrpc: '2.0'; + method: 'openpassport_submit_request'; + id: 2; + params: { + uuid: string | null; + nonce: number[]; + cipher_text: number[]; + auth_tag: number[]; + }; + }>; + _handleWebSocketMessage: ( + event: MessageEvent, + selfClient: SelfClient, + ) => Promise; _handleRegisterErrorOrFailure: (selfClient: SelfClient) => void; _startSocketIOStatusListener: ( receivedUuid: string, endpointType: EndpointType, + selfClient: SelfClient, ) => void; - _handleWsOpen: () => void; - _handleWsError: (error: Event) => void; - _handleWsClose: (event: CloseEvent) => void; + _handleWsOpen: (selfClient: SelfClient) => void; + _handleWsError: (error: Event, selfClient: SelfClient) => void; + _handleWsClose: (event: CloseEvent, selfClient: SelfClient) => void; _handlePassportNotSupported: (selfClient: SelfClient) => void; _handleAccountRecoveryChoice: (selfClient: SelfClient) => void; @@ -220,22 +239,24 @@ export const useProvingStore = create((set, get) => { ) { newActor.subscribe((state: StateFrom) => { console.log(`State transition: ${state.value}`); - trackEvent(ProofEvents.PROVING_STATE_CHANGE, { state: state.value }); + selfClient.trackEvent(ProofEvents.PROVING_STATE_CHANGE, { + state: state.value, + }); set({ currentState: state.value as ProvingStateType }); if (state.value === 'fetching_data') { - get().startFetchingData(); + get().startFetchingData(selfClient); } if (state.value === 'validating_document') { get().validatingDocument(selfClient); } if (state.value === 'init_tee_connexion') { - get().initTeeConnection(); + get().initTeeConnection(selfClient); } if (state.value === 'ready_to_prove' && get().userConfirmed) { - get().startProving(); + get().startProving(selfClient); } if (state.value === 'post_proving') { @@ -250,7 +271,7 @@ export const useProvingStore = create((set, get) => { } if (state.value === 'completed') { - trackEvent(ProofEvents.PROOF_COMPLETED, { + selfClient.trackEvent(ProofEvents.PROOF_COMPLETED, { circuitType: get().circuitType, }); @@ -313,6 +334,7 @@ export const useProvingStore = create((set, get) => { serverPublicKey: null, sharedKey: null, wsConnection: null, + wsHandlers: null, socketConnection: null, uuid: null, userConfirmed: false, @@ -325,21 +347,22 @@ export const useProvingStore = create((set, get) => { reason: null, endpointType: null, fcmToken: null, - selfClient: null, - setFcmToken: (token: string) => { + setFcmToken: (token: string, selfClient: SelfClient) => { set({ fcmToken: token }); - trackEvent(ProofEvents.FCM_TOKEN_STORED); + selfClient.trackEvent(ProofEvents.FCM_TOKEN_STORED); }, - _handleWebSocketMessage: async (event: MessageEvent) => { + _handleWebSocketMessage: async ( + event: MessageEvent, + selfClient: SelfClient, + ) => { if (!actor) { console.error('Cannot process message: State machine not initialized.'); return; } - try { const result = JSON.parse(event.data); if (result.result?.attestation) { - trackEvent(ProofEvents.ATTESTATION_RECEIVED); + selfClient?.trackEvent(ProofEvents.ATTESTATION_RECEIVED); const attestationData = result.result.attestation; set({ attestation: attestationData }); @@ -352,7 +375,7 @@ export const useProvingStore = create((set, get) => { return; } - trackEvent(ProofEvents.ATTESTATION_VERIFIED); + selfClient?.trackEvent(ProofEvents.ATTESTATION_VERIFIED); const serverKey = ec.keyFromPublic(serverPubkey as string, 'hex'); const derivedKey = clientKey.derive(serverKey.getPublic()); @@ -361,7 +384,7 @@ export const useProvingStore = create((set, get) => { serverPublicKey: serverPubkey, sharedKey: Buffer.from(derivedKey.toArray('be', 32)), }); - trackEvent(ProofEvents.SHARED_KEY_DERIVED); + selfClient?.trackEvent(ProofEvents.SHARED_KEY_DERIVED); actor!.send({ type: 'CONNECT_SUCCESS' }); } else if ( @@ -369,7 +392,7 @@ export const useProvingStore = create((set, get) => { typeof result.result === 'string' && !result.error ) { - trackEvent(ProofEvents.WS_HELLO_ACK); + selfClient?.trackEvent(ProofEvents.WS_HELLO_ACK); // Received status from TEE const statusUuid = result.result; if (get().uuid !== statusUuid) { @@ -384,18 +407,24 @@ export const useProvingStore = create((set, get) => { console.error( 'Cannot start Socket.IO listener: endpointType not set.', ); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient?.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); actor!.send({ type: 'PROVE_ERROR' }); return; } - get()._startSocketIOStatusListener(statusUuid, endpointType); + get()._startSocketIOStatusListener( + statusUuid, + endpointType, + selfClient, + ); } else if (result.error) { console.error('Received error from TEE:', result.error); - trackEvent(ProofEvents.TEE_WS_ERROR, { error: result.error }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient?.trackEvent(ProofEvents.TEE_WS_ERROR, { + error: result.error, + }); + selfClient?.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -406,15 +435,15 @@ export const useProvingStore = create((set, get) => { } catch (error) { console.error('Error processing WebSocket message:', error); if (get().currentState === 'init_tee_connexion') { - trackEvent(ProofEvents.TEE_CONN_FAILED, { + selfClient?.trackEvent(ProofEvents.TEE_CONN_FAILED, { message: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'CONNECT_ERROR' }); } else { - trackEvent(ProofEvents.TEE_WS_ERROR, { + selfClient?.trackEvent(ProofEvents.TEE_WS_ERROR, { error: error instanceof Error ? error.message : String(error), }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient?.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -440,6 +469,7 @@ export const useProvingStore = create((set, get) => { _startSocketIOStatusListener: ( receivedUuid: string, endpointType: EndpointType, + selfClient: SelfClient, ) => { if (!actor) { console.error('Cannot start Socket.IO listener: Actor not available.'); @@ -452,19 +482,19 @@ export const useProvingStore = create((set, get) => { transports: ['websocket'], }); set({ socketConnection: socket }); - trackEvent(ProofEvents.SOCKETIO_CONN_STARTED); + selfClient.trackEvent(ProofEvents.SOCKETIO_CONN_STARTED); socket.on('connect', () => { socket?.emit('subscribe', receivedUuid); - trackEvent(ProofEvents.SOCKETIO_SUBSCRIBED); + selfClient.trackEvent(ProofEvents.SOCKETIO_SUBSCRIBED); }); socket.on('connect_error', error => { console.error('SocketIO connection error:', error); - trackEvent(ProofEvents.SOCKETIO_CONNECT_ERROR, { + selfClient.trackEvent(ProofEvents.SOCKETIO_CONNECT_ERROR, { message: error instanceof Error ? error.message : String(error), }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -479,8 +509,8 @@ export const useProvingStore = create((set, get) => { console.error( 'SocketIO disconnected unexpectedly during proof listening.', ); - trackEvent(ProofEvents.SOCKETIO_DISCONNECT_UNEXPECTED); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.SOCKETIO_DISCONNECT_UNEXPECTED); + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -492,7 +522,7 @@ export const useProvingStore = create((set, get) => { socket.on('status', (message: unknown) => { const data = typeof message === 'string' ? JSON.parse(message) : message; - trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { + selfClient.trackEvent(ProofEvents.SOCKETIO_STATUS_RECEIVED, { status: data.status, }); if (data.status === 3 || data.status === 5) { @@ -501,11 +531,11 @@ export const useProvingStore = create((set, get) => { ); console.error(data); set({ error_code: data.error_code, reason: data.reason }); - trackEvent(ProofEvents.SOCKETIO_PROOF_FAILURE, { + selfClient.trackEvent(ProofEvents.SOCKETIO_PROOF_FAILURE, { error_code: data.error_code, reason: data.reason, }); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: data.error_code ?? 'unknown', }); @@ -516,15 +546,15 @@ export const useProvingStore = create((set, get) => { socket?.disconnect(); set({ socketConnection: null }); if (get().circuitType === 'register') { - trackEvent(ProofEvents.REGISTER_COMPLETED); + selfClient.trackEvent(ProofEvents.REGISTER_COMPLETED); } - trackEvent(ProofEvents.SOCKETIO_PROOF_SUCCESS); + selfClient.trackEvent(ProofEvents.SOCKETIO_PROOF_SUCCESS); actor!.send({ type: 'PROVE_SUCCESS' }); } }); }, - _handleWsOpen: () => { + _handleWsOpen: (selfClient: SelfClient) => { if (!actor) { return; } @@ -534,7 +564,7 @@ export const useProvingStore = create((set, get) => { } const connectionUuid = v4(); - trackEvent(ProofEvents.CONNECTION_UUID_GENERATED, { + selfClient.trackEvent(ProofEvents.CONNECTION_UUID_GENERATED, { connection_uuid: connectionUuid, }); @@ -551,11 +581,11 @@ export const useProvingStore = create((set, get) => { uuid: connectionUuid, }, }; - trackEvent(ProofEvents.WS_HELLO_SENT); + selfClient.trackEvent(ProofEvents.WS_HELLO_SENT); ws.send(JSON.stringify(helloBody)); }, - _handleWsError: (error: Event) => { + _handleWsError: (error: Event, selfClient: SelfClient) => { console.error('TEE WebSocket error event:', error); if (!actor) { return; @@ -564,11 +594,12 @@ export const useProvingStore = create((set, get) => { new MessageEvent('error', { data: JSON.stringify({ error: 'WebSocket connection error' }), }), + selfClient, ); }, - _handleWsClose: (event: CloseEvent) => { - trackEvent(ProofEvents.TEE_WS_CLOSED, { + _handleWsClose: (event: CloseEvent, selfClient: SelfClient) => { + selfClient.trackEvent(ProofEvents.TEE_WS_CLOSED, { code: event.code, reason: event.reason, }); @@ -588,6 +619,7 @@ export const useProvingStore = create((set, get) => { new MessageEvent('error', { data: JSON.stringify({ error: 'WebSocket closed unexpectedly' }), }), + selfClient, ); } if (get().wsConnection) { @@ -600,8 +632,8 @@ export const useProvingStore = create((set, get) => { circuitType: 'dsc' | 'disclose' | 'register', userConfirmed: boolean = false, ) => { - trackEvent(ProofEvents.PROVING_INIT); - get()._closeConnections(); + selfClient.trackEvent(ProofEvents.PROVING_INIT); + get()._closeConnections(selfClient); if (actor) { try { @@ -624,28 +656,29 @@ export const useProvingStore = create((set, get) => { circuitType, endpointType: null, env: null, - selfClient, }); actor = createActor(provingMachine); setupActorSubscriptions(actor, selfClient); actor.start(); - trackEvent(ProofEvents.DOCUMENT_LOAD_STARTED); + selfClient.trackEvent(ProofEvents.DOCUMENT_LOAD_STARTED); const selectedDocument = await loadSelectedDocument(selfClient); if (!selectedDocument) { console.error('No document found for proving'); - trackEvent(PassportEvents.PASSPORT_DATA_NOT_FOUND, { stage: 'init' }); + selfClient.trackEvent(PassportEvents.PASSPORT_DATA_NOT_FOUND, { + stage: 'init', + }); actor!.send({ type: 'PASSPORT_DATA_NOT_FOUND' }); return; } const { data: passportData } = selectedDocument; - const secret = await get().selfClient?.getPrivateKey(); + const secret = await selfClient.getPrivateKey(); if (!secret) { console.error('Could not load secret'); - trackEvent(ProofEvents.LOAD_SECRET_FAILED); + selfClient.trackEvent(ProofEvents.LOAD_SECRET_FAILED); actor!.send({ type: 'ERROR' }); return; } @@ -656,12 +689,12 @@ export const useProvingStore = create((set, get) => { set({ passportData, secret, env }); set({ circuitType }); actor.send({ type: 'FETCH_DATA' }); - trackEvent(ProofEvents.FETCH_DATA_STARTED); + selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); }, - startFetchingData: async () => { + startFetchingData: async (selfClient: SelfClient) => { _checkActorInitialized(actor); - trackEvent(ProofEvents.FETCH_DATA_STARTED); + selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); try { const { passportData, env } = get(); if (!passportData) { @@ -669,7 +702,7 @@ export const useProvingStore = create((set, get) => { } if (!passportData?.dsc_parsed) { console.error('Missing parsed DSC in passport data'); - trackEvent(ProofEvents.FETCH_DATA_FAILED, { + selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { message: 'Missing parsed DSC in passport data', }); actor!.send({ type: 'FETCH_ERROR' }); @@ -681,11 +714,11 @@ export const useProvingStore = create((set, get) => { [ document ].fetch_all(env!, (passportData as PassportData).dsc_parsed!.authorityKeyIdentifier); - trackEvent(ProofEvents.FETCH_DATA_SUCCESS); + selfClient.trackEvent(ProofEvents.FETCH_DATA_SUCCESS); actor!.send({ type: 'FETCH_SUCCESS' }); } catch (error) { console.error('Error fetching data:', error); - trackEvent(ProofEvents.FETCH_DATA_FAILED, { + selfClient.trackEvent(ProofEvents.FETCH_DATA_FAILED, { message: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'FETCH_ERROR' }); @@ -695,7 +728,7 @@ export const useProvingStore = create((set, get) => { validatingDocument: async (selfClient: SelfClient) => { _checkActorInitialized(actor); // TODO: for the disclosure, we could check that the selfApp is a valid one. - trackEvent(ProofEvents.VALIDATION_STARTED); + selfClient.trackEvent(ProofEvents.VALIDATION_STARTED); try { const { passportData, secret, circuitType } = get(); if (!passportData) { @@ -711,7 +744,7 @@ export const useProvingStore = create((set, get) => { isSupported.status, isSupported.details, ); - trackEvent(PassportEvents.UNSUPPORTED_PASSPORT, { + selfClient.trackEvent(PassportEvents.UNSUPPORTED_PASSPORT, { status: isSupported.status, details: isSupported.details, }); @@ -732,7 +765,7 @@ export const useProvingStore = create((set, get) => { getCommitmentTree, ); if (isRegisteredWithLocalCSCA) { - trackEvent(ProofEvents.VALIDATION_SUCCESS); + selfClient.trackEvent(ProofEvents.VALIDATION_SUCCESS); actor!.send({ type: 'VALIDATION_SUCCESS' }); return; } else { @@ -770,7 +803,7 @@ export const useProvingStore = create((set, get) => { } })(); - trackEvent(ProofEvents.ALREADY_REGISTERED); + selfClient.trackEvent(ProofEvents.ALREADY_REGISTERED); actor!.send({ type: 'ALREADY_REGISTERED' }); return; } @@ -779,7 +812,7 @@ export const useProvingStore = create((set, get) => { console.warn( 'Passport is nullified, but not registered with this secret. Navigating to AccountRecoveryChoice', ); - trackEvent(ProofEvents.PASSPORT_NULLIFIER_ONCHAIN); + selfClient.trackEvent(ProofEvents.PASSPORT_NULLIFIER_ONCHAIN); actor!.send({ type: 'ACCOUNT_RECOVERY_CHOICE' }); return; } @@ -789,22 +822,22 @@ export const useProvingStore = create((set, get) => { useProtocolStore.getState()[document].dsc_tree, ); if (isDscRegistered) { - trackEvent(ProofEvents.DSC_IN_TREE); + selfClient.trackEvent(ProofEvents.DSC_IN_TREE); set({ circuitType: 'register' }); } - trackEvent(ProofEvents.VALIDATION_SUCCESS); + selfClient.trackEvent(ProofEvents.VALIDATION_SUCCESS); actor!.send({ type: 'VALIDATION_SUCCESS' }); } } catch (error) { console.error('Error validating passport:', error); - trackEvent(ProofEvents.VALIDATION_FAILED, { + selfClient.trackEvent(ProofEvents.VALIDATION_FAILED, { message: error instanceof Error ? error.message : String(error), }); actor!.send({ type: 'VALIDATION_ERROR' }); } }, - initTeeConnection: async (): Promise => { + initTeeConnection: async (selfClient: SelfClient): Promise => { const { passportData } = get(); if (!passportData) { throw new Error('PassportData is not available'); @@ -850,26 +883,36 @@ export const useProvingStore = create((set, get) => { throw new Error('No WebSocket URL available for TEE connection'); } - get()._closeConnections(); - trackEvent(ProofEvents.TEE_CONN_STARTED); + get()._closeConnections(selfClient); + selfClient.trackEvent(ProofEvents.TEE_CONN_STARTED); return new Promise(resolve => { const ws = new WebSocket(wsRpcUrl); - set({ wsConnection: ws }); const handleConnectSuccess = () => { - trackEvent(ProofEvents.TEE_CONN_SUCCESS); + selfClient.trackEvent(ProofEvents.TEE_CONN_SUCCESS); resolve(true); }; const handleConnectError = (msg: string = 'connect_error') => { - trackEvent(ProofEvents.TEE_CONN_FAILED, { message: msg }); + selfClient.trackEvent(ProofEvents.TEE_CONN_FAILED, { message: msg }); resolve(false); }; - ws.addEventListener('message', get()._handleWebSocketMessage); - ws.addEventListener('open', get()._handleWsOpen); - ws.addEventListener('error', get()._handleWsError); - ws.addEventListener('close', get()._handleWsClose); + // Create stable handler functions + const wsHandlers: WsHandlers = { + message: (event: MessageEvent) => + get()._handleWebSocketMessage(event, selfClient), + open: () => get()._handleWsOpen(selfClient), + error: (error: Event) => get()._handleWsError(error, selfClient), + close: (event: CloseEvent) => get()._handleWsClose(event, selfClient), + }; + + set({ wsConnection: ws, wsHandlers }); + + ws.addEventListener('message', wsHandlers.message); + ws.addEventListener('open', wsHandlers.open); + ws.addEventListener('error', wsHandlers.error); + ws.addEventListener('close', wsHandlers.close); if (!actor) { return; @@ -886,7 +929,7 @@ export const useProvingStore = create((set, get) => { }); }, - startProving: async () => { + startProving: async (selfClient: SelfClient) => { _checkActorInitialized(actor); const { wsConnection, sharedKey, passportData, secret, uuid, fcmToken } = get(); @@ -899,7 +942,7 @@ export const useProvingStore = create((set, get) => { console.error( 'Cannot start proving: Missing wsConnection, sharedKey, passportData, secret, or uuid.', ); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -915,27 +958,27 @@ export const useProvingStore = create((set, get) => { registerDeviceToken, } = require('@/utils/notifications/notificationService'); const isMockPassport = passportData?.mock; - trackEvent(ProofEvents.DEVICE_TOKEN_REG_STARTED); + selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_STARTED); await registerDeviceToken(uuid, fcmToken, isMockPassport); - trackEvent(ProofEvents.DEVICE_TOKEN_REG_SUCCESS); + selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_SUCCESS); } catch (error) { console.error('Error registering device token:', error); - trackEvent(ProofEvents.DEVICE_TOKEN_REG_FAILED, { + selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_FAILED, { message: error instanceof Error ? error.message : String(error), }); // Continue with the proving process even if token registration fails } } - trackEvent(ProofEvents.PAYLOAD_GEN_STARTED); - const submitBody = await get()._generatePayload(); + selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_STARTED); + const submitBody = await get()._generatePayload(selfClient); wsConnection.send(JSON.stringify(submitBody)); - trackEvent(ProofEvents.PAYLOAD_SENT); - trackEvent(ProofEvents.PROVING_PROCESS_STARTED); + selfClient.trackEvent(ProofEvents.PAYLOAD_SENT); + selfClient.trackEvent(ProofEvents.PROVING_PROCESS_STARTED); actor!.send({ type: 'START_PROVING' }); } catch (error) { console.error('Error during startProving preparation/send:', error); - trackEvent(ProofEvents.PROOF_FAILED, { + selfClient.trackEvent(ProofEvents.PROOF_FAILED, { circuitType: get().circuitType, error: get().error_code ?? 'unknown', }); @@ -943,43 +986,43 @@ export const useProvingStore = create((set, get) => { } }, - setUserConfirmed: () => { + setUserConfirmed: (selfClient: SelfClient) => { set({ userConfirmed: true }); - trackEvent(ProofEvents.USER_CONFIRMED); + selfClient.trackEvent(ProofEvents.USER_CONFIRMED); if (get().currentState === 'ready_to_prove') { - get().startProving(); + get().startProving(selfClient); } }, postProving: (selfClient: SelfClient) => { _checkActorInitialized(actor); const { circuitType } = get(); - trackEvent(ProofEvents.POST_PROVING_STARTED); + selfClient.trackEvent(ProofEvents.POST_PROVING_STARTED); if (circuitType === 'dsc') { setTimeout(() => { - trackEvent(ProofEvents.POST_PROVING_CHAIN_STEP, { + selfClient.trackEvent(ProofEvents.POST_PROVING_CHAIN_STEP, { from: 'dsc', to: 'register', }); get().init(selfClient, 'register', true); }, 1500); } else if (circuitType === 'register') { - trackEvent(ProofEvents.POST_PROVING_COMPLETED); + selfClient.trackEvent(ProofEvents.POST_PROVING_COMPLETED); actor!.send({ type: 'COMPLETED' }); } else if (circuitType === 'disclose') { - trackEvent(ProofEvents.POST_PROVING_COMPLETED); + selfClient.trackEvent(ProofEvents.POST_PROVING_COMPLETED); actor!.send({ type: 'COMPLETED' }); } }, - _closeConnections: () => { - const ws = get().wsConnection; - if (ws) { + _closeConnections: (selfClient: SelfClient) => { + const { wsConnection: ws, wsHandlers } = get(); + if (ws && wsHandlers) { try { - ws.removeEventListener('message', get()._handleWebSocketMessage); - ws.removeEventListener('open', get()._handleWsOpen); - ws.removeEventListener('error', get()._handleWsError); - ws.removeEventListener('close', get()._handleWsClose); + ws.removeEventListener('message', wsHandlers.message); + ws.removeEventListener('open', wsHandlers.open); + ws.removeEventListener('error', wsHandlers.error); + ws.removeEventListener('close', wsHandlers.close); ws.close(); } catch (error) { console.error( @@ -987,7 +1030,7 @@ export const useProvingStore = create((set, get) => { error, ); } - set({ wsConnection: null }); + set({ wsConnection: null, wsHandlers: null }); } const socket = get().socketConnection; @@ -1004,7 +1047,7 @@ export const useProvingStore = create((set, get) => { }); }, - _generatePayload: async () => { + _generatePayload: async (selfClient: SelfClient) => { const { circuitType, passportData, secret, uuid, sharedKey, env } = get(); if (!passportData) { throw new Error('PassportData is not available'); @@ -1082,8 +1125,8 @@ export const useProvingStore = create((set, get) => { forgeKey, ); - trackEvent(ProofEvents.PAYLOAD_GEN_COMPLETED); - trackEvent(ProofEvents.PAYLOAD_ENCRYPTED); + selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_COMPLETED); + selfClient.trackEvent(ProofEvents.PAYLOAD_ENCRYPTED); // Persist endpointType for later Socket.IO connection set({ endpointType: endpointType as EndpointType }); diff --git a/app/tests/utils/deeplinks.test.ts b/app/tests/utils/deeplinks.test.ts index b40b58d28..23334bb68 100644 --- a/app/tests/utils/deeplinks.test.ts +++ b/app/tests/utils/deeplinks.test.ts @@ -13,7 +13,7 @@ jest.mock('@/navigation', () => ({ })); const mockSelfAppStore = { useSelfAppStore: { getState: jest.fn() } }; -jest.mock('@/stores/selfAppStore', () => mockSelfAppStore); +jest.mock('@selfxyz/mobile-sdk-alpha/stores', () => mockSelfAppStore); const mockUserStore = { default: { getState: jest.fn() } }; jest.mock('@/stores/userStore', () => ({ diff --git a/app/tests/utils/proving/provingMachine.generatePayload.test.ts b/app/tests/utils/proving/provingMachine.generatePayload.test.ts index cd0d52f62..c11084e7c 100644 --- a/app/tests/utils/proving/provingMachine.generatePayload.test.ts +++ b/app/tests/utils/proving/provingMachine.generatePayload.test.ts @@ -2,9 +2,12 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import { useProtocolStore } from '@selfxyz/mobile-sdk-alpha/stores'; +import type { SelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { + useProtocolStore, + useSelfAppStore, +} from '@selfxyz/mobile-sdk-alpha/stores'; -import { useSelfAppStore } from '@/stores/selfAppStore'; import { useProvingStore } from '@/utils/proving/provingMachine'; jest.mock('xstate', () => { @@ -17,32 +20,6 @@ jest.mock('@/utils/analytics', () => () => ({ trackEvent: jest.fn(), })); -// Mock the proving inputs to return predictable data -jest.mock('@/utils/proving/provingInputs', () => ({ - generateTEEInputsDisclose: jest.fn(() => ({ - inputs: { s: 1 }, - circuitName: 'vc_and_disclose', - endpointType: 'https', - endpoint: 'https://dis', - })), -})); - -// Mock the common register/dsc inputs where provingMachine actually imports from -jest.mock('@selfxyz/common/utils/circuits/registerInputs', () => ({ - generateTEEInputsRegister: jest.fn(() => ({ - inputs: { r: 1 }, - circuitName: 'reg', - endpointType: 'celo', - endpoint: 'https://reg', - })), - generateTEEInputsDSC: jest.fn(() => ({ - inputs: { d: 1 }, - circuitName: 'dsc', - endpointType: 'celo', - endpoint: 'https://dsc', - })), -})); - // Mock the proving utils jest.mock('@selfxyz/common/utils/proving', () => { const actual = jest.requireActual('@selfxyz/common/utils/proving') as any; @@ -54,6 +31,52 @@ jest.mock('@selfxyz/common/utils/proving', () => { cipher_text: [1], auth_tag: [2], })), + generateTEEInputsRegister: jest.fn(() => ({ + inputs: { r: 1 }, + circuitName: 'reg', + endpointType: 'celo', + endpoint: 'https://reg', + })), + generateTEEInputsDSC: jest.fn(() => ({ + inputs: { d: 1 }, + circuitName: 'dsc', + endpointType: 'celo', + endpoint: 'https://dsc', + })), + generateTEEInputsDisclose: jest.fn(() => ({ + inputs: { s: 1 }, + circuitName: 'vc_and_disclose', + endpointType: 'https', + endpoint: 'https://dis', + })), + }; +}); + +// Mock the proving utils +jest.mock('@selfxyz/common/utils/circuits/registerInputs', () => { + const actual = jest.requireActual( + '@selfxyz/common/utils/circuits/registerInputs', + ) as any; + return { + ...actual, + generateTEEInputsRegister: jest.fn(() => ({ + inputs: { r: 1 }, + circuitName: 'reg', + endpointType: 'celo', + endpoint: 'https://reg', + })), + generateTEEInputsDSC: jest.fn(() => ({ + inputs: { d: 1 }, + circuitName: 'dsc', + endpointType: 'celo', + endpoint: 'https://dsc', + })), + generateTEEInputsDiscloseStateless: jest.fn(() => ({ + inputs: { s: 1 }, + circuitName: 'vc_and_disclose', + endpointType: 'https', + endpoint: 'https://dis', + })), }; }); @@ -89,13 +112,17 @@ const { getPayload, encryptAES256GCM, } = require('@selfxyz/common/utils/proving'); -const { generateTEEInputsDisclose } = require('@/utils/proving/provingInputs'); + const { generateTEEInputsRegister, generateTEEInputsDSC, + generateTEEInputsDiscloseStateless, } = require('@selfxyz/common/utils/circuits/registerInputs'); describe('_generatePayload', () => { + const selfClient: SelfClient = { + trackEvent: jest.fn(), + } as unknown as SelfClient; beforeEach(() => { jest.clearAllMocks(); useProvingStore.setState({ @@ -203,7 +230,9 @@ describe('_generatePayload', () => { it('register circuit', async () => { useProvingStore.setState({ circuitType: 'register' }); - const payload = await useProvingStore.getState()._generatePayload(); + const payload = await useProvingStore + .getState() + ._generatePayload(selfClient); expect(generateTEEInputsRegister).toHaveBeenCalled(); expect(getPayload).toHaveBeenCalled(); expect(encryptAES256GCM).toHaveBeenCalled(); @@ -218,7 +247,9 @@ describe('_generatePayload', () => { it('dsc circuit', async () => { useProvingStore.setState({ circuitType: 'dsc' }); - const payload = await useProvingStore.getState()._generatePayload(); + const payload = await useProvingStore + .getState() + ._generatePayload(selfClient); expect(generateTEEInputsDSC).toHaveBeenCalled(); expect(useProvingStore.getState().endpointType).toBe('celo'); expect(payload.params.uuid).toBe('123'); @@ -226,8 +257,10 @@ describe('_generatePayload', () => { it('disclose circuit', async () => { useProvingStore.setState({ circuitType: 'disclose' }); - const payload = await useProvingStore.getState()._generatePayload(); - expect(generateTEEInputsDisclose).toHaveBeenCalled(); + const payload = await useProvingStore + .getState() + ._generatePayload(selfClient); + expect(generateTEEInputsDiscloseStateless).toHaveBeenCalled(); expect(useProvingStore.getState().endpointType).toBe('https'); expect(payload.params.uuid).toBe('123'); }); diff --git a/app/tests/utils/proving/provingMachine.startFetchingData.test.ts b/app/tests/utils/proving/provingMachine.startFetchingData.test.ts index 159456b9a..6dd4c4dac 100644 --- a/app/tests/utils/proving/provingMachine.startFetchingData.test.ts +++ b/app/tests/utils/proving/provingMachine.startFetchingData.test.ts @@ -55,6 +55,7 @@ jest.mock('@selfxyz/mobile-sdk-alpha', () => { }); describe('startFetchingData', () => { + let mockSelfClient: SelfClient; beforeEach(async () => { jest.clearAllMocks(); const { @@ -71,8 +72,9 @@ describe('startFetchingData', () => { unsafe_getPrivateKey.mockResolvedValue('secret'); // Create mock selfClient - const mockSelfClient = { + mockSelfClient = { getPrivateKey: jest.fn().mockResolvedValue('mock-secret'), + trackEvent: jest.fn(), } as unknown as SelfClient; useProtocolStore.setState({ @@ -91,7 +93,7 @@ describe('startFetchingData', () => { }); it('emits FETCH_ERROR when dsc_parsed is missing', async () => { - await useProvingStore.getState().startFetchingData(); + await useProvingStore.getState().startFetchingData(mockSelfClient); expect( useProtocolStore.getState().passport.fetch_all, diff --git a/app/tests/utils/proving/provingMachine.test.ts b/app/tests/utils/proving/provingMachine.test.ts index 4c38c03bd..76afb7023 100644 --- a/app/tests/utils/proving/provingMachine.test.ts +++ b/app/tests/utils/proving/provingMachine.test.ts @@ -36,8 +36,8 @@ describe('provingMachine registration completion', () => { useProvingStore(state => state.init), ); const emitMock = jest.fn(); - const selfClient = { + trackEvent: jest.fn(), emit: emitMock, } as unknown as SelfClient; diff --git a/app/vite.config.ts b/app/vite.config.ts index 73f4b6ec1..243279557 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -166,7 +166,6 @@ export default defineConfig({ ], 'screens-prove-utils': [ './src/utils/proving/index.ts', - './src/utils/proving/provingInputs.ts', './src/utils/proving/loadingScreenStateText.ts', ], diff --git a/common/index.ts b/common/index.ts index e24d5620b..add454242 100644 --- a/common/index.ts +++ b/common/index.ts @@ -55,10 +55,14 @@ export { SelfAppBuilder, bigIntToString, brutforceSignatureAlgorithmDsc, + buildSMT, + calculateUserIdentifierHash, findStartPubKeyIndex, formatEndpoint, formatMrz, genAndInitMockPassportData, + genMockIdDoc, + genMockIdDocAndInitDataParsing, generateCircuitInputsDSC, generateCircuitInputsRegister, generateCircuitInputsVCandDisclose, @@ -69,28 +73,17 @@ export { getLeafCscaTree, getLeafDscTree, getSKIPEM, + getSolidityPackedUserContextData, getUniversalLink, hashEndpointWithScope, initElliptic, initPassportDataParsing, parseCertificateSimple, parseDscCertificateData, - genMockIdDoc, - genMockIdDocAndInitDataParsing, - buildSMT, - calculateUserIdentifierHash, - getSolidityPackedUserContextData, stringToBigInt, } from './src/utils/index.js'; -export { - prepareAadhaarRegisterTestData, - prepareAadhaarDiscloseTestData, - prepareAadhaarRegisterData, -} from './src/utils/aadhaar/mockData.js'; -export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; export { createSelector } from './src/utils/aadhaar/constants.js'; - // Hash utilities export { customHasher, @@ -99,3 +92,11 @@ export { hash, packBytesAndPoseidon, } from './src/utils/hash.js'; + +export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js'; + +export { + prepareAadhaarDiscloseTestData, + prepareAadhaarRegisterData, + prepareAadhaarRegisterTestData, +} from './src/utils/aadhaar/mockData.js'; diff --git a/common/package.json b/common/package.json index e5c5ac99b..01dc5c8c0 100644 --- a/common/package.json +++ b/common/package.json @@ -647,8 +647,8 @@ "build:watch": "tsup --watch", "format": "prettier --write .", "lint": "prettier --check .", - "lint:imports": "eslint . --fix", - "lint:imports:check": "eslint .", + "lint:imports": "yarn eslint --fix .", + "lint:imports:check": "yarn eslint .", "nice": "yarn format && yarn lint:imports", "nice:check": "yarn lint && yarn lint:imports:check", "prepublishOnly": "yarn build", diff --git a/common/src/constants/constants.ts b/common/src/constants/constants.ts index 34483cabb..ae6a93d93 100644 --- a/common/src/constants/constants.ts +++ b/common/src/constants/constants.ts @@ -1,7 +1,9 @@ export type Country3LetterCode = keyof typeof countryCodes; export type document_type = 'passport' | 'id_card'; export type hashAlgosTypes = 'sha512' | 'sha384' | 'sha256' | 'sha224' | 'sha1'; +export const AADHAAR_ATTESTATION_ID = '3'; export const API_URL = 'https://api.self.xyz'; + export const API_URL_STAGING = 'https://api.staging.self.xyz'; export const CHAIN_NAME = 'celo'; @@ -42,8 +44,6 @@ export const CSCA_TREE_URL_STAGING = 'https://tree.staging.self.xyz/csca'; export const CSCA_TREE_URL_STAGING_ID_CARD = 'https://tree.staging.self.xyz/csca-id'; -export const AADHAAR_ATTESTATION_ID = '3'; - // we make it global here because passing it to generateCircuitInputsRegister caused trouble export const DEFAULT_MAJORITY = '18'; diff --git a/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts b/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts index 9681943d9..6722939fd 100644 --- a/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts +++ b/common/src/utils/aadhaar/build_aadhaar_ofac_smt.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; + import { buildAadhaarSMT } from '../trees.js'; async function build_aadhaar_ofac_smt() { diff --git a/common/src/utils/aadhaar/constants.ts b/common/src/utils/aadhaar/constants.ts index 5a7d7b07a..cb3d1cbde 100644 --- a/common/src/utils/aadhaar/constants.ts +++ b/common/src/utils/aadhaar/constants.ts @@ -1,3 +1,4 @@ +/* eslint-disable sort-exports/sort-exports */ export const MAX_FIELD_BYTE_SIZE = 31; export const NAME_MAX_LENGTH = 2 * MAX_FIELD_BYTE_SIZE; // 62 bytes export const TOTAL_REVEAL_DATA_LENGTH = 119; diff --git a/common/src/utils/aadhaar/mockData.ts b/common/src/utils/aadhaar/mockData.ts index b438a5dbc..5ba5eff5e 100644 --- a/common/src/utils/aadhaar/mockData.ts +++ b/common/src/utils/aadhaar/mockData.ts @@ -1,29 +1,34 @@ -import { calculateAge, generateTestData, testCustomData } from './utils.js'; -import { - convertBigIntToByteArray, - decompressByteArray, - splitToWords, - extractPhoto, -} from '@anon-aadhaar/core'; -import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; -import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; -import { testQRData } from './assets/dataInput.js'; -import { stringToAsciiArray } from './utils.js'; -import { packBytesAndPoseidon } from '../hash.js'; -import { poseidon5 } from 'poseidon-lite'; import forge from 'node-forge'; -import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; -import { SMT } from '@openpassport/zk-kit-smt'; +import { poseidon5 } from 'poseidon-lite'; + +import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js'; import { findIndexInTree, formatInput } from '../circuits/generateInputs.js'; +import { packBytesAndPoseidon } from '../hash.js'; import { generateMerkleProof, generateSMTProof, getNameDobLeafAadhaar, getNameYobLeafAahaar, } from '../trees.js'; +import { testQRData } from './assets/dataInput.js'; +import { + calculateAge, + extractQRDataFields, + generateTestData, + stringToAsciiArray, + testCustomData, +} from './utils.js'; -import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js'; -import { extractQRDataFields } from './utils.js'; +import { + convertBigIntToByteArray, + decompressByteArray, + extractPhoto, + splitToWords, +} from '@anon-aadhaar/core'; +import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; +import { SMT } from '@openpassport/zk-kit-smt'; +import { bufferToHex, Uint8ArrayToCharArray } from '@zk-email/helpers/dist/binary-format.js'; +import { sha256Pad } from '@zk-email/helpers/dist/sha-utils.js'; // Helper function to compute padded name function computePaddedName(name: string): number[] { @@ -163,19 +168,25 @@ function processQRDataSimple(qrData: string) { }; } -export function prepareAadhaarRegisterTestData( - privKeyPem: string, - pubkeyPem: string, +export function prepareAadhaarDiscloseTestData( + privateKeyPem: string, + merkletree: LeanIMT, + nameAndDob_smt: SMT, + nameAndYob_smt: SMT, + scope: string, secret: string, + user_identifier: string, + selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string + timestamp?: string, + updateTree?: boolean ) { const sharedData = processQRData( - privKeyPem, + privateKeyPem, name, dateOfBirth, gender, @@ -184,36 +195,14 @@ export function prepareAadhaarRegisterTestData( timestamp ); - const delimiterIndices: number[] = []; - for (let i = 0; i < sharedData.qrDataPadded.length; i++) { - if (sharedData.qrDataPadded[i] === 255) { - delimiterIndices.push(i); - } - if (delimiterIndices.length === 18) { - break; - } - } - let photoEOI = 0; - for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { - if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { - photoEOI = i + 1; - } - } - if (photoEOI === 0) { - throw new Error('Photo EOI not found'); - } - - const signatureBytes = sharedData.decodedData.slice( - sharedData.decodedData.length - 256, - sharedData.decodedData.length + const { age, currentYear, currentMonth, currentDay } = calculateAge( + sharedData.extractedFields.dob, + sharedData.extractedFields.mob, + sharedData.extractedFields.yob ); - const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); - - const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); - - const modulusHex = publicKey.n.toString(16); - const pubKey = BigInt('0x' + modulusHex); + const uppercaseName = computeUppercasePaddedName(sharedData.extractedFields.name); + const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -224,14 +213,74 @@ export function prepareAadhaarRegisterTestData( BigInt(sharedData.photoHash) ); + const paddedName = computePaddedName(sharedData.extractedFields.name); + + if (updateTree) { + merkletree.insert(BigInt(commitment)); + } + + const index = findIndexInTree(merkletree, BigInt(commitment)); + const { + siblings, + path: merkle_path, + leaf_depth, + } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); + + const namedob_leaf = getNameDobLeafAadhaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob, + sharedData.extractedFields.mob, + sharedData.extractedFields.dob + ); + const nameyob_leaf = getNameYobLeafAahaar( + sharedData.extractedFields.name, + sharedData.extractedFields.yob + ); + + const { + root: ofac_name_dob_smt_root, + closestleaf: ofac_name_dob_smt_leaf_key, + siblings: ofac_name_dob_smt_siblings, + } = generateSMTProof(nameAndDob_smt, namedob_leaf); + + const { + root: ofac_name_yob_smt_root, + closestleaf: ofac_name_yob_smt_leaf_key, + siblings: ofac_name_yob_smt_siblings, + } = generateSMTProof(nameAndYob_smt, nameyob_leaf); + const inputs = { - qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), - qrDataPaddedLength: sharedData.qrDataPaddedLen, - delimiterIndices: delimiterIndices, - signature: splitToWords(signature, BigInt(121), BigInt(17)), - pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), + attestation_id: '3', secret: secret, - photoEOI: photoEOI, + qrDataHash: BigInt(sharedData.qrHash).toString(), + gender: genderAscii.toString(), + yob: stringToAsciiArray(sharedData.extractedFields.yob), + mob: stringToAsciiArray(sharedData.extractedFields.mob), + dob: stringToAsciiArray(sharedData.extractedFields.dob), + name: formatInput(paddedName), + aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), + pincode: stringToAsciiArray(sharedData.extractedFields.pincode), + state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), + ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), + photoHash: formatInput(BigInt(sharedData.photoHash)), + merkle_root: formatInput(BigInt(merkletree.root)), + leaf_depth: formatInput(leaf_depth), + path: formatInput(merkle_path), + siblings: formatInput(siblings), + ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), + ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), + ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), + ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), + ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), + ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), + selector, + minimumAge: formatInput(age - 2), + currentYear: formatInput(currentYear), + currentMonth: formatInput(currentMonth), + currentDay: formatInput(currentDay), + scope: formatInput(BigInt(scope)), + user_identifier: formatInput(BigInt(user_identifier)), + forbidden_countries_list: [...Array(120)].map((x) => '0'), }; return { @@ -323,25 +372,19 @@ export async function prepareAadhaarRegisterData(qrData: string, secret: string, }; } -export function prepareAadhaarDiscloseTestData( - privateKeyPem: string, - merkletree: LeanIMT, - nameAndDob_smt: SMT, - nameAndYob_smt: SMT, - scope: string, +export function prepareAadhaarRegisterTestData( + privKeyPem: string, + pubkeyPem: string, secret: string, - user_identifier: string, - selector: string, name?: string, dateOfBirth?: string, gender?: string, pincode?: string, state?: string, - timestamp?: string, - updateTree?: boolean + timestamp?: string ) { const sharedData = processQRData( - privateKeyPem, + privKeyPem, name, dateOfBirth, gender, @@ -350,14 +393,36 @@ export function prepareAadhaarDiscloseTestData( timestamp ); - const { age, currentYear, currentMonth, currentDay } = calculateAge( - sharedData.extractedFields.dob, - sharedData.extractedFields.mob, - sharedData.extractedFields.yob + const delimiterIndices: number[] = []; + for (let i = 0; i < sharedData.qrDataPadded.length; i++) { + if (sharedData.qrDataPadded[i] === 255) { + delimiterIndices.push(i); + } + if (delimiterIndices.length === 18) { + break; + } + } + let photoEOI = 0; + for (let i = delimiterIndices[17]; i < sharedData.qrDataPadded.length - 1; i++) { + if (sharedData.qrDataPadded[i + 1] === 217 && sharedData.qrDataPadded[i] === 255) { + photoEOI = i + 1; + } + } + if (photoEOI === 0) { + throw new Error('Photo EOI not found'); + } + + const signatureBytes = sharedData.decodedData.slice( + sharedData.decodedData.length - 256, + sharedData.decodedData.length ); + const signature = BigInt('0x' + bufferToHex(Buffer.from(signatureBytes)).toString()); + + const publicKey = forge.pki.publicKeyFromPem(pubkeyPem); + + const modulusHex = publicKey.n.toString(16); + const pubKey = BigInt('0x' + modulusHex); - const uppercaseName = computeUppercasePaddedName(sharedData.extractedFields.name); - const genderAscii = stringToAsciiArray(sharedData.extractedFields.gender)[0]; const nullifier = nullifierHash(sharedData.extractedFields); const packedCommitment = computePackedCommitment(sharedData.extractedFields); const commitment = computeCommitment( @@ -368,74 +433,14 @@ export function prepareAadhaarDiscloseTestData( BigInt(sharedData.photoHash) ); - const paddedName = computePaddedName(sharedData.extractedFields.name); - - if (updateTree) { - merkletree.insert(BigInt(commitment)); - } - - const index = findIndexInTree(merkletree, BigInt(commitment)); - const { - siblings, - path: merkle_path, - leaf_depth, - } = generateMerkleProof(merkletree, index, COMMITMENT_TREE_DEPTH); - - const namedob_leaf = getNameDobLeafAadhaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob, - sharedData.extractedFields.mob, - sharedData.extractedFields.dob - ); - const nameyob_leaf = getNameYobLeafAahaar( - sharedData.extractedFields.name, - sharedData.extractedFields.yob - ); - - const { - root: ofac_name_dob_smt_root, - closestleaf: ofac_name_dob_smt_leaf_key, - siblings: ofac_name_dob_smt_siblings, - } = generateSMTProof(nameAndDob_smt, namedob_leaf); - - const { - root: ofac_name_yob_smt_root, - closestleaf: ofac_name_yob_smt_leaf_key, - siblings: ofac_name_yob_smt_siblings, - } = generateSMTProof(nameAndYob_smt, nameyob_leaf); - const inputs = { - attestation_id: '3', + qrDataPadded: Uint8ArrayToCharArray(sharedData.qrDataPadded), + qrDataPaddedLength: sharedData.qrDataPaddedLen, + delimiterIndices: delimiterIndices, + signature: splitToWords(signature, BigInt(121), BigInt(17)), + pubKey: splitToWords(pubKey, BigInt(121), BigInt(17)), secret: secret, - qrDataHash: BigInt(sharedData.qrHash).toString(), - gender: genderAscii.toString(), - yob: stringToAsciiArray(sharedData.extractedFields.yob), - mob: stringToAsciiArray(sharedData.extractedFields.mob), - dob: stringToAsciiArray(sharedData.extractedFields.dob), - name: formatInput(paddedName), - aadhaar_last_4digits: stringToAsciiArray(sharedData.extractedFields.aadhaarLast4Digits), - pincode: stringToAsciiArray(sharedData.extractedFields.pincode), - state: stringToAsciiArray(sharedData.extractedFields.state.padEnd(31, '\0')), - ph_no_last_4digits: stringToAsciiArray(sharedData.extractedFields.phoneNoLast4Digits), - photoHash: formatInput(BigInt(sharedData.photoHash)), - merkle_root: formatInput(BigInt(merkletree.root)), - leaf_depth: formatInput(leaf_depth), - path: formatInput(merkle_path), - siblings: formatInput(siblings), - ofac_name_dob_smt_leaf_key: formatInput(BigInt(ofac_name_dob_smt_leaf_key)), - ofac_name_dob_smt_root: formatInput(BigInt(ofac_name_dob_smt_root)), - ofac_name_dob_smt_siblings: formatInput(ofac_name_dob_smt_siblings), - ofac_name_yob_smt_leaf_key: formatInput(BigInt(ofac_name_yob_smt_leaf_key)), - ofac_name_yob_smt_root: formatInput(BigInt(ofac_name_yob_smt_root)), - ofac_name_yob_smt_siblings: formatInput(ofac_name_yob_smt_siblings), - selector, - minimumAge: formatInput(age - 2), - currentYear: formatInput(currentYear), - currentMonth: formatInput(currentMonth), - currentDay: formatInput(currentDay), - scope: formatInput(BigInt(scope)), - user_identifier: formatInput(BigInt(user_identifier)), - forbidden_countries_list: [...Array(120)].map((x) => '0'), + photoEOI: photoEOI, }; return { diff --git a/common/src/utils/aadhaar/utils.ts b/common/src/utils/aadhaar/utils.ts index f8284a453..772a450ac 100644 --- a/common/src/utils/aadhaar/utils.ts +++ b/common/src/utils/aadhaar/utils.ts @@ -1,27 +1,40 @@ +import forge from 'node-forge'; + import { convertBigIntToByteArray, decompressByteArray, - returnFullId, - rawDataToCompressedQR, - replaceBytesBetween, - IdFields, extractPhoto, - getRandomBytes, getEndIndex, + getRandomBytes, + IdFields, + rawDataToCompressedQR, + replaceBytesBetween, + returnFullId, } from '@anon-aadhaar/core'; -import forge from 'node-forge'; -export function stringToAsciiArray(str: string) { - return str.split('').map((char) => char.charCodeAt(0)); +export interface ExtractedQRData { + name: string; + yob: string; + mob: string; + dob: string; + gender: string; + pincode: string; + state: string; + aadhaarLast4Digits: string; + phoneNoLast4Digits: string; + timestamp: string; } -// This is the official test data issued by the UIDAI -// In this script we'll change the signed data to emulate the specs of the Aadhaar QR V2 -// and sign the data again with our own certificates. -// data on https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html -// This data is copied from https://github.dev/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/src/helpers/extractor.circom -export const testCustomData = - '2374971804270526477833002468783965837992554564899874087591661303561346432389832047870524302186901344489362368642972767716416349990805756094923115719687656090691368051627957878187788907419297818953295185555346288172578594637886352753543271000481717080003254556962148594350559820352806251787713278744047402230989238559317351232114240089849934148895256488140236015024800731753594740948640957680138566468247224859669467819596919398964809164399637893729212452791889199675715949918925838319591794702333094022248132120531152523331442741730158840977243402215102904932650832502847295644794421419704633765033761284508863534321317394686768650111457751139630853448637215423705157211510636160227953566227527799608082928846103264491539001327407775670834868948113753614112563650255058316849200536533335903554984254814901522086937767458409075617572843449110393213525925388131214952874629655799772119820372255291052673056372346072235458198199995637720424196884145247220163810790179386390283738429482893152518286247124911446073389185062482901364671389605727763080854673156754021728522287806275420847159574631844674460263574901590412679291518508010087116598357407343835408554094619585212373168435612645646129147973594416508676872819776522537778717985070402222824965034768103900739105784663244748432502180989441389718131079445941981681118258324511923246198334046020123727749408128519721102477302359413240175102907322619462289965085963377744024233678337951462006962521823224880199210318367946130004264196899778609815012001799773327514133268825910089483612283510244566484854597156100473055413090101948456959122378865704840756793122956663218517626099291311352417342899623681483097817511136427210593032393600010728324905512596767095096153856032112835755780472808814199620390836980020899858288860556611564167406292139646289142056168261133256777093245980048335918156712295254776487472431445495668303900536289283098315798552328294391152828182614909451410115516297083658174657554955228963550255866282688308751041517464999930825273776417639569977754844191402927594739069037851707477839207593911886893016618794870530622356073909077832279869798641545167528509966656120623184120128052588408742941658045827255866966100249857968956536613250770326334844204927432961924987891433020671754710428050564671868464658436926086493709176888821257183419013229795869757265111599482263223604228286513011751601176504567030118257385997460972803240338899836840030438830725520798480181575861397469056536579877274090338750406459700907704031830137890544492015701251066934352867527112361743047684237105216779177819594030160887368311805926405114938744235859610328064947158936962470654636736991567663705830950312548447653861922078087824048793236971354828540758657075837209006713701763902429652486225300535997260665898927924843608750347193892239342462507130025307878412116604096773706728162016134101751551184021079984480254041743057914746472840768175369369852937574401874295943063507273467384747124843744395375119899278823903202010381949145094804675442110869084589592876721655764753871572233276245590041302887094585204427900634246823674277680009401177473636685542700515621164233992970974893989913447733956146698563285998205950467321954304'; +export const FIELD_POSITIONS = { + REFERENCE_ID: 2, + NAME: 3, + DOB: 4, + GENDER: 5, + PINCODE: 11, + STATE: 13, + PHONE_NO: 17, + PHOTO: 18, +} as const; // Will sign the data with the keys generated for test const signNewTestData = (newSignedData: Uint8Array, privKeyPem: string) => { @@ -45,65 +58,33 @@ const signNewTestData = (newSignedData: Uint8Array, privKeyPem: string) => { } }; -export const generateTestData = ({ - privKeyPem, - data, - dob, - gender, - pincode, - state, - photo, - name, - timestamp, -}: { - privKeyPem: string; - data: string; - dob?: string; - gender?: string; - pincode?: string; - state?: string; - photo?: boolean; - name?: string; - timestamp?: string; -}) => { - const qrDataBytes = convertBigIntToByteArray(BigInt(data)); - const decodedData = decompressByteArray(qrDataBytes); - - // Turning test data V1 into V2 - // Adding the version specifier prefix, - // the last 4 digits of phone number and timestamp to now - const dataToSign = createCustomV2TestData({ - signedData: decodedData.slice(0, decodedData.length - 256), - dob, - pincode, - gender, - state, - photo, - name, - timestamp, - }); +export function calculateAge( + dob: string, + mob: string, + yob: string +): { age: number; currentYear: number; currentMonth: number; currentDay: number } { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11 + const currentDay = currentDate.getDate(); - // Signing the newly generated testData - const signature = signNewTestData(dataToSign, privKeyPem); + const birthYear = parseInt(yob); + const birthMonth = parseInt(mob); + const birthDay = parseInt(dob); - // Reconstructing the whole QR data - const tempData = Buffer.concat([dataToSign, signature]); + let age = currentYear - birthYear; - // Compressing the data to have it in the same format as the QR code - const newCompressedData = rawDataToCompressedQR(tempData); - const newQrData = { - testQRData: newCompressedData.toString(), - ...returnFullId(dataToSign), + if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) { + age--; + } + return { + age, + currentYear, + currentMonth, + currentDay, }; +} - return newQrData; -}; - -// This modify the test data to make it compliant with the secure Aadhaar QR V2 2022 -// - Adds the version specifier at the beginning 'V2' -// - Mocks last 4 digits of phone number '1234' after VTC -// - Refresh timestamp data to now -// - Optionally it can take parameters to change the test data fields (dob, pinCode, gender, state) export const createCustomV2TestData = ({ signedData, dob, @@ -238,99 +219,6 @@ export const createCustomV2TestData = ({ return newData; }; -export function calculateAge( - dob: string, - mob: string, - yob: string -): { age: number; currentYear: number; currentMonth: number; currentDay: number } { - const currentDate = new Date(); - const currentYear = currentDate.getFullYear(); - const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11 - const currentDay = currentDate.getDate(); - - const birthYear = parseInt(yob); - const birthMonth = parseInt(mob); - const birthDay = parseInt(dob); - - let age = currentYear - birthYear; - - if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) { - age--; - } - return { - age, - currentYear, - currentMonth, - currentDay, - }; -} - -export function returnNewDateString(timestamp?: string): string { - const newDate = timestamp ? new Date(+timestamp) : new Date(); - - // Convert the UTC date to IST by adding 5 hours and 30 minutes - const offsetHours = 5; - const offsetMinutes = 30; - newDate.setUTCHours(newDate.getUTCHours() + offsetHours); - newDate.setUTCMinutes(newDate.getUTCMinutes() + offsetMinutes); - - return ( - newDate.getUTCFullYear().toString() + - (newDate.getUTCMonth() + 1).toString().padStart(2, '0') + - newDate.getUTCDate().toString().padStart(2, '0') + - newDate.getUTCHours().toString().padStart(2, '0') + - newDate.getUTCMinutes().toString().padStart(2, '0') + - newDate.getUTCSeconds().toString().padStart(2, '0') + - newDate.getUTCMilliseconds().toString().padStart(3, '0') - ); -} -export const FIELD_POSITIONS = { - REFERENCE_ID: 2, - NAME: 3, - DOB: 4, - GENDER: 5, - PINCODE: 11, - STATE: 13, - PHONE_NO: 17, - PHOTO: 18, -} as const; - -function asciiArrayToString(asciiArray: number[]): string { - return asciiArray - .filter((byte) => byte !== 0) - .map((byte) => String.fromCharCode(byte)) - .join(''); -} - -function extractFieldData( - data: Uint8Array, - delimiterIndices: number[], - position: number -): number[] { - const startIndex = delimiterIndices[position - 1] + 1; - const endIndex = delimiterIndices[position]; - - const fieldData: number[] = []; - for (let i = startIndex; i < endIndex; i++) { - fieldData.push(data[i]); - } - - return fieldData; -} - -export interface ExtractedQRData { - name: string; - yob: string; - mob: string; - dob: string; - gender: string; - pincode: string; - state: string; - aadhaarLast4Digits: string; - phoneNoLast4Digits: string; - timestamp: string; -} - export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRData { let qrDataBytes: Uint8Array; @@ -428,3 +316,117 @@ export function extractQRDataFields(qrData: string | Uint8Array): ExtractedQRDat timestamp, }; } + +export const generateTestData = ({ + privKeyPem, + data, + dob, + gender, + pincode, + state, + photo, + name, + timestamp, +}: { + privKeyPem: string; + data: string; + dob?: string; + gender?: string; + pincode?: string; + state?: string; + photo?: boolean; + name?: string; + timestamp?: string; +}) => { + const qrDataBytes = convertBigIntToByteArray(BigInt(data)); + const decodedData = decompressByteArray(qrDataBytes); + + // Turning test data V1 into V2 + // Adding the version specifier prefix, + // the last 4 digits of phone number and timestamp to now + const dataToSign = createCustomV2TestData({ + signedData: decodedData.slice(0, decodedData.length - 256), + dob, + pincode, + gender, + state, + photo, + name, + timestamp, + }); + + // Signing the newly generated testData + const signature = signNewTestData(dataToSign, privKeyPem); + + // Reconstructing the whole QR data + const tempData = Buffer.concat([dataToSign, signature]); + + // Compressing the data to have it in the same format as the QR code + const newCompressedData = rawDataToCompressedQR(tempData); + const newQrData = { + testQRData: newCompressedData.toString(), + ...returnFullId(dataToSign), + }; + + return newQrData; +}; + +export function returnNewDateString(timestamp?: string): string { + const newDate = timestamp ? new Date(+timestamp) : new Date(); + + // Convert the UTC date to IST by adding 5 hours and 30 minutes + const offsetHours = 5; + const offsetMinutes = 30; + newDate.setUTCHours(newDate.getUTCHours() + offsetHours); + newDate.setUTCMinutes(newDate.getUTCMinutes() + offsetMinutes); + + return ( + newDate.getUTCFullYear().toString() + + (newDate.getUTCMonth() + 1).toString().padStart(2, '0') + + newDate.getUTCDate().toString().padStart(2, '0') + + newDate.getUTCHours().toString().padStart(2, '0') + + newDate.getUTCMinutes().toString().padStart(2, '0') + + newDate.getUTCSeconds().toString().padStart(2, '0') + + newDate.getUTCMilliseconds().toString().padStart(3, '0') + ); +} + +function asciiArrayToString(asciiArray: number[]): string { + return asciiArray + .filter((byte) => byte !== 0) + .map((byte) => String.fromCharCode(byte)) + .join(''); +} + +function extractFieldData( + data: Uint8Array, + delimiterIndices: number[], + position: number +): number[] { + const startIndex = delimiterIndices[position - 1] + 1; + const endIndex = delimiterIndices[position]; + + const fieldData: number[] = []; + for (let i = startIndex; i < endIndex; i++) { + fieldData.push(data[i]); + } + + return fieldData; +} + +// This is the official test data issued by the UIDAI +// In this script we'll change the signed data to emulate the specs of the Aadhaar QR V2 +// and sign the data again with our own certificates. +// data on https://uidai.gov.in/en/ecosystem/authentication-devices-documents/qr-code-reader.html +// This data is copied from https://github.dev/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/src/helpers/extractor.circom +export function stringToAsciiArray(str: string) { + return str.split('').map((char) => char.charCodeAt(0)); +} + +// This modify the test data to make it compliant with the secure Aadhaar QR V2 2022 +// - Adds the version specifier at the beginning 'V2' +// - Mocks last 4 digits of phone number '1234' after VTC +// - Refresh timestamp data to now +// - Optionally it can take parameters to change the test data fields (dob, pinCode, gender, state) +export const testCustomData = + '2374971804270526477833002468783965837992554564899874087591661303561346432389832047870524302186901344489362368642972767716416349990805756094923115719687656090691368051627957878187788907419297818953295185555346288172578594637886352753543271000481717080003254556962148594350559820352806251787713278744047402230989238559317351232114240089849934148895256488140236015024800731753594740948640957680138566468247224859669467819596919398964809164399637893729212452791889199675715949918925838319591794702333094022248132120531152523331442741730158840977243402215102904932650832502847295644794421419704633765033761284508863534321317394686768650111457751139630853448637215423705157211510636160227953566227527799608082928846103264491539001327407775670834868948113753614112563650255058316849200536533335903554984254814901522086937767458409075617572843449110393213525925388131214952874629655799772119820372255291052673056372346072235458198199995637720424196884145247220163810790179386390283738429482893152518286247124911446073389185062482901364671389605727763080854673156754021728522287806275420847159574631844674460263574901590412679291518508010087116598357407343835408554094619585212373168435612645646129147973594416508676872819776522537778717985070402222824965034768103900739105784663244748432502180989441389718131079445941981681118258324511923246198334046020123727749408128519721102477302359413240175102907322619462289965085963377744024233678337951462006962521823224880199210318367946130004264196899778609815012001799773327514133268825910089483612283510244566484854597156100473055413090101948456959122378865704840756793122956663218517626099291311352417342899623681483097817511136427210593032393600010728324905512596767095096153856032112835755780472808814199620390836980020899858288860556611564167406292139646289142056168261133256777093245980048335918156712295254776487472431445495668303900536289283098315798552328294391152828182614909451410115516297083658174657554955228963550255866282688308751041517464999930825273776417639569977754844191402927594739069037851707477839207593911886893016618794870530622356073909077832279869798641545167528509966656120623184120128052588408742941658045827255866966100249857968956536613250770326334844204927432961924987891433020671754710428050564671868464658436926086493709176888821257183419013229795869757265111599482263223604228286513011751601176504567030118257385997460972803240338899836840030438830725520798480181575861397469056536579877274090338750406459700907704031830137890544492015701251066934352867527112361743047684237105216779177819594030160887368311805926405114938744235859610328064947158936962470654636736991567663705830950312548447653861922078087824048793236971354828540758657075837209006713701763902429652486225300535997260665898927924843608750347193892239342462507130025307878412116604096773706728162016134101751551184021079984480254041743057914746472840768175369369852937574401874295943063507273467384747124843744395375119899278823903202010381949145094804675442110869084589592876721655764753871572233276245590041302887094585204427900634246823674277680009401177473636685542700515621164233992970974893989913447733956146698563285998205950467321954304'; diff --git a/common/src/utils/bytes.ts b/common/src/utils/bytes.ts index aa9456447..ffd9deeca 100644 --- a/common/src/utils/bytes.ts +++ b/common/src/utils/bytes.ts @@ -1,12 +1,12 @@ import { MAX_BYTES_IN_FIELD } from '../constants/constants.js'; export function bigIntToChunkedBytes( - num: BigInt | bigint, + num: bigint | bigint, bytesPerChunk: number, numChunks: number ) { const res: string[] = []; - const bigintNum: bigint = typeof num == 'bigint' ? num : num.valueOf(); + const bigintNum: bigint = typeof num == 'bigint' ? num : BigInt(num); const msk = (1n << BigInt(bytesPerChunk)) - 1n; for (let i = 0; i < numChunks; ++i) { res.push(((bigintNum >> BigInt(i * bytesPerChunk)) & msk).toString()); diff --git a/common/src/utils/hash.test.ts b/common/src/utils/hash.test.ts new file mode 100644 index 000000000..09c0afe39 --- /dev/null +++ b/common/src/utils/hash.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { calculateUserIdentifierHash } from './hash'; + +describe('calculateUserIdentifierHash', () => { + it('should return a bigint', () => { + const result = calculateUserIdentifierHash( + 1, + '550e8400-e29b-41d4-a716-446655440000', + 'some data' + ); + expect(typeof result).toBe('bigint'); + }); + + it('should return the same hash for identical inputs', () => { + const destChainID = 42; + const userID = 'abcdef12-3456-7890-abcd-ef1234567890'; + const userDefinedData = 'Test data'; + const hash1 = calculateUserIdentifierHash(destChainID, userID, userDefinedData); + const hash2 = calculateUserIdentifierHash(destChainID, userID, userDefinedData); + expect(hash1).toBe(hash2); + expect(hash1).toMatchInlineSnapshot(`525133570835708563534412370019423387022853755228n`); + }); + + it('should return different hash for different inputs', () => { + const hash1 = calculateUserIdentifierHash( + 42, + 'abcdef12-3456-7890-abcd-ef1234567890', + 'Test data' + ); + const hash2 = calculateUserIdentifierHash( + 42, + 'abcdef12-3456-7890-abcd-ef1234567890', + 'Different data' + ); + expect(hash1).not.toBe(hash2); + expect(hash1).toMatchInlineSnapshot(`525133570835708563534412370019423387022853755228n`); + }); + it('should handle user ids starting with 0x', () => { + const hash1 = calculateUserIdentifierHash(42, '0xabcdef1234567890', 'Test data'); + const hash2 = calculateUserIdentifierHash(42, 'abcdef1234567890', 'Test data'); + expect(hash1).toBe(hash2); + expect(hash1).toMatchInlineSnapshot(`830654111289877969679298811043657652615780822337n`); + }); +}); diff --git a/common/src/utils/hash.ts b/common/src/utils/hash.ts index 07f2cd8e1..0f0876a3f 100644 --- a/common/src/utils/hash.ts +++ b/common/src/utils/hash.ts @@ -29,7 +29,7 @@ export function calculateUserIdentifierHash( destChainID: number, userID: string, userDefinedData: string -): BigInt { +): bigint { const solidityPackedUserContextData = getSolidityPackedUserContextData( destChainID, userID, @@ -133,7 +133,8 @@ export function getSolidityPackedUserContextData( ['bytes32', 'bytes32', 'bytes'], [ ethers.zeroPadValue(ethers.toBeHex(destChainID), 32), - ethers.zeroPadValue('0x' + userIdHex, 32), + + ethers.zeroPadValue(userIdHex.startsWith('0x') ? userIdHex : '0x' + userIdHex, 32), ethers.toUtf8Bytes(userDefinedData), ] ); diff --git a/common/src/utils/trees.ts b/common/src/utils/trees.ts index e1e72fa4a..25b5e0423 100644 --- a/common/src/utils/trees.ts +++ b/common/src/utils/trees.ts @@ -2,14 +2,15 @@ import countries from 'i18n-iso-countries'; // @ts-ignore import en from 'i18n-iso-countries/langs/en.json' with { type: 'json' }; import { - poseidon12, - poseidon13, poseidon2, poseidon3, poseidon5, poseidon6, poseidon10, + poseidon12, + poseidon13, } from 'poseidon-lite'; + import { CSCA_TREE_DEPTH, DSC_TREE_DEPTH, @@ -17,6 +18,7 @@ import { max_dsc_bytes, OFAC_TREE_LEVELS, } from '../constants/constants.js'; +import { packBytes } from './bytes.js'; import type { CertificateData } from './certificate_parsing/dataStructure.js'; import { parseCertificateSimple } from './certificate_parsing/parseCertificateSimple.js'; import { stringToAsciiBigIntArray } from './circuits/uuid.js'; @@ -26,7 +28,6 @@ import { DscCertificateMetaData, parseDscCertificateData, } from './passports/passport_parsing/parseDscCertificateData.js'; -import { packBytes } from './bytes.js'; import { IMT } from '@openpassport/zk-kit-imt'; import { LeanIMT } from '@openpassport/zk-kit-lean-imt'; @@ -36,6 +37,52 @@ import { SMT } from '@openpassport/zk-kit-smt'; // SideEffect here countries.registerLocale(en); +//--------------------------- +// AADHAAR +//--------------------------- +export function buildAadhaarSMT(field: any[], treetype: string): [number, number, SMT] { + let count = 0; + let startTime = performance.now(); + + const hash2 = (childNodes: ChildNodes) => + childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes); + const tree = new SMT(hash2, true); + + for (let i = 0; i < field.length; i++) { + const entry = field[i]; + + if (i !== 0) { + console.log('Processing', treetype, 'number', i, 'out of', field.length); + } + + let leaf = BigInt(0); + let reverse_leaf = BigInt(0); + if (treetype == 'name_and_dob') { + leaf = processNameAndDobAadhaar(entry, i); + reverse_leaf = processNameAndDobAadhaar(entry, i, true); + } else if (treetype == 'name_and_yob') { + leaf = processNameAndYobAadhaar(entry, i); + reverse_leaf = processNameAndYobAadhaar(entry, i, true); + } + + if (leaf == BigInt(0) || tree.createProof(leaf).membership) { + console.log('This entry already exists in the tree, skipping...'); + continue; + } + + count += 1; + tree.add(leaf, BigInt(1)); + if (reverse_leaf == BigInt(0) || tree.createProof(reverse_leaf).membership) { + console.log('This entry already exists in the tree, skipping...'); + continue; + } + tree.add(reverse_leaf, BigInt(1)); + count += 1; + } + + return [count, performance.now() - startTime, tree]; +} + // SMT trees for 3 levels of matching : // 1. Passport Number and Nationality tree : level 3 (Absolute Match) // 2. Name and date of birth combo tree : level 2 (High Probability Match) @@ -266,12 +313,6 @@ export function getLeafCscaTree(csca_parsed: CertificateData): string { return getLeaf(csca_parsed, 'csca'); } -export function getLeafDscTree(dsc_parsed: CertificateData, csca_parsed: CertificateData): string { - const dscLeaf = getLeaf(dsc_parsed, 'dsc'); - const cscaLeaf = getLeaf(csca_parsed, 'csca'); - return poseidon2([dscLeaf, cscaLeaf]).toString(); -} - function processPassportNoAndNationality( passno: string, nationality: string, @@ -480,6 +521,12 @@ function processCountry(country1: string, country2: string, i: number) { return leaf; } +export function getLeafDscTree(dsc_parsed: CertificateData, csca_parsed: CertificateData): string { + const dscLeaf = getLeaf(dsc_parsed, 'dsc'); + const cscaLeaf = getLeaf(csca_parsed, 'csca'); + return poseidon2([dscLeaf, cscaLeaf]).toString(); +} + export function getLeafDscTreeFromDscCertificateMetadata( dscParsed: CertificateData, dscMetaData: DscCertificateMetaData @@ -501,6 +548,18 @@ export function getNameDobLeaf( return generateSmallKey(poseidon2([getDobLeaf(dobMrz), getNameLeaf(nameMrz)])); } +export const getNameDobLeafAadhaar = (name: string, year: string, month: string, day: string) => { + const paddedName = name + .toUpperCase() + .padEnd(62, '\0') + .split('') + .map((char) => char.charCodeAt(0)); + const namePacked = packBytes(paddedName); + return generateSmallKey( + poseidon5([namePacked[0], namePacked[1], BigInt(year), BigInt(month), BigInt(day)]) + ); +}; + export function getNameLeaf(nameMrz: (bigint | number)[], i?: number): bigint { const middleChunks: bigint[] = []; const chunks: (number | bigint)[][] = []; @@ -544,76 +603,6 @@ export function getNameYobLeaf( return generateSmallKey(poseidon2([getYearLeaf(yobMrz), getNameLeaf(nameMrz)])); } -export function getPassportNumberAndNationalityLeaf( - passport: (bigint | number)[], - nationality: (bigint | number)[], - i?: number -): bigint { - if (passport.length !== 9) { - console.log('parsed passport length is not 9:', i, passport); - return; - } - if (nationality.length !== 3) { - console.log('parsed nationality length is not 3:', i, nationality); - return; - } - try { - const fullHash = poseidon12(passport.concat(nationality)); - return generateSmallKey(fullHash); - } catch (err) { - console.log('err : passport', err, i, passport); - } -} - -//--------------------------- - -// AADHAAR - -//--------------------------- - -export function buildAadhaarSMT(field: any[], treetype: string): [number, number, SMT] { - let count = 0; - let startTime = performance.now(); - - const hash2 = (childNodes: ChildNodes) => - childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes); - const tree = new SMT(hash2, true); - - for (let i = 0; i < field.length; i++) { - const entry = field[i]; - - if (i !== 0) { - console.log('Processing', treetype, 'number', i, 'out of', field.length); - } - - let leaf = BigInt(0); - let reverse_leaf = BigInt(0); - if (treetype == 'name_and_dob') { - leaf = processNameAndDobAadhaar(entry, i); - reverse_leaf = processNameAndDobAadhaar(entry, i, true); - } else if (treetype == 'name_and_yob') { - leaf = processNameAndYobAadhaar(entry, i); - reverse_leaf = processNameAndYobAadhaar(entry, i, true); - } - - if (leaf == BigInt(0) || tree.createProof(leaf).membership) { - console.log('This entry already exists in the tree, skipping...'); - continue; - } - - count += 1; - tree.add(leaf, BigInt(1)); - if (reverse_leaf == BigInt(0) || tree.createProof(reverse_leaf).membership) { - console.log('This entry already exists in the tree, skipping...'); - continue; - } - tree.add(reverse_leaf, BigInt(1)); - count += 1; - } - - return [count, performance.now() - startTime, tree]; -} - const processNameAndDobAadhaar = (entry: any, i: number, reverse: boolean = false): bigint => { let firstName = entry.First_Name; let lastName = entry.Last_Name; @@ -684,18 +673,6 @@ const processDobAadhaar = (year: string, month: string, day: string): bigint[] = return [year, month, day].map(BigInt); }; -export const getNameDobLeafAadhaar = (name: string, year: string, month: string, day: string) => { - const paddedName = name - .toUpperCase() - .padEnd(62, '\0') - .split('') - .map((char) => char.charCodeAt(0)); - const namePacked = packBytes(paddedName); - return generateSmallKey( - poseidon5([namePacked[0], namePacked[1], BigInt(year), BigInt(month), BigInt(day)]) - ); -}; - export const getNameYobLeafAahaar = (name: string, year: string) => { const paddedName = name .toUpperCase() @@ -706,3 +683,24 @@ export const getNameYobLeafAahaar = (name: string, year: string) => { return generateSmallKey(poseidon3([namePacked[0], namePacked[1], BigInt(year)])); }; + +export function getPassportNumberAndNationalityLeaf( + passport: (bigint | number)[], + nationality: (bigint | number)[], + i?: number +): bigint { + if (passport.length !== 9) { + console.log('parsed passport length is not 9:', i, passport); + return; + } + if (nationality.length !== 3) { + console.log('parsed nationality length is not 3:', i, nationality); + return; + } + try { + const fullHash = poseidon12(passport.concat(nationality)); + return generateSmallKey(fullHash); + } catch (err) { + console.log('err : passport', err, i, passport); + } +} diff --git a/package.json b/package.json index 7203c7a10..9503cffb0 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "build": "yarn workspaces foreach --topological-dev --parallel --exclude @selfxyz/contracts -i --all run build", "check:versions": "node scripts/check-package-versions.mjs", "format": "SKIP_BUILD_DEPS=1 yarn format:root && yarn format:github && SKIP_BUILD_DEPS=1 yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run format", - "format:github": "prettier --parser yaml --write .github/**/*.yml --single-quote false", - "format:root": "prettier --parser markdown --write *.md scripts/**/*.md && prettier --parser yaml --write .*.{yml,yaml} --single-quote false && prettier --write scripts/**/*.{js,mjs,ts} && prettier --parser json --write scripts/**/*.json", + "format:github": "yarn prettier --parser yaml --write .github/**/*.yml --single-quote false", + "format:root": "echo 'format markdown' && yarn prettier --parser markdown --write *.md && echo 'format yaml' && yarn prettier --parser yaml --write .*.{yml,yaml} --single-quote false && yarn prettier --write scripts/**/*.{js,mjs,ts} && yarn prettier --parser json --write scripts/**/*.json", "gitleaks": "gitleaks protect --staged --redact --config=.gitleaks.toml", "postinstall": "node scripts/run-patch-package.cjs", "lint": "yarn lint:headers && yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint", @@ -46,6 +46,7 @@ "husky": "9.1.7", "knip": "^5.62.0", "patch-package": "^8.0.0", + "prettier": "^3.5.3", "typescript": "^5.9.2" }, "packageManager": "yarn@4.6.0", diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index 885973bb9..e7b698ee7 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -52,8 +52,8 @@ "demo:ios": "yarn workspace demo-app ios", "demo:start": "yarn workspace demo-app start", "demo:test": "yarn workspace demo-app test", - "fmt": "prettier --check .", - "fmt:fix": "prettier --write .", + "fmt": "yarn prettier --check .", + "fmt:fix": "yarn prettier --write .", "format": "sh -c 'if [ -z \"$SKIP_BUILD_DEPS\" ]; then yarn nice; else yarn fmt:fix; fi'", "lint": "eslint .", "lint:fix": "eslint --fix .", @@ -70,6 +70,7 @@ }, "dependencies": { "@selfxyz/common": "workspace:^", + "socket.io-client": "^4.8.1", "tslib": "^2.6.2", "zustand": "^4.5.2" }, diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index 5810a4bcc..212b89c6a 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -63,6 +63,8 @@ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; +export { generateTEEInputsDisclose } from './processing/generate-disclosure-inputs'; + // Core functions export { isPassportDataValid } from './validation/document'; @@ -73,5 +75,4 @@ export { parseNFCResponse, scanNFC } from './nfc'; export { reactNativeScannerAdapter } from './adapters/react-native/scanner'; export { scanQRProof } from './qr'; - export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index afc18791a..a500aff2c 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -98,6 +98,10 @@ export { formatDateToYYMMDD, scanMRZ } from './mrz'; export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; +export { generateTEEInputsDisclose } from './processing/generate-disclosure-inputs'; + +// Documents utils + // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts b/packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts new file mode 100644 index 000000000..24c421a9a --- /dev/null +++ b/packages/mobile-sdk-alpha/src/processing/generate-disclosure-inputs.ts @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { DocumentCategory, PassportData } from '@selfxyz/common/types'; +import type { SelfApp } from '@selfxyz/common/utils'; +import { generateTEEInputsDiscloseStateless } from '@selfxyz/common/utils/circuits/registerInputs'; + +import { useProtocolStore } from '../stores/protocolStore'; + +export function generateTEEInputsDisclose(secret: string, passportData: PassportData, selfApp: SelfApp) { + return generateTEEInputsDiscloseStateless(secret, passportData, selfApp, (document: DocumentCategory, tree) => { + const protocolStore = useProtocolStore.getState(); + const docStore = (protocolStore as any)[document]; + if (!docStore) { + throw new Error(`Unknown or unloaded document category in protocol store: ${document}`); + } + switch (tree) { + case 'ofac': + return docStore.ofac_trees; + case 'commitment': + if (!docStore.commitment_tree) { + throw new Error('Commitment tree not loaded'); + } + return docStore.commitment_tree; + default: + throw new Error('Unknown tree type'); + } + }); +} diff --git a/packages/mobile-sdk-alpha/src/stores/index.ts b/packages/mobile-sdk-alpha/src/stores/index.ts index bf1168dd4..91670f18c 100644 --- a/packages/mobile-sdk-alpha/src/stores/index.ts +++ b/packages/mobile-sdk-alpha/src/stores/index.ts @@ -3,3 +3,4 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. export { useProtocolStore } from './protocolStore'; +export { useSelfAppStore } from './selfAppStore'; diff --git a/app/src/stores/selfAppStore.tsx b/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx similarity index 88% rename from app/src/stores/selfAppStore.tsx rename to packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx index c87ade3ef..379ee87ad 100644 --- a/app/src/stores/selfAppStore.tsx +++ b/packages/mobile-sdk-alpha/src/stores/selfAppStore.tsx @@ -17,11 +17,7 @@ interface SelfAppState { cleanSelfApp: () => void; setSelfApp: (selfApp: SelfApp | null) => void; _initSocket: (sessionId: string) => Socket; - handleProofResult: ( - proof_verified: boolean, - error_code?: string, - reason?: string, - ) => void; + handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => void; } export const useSelfAppStore = create((set, get) => ({ @@ -30,9 +26,7 @@ export const useSelfAppStore = create((set, get) => ({ socket: null, _initSocket: (sessionId: string): Socket => { - const connectionUrl = WS_DB_RELAYER.startsWith('https') - ? WS_DB_RELAYER.replace(/^https/, 'wss') - : WS_DB_RELAYER; + const connectionUrl = WS_DB_RELAYER.startsWith('https') ? WS_DB_RELAYER.replace(/^https/, 'wss') : WS_DB_RELAYER; const socketUrl = `${connectionUrl}/websocket`; // Create a new socket connection using the updated URL. @@ -72,8 +66,7 @@ export const useSelfAppStore = create((set, get) => ({ // Listen for the event only once per connection attempt socket.once('self_app', (data: unknown) => { try { - const appData: SelfApp = - typeof data === 'string' ? JSON.parse(data) : (data as SelfApp); + const appData: SelfApp = typeof data === 'string' ? JSON.parse(data) : (data as SelfApp); // Basic validation if (!appData || typeof appData !== 'object' || !appData.sessionId) { @@ -130,18 +123,12 @@ export const useSelfAppStore = create((set, get) => ({ set({ selfApp: null, sessionId: null, socket: null }); }, - handleProofResult: ( - proof_verified: boolean, - error_code?: string, - reason?: string, - ) => { + handleProofResult: (proof_verified: boolean, error_code?: string, reason?: string) => { const socket = get().socket; const sessionId = get().sessionId; if (!socket || !sessionId) { - console.error( - '[SelfAppStore] Cannot handleProofResult: Socket or SessionId missing.', - ); + console.error('[SelfAppStore] Cannot handleProofResult: Socket or SessionId missing.'); return; } diff --git a/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts b/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts new file mode 100644 index 000000000..c2c7c24b0 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/processing/generate-disclosure-inputs.test.ts @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { PassportData, SelfApp } from '@selfxyz/common'; + +import { generateTEEInputsDisclose } from '../../src/processing/generate-disclosure-inputs'; +import { useProtocolStore } from '../../src/stores/protocolStore'; +// Mocks for dependencies +const mockSecret = '0x' + '00'.repeat(30) + 'a4ec'; // 32-byte hex string +const mockPassportData: PassportData = { + mrz: 'P ({ + useProtocolStore: { + getState: () => ({ + passport: { + ofac_trees: { + nameAndDob: '{"root":["0"]}', + nameAndYob: '{"root":["0"]}', + passportNoAndNationality: '{"root":["0"]}', + }, + commitment_tree: '[[]]', + }, + }), + }, +})); + +describe('generateTEEInputsDisclose', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('throws error for unknown document category', () => { + // Mock the store to return an unknown document category + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + unknown: undefined, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + `Unknown or unloaded document category in protocol store: passport`, + ); + }); + + it('throws error for unknown tree type', () => { + // This test doesn't make sense as written since tree type is determined internally + // Let's test the commitment tree validation instead + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + passport: { + ofac_trees: 'ofac-tree-data', + commitment_tree: undefined, + }, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + `Invalid OFAC tree structure: missing required fields`, + ); + }); + + it('throws error if commitment tree not loaded', () => { + vi.spyOn(useProtocolStore, 'getState').mockReturnValue({ + passport: { + ofac_trees: 'ofac-tree-data', + commitment_tree: undefined, + }, + } as any); + + expect(() => generateTEEInputsDisclose(mockSecret, mockPassportData, mockSelfApp)).toThrowError( + `Invalid OFAC tree structure: missing required fields`, + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 46760b3bb..d10e517f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5038,6 +5038,7 @@ __metadata: eslint-plugin-sort-exports: "npm:^0.8.0" jsdom: "npm:^24.0.0" prettier: "npm:^3.5.3" + socket.io-client: "npm:^4.8.1" tslib: "npm:^2.6.2" tsup: "npm:^8.0.1" typescript: "npm:^5.9.2" @@ -23234,6 +23235,7 @@ __metadata: husky: "npm:9.1.7" knip: "npm:^5.62.0" patch-package: "npm:^8.0.0" + prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-native: "npm:0.76.9" typescript: "npm:^5.9.2"