Add component to support signing cosmos tx with custom messages (#27)

Part of https://www.notion.so/Stage0-onboarding-flow-1e4a6b22d47280aba3b5da3ed1154ff5

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: LaconicNetwork/laconic-wallet-web#27
Co-authored-by: shreerang <shreerang@noreply.git.vdb.to>
Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
This commit is contained in:
shreerang 2025-05-06 13:19:54 +00:00 committed by nabarun
parent e24d697c6d
commit 36208870ab
17 changed files with 527 additions and 44 deletions

View File

@ -6,6 +6,7 @@
"@cerc-io/registry-sdk": "^0.2.5",
"@cosmjs/amino": "^0.32.3",
"@cosmjs/crypto": "^0.32.3",
"@cosmjs/encoding": "^0.33.1",
"@cosmjs/proto-signing": "^0.32.3",
"@cosmjs/stargate": "^0.32.3",
"@emotion/react": "^11.13.0",
@ -28,6 +29,7 @@
"cosmjs-types": "^0.9.0",
"ethers": "5.7.2",
"https-browserify": "^1.0.0",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -75,6 +77,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
"@types/json-bigint": "^1.0.4",
"@types/lodash": "^4.17.7",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",

View File

@ -30,7 +30,7 @@ import { getSignParamsMessage } from "./utils/wallet-connect/helpers";
import ApproveTransfer from "./screens/ApproveTransfer";
import AddNetwork from "./screens/AddNetwork";
import EditNetwork from "./screens/EditNetwork";
import { COSMOS, EIP155 } from "./utils/constants";
import { CHECK_BALANCE, COSMOS, EIP155, IS_SUFFICIENT } from "./utils/constants";
import { useNetworks } from "./context/NetworksContext";
import { NETWORK_METHODS } from "./utils/wallet-connect/common-data";
import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData";
@ -44,6 +44,7 @@ import { useWebViewHandler } from "./hooks/useWebViewHandler";
import SignRequestEmbed from "./screens/SignRequestEmbed";
import useAddAccountEmbed from "./hooks/useAddAccountEmbed";
import useExportPKEmbed from "./hooks/useExportPrivateKeyEmbed";
import { SignTxEmbed } from "./screens/SignTxEmbed";
const Stack = createStackNavigator<StackParamsList>();
@ -231,7 +232,7 @@ const App = (): React.JSX.Element => {
useEffect(() => {
const handleCheckBalance = async (event: MessageEvent) => {
if (event.data.type !== 'CHECK_BALANCE') return;
if (event.data.type !== CHECK_BALANCE) return;
const { chainId, amount } = event.data;
const network = networksData.find(net => net.chainId === chainId);
@ -271,7 +272,7 @@ const App = (): React.JSX.Element => {
const areFundsSufficient = checkSufficientFunds(amount, balance.amount);
sendMessage(event.source as Window, 'IS_SUFFICIENT', areFundsSufficient, event.origin);
sendMessage(event.source as Window, IS_SUFFICIENT, areFundsSufficient, event.origin);
};
window.addEventListener('message', handleCheckBalance);
@ -388,6 +389,13 @@ const App = (): React.JSX.Element => {
header: () => <></>,
}}
/>
<Stack.Screen
name="sign-tx-request-embed"
component={SignTxEmbed}
options={{
header: () => <></>,
}}
/>
<Stack.Screen
name="auto-sign-in"
component={AutoSignIn}
@ -396,7 +404,7 @@ const App = (): React.JSX.Element => {
}}
/>
<Stack.Screen
name="sign-request-embed"
name="sign-message-request-embed"
component={SignRequestEmbed}
options={{
header: () => <Header title="Wallet" />,

View File

@ -6,6 +6,7 @@ import useAccountsData from '../hooks/useAccountsData';
import { addAccount } from '../utils/accounts';
import { useAccounts } from '../context/AccountsContext';
import { Account, NetworksDataState } from '../types';
import { ADD_ACCOUNT_RESPONSE, REQUEST_ADD_ACCOUNT } from '../utils/constants';
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
@ -24,7 +25,7 @@ const useAddAccountEmbed = () => {
useEffect(() => {
const handleAddAccount = async (event: MessageEvent) => {
if (event.data.type !== 'ADD_ACCOUNT') return;
if (event.data.type !== REQUEST_ADD_ACCOUNT) return;
if (!REACT_APP_ALLOWED_URLS) {
console.log('Unauthorized app origin:', event.origin);
@ -44,7 +45,7 @@ const useAddAccountEmbed = () => {
const updatedAccounts = await getAccountsData(event.data.chainId);
const addresses = updatedAccounts.map((account: Account) => account.address);
sendMessage(event.source as Window, 'ADD_ACCOUNT_RESPONSE', addresses, event.origin);
sendMessage(event.source as Window, ADD_ACCOUNT_RESPONSE, addresses, event.origin);
};
window.addEventListener('message', handleAddAccount);

View File

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useAccounts } from '../context/AccountsContext';
import { getPathKey, sendMessage } from '../utils/misc';
import { ACCOUNT_PK_RESPONSE, REQUEST_ACCOUNT_PK } from '../utils/constants';
const useExportPKEmbed = () => {
const { accounts } = useAccounts();
@ -10,7 +11,7 @@ const useExportPKEmbed = () => {
const handleMessage = async (event: MessageEvent) => {
const { type, chainId, address } = event.data;
if (type !== 'REQUEST_ACCOUNT_PK') return;
if (type !== REQUEST_ACCOUNT_PK) return;
try {
const selectedAccount = accounts.find(account => account.address === address);
@ -23,7 +24,7 @@ const useExportPKEmbed = () => {
sendMessage(
event.source as Window,
'ACCOUNT_PK_DATA',
ACCOUNT_PK_RESPONSE,
{ privateKey },
event.origin,
);

View File

@ -5,6 +5,7 @@ import { sendMessage } from "../utils/misc";
import useAccountsData from "./useAccountsData";
import { useNetworks } from "../context/NetworksContext";
import { useAccounts } from "../context/AccountsContext";
import { REQUEST_CREATE_OR_GET_ACCOUNTS, WALLET_ACCOUNTS_DATA } from "../utils/constants";
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
@ -31,7 +32,7 @@ const useGetOrCreateAccounts = () => {
useEffect(() => {
const handleCreateAccounts = async (event: MessageEvent) => {
if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return;
if (event.data.type !== REQUEST_CREATE_OR_GET_ACCOUNTS) return;
if (!REACT_APP_ALLOWED_URLS) {
console.log('Allowed URLs are not set');
@ -50,7 +51,7 @@ const useGetOrCreateAccounts = () => {
console.log('Sending WALLET_ACCOUNTS_DATA accounts:', accountsAddressList);
sendMessage(
event.source as Window, 'WALLET_ACCOUNTS_DATA',
event.source as Window, WALLET_ACCOUNTS_DATA,
accountsAddressList,
event.origin
);

View File

@ -23,7 +23,7 @@ import {
INVALID_URL_ERROR,
IS_NUMBER_REGEX,
} from "../utils/constants";
import { getCosmosAccounts } from "../utils/accounts";
import { getCosmosAccountByHDPath } from "../utils/accounts";
import ETH_CHAINS from "../assets/ethereum-chains.json";
import {
getInternetCredentials,
@ -163,7 +163,7 @@ const AddNetwork = () => {
case COSMOS:
address = (
await getCosmosAccounts(
await getCosmosAccountByHDPath(
mnemonic,
hdPath,
(newNetworkData as z.infer<typeof cosmosNetworkDataSchema>)

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { useNetworks } from '../context/NetworksContext';
import { signMessage } from '../utils/sign-message';
import { EIP155 } from '../utils/constants';
import { AUTO_SIGN_IN, EIP155, SIGN_IN_RESPONSE } from '../utils/constants';
import { sendMessage } from '../utils/misc';
import useAccountsData from '../hooks/useAccountsData';
import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts';
@ -16,7 +16,7 @@ export const AutoSignIn = () => {
useEffect(() => {
const handleSignIn = async (event: MessageEvent) => {
if (event.data.type !== 'AUTO_SIGN_IN') return;
if (event.data.type !== AUTO_SIGN_IN) return;
if (!REACT_APP_ALLOWED_URLS) {
console.log('Allowed URLs are not set');
@ -38,7 +38,7 @@ export const AutoSignIn = () => {
const signature = await signMessage({ message: event.data.message, accountId: accountsData[0].index, chainId: event.data.chainId, namespace: EIP155 })
sendMessage(event.source as Window, 'SIGN_IN_RESPONSE', { message: event.data.message, signature }, event.origin);
sendMessage(event.source as Window, SIGN_IN_RESPONSE, { message: event.data.message, signature }, event.origin);
};
window.addEventListener('message', handleSignIn);

View File

@ -12,18 +12,20 @@ import { getHeaderTitle } from '@react-navigation/elements';
import { Account, StackParamsList } from '../types';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import { getCosmosAccounts, retrieveSingleAccount } from '../utils/accounts';
import { getCosmosAccountByHDPath, retrieveSingleAccount } from '../utils/accounts';
import { getMnemonic, getPathKey, sendMessage } from '../utils/misc';
import { COSMOS } from '../utils/constants';
import { COSMOS, REQUEST_SIGN_MESSAGE, SIGN_MESSAGE_RESPONSE } from '../utils/constants';
import { useNetworks } from '../context/NetworksContext';
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'sign-request-embed'>;
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'sign-message-request-embed'>;
const SignRequestEmbed = ({ route }: SignRequestProps) => {
const [displayAccount, setDisplayAccount] = useState<Account>();
const [message, setMessage] = useState<string>('');
const [chainId, setChainId] = useState<string>('');
const [namespace, setNamespace] = useState<string>('');
const [signDoc, setSignDoc] = useState<any>(null);
const [signerAddress, setSignerAddress] = useState<string>('');
const [origin, setOrigin] = useState<string>('');
@ -31,6 +33,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
const [isLoading, setIsLoading] = useState(true);
const [isApproving, setIsApproving] = useState(false);
const { networksData } = useNetworks();
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -39,10 +42,21 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
setIsApproving(true);
try {
const requestAccount = await retrieveSingleAccount(COSMOS, chainId, signerAddress);
const path = (await getPathKey(`${COSMOS}:${chainId}`, requestAccount!.index)).path;
if (namespace !== COSMOS) {
// TODO: Support ETH namespace
throw new Error(`namespace ${namespace} is not supported`)
}
const requestAccount = await retrieveSingleAccount(namespace, chainId, signerAddress);
const path = (await getPathKey(`${namespace}:${chainId}`, requestAccount!.index)).path;
const mnemonic = await getMnemonic();
const cosmosAccount = await getCosmosAccounts(mnemonic, path, 'zenith');
const requestedNetworkData = networksData.find(networkData => networkData.chainId === chainId)
if (!requestedNetworkData) {
throw new Error("Requested network not found")
}
const cosmosAccount = await getCosmosAccountByHDPath(mnemonic, path, requestedNetworkData?.addressPrefix);
const cosmosAminoSignature = await cosmosAccount.cosmosWallet.signAmino(
signerAddress,
@ -53,7 +67,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
sendMessage(
sourceWindow,
'ZENITH_SIGNED_MESSAGE',
SIGN_MESSAGE_RESPONSE,
{ signature },
origin,
);
@ -63,7 +77,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
console.error('Signing failed:', err);
sendMessage(
sourceWindow!,
'ZENITH_SIGNED_MESSAGE',
SIGN_MESSAGE_RESPONSE,
{ error: err },
origin,
);
@ -76,7 +90,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
if (sourceWindow && origin) {
sendMessage(
sourceWindow,
'ZENITH_SIGNED_MESSAGE',
SIGN_MESSAGE_RESPONSE,
{ error: 'User rejected the request' },
origin,
);
@ -85,7 +99,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
useEffect(() => {
const handleCosmosSignMessage = async (event: MessageEvent) => {
if (event.data.type !== 'SIGN_ZENITH_MESSAGE') return;
if (event.data.type !== REQUEST_SIGN_MESSAGE) return;
if (!REACT_APP_ALLOWED_URLS) {
@ -103,16 +117,25 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => {
try {
const { signerAddress, signDoc } = event.data.params;
const receivedNamespace = event.data.chainId.split(':')[0]
const receivedChainId = event.data.chainId.split(':')[1]
if (receivedNamespace !== COSMOS) {
// TODO: Support ETH namespace
throw new Error(`namespace ${receivedNamespace} is not supported`)
}
setSignerAddress(signerAddress);
setSignDoc(signDoc);
setMessage(signDoc.memo || '');
setOrigin(event.origin);
setSourceWindow(event.source as Window);
setChainId(event.data.chainId);
setNamespace(receivedNamespace);
setChainId(receivedChainId);
const requestAccount = await retrieveSingleAccount(
COSMOS,
event.data.chainId,
receivedNamespace,
receivedChainId,
signerAddress,
);

392
src/screens/SignTxEmbed.tsx Normal file
View File

@ -0,0 +1,392 @@
import React, { useEffect, useState, useCallback } from 'react';
import { ScrollView, View } from 'react-native';
import {
Button,
Text,
} from 'react-native-paper';
import JSONbig from 'json-bigint';
import { AuthInfo, SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { DirectSecp256k1Wallet, Algo, TxBodyEncodeObject, decodeOptionalPubkey } from '@cosmjs/proto-signing';
import { SigningStargateClient } from '@cosmjs/stargate';
import { toHex } from '@cosmjs/encoding';
import { getCosmosAccountByHDPath, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import { getMnemonic, getPathKey, sendMessage } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import TxErrorDialog from '../components/TxErrorDialog';
import { Account, NetworksDataState } from '../types';
import { REQUEST_SIGN_TX, REQUEST_COSMOS_ACCOUNTS, COSMOS_ACCOUNTS_RESPONSE, SIGN_TX_RESPONSE } from '../utils/constants';
// Type Definitions
interface GetAccountsRequestData {
chainId: string,
}
interface SignTxRequestData {
address: string;
signDoc: SignDoc;
txBody: TxBodyEncodeObject;
}
type IncomingMessageData = SignTxRequestData | GetAccountsRequestData;
interface IncomingMessageEventData {
id: string;
type: typeof REQUEST_SIGN_TX | typeof REQUEST_COSMOS_ACCOUNTS;
data: IncomingMessageData;
}
type TransactionDetails = {
source: MessageEventSource;
origin: string;
signerAddress: string;
chainId: string;
account: Account;
requestedNetwork: NetworksDataState;
balance: string;
signDoc: SignDoc;
txBody: TxBodyEncodeObject;
};
interface GetAccountsResponse {
accounts: Array<{
algo: Algo;
address: string;
pubkey: string;
}>;
}
const REACT_APP_ALLOWED_URLS = process.env.REACT_APP_ALLOWED_URLS;
export const SignTxEmbed = () => {
const [isTxApprovalVisible, setIsTxApprovalVisible] = useState<boolean>(false);
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
const [isTxLoading, setIsTxLoading] = useState(false);
const [txError, setTxError] = useState<string | null>(null);
const { networksData } = useNetworks();
// Message Handlers
const handleGetCosmosAccountsRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
const { data } = event.data;
const source = event.source as Window;
const origin = event.origin;
const requestData = data as GetAccountsRequestData;
const mnemonic = await getMnemonic();
try {
const requestedNetworkData = networksData.find(networkData => networkData.chainId === requestData.chainId)
if(!requestedNetworkData) {
throw new Error("Network data not found")
}
const allAccounts = await retrieveAccounts(requestedNetworkData);
if (!allAccounts || allAccounts.length === 0) {
throw new Error("Accounts not found for network")
}
const responseAccounts = await Promise.all(
allAccounts.map(async (acc) => {
const cosmosAccount = (await getCosmosAccountByHDPath(mnemonic, acc.hdPath, requestedNetworkData.addressPrefix)).data;
return {
...cosmosAccount,
pubkey: toHex(cosmosAccount.pubkey),
};
})
);
const response: GetAccountsResponse = { accounts: responseAccounts };
sendMessage(source, COSMOS_ACCOUNTS_RESPONSE, {data: response}, origin);
} catch (error: unknown) {
console.error(`Error handling ${REQUEST_COSMOS_ACCOUNTS}:`, error);
const errorMsg = error instanceof Error ? error.message : String(error);
sendMessage(source, COSMOS_ACCOUNTS_RESPONSE, { error: `Failed to get accounts: ${errorMsg}` }, origin);
}
}, [networksData]);
const handleSignTxRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
const { data } = event.data;
const source = event.source as Window;
const origin = event.origin;
const requestData = data as SignTxRequestData;
setIsTxApprovalVisible(false);
setTransactionDetails(null);
setTxError(null);
try {
const { address: signerAddress, signDoc, txBody } = requestData;
const network = networksData.find(net => net.chainId === signDoc.chainId);
if (!network) throw new Error(`Network with chainId "${signDoc.chainId}" not supported.`);
const account = await retrieveSingleAccount(network.namespace, network.chainId, signerAddress);
if (!account) throw new Error(`Account not found for address "${signerAddress}" on chain "${signDoc.chainId}".`);
// Balance Check
// Use a temporary read-only client for balance
const { privKey } = await getPathKey(`${network.namespace}:${network.chainId}`, account.index)
const tempWallet = await DirectSecp256k1Wallet.fromKey(
new Uint8Array(Buffer.from(privKey.replace(/^0x/, ''), 'hex')),
network.addressPrefix
);
const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, tempWallet);
const balance = await client.getBalance(account.address, network.nativeDenom!);
client.disconnect();
if (!balance || balance.amount === "0") {
throw new Error(`${account.address} does not have any balance`)
}
setTransactionDetails({
source: source,
origin: origin,
signerAddress,
chainId: signDoc.chainId,
account,
requestedNetwork: network,
balance: balance.amount,
signDoc,
txBody
});
setIsTxApprovalVisible(true);
} catch (error: unknown) {
console.error(`Error handling ${REQUEST_SIGN_TX}:`, error);
const errorMsg = error instanceof Error ? error.message : String(error);
sendMessage(source, SIGN_TX_RESPONSE, { error: `Failed to prepare transaction: ${errorMsg}` }, origin);
setTxError(errorMsg);
}
}, [networksData]);
const handleIncomingMessage = useCallback((event: MessageEvent) => {
if (!event.data || typeof event.data !== 'object' || !event.data.type || !event.source || event.source === window) {
return; // Basic validation
}
if (!REACT_APP_ALLOWED_URLS) {
console.log('Allowed URLs are not set');
return;
}
const allowedUrls = REACT_APP_ALLOWED_URLS.split(',').map(url => url.trim());
if (!allowedUrls.includes(event.origin)) {
console.log('Unauthorized app.');
return;
}
const messageData = event.data as IncomingMessageEventData;
switch (messageData.type) {
case REQUEST_COSMOS_ACCOUNTS:
handleGetCosmosAccountsRequest(event as MessageEvent<IncomingMessageEventData>);
break;
case REQUEST_SIGN_TX:
handleSignTxRequest(event as MessageEvent<IncomingMessageEventData>);
break;
default:
console.warn(`Received unknown message type: ${messageData.type}`);
}
}, [handleGetCosmosAccountsRequest, handleSignTxRequest]);
useEffect(() => {
window.addEventListener('message', handleIncomingMessage);
return () => {
window.removeEventListener('message', handleIncomingMessage);
};
}, [handleIncomingMessage]);
// Action Handlers
const acceptRequestHandler = async () => {
if (!transactionDetails) {
setTxError("Transaction details are missing.");
return;
}
setIsTxLoading(true);
setTxError(null);
const { source, origin, requestedNetwork, chainId, account, signerAddress, signDoc } = transactionDetails;
try {
const { privKey } = await getPathKey(`${requestedNetwork.namespace}:${chainId}`, account.index);
const privateKeyBytes = Buffer.from(privKey.replace(/^0x/, ''), 'hex');
const wallet = await DirectSecp256k1Wallet.fromKey(new Uint8Array(privateKeyBytes), requestedNetwork.addressPrefix); // Wrap in Uint8Array
// Perform the actual signing
const signResponse = await wallet.signDirect(signerAddress, signDoc);
sendMessage(source as Window, SIGN_TX_RESPONSE, {data: signResponse}, origin);
setIsTxApprovalVisible(false);
setTransactionDetails(null);
} catch (error: unknown) {
console.error("Error during signDirect:", error);
const errorMsg = error instanceof Error ? error.message : String(error);
setTxError(errorMsg);
sendMessage(source as Window, SIGN_TX_RESPONSE, { error: `Failed to sign transaction: ${errorMsg}` }, origin);
} finally {
setIsTxLoading(false);
}
};
const rejectRequestHandler = () => {
if (!transactionDetails) return;
const { source, origin } = transactionDetails;
sendMessage(source as Window, SIGN_TX_RESPONSE, { error: "User rejected the signature request." }, origin);
setIsTxApprovalVisible(false);
setTransactionDetails(null);
setTxError(null);
};
const decodedAuth = React.useMemo(() => {
if (!transactionDetails) {
return
}
const info = AuthInfo.decode(transactionDetails.signDoc.authInfoBytes);
return {
...info,
signerInfos: info.signerInfos.map((signerInfo) => ({
...signerInfo,
publicKey: decodeOptionalPubkey(signerInfo.publicKey),
})),
};
}, [transactionDetails]);
const formattedTxBody = React.useMemo(
() => {
if (!transactionDetails) {
return
}
return JSONbig.stringify(transactionDetails.txBody, null, 2)
},
[transactionDetails]
);
const formattedAuthInfo = React.useMemo(
() => JSONbig.stringify(decodedAuth, null, 2),
[decodedAuth]
);
const formattedSignDoc = React.useMemo(
() =>
{
if (!transactionDetails) {
return
}
return JSONbig.stringify(
{
...transactionDetails.signDoc,
bodyBytes: toHex(transactionDetails.signDoc.bodyBytes),
authInfoBytes: toHex(transactionDetails.signDoc.authInfoBytes),
},
null,
2
)
},
[transactionDetails]
);
return (
<>
{isTxApprovalVisible && transactionDetails ? (
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={{ marginBottom: 16 }}>
<Text variant='titleLarge'>Account</Text>
<View style={styles.dataBox}>
<AccountDetails account={transactionDetails.account} />
</View>
</View>
<View style={{ marginBottom: 16 }}>
<Text variant='titleLarge'>{`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}</Text>
<View style={styles.dataBox}>
<Text variant='bodyLarge'>
{transactionDetails.balance}
</Text>
</View>
</View>
<View style={{ marginBottom: 16 }}>
<Text variant='titleLarge'>Transaction Body</Text>
<ScrollView style={styles.messageBody} contentContainerStyle={{ padding: 10 }}>
<Text style={styles.codeText}>
{formattedTxBody}
</Text>
</ScrollView>
</View>
<View style={{ marginBottom: 16 }}>
<Text variant='titleLarge'>Auth Info</Text>
<ScrollView style={styles.messageBody} contentContainerStyle={{ padding: 10 }}>
<Text style={styles.codeText}>
{formattedAuthInfo}
</Text>
</ScrollView>
</View>
<View style={{ marginBottom: 16 }}>
<Text variant='titleLarge'>Transaction Data To Be Signed</Text>
<ScrollView style={styles.messageBody} contentContainerStyle={{ padding: 10 }}>
<Text style={styles.codeText}>
{formattedSignDoc}
</Text>
</ScrollView>
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={acceptRequestHandler}
loading={isTxLoading}
disabled={isTxLoading}
style={{marginTop: 10}}
>
{isTxLoading ? 'Processing...' : 'Approve'}
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D"
disabled={isTxLoading}
style={{ marginTop: 10 }}
>
Reject
</Button>
</View>
</>
) : (
<View style={styles.spinnerContainer}>
<Text style={{ marginTop: 50, textAlign: 'center' }}>Waiting for request...</Text>
</View>
)}
<TxErrorDialog
error={txError!}
visible={!!txError}
hideDialog={() => setTxError(null)}
/>
</>
);
};

View File

@ -26,6 +26,7 @@ import { MEMO } from '../screens/ApproveTransfer';
import { Account, NetworksDataState } from '../types';
import useGetOrCreateAccounts from '../hooks/useGetOrCreateAccounts';
import useAccountsData from '../hooks/useAccountsData';
import { REQUEST_TX, REQUEST_WALLET_ACCOUNTS, TRANSACTION_RESPONSE, WALLET_ACCOUNTS_DATA } from '../utils/constants';
type TransactionDetails = {
chainId: string;
@ -51,7 +52,7 @@ export const WalletEmbed = () => {
useEffect(() => {
const handleGetAccounts = async (event: MessageEvent) => {
if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return;
if (event.data.type !== REQUEST_WALLET_ACCOUNTS) return;
const accountsData = await getAccountsData(event.data.chainId);
@ -62,7 +63,7 @@ export const WalletEmbed = () => {
sendMessage(
event.source as Window,
'WALLET_ACCOUNTS_DATA',
WALLET_ACCOUNTS_DATA,
accountsData.map(account => account.address),
event.origin
);
@ -81,7 +82,7 @@ export const WalletEmbed = () => {
const handleTxRequested = useCallback(
async (event: MessageEvent) => {
try {
if (event.data.type !== 'REQUEST_TX') return;
if (event.data.type !== REQUEST_TX) return;
txEventRef.current = event;
@ -139,7 +140,6 @@ export const WalletEmbed = () => {
});
if (!checkSufficientFunds(amount, balance.amount)) {
console.log("Insufficient funds detected. Throwing error.");
throw new Error('Insufficient funds');
}
@ -207,7 +207,7 @@ export const WalletEmbed = () => {
const event = txEventRef.current;
if (event?.source) {
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', txResult.transactionHash, event.origin);
sendMessage(event.source as Window, TRANSACTION_RESPONSE, txResult.transactionHash, event.origin);
} else {
console.error('No event source available to send message');
}
@ -227,7 +227,7 @@ export const WalletEmbed = () => {
setIsTxRequested(false);
setTransactionDetails(null);
if (event?.source) {
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', null, event.origin);
sendMessage(event.source as Window, TRANSACTION_RESPONSE, null, event.origin);
} else {
console.error('No event source available to send message');
}
@ -307,7 +307,7 @@ export const WalletEmbed = () => {
hideDialog={() => {
setTxError(null)
if (window.parent) {
sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*');
sendMessage(window.parent, TRANSACTION_RESPONSE, null, '*');
sendMessage(window.parent, 'closeIframe', null, '*');
}
}}

View File

@ -355,6 +355,10 @@ const styles = StyleSheet.create({
marginTop: 12,
marginBottom: 20,
},
codeText: {
fontFamily: 'monospace',
fontSize: 12,
},
});
export default styles;

View File

@ -40,7 +40,8 @@ export type StackParamsList = {
};
"wallet-embed": undefined;
"auto-sign-in": undefined;
"sign-request-embed": undefined;
"sign-message-request-embed": undefined;
"sign-tx-request-embed": undefined;
};
export type Account = {

View File

@ -56,7 +56,7 @@ const createWalletFromMnemonic = async (
case COSMOS:
address = (
await getCosmosAccounts(mnemonic, hdPath, network.addressPrefix)
await getCosmosAccountByHDPath(mnemonic, hdPath, network.addressPrefix)
).data.address;
break;
@ -281,7 +281,7 @@ const accountInfoFromHDPath = async (
break;
case COSMOS:
address = (
await getCosmosAccounts(mnemonic, hdPath, networkData.addressPrefix)
await getCosmosAccountByHDPath(mnemonic, hdPath, networkData.addressPrefix)
).data.address;
break;
default:
@ -323,7 +323,7 @@ const updateAccountCounter = async (
);
};
const getCosmosAccounts = async (
const getCosmosAccountByHDPath = async (
mnemonic: string,
path: string,
prefix: string = COSMOS,
@ -351,5 +351,5 @@ export {
accountInfoFromHDPath,
getNextAccountId,
updateAccountCounter,
getCosmosAccounts,
getCosmosAccountByHDPath,
};

View File

@ -74,3 +74,26 @@ export const INVALID_URL_ERROR = 'Invalid URL';
export const IS_NUMBER_REGEX = /^\d+$/;
export const IS_IMPORT_WALLET_ENABLED = false;
// iframe request types
export const REQUEST_COSMOS_ACCOUNTS = 'REQUEST_COSMOS_ACCOUNTS';
export const REQUEST_SIGN_TX = 'REQUEST_SIGN_TX';
export const REQUEST_SIGN_MESSAGE = 'REQUEST_SIGN_MESSAGE';
export const REQUEST_WALLET_ACCOUNTS = 'REQUEST_WALLET_ACCOUNTS';
export const REQUEST_CREATE_OR_GET_ACCOUNTS = 'REQUEST_CREATE_OR_GET_ACCOUNTS';
export const REQUEST_TX = 'REQUEST_TX';
export const REQUEST_ACCOUNT_PK = 'REQUEST_ACCOUNT_PK';
export const REQUEST_ADD_ACCOUNT = 'REQUEST_ADD_ACCOUNT';
export const AUTO_SIGN_IN = 'AUTO_SIGN_IN';
export const CHECK_BALANCE = 'CHECK_BALANCE';
// iframe response types
export const COSMOS_ACCOUNTS_RESPONSE = 'COSMOS_ACCOUNTS_RESPONSE';
export const SIGN_TX_RESPONSE = 'SIGN_TX_RESPONSE';
export const SIGN_MESSAGE_RESPONSE = 'SIGN_MESSAGE_RESPONSE';
export const TRANSACTION_RESPONSE = 'TRANSACTION_RESPONSE';
export const SIGN_IN_RESPONSE = 'SIGN_IN_RESPONSE';
export const ACCOUNT_PK_RESPONSE = 'ACCOUNT_PK_RESPONSE';
export const ADD_ACCOUNT_RESPONSE = 'ADD_ACCOUNT_RESPONSE';
export const WALLET_ACCOUNTS_DATA = 'WALLET_ACCOUNTS_DATA';
export const IS_SUFFICIENT = 'IS_SUFFICIENT';

View File

@ -10,7 +10,7 @@ import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
import { SignMessageParams } from '../types';
import { getDirectWallet, getMnemonic, getPathKey } from './misc';
import { getCosmosAccounts } from './accounts';
import { getCosmosAccountByHDPath } from './accounts';
import { COSMOS, EIP155 } from './constants';
const signMessage = async ({
@ -58,7 +58,7 @@ const signCosmosMessage = async (
const mnemonic = await getMnemonic();
const addressPrefix = fromBech32(cosmosAddress).prefix
const cosmosAccount = await getCosmosAccounts(mnemonic, path, addressPrefix);
const cosmosAccount = await getCosmosAccountByHDPath(mnemonic, path, addressPrefix);
const address = cosmosAccount.data.address;
const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino(
address,

View File

@ -18,7 +18,7 @@ import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { signDirectMessage, signEthMessage } from '../sign-message';
import { Account } from '../../types';
import { getMnemonic, getPathKey } from '../misc';
import { getCosmosAccounts } from '../accounts';
import { getCosmosAccountByHDPath } from '../accounts';
import { COSMOS_METHODS } from './COSMOSData';
import { COSMOS } from '../constants';
@ -88,7 +88,7 @@ export async function approveWalletConnectRequest(
addressPrefix = fromBech32(account.address).prefix
}
const cosmosAccount = await getCosmosAccounts(mnemonic, path, addressPrefix);
const cosmosAccount = await getCosmosAccountByHDPath(mnemonic, path, addressPrefix);
const address = account.address;
switch (request.method) {

View File

@ -1410,6 +1410,15 @@
bech32 "^1.1.4"
readonly-date "^1.0.0"
"@cosmjs/encoding@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.33.1.tgz#77d6a8e0152c658ecf07b5aee3f5968d9071da50"
integrity sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw==
dependencies:
base64-js "^1.3.0"
bech32 "^1.1.4"
readonly-date "^1.0.0"
"@cosmjs/json-rpc@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.32.4.tgz#be91eb89ea78bd5dc02d0a9fa184dd6790790f0b"
@ -3939,6 +3948,11 @@
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/json-bigint@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.4.tgz#250d29e593375499d8ba6efaab22d094c3199ef3"
integrity sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==
"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@ -5375,6 +5389,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd"
integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@ -9822,6 +9841,13 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"