Implement handlers for signing onboard tx
This commit is contained in:
parent
7b1a6b5666
commit
b85a40817e
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@ -6,268 +6,410 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
} from 'react-native-paper';
|
||||
import { Box } from '@mui/system';
|
||||
|
||||
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
||||
import {
|
||||
calculateFee,
|
||||
GasPrice,
|
||||
SigningStargateClient,
|
||||
} from '@cosmjs/stargate';
|
||||
import { DirectSecp256k1Wallet, Algo } from '@cosmjs/proto-signing';
|
||||
import { SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
|
||||
import { calculateFee, GasPrice, SigningStargateClient } from '@cosmjs/stargate';
|
||||
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
import { retrieveAccounts, retrieveSingleAccount } from '../utils/accounts'; // Use retrieveAccounts
|
||||
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 Definitions ---
|
||||
|
||||
const GET_ACCOUNTS_RESPONSE = "GET_ACCOUNTS_RESPONSE";
|
||||
const SIGN_ONBOARD_TX_RESPONSE = "SIGN_ONBOARD_TX_RESPONSE";
|
||||
|
||||
interface SignOnboardTxRequestData {
|
||||
address: string;
|
||||
signDoc: SignDoc;
|
||||
}
|
||||
|
||||
interface GetAccountsRequestData {} // Currently no specific data needed
|
||||
|
||||
type IncomingMessageData = SignOnboardTxRequestData | GetAccountsRequestData;
|
||||
|
||||
interface IncomingMessageEventData {
|
||||
id: string;
|
||||
type: 'SIGN_ONBOARD_TX_REQUEST' | 'GET_ACCOUNTS_REQUEST';
|
||||
data: IncomingMessageData;
|
||||
}
|
||||
|
||||
type TransactionDetails = {
|
||||
requestId: string;
|
||||
source: MessageEventSource;
|
||||
origin: string;
|
||||
signerAddress: string;
|
||||
chainId: string;
|
||||
account: Account;
|
||||
account: Account; // Wallet's internal Account type
|
||||
requestedNetwork: NetworksDataState;
|
||||
balance: string;
|
||||
attestation: any
|
||||
signDoc: SignDoc; // Deserialized SignDoc
|
||||
};
|
||||
|
||||
interface GetAccountsResponse {
|
||||
accounts: Array<{
|
||||
algo: Algo;
|
||||
address: string;
|
||||
pubkey: string; // hex encoded pubkey
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SignDirectResponseData {
|
||||
signed: {
|
||||
bodyBytes: string; // base64
|
||||
authInfoBytes: string; // base64
|
||||
chainId: string;
|
||||
accountNumber: string; // string representation of BigInt
|
||||
};
|
||||
signature: {
|
||||
pub_key: {
|
||||
type: string; // e.g., "tendermint/PubKeySecp256k1"
|
||||
value: string; // base64 encoded pubkey value
|
||||
};
|
||||
signature: string; // base64 encoded signature
|
||||
};
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export const SendTxEmbed = () => {
|
||||
const [isTxRequested, setIsTxRequested] = useState<boolean>(false);
|
||||
const [isTxApprovalVisible, setIsTxApprovalVisible] = useState<boolean>(false);
|
||||
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
|
||||
const [fees, setFees] = useState<string>('');
|
||||
const [gasLimit, setGasLimit] = useState<string>('');
|
||||
const [gasLimit, setGasLimit] = useState<string>('200000'); // TODO: Revisit gas estimation
|
||||
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) => {
|
||||
// --- Message Handlers ---
|
||||
|
||||
const handleGetAccountsRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
|
||||
const { id } = event.data;
|
||||
const source = event.source as Window;
|
||||
const origin = event.origin;
|
||||
|
||||
console.log("Received GET_ACCOUNTS_REQUEST", id);
|
||||
try {
|
||||
const zenithNetworkData = networksData.find(networkData => networkData.chainId === "zenith-testnet")
|
||||
|
||||
if(!zenithNetworkData) {
|
||||
throw new Error("Zenith network data not found")
|
||||
}
|
||||
// Ensure retrieveAccounts exists and returns Account[]
|
||||
const allAccounts = await retrieveAccounts(zenithNetworkData); // Use retrieveAccounts
|
||||
|
||||
if (!allAccounts || allAccounts.length === 0) {
|
||||
throw new Error("Accounts not found for zenithNetwork")
|
||||
}
|
||||
|
||||
const responseAccounts = allAccounts.map((acc) => ({
|
||||
algo: 'secp256k1' as Algo, // Assuming secp256k1
|
||||
address: acc.address,
|
||||
pubkey: acc.pubKey.startsWith('0x') ? acc.pubKey : `0x${acc.pubKey}`, // Ensure hex format
|
||||
}));
|
||||
|
||||
const response: GetAccountsResponse = { accounts: responseAccounts };
|
||||
sendMessage(source, GET_ACCOUNTS_RESPONSE, {id, data: response}, origin);
|
||||
} catch (error: unknown) {
|
||||
console.error("Error handling GET_ACCOUNTS_REQUEST:", error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
// Check if source is a Window before sending message
|
||||
if (source instanceof Window) {
|
||||
sendMessage(source, GET_ACCOUNTS_RESPONSE, { id, error: `Failed to get accounts: ${errorMsg}` }, origin);
|
||||
} else {
|
||||
console.error("Cannot send error message: source is not a Window");
|
||||
}
|
||||
}
|
||||
}, [networksData]); // Add dependencies like retrieveAccounts if needed
|
||||
|
||||
const handleSignOnboardTxRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
|
||||
const { id, data } = event.data;
|
||||
const source = event.source as Window;
|
||||
const origin = event.origin;
|
||||
const requestData = data as SignOnboardTxRequestData;
|
||||
|
||||
console.log("Received SIGN_ONBOARD_TX_REQUEST", id);
|
||||
setIsTxApprovalVisible(false); // Hide previous request first
|
||||
setTransactionDetails(null);
|
||||
setTxError(null);
|
||||
|
||||
try {
|
||||
const { address: signerAddress, signDoc } = requestData;
|
||||
|
||||
const network = networksData.find(net => net.chainId === signDoc.chainId);
|
||||
if (!network) throw new Error(`Network with chainId "${signDoc.chainId}" not supported.`);
|
||||
|
||||
const account = await retrieveSingleAccount(network.namespace, network.chainId, signerAddress);
|
||||
if (!account) throw new Error(`Account not found for address "${signerAddress}" on chain "${signDoc.chainId}".`);
|
||||
|
||||
// Balance Check
|
||||
let balanceAmount = '0';
|
||||
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'),
|
||||
// Use a temporary read-only client for balance check if possible, or the signing client
|
||||
const tempWallet = await DirectSecp256k1Wallet.fromKey(
|
||||
new Uint8Array(Buffer.from((await getPathKey(`${network.namespace}:${network.chainId}`, account.index)).privKey.replace(/^0x/, ''), 'hex')), // Wrap in Uint8Array
|
||||
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);
|
||||
const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, tempWallet);
|
||||
const balance = await client.getBalance(account.address, network.nativeDenom!);
|
||||
balanceAmount = balance.amount;
|
||||
client.disconnect();
|
||||
} catch (balanceError) {
|
||||
console.warn("Could not retrieve balance:", balanceError);
|
||||
}
|
||||
}, [networksData]);
|
||||
|
||||
// Fee Calculation
|
||||
const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`);
|
||||
const calculatedFee = calculateFee(Number(gasLimit), gasPrice);
|
||||
setFees(calculatedFee.amount[0].amount);
|
||||
|
||||
setTransactionDetails({
|
||||
requestId: id,
|
||||
source: source,
|
||||
origin: origin,
|
||||
signerAddress,
|
||||
chainId: signDoc.chainId,
|
||||
account,
|
||||
requestedNetwork: network,
|
||||
balance: balanceAmount,
|
||||
signDoc,
|
||||
});
|
||||
|
||||
setIsTxApprovalVisible(true);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("Error handling SIGN_ONBOARD_TX_REQUEST:", error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
// Check if source is a Window before sending message
|
||||
if (source instanceof Window) {
|
||||
sendMessage(source, id, { error: `Failed to prepare transaction: ${errorMsg}` }, origin);
|
||||
} else {
|
||||
console.error("Cannot send error message: source is not a Window");
|
||||
}
|
||||
setTxError(errorMsg);
|
||||
}
|
||||
}, [networksData, gasLimit]); // Dependencies: networksData, gasLimit
|
||||
|
||||
const handleIncomingMessage = useCallback((event: MessageEvent) => {
|
||||
if (!event.data || typeof event.data !== 'object' || !event.data.type || !event.data.id || !event.source || event.source === window) {
|
||||
return; // Basic validation
|
||||
}
|
||||
|
||||
const messageData = event.data as IncomingMessageEventData;
|
||||
|
||||
switch (messageData.type) {
|
||||
case 'GET_ACCOUNTS_REQUEST':
|
||||
handleGetAccountsRequest(event as MessageEvent<IncomingMessageEventData>);
|
||||
break;
|
||||
case 'SIGN_ONBOARD_TX_REQUEST':
|
||||
handleSignOnboardTxRequest(event as MessageEvent<IncomingMessageEventData>);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Received unknown message type: ${messageData.type}`);
|
||||
}
|
||||
}, [handleGetAccountsRequest, handleSignOnboardTxRequest]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleTxRequested);
|
||||
return () => window.removeEventListener('message', handleTxRequested);
|
||||
}, [handleTxRequested]);
|
||||
window.addEventListener('message', handleIncomingMessage);
|
||||
console.log("SendTxEmbed: Message listener added.");
|
||||
return () => {
|
||||
window.removeEventListener('message', handleIncomingMessage);
|
||||
console.log("SendTxEmbed: Message listener removed.");
|
||||
};
|
||||
}, [handleIncomingMessage]);
|
||||
|
||||
// --- UI Action Handlers ---
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
if (!transactionDetails) {
|
||||
setTxError("Transaction details are missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTxLoading(true);
|
||||
setTxError(null);
|
||||
|
||||
const { requestId, source, origin, requestedNetwork, chainId, account, signerAddress, signDoc } = transactionDetails;
|
||||
|
||||
try {
|
||||
setIsTxLoading(true);
|
||||
if (!transactionDetails) {
|
||||
throw new Error('Tx details not set');
|
||||
}
|
||||
const { privKey } = await getPathKey(`${requestedNetwork.namespace}:${chainId}`, account.index);
|
||||
const privateKeyBytes = Buffer.from(privKey.replace(/^0x/, ''), 'hex');
|
||||
const wallet = await DirectSecp256k1Wallet.fromKey(new Uint8Array(privateKeyBytes), requestedNetwork.addressPrefix); // Wrap in Uint8Array
|
||||
|
||||
const cosmosPrivKey = (
|
||||
await getPathKey(`${transactionDetails.requestedNetwork.namespace}:${transactionDetails.chainId}`, transactionDetails.account.index)
|
||||
).privKey;
|
||||
// Perform the actual signing
|
||||
const signResponse = await wallet.signDirect(signerAddress, signDoc);
|
||||
|
||||
const sender = await DirectSecp256k1Wallet.fromKey(
|
||||
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
|
||||
transactionDetails.requestedNetwork.addressPrefix
|
||||
);
|
||||
sendMessage(source as Window, SIGN_ONBOARD_TX_RESPONSE, {id: requestId, data: signResponse}, origin);
|
||||
console.log("Sent signDirect response:", requestId);
|
||||
|
||||
const client = await SigningStargateClient.connectWithSigner(
|
||||
transactionDetails.requestedNetwork.rpcUrl!,
|
||||
sender
|
||||
);
|
||||
setIsTxApprovalVisible(false);
|
||||
setTransactionDetails(null);
|
||||
|
||||
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);
|
||||
} catch (error: unknown) {
|
||||
console.error("Error during signDirect:", error);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
setTxError(errorMsg);
|
||||
// Check if source is a Window before sending message
|
||||
if (source instanceof Window) {
|
||||
sendMessage(source, SIGN_ONBOARD_TX_RESPONSE, {id: requestId, error: `Failed to sign transaction: ${errorMsg}` }, origin);
|
||||
} else {
|
||||
console.error('No event source available to send message');
|
||||
console.error("Cannot send error message: source is not a Window");
|
||||
}
|
||||
} 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);
|
||||
if (!transactionDetails) return;
|
||||
const { requestId, source, origin } = transactionDetails;
|
||||
console.log("Rejecting request:", requestId);
|
||||
// Check if source is a Window before sending message
|
||||
if (source instanceof Window) {
|
||||
sendMessage(source, SIGN_ONBOARD_TX_RESPONSE, {id: requestId, error: "User rejected the signature request." }, origin);
|
||||
} else {
|
||||
console.error('No event source available to send message');
|
||||
console.error("Cannot send rejection message: source is not a Window");
|
||||
}
|
||||
setIsTxApprovalVisible(false);
|
||||
setTransactionDetails(null);
|
||||
setTxError(null);
|
||||
};
|
||||
|
||||
// --- Display Logic ---
|
||||
|
||||
const safeStringify = useCallback((obj: any, replacer: any = null, space: number = 2) => {
|
||||
return JSON.stringify(
|
||||
obj,
|
||||
(key, value) => {
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
return replacer ? replacer(key, value) : value;
|
||||
},
|
||||
space
|
||||
);
|
||||
}, [])
|
||||
|
||||
const decodeUint8Arrays = useCallback((obj: any): any => {
|
||||
if (obj instanceof Uint8Array) {
|
||||
try {
|
||||
return new TextDecoder().decode(obj);
|
||||
} catch (e) {
|
||||
return obj; // fallback if decoding fails
|
||||
}
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(decodeUint8Arrays);
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const newObj: any = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
newObj[key] = decodeUint8Arrays(value);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return obj;
|
||||
}, [])
|
||||
|
||||
const displaySignDoc = useMemo(() => {
|
||||
if (!transactionDetails?.signDoc) return null;
|
||||
|
||||
try {
|
||||
const signDocCopy = typeof structuredClone === 'function'
|
||||
? structuredClone(transactionDetails.signDoc)
|
||||
: JSON.parse(safeStringify(transactionDetails.signDoc));
|
||||
|
||||
// Attempt to parse attestation
|
||||
if (
|
||||
signDocCopy.msgs &&
|
||||
signDocCopy.msgs[0]?.value?.attestation &&
|
||||
typeof signDocCopy.msgs[0].value.attestation === 'string'
|
||||
) {
|
||||
try {
|
||||
signDocCopy.msgs[0].value.attestation = JSON.parse(signDocCopy.msgs[0].value.attestation);
|
||||
} catch (e) {
|
||||
console.warn('Could not parse attestation string:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const decoded = decodeUint8Arrays(signDocCopy);
|
||||
return decoded;
|
||||
} catch (e) {
|
||||
console.error('Error processing SignDoc:', e);
|
||||
return transactionDetails.signDoc;
|
||||
}
|
||||
}, [transactionDetails?.signDoc, decodeUint8Arrays, safeStringify]);
|
||||
|
||||
// --- Render ---
|
||||
|
||||
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>
|
||||
{isTxApprovalVisible && transactionDetails ? (
|
||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>Sign Request From</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<Text>Origin: {transactionDetails.origin}</Text>
|
||||
</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)}
|
||||
</View>
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>Account</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<AccountDetails account={transactionDetails.account} />
|
||||
</View>
|
||||
</View>
|
||||
<DataBox
|
||||
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
|
||||
data={transactionDetails.balance === 'N/A' ? 'Could not load' : transactionDetails.balance}
|
||||
/>
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>Transaction Details</Text>
|
||||
<Box sx={{ backgroundColor: "lightgray", padding: 1, borderRadius: 1, wordWrap: "break-word", overflowX: "auto", mb: 2 }}>
|
||||
<pre style={{ whiteSpace: "pre-wrap", margin: 0, fontSize: '0.8em' }}>
|
||||
{safeStringify(displaySignDoc, null, 2)}
|
||||
</pre>
|
||||
</Box>
|
||||
<DataBox
|
||||
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
|
||||
data={
|
||||
transactionDetails.balance === '' ||
|
||||
transactionDetails.balance === undefined
|
||||
? 'Loading balance...'
|
||||
: `${transactionDetails.balance}`
|
||||
}
|
||||
</View>
|
||||
<View style={styles.approveTransfer}>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Estimated Fee"
|
||||
value={fees}
|
||||
editable={false}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<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>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Gas Limit"
|
||||
value={gasLimit}
|
||||
onChangeText={setGasLimit}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isTxLoading}
|
||||
// disabled={!transactionDetails.balance || !fees || isTxLoading}
|
||||
>
|
||||
{isTxLoading ? 'Processing' : 'Yes'}
|
||||
<Button mode="contained" onPress={acceptRequestHandler} loading={isTxLoading} disabled={isTxLoading}>
|
||||
{isTxLoading ? 'Processing...' : 'Approve'}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D"
|
||||
disabled={isTxLoading}
|
||||
>
|
||||
No
|
||||
<Button mode="contained" onPress={rejectRequestHandler} buttonColor="#B82B0D" disabled={isTxLoading}>
|
||||
Reject
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<View style={{ marginTop: 50 }}></View>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
<Text style={{ marginTop: 50, textAlign: 'center' }}>Waiting for request...</Text>
|
||||
</View>
|
||||
)}
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={!!txError}
|
||||
hideDialog={() => {
|
||||
setTxError(null)
|
||||
if (window.parent) {
|
||||
sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*');
|
||||
sendMessage(window.parent, 'closeIframe', null, '*');
|
||||
}
|
||||
}}
|
||||
hideDialog={() => setTxError(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -51,6 +51,7 @@ export const WalletEmbed = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleGetAccounts = async (event: MessageEvent) => {
|
||||
// TODO: Keep event data types in constant file
|
||||
if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return;
|
||||
|
||||
const accountsData = await getAccountsData(event.data.chainId);
|
||||
|
Loading…
Reference in New Issue
Block a user