Display decoded tx body and auth info bytes while signing tx

This commit is contained in:
Shreerang Kale 2025-04-30 17:42:38 +05:30
parent b7cda1da18
commit 5fcdc444d0
4 changed files with 145 additions and 147 deletions

View File

@ -28,6 +28,7 @@
"cosmjs-types": "^0.9.0",
"ethers": "5.7.2",
"https-browserify": "^1.0.0",
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@ -75,6 +76,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
"@types/json-bigint": "^1.0.4",
"@types/lodash": "^4.17.7",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",

View File

@ -1,16 +1,14 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { ScrollView, View } from 'react-native';
import {
ActivityIndicator,
Button,
Text,
TextInput,
} from 'react-native-paper';
import { Box } from '@mui/system';
import JSONbig from 'json-bigint';
import { AuthInfo, SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { DirectSecp256k1Wallet, Algo } from '@cosmjs/proto-signing';
import { SignDoc } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { calculateFee, GasPrice, SigningStargateClient } from '@cosmjs/stargate';
import { DirectSecp256k1Wallet, Algo, TxBodyEncodeObject } from '@cosmjs/proto-signing';
import { SigningStargateClient } from '@cosmjs/stargate';
import { retrieveAccounts, retrieveSingleAccount } from '../utils/accounts'; // Use retrieveAccounts
import AccountDetails from '../components/AccountDetails';
@ -21,14 +19,15 @@ import { useNetworks } from '../context/NetworksContext';
import TxErrorDialog from '../components/TxErrorDialog';
import { Account, NetworksDataState } from '../types';
// --- Type Definitions ---
const GET_ACCOUNTS_RESPONSE = "GET_ACCOUNTS_RESPONSE";
const SIGN_ONBOARD_TX_RESPONSE = "SIGN_ONBOARD_TX_RESPONSE";
// Type Definitions
interface SignOnboardTxRequestData {
address: string;
signDoc: SignDoc;
txBody: TxBodyEncodeObject;
}
interface GetAccountsRequestData {} // Currently no specific data needed
@ -51,6 +50,8 @@ type TransactionDetails = {
requestedNetwork: NetworksDataState;
balance: string;
signDoc: SignDoc; // Deserialized SignDoc
txBody: TxBodyEncodeObject;
// AuthInfo: SignerInfo[];
};
interface GetAccountsResponse {
@ -61,35 +62,15 @@ interface GetAccountsResponse {
}>;
}
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 [isTxApprovalVisible, setIsTxApprovalVisible] = useState<boolean>(false);
const [transactionDetails, setTransactionDetails] = useState<TransactionDetails | null>(null);
const [fees, setFees] = 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 { networksData } = useNetworks();
// --- Message Handlers ---
// Message Handlers
const handleGetAccountsRequest = useCallback(async (event: MessageEvent<IncomingMessageEventData>) => {
const { id } = event.data;
@ -142,7 +123,7 @@ export const SendTxEmbed = () => {
setTxError(null);
try {
const { address: signerAddress, signDoc } = requestData;
const { address: signerAddress, signDoc, txBody } = requestData;
const network = networksData.find(net => net.chainId === signDoc.chainId);
if (!network) throw new Error(`Network with chainId "${signDoc.chainId}" not supported.`);
@ -151,13 +132,14 @@ export const SendTxEmbed = () => {
if (!account) throw new Error(`Account not found for address "${signerAddress}" on chain "${signDoc.chainId}".`);
// Balance Check
let balanceAmount = '0';
let balanceAmount = 'N/A';
try {
// 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!, tempWallet);
const balance = await client.getBalance(account.address, network.nativeDenom!);
balanceAmount = balance.amount;
@ -166,11 +148,6 @@ export const SendTxEmbed = () => {
console.warn("Could not retrieve balance:", balanceError);
}
// 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,
@ -181,6 +158,7 @@ export const SendTxEmbed = () => {
requestedNetwork: network,
balance: balanceAmount,
signDoc,
txBody
});
setIsTxApprovalVisible(true);
@ -196,7 +174,7 @@ export const SendTxEmbed = () => {
}
setTxError(errorMsg);
}
}, [networksData, gasLimit]); // Dependencies: networksData, gasLimit
}, [networksData]); // 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) {
@ -226,7 +204,7 @@ export const SendTxEmbed = () => {
};
}, [handleIncomingMessage]);
// --- UI Action Handlers ---
// Action Handlers
const acceptRequestHandler = async () => {
if (!transactionDetails) {
@ -279,124 +257,98 @@ export const SendTxEmbed = () => {
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 (
<>
{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>
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Account</Text>
<View style={styles.dataBox}>
<AccountDetails account={transactionDetails.account} />
</View>
</View>
</View>
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>Account</Text>
<View style={styles.dataBox}>
<AccountDetails account={transactionDetails.account} />
<DataBox
label={`Balance (${transactionDetails.requestedNetwork.nativeDenom})`}
data={transactionDetails.balance === 'N/A' ? 'Could not load' : transactionDetails.balance}
/>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Transaction Body</Text>
<ScrollView
style={styles.codeContainer}
contentContainerStyle={{ padding: 10 }}
>
<Text style={styles.codeText}>
{JSONbig.stringify(transactionDetails.txBody, null, 2)}
</Text>
</ScrollView>
</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>
</View>
<View style={styles.approveTransfer}>
<TextInput
mode="outlined"
label="Estimated Fee"
value={fees}
editable={false}
style={styles.transactionFeesInput}
/>
<TextInput
mode="outlined"
label="Gas Limit"
value={gasLimit}
onChangeText={setGasLimit}
keyboardType="numeric"
/>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Auth Info</Text>
<ScrollView style={styles.codeContainer} contentContainerStyle={{ padding: 10 }}>
<Text style={styles.codeText}>
{JSONbig.stringify(
{
...AuthInfo.decode(transactionDetails.signDoc.authInfoBytes),
signerInfos: AuthInfo.decode(transactionDetails.signDoc.authInfoBytes).signerInfos.map((info) => ({
...info,
publicKey: info.publicKey
? {
...info.publicKey,
value: info.publicKey.value.toString(),
}
: undefined,
})),
},
null,
2
)}
</Text>
</ScrollView>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Transaction Data To Be Signed</Text>
<ScrollView style={styles.codeContainer} contentContainerStyle={{ padding: 10 }}>
<Text style={styles.codeText}>
{JSONbig.stringify(
{
...transactionDetails.signDoc,
bodyBytes: transactionDetails.signDoc.bodyBytes?.toString?.() ?? transactionDetails.signDoc.bodyBytes,
authInfoBytes: transactionDetails.signDoc.authInfoBytes?.toString?.() ?? transactionDetails.signDoc.authInfoBytes,
},
null,
2
)}
</Text>
</ScrollView>
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<Button mode="contained" onPress={acceptRequestHandler} loading={isTxLoading} disabled={isTxLoading}>
<Button
mode="contained"
onPress={acceptRequestHandler}
loading={isTxLoading}
disabled={isTxLoading}
style={{marginTop: 10}}
>
{isTxLoading ? 'Processing...' : 'Approve'}
</Button>
<Button mode="contained" onPress={rejectRequestHandler} buttonColor="#B82B0D" disabled={isTxLoading}>
Reject
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D"
disabled={isTxLoading}
style={{ marginTop: 10 }}
>
Reject
</Button>
</View>
</ScrollView>
</>
) : (
<View style={styles.spinnerContainer}>
<Text style={{ marginTop: 50, textAlign: 'center' }}>Waiting for request...</Text>

View File

@ -355,6 +355,33 @@ const styles = StyleSheet.create({
marginTop: 12,
marginBottom: 20,
},
section: {
marginBottom: 16,
},
sectionTitle: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
codeContainer: {
backgroundColor: '#e0e0e0',
borderRadius: 6,
maxHeight: 200,
},
codeText: {
fontFamily: 'monospace',
fontSize: 12,
color: '#333',
},
feeContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 10,
marginBottom: 16,
},
input: {
flex: 1,
},
});
export default styles;

View File

@ -3939,6 +3939,11 @@
expect "^29.0.0"
pretty-format "^29.0.0"
"@types/json-bigint@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.4.tgz#250d29e593375499d8ba6efaab22d094c3199ef3"
integrity sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==
"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@ -5375,6 +5380,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bignumber.js@^9.0.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd"
integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
@ -9822,6 +9832,13 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"
integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==
dependencies:
bignumber.js "^9.0.0"
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"