From a496060043296494c144e4adb92b6247f2cf0c67 Mon Sep 17 00:00:00 2001 From: Jose Felix Date: Sat, 4 Jan 2025 04:37:56 -0400 Subject: [PATCH] feat: set up smart account wallet connection --- packages/mobile/app/_layout.tsx | 6 +- .../mobile/app/onboarding/camera-scan.tsx | 190 +++++++++------ packages/mobile/app/onboarding/connect.tsx | 47 ---- .../assets/images/connecting-onboarding.png | Bin 0 -> 73449 bytes .../mobile/assets/images/error-onboarding.png | Bin 0 -> 63641 bytes .../assets/images/success-onboarding.png | Bin 0 -> 66559 bytes .../components/connection-progress-modal.tsx | 217 ++++++++++++++++++ .../onboarding/connect-wallet-screen.tsx | 175 -------------- .../onboarding/connection-status-screen.tsx | 217 ------------------ .../mobile/components/onboarding/index.ts | 5 - packages/mobile/components/ui/button.tsx | 17 +- packages/mobile/hooks/use-wallets.ts | 14 +- packages/mobile/package.json | 1 + packages/mobile/stores/current-wallet.ts | 2 +- packages/mobile/stores/keyring.ts | 2 + packages/mobile/utils/encryption.ts | 19 ++ packages/mobile/utils/wallet-factory/index.ts | 10 +- packages/utils/src/web-rtc.ts | 23 +- .../use-create-mobile-session.ts | 213 +++++++++++++++++ .../use-remove-mobile-session.ts | 59 +++++ packages/web/package.json | 3 +- packages/web/pages/mobile-testing.tsx | 74 ++++-- packages/web/tailwind.config.js | 2 +- packages/web/utils/encryption.ts | 11 + yarn.lock | 158 ++++++++++++- 25 files changed, 916 insertions(+), 549 deletions(-) delete mode 100644 packages/mobile/app/onboarding/connect.tsx create mode 100644 packages/mobile/assets/images/connecting-onboarding.png create mode 100644 packages/mobile/assets/images/error-onboarding.png create mode 100644 packages/mobile/assets/images/success-onboarding.png create mode 100644 packages/mobile/components/connection-progress-modal.tsx delete mode 100644 packages/mobile/components/onboarding/connect-wallet-screen.tsx delete mode 100644 packages/mobile/components/onboarding/connection-status-screen.tsx delete mode 100644 packages/mobile/components/onboarding/index.ts create mode 100644 packages/mobile/utils/encryption.ts create mode 100644 packages/web/hooks/mutations/mobile-session/use-create-mobile-session.ts create mode 100644 packages/web/hooks/mutations/mobile-session/use-remove-mobile-session.ts create mode 100644 packages/web/utils/encryption.ts diff --git a/packages/mobile/app/_layout.tsx b/packages/mobile/app/_layout.tsx index 5fae4c9007..8f073b8d0c 100644 --- a/packages/mobile/app/_layout.tsx +++ b/packages/mobile/app/_layout.tsx @@ -9,7 +9,6 @@ import { ThemeProvider } from "@react-navigation/native"; import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; -import { loggerLink } from "@trpc/client"; import { useFonts } from "expo-font"; import { Redirect, Stack } from "expo-router"; import * as SplashScreen from "expo-splash-screen"; @@ -139,7 +138,6 @@ export default function RootLayout() { const possibleOsmosisFePath = pathParts.join("."); if (basePath === "osmosisFe") { - console.log("possibleOsmosisFePath", possibleOsmosisFePath); return servers[constructEdgeRouterKey("main")]({ ...ctx, op: { @@ -204,9 +202,9 @@ export default function RootLayout() { } const OnboardingObserver = () => { - const { currentWallet } = useWallets(); + const { currentWallet, wallets } = useWallets(); - if (!currentWallet) { + if (!currentWallet && wallets.length === 0) { return ; } diff --git a/packages/mobile/app/onboarding/camera-scan.tsx b/packages/mobile/app/onboarding/camera-scan.tsx index 14ae285366..3153403db9 100644 --- a/packages/mobile/app/onboarding/camera-scan.tsx +++ b/packages/mobile/app/onboarding/camera-scan.tsx @@ -1,3 +1,4 @@ +import { AvailableOneClickTradingMessages } from "@osmosis-labs/types"; import { deserializeWebRTCMessage, serializeWebRTCMessage, @@ -6,22 +7,20 @@ import { import MaskedView from "@react-native-masked-view/masked-view"; import { BarcodeScanningResult, CameraView, FocusMode } from "expo-camera"; import { getRandomBytes } from "expo-crypto"; +import { router } from "expo-router"; import { useEffect, useRef, useState } from "react"; -import { - ActivityIndicator, - Dimensions, - StyleSheet, - TouchableOpacity, - View, -} from "react-native"; +import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { RTCIceCandidate, RTCPeerConnection } from "react-native-webrtc"; +import { ConnectionProgressModal } from "~/components/connection-progress-modal"; import { RouteHeader } from "~/components/route-header"; -import { Button } from "~/components/ui/button"; import { Text } from "~/components/ui/text"; import { Colors } from "~/constants/theme-colors"; +import { useStateRef } from "~/hooks/use-state-ref"; +import { decryptAES } from "~/utils/encryption"; import { api } from "~/utils/trpc"; +import { WalletFactory } from "~/utils/wallet-factory"; const CAMERA_ASPECT_RATIO = 4 / 3; const SCAN_ICON_WIDTH_RATIO = 0.7; @@ -29,14 +28,10 @@ const SCAN_ICON_WIDTH_RATIO = 0.7; // Define a type for the statuses type WebRTCStatus = | "Init" - | "FetchingOffer" | "NoOffer" - | "CreatingPC" - | "ChannelOpen" - | "CreatingAnswer" - | "PostingAnswer" | "AwaitingConnection" | "AwaitingVerification" + | "Verifying" | "Verified" | "Error"; @@ -51,10 +46,15 @@ const generateSecureSecret = () => { return base64Key; }; -const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { +const useWalletCreationWebRTC = ({ + sessionToken, +}: { + sessionToken: string; +}) => { const [status, setStatus] = useState("Init"); const [connected, setConnected] = useState(false); const [verificationCode, setVerificationCode] = useState(""); + const [_, setSecret, secretRef] = useStateRef(""); const apiUtils = api.useUtils(); const postCandidateMutation = @@ -76,7 +76,6 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { try { if (isCleanedUp) return; - setStatus("FetchingOffer"); // 1) Fetch the offer const offerRes = await apiUtils.osmosisFe.webRTC.fetchOffer.ensureData({ sessionToken, @@ -86,7 +85,8 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { return; } - setStatus("CreatingPC"); + setStatus("AwaitingConnection"); + // 2) Create peer connection const pc = new RTCPeerConnection({ iceServers: [STUN_SERVER] }); peerConnectionRef.current = pc; @@ -115,13 +115,13 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { channel.addEventListener("open", () => { console.log("[Mobile] Data channel open, can receive data."); - setStatus("ChannelOpen"); setConnected(true); - // Generate and send verification code + // Generate and send verification code and secret const code = generateVerificationCode(); - setVerificationCode(code); const secret = generateSecureSecret(); + setVerificationCode(code); + setSecret(secret); channel.send( serializeWebRTCMessage({ type: "verification", @@ -137,27 +137,68 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { try { const data = await deserializeWebRTCMessage(msgEvent.data); if (data.type === "verification_success") { + // Decrypt the verification success data using the secret + const decryptedData = decryptAES( + data.encryptedData, + secretRef.current + ); + const { address, allowedMessages, key, publicKey } = JSON.parse( + decryptedData + ) as { + address: string; + allowedMessages: AvailableOneClickTradingMessages[]; + key: string; + publicKey: string; + }; + + // Store the decrypted data in secure storage + await new WalletFactory().createWallet({ + keyInfo: { + type: "smart-account", + address, + allowedMessages, + privateKey: key, + publicKey, + name: "Wallet 1", + version: 1, + }, + }); + setStatus("Verified"); } + if (data.type === "verification_failed") { + setStatus("Error"); + } + if (data.type === "starting_verification") { + setStatus("Verifying"); + } } catch (e) { console.error("Failed to parse message:", e); + setStatus("Error"); } }); channel.addEventListener("close", () => { console.log("[Mobile] Data channel closed"); setConnected(false); + setStatus("Error"); }); }); - setStatus("CreatingAnswer"); + pc.addEventListener("connectionstatechange", () => { + if (pc.connectionState === "disconnected") { + console.log("[Mobile] Connection lost."); + setConnected(false); + setStatus("Error"); + } + }); + // 6) Create answer if (isCleanedUp) return; const answer = await pc.createAnswer(); if (isCleanedUp) return; await pc.setLocalDescription(answer); - setStatus("PostingAnswer"); await postAnswerMutation.mutateAsync({ sessionToken, answerSDP: answer.sdp ?? "", @@ -179,8 +220,6 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { } } }, 3000); - - setStatus("AwaitingConnection"); } catch (err: any) { console.error(err); if (!isCleanedUp) setStatus("Error"); @@ -201,22 +240,17 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionToken]); - const handleSendTest = () => { - if ( - dataChannelRef.current && - dataChannelRef.current.readyState === "open" - ) { - dataChannelRef.current.send("Test message from mobile!"); - } else { - console.log("[Mobile] Data channel not ready"); - } + const reset = () => { + setStatus("Init"); + setConnected(false); + setVerificationCode(""); }; return { status, - handleSendTest, connected, verificationCode, + reset, }; }; @@ -224,15 +258,14 @@ export default function Welcome() { const { top, bottom } = useSafeAreaInsets(); const [autoFocus, setAutoFocus] = useState("off"); const [sessionToken, setSessionToken] = useState(""); + const [showConnectionModal, setShowConnectionModal] = useState(false); - const shouldFreezeCamera = sessionToken !== ""; + const shouldFreezeCamera = !!sessionToken; - const { status, handleSendTest, connected, verificationCode } = useWebRTC({ + const { status, verificationCode, reset } = useWalletCreationWebRTC({ sessionToken, }); - console.log("verificationCode", verificationCode); - const resetCameraAutoFocus = () => { const abortController = new AbortController(); setAutoFocus("off"); @@ -251,8 +284,47 @@ export default function Welcome() { const parsedData = JSON.parse(data); setSessionToken(parsedData.sessionToken); + setShowConnectionModal(true); }; + const handleRetry = () => { + setSessionToken(""); + setShowConnectionModal(false); + reset(); + }; + + const handleClose = () => { + if (status === "Verified") { + router.replace("/(tabs)"); + } else { + setShowConnectionModal(false); + reset(); + } + }; + + const getConnectionState = () => { + if (!showConnectionModal) return null; + + switch (status) { + case "Init": + case "AwaitingConnection": + return "connecting"; + case "AwaitingVerification": + return "input-code"; + case "Verified": + return "success"; + case "Verifying": + return "verifying"; + case "Error": + case "NoOffer": + return "failed"; + default: + return "connecting"; + } + }; + + const connectionState = getConnectionState(); + const overlayWidth = Dimensions.get("window").height / CAMERA_ASPECT_RATIO; const cameraWidth = Dimensions.get("window").width; const scannerSize = @@ -314,11 +386,13 @@ export default function Welcome() { } > - + {!connectionState && ( + + )} {!shouldFreezeCamera && ( - {shouldFreezeCamera && ( - - - Loading... - + {connectionState && ( + )} - {connected && ( -