diff --git a/src/App.tsx b/src/App.tsx index 1ed1a09..56845fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 { SendTxEmbed } from "./screens/SendTxEmbed"; const Stack = createStackNavigator(); @@ -388,6 +389,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 handleTxRequested = useCallback( + async (event: MessageEvent) => { + try { + if (event.data.type !== 'REQUEST_ZENITH_SEND_TX') return; + + txEventRef.current = event; + + const { chainId, signerAddress, attestation } = 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, signerAddress); + 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 sendMsg = { + // TODO: Update with actual type + typeUrl: '/laconic.onboarding.v1beta1.MsgOnboard', + value: { + attestation + }, + }; + + // TODO: Check funds for the tx + const balance = await client.getBalance( + account.address, + network.nativeDenom!.toLowerCase() + ); + + setTransactionDetails({ + signerAddress, + chainId, + account, + requestedNetwork: network, + balance: balance.amount, + attestation, + }); + + const gasEstimation = await client.simulate(signerAddress, [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 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.signAndBroadcast(transactionDetails.signerAddress, [transactionDetails.attestation], fee); + + const event = txEventRef.current; + + if (event?.source) { + sendMessage(event.source as Window, 'ZENITH_TRANSACTION_RESPONSE', {txHash: 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', {txHash: null}, event.origin); + } else { + console.error('No event source available to send message'); + } + }; + + return ( + <> + {isTxRequested && transactionDetails ? ( + <> + + + From + + + + + +
+                {JSON.stringify(JSON.parse(transactionDetails.attestation), null, 2)}
+              
+
+ + + + + /^\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 31d4c3f..6d34515 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export type StackParamsList = { "wallet-embed": undefined; "auto-sign-in": undefined; "sign-request-embed": undefined; + "send-tx-embed": undefined; }; export type Account = {