Display decoded tx body and auth info bytes while signing tx
This commit is contained in:
parent
b7cda1da18
commit
5fcdc444d0
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user