diff --git a/package.json b/package.json index 527e2a3..8d8b52d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 1ed1a09..fd8c034 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(); @@ -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: () => <>, }} /> + <>, + }} + /> { }} />
, diff --git a/src/hooks/useAddAccountEmbed.ts b/src/hooks/useAddAccountEmbed.ts index d226484..2514b1d 100644 --- a/src/hooks/useAddAccountEmbed.ts +++ b/src/hooks/useAddAccountEmbed.ts @@ -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); diff --git a/src/hooks/useExportPrivateKeyEmbed.ts b/src/hooks/useExportPrivateKeyEmbed.ts index 40b5684..ec0e1cb 100644 --- a/src/hooks/useExportPrivateKeyEmbed.ts +++ b/src/hooks/useExportPrivateKeyEmbed.ts @@ -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, ); diff --git a/src/hooks/useGetOrCreateAccounts.ts b/src/hooks/useGetOrCreateAccounts.ts index 2a111d9..a2418e8 100644 --- a/src/hooks/useGetOrCreateAccounts.ts +++ b/src/hooks/useGetOrCreateAccounts.ts @@ -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 ); diff --git a/src/screens/AddNetwork.tsx b/src/screens/AddNetwork.tsx index ee0cd68..d57597c 100644 --- a/src/screens/AddNetwork.tsx +++ b/src/screens/AddNetwork.tsx @@ -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) diff --git a/src/screens/AutoSignIn.tsx b/src/screens/AutoSignIn.tsx index 0a16e00..1354534 100644 --- a/src/screens/AutoSignIn.tsx +++ b/src/screens/AutoSignIn.tsx @@ -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); diff --git a/src/screens/SignRequestEmbed.tsx b/src/screens/SignRequestEmbed.tsx index 7a3fbfd..7a789d6 100644 --- a/src/screens/SignRequestEmbed.tsx +++ b/src/screens/SignRequestEmbed.tsx @@ -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; +type SignRequestProps = NativeStackScreenProps; const SignRequestEmbed = ({ route }: SignRequestProps) => { const [displayAccount, setDisplayAccount] = useState(); const [message, setMessage] = useState(''); const [chainId, setChainId] = useState(''); + const [namespace, setNamespace] = useState(''); const [signDoc, setSignDoc] = useState(null); const [signerAddress, setSignerAddress] = useState(''); const [origin, setOrigin] = useState(''); @@ -31,6 +33,7 @@ const SignRequestEmbed = ({ route }: SignRequestProps) => { const [isLoading, setIsLoading] = useState(true); const [isApproving, setIsApproving] = useState(false); + const { networksData } = useNetworks(); const navigation = useNavigation>(); @@ -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, ); diff --git a/src/screens/SignTxEmbed.tsx b/src/screens/SignTxEmbed.tsx new file mode 100644 index 0000000..472fe3c --- /dev/null +++ b/src/screens/SignTxEmbed.tsx @@ -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(false); + const [transactionDetails, setTransactionDetails] = useState(null); + const [isTxLoading, setIsTxLoading] = useState(false); + const [txError, setTxError] = useState(null); + + const { networksData } = useNetworks(); + + // Message Handlers + + const handleGetCosmosAccountsRequest = useCallback(async (event: MessageEvent) => { + 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) => { + 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); + break; + case REQUEST_SIGN_TX: + handleSignTxRequest(event as MessageEvent); + 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 ? ( + <> + + + Account + + + + + + + {`Balance (${transactionDetails.requestedNetwork.nativeDenom})`} + + + {transactionDetails.balance} + + + + + + Transaction Body + + + {formattedTxBody} + + + + + + Auth Info + + + {formattedAuthInfo} + + + + + + Transaction Data To Be Signed + + + {formattedSignDoc} + + + + + + + + + + + ) : ( + + Waiting for request... + + )} + setTxError(null)} + /> + + ); +}; diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index c3e836d..8646d54 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -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, '*'); } }} diff --git a/src/styles/stylesheet.js b/src/styles/stylesheet.js index a71df18..60091a5 100644 --- a/src/styles/stylesheet.js +++ b/src/styles/stylesheet.js @@ -355,6 +355,10 @@ const styles = StyleSheet.create({ marginTop: 12, marginBottom: 20, }, + codeText: { + fontFamily: 'monospace', + fontSize: 12, + }, }); export default styles; diff --git a/src/types.ts b/src/types.ts index 31d4c3f..ae93310 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 = { diff --git a/src/utils/accounts.ts b/src/utils/accounts.ts index d7921a6..3b010d5 100644 --- a/src/utils/accounts.ts +++ b/src/utils/accounts.ts @@ -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, }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 5549b7c..959cb85 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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'; diff --git a/src/utils/sign-message.ts b/src/utils/sign-message.ts index bb3904c..ef5ec06 100644 --- a/src/utils/sign-message.ts +++ b/src/utils/sign-message.ts @@ -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, diff --git a/src/utils/wallet-connect/wallet-connect-requests.ts b/src/utils/wallet-connect/wallet-connect-requests.ts index db3d8f4..d00fef2 100644 --- a/src/utils/wallet-connect/wallet-connect-requests.ts +++ b/src/utils/wallet-connect/wallet-connect-requests.ts @@ -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) { diff --git a/yarn.lock b/yarn.lock index 054ff5b..e7b7d0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"