From 7db0edce75b583065613b6c15425bb1baf602d95 Mon Sep 17 00:00:00 2001 From: Adwait Gharpure <69599306+Adw8@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:37:26 +0530 Subject: [PATCH] Allow incoming signature requests from intents (#32) * Add url scheme and linking * Pass account to sign request page * Provide functionality for accepting or rejecting requests * Load account state before handling url * Refactor code * Make intent work when app is cleared from recents * Fix bug to populate data from intents * Fix bug to update data on subsequent requests * Pass correct network to sign messages from cosmos account * Fix bug to populate data for incoming intent * Allow spaces in url * Bad signature page * Review changes * Change page heading * Use correct regex * Clean up code * Use https in url * Set state properly --------- Co-authored-by: Adw8 --- App.tsx | 31 +++++- android/app/src/main/AndroidManifest.xml | 9 ++ components/AccountDetails.tsx | 2 +- components/HomeScreen.tsx | 4 +- components/InvalidPath.tsx | 28 +++++ components/SignRequest.tsx | 129 +++++++++++++++++++++++ styles/stylesheet.js | 40 ++++++- types.ts | 4 + utils/accounts.ts | 47 ++++++++- 9 files changed, 280 insertions(+), 14 deletions(-) create mode 100644 components/InvalidPath.tsx create mode 100644 components/SignRequest.tsx diff --git a/App.tsx b/App.tsx index 0effacf..e3a81b1 100644 --- a/App.tsx +++ b/App.tsx @@ -5,20 +5,34 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import SignMessage from './components/SignMessage'; import HomeScreen from './components/HomeScreen'; +import SignRequest from './components/SignRequest'; +import InvalidPath from './components/InvalidPath'; import { StackParamsList } from './types'; const Stack = createNativeStackNavigator(); const App = (): React.JSX.Element => { + const linking = { + prefixes: ['https://www.laconic-wallet.com'], + config: { + screens: { + SignRequest: { + path: 'sign/:network/:address/:message', + }, + }, + }, + }; + return ( - + { }} initialParams={{ selectedNetwork: 'Ethereum' }} /> + + ); diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8371cd4..cd45c2b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,15 @@ + + + + + + diff --git a/components/AccountDetails.tsx b/components/AccountDetails.tsx index 157670f..6a5329d 100644 --- a/components/AccountDetails.tsx +++ b/components/AccountDetails.tsx @@ -21,7 +21,7 @@ const AccountDetails: React.FC = ({ account }) => { {account?.pubKey} - HD Path: + HD Path: {account?.hdPath} diff --git a/components/HomeScreen.tsx b/components/HomeScreen.tsx index 32bc93a..39e7362 100644 --- a/components/HomeScreen.tsx +++ b/components/HomeScreen.tsx @@ -110,9 +110,7 @@ const HomeScreen = () => { {!isAccountsFetched ? ( - - Loading... - + Loading... ) : isWalletCreated ? ( diff --git a/components/InvalidPath.tsx b/components/InvalidPath.tsx new file mode 100644 index 0000000..b492be4 --- /dev/null +++ b/components/InvalidPath.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import { Button } from 'react-native-paper'; + +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useNavigation } from '@react-navigation/native'; + +import { StackParamsList } from '../types'; +import styles from '../styles/stylesheet'; + +const InvalidPath = () => { + const navigation = + useNavigation>(); + return ( + + The signature request was invalid. + + + ); +}; + +export default InvalidPath; diff --git a/components/SignRequest.tsx b/components/SignRequest.tsx new file mode 100644 index 0000000..ea735df --- /dev/null +++ b/components/SignRequest.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from 'react'; +import { Alert, View } from 'react-native'; +import { ActivityIndicator, Button, Text } from 'react-native-paper'; + +import { useNavigation } from '@react-navigation/native'; +import { + NativeStackNavigationProp, + NativeStackScreenProps, +} from '@react-navigation/native-stack'; + +import { Account, StackParamsList } from '../types'; +import AccountDetails from './AccountDetails'; +import styles from '../styles/stylesheet'; +import { signMessage } from '../utils/sign-message'; +import { retrieveSingleAccount } from '../utils/accounts'; + +type SignRequestProps = NativeStackScreenProps; + +const SignRequest = ({ route }: SignRequestProps) => { + const [account, setAccount] = useState(); + const [message, setMessage] = useState(''); + const [network, setNetwork] = useState(''); + const [loaded, setLoaded] = useState(false); + + const navigation = + useNavigation>(); + + const retrieveData = async ( + requestNetwork: string, + requestAddress: string, + requestMessage: string, + ) => { + const requestAccount = await retrieveSingleAccount( + requestNetwork, + requestAddress, + ); + if (!requestAccount) { + navigation.navigate('InvalidPath'); + } + if (requestAccount && requestAccount !== account) { + setAccount(requestAccount); + } + if (requestMessage && requestMessage !== message) { + setMessage(decodeURIComponent(requestMessage)); + } + if (requestNetwork && requestNetwork !== network) { + setNetwork(requestNetwork); + } + }; + + const sanitizePath = (path: string) => { + const regex = /^\/sign\/(eth|cosmos)\/(.+)\/(.+)$/; + const match = path.match(regex); + + if (match) { + const [network, address, message] = match; + return { + network, + address, + message, + }; + } else { + navigation.navigate('InvalidPath'); + } + return null; + }; + + useEffect(() => { + setLoaded(true); + if (route.path) { + const sanitizedRoute = sanitizePath(route.path); + sanitizedRoute && + route.params && + retrieveData( + route.params?.network, + route.params?.address, + route.params?.message, + ); + } + setLoaded(false); + }, [route]); + + const signMessageHandler = async () => { + if (!account) { + throw new Error('Account is not valid'); + } + if (message) { + const signedMessage = await signMessage({ + message, + network, + accountId: account.counterId, + }); + Alert.alert('Signature', signedMessage); + navigation.navigate('Laconic'); + } + }; + + const rejectRequestHandler = async () => { + navigation.navigate('Laconic'); + }; + + return ( + <> + {!loaded ? ( + + + + {message} + + + + + + + ) : ( + + )} + + ); +}; + +export default SignRequest; diff --git a/styles/stylesheet.js b/styles/stylesheet.js index 0976861..3a77e36 100644 --- a/styles/stylesheet.js +++ b/styles/stylesheet.js @@ -101,11 +101,41 @@ const styles = StyleSheet.create({ marginTop: 20, width: 200, alignSelf: 'center', - spinnerContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, + }, + spinnerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + LoadingText: { + color: 'black', + fontSize: 18, + padding: 10, + }, + requestMessage: { + borderWidth: 1, + borderRadius: 5, + marginTop: 50, + height: 50, + alignItems: 'center', + justifyContent: 'center', + }, + buttonContainer: { + marginTop: 50, + flexDirection: 'row', + marginLeft: 20, + justifyContent: 'space-evenly', + }, + badRequestContainer: { + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + messageText: { + color: 'black', + fontSize: 16, + textAlign: 'center', + marginBottom: 20, }, }); diff --git a/types.ts b/types.ts index 94b60ad..3fafac2 100644 --- a/types.ts +++ b/types.ts @@ -1,6 +1,10 @@ export type StackParamsList = { Laconic: undefined; SignMessage: { selectedNetwork: string; accountInfo: Account } | undefined; + SignRequest: + | { network: string; address: string; message: string } + | undefined; + InvalidPath: undefined; }; export type Account = { diff --git a/utils/accounts.ts b/utils/accounts.ts index 7c50e30..a4678a2 100644 --- a/utils/accounts.ts +++ b/utils/accounts.ts @@ -38,10 +38,12 @@ const createWallet = async (): Promise => { const cosmosAddress = (await getCosmosAccounts(mnemonic, "0'/0/0")).data .address; - const ethAccountInfo = `${"0'/0/0"},${ethNode.privateKey},${ethNode.publicKey - },${ethAddress}`; - const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${cosmosNode.publicKey - },${cosmosAddress}`; + const ethAccountInfo = `${"0'/0/0"},${ethNode.privateKey},${ + ethNode.publicKey + },${ethAddress}`; + const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${ + cosmosNode.publicKey + },${cosmosAddress}`; await Promise.all([ setInternetCredentials( @@ -184,6 +186,42 @@ const retrieveAccounts = async (): Promise<{ return { ethLoadedAccounts, cosmosLoadedAccounts }; }; +const retrieveSingleAccount = async (network: string, address: string) => { + let loadedAccounts; + + switch (network) { + case 'eth': + const ethServer = await getInternetCredentials('eth:globalCounter'); + const ethCounter = ethServer && ethServer.password; + + if (ethCounter) { + loadedAccounts = await retrieveAccountsForNetwork(network, ethCounter); + } + break; + + case 'cosmos': + const cosmosServer = await getInternetCredentials('cosmos:globalCounter'); + const cosmosCounter = cosmosServer && cosmosServer.password; + + if (cosmosCounter) { + loadedAccounts = await retrieveAccountsForNetwork( + network, + cosmosCounter, + ); + } + break; + + default: + break; + } + + if (loadedAccounts) { + return loadedAccounts.find(account => account.address === address); + } + + return undefined; +}; + const resetWallet = async () => { try { await Promise.all([ @@ -206,5 +244,6 @@ export { addAccount, addAccountFromHDPath, retrieveAccounts, + retrieveSingleAccount, resetWallet, };