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 { ScrollView, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@ -6,268 +6,410 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
|
import { Box } from '@mui/system';
|
||||||
|
|
||||||
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
import { DirectSecp256k1Wallet, Algo } from '@cosmjs/proto-signing';
|
||||||
import {
|
import { SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
|
||||||
calculateFee,
|
import { calculateFee, GasPrice, SigningStargateClient } from '@cosmjs/stargate';
|
||||||
GasPrice,
|
|
||||||
SigningStargateClient,
|
|
||||||
} from '@cosmjs/stargate';
|
|
||||||
|
|
||||||
import { retrieveSingleAccount } from '../utils/accounts';
|
import { retrieveAccounts, retrieveSingleAccount } from '../utils/accounts'; // Use retrieveAccounts
|
||||||
import AccountDetails from '../components/AccountDetails';
|
import AccountDetails from '../components/AccountDetails';
|
||||||
import styles from '../styles/stylesheet';
|
import styles from '../styles/stylesheet';
|
||||||
import DataBox from '../components/DataBox';
|
import DataBox from '../components/DataBox';
|
||||||
import { getPathKey, sendMessage } from '../utils/misc';
|
import { getPathKey, sendMessage } from '../utils/misc';
|
||||||
import { useNetworks } from '../context/NetworksContext';
|
import { useNetworks } from '../context/NetworksContext';
|
||||||
import TxErrorDialog from '../components/TxErrorDialog';
|
import TxErrorDialog from '../components/TxErrorDialog';
|
||||||
import { MEMO } from './ApproveTransfer';
|
|
||||||
import { Account, NetworksDataState } from '../types';
|
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 = {
|
type TransactionDetails = {
|
||||||
|
requestId: string;
|
||||||
|
source: MessageEventSource;
|
||||||
|
origin: string;
|
||||||
signerAddress: string;
|
signerAddress: string;
|
||||||
chainId: string;
|
chainId: string;
|
||||||
account: Account;
|
account: Account; // Wallet's internal Account type
|
||||||
requestedNetwork: NetworksDataState;
|
requestedNetwork: NetworksDataState;
|
||||||
balance: string;
|
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 = () => {
|
export const SendTxEmbed = () => {
|
||||||
const [isTxRequested, setIsTxRequested] = useState<boolean>(false);
|
const [isTxApprovalVisible, setIsTxApprovalVisible] = useState<boolean>(false);
|
||||||
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
|
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
|
||||||
const [fees, setFees] = useState<string>('');
|
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 [isTxLoading, setIsTxLoading] = useState(false);
|
||||||
const [txError, setTxError] = useState<string | null>(null);
|
const [txError, setTxError] = useState<string | null>(null);
|
||||||
const txEventRef = useRef<MessageEvent | null>(null);
|
|
||||||
|
|
||||||
const { networksData } = useNetworks();
|
const { networksData } = useNetworks();
|
||||||
|
|
||||||
const handleTxRequested = useCallback(
|
// --- Message Handlers ---
|
||||||
async (event: MessageEvent) => {
|
|
||||||
|
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 {
|
try {
|
||||||
if (event.data.type !== 'REQUEST_ZENITH_SEND_TX') return;
|
// Use a temporary read-only client for balance check if possible, or the signing client
|
||||||
|
const tempWallet = await DirectSecp256k1Wallet.fromKey(
|
||||||
txEventRef.current = event;
|
new Uint8Array(Buffer.from((await getPathKey(`${network.namespace}:${network.chainId}`, account.index)).privKey.replace(/^0x/, ''), 'hex')), // Wrap in Uint8Array
|
||||||
|
|
||||||
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
|
network.addressPrefix
|
||||||
);
|
);
|
||||||
|
const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, tempWallet);
|
||||||
const client = await SigningStargateClient.connectWithSigner(network.rpcUrl!, sender);
|
const balance = await client.getBalance(account.address, network.nativeDenom!);
|
||||||
|
balanceAmount = balance.amount;
|
||||||
const sendMsg = {
|
client.disconnect();
|
||||||
// TODO: Update with actual type
|
} catch (balanceError) {
|
||||||
typeUrl: '/laconic.onboarding.v1beta1.MsgOnboard',
|
console.warn("Could not retrieve balance:", balanceError);
|
||||||
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]);
|
|
||||||
|
// 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(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('message', handleTxRequested);
|
window.addEventListener('message', handleIncomingMessage);
|
||||||
return () => window.removeEventListener('message', handleTxRequested);
|
console.log("SendTxEmbed: Message listener added.");
|
||||||
}, [handleTxRequested]);
|
return () => {
|
||||||
|
window.removeEventListener('message', handleIncomingMessage);
|
||||||
|
console.log("SendTxEmbed: Message listener removed.");
|
||||||
|
};
|
||||||
|
}, [handleIncomingMessage]);
|
||||||
|
|
||||||
|
// --- UI Action Handlers ---
|
||||||
|
|
||||||
const acceptRequestHandler = async () => {
|
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 {
|
try {
|
||||||
setIsTxLoading(true);
|
const { privKey } = await getPathKey(`${requestedNetwork.namespace}:${chainId}`, account.index);
|
||||||
if (!transactionDetails) {
|
const privateKeyBytes = Buffer.from(privKey.replace(/^0x/, ''), 'hex');
|
||||||
throw new Error('Tx details not set');
|
const wallet = await DirectSecp256k1Wallet.fromKey(new Uint8Array(privateKeyBytes), requestedNetwork.addressPrefix); // Wrap in Uint8Array
|
||||||
}
|
|
||||||
|
|
||||||
const cosmosPrivKey = (
|
// Perform the actual signing
|
||||||
await getPathKey(`${transactionDetails.requestedNetwork.namespace}:${transactionDetails.chainId}`, transactionDetails.account.index)
|
const signResponse = await wallet.signDirect(signerAddress, signDoc);
|
||||||
).privKey;
|
|
||||||
|
|
||||||
const sender = await DirectSecp256k1Wallet.fromKey(
|
sendMessage(source as Window, SIGN_ONBOARD_TX_RESPONSE, {id: requestId, data: signResponse}, origin);
|
||||||
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
|
console.log("Sent signDirect response:", requestId);
|
||||||
transactionDetails.requestedNetwork.addressPrefix
|
|
||||||
);
|
|
||||||
|
|
||||||
const client = await SigningStargateClient.connectWithSigner(
|
setIsTxApprovalVisible(false);
|
||||||
transactionDetails.requestedNetwork.rpcUrl!,
|
setTransactionDetails(null);
|
||||||
sender
|
|
||||||
);
|
|
||||||
|
|
||||||
const fee = calculateFee(
|
} catch (error: unknown) {
|
||||||
Number(gasLimit),
|
console.error("Error during signDirect:", error);
|
||||||
GasPrice.fromString(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`)
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
);
|
setTxError(errorMsg);
|
||||||
|
// Check if source is a Window before sending message
|
||||||
const txResult = await client.signAndBroadcast(transactionDetails.signerAddress, [transactionDetails.attestation], fee);
|
if (source instanceof Window) {
|
||||||
|
sendMessage(source, SIGN_ONBOARD_TX_RESPONSE, {id: requestId, error: `Failed to sign transaction: ${errorMsg}` }, origin);
|
||||||
const event = txEventRef.current;
|
|
||||||
|
|
||||||
if (event?.source) {
|
|
||||||
sendMessage(event.source as Window, 'ZENITH_TRANSACTION_RESPONSE', {txHash: txResult.transactionHash}, event.origin);
|
|
||||||
} else {
|
} 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 {
|
} finally {
|
||||||
setIsTxLoading(false);
|
setIsTxLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectRequestHandler = () => {
|
const rejectRequestHandler = () => {
|
||||||
const event = txEventRef.current;
|
if (!transactionDetails) return;
|
||||||
|
const { requestId, source, origin } = transactionDetails;
|
||||||
setIsTxRequested(false);
|
console.log("Rejecting request:", requestId);
|
||||||
setTransactionDetails(null);
|
// Check if source is a Window before sending message
|
||||||
if (event?.source) {
|
if (source instanceof Window) {
|
||||||
sendMessage(event.source as Window, 'TRANSACTION_RESPONSE', {txHash: null}, event.origin);
|
sendMessage(source, SIGN_ONBOARD_TX_RESPONSE, {id: requestId, error: "User rejected the signature request." }, origin);
|
||||||
} else {
|
} 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isTxRequested && transactionDetails ? (
|
{isTxApprovalVisible && transactionDetails ? (
|
||||||
<>
|
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
<View style={styles.dataBoxContainer}>
|
||||||
<View style={styles.dataBoxContainer}>
|
<Text style={styles.dataBoxLabel}>Sign Request From</Text>
|
||||||
<Text style={styles.dataBoxLabel}>From</Text>
|
<View style={styles.dataBox}>
|
||||||
<View style={styles.dataBox}>
|
<Text>Origin: {transactionDetails.origin}</Text>
|
||||||
<AccountDetails account={transactionDetails.account} />
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<Box
|
</View>
|
||||||
sx={{
|
<View style={styles.dataBoxContainer}>
|
||||||
backgroundColor: "lightgray",
|
<Text style={styles.dataBoxLabel}>Account</Text>
|
||||||
padding: 3,
|
<View style={styles.dataBox}>
|
||||||
borderRadius: 2,
|
<AccountDetails account={transactionDetails.account} />
|
||||||
wordWrap: "break-word",
|
</View>
|
||||||
overflowX: "auto",
|
</View>
|
||||||
mb: 2,
|
<DataBox
|
||||||
}}
|
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
|
||||||
>
|
data={transactionDetails.balance === 'N/A' ? 'Could not load' : transactionDetails.balance}
|
||||||
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
|
/>
|
||||||
{JSON.stringify(JSON.parse(transactionDetails.attestation), null, 2)}
|
<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>
|
</pre>
|
||||||
</Box>
|
</Box>
|
||||||
<DataBox
|
</View>
|
||||||
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
|
<View style={styles.approveTransfer}>
|
||||||
data={
|
<TextInput
|
||||||
transactionDetails.balance === '' ||
|
mode="outlined"
|
||||||
transactionDetails.balance === undefined
|
label="Estimated Fee"
|
||||||
? 'Loading balance...'
|
value={fees}
|
||||||
: `${transactionDetails.balance}`
|
editable={false}
|
||||||
}
|
style={styles.transactionFeesInput}
|
||||||
/>
|
/>
|
||||||
<View style={styles.approveTransfer}>
|
<TextInput
|
||||||
<TextInput
|
mode="outlined"
|
||||||
mode="outlined"
|
label="Gas Limit"
|
||||||
label="Fee"
|
value={gasLimit}
|
||||||
value={fees}
|
onChangeText={setGasLimit}
|
||||||
onChangeText={setFees}
|
keyboardType="numeric"
|
||||||
style={styles.transactionFeesInput}
|
/>
|
||||||
/>
|
</View>
|
||||||
<TextInput
|
|
||||||
mode="outlined"
|
|
||||||
label="Gas Limit"
|
|
||||||
value={gasLimit}
|
|
||||||
onChangeText={value =>
|
|
||||||
/^\d+$/.test(value) ? setGasLimit(value) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<Button
|
<Button mode="contained" onPress={acceptRequestHandler} loading={isTxLoading} disabled={isTxLoading}>
|
||||||
mode="contained"
|
{isTxLoading ? 'Processing...' : 'Approve'}
|
||||||
onPress={acceptRequestHandler}
|
|
||||||
loading={isTxLoading}
|
|
||||||
// disabled={!transactionDetails.balance || !fees || isTxLoading}
|
|
||||||
>
|
|
||||||
{isTxLoading ? 'Processing' : 'Yes'}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button mode="contained" onPress={rejectRequestHandler} buttonColor="#B82B0D" disabled={isTxLoading}>
|
||||||
mode="contained"
|
Reject
|
||||||
onPress={rejectRequestHandler}
|
|
||||||
buttonColor="#B82B0D"
|
|
||||||
disabled={isTxLoading}
|
|
||||||
>
|
|
||||||
No
|
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</ScrollView>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.spinnerContainer}>
|
<View style={styles.spinnerContainer}>
|
||||||
<View style={{ marginTop: 50 }}></View>
|
<Text style={{ marginTop: 50, textAlign: 'center' }}>Waiting for request...</Text>
|
||||||
<ActivityIndicator size="large" color="#0000ff" />
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<TxErrorDialog
|
<TxErrorDialog
|
||||||
error={txError!}
|
error={txError!}
|
||||||
visible={!!txError}
|
visible={!!txError}
|
||||||
hideDialog={() => {
|
hideDialog={() => setTxError(null)}
|
||||||
setTxError(null)
|
|
||||||
if (window.parent) {
|
|
||||||
sendMessage(window.parent, 'TRANSACTION_RESPONSE', null, '*');
|
|
||||||
sendMessage(window.parent, 'closeIframe', null, '*');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -51,6 +51,7 @@ export const WalletEmbed = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleGetAccounts = async (event: MessageEvent) => {
|
const handleGetAccounts = async (event: MessageEvent) => {
|
||||||
|
// TODO: Keep event data types in constant file
|
||||||
if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return;
|
if (event.data.type !== 'REQUEST_WALLET_ACCOUNTS') return;
|
||||||
|
|
||||||
const accountsData = await getAccountsData(event.data.chainId);
|
const accountsData = await getAccountsData(event.data.chainId);
|
||||||
|
Loading…
Reference in New Issue
Block a user