forked from cerc-io/laconic-wallet
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:
parent
fc034356a1
commit
7db0edce75
31
App.tsx
31
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<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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 ? (
|
||||
|
28
components/InvalidPath.tsx
Normal file
28
components/InvalidPath.tsx
Normal 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
129
components/SignRequest.tsx
Normal 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;
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
4
types.ts
4
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 = {
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user