From c1fdbf6db3f37704c92db3abfd8004ef1a1113a5 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Wed, 6 Nov 2024 16:18:45 +0530 Subject: [PATCH 01/16] Add event listener for iframe message --- src/index.tsx | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 61a4189..3944fed 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,6 +18,35 @@ import { createTheme, ThemeProvider } from "@mui/material"; globalThis.Buffer = Buffer; +window.addEventListener('message', (event: MessageEvent) => { + if (event.origin !== 'http://localhost:3000') return; + + if (event.data.type === 'REQUEST_WALLET_ACCOUNTS') { + try { + const accountsData = localStorage.getItem('accounts/cosmos:laconic-testnet-2/0'); + + if (!accountsData) { + event.source?.postMessage({ + type: 'ERROR', + message: 'Wallet accounts not found in local storage' + }); + return; + } + + (event.source as Window)?.postMessage({ + type: 'WALLET_ACCOUNTS_DATA', + data: accountsData + }, 'http://localhost:3000'); + + } catch (error) { + (event.source as Window)?.postMessage({ + type: 'ERROR', + message: 'Error accessing wallet accounts data' + }, 'http://localhost:3000'); + } + } +}); + const linking = { prefixes: ["https://wallet.laconic.com"], }; -- 2.45.2 From 6664a478864b1877cab3446159ed9d3b1e00b157 Mon Sep 17 00:00:00 2001 From: Isha Date: Thu, 7 Nov 2024 10:48:27 +0530 Subject: [PATCH 02/16] Pass address for specific chainId in message --- src/index.tsx | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 3944fed..f7b8b76 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,20 +23,36 @@ window.addEventListener('message', (event: MessageEvent) => { if (event.data.type === 'REQUEST_WALLET_ACCOUNTS') { try { - const accountsData = localStorage.getItem('accounts/cosmos:laconic-testnet-2/0'); + let accountsData = ''; + const indices = localStorage.getItem(`addAccountCounter/cosmos:${event.data.chainId}`); + for (let i = 0; i < Number(indices); i++) { + const account = localStorage.getItem(`accounts/cosmos:${event.data.chainId}/${i}`); + if (account) { + accountsData += `${account},`; + } + } + // Remove trailing comma + accountsData = accountsData.slice(0, -1); + if (!accountsData) { event.source?.postMessage({ type: 'ERROR', - message: 'Wallet accounts not found in local storage' + message: 'Wallet accounts not found in local storage', }); return; } - - (event.source as Window)?.postMessage({ - type: 'WALLET_ACCOUNTS_DATA', - data: accountsData - }, 'http://localhost:3000'); + + const elements = accountsData.split(','); + const addresses = elements.filter((_, index) => (index + 1) % 4 === 0); + + (event.source as Window)?.postMessage( + { + type: 'WALLET_ACCOUNTS_DATA', + data: addresses, + }, + 'http://localhost:3000' + ); } catch (error) { (event.source as Window)?.postMessage({ -- 2.45.2 From 392d961ab70399a51df7f3d7731aa912524af317 Mon Sep 17 00:00:00 2001 From: Isha Date: Thu, 7 Nov 2024 11:21:51 +0530 Subject: [PATCH 03/16] Add component to handle iframe messages --- src/App.tsx | 5 +++ src/components/WalletEmbed.tsx | 64 ++++++++++++++++++++++++++++++++++ src/types.ts | 1 + 3 files changed, 70 insertions(+) create mode 100644 src/components/WalletEmbed.tsx diff --git a/src/App.tsx b/src/App.tsx index 49881f8..d2f255c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,7 @@ import { NETWORK_METHODS } from "./utils/wallet-connect/common-data"; import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData"; import styles from "./styles/stylesheet"; import { Header } from "./components/Header"; +import { WalletEmbed } from "./components/WalletEmbed"; const Stack = createStackNavigator(); @@ -313,6 +314,10 @@ const App = (): React.JSX.Element => { header: () =>
, }} /> + { + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.origin !== 'http://localhost:3000') return; + + if (event.data.type === 'REQUEST_WALLET_ACCOUNTS') { + try { + let accountsData = ''; + const indices = localStorage.getItem(`addAccountCounter/cosmos:${event.data.chainId}`); + for (let i = 0; i < Number(indices); i++) { + const account = localStorage.getItem(`accounts/cosmos:${event.data.chainId}/${i}`); + if (account) { + accountsData += `${account},`; + } + } + + // Remove trailing comma + accountsData = accountsData.slice(0, -1); + + if (!accountsData) { + event.source?.postMessage({ + type: 'ERROR', + message: 'Wallet accounts not found in local storage', + }); + return; + } + + const elements = accountsData.split(','); + const addresses = elements.filter((_, index) => (index + 1) % 4 === 0); + + (event.source as Window)?.postMessage( + { + type: 'WALLET_ACCOUNTS_DATA', + data: addresses, + }, + 'http://localhost:3000' + ); + + } catch (error) { + (event.source as Window)?.postMessage({ + type: 'ERROR', + message: 'Error accessing wallet accounts data' + }, 'http://localhost:3000'); + } + } + }; + + // Set up the message event listener + window.addEventListener('message', handleMessage); + + // Clean up the event listener on component unmount + return () => { + window.removeEventListener('message', handleMessage); + }; + }, []); + + return ( +
+

Text

+
+ ); +}; diff --git a/src/types.ts b/src/types.ts index e6c37c1..a6499ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,7 @@ export type StackParamsList = { requestEvent: Web3WalletTypes.SessionRequest; requestSessionData: SessionTypes.Struct; }; + WalletEmbed: undefined; }; export type Account = { -- 2.45.2 From b5005b8e47afbf89b92e7aab21f45522c59ced8d Mon Sep 17 00:00:00 2001 From: Nabarun Date: Thu, 7 Nov 2024 13:21:40 +0530 Subject: [PATCH 04/16] Return message data to event origin --- src/components/WalletEmbed.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/WalletEmbed.tsx b/src/components/WalletEmbed.tsx index 038d4fe..f28542e 100644 --- a/src/components/WalletEmbed.tsx +++ b/src/components/WalletEmbed.tsx @@ -3,7 +3,7 @@ import React, { useEffect } from 'react' export const WalletEmbed = () => { useEffect(() => { const handleMessage = (event: MessageEvent) => { - if (event.origin !== 'http://localhost:3000') return; + // Not checking for event origin as only account addresses are returned if (event.data.type === 'REQUEST_WALLET_ACCOUNTS') { try { @@ -35,14 +35,14 @@ export const WalletEmbed = () => { type: 'WALLET_ACCOUNTS_DATA', data: addresses, }, - 'http://localhost:3000' + event.origin ); } catch (error) { (event.source as Window)?.postMessage({ type: 'ERROR', message: 'Error accessing wallet accounts data' - }, 'http://localhost:3000'); + }, event.origin); } } }; -- 2.45.2 From 0cba7ab55d73bc2642e712fc2da81fbe128a4284 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 7 Nov 2024 13:40:12 +0530 Subject: [PATCH 05/16] Add bundled method to create account --- src/components/WalletEmbed.tsx | 121 ++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/src/components/WalletEmbed.tsx b/src/components/WalletEmbed.tsx index f28542e..cfc42f7 100644 --- a/src/components/WalletEmbed.tsx +++ b/src/components/WalletEmbed.tsx @@ -1,64 +1,77 @@ -import React, { useEffect } from 'react' +import React, { useEffect } from 'react'; +import { createWallet } from '../utils/accounts'; +import { useNetworks } from '../context/NetworksContext'; export const WalletEmbed = () => { + const { networksData } = useNetworks(); + + const getAccountsData = (chainId: string): string => { + const accountCount = localStorage.getItem(`addAccountCounter/cosmos:${chainId}`); + if (!accountCount) return ''; + + let accountsData = ''; + for (let i = 0; i < Number(accountCount); i++) { + const account = localStorage.getItem(`accounts/cosmos:${chainId}/${i}`); + if (account) accountsData += `${account},`; + } + + return accountsData.slice(0, -1); // Remove trailing comma + }; + + const getAddressesFromData = (accountsData: string): string[] => { + return accountsData + ? accountsData.split(',').filter((_, index) => (index + 1) % 4 === 0) + : []; + }; + + const sendMessage = ( + source: Window | null, + type: string, + data: any, + origin: string + ): void => { + source?.postMessage({ type, data }, origin); + }; + useEffect(() => { - const handleMessage = (event: MessageEvent) => { - // Not checking for event origin as only account addresses are returned + const handleGetAccounts = (event: MessageEvent) => { + if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return; - if (event.data.type === 'REQUEST_WALLET_ACCOUNTS') { - try { - let accountsData = ''; - const indices = localStorage.getItem(`addAccountCounter/cosmos:${event.data.chainId}`); - for (let i = 0; i < Number(indices); i++) { - const account = localStorage.getItem(`accounts/cosmos:${event.data.chainId}/${i}`); - if (account) { - accountsData += `${account},`; - } - } - - // Remove trailing comma - accountsData = accountsData.slice(0, -1); - - if (!accountsData) { - event.source?.postMessage({ - type: 'ERROR', - message: 'Wallet accounts not found in local storage', - }); - return; - } - - const elements = accountsData.split(','); - const addresses = elements.filter((_, index) => (index + 1) % 4 === 0); - - (event.source as Window)?.postMessage( - { - type: 'WALLET_ACCOUNTS_DATA', - data: addresses, - }, - event.origin - ); - - } catch (error) { - (event.source as Window)?.postMessage({ - type: 'ERROR', - message: 'Error accessing wallet accounts data' - }, event.origin); - } + const accountsData = getAccountsData(event.data.chainId); + if (!accountsData) { + sendMessage(event.source as Window, 'ERROR', 'Wallet accounts not found in local storage', event.origin); + return; } + + const addresses = getAddressesFromData(accountsData); + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', addresses, event.origin); }; - // Set up the message event listener - window.addEventListener('message', handleMessage); - - // Clean up the event listener on component unmount - return () => { - window.removeEventListener('message', handleMessage); - }; + window.addEventListener('message', handleGetAccounts); + return () => window.removeEventListener('message', handleGetAccounts); }, []); - return ( -
-

Text

-
- ); + useEffect(() => { + const handleCreateAccounts = async (event: MessageEvent) => { + if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return; + + let accountsData = getAccountsData(event.data.chainId); + + if (!accountsData) { + console.log("Accounts not found, creating wallet..."); + await createWallet(networksData); + + // Re-fetch newly created accounts + accountsData = getAccountsData(event.data.chainId); + } + + const addresses = getAddressesFromData(accountsData); + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', addresses, event.origin); + }; + + window.addEventListener('message', handleCreateAccounts); + return () => window.removeEventListener('message', handleCreateAccounts); + }, [networksData]); + + return

Text

; }; -- 2.45.2 From a1cc6307cf27b8df0af423cbb93b7785b0db2cc2 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 7 Nov 2024 16:27:50 +0530 Subject: [PATCH 06/16] Remove event listener from index.tsx --- src/index.tsx | 45 --------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index f7b8b76..61a4189 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -18,51 +18,6 @@ import { createTheme, ThemeProvider } from "@mui/material"; globalThis.Buffer = Buffer; -window.addEventListener('message', (event: MessageEvent) => { - if (event.origin !== 'http://localhost:3000') return; - - if (event.data.type === 'REQUEST_WALLET_ACCOUNTS') { - try { - let accountsData = ''; - const indices = localStorage.getItem(`addAccountCounter/cosmos:${event.data.chainId}`); - for (let i = 0; i < Number(indices); i++) { - const account = localStorage.getItem(`accounts/cosmos:${event.data.chainId}/${i}`); - if (account) { - accountsData += `${account},`; - } - } - - // Remove trailing comma - accountsData = accountsData.slice(0, -1); - - if (!accountsData) { - event.source?.postMessage({ - type: 'ERROR', - message: 'Wallet accounts not found in local storage', - }); - return; - } - - const elements = accountsData.split(','); - const addresses = elements.filter((_, index) => (index + 1) % 4 === 0); - - (event.source as Window)?.postMessage( - { - type: 'WALLET_ACCOUNTS_DATA', - data: addresses, - }, - 'http://localhost:3000' - ); - - } catch (error) { - (event.source as Window)?.postMessage({ - type: 'ERROR', - message: 'Error accessing wallet accounts data' - }, 'http://localhost:3000'); - } - } -}); - const linking = { prefixes: ["https://wallet.laconic.com"], }; -- 2.45.2 From 42d0c06b504ee149d2edb9278192e70e652b1880 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 7 Nov 2024 16:30:12 +0530 Subject: [PATCH 07/16] Return null in iframe component --- src/components/WalletEmbed.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/WalletEmbed.tsx b/src/components/WalletEmbed.tsx index cfc42f7..bd271b8 100644 --- a/src/components/WalletEmbed.tsx +++ b/src/components/WalletEmbed.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { createWallet } from '../utils/accounts'; import { useNetworks } from '../context/NetworksContext'; @@ -73,5 +73,5 @@ export const WalletEmbed = () => { return () => window.removeEventListener('message', handleCreateAccounts); }, [networksData]); - return

Text

; + return null; }; -- 2.45.2 From 92e7d058b0f2990b2206d5e58a74d5ff891fae1f Mon Sep 17 00:00:00 2001 From: Adw8 Date: Thu, 7 Nov 2024 18:09:45 +0530 Subject: [PATCH 08/16] Use existing method to retrieve accounts --- src/App.tsx | 5 +- src/{components => screens}/WalletEmbed.tsx | 54 +++++++++++++-------- 2 files changed, 37 insertions(+), 22 deletions(-) rename src/{components => screens}/WalletEmbed.tsx (56%) diff --git a/src/App.tsx b/src/App.tsx index d2f255c..b369186 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,7 +34,7 @@ import { NETWORK_METHODS } from "./utils/wallet-connect/common-data"; import { COSMOS_METHODS } from "./utils/wallet-connect/COSMOSData"; import styles from "./styles/stylesheet"; import { Header } from "./components/Header"; -import { WalletEmbed } from "./components/WalletEmbed"; +import { WalletEmbed } from "./screens/WalletEmbed"; const Stack = createStackNavigator(); @@ -317,6 +317,9 @@ const App = (): React.JSX.Element => { <>, + }} /> { const { networksData } = useNetworks(); - const getAccountsData = (chainId: string): string => { - const accountCount = localStorage.getItem(`addAccountCounter/cosmos:${chainId}`); - if (!accountCount) return ''; + const getAccountsData = useCallback(async (chainId: string) => { + const targetNetwork = networksData.find(network => network.chainId === "laconic-testnet-2"); - let accountsData = ''; - for (let i = 0; i < Number(accountCount); i++) { - const account = localStorage.getItem(`accounts/cosmos:${chainId}/${i}`); - if (account) accountsData += `${account},`; + if (!targetNetwork) { + return ''; } - return accountsData.slice(0, -1); // Remove trailing comma - }; + const accounts = await retrieveAccounts(targetNetwork); + + if (!accounts || accounts.length === 0) { + return ''; + } + + const accountsData = accounts.map(account => account.address).join(','); + return accountsData; + }, [networksData]); + const getAddressesFromData = (accountsData: string): string[] => { return accountsData - ? accountsData.split(',').filter((_, index) => (index + 1) % 4 === 0) + ? accountsData.split(',') : []; }; @@ -34,12 +40,12 @@ export const WalletEmbed = () => { }; useEffect(() => { - const handleGetAccounts = (event: MessageEvent) => { + const handleGetAccounts = async (event: MessageEvent) => { if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return; - const accountsData = getAccountsData(event.data.chainId); + const accountsData = await getAccountsData(event.data.chainId); if (!accountsData) { - sendMessage(event.source as Window, 'ERROR', 'Wallet accounts not found in local storage', event.origin); + sendMessage(event.source as Window, 'ERROR', 'Wallet accounts not found', event.origin); return; } @@ -48,21 +54,24 @@ export const WalletEmbed = () => { }; window.addEventListener('message', handleGetAccounts); - return () => window.removeEventListener('message', handleGetAccounts); - }, []); + + return () => { + window.removeEventListener('message', handleGetAccounts); + }; + }, [getAccountsData]); useEffect(() => { const handleCreateAccounts = async (event: MessageEvent) => { if (event.data.type !== 'REQUEST_CREATE_OR_GET_ACCOUNTS') return; - let accountsData = getAccountsData(event.data.chainId); + let accountsData = await getAccountsData(event.data.chainId); if (!accountsData) { console.log("Accounts not found, creating wallet..."); await createWallet(networksData); // Re-fetch newly created accounts - accountsData = getAccountsData(event.data.chainId); + accountsData = await getAccountsData(event.data.chainId); } const addresses = getAddressesFromData(accountsData); @@ -70,8 +79,11 @@ export const WalletEmbed = () => { }; window.addEventListener('message', handleCreateAccounts); - return () => window.removeEventListener('message', handleCreateAccounts); - }, [networksData]); + + return () => { + window.removeEventListener('message', handleCreateAccounts); + }; + }, [networksData, getAccountsData]); return null; }; -- 2.45.2 From f82ff49d77e27a4c306ac5974933dc154f29edfb Mon Sep 17 00:00:00 2001 From: Isha Date: Thu, 7 Nov 2024 19:58:28 +0530 Subject: [PATCH 09/16] Implement functionality to approve tx --- src/screens/WalletEmbed.tsx | 243 +++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 2 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 652eb9c..ee9f150 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -1,9 +1,38 @@ -import { useCallback, useEffect } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; + +import { ScrollView, View } from 'react-native'; +import { + Button, + Text, + TextInput, +} from 'react-native-paper'; + +import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; +import { + calculateFee, + GasPrice, + SigningStargateClient, +} from '@cosmjs/stargate'; import { createWallet, retrieveAccounts } from '../utils/accounts'; +import AccountDetails from '../components/AccountDetails'; +import styles from '../styles/stylesheet'; +import { retrieveSingleAccount } from '../utils/accounts'; +import DataBox from '../components/DataBox'; +import { getPathKey } from '../utils/misc'; import { useNetworks } from '../context/NetworksContext'; +import TxErrorDialog from '../components/TxErrorDialog'; +import { BigNumber } from 'ethers'; +import { MEMO } from '../screens/ApproveTransfer'; export const WalletEmbed = () => { + const [isTxRequested, setIsTxRequested] = useState(false); + const [transactionDetails, setTransactionDetails] = useState(null); + const [fees, setFees] = useState(''); + const [gasLimit, setGasLimit] = useState(''); + const [isTxLoading, setIsTxLoading] = useState(false); + const [txError, setTxError] = useState(null); + const { networksData } = useNetworks(); const getAccountsData = useCallback(async (chainId: string) => { @@ -85,5 +114,215 @@ export const WalletEmbed = () => { }; }, [networksData, getAccountsData]); - return null; + const handleTxRequested = async (event: MessageEvent) => { + if (event.data.type !== 'REQUEST_TX') return; + + const { chainId, fromAddress, toAddress, amount } = event.data; + const network = networksData.find( + net => net.chainId === chainId + ); + + if (!network) { + console.error('Network not found'); + return; + } + + const account = await retrieveSingleAccount(network.namespace, network.chainId, fromAddress); + if (!account) { + throw new Error('Account not found'); + } + + const cosmosPrivKey = ( + await getPathKey(`${network.namespace}:${chainId}`, account.index) + ).privKey; + const sender = await DirectSecp256k1Wallet.fromKey( + Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), + network.addressPrefix + ); + + const client = await SigningStargateClient.connectWithSigner( + network.rpcUrl!, + sender + ); + + const balance = await client?.getBalance( + account.address, + network!.nativeDenom!.toLowerCase(), + ); + + const gasPrice = GasPrice.fromString( + network.gasPrice! + network.nativeDenom, + ); + + const sendMsg = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: fromAddress, + toAddress: toAddress, + amount: [ + { + amount: String(amount), + denom: network.nativeDenom!, + }, + ], + }, + }; + + const gasEstimation = await client.simulate( + fromAddress, + [sendMsg], + MEMO, + ); + + const gasLimit = + String( + Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)), + ) + + const cosmosFees = calculateFee(Number(gasLimit), gasPrice); + setGasLimit(gasLimit); + + setFees(cosmosFees.amount[0].amount); + + setTransactionDetails({ + chainId, + fromAddress, + toAddress, + amount, + account, + balance: balance.amount, + requestedNetwork: network, + }); + + setIsTxRequested(true); + }; + + useEffect(() => { + window.addEventListener('message', handleTxRequested); + return () => window.removeEventListener('message', handleTxRequested); + }, [networksData]); + + const acceptRequestHandler = async () => { + try { + setIsTxLoading(true); + const { chainId, fromAddress, toAddress, amount, requestedNetwork } = + transactionDetails; + + const balanceBigNum = BigNumber.from(transactionDetails.balance); + const amountBigNum = BigNumber.from(String(amount)); + if (amountBigNum.gte(balanceBigNum)) { + throw new Error('Insufficient funds'); + } + + const cosmosPrivKey = ( + await getPathKey(`${requestedNetwork.namespace}:${chainId}`, transactionDetails.account.index) + ).privKey; + + const sender = await DirectSecp256k1Wallet.fromKey( + Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), + requestedNetwork.addressPrefix + ); + + const client = await SigningStargateClient.connectWithSigner( + requestedNetwork.rpcUrl!, + sender + ); + + const fee = calculateFee( + Number(gasLimit), + GasPrice.fromString(`${requestedNetwork.gasPrice}${requestedNetwork.nativeDenom}`) + ); + + const txResult = await client.sendTokens( + fromAddress, + toAddress, + [{ amount: String(amount), denom: requestedNetwork.nativeDenom }], + fee + ); + + sendMessage(window, 'TRANSACTION_SUCCESS', txResult.transactionHash, '*'); + } catch (error) { + console.error('Transaction error:', error); + setTxError('Transaction failed'); + } finally { + setIsTxLoading(false); + } + }; + + const rejectRequestHandler = () => { + setIsTxRequested(false); + setTransactionDetails(null); + sendMessage(window, 'TRANSACTION_REJECTED', null, '*'); + }; + + return ( + <> + {isTxRequested && transactionDetails ? ( + <> + + + From + + + + + + + + + + + /^\d+$/.test(value) ? setGasLimit(value) : null + } + /> + + + + + + + + ) : null} + + setTxError(null)} + /> + + ); }; -- 2.45.2 From 08711b50d6bcdfc62e94791b0451039083983477 Mon Sep 17 00:00:00 2001 From: Isha Date: Fri, 8 Nov 2024 10:01:31 +0530 Subject: [PATCH 10/16] Set event for tx success --- src/screens/WalletEmbed.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index ee9f150..8873e51 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { ScrollView, View } from 'react-native'; import { @@ -32,6 +32,7 @@ export const WalletEmbed = () => { const [gasLimit, setGasLimit] = useState(''); const [isTxLoading, setIsTxLoading] = useState(false); const [txError, setTxError] = useState(null); + const txEventRef = useRef(null); const { networksData } = useNetworks(); @@ -117,6 +118,8 @@ export const WalletEmbed = () => { const handleTxRequested = async (event: MessageEvent) => { if (event.data.type !== 'REQUEST_TX') return; + txEventRef.current = event; + const { chainId, fromAddress, toAddress, amount } = event.data; const network = networksData.find( net => net.chainId === chainId @@ -200,7 +203,7 @@ export const WalletEmbed = () => { useEffect(() => { window.addEventListener('message', handleTxRequested); return () => window.removeEventListener('message', handleTxRequested); - }, [networksData]); + }, [networksData, handleTxRequested]); const acceptRequestHandler = async () => { try { @@ -240,7 +243,12 @@ export const WalletEmbed = () => { fee ); - sendMessage(window, 'TRANSACTION_SUCCESS', txResult.transactionHash, '*'); + const event = txEventRef.current; + if (event?.source) { + sendMessage(event.source as Window, 'TRANSACTION_SUCCESS', txResult.transactionHash, '*'); + } else { + console.error('No event source available to send message'); + } } catch (error) { console.error('Transaction error:', error); setTxError('Transaction failed'); -- 2.45.2 From 6ac15085ffade58bc55e6a4120a9100ded634cc8 Mon Sep 17 00:00:00 2001 From: Isha Date: Fri, 8 Nov 2024 10:34:47 +0530 Subject: [PATCH 11/16] Disable no button after tx is approved --- src/screens/WalletEmbed.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 8873e51..2a8d2f9 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; - import { ScrollView, View } from 'react-native'; import { Button, @@ -153,10 +152,6 @@ export const WalletEmbed = () => { network!.nativeDenom!.toLowerCase(), ); - const gasPrice = GasPrice.fromString( - network.gasPrice! + network.nativeDenom, - ); - const sendMsg = { typeUrl: '/cosmos.bank.v1beta1.MsgSend', value: { @@ -171,20 +166,15 @@ export const WalletEmbed = () => { }, }; - const gasEstimation = await client.simulate( - fromAddress, - [sendMsg], - MEMO, - ); - + const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO); const gasLimit = String( Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)), ) - - const cosmosFees = calculateFee(Number(gasLimit), gasPrice); setGasLimit(gasLimit); - + + const gasPrice = GasPrice.fromString(network.gasPrice! + network.nativeDenom); + const cosmosFees = calculateFee(Number(gasLimit), gasPrice); setFees(cosmosFees.amount[0].amount); setTransactionDetails({ @@ -319,13 +309,13 @@ export const WalletEmbed = () => { mode="contained" onPress={rejectRequestHandler} buttonColor="#B82B0D" + disabled={isTxLoading} > No ) : null} - Date: Fri, 8 Nov 2024 13:12:00 +0530 Subject: [PATCH 12/16] Refactor handleTxRequest method --- src/screens/WalletEmbed.tsx | 181 +++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 87 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 2a8d2f9..8dcf038 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -5,6 +5,7 @@ import { Text, TextInput, } from 'react-native-paper'; +import { BigNumber } from 'ethers'; import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { @@ -13,22 +14,31 @@ import { SigningStargateClient, } from '@cosmjs/stargate'; -import { createWallet, retrieveAccounts } from '../utils/accounts'; +import { createWallet, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts'; import AccountDetails from '../components/AccountDetails'; import styles from '../styles/stylesheet'; -import { retrieveSingleAccount } from '../utils/accounts'; import DataBox from '../components/DataBox'; import { getPathKey } from '../utils/misc'; import { useNetworks } from '../context/NetworksContext'; import TxErrorDialog from '../components/TxErrorDialog'; -import { BigNumber } from 'ethers'; import { MEMO } from '../screens/ApproveTransfer'; +import { Account, NetworksDataState } from '../types'; + +type TransactionDetails = { + chainId: string; + fromAddress: string; + toAddress: string; + amount: string; + account: Account + balance: string; + requestedNetwork: NetworksDataState +}; export const WalletEmbed = () => { const [isTxRequested, setIsTxRequested] = useState(false); - const [transactionDetails, setTransactionDetails] = useState(null); - const [fees, setFees] = useState(''); - const [gasLimit, setGasLimit] = useState(''); + const [transactionDetails, setTransactionDetails] = useState(null); + const [fees, setFees] = useState(''); + const [gasLimit, setGasLimit] = useState(''); const [isTxLoading, setIsTxLoading] = useState(false); const [txError, setTxError] = useState(null); const txEventRef = useRef(null); @@ -36,7 +46,7 @@ export const WalletEmbed = () => { const { networksData } = useNetworks(); const getAccountsData = useCallback(async (chainId: string) => { - const targetNetwork = networksData.find(network => network.chainId === "laconic-testnet-2"); + const targetNetwork = networksData.find(network => network.chainId === chainId); if (!targetNetwork) { return ''; @@ -53,11 +63,8 @@ export const WalletEmbed = () => { }, [networksData]); - const getAddressesFromData = (accountsData: string): string[] => { - return accountsData - ? accountsData.split(',') - : []; - }; + const getAddressesFromData = (accountsData: string): string[] => + accountsData?.split(',') || []; const sendMessage = ( source: Window | null, @@ -114,81 +121,81 @@ export const WalletEmbed = () => { }; }, [networksData, getAccountsData]); - const handleTxRequested = async (event: MessageEvent) => { - if (event.data.type !== 'REQUEST_TX') return; + const handleTxRequested = useCallback( + async (event: MessageEvent) => { + try { + if (event.data.type !== 'REQUEST_TX') return; - txEventRef.current = event; + txEventRef.current = event; - const { chainId, fromAddress, toAddress, amount } = event.data; - const network = networksData.find( - net => net.chainId === chainId - ); + const { chainId, fromAddress, toAddress, amount } = event.data; + const network = networksData.find(net => net.chainId === chainId); - if (!network) { - console.error('Network not found'); - return; - } + if (!network) { + console.error('Network not found'); + throw new Error('Requested network not supported.'); + } - const account = await retrieveSingleAccount(network.namespace, network.chainId, fromAddress); - if (!account) { - throw new Error('Account not found'); - } + const account = await retrieveSingleAccount(network.namespace, network.chainId, fromAddress); + if (!account) { + throw new Error('Account not found for the requested address.'); + } - const cosmosPrivKey = ( - await getPathKey(`${network.namespace}:${chainId}`, account.index) - ).privKey; - const sender = await DirectSecp256k1Wallet.fromKey( - Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), - network.addressPrefix - ); + const cosmosPrivKey = ( + await getPathKey(`${network.namespace}:${chainId}`, account.index) + ).privKey; - const client = await SigningStargateClient.connectWithSigner( - network.rpcUrl!, - sender - ); + const sender = await DirectSecp256k1Wallet.fromKey( + Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), + network.addressPrefix + ); - const balance = await client?.getBalance( - account.address, - network!.nativeDenom!.toLowerCase(), - ); + const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, sender); - const sendMsg = { - typeUrl: '/cosmos.bank.v1beta1.MsgSend', - value: { - fromAddress: fromAddress, - toAddress: toAddress, - amount: [ - { - amount: String(amount), - denom: network.nativeDenom!, + const balance = await client.getBalance( + account.address, + network.nativeDenom!.toLowerCase() + ); + + const sendMsg = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: fromAddress, + toAddress: toAddress, + amount: [ + { + amount: String(amount), + denom: network.nativeDenom!, + }, + ], }, - ], - }, - }; + }; - const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO); - const gasLimit = - String( - Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)), - ) - setGasLimit(gasLimit); - - const gasPrice = GasPrice.fromString(network.gasPrice! + network.nativeDenom); - const cosmosFees = calculateFee(Number(gasLimit), gasPrice); - setFees(cosmosFees.amount[0].amount); + const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO); + const gasLimit = String( + Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)) + ); + setGasLimit(gasLimit); - setTransactionDetails({ - chainId, - fromAddress, - toAddress, - amount, - account, - balance: balance.amount, - requestedNetwork: network, - }); + const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`); + const cosmosFees = calculateFee(Number(gasLimit), gasPrice); + setFees(cosmosFees.amount[0].amount); - setIsTxRequested(true); - }; + setTransactionDetails({ + chainId, + fromAddress, + toAddress, + amount, + account, + balance: balance.amount, + requestedNetwork: network, + }); + + setIsTxRequested(true); + } catch (error) { + console.error('Error processing transaction request:', error); + } + }, [networksData]); useEffect(() => { window.addEventListener('message', handleTxRequested); @@ -198,44 +205,44 @@ export const WalletEmbed = () => { const acceptRequestHandler = async () => { try { setIsTxLoading(true); - const { chainId, fromAddress, toAddress, amount, requestedNetwork } = - transactionDetails; - + if (!transactionDetails) { + throw new Error('Tx details not set'); + } const balanceBigNum = BigNumber.from(transactionDetails.balance); - const amountBigNum = BigNumber.from(String(amount)); + const amountBigNum = BigNumber.from(String(transactionDetails.amount)); if (amountBigNum.gte(balanceBigNum)) { throw new Error('Insufficient funds'); } const cosmosPrivKey = ( - await getPathKey(`${requestedNetwork.namespace}:${chainId}`, transactionDetails.account.index) + await getPathKey(`${transactionDetails.requestedNetwork.namespace}:${transactionDetails.chainId}`, transactionDetails.account.index) ).privKey; const sender = await DirectSecp256k1Wallet.fromKey( Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), - requestedNetwork.addressPrefix + transactionDetails.requestedNetwork.addressPrefix ); const client = await SigningStargateClient.connectWithSigner( - requestedNetwork.rpcUrl!, + transactionDetails.requestedNetwork.rpcUrl!, sender ); const fee = calculateFee( Number(gasLimit), - GasPrice.fromString(`${requestedNetwork.gasPrice}${requestedNetwork.nativeDenom}`) + GasPrice.fromString(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`) ); const txResult = await client.sendTokens( - fromAddress, - toAddress, - [{ amount: String(amount), denom: requestedNetwork.nativeDenom }], + transactionDetails.fromAddress, + transactionDetails.toAddress, + [{ amount: String(transactionDetails.amount), denom: transactionDetails.requestedNetwork.nativeDenom! }], fee ); const event = txEventRef.current; if (event?.source) { - sendMessage(event.source as Window, 'TRANSACTION_SUCCESS', txResult.transactionHash, '*'); + sendMessage(event.source as Window, 'TRANSACTION_SUCCESS', txResult.transactionHash, event.origin); } else { console.error('No event source available to send message'); } -- 2.45.2 From 198ccd98c34d6dd1ad12acda4ecfcd13606f860d Mon Sep 17 00:00:00 2001 From: Isha Date: Fri, 8 Nov 2024 14:33:27 +0530 Subject: [PATCH 13/16] Handle reject tx --- src/screens/WalletEmbed.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 8dcf038..4cbd1b5 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -200,7 +200,7 @@ export const WalletEmbed = () => { useEffect(() => { window.addEventListener('message', handleTxRequested); return () => window.removeEventListener('message', handleTxRequested); - }, [networksData, handleTxRequested]); + }, [handleTxRequested]); const acceptRequestHandler = async () => { try { @@ -242,7 +242,7 @@ export const WalletEmbed = () => { const event = txEventRef.current; if (event?.source) { - sendMessage(event.source as Window, 'TRANSACTION_SUCCESS', 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'); } @@ -255,9 +255,15 @@ export const WalletEmbed = () => { }; const rejectRequestHandler = () => { + const event = txEventRef.current; + setIsTxRequested(false); setTransactionDetails(null); - sendMessage(window, 'TRANSACTION_REJECTED', null, '*'); + if (event?.source) { + sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', null, event.origin); + } else { + console.error('No event source available to send message'); + } }; return ( -- 2.45.2 From cd640e77f803a166fde824691421d492ed0046a8 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Mon, 11 Nov 2024 10:21:34 +0530 Subject: [PATCH 14/16] Display transaction error in wallet embed component --- src/screens/WalletEmbed.tsx | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 4cbd1b5..894d6b0 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; import { ScrollView, View } from 'react-native'; import { + ActivityIndicator, Button, Text, TextInput, @@ -193,7 +194,10 @@ export const WalletEmbed = () => { setIsTxRequested(true); } catch (error) { - console.error('Error processing transaction request:', error); + if (!(error instanceof Error)) { + throw error; + } + setTxError(error.message); } }, [networksData]); @@ -247,8 +251,10 @@ export const WalletEmbed = () => { console.error('No event source available to send message'); } } catch (error) { - console.error('Transaction error:', error); - setTxError('Transaction failed'); + if (!(error instanceof Error)) { + throw error; + } + setTxError(error.message); } finally { setIsTxLoading(false); } @@ -314,7 +320,7 @@ export const WalletEmbed = () => { mode="contained" onPress={acceptRequestHandler} loading={isTxLoading} - disabled={!transactionDetails.balance || !fees} + disabled={!transactionDetails.balance || !fees || isTxLoading} > {isTxLoading ? 'Processing' : 'Yes'} @@ -328,11 +334,21 @@ export const WalletEmbed = () => { - ) : null} + ) : ( + + Loading... + + + )} setTxError(null)} + hideDialog={() => { + setTxError(null) + if (window.parent) { + window.parent.postMessage('closeIframe', '*'); + } + }} /> ); -- 2.45.2 From 65c9014a5d00215d7591df74ce1de005a1d96523 Mon Sep 17 00:00:00 2001 From: Adw8 Date: Mon, 11 Nov 2024 12:11:13 +0530 Subject: [PATCH 15/16] Add check for insufficient funds --- src/screens/WalletEmbed.tsx | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 894d6b0..0ca16d9 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -76,6 +76,13 @@ export const WalletEmbed = () => { source?.postMessage({ type, data }, origin); }; + const checkSufficientFunds = (amount: string, balance: string) => { + const amountBigNum = BigNumber.from(String(amount)); + const balanceBigNum = BigNumber.from(balance); + + return balanceBigNum.gt(amountBigNum); + }; + useEffect(() => { const handleGetAccounts = async (event: MessageEvent) => { if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return; @@ -172,16 +179,6 @@ export const WalletEmbed = () => { }, }; - const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO); - const gasLimit = String( - Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)) - ); - setGasLimit(gasLimit); - - const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`); - const cosmosFees = calculateFee(Number(gasLimit), gasPrice); - setFees(cosmosFees.amount[0].amount); - setTransactionDetails({ chainId, fromAddress, @@ -192,6 +189,21 @@ export const WalletEmbed = () => { requestedNetwork: network, }); + if (!checkSufficientFunds(amount, balance.amount)) { + console.log("Insufficient funds detected. Throwing error."); + throw new Error('Insufficient funds'); + } + + const gasEstimation = await client.simulate(fromAddress, [sendMsg], MEMO); + const gasLimit = String( + Math.round(gasEstimation * Number(process.env.REACT_APP_GAS_ADJUSTMENT)) + ); + setGasLimit(gasLimit); + + const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`); + const cosmosFees = calculateFee(Number(gasLimit), gasPrice); + setFees(cosmosFees.amount[0].amount); + setIsTxRequested(true); } catch (error) { if (!(error instanceof Error)) { @@ -346,7 +358,8 @@ export const WalletEmbed = () => { hideDialog={() => { setTxError(null) if (window.parent) { - window.parent.postMessage('closeIframe', '*'); + sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*'); + sendMessage(window.parent, 'closeIframe', null, '*'); } }} /> -- 2.45.2 From a970e1101419456012b9f136db10918fa928683f Mon Sep 17 00:00:00 2001 From: Adw8 Date: Mon, 11 Nov 2024 12:46:12 +0530 Subject: [PATCH 16/16] Update method to get accounts --- src/screens/WalletEmbed.tsx | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/screens/WalletEmbed.tsx b/src/screens/WalletEmbed.tsx index 0ca16d9..dd0a47f 100644 --- a/src/screens/WalletEmbed.tsx +++ b/src/screens/WalletEmbed.tsx @@ -46,27 +46,22 @@ export const WalletEmbed = () => { const { networksData } = useNetworks(); - const getAccountsData = useCallback(async (chainId: string) => { + const getAccountsData = useCallback(async (chainId: string): Promise => { const targetNetwork = networksData.find(network => network.chainId === chainId); if (!targetNetwork) { - return ''; + return []; } const accounts = await retrieveAccounts(targetNetwork); if (!accounts || accounts.length === 0) { - return ''; + return []; } - const accountsData = accounts.map(account => account.address).join(','); - return accountsData; + return accounts.map(account => account.address); }, [networksData]); - - const getAddressesFromData = (accountsData: string): string[] => - accountsData?.split(',') || []; - const sendMessage = ( source: Window | null, type: string, @@ -88,13 +83,13 @@ export const WalletEmbed = () => { if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return; const accountsData = await getAccountsData(event.data.chainId); - if (!accountsData) { + + if (accountsData.length === 0) { sendMessage(event.source as Window, 'ERROR', 'Wallet accounts not found', event.origin); return; } - const addresses = getAddressesFromData(accountsData); - sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', addresses, event.origin); + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, event.origin); }; window.addEventListener('message', handleGetAccounts); @@ -110,7 +105,7 @@ export const WalletEmbed = () => { let accountsData = await getAccountsData(event.data.chainId); - if (!accountsData) { + if (accountsData.length === 0) { console.log("Accounts not found, creating wallet..."); await createWallet(networksData); @@ -118,8 +113,7 @@ export const WalletEmbed = () => { accountsData = await getAccountsData(event.data.chainId); } - const addresses = getAddressesFromData(accountsData); - sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', addresses, event.origin); + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, event.origin); }; window.addEventListener('message', handleCreateAccounts); @@ -348,7 +342,7 @@ export const WalletEmbed = () => { ) : ( - Loading... + )} -- 2.45.2