import React, { useEffect, useState, useCallback, useRef } from 'react'; import { ScrollView, View } from 'react-native'; import { ActivityIndicator, Button, Text, TextInput, } from 'react-native-paper'; import { BigNumber } from 'ethers'; import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { calculateFee, GasPrice, SigningStargateClient, } from '@cosmjs/stargate'; import { createWallet, retrieveAccounts, retrieveSingleAccount } from '../utils/accounts'; import AccountDetails from '../components/AccountDetails'; import styles from '../styles/stylesheet'; import DataBox from '../components/DataBox'; import { getPathKey } from '../utils/misc'; import { useNetworks } from '../context/NetworksContext'; import TxErrorDialog from '../components/TxErrorDialog'; 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 [isTxLoading, setIsTxLoading] = useState(false); const [txError, setTxError] = useState(null); const txEventRef = useRef(null); const { networksData } = useNetworks(); const getAccountsData = useCallback(async (chainId: string) => { const targetNetwork = networksData.find(network => network.chainId === chainId); if (!targetNetwork) { return ''; } 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[] => accountsData?.split(',') || []; const sendMessage = ( source: Window | null, type: string, data: any, origin: string ): void => { source?.postMessage({ type, data }, origin); }; useEffect(() => { const handleGetAccounts = async (event: MessageEvent) => { if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return; const accountsData = await getAccountsData(event.data.chainId); if (!accountsData) { 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); }; window.addEventListener('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 = await getAccountsData(event.data.chainId); if (!accountsData) { console.log("Accounts not found, creating wallet..."); await createWallet(networksData); // Re-fetch newly created accounts accountsData = await 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, getAccountsData]); const handleTxRequested = useCallback( async (event: MessageEvent) => { try { 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); 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 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 client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, sender); 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); setTransactionDetails({ chainId, fromAddress, toAddress, amount, account, balance: balance.amount, requestedNetwork: network, }); setIsTxRequested(true); } catch (error) { if (!(error instanceof Error)) { throw error; } setTxError(error.message); } }, [networksData]); useEffect(() => { window.addEventListener('message', handleTxRequested); return () => window.removeEventListener('message', handleTxRequested); }, [handleTxRequested]); const acceptRequestHandler = async () => { try { setIsTxLoading(true); if (!transactionDetails) { throw new Error('Tx details not set'); } const balanceBigNum = BigNumber.from(transactionDetails.balance); const amountBigNum = BigNumber.from(String(transactionDetails.amount)); if (amountBigNum.gte(balanceBigNum)) { throw new Error('Insufficient funds'); } const cosmosPrivKey = ( await getPathKey(`${transactionDetails.requestedNetwork.namespace}:${transactionDetails.chainId}`, transactionDetails.account.index) ).privKey; const sender = await DirectSecp256k1Wallet.fromKey( Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'), transactionDetails.requestedNetwork.addressPrefix ); const client = await SigningStargateClient.connectWithSigner( transactionDetails.requestedNetwork.rpcUrl!, sender ); const fee = calculateFee( Number(gasLimit), GasPrice.fromString(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`) ); const txResult = await client.sendTokens( 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_RESPONSE', txResult.transactionHash, event.origin); } else { console.error('No event source available to send message'); } } catch (error) { if (!(error instanceof Error)) { throw error; } setTxError(error.message); } finally { setIsTxLoading(false); } }; const rejectRequestHandler = () => { const event = txEventRef.current; setIsTxRequested(false); setTransactionDetails(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 ( <> {isTxRequested && transactionDetails ? ( <> From /^\d+$/.test(value) ? setGasLimit(value) : null } /> ) : ( Loading... )} { setTxError(null) if (window.parent) { window.parent.postMessage('closeIframe', '*'); } }} /> ); };