Move self app store to mobile sdk (#1040)

This commit is contained in:
Aaron DeRuvo
2025-09-11 17:30:01 +02:00
committed by GitHub
parent f416211037
commit 1f362b33ce
35 changed files with 968 additions and 680 deletions

View File

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

View File

@@ -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",

View File

@@ -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';

View File

@@ -60,13 +60,13 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
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();

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

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

View File

@@ -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');
}
},
);
}

View File

@@ -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<void>;
startFetchingData: () => Promise<void>;
startFetchingData: (selfClient: SelfClient) => Promise<void>;
validatingDocument: (selfClient: SelfClient) => Promise<void>;
initTeeConnection: () => Promise<boolean>;
startProving: () => Promise<void>;
initTeeConnection: (selfClient: SelfClient) => Promise<boolean>;
startProving: (selfClient: SelfClient) => Promise<void>;
postProving: (selfClient: SelfClient) => void;
setUserConfirmed: () => void;
_closeConnections: () => void;
_generatePayload: () => Promise<unknown>;
_handleWebSocketMessage: (event: MessageEvent) => Promise<void>;
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<void>;
_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<ProvingState>((set, get) => {
) {
newActor.subscribe((state: StateFrom<typeof provingMachine>) => {
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<ProvingState>((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<ProvingState>((set, get) => {
serverPublicKey: null,
sharedKey: null,
wsConnection: null,
wsHandlers: null,
socketConnection: null,
uuid: null,
userConfirmed: false,
@@ -325,21 +347,22 @@ export const useProvingStore = create<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((set, get) => {
new MessageEvent('error', {
data: JSON.stringify({ error: 'WebSocket closed unexpectedly' }),
}),
selfClient,
);
}
if (get().wsConnection) {
@@ -600,8 +632,8 @@ export const useProvingStore = create<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<boolean> => {
initTeeConnection: async (selfClient: SelfClient): Promise<boolean> => {
const { passportData } = get();
if (!passportData) {
throw new Error('PassportData is not available');
@@ -850,26 +883,36 @@ export const useProvingStore = create<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((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<ProvingState>((set, get) => {
error,
);
}
set({ wsConnection: null });
set({ wsConnection: null, wsHandlers: null });
}
const socket = get().socketConnection;
@@ -1004,7 +1047,7 @@ export const useProvingStore = create<ProvingState>((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<ProvingState>((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 });

View File

@@ -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', () => ({

View File

@@ -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');
});

View File

@@ -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,

View File

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

View File

@@ -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',
],

View File

@@ -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';

View File

@@ -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",

View File

@@ -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';

View File

@@ -1,4 +1,5 @@
import * as fs from 'fs';
import { buildAadhaarSMT } from '../trees.js';
async function build_aadhaar_ofac_smt() {

View File

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

View File

@@ -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,166 +168,6 @@ function processQRDataSimple(qrData: string) {
};
}
export function prepareAadhaarRegisterTestData(
privKeyPem: string,
pubkeyPem: string,
secret: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
) {
const sharedData = processQRData(
privKeyPem,
name,
dateOfBirth,
gender,
pincode,
state,
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 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 nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
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)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export async function prepareAadhaarRegisterData(qrData: string, secret: string, certs: string[]) {
const sharedData = processQRDataSimple(qrData);
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());
//do promise.all for all certs and pick the one that is valid
const certificates = await Promise.all(
certs.map(async (cert) => {
const certificate = forge.pki.certificateFromPem(cert);
const publicKey = certificate.publicKey as forge.pki.rsa.PublicKey;
try {
const md = forge.md.sha256.create();
md.update(forge.util.binary.raw.encode(sharedData.signedData));
const isValid = publicKey.verify(md.digest().getBytes(), signatureBytes);
return isValid;
} catch (error) {
return false;
}
})
);
//find the valid cert
const validCert = certificates.indexOf(true);
if (validCert === -1) {
throw new Error('No valid certificate found');
}
const certPem = certs[validCert];
const cert = forge.pki.certificateFromPem(certPem);
const modulusHex = (cert.publicKey as forge.pki.rsa.PublicKey).n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
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)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export function prepareAadhaarDiscloseTestData(
privateKeyPem: string,
merkletree: LeanIMT,
@@ -444,3 +289,163 @@ export function prepareAadhaarDiscloseTestData(
commitment,
};
}
export async function prepareAadhaarRegisterData(qrData: string, secret: string, certs: string[]) {
const sharedData = processQRDataSimple(qrData);
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());
//do promise.all for all certs and pick the one that is valid
const certificates = await Promise.all(
certs.map(async (cert) => {
const certificate = forge.pki.certificateFromPem(cert);
const publicKey = certificate.publicKey as forge.pki.rsa.PublicKey;
try {
const md = forge.md.sha256.create();
md.update(forge.util.binary.raw.encode(sharedData.signedData));
const isValid = publicKey.verify(md.digest().getBytes(), signatureBytes);
return isValid;
} catch (error) {
return false;
}
})
);
//find the valid cert
const validCert = certificates.indexOf(true);
if (validCert === -1) {
throw new Error('No valid certificate found');
}
const certPem = certs[validCert];
const cert = forge.pki.certificateFromPem(certPem);
const modulusHex = (cert.publicKey as forge.pki.rsa.PublicKey).n.toString(16);
const pubKey = BigInt('0x' + modulusHex);
const nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
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)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}
export function prepareAadhaarRegisterTestData(
privKeyPem: string,
pubkeyPem: string,
secret: string,
name?: string,
dateOfBirth?: string,
gender?: string,
pincode?: string,
state?: string,
timestamp?: string
) {
const sharedData = processQRData(
privKeyPem,
name,
dateOfBirth,
gender,
pincode,
state,
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 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 nullifier = nullifierHash(sharedData.extractedFields);
const packedCommitment = computePackedCommitment(sharedData.extractedFields);
const commitment = computeCommitment(
BigInt(secret),
BigInt(sharedData.qrHash),
nullifier,
packedCommitment,
BigInt(sharedData.photoHash)
);
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)),
secret: secret,
photoEOI: photoEOI,
};
return {
inputs,
nullifier,
commitment,
};
}

View File

@@ -1,27 +1,40 @@
import forge from 'node-forge';
import {
convertBigIntToByteArray,
decompressByteArray,
returnFullId,
extractPhoto,
getEndIndex,
getRandomBytes,
IdFields,
rawDataToCompressedQR,
replaceBytesBetween,
IdFields,
extractPhoto,
getRandomBytes,
getEndIndex,
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);
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();
// 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,
});
const birthYear = parseInt(yob);
const birthMonth = parseInt(mob);
const birthDay = parseInt(dob);
// Signing the newly generated testData
const signature = signNewTestData(dataToSign, privKeyPem);
let age = currentYear - birthYear;
// 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),
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';

View File

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

View File

@@ -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`);
});
});

View File

@@ -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),
]
);

View File

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

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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';

View File

@@ -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';

View File

@@ -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');
}
});
}

View File

@@ -3,3 +3,4 @@
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
export { useProtocolStore } from './protocolStore';
export { useSelfAppStore } from './selfAppStore';

View File

@@ -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<SelfAppState>((set, get) => ({
@@ -30,9 +26,7 @@ export const useSelfAppStore = create<SelfAppState>((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<SelfAppState>((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<SelfAppState>((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;
}

View File

@@ -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<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<<<<L898902C36UTO7408122F1204159ZE184226B<<<<',
dsc: '',
eContent: [],
signedAttr: [],
encryptedDigest: [],
passportMetadata: {
dataGroups: 'dg1',
dg1Size: 100,
dg1HashSize: 32,
dg1HashFunction: 'sha256',
dg1HashOffset: 0,
dgPaddingBytes: 0,
eContentSize: 100,
eContentHashFunction: 'sha256',
eContentHashOffset: 0,
signedAttrSize: 100,
signedAttrHashFunction: 'sha256',
signatureAlgorithm: 'rsa',
saltLength: 32,
curveOrExponent: '65537',
signatureAlgorithmBits: 0,
countryCode: '',
cscaFound: false,
cscaHashFunction: '',
cscaSignatureAlgorithm: '',
cscaSaltLength: 0,
cscaCurveOrExponent: '',
cscaSignatureAlgorithmBits: 0,
dsc: '',
csca: '',
},
dsc_parsed: {
tbsBytes: new Array(100).fill(1),
signatureAlgorithm: 'rsa',
publicKeyAlgorithm: 'rsa',
publicKeyDetails: {
modulus: '12345',
exponent: '65537',
},
signature: new Array(100).fill(1),
} as any,
csca_parsed: {
tbsBytes: new Array(100).fill(1),
signatureAlgorithm: 'rsa',
publicKeyAlgorithm: 'rsa',
publicKeyDetails: {
modulus: '12345',
exponent: '65537',
},
signature: new Array(100).fill(1),
} as any,
documentType: 'passport',
documentCategory: 'passport',
mock: true,
};
const mockSelfApp: SelfApp = {
userId: '0x0000000000000000000000000000000000000000000000000000000000000000',
appName: 'TestSelfApp',
logoBase64: '',
endpointType: 'https',
endpoint: 'https://test.example.com',
deeplinkCallback: '',
header: '',
scope: 'test',
sessionId: '',
userIdType: 'hex',
devMode: false,
disclosures: {},
version: 0,
chainID: 42220,
userDefinedData: '',
};
vi.mock('../../src/stores/protocolStore', () => ({
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`,
);
});
});

View File

@@ -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"