Recovery.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. import {
  2. ChainId,
  3. CHAIN_ID_SOLANA,
  4. CHAIN_ID_TERRA,
  5. getEmitterAddressEth,
  6. getEmitterAddressSolana,
  7. getEmitterAddressTerra,
  8. hexToNativeString,
  9. hexToUint8Array,
  10. isEVMChain,
  11. parseNFTPayload,
  12. parseSequenceFromLogEth,
  13. parseSequenceFromLogSolana,
  14. parseSequenceFromLogTerra,
  15. parseTransferPayload,
  16. uint8ArrayToHex,
  17. } from "@certusone/wormhole-sdk";
  18. import {
  19. Accordion,
  20. AccordionDetails,
  21. AccordionSummary,
  22. Box,
  23. Card,
  24. CircularProgress,
  25. Container,
  26. Divider,
  27. makeStyles,
  28. MenuItem,
  29. TextField,
  30. Typography,
  31. } from "@material-ui/core";
  32. import { ExpandMore } from "@material-ui/icons";
  33. import { Alert } from "@material-ui/lab";
  34. import { Connection } from "@solana/web3.js";
  35. import { LCDClient } from "@terra-money/terra.js";
  36. import axios from "axios";
  37. import { ethers } from "ethers";
  38. import { useSnackbar } from "notistack";
  39. import { useCallback, useEffect, useMemo, useState } from "react";
  40. import { useDispatch } from "react-redux";
  41. import { useHistory, useLocation } from "react-router";
  42. import { useEthereumProvider } from "../contexts/EthereumProviderContext";
  43. import useIsWalletReady from "../hooks/useIsWalletReady";
  44. import useRelayersAvailable, { Relayer } from "../hooks/useRelayersAvailable";
  45. import { COLORS } from "../muiTheme";
  46. import { setRecoveryVaa as setRecoveryNFTVaa } from "../store/nftSlice";
  47. import { setRecoveryVaa } from "../store/transferSlice";
  48. import {
  49. CHAINS,
  50. CHAINS_BY_ID,
  51. CHAINS_WITH_NFT_SUPPORT,
  52. getBridgeAddressForChain,
  53. getNFTBridgeAddressForChain,
  54. getTokenBridgeAddressForChain,
  55. RELAY_URL_EXTENSION,
  56. SOLANA_HOST,
  57. SOL_NFT_BRIDGE_ADDRESS,
  58. SOL_TOKEN_BRIDGE_ADDRESS,
  59. TERRA_HOST,
  60. TERRA_TOKEN_BRIDGE_ADDRESS,
  61. WORMHOLE_RPC_HOSTS,
  62. } from "../utils/consts";
  63. import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
  64. import parseError from "../utils/parseError";
  65. import ButtonWithLoader from "./ButtonWithLoader";
  66. import ChainSelect from "./ChainSelect";
  67. import KeyAndBalance from "./KeyAndBalance";
  68. import RelaySelector from "./RelaySelector";
  69. const useStyles = makeStyles((theme) => ({
  70. mainCard: {
  71. padding: "32px 32px 16px",
  72. backgroundColor: COLORS.whiteWithTransparency,
  73. },
  74. advancedContainer: {
  75. padding: theme.spacing(2, 0),
  76. },
  77. relayAlert: {
  78. marginTop: theme.spacing(2),
  79. marginBottom: theme.spacing(2),
  80. "& > .MuiAlert-message": {
  81. width: "100%",
  82. },
  83. },
  84. }));
  85. async function evm(
  86. provider: ethers.providers.Web3Provider,
  87. tx: string,
  88. enqueueSnackbar: any,
  89. chainId: ChainId,
  90. nft: boolean
  91. ) {
  92. try {
  93. const receipt = await provider.getTransactionReceipt(tx);
  94. const sequence = parseSequenceFromLogEth(
  95. receipt,
  96. getBridgeAddressForChain(chainId)
  97. );
  98. const emitterAddress = getEmitterAddressEth(
  99. nft
  100. ? getNFTBridgeAddressForChain(chainId)
  101. : getTokenBridgeAddressForChain(chainId)
  102. );
  103. const { vaaBytes } = await getSignedVAAWithRetry(
  104. chainId,
  105. emitterAddress,
  106. sequence.toString(),
  107. WORMHOLE_RPC_HOSTS.length
  108. );
  109. return { vaa: uint8ArrayToHex(vaaBytes), error: null };
  110. } catch (e) {
  111. console.error(e);
  112. enqueueSnackbar(null, {
  113. content: <Alert severity="error">{parseError(e)}</Alert>,
  114. });
  115. return { vaa: null, error: parseError(e) };
  116. }
  117. }
  118. async function solana(tx: string, enqueueSnackbar: any, nft: boolean) {
  119. try {
  120. const connection = new Connection(SOLANA_HOST, "confirmed");
  121. const info = await connection.getTransaction(tx);
  122. if (!info) {
  123. throw new Error("An error occurred while fetching the transaction info");
  124. }
  125. const sequence = parseSequenceFromLogSolana(info);
  126. const emitterAddress = await getEmitterAddressSolana(
  127. nft ? SOL_NFT_BRIDGE_ADDRESS : SOL_TOKEN_BRIDGE_ADDRESS
  128. );
  129. const { vaaBytes } = await getSignedVAAWithRetry(
  130. CHAIN_ID_SOLANA,
  131. emitterAddress,
  132. sequence.toString(),
  133. WORMHOLE_RPC_HOSTS.length
  134. );
  135. return { vaa: uint8ArrayToHex(vaaBytes), error: null };
  136. } catch (e) {
  137. console.error(e);
  138. enqueueSnackbar(null, {
  139. content: <Alert severity="error">{parseError(e)}</Alert>,
  140. });
  141. return { vaa: null, error: parseError(e) };
  142. }
  143. }
  144. async function terra(tx: string, enqueueSnackbar: any) {
  145. try {
  146. const lcd = new LCDClient(TERRA_HOST);
  147. const info = await lcd.tx.txInfo(tx);
  148. const sequence = parseSequenceFromLogTerra(info);
  149. if (!sequence) {
  150. throw new Error("Sequence not found");
  151. }
  152. const emitterAddress = await getEmitterAddressTerra(
  153. TERRA_TOKEN_BRIDGE_ADDRESS
  154. );
  155. const { vaaBytes } = await getSignedVAAWithRetry(
  156. CHAIN_ID_TERRA,
  157. emitterAddress,
  158. sequence,
  159. WORMHOLE_RPC_HOSTS.length
  160. );
  161. return { vaa: uint8ArrayToHex(vaaBytes), error: null };
  162. } catch (e) {
  163. console.error(e);
  164. enqueueSnackbar(null, {
  165. content: <Alert severity="error">{parseError(e)}</Alert>,
  166. });
  167. return { vaa: null, error: parseError(e) };
  168. }
  169. }
  170. function RelayerRecovery({
  171. parsedPayload,
  172. signedVaa,
  173. onClick,
  174. }: {
  175. parsedPayload: any;
  176. signedVaa: string;
  177. onClick: () => void;
  178. }) {
  179. const classes = useStyles();
  180. const relayerInfo = useRelayersAvailable(true);
  181. const [selectedRelayer, setSelectedRelayer] = useState<Relayer | null>(null);
  182. const [isAttemptingToSchedule, setIsAttemptingToSchedule] = useState(false);
  183. const { enqueueSnackbar } = useSnackbar();
  184. console.log(parsedPayload, relayerInfo, "in recovery relayer");
  185. const fee =
  186. (parsedPayload && parsedPayload.fee && parseInt(parsedPayload.fee)) || null;
  187. //This check is probably more sophisticated in the future. Possibly a net call.
  188. const isEligible =
  189. fee &&
  190. fee > 0 &&
  191. relayerInfo?.data?.relayers?.length &&
  192. relayerInfo?.data?.relayers?.length > 0;
  193. const handleRelayerChange = useCallback(
  194. (relayer: Relayer | null) => {
  195. setSelectedRelayer(relayer);
  196. },
  197. [setSelectedRelayer]
  198. );
  199. const handleGo = useCallback(async () => {
  200. console.log("handle go", selectedRelayer, parsedPayload);
  201. if (!(selectedRelayer && selectedRelayer.url)) {
  202. return;
  203. }
  204. setIsAttemptingToSchedule(true);
  205. axios
  206. .get(
  207. selectedRelayer.url +
  208. RELAY_URL_EXTENSION +
  209. encodeURIComponent(
  210. Buffer.from(hexToUint8Array(signedVaa)).toString("base64")
  211. )
  212. )
  213. .then(
  214. () => {
  215. setIsAttemptingToSchedule(false);
  216. onClick();
  217. },
  218. (error) => {
  219. setIsAttemptingToSchedule(false);
  220. enqueueSnackbar(null, {
  221. content: (
  222. <Alert severity="error">
  223. {"Relay request rejected. Error: " + error.message}
  224. </Alert>
  225. ),
  226. });
  227. }
  228. );
  229. }, [selectedRelayer, enqueueSnackbar, onClick, signedVaa, parsedPayload]);
  230. if (!isEligible) {
  231. return null;
  232. }
  233. return (
  234. <Alert variant="outlined" severity="info" className={classes.relayAlert}>
  235. <Typography>{"This transaction is eligible to be relayed"}</Typography>
  236. <RelaySelector
  237. selectedValue={selectedRelayer}
  238. onChange={handleRelayerChange}
  239. />
  240. <ButtonWithLoader
  241. disabled={!selectedRelayer}
  242. onClick={handleGo}
  243. showLoader={isAttemptingToSchedule}
  244. >
  245. Request Relay
  246. </ButtonWithLoader>
  247. </Alert>
  248. );
  249. }
  250. export default function Recovery() {
  251. const classes = useStyles();
  252. const { push } = useHistory();
  253. const { enqueueSnackbar } = useSnackbar();
  254. const dispatch = useDispatch();
  255. const { provider } = useEthereumProvider();
  256. const [type, setType] = useState("Token");
  257. const isNFT = type === "NFT";
  258. const [recoverySourceChain, setRecoverySourceChain] =
  259. useState(CHAIN_ID_SOLANA);
  260. const [recoverySourceTx, setRecoverySourceTx] = useState("");
  261. const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
  262. useState(false);
  263. const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
  264. const [recoverySignedVAA, setRecoverySignedVAA] = useState("");
  265. const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
  266. const { isReady, statusMessage } = useIsWalletReady(recoverySourceChain);
  267. const walletConnectError =
  268. isEVMChain(recoverySourceChain) && !isReady ? statusMessage : "";
  269. const parsedPayload = useMemo(() => {
  270. try {
  271. return recoveryParsedVAA?.payload
  272. ? isNFT
  273. ? parseNFTPayload(
  274. Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
  275. )
  276. : parseTransferPayload(
  277. Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
  278. )
  279. : null;
  280. } catch (e) {
  281. console.error(e);
  282. return null;
  283. }
  284. }, [recoveryParsedVAA, isNFT]);
  285. const { search } = useLocation();
  286. const query = useMemo(() => new URLSearchParams(search), [search]);
  287. const pathSourceChain = query.get("sourceChain");
  288. const pathSourceTransaction = query.get("transactionId");
  289. //This effect initializes the state based on the path params.
  290. useEffect(() => {
  291. if (!pathSourceChain && !pathSourceTransaction) {
  292. return;
  293. }
  294. try {
  295. const sourceChain: ChainId =
  296. CHAINS_BY_ID[parseFloat(pathSourceChain || "") as ChainId]?.id;
  297. if (sourceChain) {
  298. setRecoverySourceChain(sourceChain);
  299. }
  300. if (pathSourceTransaction) {
  301. setRecoverySourceTx(pathSourceTransaction);
  302. }
  303. } catch (e) {
  304. console.error(e);
  305. console.error("Invalid path params specified.");
  306. }
  307. }, [pathSourceChain, pathSourceTransaction]);
  308. useEffect(() => {
  309. if (recoverySourceTx && (!isEVMChain(recoverySourceChain) || isReady)) {
  310. let cancelled = false;
  311. if (isEVMChain(recoverySourceChain) && provider) {
  312. setRecoverySourceTxError("");
  313. setRecoverySourceTxIsLoading(true);
  314. (async () => {
  315. const { vaa, error } = await evm(
  316. provider,
  317. recoverySourceTx,
  318. enqueueSnackbar,
  319. recoverySourceChain,
  320. isNFT
  321. );
  322. if (!cancelled) {
  323. setRecoverySourceTxIsLoading(false);
  324. if (vaa) {
  325. setRecoverySignedVAA(vaa);
  326. }
  327. if (error) {
  328. setRecoverySourceTxError(error);
  329. }
  330. }
  331. })();
  332. } else if (recoverySourceChain === CHAIN_ID_SOLANA) {
  333. setRecoverySourceTxError("");
  334. setRecoverySourceTxIsLoading(true);
  335. (async () => {
  336. const { vaa, error } = await solana(
  337. recoverySourceTx,
  338. enqueueSnackbar,
  339. isNFT
  340. );
  341. if (!cancelled) {
  342. setRecoverySourceTxIsLoading(false);
  343. if (vaa) {
  344. setRecoverySignedVAA(vaa);
  345. }
  346. if (error) {
  347. setRecoverySourceTxError(error);
  348. }
  349. }
  350. })();
  351. } else if (recoverySourceChain === CHAIN_ID_TERRA) {
  352. setRecoverySourceTxError("");
  353. setRecoverySourceTxIsLoading(true);
  354. (async () => {
  355. const { vaa, error } = await terra(recoverySourceTx, enqueueSnackbar);
  356. if (!cancelled) {
  357. setRecoverySourceTxIsLoading(false);
  358. if (vaa) {
  359. setRecoverySignedVAA(vaa);
  360. }
  361. if (error) {
  362. setRecoverySourceTxError(error);
  363. }
  364. }
  365. })();
  366. }
  367. return () => {
  368. cancelled = true;
  369. };
  370. }
  371. }, [
  372. recoverySourceChain,
  373. recoverySourceTx,
  374. provider,
  375. enqueueSnackbar,
  376. isNFT,
  377. isReady,
  378. ]);
  379. const handleTypeChange = useCallback((event) => {
  380. setRecoverySourceChain((prevChain) =>
  381. event.target.value === "NFT" &&
  382. !CHAINS_WITH_NFT_SUPPORT.find((chain) => chain.id === prevChain)
  383. ? CHAIN_ID_SOLANA
  384. : prevChain
  385. );
  386. setType(event.target.value);
  387. }, []);
  388. const handleSourceChainChange = useCallback((event) => {
  389. setRecoverySourceTx("");
  390. setRecoverySourceChain(event.target.value);
  391. }, []);
  392. const handleSourceTxChange = useCallback((event) => {
  393. setRecoverySourceTx(event.target.value.trim());
  394. }, []);
  395. const handleSignedVAAChange = useCallback((event) => {
  396. setRecoverySignedVAA(event.target.value.trim());
  397. }, []);
  398. useEffect(() => {
  399. let cancelled = false;
  400. if (recoverySignedVAA) {
  401. (async () => {
  402. try {
  403. const { parse_vaa } = await import(
  404. "@certusone/wormhole-sdk/lib/esm/solana/core/bridge"
  405. );
  406. const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
  407. if (!cancelled) {
  408. setRecoveryParsedVAA(parsedVAA);
  409. }
  410. } catch (e) {
  411. console.log(e);
  412. if (!cancelled) {
  413. setRecoveryParsedVAA(null);
  414. }
  415. }
  416. })();
  417. }
  418. return () => {
  419. cancelled = true;
  420. };
  421. }, [recoverySignedVAA]);
  422. const parsedPayloadTargetChain = parsedPayload?.targetChain;
  423. const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
  424. const handleRecoverClickBase = useCallback(
  425. (useRelayer: boolean) => {
  426. if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
  427. // TODO: make recovery reducer
  428. if (isNFT) {
  429. dispatch(
  430. setRecoveryNFTVaa({
  431. vaa: recoverySignedVAA,
  432. parsedPayload: {
  433. targetChain: parsedPayload.targetChain,
  434. targetAddress: parsedPayload.targetAddress,
  435. originChain: parsedPayload.originChain,
  436. originAddress: parsedPayload.originAddress,
  437. },
  438. })
  439. );
  440. push("/nft");
  441. } else {
  442. dispatch(
  443. setRecoveryVaa({
  444. vaa: recoverySignedVAA,
  445. useRelayer,
  446. parsedPayload: {
  447. targetChain: parsedPayload.targetChain,
  448. targetAddress: parsedPayload.targetAddress,
  449. originChain: parsedPayload.originChain,
  450. originAddress: parsedPayload.originAddress,
  451. amount:
  452. "amount" in parsedPayload
  453. ? parsedPayload.amount.toString()
  454. : "",
  455. },
  456. })
  457. );
  458. push("/transfer");
  459. }
  460. }
  461. },
  462. [
  463. dispatch,
  464. enableRecovery,
  465. recoverySignedVAA,
  466. parsedPayloadTargetChain,
  467. parsedPayload,
  468. isNFT,
  469. push,
  470. ]
  471. );
  472. const handleRecoverClick = useCallback(() => {
  473. handleRecoverClickBase(false);
  474. }, [handleRecoverClickBase]);
  475. const handleRecoverWithRelayerClick = useCallback(() => {
  476. handleRecoverClickBase(true);
  477. }, [handleRecoverClickBase]);
  478. return (
  479. <Container maxWidth="md">
  480. <Card className={classes.mainCard}>
  481. <Alert severity="info" variant="outlined">
  482. If you have sent your tokens but have not redeemed them, you may paste
  483. in the Source Transaction ID (from Step 3) to resume your transfer.
  484. </Alert>
  485. <TextField
  486. select
  487. variant="outlined"
  488. label="Type"
  489. disabled={!!recoverySignedVAA}
  490. value={type}
  491. onChange={handleTypeChange}
  492. fullWidth
  493. margin="normal"
  494. >
  495. <MenuItem value="Token">Token</MenuItem>
  496. <MenuItem value="NFT">NFT</MenuItem>
  497. </TextField>
  498. <ChainSelect
  499. select
  500. variant="outlined"
  501. label="Source Chain"
  502. disabled={!!recoverySignedVAA}
  503. value={recoverySourceChain}
  504. onChange={handleSourceChainChange}
  505. fullWidth
  506. margin="normal"
  507. chains={isNFT ? CHAINS_WITH_NFT_SUPPORT : CHAINS}
  508. />
  509. {isEVMChain(recoverySourceChain) ? (
  510. <KeyAndBalance chainId={recoverySourceChain} />
  511. ) : null}
  512. <TextField
  513. variant="outlined"
  514. label="Source Tx (paste here)"
  515. disabled={
  516. !!recoverySignedVAA ||
  517. recoverySourceTxIsLoading ||
  518. !!walletConnectError
  519. }
  520. value={recoverySourceTx}
  521. onChange={handleSourceTxChange}
  522. error={!!recoverySourceTxError || !!walletConnectError}
  523. helperText={recoverySourceTxError || walletConnectError}
  524. fullWidth
  525. margin="normal"
  526. />
  527. <RelayerRecovery
  528. parsedPayload={parsedPayload}
  529. signedVaa={recoverySignedVAA}
  530. onClick={handleRecoverWithRelayerClick}
  531. />
  532. <ButtonWithLoader
  533. onClick={handleRecoverClick}
  534. disabled={!enableRecovery}
  535. showLoader={recoverySourceTxIsLoading}
  536. >
  537. Recover
  538. </ButtonWithLoader>
  539. <div className={classes.advancedContainer}>
  540. <Accordion>
  541. <AccordionSummary expandIcon={<ExpandMore />}>
  542. Advanced
  543. </AccordionSummary>
  544. <AccordionDetails>
  545. <div>
  546. <Box position="relative">
  547. <TextField
  548. variant="outlined"
  549. label="Signed VAA (Hex)"
  550. disabled={recoverySourceTxIsLoading}
  551. value={recoverySignedVAA || ""}
  552. onChange={handleSignedVAAChange}
  553. fullWidth
  554. margin="normal"
  555. />
  556. {recoverySourceTxIsLoading ? (
  557. <Box
  558. position="absolute"
  559. style={{
  560. top: 0,
  561. right: 0,
  562. left: 0,
  563. bottom: 0,
  564. backgroundColor: "rgba(0,0,0,0.5)",
  565. display: "flex",
  566. alignItems: "center",
  567. justifyContent: "center",
  568. }}
  569. >
  570. <CircularProgress />
  571. </Box>
  572. ) : null}
  573. </Box>
  574. <Box my={4}>
  575. <Divider />
  576. </Box>
  577. <TextField
  578. variant="outlined"
  579. label="Emitter Chain"
  580. disabled
  581. value={recoveryParsedVAA?.emitter_chain || ""}
  582. fullWidth
  583. margin="normal"
  584. />
  585. <TextField
  586. variant="outlined"
  587. label="Emitter Address"
  588. disabled
  589. value={
  590. (recoveryParsedVAA &&
  591. hexToNativeString(
  592. recoveryParsedVAA.emitter_address,
  593. recoveryParsedVAA.emitter_chain
  594. )) ||
  595. ""
  596. }
  597. fullWidth
  598. margin="normal"
  599. />
  600. <TextField
  601. variant="outlined"
  602. label="Sequence"
  603. disabled
  604. value={recoveryParsedVAA?.sequence || ""}
  605. fullWidth
  606. margin="normal"
  607. />
  608. <TextField
  609. variant="outlined"
  610. label="Timestamp"
  611. disabled
  612. value={
  613. (recoveryParsedVAA &&
  614. new Date(
  615. recoveryParsedVAA.timestamp * 1000
  616. ).toLocaleString()) ||
  617. ""
  618. }
  619. fullWidth
  620. margin="normal"
  621. />
  622. <Box my={4}>
  623. <Divider />
  624. </Box>
  625. <TextField
  626. variant="outlined"
  627. label="Origin Chain"
  628. disabled
  629. value={parsedPayload?.originChain.toString() || ""}
  630. fullWidth
  631. margin="normal"
  632. />
  633. <TextField
  634. variant="outlined"
  635. label="Origin Token Address"
  636. disabled
  637. value={
  638. (parsedPayload &&
  639. hexToNativeString(
  640. parsedPayload.originAddress,
  641. parsedPayload.originChain
  642. )) ||
  643. ""
  644. }
  645. fullWidth
  646. margin="normal"
  647. />
  648. {isNFT ? (
  649. <TextField
  650. variant="outlined"
  651. label="Origin Token ID"
  652. disabled
  653. // @ts-ignore
  654. value={parsedPayload?.tokenId || ""}
  655. fullWidth
  656. margin="normal"
  657. />
  658. ) : null}
  659. <TextField
  660. variant="outlined"
  661. label="Target Chain"
  662. disabled
  663. value={parsedPayload?.targetChain.toString() || ""}
  664. fullWidth
  665. margin="normal"
  666. />
  667. <TextField
  668. variant="outlined"
  669. label="Target Address"
  670. disabled
  671. value={
  672. (parsedPayload &&
  673. hexToNativeString(
  674. parsedPayload.targetAddress,
  675. parsedPayload.targetChain
  676. )) ||
  677. ""
  678. }
  679. fullWidth
  680. margin="normal"
  681. />
  682. {isNFT ? null : (
  683. <>
  684. <TextField
  685. variant="outlined"
  686. label="Amount"
  687. disabled
  688. // @ts-ignore
  689. value={parsedPayload?.amount.toString() || ""}
  690. fullWidth
  691. margin="normal"
  692. />
  693. <TextField
  694. variant="outlined"
  695. label="Relayer Fee"
  696. disabled
  697. // @ts-ignore
  698. value={parsedPayload?.fee.toString() || ""}
  699. fullWidth
  700. margin="normal"
  701. />
  702. </>
  703. )}
  704. </div>
  705. </AccordionDetails>
  706. </Accordion>
  707. </div>
  708. </Card>
  709. </Container>
  710. );
  711. }