import React, { useEffect, useState, useCallback, useRef } 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 txEventRef = useRef(null); const { networksData } = useNetworks(); const getAccountsData = useCallback(async (chainId: string) => { const targetNetwork = networksData.find(network => network.chainId === "laconic-testnet-2"); 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[] => { return accountsData ? 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 = 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 ); 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 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); }; useEffect(() => { window.addEventListener('message', handleTxRequested); return () => window.removeEventListener('message', handleTxRequested); }, [networksData, handleTxRequested]); 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 ); 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'); } 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)} /> ); };