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 <adwait@deepstacksoft.com>
This commit is contained in:
Adwait Gharpure 2024-02-28 19:37:26 +05:30 committed by GitHub
parent fc034356a1
commit 7db0edce75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 280 additions and 14 deletions

31
App.tsx
View File

@ -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<StackParamsList>();
const App = (): React.JSX.Element => {
const linking = {
prefixes: ['https://www.laconic-wallet.com'],
config: {
screens: {
SignRequest: {
path: 'sign/:network/:address/:message',
},
},
},
};
return (
<NavigationContainer>
<NavigationContainer linking={linking}>
<Stack.Navigator>
<Stack.Screen
name="Laconic"
component={HomeScreen}
options={{
title: 'Laconic Wallet',
headerBackVisible: false,
}}
/>
<Stack.Screen
@ -29,6 +43,21 @@ const App = (): React.JSX.Element => {
}}
initialParams={{ selectedNetwork: 'Ethereum' }}
/>
<Stack.Screen
name="SignRequest"
component={SignRequest}
options={{
title: 'Sign Message?',
}}
/>
<Stack.Screen
name="InvalidPath"
component={InvalidPath}
options={{
title: 'Bad Request',
headerBackVisible: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
);

View File

@ -20,6 +20,15 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="www.laconic-wallet.com"
android:pathPrefix="/sign"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -21,7 +21,7 @@ const AccountDetails: React.FC<AccountDetailsProps> = ({ account }) => {
{account?.pubKey}
</Text>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>HD Path: </Text>
<Text style={styles.highlight}>HD Path: </Text>
{account?.hdPath}
</Text>
</View>

View File

@ -110,9 +110,7 @@ const HomeScreen = () => {
<View style={styles.appContainer}>
{!isAccountsFetched ? (
<View style={styles.spinnerContainer}>
<Text style={{ color: 'black', fontSize: 18, padding: 10 }}>
Loading...
</Text>
<Text style={styles.LoadingText}>Loading...</Text>
<ActivityIndicator size="large" color="#0000ff" />
</View>
) : isWalletCreated ? (

View File

@ -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<NativeStackNavigationProp<StackParamsList>>();
return (
<View style={styles.badRequestContainer}>
<Text style={styles.messageText}>The signature request was invalid.</Text>
<Button
mode="contained"
onPress={() => {
navigation.navigate('Laconic');
}}>
Home
</Button>
</View>
);
};
export default InvalidPath;

129
components/SignRequest.tsx Normal file
View File

@ -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<StackParamsList, 'SignRequest'>;
const SignRequest = ({ route }: SignRequestProps) => {
const [account, setAccount] = useState<Account>();
const [message, setMessage] = useState<string>('');
const [network, setNetwork] = useState<string>('');
const [loaded, setLoaded] = useState(false);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
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 ? (
<View style={styles.appContainer}>
<AccountDetails account={account} />
<View style={styles.requestMessage}>
<Text variant="bodyLarge">{message}</Text>
</View>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D">
No
</Button>
<Button mode="contained" onPress={signMessageHandler}>
Yes
</Button>
</View>
</View>
) : (
<ActivityIndicator />
)}
</>
);
};
export default SignRequest;

View File

@ -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,
},
});

View File

@ -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 = {

View File

@ -38,10 +38,12 @@ const createWallet = async (): Promise<WalletDetails> => {
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,
};