Add iframe for sending zenith txs

This commit is contained in:
Shreerang Kale 2025-04-28 17:18:49 +05:30
parent b2eafe59b3
commit c4d9170777
3 changed files with 283 additions and 0 deletions

View File

@ -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<StackParamsList>();
@ -388,6 +389,13 @@ const App = (): React.JSX.Element => {
header: () => <></>,
}}
/>
<Stack.Screen
name="send-tx-embed"
component={SendTxEmbed}
options={{
header: () => <></>,
}}
/>
<Stack.Screen
name="auto-sign-in"
component={AutoSignIn}

274
src/screens/SendTxEmbed.tsx Normal file
View File

@ -0,0 +1,274 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { ScrollView, View } from 'react-native';
import {
ActivityIndicator,
Button,
Text,
TextInput,
} from 'react-native-paper';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import {
calculateFee,
GasPrice,
SigningStargateClient,
} from '@cosmjs/stargate';
import { retrieveSingleAccount } from '../utils/accounts';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import DataBox from '../components/DataBox';
import { getPathKey, sendMessage } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import TxErrorDialog from '../components/TxErrorDialog';
import { MEMO } from './ApproveTransfer';
import { Account, NetworksDataState } from '../types';
import { Box } from '@mui/system';
type TransactionDetails = {
signerAddress: string;
chainId: string;
account: Account;
requestedNetwork: NetworksDataState;
balance: string;
attestation: any
};
export const SendTxEmbed = () => {
const [isTxRequested, setIsTxRequested] = useState<boolean>(false);
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
const [fees, setFees] = useState<string>('');
const [gasLimit, setGasLimit] = useState<string>('');
const [isTxLoading, setIsTxLoading] = useState(false);
const [txError, setTxError] = useState<string | null>(null);
const txEventRef = useRef<MessageEvent | null>(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 ? (
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>From</Text>
<View style={styles.dataBox}>
<AccountDetails account={transactionDetails.account} />
</View>
</View>
<Box
sx={{
backgroundColor: "lightgray",
padding: 3,
borderRadius: 2,
wordWrap: "break-word",
overflowX: "auto",
mb: 2,
}}
>
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
{JSON.stringify(JSON.parse(transactionDetails.attestation), null, 2)}
</pre>
</Box>
<DataBox
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
data={
transactionDetails.balance === '' ||
transactionDetails.balance === undefined
? 'Loading balance...'
: `${transactionDetails.balance}`
}
/>
<View style={styles.approveTransfer}>
<TextInput
mode="outlined"
label="Fee"
value={fees}
onChangeText={setFees}
style={styles.transactionFeesInput}
/>
<TextInput
mode="outlined"
label="Gas Limit"
value={gasLimit}
onChangeText={value =>
/^\d+$/.test(value) ? setGasLimit(value) : null
}
/>
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={acceptRequestHandler}
loading={isTxLoading}
// disabled={!transactionDetails.balance || !fees || isTxLoading}
>
{isTxLoading ? 'Processing' : 'Yes'}
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D"
disabled={isTxLoading}
>
No
</Button>
</View>
</>
) : (
<View style={styles.spinnerContainer}>
<View style={{ marginTop: 50 }}></View>
<ActivityIndicator size="large" color="#0000ff" />
</View>
)}
<TxErrorDialog
error={txError!}
visible={!!txError}
hideDialog={() => {
setTxError(null)
if (window.parent) {
sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*');
sendMessage(window.parent, 'closeIframe', null, '*');
}
}}
/>
</>
);
};

View File

@ -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 = {