Compare commits

...

3 Commits

Author SHA1 Message Date
bb5223afda Add support for staking module tx MsgCreateValidator (#14)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#14
2024-08-09 09:41:46 +00:00
e975f4c9f7 Add copy button for mnemonic after creating new wallet (#13)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#13
2024-08-08 07:35:50 +00:00
4827fa8c7c Fix accounts order in namespaces object and prevent re-render from useEffect (#12)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)
- Send namespaces object with correct accounts sequence while pairing with dApps

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#12
2024-08-01 10:38:21 +00:00
9 changed files with 114 additions and 72 deletions

View File

@ -21,6 +21,7 @@
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@json-rpc-tools/utils": "^1.7.6", "@json-rpc-tools/utils": "^1.7.6",
"@react-native-async-storage/async-storage": "^1.22.3", "@react-native-async-storage/async-storage": "^1.22.3",
"@react-native-clipboard/clipboard": "^1.14.1",
"@react-native-community/netinfo": "^11.3.1", "@react-native-community/netinfo": "^11.3.1",
"@react-navigation/elements": "^1.3.30", "@react-navigation/elements": "^1.3.30",
"@react-navigation/native": "^6.1.10", "@react-navigation/native": "^6.1.10",

31
scripts/build-apk.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Default value for IS_RELEASE
IS_RELEASE=${IS_RELEASE:-false}
# Install dependencies
echo "Installing dependencies..."
yarn
# Create the necessary directory for assets
mkdir -p android/app/src/main/assets/
# Bundle the React Native application
yarn react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output android/app/src/main/assets/index.android.bundle \
--assets-dest android/app/src/main/res
# Navigate to the android directory
cd android
# Run the Gradle build based on the IS_RELEASE flag
if [ "$IS_RELEASE" = "true" ]; then
echo "Building release version..."
./gradlew assembleRelease
else
echo "Building debug version..."
./gradlew assembleDebug
fi

View File

@ -60,7 +60,6 @@ const Accounts = () => {
networksData, networksData,
selectedNetwork!, selectedNetwork!,
accounts, accounts,
currentIndex,
); );
if (!updatedNamespaces) { if (!updatedNamespaces) {

View File

@ -1,6 +1,8 @@
import React from 'react'; import React, { useState } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { Button, Dialog, Portal, Text } from 'react-native-paper'; import { Button, Dialog, Portal, Snackbar, Text } from 'react-native-paper';
import Clipboard from '@react-native-clipboard/clipboard';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import GridView from './Grid'; import GridView from './Grid';
@ -11,29 +13,45 @@ const DialogComponent = ({
hideDialog, hideDialog,
contentText, contentText,
}: CustomDialogProps) => { }: CustomDialogProps) => {
const [toastVisible, setToastVisible] = useState(false);
const words = contentText.split(' '); const words = contentText.split(' ');
const handleCopy = () => {
Clipboard.setString(contentText);
setToastVisible(true);
};
return ( return (
<Portal> <>
<Dialog visible={visible} onDismiss={hideDialog}> <Portal>
<Dialog.Content> <Dialog visible={visible} onDismiss={hideDialog}>
<Text variant="titleLarge">Mnemonic</Text> <Dialog.Content>
<View style={styles.dialogTitle}> <Text variant="titleLarge">Mnemonic</Text>
<Text variant="titleMedium"> <View style={styles.dialogTitle}>
Your mnemonic provides full access to your wallet and funds. Make <Text variant="titleMedium">
sure to note it down.{' '} Your mnemonic provides full access to your wallet and funds.
</Text> Make sure to note it down.{' '}
<Text variant="titleMedium" style={styles.dialogWarning}> </Text>
Do not share your mnemonic with anyone <Text variant="titleMedium" style={styles.dialogWarning}>
</Text> Do not share your mnemonic with anyone
<GridView words={words} /> </Text>
</View> <GridView words={words} />
</Dialog.Content> </View>
<Dialog.Actions> </Dialog.Content>
<Button onPress={hideDialog}>Done</Button> <Dialog.Actions>
</Dialog.Actions> <Button onPress={handleCopy}>Copy</Button>
</Dialog> <Button onPress={hideDialog}>Done</Button>
</Portal> </Dialog.Actions>
</Dialog>
</Portal>
<Snackbar
visible={toastVisible}
onDismiss={() => setToastVisible(false)}
duration={1000}>
Mnemonic copied to clipboard
</Snackbar>
</>
); );
}; };

View File

@ -20,7 +20,7 @@ const PairingModal = ({
setModalVisible, setModalVisible,
setToastVisible, setToastVisible,
}: PairingModalProps) => { }: PairingModalProps) => {
const { accounts, currentIndex } = useAccounts(); const { accounts } = useAccounts();
const { selectedNetwork, networksData } = useNetworks(); const { selectedNetwork, networksData } = useNetworks();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [chainError, setChainError] = useState(''); const [chainError, setChainError] = useState('');
@ -103,7 +103,6 @@ const PairingModal = ({
networksData, networksData,
selectedNetwork!, selectedNetwork!,
accounts, accounts,
currentIndex,
); );
setSupportedNamespaces(nameSpaces); setSupportedNamespaces(nameSpaces);
} catch (err) { } catch (err) {
@ -129,7 +128,6 @@ const PairingModal = ({
networksData, networksData,
selectedNetwork, selectedNetwork,
accounts, accounts,
currentIndex,
web3wallet, web3wallet,
setCurrentProposal, setCurrentProposal,
setModalVisible, setModalVisible,

View File

@ -1,15 +1,16 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Image, ScrollView, View } from 'react-native'; import { Image, ScrollView, View } from 'react-native';
import { Button, Text, TextInput } from 'react-native-paper'; import { Button, Text, TextInput } from 'react-native-paper';
import { SvgUri } from 'react-native-svg'; import { SvgUri } from 'react-native-svg';
import Config from 'react-native-config'; import Config from 'react-native-config';
import { MsgCreateValidator } from 'cosmjs-types/cosmos/staking/v1beta1/tx';
import { import {
NativeStackNavigationProp, NativeStackNavigationProp,
NativeStackScreenProps, NativeStackScreenProps,
} from '@react-navigation/native-stack'; } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { DirectSecp256k1Wallet, EncodeObject } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk'; import { LaconicClient } from '@cerc-io/registry-sdk';
import { GasPrice, calculateFee } from '@cosmjs/stargate'; import { GasPrice, calculateFee } from '@cosmjs/stargate';
import { formatJsonRpcError } from '@json-rpc-tools/utils'; import { formatJsonRpcError } from '@json-rpc-tools/utils';
@ -42,7 +43,6 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
const requestName = requestSession.peer.metadata.name; const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0]; const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url; const requestURL = requestSession.peer.metadata.url;
const transactionMessage = route.params.transactionMessage;
const signer = route.params.signer; const signer = route.params.signer;
const requestEvent = route.params.requestEvent; const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId; const chainId = requestEvent.params.chainId;
@ -69,6 +69,20 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
); );
const namespace = requestedNetwork!.namespace; const namespace = requestedNetwork!.namespace;
const transactionMessage = useMemo((): EncodeObject => {
const inputTxMsg = route.params.transactionMessage;
// If it's a MsgCreateValidator, decode the tx msg value using MsgCreateValidator type
if (inputTxMsg.typeUrl.includes('MsgCreateValidator')) {
return {
typeUrl: inputTxMsg.typeUrl,
value: MsgCreateValidator.fromJSON(inputTxMsg.value),
};
}
return inputTxMsg;
}, [route.params.transactionMessage]);
useEffect(() => { useEffect(() => {
if (namespace !== COSMOS) { if (namespace !== COSMOS) {
return; return;
@ -144,7 +158,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
return; return;
} }
const gasEstimation = await cosmosStargateClient!.simulate( const gasEstimation = await cosmosStargateClient!.simulate(
transactionMessage.value.participant!, signer,
[transactionMessage], [transactionMessage],
MEMO, MEMO,
); );
@ -168,6 +182,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
requestEventId, requestEventId,
topic, topic,
web3wallet, web3wallet,
signer,
]); ]);
useEffect(() => { useEffect(() => {
@ -243,6 +258,13 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
navigation.navigate('Laconic'); navigation.navigate('Laconic');
}; };
const replacer = (key: string, value: any): any => {
if (value instanceof Uint8Array) {
return Buffer.from(value).toString('hex');
}
return value;
};
return ( return (
<> <>
<ScrollView contentContainerStyle={styles.approveTransaction}> <ScrollView contentContainerStyle={styles.approveTransaction}>
@ -267,7 +289,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
</Text> </Text>
<View style={styles.messageBody}> <View style={styles.messageBody}>
<Text variant="bodyLarge"> <Text variant="bodyLarge">
{JSON.stringify(transactionMessage, null, 2)} {JSON.stringify(transactionMessage, replacer, 2)}
</Text> </Text>
</View> </View>
<> <>

View File

@ -88,21 +88,13 @@ const SignRequest = ({ route }: SignRequestProps) => {
return; return;
} }
if (requestAccount !== account) { setAccount(requestAccount);
setAccount(requestAccount); setMessage(decodeURIComponent(requestMessage));
} setNamespace(requestNamespace);
if (requestMessage !== message) { setChainId(requestChainId);
setMessage(decodeURIComponent(requestMessage));
}
if (requestNamespace !== namespace) {
setNamespace(requestNamespace);
}
if (requestChainId !== chainId) {
setChainId(requestChainId);
}
setIsLoading(false); setIsLoading(false);
}, },
[account, message, navigation, namespace, chainId], [navigation],
); );
const sanitizePath = useCallback( const sanitizePath = useCallback(

View File

@ -40,7 +40,6 @@ export const getNamespaces = async (
networksData: NetworksDataState[], networksData: NetworksDataState[],
selectedNetwork: NetworksDataState, selectedNetwork: NetworksDataState,
accounts: Account[], accounts: Account[],
currentIndex: number,
) => { ) => {
const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`; const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`;
@ -101,23 +100,6 @@ export const getNamespaces = async (
const requiredAddressesArray = await Promise.all(requiredAddressesPromise); const requiredAddressesArray = await Promise.all(requiredAddressesPromise);
const requiredAddresses = requiredAddressesArray.flat(); const requiredAddresses = requiredAddressesArray.flat();
let sortedAccounts = requiredAddresses;
// If selected network is included in chains requested from dApp,
// Put selected account as first account
if (walletConnectChains.includes(namespaceChainId)) {
const currentAddresses = requiredAddresses.filter(address =>
address.includes(namespaceChainId),
);
sortedAccounts = [
currentAddresses[currentIndex],
...currentAddresses.filter((address, index) => index !== currentIndex),
...requiredAddresses.filter(
address => !currentAddresses.includes(address),
),
];
}
// construct namespace object // construct namespace object
const newNamespaces = { const newNamespaces = {
eip155: { eip155: {
@ -133,7 +115,7 @@ export const getNamespaces = async (
...(optionalNamespaces.eip155?.events ?? []), ...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []), ...(requiredNamespaces.eip155?.events ?? []),
], ],
accounts: sortedAccounts.filter(account => account.includes(EIP155)), accounts: requiredAddresses.filter(account => account.includes(EIP155)),
}, },
cosmos: { cosmos: {
chains: walletConnectChains.filter(chain => chain.includes(COSMOS)), chains: walletConnectChains.filter(chain => chain.includes(COSMOS)),
@ -147,18 +129,12 @@ export const getNamespaces = async (
...(optionalNamespaces.cosmos?.events ?? []), ...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []), ...(requiredNamespaces.cosmos?.events ?? []),
], ],
accounts: sortedAccounts.filter(account => account.includes(COSMOS)), accounts: requiredAddresses.filter(account => account.includes(COSMOS)),
}, },
}; };
return newNamespaces; return newNamespaces;
} else { } else {
// Set selected account as the first account in supported namespaces
const sortedAccounts = [
accounts[currentIndex],
...accounts.filter((account, index) => index !== currentIndex),
];
switch (selectedNetwork.namespace) { switch (selectedNetwork.namespace) {
case EIP155: case EIP155:
return { return {
@ -175,7 +151,7 @@ export const getNamespaces = async (
...(optionalNamespaces.eip155?.events ?? []), ...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []), ...(requiredNamespaces.eip155?.events ?? []),
], ],
accounts: sortedAccounts.map(ethAccount => { accounts: accounts.map(ethAccount => {
return `${namespaceChainId}:${ethAccount.address}`; return `${namespaceChainId}:${ethAccount.address}`;
}), }),
}, },
@ -200,7 +176,7 @@ export const getNamespaces = async (
...(optionalNamespaces.cosmos?.events ?? []), ...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []), ...(requiredNamespaces.cosmos?.events ?? []),
], ],
accounts: sortedAccounts.map(cosmosAccount => { accounts: accounts.map(cosmosAccount => {
return `${namespaceChainId}:${cosmosAccount.address}`; return `${namespaceChainId}:${cosmosAccount.address}`;
}), }),
}, },

View File

@ -2071,6 +2071,11 @@
dependencies: dependencies:
merge-options "^3.0.4" merge-options "^3.0.4"
"@react-native-clipboard/clipboard@^1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.14.1.tgz#835f82fc86881a0808a8405f2576617bb5383554"
integrity sha512-SM3el0A28SwoeJljVNhF217o0nI4E7RfalLmuRQcT1/7tGcxUjgFa3jyrEndYUct8/uxxK5EUNGUu1YEDqzxqw==
"@react-native-community/cli-clean@12.3.2": "@react-native-community/cli-clean@12.3.2":
version "12.3.2" version "12.3.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-12.3.2.tgz#d4f1730c3d22d816b4d513d330d5f3896a3f5921" resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-12.3.2.tgz#d4f1730c3d22d816b4d513d330d5f3896a3f5921"