Feat/extend id support (#517)

* refactor proving impleting xstate, speedup proving

* add disclosure proof support

* keep refactoring provingMachine, clean old implementation

* call init method when switching from dsc to register

* rebase with dev to display why the proof verification failed

* refactor ws connexion between front-end and mobile to retrieve self-app

* update the webclient at proofVerification and use selfAppStore in provingMachine

* fix provintStore.init in ProveScreen

* yarn nice

* fetch data correctly in splash screen
This commit is contained in:
turnoffthiscomputer
2025-04-24 08:55:47 -04:00
committed by GitHub
parent 0d7d1170f3
commit 0bf324e639
26 changed files with 1507 additions and 1393 deletions

View File

@@ -6,10 +6,8 @@ import { YStack } from 'tamagui';
import AppNavigation from './src/Navigation';
import { initSentry, wrapWithSentry } from './src/Sentry';
import { AppProvider } from './src/stores/appProvider';
import { AuthProvider } from './src/stores/authProvider';
import { PassportProvider } from './src/stores/passportDataProvider';
import { ProofProvider } from './src/stores/proofProvider';
initSentry();
@@ -20,11 +18,7 @@ function App(): React.JSX.Element {
<YStack f={1} h="100%" w="100%">
<AuthProvider>
<PassportProvider>
<AppProvider>
<ProofProvider>
<AppNavigation />
</ProofProvider>
</AppProvider>
<AppNavigation />
</PassportProvider>
</AuthProvider>
</YStack>

View File

@@ -96,6 +96,7 @@
"socket.io-client": "^4.7.5",
"tamagui": "1.110.0",
"uuid": "^11.0.5",
"xstate": "^5.19.2",
"zustand": "^4.5.2"
},
"devDependencies": {

View File

@@ -11,11 +11,9 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { DefaultNavBar } from '../components/NavBar';
import AppLayout from '../layouts/AppLayout';
import { useApp } from '../stores/appProvider';
import { useProofInfo } from '../stores/proofProvider';
import analytics from '../utils/analytics';
import { white } from '../utils/colors';
import { setupUniversalLinkListenerInNavigation } from '../utils/qrCodeNew';
import { setupUniversalLinkListenerInNavigation } from '../utils/deeplinks';
import accountScreens from './account';
import aesopScreens from './aesop';
import homeScreens from './home';
@@ -70,23 +68,14 @@ const NavigationWithTracking = () => {
}
};
// Add these hooks to get access to the necessary functions
const { setSelectedApp, cleanSelfApp } = useProofInfo();
const { startAppListener } = useApp();
// Setup universal link handling at the navigation level
React.useEffect(() => {
const cleanup = setupUniversalLinkListenerInNavigation(
navigationRef,
setSelectedApp,
cleanSelfApp,
startAppListener,
);
const cleanup = setupUniversalLinkListenerInNavigation();
return () => {
cleanup();
};
}, [setSelectedApp, cleanSelfApp, startAppListener]);
}, []);
return (
<GestureHandlerRootView>

View File

@@ -16,7 +16,7 @@ import { loadPassportDataAndSecret } from '../../stores/passportDataProvider';
import { useSettingStore } from '../../stores/settingStore';
import { STORAGE_NAME, useBackupMnemonic } from '../../utils/cloudBackup';
import { black, slate500, slate600, white } from '../../utils/colors';
import { isUserRegistered } from '../../utils/proving/payload';
import { isUserRegistered } from '../../utils/proving/validateDocument';
interface AccountRecoveryChoiceScreenProps {}

View File

@@ -18,7 +18,7 @@ import {
slate700,
white,
} from '../../utils/colors';
import { isUserRegistered } from '../../utils/proving/payload';
import { isUserRegistered } from '../../utils/proving/validateDocument';
interface RecoverWithPhraseScreenProps {}

View File

@@ -10,6 +10,7 @@ import useHapticNavigation from '../../hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { black, white } from '../../utils/colors';
import { notificationSuccess } from '../../utils/haptic';
import { useProvingStore } from '../../utils/proving/provingMachine';
import { styles } from '../ProveFlow/ProofRequestStatusScreen';
type ConfirmBelongingScreenProps = StaticScreenProps<
@@ -23,15 +24,34 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = ({
route,
}) => {
const mockPassportFlow = route.params?.mockPassportFlow;
const onOkPress = useHapticNavigation('LoadingScreen', {
const navigate = useHapticNavigation('LoadingScreen', {
params: {
mockPassportFlow,
},
});
const provingStore = useProvingStore();
useEffect(() => {
notificationSuccess();
provingStore.init('dsc');
}, []);
const onOkPress = async () => {
// Initialize the proving process just before navigation
// This ensures a fresh start each time
try {
// Initialize the state machine
// Mark as user confirmed - proving will start automatically when ready
provingStore.setUserConfirmed();
// Navigate to loading screen
navigate();
} catch (error) {
console.error('Error initializing proving process:', error);
}
};
// Prevents back navigation
usePreventRemove(true, () => {});

View File

@@ -1,117 +1,35 @@
import { StaticScreenProps, useNavigation } from '@react-navigation/native';
import { StaticScreenProps } from '@react-navigation/native';
import { useIsFocused } from '@react-navigation/native';
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import failAnimation from '../../assets/animations/loading/fail.json';
import miscAnimation from '../../assets/animations/loading/misc.json';
import successAnimation from '../../assets/animations/loading/success.json';
import useHapticNavigation from '../../hooks/useHapticNavigation';
import { usePassport } from '../../stores/passportDataProvider';
import { ProofStatusEnum, useProofInfo } from '../../stores/proofProvider';
import analytics from '../../utils/analytics';
import {
checkPassportSupported,
isPassportNullified,
isUserRegistered,
registerPassport,
} from '../../utils/proving/payload';
const { trackEvent } = analytics();
import { useProvingStore } from '../../utils/proving/provingMachine';
type LoadingScreenProps = StaticScreenProps<{}>;
const LoadingScreen: React.FC<LoadingScreenProps> = ({}) => {
const goToSuccessScreen = useHapticNavigation('AccountVerifiedSuccess');
const goToErrorScreen = useHapticNavigation('Launch');
const goToUnsupportedScreen = useHapticNavigation('UnsupportedPassport');
const navigation = useNavigation();
const goToSuccessScreenWithDelay = () => {
setTimeout(() => {
goToSuccessScreen();
}, 3000);
};
const goToErrorScreenWithDelay = () => {
setTimeout(() => {
goToErrorScreen();
}, 3000);
};
const [animationSource, setAnimationSource] = useState<any>(miscAnimation);
const { registrationStatus, resetProof } = useProofInfo();
const { getPassportDataAndSecret, clearPassportData } = usePassport();
const currentState = useProvingStore(state => state.currentState);
const isFocused = useIsFocused();
// Monitor the state of the proving machine
useEffect(() => {
// TODO this makes sense if reset proof was only about passport registration
resetProof();
}, []);
if (isFocused) {
console.log('[LoadingScreen] Current proving state:', currentState);
}
useEffect(() => {
console.log('registrationStatus', registrationStatus);
if (registrationStatus === ProofStatusEnum.SUCCESS) {
if (currentState === 'completed') {
setAnimationSource(successAnimation);
goToSuccessScreenWithDelay();
setTimeout(() => resetProof(), 3000);
} else if (
registrationStatus === ProofStatusEnum.FAILURE ||
registrationStatus === ProofStatusEnum.ERROR
) {
} else if (currentState === 'error') {
setAnimationSource(failAnimation);
goToErrorScreenWithDelay();
setTimeout(() => resetProof(), 3000);
} else {
setAnimationSource(miscAnimation);
}
}, [registrationStatus]);
const processPayloadCalled = useRef(false);
useEffect(() => {
if (!processPayloadCalled.current) {
processPayloadCalled.current = true;
const processPayload = async () => {
try {
const passportDataAndSecret = await getPassportDataAndSecret();
if (!passportDataAndSecret) {
return;
}
const { passportData, secret } = passportDataAndSecret.data;
const isSupported = await checkPassportSupported(passportData);
if (isSupported.status !== 'passport_supported') {
trackEvent('Passport not supported', {
reason: isSupported.status,
details: isSupported.details,
});
goToUnsupportedScreen();
console.log('Passport not supported');
clearPassportData();
return;
}
const isRegistered = await isUserRegistered(passportData, secret);
console.log('User is registered:', isRegistered);
if (isRegistered) {
console.log(
'Passport is registered already. Skipping to AccountVerifiedSuccess',
);
navigation.navigate('AccountVerifiedSuccess');
return;
}
const isNullifierOnchain = await isPassportNullified(passportData);
console.log('Passport is nullified:', isNullifierOnchain);
if (isNullifierOnchain) {
console.log(
'Passport is nullified, but not registered with this secret. Prompt to restore secret from iCloud or manual backup',
);
navigation.navigate('AccountRecoveryChoice');
return;
}
registerPassport(passportData, secret);
} catch (error) {
console.error('Error processing payload:', error);
setTimeout(() => resetProof(), 1000);
}
};
processPayload();
}
}, []);
}, [currentState, isFocused]);
return (
<View style={styles.container}>

View File

@@ -1,5 +1,6 @@
import { useIsFocused } from '@react-navigation/native';
import LottieView from 'lottie-react-native';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { StatusBar, StyleSheet, View } from 'react-native';
import { ScrollView, Spinner } from 'tamagui';
@@ -13,20 +14,27 @@ import { typography } from '../../components/typography/styles';
import { Title } from '../../components/typography/Title';
import useHapticNavigation from '../../hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { ProofStatusEnum, useProofInfo } from '../../stores/proofProvider';
import { useSelfAppStore } from '../../stores/selfAppStore';
import { black, white } from '../../utils/colors';
import {
buttonTap,
notificationError,
notificationSuccess,
} from '../../utils/haptic';
import { useProvingStore } from '../../utils/proving/provingMachine';
const SuccessScreen: React.FC = () => {
const { selectedApp, disclosureStatus, discloseError, cleanSelfApp } =
useProofInfo();
const appName = selectedApp?.appName;
const { selfApp, cleanSelfApp } = useSelfAppStore();
const appName = selfApp?.appName;
const goHome = useHapticNavigation('Home');
const currentState = useProvingStore(state => state.currentState);
const reason = useProvingStore(state => state.reason);
const isFocused = useIsFocused();
const [animationSource, setAnimationSource] = useState<any>(loadingAnimation);
function onOkPress() {
buttonTap();
cleanSelfApp();
@@ -34,12 +42,22 @@ const SuccessScreen: React.FC = () => {
}
useEffect(() => {
if (disclosureStatus === 'success') {
notificationSuccess();
} else if (disclosureStatus === 'failure' || disclosureStatus === 'error') {
notificationError();
if (isFocused) {
console.log(
'[ProofRequestStatusScreen] State update while focused:',
currentState,
);
}
}, [disclosureStatus]);
if (currentState === 'completed') {
notificationSuccess();
setAnimationSource(succesAnimation);
} else if (currentState === 'failure' || currentState === 'error') {
notificationError();
setAnimationSource(failAnimation);
} else {
setAnimationSource(loadingAnimation);
}
}, [currentState, isFocused]);
return (
<ExpandableBottomLayout.Layout backgroundColor={white}>
@@ -51,8 +69,8 @@ const SuccessScreen: React.FC = () => {
>
<LottieView
autoPlay
loop={disclosureStatus === 'pending'}
source={getAnimation(disclosureStatus)}
loop={animationSource === loadingAnimation}
source={animationSource}
style={styles.animation}
cacheComposition={false}
renderMode="HARDWARE"
@@ -65,39 +83,37 @@ const SuccessScreen: React.FC = () => {
backgroundColor={white}
>
<View style={styles.content}>
<Title size="large">{getTitle(disclosureStatus)}</Title>
<Title size="large">{getTitle(currentState)}</Title>
<Info
status={disclosureStatus}
appName={appName === '' ? 'The app' : appName}
reason={discloseError?.reason ?? undefined}
currentState={currentState}
appName={appName ?? 'The app'}
reason={reason ?? undefined}
/>
</View>
<PrimaryButton
disabled={disclosureStatus === 'pending'}
disabled={
currentState !== 'completed' &&
currentState !== 'error' &&
currentState !== 'failure'
}
onPress={onOkPress}
>
{disclosureStatus === 'pending' ? <Spinner /> : 'OK'}
{currentState !== 'completed' &&
currentState !== 'error' &&
currentState !== 'failure' ? (
<Spinner />
) : (
'OK'
)}
</PrimaryButton>
</ExpandableBottomLayout.BottomSection>
</ExpandableBottomLayout.Layout>
);
};
function getAnimation(status: ProofStatusEnum) {
switch (status) {
case 'success':
return succesAnimation;
case 'failure':
case 'error':
return failAnimation;
default:
return loadingAnimation;
}
}
function getTitle(status: ProofStatusEnum) {
switch (status) {
case 'success':
function getTitle(currentState: string) {
switch (currentState) {
case 'completed':
return 'Proof Verified';
case 'failure':
case 'error':
@@ -108,30 +124,30 @@ function getTitle(status: ProofStatusEnum) {
}
function Info({
status,
currentState,
appName,
reason,
}: {
status: ProofStatusEnum;
currentState: string;
appName: string;
reason?: string;
}) {
if (status === 'success') {
if (currentState === 'completed') {
return (
<Description>
You've successfully proved your identity to{' '}
<BodyText style={typography.strong}>{appName}</BodyText>
</Description>
);
} else if (status === 'failure' || status === 'error') {
} else if (currentState === 'error' || currentState === 'failure') {
return (
<View style={{ gap: 8 }}>
<Description>
Unable to prove your identity to{' '}
<BodyText style={typography.strong}>{appName}</BodyText>
{status === 'error' && '. Due to technical issues.'}
{currentState === 'error' && '. Due to technical issues.'}
</Description>
{status === 'failure' && reason && (
{currentState === 'failure' && reason && (
<>
<Description>
<BodyText style={[typography.strong, { fontSize: 14 }]}>

View File

@@ -1,4 +1,4 @@
import { useNavigation } from '@react-navigation/native';
import { useIsFocused, useNavigation } from '@react-navigation/native';
import LottieView from 'lottie-react-native';
import React, {
useCallback,
@@ -24,27 +24,16 @@ import Disclosures from '../../components/Disclosures';
import { BodyText } from '../../components/typography/BodyText';
import { Caption } from '../../components/typography/Caption';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useApp } from '../../stores/appProvider';
import { usePassport } from '../../stores/passportDataProvider';
import {
globalSetDisclosureStatus,
ProofStatusEnum,
useProofInfo,
} from '../../stores/proofProvider';
import { useSelfAppStore } from '../../stores/selfAppStore';
import { black, slate300, white } from '../../utils/colors';
import { buttonTap } from '../../utils/haptic';
import {
isUserRegistered,
sendVcAndDisclosePayload,
} from '../../utils/proving/payload';
import { useProvingStore } from '../../utils/proving/provingMachine';
const ProveScreen: React.FC = () => {
const { navigate } = useNavigation();
const { getPassportDataAndSecret } = usePassport();
const { selectedApp, resetProof, cleanSelfApp } = useProofInfo();
const { handleProofResult } = useApp();
const isFocused = useIsFocused();
const selectedApp = useSelfAppStore(state => state.selfApp);
const selectedAppRef = useRef(selectedApp);
const isProcessing = useRef(false);
const [hasScrolledToBottom, setHasScrolledToBottom] = useState(false);
const [scrollViewContentHeight, setScrollViewContentHeight] = useState(0);
@@ -55,6 +44,7 @@ const ProveScreen: React.FC = () => {
() => scrollViewContentHeight <= scrollViewHeight,
[scrollViewContentHeight, scrollViewHeight],
);
const provingStore = useProvingStore();
/**
* Whenever the relationship between content height vs. scroll view height changes,
@@ -70,14 +60,16 @@ const ProveScreen: React.FC = () => {
useEffect(() => {
if (
!isFocused ||
!selectedApp ||
selectedAppRef.current?.sessionId === selectedApp.sessionId
) {
return; // Avoid unnecessary updates
return; // Avoid unnecessary updates or processing when not focused
}
selectedAppRef.current = selectedApp;
console.log('[ProveScreen] Selected app updated:', selectedApp);
}, [selectedApp]);
provingStore.init('disclose');
}, [selectedApp, isFocused]);
const disclosureOptions = useMemo(() => {
return (selectedApp?.disclosures as SelfAppDisclosureConfig) || [];
@@ -111,75 +103,13 @@ const ProveScreen: React.FC = () => {
return formatEndpoint(selectedApp.endpoint);
}, [selectedApp?.endpoint]);
const onVerify = useCallback(
async function () {
if (isProcessing.current) {
return;
}
isProcessing.current = true;
resetProof();
buttonTap();
const currentApp = selectedAppRef.current;
try {
let timeToNavigateToStatusScreen: NodeJS.Timeout;
const passportDataAndSecret = await getPassportDataAndSecret().catch(
(e: Error) => {
console.error('Error getting passport data', e);
globalSetDisclosureStatus?.(ProofStatusEnum.ERROR);
},
);
timeToNavigateToStatusScreen = setTimeout(() => {
navigate('ProofRequestStatusScreen');
}, 200);
if (!passportDataAndSecret) {
console.log('No passport data or secret');
globalSetDisclosureStatus?.(ProofStatusEnum.ERROR);
setTimeout(() => {
navigate('PassportDataNotFound');
}, 3000);
return;
}
const { passportData, secret } = passportDataAndSecret.data;
const isRegistered = await isUserRegistered(passportData, secret);
console.log('isRegistered', isRegistered);
if (!isRegistered) {
clearTimeout(timeToNavigateToStatusScreen);
console.log(
'User is not registered, sending to ConfirmBelongingScreen',
);
navigate('ConfirmBelongingScreen');
cleanSelfApp();
return;
}
console.log('currentApp', currentApp);
const status = await sendVcAndDisclosePayload(
secret,
passportData,
currentApp,
);
handleProofResult(
currentApp.sessionId,
status?.status === ProofStatusEnum.SUCCESS,
status?.error_code,
status?.reason,
);
} catch (e) {
console.log('Error in verification process');
globalSetDisclosureStatus?.(ProofStatusEnum.ERROR);
} finally {
isProcessing.current = false;
}
},
[navigate, getPassportDataAndSecret, handleProofResult, resetProof],
);
function onVerify() {
provingStore.setUserConfirmed();
buttonTap();
setTimeout(() => {
navigate('ProofRequestStatusScreen');
}, 200);
}
const handleScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
@@ -215,7 +145,7 @@ const ProveScreen: React.FC = () => {
<ExpandableBottomLayout.Layout flex={1} backgroundColor={black}>
<ExpandableBottomLayout.TopSection backgroundColor={black}>
<YStack alignItems="center">
{!selectedApp.sessionId ? (
{!selectedApp?.sessionId ? (
<LottieView
source={miscAnimation}
autoPlay
@@ -272,13 +202,13 @@ const ProveScreen: React.FC = () => {
paddingBottom={20}
>
Self will confirm that these details are accurate and none of your
confidential info will be revealed to {selectedApp.appName}
confidential info will be revealed to {selectedApp?.appName}
</Caption>
</View>
</ScrollView>
<HeldPrimaryButton
onPress={onVerify}
disabled={!selectedApp.sessionId || !hasScrolledToBottom}
disabled={!selectedApp?.sessionId || !hasScrolledToBottom}
>
{hasScrolledToBottom
? 'Hold To Verify'

View File

@@ -21,8 +21,7 @@ import useConnectionModal from '../../hooks/useConnectionModal';
import useHapticNavigation from '../../hooks/useHapticNavigation';
import QRScan from '../../images/icons/qr_code.svg';
import { ExpandableBottomLayout } from '../../layouts/ExpandableBottomLayout';
import { useApp } from '../../stores/appProvider';
import { useProofInfo } from '../../stores/proofProvider';
import { useSelfAppStore } from '../../stores/selfAppStore';
import { black, slate800, white } from '../../utils/colors';
interface QRCodeViewFinderScreenProps {}
@@ -45,9 +44,7 @@ const QRCodeViewFinderScreen: React.FC<QRCodeViewFinderScreenProps> = ({}) => {
const { visible: connectionModalVisible } = useConnectionModal();
const navigation = useNavigation();
const isFocused = useIsFocused();
const { setSelectedApp, cleanSelfApp } = useProofInfo();
const [doneScanningQR, setDoneScanningQR] = useState(false);
const { startAppListener } = useApp();
const navigateToProveScreen = useHapticNavigation('ProveScreen');
const onCancelPress = useHapticNavigation('Home');
@@ -73,14 +70,14 @@ const QRCodeViewFinderScreen: React.FC<QRCodeViewFinderScreenProps> = ({}) => {
const selfApp = encodedData.get('selfApp');
if (selfApp) {
const selfAppJson = JSON.parse(selfApp);
setSelectedApp(selfAppJson);
startAppListener(selfAppJson.sessionId, setSelectedApp);
useSelfAppStore.getState().setSelfApp(selfAppJson);
useSelfAppStore.getState().startAppListener(selfAppJson.sessionId);
setTimeout(() => {
navigateToProveScreen();
}, 100);
} else if (sessionId) {
cleanSelfApp();
startAppListener(sessionId, setSelectedApp);
useSelfAppStore.getState().cleanSelfApp();
useSelfAppStore.getState().startAppListener(sessionId);
setTimeout(() => {
navigateToProveScreen();
}, 100);
@@ -92,14 +89,7 @@ const QRCodeViewFinderScreen: React.FC<QRCodeViewFinderScreenProps> = ({}) => {
}
}
},
[
doneScanningQR,
navigation,
startAppListener,
cleanSelfApp,
setSelectedApp,
navigateToProveScreen,
],
[doneScanningQR, navigation, navigateToProveScreen],
);
const shouldRenderCamera = !connectionModalVisible && !doneScanningQR;

View File

@@ -3,13 +3,15 @@ import LottieView from 'lottie-react-native';
import React, { useCallback, useEffect } from 'react';
import { StyleSheet } from 'react-native';
import { PassportData } from '../../../common/src/utils/types';
import splashAnimation from '../assets/animations/splash.json';
import { useAuth } from '../stores/authProvider';
import { loadPassportDataAndSecret } from '../stores/passportDataProvider';
import { useProtocolStore } from '../stores/protocolStore';
import { useSettingStore } from '../stores/settingStore';
import { black } from '../utils/colors';
import { impactLight } from '../utils/haptic';
import { isUserRegistered } from '../utils/proving/payload';
import { isUserRegistered } from '../utils/proving/validateDocument';
const SplashScreen: React.FC = ({}) => {
const navigation = useNavigation();
@@ -35,7 +37,16 @@ const SplashScreen: React.FC = ({}) => {
}
const { passportData, secret } = JSON.parse(passportDataAndSecret);
if (!isPassportDataValid(passportData)) {
navigation.navigate('Launch');
return;
}
const environment =
(passportData as PassportData).documentType &&
(passportData as PassportData).documentType !== 'passport'
? 'stg'
: 'prod';
await useProtocolStore.getState().passport.fetch_all(environment);
const isRegistered = await isUserRegistered(passportData, secret);
console.log('User is registered:', isRegistered);
if (isRegistered) {
@@ -82,3 +93,22 @@ const styles = StyleSheet.create({
});
export default SplashScreen;
function isPassportDataValid(passportData: PassportData) {
if (!passportData) {
return false;
}
if (!passportData.passportMetadata) {
return false;
}
if (!passportData.passportMetadata.dg1HashFunction) {
return false;
}
if (!passportData.passportMetadata.eContentHashFunction) {
return false;
}
if (!passportData.passportMetadata.signedAttrHashFunction) {
return false;
}
return true;
}

View File

@@ -1,184 +0,0 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import io, { Socket } from 'socket.io-client';
import { WS_DB_RELAYER } from '../../../common/src/constants/constants';
import { SelfApp } from '../../../common/src/utils/appType';
interface IAppContext {
/**
* Call this function with the sessionId (scanned via ViewFinder) to
* start the mobile WS connection. Once connected, the server (via our
* Rust handler) will update the web client about mobile connectivity,
* prompting the web to send its SelfApp over. The mobile provider here
* listens for the "self_app" event and updates the navigation store.
*
* @param sessionId - The session ID from the scanned QR code.
* @param setSelectedApp - The function to update the selected app in the navigation store.
*/
startAppListener: (
sessionId: string,
setSelectedApp: (app: SelfApp) => void,
) => void;
/**
* Call this function with the sessionId and success status to notify the web app
* that the proof has been verified.
*
* @param sessionId - The session ID from the scanned QR code.
* @param success - Whether the proof was verified successfully.
*/
handleProofResult: (
sessionId: string,
success: boolean,
error_code?: string,
reason?: string,
) => void;
}
const AppContext = createContext<IAppContext>({
startAppListener: () => {},
handleProofResult: () => {},
});
const initSocket = (sessionId: string) => {
// Ensure the URL uses the proper WebSocket scheme.
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.
const socket = io(socketUrl, {
path: '/',
transports: ['websocket'],
forceNew: true,
query: {
sessionId,
clientType: 'mobile',
},
});
return socket;
};
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const socketRef = useRef<Socket | null>(null);
const startAppListener = (
sessionId: string,
setSelectedApp: (app: SelfApp) => void,
) => {
console.log(
`[AppProvider] Initializing WS connection with sessionId: ${sessionId}`,
);
try {
// If a socket connection already exists, disconnect it.
if (socketRef.current) {
console.log('[AppProvider] Disconnecting existing socket');
socketRef.current.disconnect();
}
const socket = initSocket(sessionId);
socketRef.current = socket;
socket.on('connect', () => {
console.log(
`[AppProvider] Mobile WS connected (id: ${socket.id}) with sessionId: ${sessionId}`,
);
});
// Listen for the event only once so that duplicate self_app events are ignored.
socket.once('self_app', (data: any) => {
console.log('[AppProvider] Received self_app event with data:', data);
try {
const appData: SelfApp =
typeof data === 'string' ? JSON.parse(data) : data;
if (!appData || !appData.sessionId) {
console.error('[AppProvider] Invalid app data received');
return;
}
console.log(
'[AppProvider] Processing valid app data:',
JSON.stringify(appData),
);
setSelectedApp(appData);
} catch (error) {
console.error('[AppProvider] Error processing app data:', error);
}
});
socket.on('connect_error', error => {
console.error('[AppProvider] Mobile WS connection error:', error);
});
socket.on('error', error => {
console.error('[AppProvider] Mobile WS error:', error);
});
socket.on('disconnect', (reason: string) => {
console.log('[AppProvider] Mobile WS disconnected:', reason);
});
} catch (error) {
console.error('[AppProvider] Exception in startAppListener:', error);
}
};
const handleProofResult = (
sessionId: string,
proof_verified: boolean,
error_code?: string,
reason?: string,
) => {
console.log(
'[AppProvider] handleProofResult called with sessionId:',
sessionId,
);
if (!socketRef.current) {
socketRef.current = initSocket(sessionId);
}
if (proof_verified) {
console.log('[AppProvider] Emitting proof_verified event with data:', {
session_id: sessionId,
});
socketRef.current.emit('proof_verified', {
session_id: sessionId,
});
} else {
console.log(
'[AppProvider] Emitting proof_generation_failed event with data:',
{
session_id: sessionId,
error_code,
reason,
},
);
socketRef.current.emit('proof_generation_failed', {
session_id: sessionId,
error_code,
reason,
});
}
};
useEffect(() => {
return () => {
if (socketRef.current) {
console.log('[AppProvider] Cleaning up WS connection on unmount');
socketRef.current.disconnect();
}
};
}, []);
return (
<AppContext.Provider value={{ startAppListener, handleProofResult }}>
{children}
</AppContext.Provider>
);
};
export const useApp = () => useContext(AppContext);

View File

@@ -1,146 +0,0 @@
import React, {
createContext,
PropsWithChildren,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { SelfApp } from '../../../common/src/utils/appType';
export enum ProofStatusEnum {
PENDING = 'pending',
SUCCESS = 'success',
FAILURE = 'failure',
ERROR = 'error',
}
export type DiscloseError = {
error_code?: string;
reason?: string;
};
interface IProofContext {
registrationStatus: ProofStatusEnum;
disclosureStatus: ProofStatusEnum;
discloseError: DiscloseError | undefined;
selectedApp: SelfApp;
setSelectedApp: (app: SelfApp) => void;
cleanSelfApp: () => void;
resetProof: () => void;
}
const defaults: IProofContext = {
registrationStatus: ProofStatusEnum.PENDING,
disclosureStatus: ProofStatusEnum.PENDING,
discloseError: undefined,
selectedApp: {
appName: '',
logoBase64: '',
scope: '',
endpointType: 'https',
endpoint: '',
header: '',
sessionId: '',
userId: '',
userIdType: 'uuid',
devMode: true,
disclosures: {},
},
setSelectedApp: (_: SelfApp) => undefined,
cleanSelfApp: () => undefined,
resetProof: () => undefined,
};
export const ProofContext = createContext<IProofContext>(defaults);
export let globalSetRegistrationStatus:
| ((status: ProofStatusEnum) => void)
| null = null;
export let globalSetDisclosureStatus:
| ((status: ProofStatusEnum, error?: DiscloseError) => void)
| null = null;
/*
store to manage the proof verification process, including app the is requesting, intemidiate status and final result
*/
export function ProofProvider({ children }: PropsWithChildren<{}>) {
const [registrationStatus, setRegistrationStatus] = useState<ProofStatusEnum>(
ProofStatusEnum.PENDING,
);
const [disclosureStatus, setDisclosureStatus] = useState<ProofStatusEnum>(
ProofStatusEnum.PENDING,
);
const [discloseError, setDiscloseError] = useState<DiscloseError | undefined>(
undefined,
);
const [selectedApp, setSelectedAppInternal] = useState<SelfApp>(
defaults.selectedApp,
);
const setSelectedApp = useCallback((app: SelfApp) => {
if (!app || Object.keys(app).length === 0) {
return;
}
setRegistrationStatus(ProofStatusEnum.PENDING);
setDiscloseError(undefined);
setSelectedAppInternal(app);
}, []);
const cleanSelfApp = useCallback(() => {
setSelectedAppInternal(defaults.selectedApp);
}, []);
// why do we have both resetProof and cleanSelfApp?
// possible we can make resetProof only about registration status, and clean app about disclosures status
const resetProof = useCallback(() => {
setRegistrationStatus(ProofStatusEnum.PENDING);
setDisclosureStatus(ProofStatusEnum.PENDING);
setDiscloseError(undefined);
}, []);
useEffect(() => {
globalSetRegistrationStatus = setRegistrationStatus;
globalSetDisclosureStatus = (status, error) => {
setDisclosureStatus(status);
setDiscloseError(error);
};
return () => {
globalSetRegistrationStatus = null;
globalSetDisclosureStatus = null;
};
}, [setRegistrationStatus, setDisclosureStatus]);
const publicApi: IProofContext = useMemo(
() => ({
registrationStatus,
disclosureStatus,
discloseError,
selectedApp,
setSelectedApp,
cleanSelfApp,
resetProof,
}),
[
registrationStatus,
disclosureStatus,
discloseError,
selectedApp,
setSelectedApp,
setDiscloseError,
cleanSelfApp,
resetProof,
],
);
return (
<ProofContext.Provider value={publicApi}>{children}</ProofContext.Provider>
);
}
export const useProofInfo = () => {
return React.useContext(ProofContext);
};

View File

@@ -0,0 +1,88 @@
import { create } from 'zustand';
import {
API_URL,
API_URL_STAGING,
CSCA_TREE_URL,
CSCA_TREE_URL_STAGING,
DSC_TREE_URL,
DSC_TREE_URL_STAGING,
IDENTITY_TREE_URL,
IDENTITY_TREE_URL_STAGING,
} from '../../../common/src/constants/constants';
interface ProtocolState {
passport: {
commitment_tree: any;
dsc_tree: any;
csca_tree: any;
deployed_circuits: any;
circuits_dns_mapping: any;
fetch_deployed_circuits: (environment: 'prod' | 'stg') => Promise<void>;
fetch_circuits_dns_mapping: (environment: 'prod' | 'stg') => Promise<void>;
fetch_csca_tree: (environment: 'prod' | 'stg') => Promise<void>;
fetch_dsc_tree: (environment: 'prod' | 'stg') => Promise<void>;
fetch_identity_tree: (environment: 'prod' | 'stg') => Promise<void>;
fetch_all: (environment: 'prod' | 'stg') => Promise<void>;
};
}
export const useProtocolStore = create<ProtocolState>((set, get) => ({
passport: {
commitment_tree: null,
dsc_tree: null,
csca_tree: null,
deployed_circuits: null,
circuits_dns_mapping: null,
fetch_all: async (environment: 'prod' | 'stg') => {
await Promise.all([
get().passport.fetch_deployed_circuits(environment),
get().passport.fetch_circuits_dns_mapping(environment),
get().passport.fetch_csca_tree(environment),
get().passport.fetch_dsc_tree(environment),
get().passport.fetch_identity_tree(environment),
]);
},
fetch_deployed_circuits: async (environment: 'prod' | 'stg') => {
const response = await fetch(
`${
environment === 'prod' ? API_URL : API_URL_STAGING
}/deployed-circuits`,
);
const data = await response.json();
set({ passport: { ...get().passport, deployed_circuits: data.data } });
},
fetch_circuits_dns_mapping: async (environment: 'prod' | 'stg') => {
const response = await fetch(
`${
environment === 'prod' ? API_URL : API_URL_STAGING
}/circuit-dns-mapping`,
);
const data = await response.json();
set({ passport: { ...get().passport, circuits_dns_mapping: data.data } });
},
fetch_csca_tree: async (environment: 'prod' | 'stg') => {
const response = await fetch(
`${environment === 'prod' ? CSCA_TREE_URL : CSCA_TREE_URL_STAGING}`,
);
const data = await response.json();
set({ passport: { ...get().passport, csca_tree: data.data } });
},
fetch_dsc_tree: async (environment: 'prod' | 'stg') => {
const response = await fetch(
`${environment === 'prod' ? DSC_TREE_URL : DSC_TREE_URL_STAGING}`,
);
const data = await response.json();
set({ passport: { ...get().passport, dsc_tree: data.data } });
},
fetch_identity_tree: async (environment: 'prod' | 'stg') => {
const response = await fetch(
`${
environment === 'prod' ? IDENTITY_TREE_URL : IDENTITY_TREE_URL_STAGING
}`,
);
const data = await response.json();
set({ passport: { ...get().passport, commitment_tree: data.data } });
},
},
}));

View File

@@ -0,0 +1,189 @@
import io, { Socket } from 'socket.io-client';
import { create } from 'zustand';
import { WS_DB_RELAYER } from '../../../common/src/constants/constants';
import { SelfApp } from '../../../common/src/utils/appType';
interface SelfAppState {
selfApp: SelfApp | null;
sessionId: string | null;
socket: Socket | null;
startAppListener: (sessionId: string) => void;
cleanSelfApp: () => void;
setSelfApp: (selfApp: SelfApp | null) => void;
_initSocket: (sessionId: string) => Socket;
handleProofResult: (
proof_verified: boolean,
error_code?: string,
reason?: string,
) => void;
}
export const useSelfAppStore = create<SelfAppState>((set, get) => ({
selfApp: null,
sessionId: null,
socket: null,
_initSocket: (sessionId: string): Socket => {
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.
const socket = io(socketUrl, {
path: '/',
transports: ['websocket'],
forceNew: true, // Ensure a new connection is established
query: {
sessionId,
clientType: 'mobile',
},
});
return socket;
},
setSelfApp: (selfApp: SelfApp | null) => {
set({ selfApp });
},
startAppListener: (sessionId: string) => {
console.log(
`[SelfAppStore] Initializing WS connection with sessionId: ${sessionId}`,
);
const currentSocket = get().socket;
// If a socket connection exists for a different session, disconnect it.
if (currentSocket && get().sessionId !== sessionId) {
console.log(
'[SelfAppStore] Disconnecting existing socket for old session.',
);
currentSocket.disconnect();
set({ socket: null, sessionId: null, selfApp: null });
} else if (currentSocket && get().sessionId === sessionId) {
console.log('[SelfAppStore] Already connected with the same session ID.');
return; // Avoid reconnecting if already connected with the same session
}
try {
const socket = get()._initSocket(sessionId);
set({ socket, sessionId });
socket.on('connect', () => {
console.log(
`[SelfAppStore] Mobile WS connected (id: ${socket.id}) with sessionId: ${sessionId}`,
);
});
// Listen for the event only once per connection attempt
socket.once('self_app', (data: any) => {
console.log('[SelfAppStore] Received self_app event with data:', data);
try {
const appData: SelfApp =
typeof data === 'string' ? JSON.parse(data) : data;
// Basic validation
if (!appData || typeof appData !== 'object' || !appData.sessionId) {
console.error('[SelfAppStore] Invalid app data received:', appData);
// Optionally clear the app data or handle the error appropriately
set({ selfApp: null });
return;
}
if (appData.sessionId !== get().sessionId) {
console.warn(
`[SelfAppStore] Received SelfApp for session ${
appData.sessionId
}, but current session is ${get().sessionId}. Ignoring.`,
);
return;
}
console.log(
'[SelfAppStore] Processing valid app data:',
JSON.stringify(appData),
);
set({ selfApp: appData });
} catch (error) {
console.error('[SelfAppStore] Error processing app data:', error);
set({ selfApp: null }); // Clear app data on parsing error
}
});
socket.on('connect_error', error => {
console.error('[SelfAppStore] Mobile WS connection error:', error);
// Clean up on connection error
get().cleanSelfApp();
});
socket.on('error', error => {
console.error('[SelfAppStore] Mobile WS error:', error);
// Consider if cleanup is needed here as well
});
socket.on('disconnect', (reason: string) => {
console.log('[SelfAppStore] Mobile WS disconnected:', reason);
// Prevent cleaning up if disconnect was initiated by cleanSelfApp
if (get().socket === socket) {
console.log('[SelfAppStore] Cleaning up state on disconnect.');
set({ socket: null, sessionId: null, selfApp: null });
}
});
} catch (error) {
console.error('[SelfAppStore] Exception in startAppListener:', error);
get().cleanSelfApp(); // Clean up on exception
}
},
cleanSelfApp: () => {
console.log('[SelfAppStore] Cleaning up SelfApp state and WS connection.');
const socket = get().socket;
if (socket) {
socket.disconnect();
}
// Reset state
set({ selfApp: null, sessionId: null, socket: null });
},
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.',
);
return;
}
console.log(
`[SelfAppStore] handleProofResult called for sessionId: ${sessionId}, verified: ${proof_verified}`,
);
if (proof_verified) {
console.log('[SelfAppStore] Emitting proof_verified event with data:', {
session_id: sessionId,
});
socket.emit('proof_verified', {
session_id: sessionId,
});
} else {
console.log(
'[SelfAppStore] Emitting proof_generation_failed event with data:',
{
session_id: sessionId,
error_code,
reason,
},
);
socket.emit('proof_generation_failed', {
session_id: sessionId,
error_code,
reason,
});
}
},
}));

View File

@@ -0,0 +1,68 @@
import queryString from 'query-string';
import { Linking } from 'react-native';
import { navigationRef } from '../Navigation';
import { useSelfAppStore } from '../stores/selfAppStore';
/**
* Decodes a URL-encoded string.
* @param {string} encodedUrl
* @returns {string}
*/
const decodeUrl = (encodedUrl: string): string => {
try {
return decodeURIComponent(encodedUrl);
} catch (error) {
console.error('Error decoding URL:', error);
return encodedUrl;
}
};
const handleUrl = (uri: string) => {
const decodedUri = decodeUrl(uri);
const encodedData = queryString.parseUrl(decodedUri).query;
const sessionId = encodedData.sessionId;
const selfAppStr = encodedData.selfApp as string | undefined;
if (selfAppStr) {
try {
const selfAppJson = JSON.parse(selfAppStr);
useSelfAppStore.getState().setSelfApp(selfAppJson);
navigationRef.navigate('ProveScreen');
return;
} catch (error) {
console.error('Error parsing selfApp:', error);
navigationRef.navigate('QRCodeTrouble');
}
}
if (sessionId && typeof sessionId === 'string') {
useSelfAppStore.getState().cleanSelfApp();
useSelfAppStore.getState().startAppListener(sessionId);
navigationRef.navigate('ProveScreen');
} else {
console.error('No sessionId or selfApp found in the data');
navigationRef.navigate('QRCodeTrouble');
}
};
export const setupUniversalLinkListenerInNavigation = () => {
const handleNavigation = (url: string) => {
handleUrl(url);
};
Linking.getInitialURL().then(url => {
if (url) {
handleNavigation(url);
}
});
const linkingEventListener = Linking.addEventListener('url', ({ url }) => {
handleNavigation(url);
});
return () => {
linkingEventListener.remove();
};
};

View File

@@ -1,330 +0,0 @@
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { poseidon2 } from 'poseidon-lite';
import {
API_URL,
API_URL_STAGING,
PASSPORT_ATTESTATION_ID,
WS_RPC_URL_VC_AND_DISCLOSE,
} from '../../../../common/src/constants/constants';
import { EndpointType, SelfApp } from '../../../../common/src/utils/appType';
import { getCircuitNameFromPassportData } from '../../../../common/src/utils/circuits/circuitsName';
import {
generateCommitment,
generateNullifier,
} from '../../../../common/src/utils/passports/passport';
import {
getCommitmentTree,
getDSCTree,
getLeafDscTree,
} from '../../../../common/src/utils/trees';
import { PassportData } from '../../../../common/src/utils/types';
import { ProofStatusEnum } from '../../stores/proofProvider';
import {
generateTeeInputsDsc,
generateTeeInputsRegister,
generateTeeInputsVCAndDisclose,
} from './inputs';
import { sendPayload } from './tee';
export type PassportSupportStatus =
| 'passport_metadata_missing'
| 'csca_not_found'
| 'registration_circuit_not_supported'
| 'dsc_circuit_not_supported'
| 'passport_supported';
export async function checkPassportSupported(
passportData: PassportData,
): Promise<{
status: PassportSupportStatus;
details: string;
}> {
const passportMetadata = passportData.passportMetadata;
if (!passportMetadata) {
console.log('Passport metadata is null');
return { status: 'passport_metadata_missing', details: passportData.dsc };
}
if (!passportMetadata.cscaFound) {
console.log('CSCA not found');
return { status: 'csca_not_found', details: passportData.dsc };
}
const circuitNameRegister = getCircuitNameFromPassportData(
passportData,
'register',
);
const deployedCircuits = await getDeployedCircuits(passportData.documentType);
console.log('circuitNameRegister', circuitNameRegister);
if (
!circuitNameRegister ||
!deployedCircuits.REGISTER.includes(circuitNameRegister)
) {
return {
status: 'registration_circuit_not_supported',
details: circuitNameRegister,
};
}
const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc');
if (!circuitNameDsc || !deployedCircuits.DSC.includes(circuitNameDsc)) {
console.log('DSC circuit not supported:', circuitNameDsc);
return { status: 'dsc_circuit_not_supported', details: circuitNameDsc };
}
console.log('Passport supported');
return { status: 'passport_supported', details: 'null' };
}
export async function sendRegisterPayload(
passportData: PassportData,
secret: string,
circuitDNSMapping: Record<string, string>,
endpointType: EndpointType,
) {
const { inputs, circuitName } = await generateTeeInputsRegister(
secret,
passportData,
endpointType,
);
await sendPayload(
inputs,
'register',
circuitName,
endpointType,
'https://self.xyz',
(circuitDNSMapping as any).REGISTER[circuitName],
undefined,
{
updateGlobalOnSuccess: true,
updateGlobalOnFailure: true,
flow: 'registration',
},
);
}
async function checkIdPassportDscIsInTree(
passportData: PassportData,
dscTree: string,
circuitDNSMapping: Record<string, string>,
endpointType: EndpointType,
): Promise<boolean> {
const hashFunction = (a: any, b: any) => poseidon2([a, b]);
const tree = LeanIMT.import(hashFunction, dscTree);
const leaf = getLeafDscTree(
passportData.dsc_parsed!,
passportData.csca_parsed!,
);
console.log('DSC leaf:', leaf);
const index = tree.indexOf(BigInt(leaf));
if (index === -1) {
console.log('DSC is not found in the tree, sending DSC payload');
const dscStatus = await sendDscPayload(
passportData,
circuitDNSMapping,
endpointType,
);
if (dscStatus.status !== ProofStatusEnum.SUCCESS) {
console.log('DSC proof failed');
return false;
}
} else {
// console.log('DSC i found in the tree, sending DSC payload for debug');
// const dscStatus = await sendDscPayload(passportData);
// if (dscStatus !== ProofStatusEnum.SUCCESS) {
// console.log('DSC proof failed');
// return false;
// }
console.log('DSC is found in the tree, skipping DSC payload');
}
return true;
}
export async function sendDscPayload(
passportData: PassportData,
circuitDNSMapping: Record<string, string>,
endpointType: EndpointType,
): Promise<{ status: ProofStatusEnum; error_code?: string; reason?: string }> {
if (!passportData) {
return { status: ProofStatusEnum.FAILURE };
}
// const isSupported = checkPassportSupported(passportData);
// if (!isSupported) {
// console.log('Passport not supported');
// return false;
// }
const { inputs, circuitName } = await generateTeeInputsDsc(
passportData,
endpointType,
);
const dscStatus = await sendPayload(
inputs,
'dsc',
circuitName,
endpointType,
'https://self.xyz',
(circuitDNSMapping.DSC as any)[circuitName],
undefined,
{ updateGlobalOnSuccess: false },
);
return dscStatus;
}
export async function sendVcAndDisclosePayload(
secret: string,
passportData: PassportData | null,
selfApp: SelfApp,
) {
if (!passportData) {
return null;
}
const { inputs, circuitName } = await generateTeeInputsVCAndDisclose(
secret,
passportData,
selfApp,
);
return await sendPayload(
inputs,
'vc_and_disclose',
circuitName,
selfApp.endpointType,
selfApp.endpoint,
WS_RPC_URL_VC_AND_DISCLOSE,
undefined,
{
updateGlobalOnSuccess: true,
updateGlobalOnFailure: true,
flow: 'disclosure',
},
);
}
/*** Logic Flow ****/
export async function isUserRegistered(
passportData: PassportData,
secret: string,
) {
if (!passportData) {
return false;
}
const commitment = generateCommitment(
secret,
PASSPORT_ATTESTATION_ID,
passportData,
);
const serializedTree = await getCommitmentTree(passportData.documentType);
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
const index = tree.indexOf(BigInt(commitment));
return index !== -1;
}
export async function isPassportNullified(passportData: PassportData) {
const nullifier = generateNullifier(passportData);
const nullifierHex = `0x${BigInt(nullifier).toString(16)}`;
console.log('checking for nullifier', nullifierHex);
const response = await fetch(`${API_URL}/is-nullifier-onchain/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ nullifier: nullifierHex }),
});
const data = await response.json();
console.log('isPassportNullified', data);
return data.data;
}
export async function registerPassport(
passportData: PassportData,
secret: string,
) {
// First get the mapping, then use it for the check
const endpointType =
passportData.documentType && passportData.documentType === 'mock_passport'
? 'staging_celo'
: 'celo';
const [circuitDNSMapping, dscTree] = await Promise.all([
getCircuitDNSMapping(endpointType),
getDSCTree(endpointType),
]);
console.log('circuitDNSMapping', circuitDNSMapping);
const dscOk = await checkIdPassportDscIsInTree(
passportData,
dscTree,
circuitDNSMapping,
endpointType,
);
if (!dscOk) {
return;
}
await sendRegisterPayload(
passportData,
secret,
circuitDNSMapping,
endpointType,
);
}
export async function getDeployedCircuits(documentType: string) {
console.log('Fetching deployed circuits from api');
const baseUrl =
!documentType ||
typeof documentType !== 'string' ||
documentType === 'passport'
? API_URL
: API_URL_STAGING;
const response = await fetch(`${baseUrl}/deployed-circuits/`);
if (!response.ok) {
throw new Error(
`API server error: ${response.status} ${response.statusText}`,
);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/html')) {
throw new Error(
'API returned HTML instead of JSON - server may be down or misconfigured',
);
}
try {
const data = await response.json();
if (!data.data || !data.data.REGISTER || !data.data.DSC) {
throw new Error(
'Invalid data structure received from API: missing REGISTER or DSC fields',
);
}
return data.data;
} catch (error) {
throw new Error('API returned invalid JSON response - server may be down');
}
}
export async function getCircuitDNSMapping(endpointType?: EndpointType) {
console.log('Fetching deployed circuits from api');
const baseUrl =
endpointType === 'celo' || endpointType === 'https'
? API_URL
: API_URL_STAGING;
const response = await fetch(`${baseUrl}/circuit-dns-mapping/`);
if (!response.ok) {
throw new Error(
`API server error: ${response.status} ${response.statusText}`,
);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/html')) {
throw new Error(
'API returned HTML instead of JSON - server may be down or misconfigured',
);
}
try {
const data = await response.json();
if (!data.data) {
throw new Error(
'Invalid data structure received from API: missing data field',
);
}
return data.data;
} catch (error) {
throw new Error('API returned invalid JSON response - server may be down');
}
}

View File

@@ -10,7 +10,7 @@ import {
DEFAULT_MAJORITY,
PASSPORT_ATTESTATION_ID,
} from '../../../../common/src/constants/constants';
import { EndpointType, SelfApp } from '../../../../common/src/utils/appType';
import { SelfApp } from '../../../../common/src/utils/appType';
import { getCircuitNameFromPassportData } from '../../../../common/src/utils/circuits/circuitsName';
import {
generateCircuitInputsDSC,
@@ -18,48 +18,39 @@ import {
generateCircuitInputsVCandDisclose,
} from '../../../../common/src/utils/circuits/generateInputs';
import { hashEndpointWithScope } from '../../../../common/src/utils/scope';
import {
getCommitmentTree,
getCSCATree,
getDSCTree,
} from '../../../../common/src/utils/trees';
import { PassportData } from '../../../../common/src/utils/types';
import { useProtocolStore } from '../../stores/protocolStore';
export async function generateTeeInputsRegister(
export function generateTEEInputsRegister(
secret: string,
passportData: PassportData,
endpointType: EndpointType,
dscTree: string,
) {
const serialized_dsc_tree = await getDSCTree(endpointType);
const inputs = generateCircuitInputsRegister(
secret,
passportData,
serialized_dsc_tree,
);
const inputs = generateCircuitInputsRegister(secret, passportData, dscTree);
const circuitName = getCircuitNameFromPassportData(passportData, 'register');
if (circuitName == null) {
throw new Error('Circuit name is null');
}
return { inputs, circuitName };
const endpointType =
passportData.documentType && passportData.documentType !== 'passport'
? 'staging_celo'
: 'celo';
const endpoint = 'https://self.xyz';
return { inputs, circuitName, endpointType, endpoint };
}
export async function generateTeeInputsDsc(
export function generateTEEInputsDSC(
passportData: PassportData,
endpointType: EndpointType,
cscaTree: string[][],
) {
const serialized_csca_tree = await getCSCATree(endpointType);
const inputs = generateCircuitInputsDSC(
passportData.dsc,
serialized_csca_tree,
);
const inputs = generateCircuitInputsDSC(passportData.dsc, cscaTree);
const circuitName = getCircuitNameFromPassportData(passportData, 'dsc');
if (circuitName == null) {
throw new Error('Circuit name is null');
}
return { inputs, circuitName };
const endpointType =
passportData.documentType && passportData.documentType !== 'passport'
? 'staging_celo'
: 'celo';
const endpoint = 'https://self.xyz';
return { inputs, circuitName, endpointType, endpoint };
}
export async function generateTeeInputsVCAndDisclose(
export function generateTEEInputsDisclose(
secret: string,
passportData: PassportData,
selfApp: SelfApp,
@@ -67,7 +58,6 @@ export async function generateTeeInputsVCAndDisclose(
const { scope, userId, disclosures, endpoint } = selfApp;
const scope_hash = hashEndpointWithScope(endpoint, scope);
const selector_dg1 = Array(88).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
@@ -87,17 +77,9 @@ export async function generateTeeInputsVCAndDisclose(
const selector_ofac = disclosures.ofac ? 1 : 0;
const { passportNoAndNationalitySMT, nameAndDobSMT, nameAndYobSMT } =
await getOfacSMTs();
const serialized_tree = await getCommitmentTree(passportData.documentType);
getOfacSMTs();
const serialized_tree = useProtocolStore.getState().passport.commitment_tree; //await getCommitmentTree(passportData.documentType);
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serialized_tree);
console.log('tree', tree);
// const commitment = generateCommitment(
// secret,
// PASSPORT_ATTESTATION_ID,
// passportData,
// );
// tree.insert(BigInt(commitment));
// Uncomment to add artificially the commitment to the tree
const inputs = generateCircuitInputsVCandDisclose(
secret,
@@ -115,12 +97,17 @@ export async function generateTeeInputsVCAndDisclose(
disclosures.excludedCountries ?? [],
userId,
);
return { inputs, circuitName: 'vc_and_disclose' };
return {
inputs,
circuitName: 'vc_and_disclose',
endpointType: selfApp.endpointType,
endpoint: selfApp.endpoint,
};
}
/*** DISCLOSURE ***/
async function getOfacSMTs() {
function getOfacSMTs() {
// TODO: get the SMT from an endpoint
const passportNoAndNationalitySMT = new SMT(poseidon2, true);
passportNoAndNationalitySMT.import(passportNoAndNationalitySMTData);

View File

@@ -0,0 +1,751 @@
import forge from 'node-forge';
import io, { Socket } from 'socket.io-client';
import { v4 } from 'uuid';
import { AnyActorRef, createActor, createMachine } from 'xstate';
import { create } from 'zustand';
import { WS_RPC_URL_VC_AND_DISCLOSE } from '../../../../common/src/constants/constants';
import { EndpointType, SelfApp } from '../../../../common/src/utils/appType';
import { getCircuitNameFromPassportData } from '../../../../common/src/utils/circuits/circuitsName';
import { navigationRef } from '../../Navigation';
import {
clearPassportData,
loadPassportDataAndSecret,
} from '../../stores/passportDataProvider';
import { useProtocolStore } from '../../stores/protocolStore';
import { useSelfAppStore } from '../../stores/selfAppStore';
import { getPublicKey, verifyAttestation } from './attest';
import {
generateTEEInputsDisclose,
generateTEEInputsDSC,
generateTEEInputsRegister,
} from './provingInputs';
import {
clientKey,
clientPublicKeyHex,
ec,
encryptAES256GCM,
getPayload,
getWSDbRelayerUrl,
} from './provingUtils';
import {
checkIfPassportDscIsInTree,
checkPassportSupported,
isPassportNullified,
isUserRegistered,
} from './validateDocument';
const provingMachine = createMachine({
id: 'proving',
initial: 'idle',
states: {
idle: {
on: {
FETCH_DATA: 'fetching_data',
ERROR: 'error',
},
},
fetching_data: {
on: {
FETCH_SUCCESS: 'validating_document',
FETCH_ERROR: 'error',
},
},
validating_document: {
on: {
VALIDATION_SUCCESS: 'init_tee_connexion',
VALIDATION_ERROR: 'error',
ALREADY_REGISTERED: 'completed',
PASSPORT_NOT_SUPPORTED: 'passport_not_supported',
ACCOUNT_RECOVERY_CHOICE: 'account_recovery_choice',
PASSPORT_DATA_NOT_FOUND: 'passport_data_not_found',
},
},
init_tee_connexion: {
on: {
CONNECT_SUCCESS: 'ready_to_prove',
CONNECT_ERROR: 'error',
},
},
ready_to_prove: {
on: {
START_PROVING: 'proving',
PROVE_ERROR: 'error',
},
},
proving: {
on: {
PROVE_SUCCESS: 'post_proving',
PROVE_ERROR: 'error',
PROVE_FAILURE: 'failure',
},
},
post_proving: {
on: {
SWITCH_TO_REGISTER: 'fetching_data',
COMPLETED: 'completed',
},
},
completed: {
type: 'final',
},
error: {
type: 'final',
},
passport_not_supported: {
type: 'final',
},
account_recovery_choice: {
type: 'final',
},
passport_data_not_found: {
type: 'final',
},
failure: {
type: 'final',
},
},
});
export type provingMachineCircuitType = 'register' | 'dsc' | 'disclose';
interface ProvingState {
currentState: string;
attestation: any;
serverPublicKey: string | null;
sharedKey: Buffer | null;
wsConnection: WebSocket | null;
socketConnection: Socket | null;
uuid: string | null;
userConfirmed: boolean;
passportData: any | null;
secret: string | null;
circuitType: provingMachineCircuitType | null;
error_code: string | null;
reason: string | null;
init: (circuitType: 'dsc' | 'disclose' | 'register') => Promise<void>;
startFetchingData: () => Promise<void>;
validatingDocument: () => Promise<void>;
initTeeConnection: () => Promise<boolean>;
startProving: () => Promise<void>;
postProving: () => void;
setUserConfirmed: () => void;
_closeConnections: () => void;
_generatePayload: () => Promise<any>;
_handleWebSocketMessage: (event: MessageEvent) => Promise<void>;
_startSocketIOStatusListener: (
receivedUuid: string,
endpointType: EndpointType,
) => void;
_handleWsOpen: () => void;
_handleWsError: (error: Event) => void;
_handleWsClose: (event: CloseEvent) => void;
}
export const useProvingStore = create<ProvingState>((set, get) => {
let actor: AnyActorRef | null = null;
function setupActorSubscriptions(newActor: AnyActorRef) {
newActor.subscribe((state: any) => {
console.log(`State transition: ${state.value}`);
set({ currentState: state.value as string });
if (state.value === 'fetching_data') {
get().startFetchingData();
}
if (state.value === 'validating_document') {
get().validatingDocument();
}
if (state.value === 'init_tee_connexion') {
get().initTeeConnection();
}
if (state.value === 'ready_to_prove' && get().userConfirmed) {
get().startProving();
}
if (state.value === 'post_proving') {
get().postProving();
}
if (get().circuitType !== 'disclose' && state.value === 'error') {
setTimeout(() => {
if (navigationRef.isReady()) {
navigationRef.navigate('Launch');
}
}, 3000);
}
if (state.value === 'completed') {
if (get().circuitType !== 'disclose' && navigationRef.isReady()) {
setTimeout(() => {
navigationRef.navigate('AccountVerifiedSuccess');
}, 3000);
}
if (get().circuitType === 'disclose') {
useSelfAppStore.getState().handleProofResult(true);
}
}
if (state.value === 'passport_not_supported') {
if (navigationRef.isReady()) {
navigationRef.navigate('UnsupportedPassport');
}
}
if (state.value === 'account_recovery_choice') {
if (navigationRef.isReady()) {
navigationRef.navigate('AccountRecoveryChoice');
}
}
if (state.value === 'passport_data_not_found') {
if (navigationRef.isReady()) {
navigationRef.navigate('PassportDataNotFound');
}
}
if (state.value === 'failure') {
if (get().circuitType === 'disclose') {
const { error_code, reason } = get();
useSelfAppStore
.getState()
.handleProofResult(
false,
error_code ?? undefined,
reason ?? undefined,
);
}
}
if (state.value === 'error') {
if (get().circuitType === 'disclose') {
useSelfAppStore.getState().handleProofResult(false, 'error', 'error');
}
}
});
}
return {
currentState: 'idle',
attestation: null,
serverPublicKey: null,
sharedKey: null,
wsConnection: null,
socketConnection: null,
uuid: null,
userConfirmed: false,
passportData: null,
secret: null,
circuitType: null,
selfApp: null,
error_code: null,
reason: null,
_handleWebSocketMessage: async (event: MessageEvent) => {
if (!actor) {
console.error('Cannot process message: State machine not initialized.');
return;
}
try {
const result = JSON.parse(event.data);
if (result.result?.attestation) {
const attestationData = result.result.attestation;
set({ attestation: attestationData });
const serverPubkey = getPublicKey(attestationData);
const verified = await verifyAttestation(attestationData);
if (!verified) {
console.error('Attestation verification failed');
actor!.send({ type: 'CONNECT_ERROR' });
return;
}
const serverKey = ec.keyFromPublic(serverPubkey as string, 'hex');
const derivedKey = clientKey.derive(serverKey.getPublic());
set({
serverPublicKey: serverPubkey,
sharedKey: Buffer.from(derivedKey.toArray('be', 32)),
});
actor!.send({ type: 'CONNECT_SUCCESS' });
} else if (
result.id === 2 &&
typeof result.result === 'string' &&
!result.error
) {
console.log('Received message with status:', result.id);
const statusUuid = result.result;
if (get().uuid !== statusUuid) {
console.warn(
`Received status UUID (${statusUuid}) does not match stored UUID (${
get().uuid
}). Using received UUID.`,
);
}
const { passportData } = get();
if (!statusUuid) {
console.error(
'Cannot start Socket.IO listener: UUID missing from state or response.',
);
actor!.send({ type: 'PROVE_ERROR' });
return;
}
if (!passportData) {
console.error(
'Cannot start Socket.IO listener: passportData missing from state.',
);
actor!.send({ type: 'PROVE_ERROR' });
return;
}
const socketEndpointType =
passportData.documentType === 'passport' ? 'celo' : 'staging_celo';
get()._startSocketIOStatusListener(statusUuid, socketEndpointType);
} else if (result.error) {
console.error('Received error from TEE:', result.error);
actor!.send({ type: 'PROVE_ERROR' });
} else {
console.warn('Received unknown message format from TEE:', result);
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
if (get().currentState === 'init_tee_connexion') {
actor!.send({ type: 'CONNECT_ERROR' });
} else {
actor!.send({ type: 'PROVE_ERROR' });
}
}
},
_startSocketIOStatusListener: (
receivedUuid: string,
endpointType: EndpointType,
) => {
if (!actor) {
console.error('Cannot start Socket.IO listener: Actor not available.');
return;
}
const url = getWSDbRelayerUrl(endpointType);
let socket: Socket | null = io(url, {
path: '/',
transports: ['websocket'],
});
set({ socketConnection: socket });
socket.on('connect', () => {
socket?.emit('subscribe', receivedUuid);
});
socket.on('status', (message: any) => {
const data =
typeof message === 'string' ? JSON.parse(message) : message;
console.log('Received status update with status:', data.status);
if (data.status === 3 || data.status === 5) {
console.error(
'Proof generation/verification failed (status 3 or 5).',
);
set({ error_code: data.error_code, reason: data.reason });
actor!.send({ type: 'PROVE_FAILURE' });
socket?.disconnect();
set({ socketConnection: null });
} else if (data.status === 4) {
socket?.disconnect();
set({ socketConnection: null });
actor!.send({ type: 'PROVE_SUCCESS' });
}
});
socket.on('disconnect', (reason: string) => {
console.log(`SocketIO disconnected. Reason: ${reason}`);
const currentActor = actor;
if (get().currentState === 'ready_to_prove' && currentActor) {
console.error(
'SocketIO disconnected unexpectedly during proof listening.',
);
currentActor.send({ type: 'PROVE_ERROR' });
}
set({ socketConnection: null });
});
socket.on('connect_error', error => {
console.error('SocketIO connection error:', error);
actor!.send({ type: 'PROVE_ERROR' });
set({ socketConnection: null });
});
},
_handleWsOpen: () => {
if (!actor) {
return;
}
const ws = get().wsConnection;
if (!ws) {
return;
}
const connectionUuid = v4();
set({ uuid: connectionUuid });
const helloBody = {
jsonrpc: '2.0',
method: 'openpassport_hello',
id: 1,
params: {
user_pubkey: [
4,
...Array.from(Buffer.from(clientPublicKeyHex, 'hex')),
],
uuid: connectionUuid,
},
};
ws.send(JSON.stringify(helloBody));
},
_handleWsError: (error: Event) => {
console.error('TEE WebSocket error event:', error);
if (!actor) {
return;
}
get()._handleWebSocketMessage(
new MessageEvent('error', {
data: JSON.stringify({ error: 'WebSocket connection error' }),
}),
);
},
_handleWsClose: (event: CloseEvent) => {
console.log(
`TEE WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
);
if (!actor) {
return;
}
const currentState = get().currentState;
if (
currentState === 'init_tee_connexion' ||
currentState === 'proving' ||
currentState === 'listening_for_status'
) {
console.error(
`TEE WebSocket closed unexpectedly during ${currentState}.`,
);
get()._handleWebSocketMessage(
new MessageEvent('error', {
data: JSON.stringify({ error: 'WebSocket closed unexpectedly' }),
}),
);
}
if (get().wsConnection) {
set({ wsConnection: null });
}
},
init: async (circuitType: 'dsc' | 'disclose' | 'register') => {
get()._closeConnections();
if (actor) {
try {
actor.stop();
} catch (error) {
console.error('Error stopping actor:', error);
}
}
set({
currentState: 'idle',
attestation: null,
serverPublicKey: null,
sharedKey: null,
wsConnection: null,
socketConnection: null,
uuid: null,
userConfirmed: false,
passportData: null,
secret: null,
});
actor = createActor(provingMachine);
setupActorSubscriptions(actor);
actor.start();
const passportDataAndSecretStr = await loadPassportDataAndSecret();
if (!passportDataAndSecretStr) {
actor!.send({ type: 'ERROR' });
return;
}
const passportDataAndSecret = JSON.parse(passportDataAndSecretStr);
const { passportData, secret } = passportDataAndSecret;
set({ passportData, secret });
set({ circuitType });
actor.send({ type: 'FETCH_DATA' });
},
startFetchingData: async () => {
_checkActorInitialized(actor);
try {
const { passportData } = get();
const env =
passportData.documentType && passportData.documentType !== 'passport'
? 'stg'
: 'prod';
await useProtocolStore.getState().passport.fetch_all(env);
actor!.send({ type: 'FETCH_SUCCESS' });
} catch (error) {
console.error('Error fetching data:', error);
actor!.send({ type: 'FETCH_ERROR' });
}
},
validatingDocument: async () => {
_checkActorInitialized(actor);
// TODO: for the disclosure, we could check that the selfApp is a valid one.
try {
const { passportData, secret, circuitType } = get();
const isSupported = await checkPassportSupported(passportData);
if (isSupported.status !== 'passport_supported') {
console.error(
'Passport not supported:',
isSupported.status,
isSupported.details,
);
await clearPassportData();
actor!.send({ type: 'PASSPORT_NOT_SUPPORTED' });
return;
}
const isRegistered = await isUserRegistered(
passportData,
secret as string,
);
if (circuitType === 'disclose') {
if (isRegistered) {
actor!.send({ type: 'VALIDATION_SUCCESS' });
return;
} else {
actor!.send({ type: 'PASSPORT_DATA_NOT_FOUND' });
return;
}
} else if (isRegistered) {
actor!.send({ type: 'ALREADY_REGISTERED' });
return;
}
const isNullifierOnchain = await isPassportNullified(passportData);
if (isNullifierOnchain) {
console.log(
'Passport is nullified, but not registered with this secret. Navigating to AccountRecoveryChoice',
);
actor!.send({ type: 'ACCOUNT_RECOVERY_CHOICE' });
return;
}
const isDscRegistered = await checkIfPassportDscIsInTree(
passportData,
useProtocolStore.getState().passport.dsc_tree,
);
if (isDscRegistered) {
set({ circuitType: 'register' });
}
actor!.send({ type: 'VALIDATION_SUCCESS' });
} catch (error) {
console.error('Error validating passport:', error);
actor!.send({ type: 'VALIDATION_ERROR' });
}
},
initTeeConnection: async (): Promise<boolean> => {
const circuitsMapping =
useProtocolStore.getState().passport.circuits_dns_mapping;
const passportData = get().passportData;
let circuitName, wsRpcUrl;
if (get().circuitType === 'disclose') {
circuitName = 'disclose';
wsRpcUrl = WS_RPC_URL_VC_AND_DISCLOSE;
} else {
circuitName = getCircuitNameFromPassportData(
passportData,
get().circuitType as 'register' | 'dsc',
);
if (get().circuitType === 'register') {
wsRpcUrl = circuitsMapping?.REGISTER?.[circuitName];
} else {
wsRpcUrl = circuitsMapping?.DSC?.[circuitName];
}
}
if (!circuitName) {
actor?.send({ type: 'CONNECT_ERROR' });
throw new Error('Could not determine circuit name');
}
if (!wsRpcUrl) {
throw new Error('No WebSocket URL available for TEE connection');
}
get()._closeConnections();
return new Promise(resolve => {
const ws = new WebSocket(wsRpcUrl);
set({ wsConnection: ws });
const handleConnectSuccess = () => resolve(true);
const handleConnectError = () => resolve(false);
ws.addEventListener('message', get()._handleWebSocketMessage);
ws.addEventListener('open', get()._handleWsOpen);
ws.addEventListener('error', get()._handleWsError);
ws.addEventListener('close', get()._handleWsClose);
if (!actor) {
return;
}
const unsubscribe = actor.subscribe(state => {
if (state.matches('ready_to_prove')) {
handleConnectSuccess();
unsubscribe.unsubscribe();
} else if (state.matches('error')) {
handleConnectError();
unsubscribe.unsubscribe();
}
});
});
},
startProving: async () => {
_checkActorInitialized(actor);
const { wsConnection, sharedKey, passportData, secret } = get();
if (get().currentState !== 'ready_to_prove') {
console.error('Cannot start proving: Not in ready_to_prove state.');
return;
}
if (!wsConnection || !sharedKey || !passportData || !secret) {
console.error(
'Cannot start proving: Missing wsConnection, sharedKey, passportData, or secret.',
);
actor!.send({ type: 'PROVE_ERROR' });
return;
}
try {
const submitBody = await get()._generatePayload();
wsConnection.send(JSON.stringify(submitBody));
actor!.send({ type: 'START_PROVING' });
} catch (error) {
console.error('Error during startProving preparation/send:', error);
actor!.send({ type: 'PROVE_ERROR' });
}
},
setUserConfirmed: () => {
set({ userConfirmed: true });
if (get().currentState === 'ready_to_prove') {
get().startProving();
}
},
postProving: () => {
_checkActorInitialized(actor);
const { circuitType } = get();
if (circuitType === 'dsc') {
get().init('register');
} else if (circuitType === 'register') {
actor!.send({ type: 'COMPLETED' });
} else if (circuitType === 'disclose') {
actor!.send({ type: 'COMPLETED' });
}
},
_closeConnections: () => {
const ws = get().wsConnection;
if (ws) {
try {
ws.removeEventListener('message', get()._handleWebSocketMessage);
ws.removeEventListener('open', get()._handleWsOpen);
ws.removeEventListener('error', get()._handleWsError);
ws.removeEventListener('close', get()._handleWsClose);
ws.close();
} catch (error) {
console.error(
'Error removing listeners or closing WebSocket:',
error,
);
}
set({ wsConnection: null });
}
const socket = get().socketConnection;
if (socket) {
socket.close();
set({ socketConnection: null });
}
set({
attestation: null,
serverPublicKey: null,
sharedKey: null,
uuid: null,
});
},
_generatePayload: async () => {
const { circuitType, passportData, secret, uuid, sharedKey } = get();
const selfApp = useSelfAppStore.getState().selfApp;
// TODO: according to the circuitType we could check that the params are valid.
let inputs, circuitName, endpointType, endpoint;
const protocolStore = useProtocolStore.getState();
switch (circuitType) {
case 'register':
({ inputs, circuitName, endpointType, endpoint } =
generateTEEInputsRegister(
secret as string,
passportData,
protocolStore.passport.dsc_tree,
));
break;
case 'dsc':
({ inputs, circuitName, endpointType, endpoint } =
generateTEEInputsDSC(
passportData,
protocolStore.passport.csca_tree,
));
break;
case 'disclose':
({ inputs, circuitName, endpointType, endpoint } =
generateTEEInputsDisclose(
secret as string,
passportData,
selfApp as SelfApp,
));
break;
default:
console.error('Invalid circuit type:' + circuitType);
throw new Error('Invalid circuit type:' + circuitType);
}
const payload = getPayload(
inputs,
circuitType as provingMachineCircuitType,
circuitName as string,
endpointType as EndpointType,
endpoint as string,
);
const forgeKey = forge.util.createBuffer(
sharedKey?.toString('binary') as string,
);
const encryptedPayload = encryptAES256GCM(
JSON.stringify(payload),
forgeKey,
);
return {
jsonrpc: '2.0',
method: 'openpassport_submit_request',
id: 2,
params: {
uuid: uuid,
...encryptedPayload,
},
};
},
};
});
function _checkActorInitialized(actor: AnyActorRef | null) {
if (!actor) {
throw new Error('State machine not initialized. Call init() first.');
}
}

View File

@@ -0,0 +1,92 @@
import forge from 'node-forge';
import { WS_DB_RELAYER_STAGING } from '../../../../common/src/constants/constants';
import { WS_DB_RELAYER } from '../../../../common/src/constants/constants';
import { EndpointType } from '../../../../common/src/utils/appType';
import { initElliptic } from '../../../../common/src/utils/certificate_parsing/elliptic';
const elliptic = initElliptic();
const { ec: EC } = elliptic;
export const ec = new EC('p256');
export const clientKey = ec.genKeyPair(); // Use a consistent client keypair for the session
export const clientPublicKeyHex =
clientKey.getPublic().getX().toString('hex').padStart(64, '0') +
clientKey.getPublic().getY().toString('hex').padStart(64, '0');
export function encryptAES256GCM(
plaintext: string,
key: forge.util.ByteStringBuffer,
) {
const iv = forge.random.getBytesSync(12);
const cipher = forge.cipher.createCipher('AES-GCM', key);
cipher.start({ iv: iv, tagLength: 128 });
cipher.update(forge.util.createBuffer(plaintext, 'utf8'));
cipher.finish();
const encrypted = cipher.output.getBytes();
const authTag = cipher.mode.tag.getBytes();
return {
nonce: Array.from(Buffer.from(iv, 'binary')),
cipher_text: Array.from(Buffer.from(encrypted, 'binary')),
auth_tag: Array.from(Buffer.from(authTag, 'binary')),
};
}
export type TEEPayloadDisclose = {
type: 'disclose';
endpointType: string;
endpoint: string;
onchain: boolean;
circuit: {
name: string;
inputs: string;
};
};
export type TEEPayload = {
type: 'register' | 'dsc';
onchain: true;
endpointType: string;
circuit: {
name: string;
inputs: string;
};
};
export function getPayload(
inputs: any,
circuitType: 'register' | 'dsc' | 'disclose',
circuitName: string,
endpointType: EndpointType,
endpoint: string,
) {
if (circuitType === 'disclose') {
const payload: TEEPayloadDisclose = {
type: 'disclose',
endpointType: endpointType,
endpoint: endpoint,
onchain: endpointType === 'celo' ? true : false,
circuit: {
name: circuitName,
inputs: JSON.stringify(inputs),
},
};
return payload;
} else {
const payload: TEEPayload = {
type: circuitType as 'register' | 'dsc',
onchain: true,
endpointType: endpointType,
circuit: {
name: circuitName,
inputs: JSON.stringify(inputs),
},
};
return payload;
}
}
export function getWSDbRelayerUrl(endpointType: EndpointType) {
return endpointType === 'celo' || endpointType === 'https'
? WS_DB_RELAYER
: WS_DB_RELAYER_STAGING;
}

View File

@@ -1,314 +0,0 @@
import elliptic from 'elliptic';
import forge from 'node-forge';
import io, { Socket } from 'socket.io-client';
import { v4 } from 'uuid';
import {
CIRCUIT_TYPES,
WS_DB_RELAYER,
WS_DB_RELAYER_STAGING,
} from '../../../../common/src/constants/constants';
import { EndpointType } from '../../../../common/src/utils/appType';
import {
DiscloseError,
globalSetDisclosureStatus,
globalSetRegistrationStatus,
ProofStatusEnum,
} from '../../stores/proofProvider';
import { getPublicKey, verifyAttestation } from './attest';
const { ec: EC } = elliptic;
/**
* @notice Encrypts plaintext using AES-256-GCM encryption.
* @param plaintext The string to be encrypted.
* @param key The encryption key as a forge ByteStringBuffer.
* @return An object containing the nonce, cipher_text, and auth_tag as arrays of numbers.
*/
function encryptAES256GCM(plaintext: string, key: forge.util.ByteStringBuffer) {
const iv = forge.random.getBytesSync(12);
const cipher = forge.cipher.createCipher('AES-GCM', key);
cipher.start({ iv: iv, tagLength: 128 });
cipher.update(forge.util.createBuffer(plaintext, 'utf8'));
cipher.finish();
const encrypted = cipher.output.getBytes();
const authTag = cipher.mode.tag.getBytes();
return {
nonce: Array.from(Buffer.from(iv, 'binary')),
cipher_text: Array.from(Buffer.from(encrypted, 'binary')),
auth_tag: Array.from(Buffer.from(authTag, 'binary')),
};
}
const ec = new EC('p256');
const key1 = ec.genKeyPair();
const pubkey =
key1.getPublic().getX().toString('hex').padStart(64, '0') +
key1.getPublic().getY().toString('hex').padStart(64, '0');
/**
* @notice Sends a payload over WebSocket connecting to the TEE server, processes the attestation,
* and submits a registration request encrypted via a shared key derived using ECDH.
* @param inputs The circuit input parameters.
* @param circuitName The name of the circuit.
* @param timeoutMs The timeout in milliseconds (default is 1200000 ms).
* @return A promise that resolves when the request completes or rejects on error/timeout.
* @dev This function sets up two WebSocket connections: one for RPC and one for subscription updates.
*/
export async function sendPayload(
inputs: any,
circuit: (typeof CIRCUIT_TYPES)[number],
circuitName: string,
endpointType: EndpointType,
endpoint: string,
wsRpcUrl: string,
timeoutMs = 1200000,
options?: {
updateGlobalOnSuccess?: boolean;
updateGlobalOnFailure?: boolean;
flow?: 'registration' | 'disclosure';
},
): Promise<{ status: ProofStatusEnum; error_code?: string; reason?: string }> {
const opts = {
updateGlobalOnSuccess: true,
updateGlobalOnFailure: true,
...options,
};
return new Promise(resolve => {
let finalized = false;
function finalize(
status: ProofStatusEnum,
error_code?: string,
reason?: string,
) {
if (!finalized) {
finalized = true;
clearTimeout(timer);
if (
(status === ProofStatusEnum.SUCCESS && opts.updateGlobalOnSuccess) ||
(status !== ProofStatusEnum.SUCCESS && opts.updateGlobalOnFailure)
) {
if (options?.flow === 'disclosure') {
let discloseError: DiscloseError | undefined =
error_code || reason ? { error_code, reason } : undefined;
globalSetDisclosureStatus &&
globalSetDisclosureStatus(status, discloseError);
} else {
globalSetRegistrationStatus && globalSetRegistrationStatus(status);
}
}
resolve({ status, error_code, reason });
}
}
const uuid = v4();
const ws = new WebSocket(wsRpcUrl);
let socket: Socket | null = null;
function createHelloBody(uuidString: string) {
return {
jsonrpc: '2.0',
method: 'openpassport_hello',
id: 1,
params: {
user_pubkey: [4, ...Array.from(Buffer.from(pubkey, 'hex'))],
uuid: uuidString,
},
};
}
ws.addEventListener('open', () => {
const helloBody = createHelloBody(uuid);
console.log('Connected to rpc, sending hello body:', helloBody);
ws.send(JSON.stringify(helloBody));
});
ws.addEventListener('message', async event => {
try {
const result = JSON.parse(event.data);
if (result.result?.attestation !== undefined) {
const serverPubkey = getPublicKey(result.result.attestation);
const verified = await verifyAttestation(result.result.attestation);
if (!verified) {
finalize(ProofStatusEnum.FAILURE);
throw new Error('Attestation verification failed');
}
const key2 = ec.keyFromPublic(serverPubkey as string, 'hex');
const sharedKey = key1.derive(key2.getPublic());
const forgeKey = forge.util.createBuffer(
Buffer.from(
sharedKey.toString('hex').padStart(64, '0'),
'hex',
).toString('binary'),
);
const payload = getPayload(
inputs,
circuit,
circuitName,
endpointType,
endpoint,
);
const encryptionData = encryptAES256GCM(
JSON.stringify(payload),
forgeKey,
);
const submitBody = {
jsonrpc: '2.0',
method: 'openpassport_submit_request',
id: 1,
params: {
uuid: result.result.uuid,
...encryptionData,
},
};
console.log('Sending submit body');
const truncatedBody = {
...submitBody,
params: {
uuid: submitBody.params.uuid,
nonce: submitBody.params.nonce.slice(0, 3) + '...',
cipher_text: submitBody.params.cipher_text.slice(0, 3) + '...',
auth_tag: submitBody.params.auth_tag.slice(0, 3) + '...',
},
};
console.log('Truncated submit body:', truncatedBody);
ws.send(JSON.stringify(submitBody));
} else {
if (result.error) {
finalize(ProofStatusEnum.ERROR);
}
const receivedUuid = result.result;
console.log('Received UUID:', receivedUuid);
console.log(result);
if (!socket) {
socket = io(getWSDbRelayerUrl(endpointType), {
path: '/',
transports: ['websocket'],
});
socket.on('connect', () => {
console.log('SocketIO: Connection opened');
socket?.emit('subscribe', receivedUuid);
});
socket.on('status', message => {
const data =
typeof message === 'string' ? JSON.parse(message) : message;
console.log('SocketIO message:', data);
if (data.status === 3) {
console.log('Failed to generate proof');
socket?.disconnect();
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
finalize(ProofStatusEnum.FAILURE);
} else if (data.status === 4) {
console.log('Proof verified');
socket?.disconnect();
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
finalize(ProofStatusEnum.SUCCESS);
} else if (data.status === 5) {
console.log('Failed to verify proof');
socket?.disconnect();
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
finalize(ProofStatusEnum.FAILURE, data.error_code, data.reason);
}
});
socket.on('disconnect', reason => {
console.log(`SocketIO disconnected. Reason: ${reason}`);
});
}
}
} catch (error) {
console.error('Error processing message:', error);
finalize(ProofStatusEnum.ERROR);
}
});
ws.addEventListener('error', error => {
console.error('WebSocket error:', error);
finalize(ProofStatusEnum.ERROR);
});
ws.addEventListener('close', event => {
console.log(
`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
);
if (!finalized) {
finalize(ProofStatusEnum.FAILURE);
}
});
const timer = setTimeout(() => {
if (socket) {
socket.disconnect();
}
if (
ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING
) {
ws.close();
}
finalize(ProofStatusEnum.ERROR);
}, timeoutMs);
});
}
export { encryptAES256GCM };
/***
* types
* ***/
export type TEEPayloadDisclose = {
type: 'disclose';
endpointType: string;
endpoint: string;
onchain: boolean;
circuit: {
name: string;
inputs: string;
};
};
export type TEEPayload = {
type: 'register' | 'dsc';
onchain: true;
endpointType: string;
circuit: {
name: string;
inputs: string;
};
};
export function getPayload(
inputs: any,
circuit: string,
circuitName: string,
endpointType: string,
endpoint: string,
) {
if (circuit === 'vc_and_disclose') {
const payload: TEEPayloadDisclose = {
type: 'disclose',
endpointType: endpointType,
endpoint: endpoint,
onchain: endpointType === 'celo' ? true : false,
circuit: {
name: circuitName,
inputs: JSON.stringify(inputs),
},
};
return payload;
} else if (circuit === 'register' || circuit === 'dsc') {
const payload: TEEPayload = {
type: circuit as 'register' | 'dsc',
onchain: true,
endpointType: endpointType,
circuit: {
name: circuitName,
inputs: JSON.stringify(inputs),
},
};
return payload;
}
}
function getWSDbRelayerUrl(endpointType: EndpointType) {
return endpointType === 'celo' || endpointType === 'https'
? WS_DB_RELAYER
: WS_DB_RELAYER_STAGING;
}

View File

@@ -0,0 +1,116 @@
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { poseidon2 } from 'poseidon-lite';
import {
API_URL,
PASSPORT_ATTESTATION_ID,
} from '../../../../common/src/constants/constants';
import { getCircuitNameFromPassportData } from '../../../../common/src/utils/circuits/circuitsName';
import {
generateCommitment,
generateNullifier,
} from '../../../../common/src/utils/passports/passport';
import { getLeafDscTree } from '../../../../common/src/utils/trees';
import { PassportData } from '../../../../common/src/utils/types';
import { useProtocolStore } from '../../stores/protocolStore';
export type PassportSupportStatus =
| 'passport_metadata_missing'
| 'csca_not_found'
| 'registration_circuit_not_supported'
| 'dsc_circuit_not_supported'
| 'passport_supported';
export async function checkPassportSupported(
passportData: PassportData,
): Promise<{
status: PassportSupportStatus;
details: string;
}> {
const passportMetadata = passportData.passportMetadata;
if (!passportMetadata) {
console.log('Passport metadata is null');
return { status: 'passport_metadata_missing', details: passportData.dsc };
}
if (!passportMetadata.cscaFound) {
console.log('CSCA not found');
return { status: 'csca_not_found', details: passportData.dsc };
}
const circuitNameRegister = getCircuitNameFromPassportData(
passportData,
'register',
);
const deployedCircuits =
useProtocolStore.getState().passport.deployed_circuits;
console.log('circuitNameRegister', circuitNameRegister);
if (
!circuitNameRegister ||
!deployedCircuits.REGISTER.includes(circuitNameRegister)
) {
return {
status: 'registration_circuit_not_supported',
details: circuitNameRegister,
};
}
const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc');
if (!circuitNameDsc || !deployedCircuits.DSC.includes(circuitNameDsc)) {
console.log('DSC circuit not supported:', circuitNameDsc);
return { status: 'dsc_circuit_not_supported', details: circuitNameDsc };
}
console.log('Passport supported');
return { status: 'passport_supported', details: 'null' };
}
export async function isUserRegistered(
passportData: PassportData,
secret: string,
) {
if (!passportData) {
return false;
}
const commitment = generateCommitment(
secret,
PASSPORT_ATTESTATION_ID,
passportData,
);
const serializedTree = useProtocolStore.getState().passport.commitment_tree;
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
const index = tree.indexOf(BigInt(commitment));
return index !== -1;
}
export async function isPassportNullified(passportData: PassportData) {
const nullifier = generateNullifier(passportData);
const nullifierHex = `0x${BigInt(nullifier).toString(16)}`;
console.log('checking for nullifier', nullifierHex);
const response = await fetch(`${API_URL}/is-nullifier-onchain/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ nullifier: nullifierHex }),
});
const data = await response.json();
console.log('isPassportNullified', data);
return data.data;
}
export async function checkIfPassportDscIsInTree(
passportData: PassportData,
dscTree: string,
): Promise<boolean> {
const hashFunction = (a: any, b: any) => poseidon2([a, b]);
const tree = LeanIMT.import(hashFunction, dscTree);
const leaf = getLeafDscTree(
passportData.dsc_parsed!,
passportData.csca_parsed!,
);
console.log('DSC leaf:', leaf);
const index = tree.indexOf(BigInt(leaf));
if (index === -1) {
console.log('DSC not found in the tree');
return false;
} else {
console.log('DSC found in the tree');
return true;
}
}

View File

@@ -1,107 +0,0 @@
import queryString from 'query-string';
import { Linking } from 'react-native';
import { SelfApp } from '../../../common/src/utils/appType';
/**
* Decodes a URL-encoded string.
* @param {string} encodedUrl
* @returns {string}
*/
const decodeUrl = (encodedUrl: string): string => {
try {
return decodeURIComponent(encodedUrl);
} catch (error) {
console.error('Error decoding URL:', error);
return encodedUrl;
}
};
const handleQRCodeData = (
uri: string,
setApp: (app: SelfApp) => void,
cleanSelfApp: () => void,
startAppListener: (sessionId: string, setApp: (app: SelfApp) => void) => void,
onNavigationNeeded?: () => void,
onErrorCallback?: () => void,
) => {
const decodedUri = decodeUrl(uri);
const encodedData = queryString.parseUrl(decodedUri).query;
const sessionId = encodedData.sessionId;
const selfAppStr = encodedData.selfApp as string | undefined;
if (selfAppStr) {
try {
const selfAppJson = JSON.parse(selfAppStr);
setApp(selfAppJson);
if (onNavigationNeeded) {
setTimeout(() => {
onNavigationNeeded();
}, 100);
}
return;
} catch (error) {
console.error('Error parsing selfApp:', error);
if (onErrorCallback) {
onErrorCallback();
}
}
}
if (sessionId && typeof sessionId === 'string') {
cleanSelfApp();
startAppListener(sessionId, setApp);
if (onNavigationNeeded) {
setTimeout(() => {
onNavigationNeeded();
}, 100);
}
} else {
console.error('No sessionId or selfApp found in the data');
if (onErrorCallback) {
onErrorCallback();
}
}
};
export const setupUniversalLinkListenerInNavigation = (
navigation: any,
setApp: (app: SelfApp) => void,
cleanSelfApp: () => void,
startAppListener: (sessionId: string, setApp: (app: SelfApp) => void) => void,
) => {
const handleNavigation = (url: string) => {
handleQRCodeData(
url,
setApp,
cleanSelfApp,
startAppListener,
() => {
if (navigation.isReady()) {
navigation.navigate('ProveScreen');
}
},
() => {
if (navigation.isReady()) {
navigation.navigate('QRCodeTrouble');
}
},
);
};
Linking.getInitialURL().then(url => {
if (url) {
handleNavigation(url);
}
});
const linkingEventListener = Linking.addEventListener('url', ({ url }) => {
handleNavigation(url);
});
return () => {
linkingEventListener.remove();
};
};

View File

@@ -11968,6 +11968,7 @@ __metadata:
tamagui: "npm:1.110.0"
typescript: "npm:5.0.4"
uuid: "npm:^11.0.5"
xstate: "npm:^5.19.2"
zustand: "npm:^4.5.2"
languageName: unknown
linkType: soft
@@ -14941,6 +14942,13 @@ __metadata:
languageName: node
linkType: hard
"xstate@npm:^5.19.2":
version: 5.19.2
resolution: "xstate@npm:5.19.2"
checksum: 10c0/0ff24a88af9fbb8182240b1c0929b3aa46f5777e91e6446939016fa7128afffe2421619613720e6266cc2436a91631e471ee0572215e127a5bdf841891a2b057
languageName: node
linkType: hard
"xtend@npm:~4.0.1":
version: 4.0.2
resolution: "xtend@npm:4.0.2"