mirror of
https://github.com/selfxyz/self.git
synced 2026-04-27 03:01:15 -04:00
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:
committed by
GitHub
parent
0d7d1170f3
commit
0bf324e639
@@ -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>
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
slate700,
|
||||
white,
|
||||
} from '../../utils/colors';
|
||||
import { isUserRegistered } from '../../utils/proving/payload';
|
||||
import { isUserRegistered } from '../../utils/proving/validateDocument';
|
||||
|
||||
interface RecoverWithPhraseScreenProps {}
|
||||
|
||||
|
||||
@@ -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, () => {});
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 }]}>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
};
|
||||
88
app/src/stores/protocolStore.ts
Normal file
88
app/src/stores/protocolStore.ts
Normal 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 } });
|
||||
},
|
||||
},
|
||||
}));
|
||||
189
app/src/stores/selfAppStore.tsx
Normal file
189
app/src/stores/selfAppStore.tsx
Normal 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,
|
||||
});
|
||||
}
|
||||
},
|
||||
}));
|
||||
68
app/src/utils/deeplinks.ts
Normal file
68
app/src/utils/deeplinks.ts
Normal 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();
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
751
app/src/utils/proving/provingMachine.ts
Normal file
751
app/src/utils/proving/provingMachine.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
92
app/src/utils/proving/provingUtils.ts
Normal file
92
app/src/utils/proving/provingUtils.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
116
app/src/utils/proving/validateDocument.ts
Normal file
116
app/src/utils/proving/validateDocument.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user