Skip to content

Commit

Permalink
feat: set up smart account wallet connection
Browse files Browse the repository at this point in the history
  • Loading branch information
JoseRFelix committed Jan 4, 2025
1 parent 079bf0f commit a496060
Show file tree
Hide file tree
Showing 25 changed files with 916 additions and 549 deletions.
6 changes: 2 additions & 4 deletions packages/mobile/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 <Redirect href="/onboarding/welcome" />;
}

Expand Down
190 changes: 123 additions & 67 deletions packages/mobile/app/onboarding/camera-scan.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AvailableOneClickTradingMessages } from "@osmosis-labs/types";
import {
deserializeWebRTCMessage,
serializeWebRTCMessage,
Expand All @@ -6,37 +7,31 @@ 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;

// Define a type for the statuses
type WebRTCStatus =
| "Init"
| "FetchingOffer"
| "NoOffer"
| "CreatingPC"
| "ChannelOpen"
| "CreatingAnswer"
| "PostingAnswer"
| "AwaitingConnection"
| "AwaitingVerification"
| "Verifying"
| "Verified"
| "Error";

Expand All @@ -51,10 +46,15 @@ const generateSecureSecret = () => {
return base64Key;
};

const useWebRTC = ({ sessionToken }: { sessionToken: string }) => {
const useWalletCreationWebRTC = ({
sessionToken,
}: {
sessionToken: string;
}) => {
const [status, setStatus] = useState<WebRTCStatus>("Init");
const [connected, setConnected] = useState(false);
const [verificationCode, setVerificationCode] = useState<string>("");
const [_, setSecret, secretRef] = useStateRef<string>("");

const apiUtils = api.useUtils();
const postCandidateMutation =
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -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 ?? "",
Expand All @@ -179,8 +220,6 @@ const useWebRTC = ({ sessionToken }: { sessionToken: string }) => {
}
}
}, 3000);

setStatus("AwaitingConnection");
} catch (err: any) {
console.error(err);
if (!isCleanedUp) setStatus("Error");
Expand All @@ -201,38 +240,32 @@ 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,
};
};

export default function Welcome() {
const { top, bottom } = useSafeAreaInsets();
const [autoFocus, setAutoFocus] = useState<FocusMode>("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");
Expand All @@ -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 =
Expand Down Expand Up @@ -314,11 +386,13 @@ export default function Welcome() {
</View>
}
>
<CameraView
style={{ flex: 1 }}
onBarcodeScanned={onScanCode}
autofocus={autoFocus}
/>
{!connectionState && (
<CameraView
style={{ flex: 1 }}
onBarcodeScanned={onScanCode}
autofocus={autoFocus}
/>
)}
{!shouldFreezeCamera && (
<View
style={{
Expand Down Expand Up @@ -346,23 +420,13 @@ export default function Welcome() {
onPress={resetCameraAutoFocus}
/>
</MaskedView>
{shouldFreezeCamera && (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
gap: 12,
}}
>
<ActivityIndicator color={Colors.white.full} />
<Text style={{ fontSize: 20, fontWeight: 600 }}>Loading...</Text>
</View>
{connectionState && (
<ConnectionProgressModal
state={connectionState}
verificationCode={verificationCode}
onRetry={handleRetry}
onClose={handleClose}
/>
)}
<View
style={{
Expand All @@ -385,14 +449,6 @@ export default function Welcome() {
to &apos;Profile&apos; &gt; &apos;Link mobile device&apos; on{"\n"}
Osmosis web app.
</Text>
{connected && (
<Button
title="Send test message"
onPress={() => {
handleSendTest();
}}
/>
)}
</View>
</View>
);
Expand Down
Loading

0 comments on commit a496060

Please sign in to comment.