diff --git a/src/App.tsx b/src/App.tsx index 49881f8..b369186 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 "./screens/WalletEmbed"; const Stack = createStackNavigator(); @@ -313,6 +314,13 @@ const App = (): React.JSX.Element => { header: () =>
, }} /> + <>, + }} + /> { + 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): Promise => { + const targetNetwork = networksData.find(network => network.chainId === chainId); + + if (!targetNetwork) { + return []; + } + + const accounts = await retrieveAccounts(targetNetwork); + + if (!accounts || accounts.length === 0) { + return []; + } + + return accounts.map(account => account.address); + }, [networksData]); + + const sendMessage = ( + source: Window | null, + type: string, + data: any, + origin: string + ): void => { + 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; + + const accountsData = await getAccountsData(event.data.chainId); + + if (accountsData.length === 0) { + sendMessage(event.source as Window, 'ERROR', 'Wallet accounts not found', event.origin); + return; + } + + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, 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.length === 0) { + console.log("Accounts not found, creating wallet..."); + await createWallet(networksData); + + // Re-fetch newly created accounts + accountsData = await getAccountsData(event.data.chainId); + } + + sendMessage(event.source as Window, 'WALLET_ACCOUNTS_DATA', accountsData, 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!, + }, + ], + }, + }; + + setTransactionDetails({ + chainId, + fromAddress, + toAddress, + amount, + account, + balance: balance.amount, + 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)) { + 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 + } + /> + + + + + + + + ) : ( + + + + + )} + { + setTxError(null) + if (window.parent) { + sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*'); + sendMessage(window.parent, 'closeIframe', null, '*'); + } + }} + /> + + ); +}; 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 = {