diff --git a/package.json b/package.json index f44f06f..2c2d18a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/screens/SendTxEmbed.tsx b/src/screens/SendTxEmbed.tsx index 00d855c..025f0ed 100644 --- a/src/screens/SendTxEmbed.tsx +++ b/src/screens/SendTxEmbed.tsx @@ -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(false); const [transactionDetails, setTransactionDetails] = useState(null); - const [fees, setFees] = useState(''); - const [gasLimit, setGasLimit] = useState('200000'); // TODO: Revisit gas estimation const [isTxLoading, setIsTxLoading] = useState(false); const [txError, setTxError] = useState(null); const { networksData } = useNetworks(); - // --- Message Handlers --- + // Message Handlers const handleGetAccountsRequest = useCallback(async (event: MessageEvent) => { 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 ? ( - - - Sign Request From - - Origin: {transactionDetails.origin} + <> + + + Account + + + - - - Account - - + + + + + Transaction Body + + + {JSONbig.stringify(transactionDetails.txBody, null, 2)} + + - - - - Transaction Details - -
-                {safeStringify(displaySignDoc, null, 2)}
-              
-
-
- - - - + + + Auth Info + + + {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 + )} + + + + + + Transaction Data To Be Signed + + + {JSONbig.stringify( + { + ...transactionDetails.signDoc, + bodyBytes: transactionDetails.signDoc.bodyBytes?.toString?.() ?? transactionDetails.signDoc.bodyBytes, + authInfoBytes: transactionDetails.signDoc.authInfoBytes?.toString?.() ?? transactionDetails.signDoc.authInfoBytes, + }, + null, + 2 + )} + + + +
+ - - - + ) : ( Waiting for request... diff --git a/src/styles/stylesheet.js b/src/styles/stylesheet.js index a71df18..cb2f368 100644 --- a/src/styles/stylesheet.js +++ b/src/styles/stylesheet.js @@ -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; diff --git a/yarn.lock b/yarn.lock index 054ff5b..9caa949 100644 --- a/yarn.lock +++ b/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"