| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732 |
- import {
- ChainId,
- CHAIN_ID_SOLANA,
- CHAIN_ID_TERRA,
- getEmitterAddressEth,
- getEmitterAddressSolana,
- getEmitterAddressTerra,
- hexToNativeString,
- hexToUint8Array,
- isEVMChain,
- parseNFTPayload,
- parseSequenceFromLogEth,
- parseSequenceFromLogSolana,
- parseSequenceFromLogTerra,
- parseTransferPayload,
- uint8ArrayToHex,
- } from "@certusone/wormhole-sdk";
- import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
- Box,
- Card,
- CircularProgress,
- Container,
- Divider,
- makeStyles,
- MenuItem,
- TextField,
- Typography,
- } from "@material-ui/core";
- import { ExpandMore } from "@material-ui/icons";
- import { Alert } from "@material-ui/lab";
- import { Connection } from "@solana/web3.js";
- import { LCDClient } from "@terra-money/terra.js";
- import axios from "axios";
- import { ethers } from "ethers";
- import { useSnackbar } from "notistack";
- import { useCallback, useEffect, useMemo, useState } from "react";
- import { useDispatch } from "react-redux";
- import { useHistory, useLocation } from "react-router";
- import { useEthereumProvider } from "../contexts/EthereumProviderContext";
- import useIsWalletReady from "../hooks/useIsWalletReady";
- import useRelayersAvailable, { Relayer } from "../hooks/useRelayersAvailable";
- import { COLORS } from "../muiTheme";
- import { setRecoveryVaa as setRecoveryNFTVaa } from "../store/nftSlice";
- import { setRecoveryVaa } from "../store/transferSlice";
- import {
- CHAINS,
- CHAINS_BY_ID,
- CHAINS_WITH_NFT_SUPPORT,
- getBridgeAddressForChain,
- getNFTBridgeAddressForChain,
- getTokenBridgeAddressForChain,
- RELAY_URL_EXTENSION,
- SOLANA_HOST,
- SOL_NFT_BRIDGE_ADDRESS,
- SOL_TOKEN_BRIDGE_ADDRESS,
- TERRA_HOST,
- TERRA_TOKEN_BRIDGE_ADDRESS,
- WORMHOLE_RPC_HOSTS,
- } from "../utils/consts";
- import { getSignedVAAWithRetry } from "../utils/getSignedVAAWithRetry";
- import parseError from "../utils/parseError";
- import ButtonWithLoader from "./ButtonWithLoader";
- import ChainSelect from "./ChainSelect";
- import KeyAndBalance from "./KeyAndBalance";
- import RelaySelector from "./RelaySelector";
- const useStyles = makeStyles((theme) => ({
- mainCard: {
- padding: "32px 32px 16px",
- backgroundColor: COLORS.whiteWithTransparency,
- },
- advancedContainer: {
- padding: theme.spacing(2, 0),
- },
- relayAlert: {
- marginTop: theme.spacing(2),
- marginBottom: theme.spacing(2),
- "& > .MuiAlert-message": {
- width: "100%",
- },
- },
- }));
- async function evm(
- provider: ethers.providers.Web3Provider,
- tx: string,
- enqueueSnackbar: any,
- chainId: ChainId,
- nft: boolean
- ) {
- try {
- const receipt = await provider.getTransactionReceipt(tx);
- const sequence = parseSequenceFromLogEth(
- receipt,
- getBridgeAddressForChain(chainId)
- );
- const emitterAddress = getEmitterAddressEth(
- nft
- ? getNFTBridgeAddressForChain(chainId)
- : getTokenBridgeAddressForChain(chainId)
- );
- const { vaaBytes } = await getSignedVAAWithRetry(
- chainId,
- emitterAddress,
- sequence.toString(),
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
- } catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: <Alert severity="error">{parseError(e)}</Alert>,
- });
- return { vaa: null, error: parseError(e) };
- }
- }
- async function solana(tx: string, enqueueSnackbar: any, nft: boolean) {
- try {
- const connection = new Connection(SOLANA_HOST, "confirmed");
- const info = await connection.getTransaction(tx);
- if (!info) {
- throw new Error("An error occurred while fetching the transaction info");
- }
- const sequence = parseSequenceFromLogSolana(info);
- const emitterAddress = await getEmitterAddressSolana(
- nft ? SOL_NFT_BRIDGE_ADDRESS : SOL_TOKEN_BRIDGE_ADDRESS
- );
- const { vaaBytes } = await getSignedVAAWithRetry(
- CHAIN_ID_SOLANA,
- emitterAddress,
- sequence.toString(),
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
- } catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: <Alert severity="error">{parseError(e)}</Alert>,
- });
- return { vaa: null, error: parseError(e) };
- }
- }
- async function terra(tx: string, enqueueSnackbar: any) {
- try {
- const lcd = new LCDClient(TERRA_HOST);
- const info = await lcd.tx.txInfo(tx);
- const sequence = parseSequenceFromLogTerra(info);
- if (!sequence) {
- throw new Error("Sequence not found");
- }
- const emitterAddress = await getEmitterAddressTerra(
- TERRA_TOKEN_BRIDGE_ADDRESS
- );
- const { vaaBytes } = await getSignedVAAWithRetry(
- CHAIN_ID_TERRA,
- emitterAddress,
- sequence,
- WORMHOLE_RPC_HOSTS.length
- );
- return { vaa: uint8ArrayToHex(vaaBytes), error: null };
- } catch (e) {
- console.error(e);
- enqueueSnackbar(null, {
- content: <Alert severity="error">{parseError(e)}</Alert>,
- });
- return { vaa: null, error: parseError(e) };
- }
- }
- function RelayerRecovery({
- parsedPayload,
- signedVaa,
- onClick,
- }: {
- parsedPayload: any;
- signedVaa: string;
- onClick: () => void;
- }) {
- const classes = useStyles();
- const relayerInfo = useRelayersAvailable(true);
- const [selectedRelayer, setSelectedRelayer] = useState<Relayer | null>(null);
- const [isAttemptingToSchedule, setIsAttemptingToSchedule] = useState(false);
- const { enqueueSnackbar } = useSnackbar();
- console.log(parsedPayload, relayerInfo, "in recovery relayer");
- const fee =
- (parsedPayload && parsedPayload.fee && parseInt(parsedPayload.fee)) || null;
- //This check is probably more sophisticated in the future. Possibly a net call.
- const isEligible =
- fee &&
- fee > 0 &&
- relayerInfo?.data?.relayers?.length &&
- relayerInfo?.data?.relayers?.length > 0;
- const handleRelayerChange = useCallback(
- (relayer: Relayer | null) => {
- setSelectedRelayer(relayer);
- },
- [setSelectedRelayer]
- );
- const handleGo = useCallback(async () => {
- console.log("handle go", selectedRelayer, parsedPayload);
- if (!(selectedRelayer && selectedRelayer.url)) {
- return;
- }
- setIsAttemptingToSchedule(true);
- axios
- .get(
- selectedRelayer.url +
- RELAY_URL_EXTENSION +
- encodeURIComponent(
- Buffer.from(hexToUint8Array(signedVaa)).toString("base64")
- )
- )
- .then(
- () => {
- setIsAttemptingToSchedule(false);
- onClick();
- },
- (error) => {
- setIsAttemptingToSchedule(false);
- enqueueSnackbar(null, {
- content: (
- <Alert severity="error">
- {"Relay request rejected. Error: " + error.message}
- </Alert>
- ),
- });
- }
- );
- }, [selectedRelayer, enqueueSnackbar, onClick, signedVaa, parsedPayload]);
- if (!isEligible) {
- return null;
- }
- return (
- <Alert variant="outlined" severity="info" className={classes.relayAlert}>
- <Typography>{"This transaction is eligible to be relayed"}</Typography>
- <RelaySelector
- selectedValue={selectedRelayer}
- onChange={handleRelayerChange}
- />
- <ButtonWithLoader
- disabled={!selectedRelayer}
- onClick={handleGo}
- showLoader={isAttemptingToSchedule}
- >
- Request Relay
- </ButtonWithLoader>
- </Alert>
- );
- }
- export default function Recovery() {
- const classes = useStyles();
- const { push } = useHistory();
- const { enqueueSnackbar } = useSnackbar();
- const dispatch = useDispatch();
- const { provider } = useEthereumProvider();
- const [type, setType] = useState("Token");
- const isNFT = type === "NFT";
- const [recoverySourceChain, setRecoverySourceChain] =
- useState(CHAIN_ID_SOLANA);
- const [recoverySourceTx, setRecoverySourceTx] = useState("");
- const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
- useState(false);
- const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
- const [recoverySignedVAA, setRecoverySignedVAA] = useState("");
- const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
- const { isReady, statusMessage } = useIsWalletReady(recoverySourceChain);
- const walletConnectError =
- isEVMChain(recoverySourceChain) && !isReady ? statusMessage : "";
- const parsedPayload = useMemo(() => {
- try {
- return recoveryParsedVAA?.payload
- ? isNFT
- ? parseNFTPayload(
- Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
- )
- : parseTransferPayload(
- Buffer.from(new Uint8Array(recoveryParsedVAA.payload))
- )
- : null;
- } catch (e) {
- console.error(e);
- return null;
- }
- }, [recoveryParsedVAA, isNFT]);
- const { search } = useLocation();
- const query = useMemo(() => new URLSearchParams(search), [search]);
- const pathSourceChain = query.get("sourceChain");
- const pathSourceTransaction = query.get("transactionId");
- //This effect initializes the state based on the path params.
- useEffect(() => {
- if (!pathSourceChain && !pathSourceTransaction) {
- return;
- }
- try {
- const sourceChain: ChainId =
- CHAINS_BY_ID[parseFloat(pathSourceChain || "") as ChainId]?.id;
- if (sourceChain) {
- setRecoverySourceChain(sourceChain);
- }
- if (pathSourceTransaction) {
- setRecoverySourceTx(pathSourceTransaction);
- }
- } catch (e) {
- console.error(e);
- console.error("Invalid path params specified.");
- }
- }, [pathSourceChain, pathSourceTransaction]);
- useEffect(() => {
- if (recoverySourceTx && (!isEVMChain(recoverySourceChain) || isReady)) {
- let cancelled = false;
- if (isEVMChain(recoverySourceChain) && provider) {
- setRecoverySourceTxError("");
- setRecoverySourceTxIsLoading(true);
- (async () => {
- const { vaa, error } = await evm(
- provider,
- recoverySourceTx,
- enqueueSnackbar,
- recoverySourceChain,
- isNFT
- );
- if (!cancelled) {
- setRecoverySourceTxIsLoading(false);
- if (vaa) {
- setRecoverySignedVAA(vaa);
- }
- if (error) {
- setRecoverySourceTxError(error);
- }
- }
- })();
- } else if (recoverySourceChain === CHAIN_ID_SOLANA) {
- setRecoverySourceTxError("");
- setRecoverySourceTxIsLoading(true);
- (async () => {
- const { vaa, error } = await solana(
- recoverySourceTx,
- enqueueSnackbar,
- isNFT
- );
- if (!cancelled) {
- setRecoverySourceTxIsLoading(false);
- if (vaa) {
- setRecoverySignedVAA(vaa);
- }
- if (error) {
- setRecoverySourceTxError(error);
- }
- }
- })();
- } else if (recoverySourceChain === CHAIN_ID_TERRA) {
- setRecoverySourceTxError("");
- setRecoverySourceTxIsLoading(true);
- (async () => {
- const { vaa, error } = await terra(recoverySourceTx, enqueueSnackbar);
- if (!cancelled) {
- setRecoverySourceTxIsLoading(false);
- if (vaa) {
- setRecoverySignedVAA(vaa);
- }
- if (error) {
- setRecoverySourceTxError(error);
- }
- }
- })();
- }
- return () => {
- cancelled = true;
- };
- }
- }, [
- recoverySourceChain,
- recoverySourceTx,
- provider,
- enqueueSnackbar,
- isNFT,
- isReady,
- ]);
- const handleTypeChange = useCallback((event) => {
- setRecoverySourceChain((prevChain) =>
- event.target.value === "NFT" &&
- !CHAINS_WITH_NFT_SUPPORT.find((chain) => chain.id === prevChain)
- ? CHAIN_ID_SOLANA
- : prevChain
- );
- setType(event.target.value);
- }, []);
- const handleSourceChainChange = useCallback((event) => {
- setRecoverySourceTx("");
- setRecoverySourceChain(event.target.value);
- }, []);
- const handleSourceTxChange = useCallback((event) => {
- setRecoverySourceTx(event.target.value.trim());
- }, []);
- const handleSignedVAAChange = useCallback((event) => {
- setRecoverySignedVAA(event.target.value.trim());
- }, []);
- useEffect(() => {
- let cancelled = false;
- if (recoverySignedVAA) {
- (async () => {
- try {
- const { parse_vaa } = await import(
- "@certusone/wormhole-sdk/lib/esm/solana/core/bridge"
- );
- const parsedVAA = parse_vaa(hexToUint8Array(recoverySignedVAA));
- if (!cancelled) {
- setRecoveryParsedVAA(parsedVAA);
- }
- } catch (e) {
- console.log(e);
- if (!cancelled) {
- setRecoveryParsedVAA(null);
- }
- }
- })();
- }
- return () => {
- cancelled = true;
- };
- }, [recoverySignedVAA]);
- const parsedPayloadTargetChain = parsedPayload?.targetChain;
- const enableRecovery = recoverySignedVAA && parsedPayloadTargetChain;
- const handleRecoverClickBase = useCallback(
- (useRelayer: boolean) => {
- if (enableRecovery && recoverySignedVAA && parsedPayloadTargetChain) {
- // TODO: make recovery reducer
- if (isNFT) {
- dispatch(
- setRecoveryNFTVaa({
- vaa: recoverySignedVAA,
- parsedPayload: {
- targetChain: parsedPayload.targetChain,
- targetAddress: parsedPayload.targetAddress,
- originChain: parsedPayload.originChain,
- originAddress: parsedPayload.originAddress,
- },
- })
- );
- push("/nft");
- } else {
- dispatch(
- setRecoveryVaa({
- vaa: recoverySignedVAA,
- useRelayer,
- parsedPayload: {
- targetChain: parsedPayload.targetChain,
- targetAddress: parsedPayload.targetAddress,
- originChain: parsedPayload.originChain,
- originAddress: parsedPayload.originAddress,
- amount:
- "amount" in parsedPayload
- ? parsedPayload.amount.toString()
- : "",
- },
- })
- );
- push("/transfer");
- }
- }
- },
- [
- dispatch,
- enableRecovery,
- recoverySignedVAA,
- parsedPayloadTargetChain,
- parsedPayload,
- isNFT,
- push,
- ]
- );
- const handleRecoverClick = useCallback(() => {
- handleRecoverClickBase(false);
- }, [handleRecoverClickBase]);
- const handleRecoverWithRelayerClick = useCallback(() => {
- handleRecoverClickBase(true);
- }, [handleRecoverClickBase]);
- return (
- <Container maxWidth="md">
- <Card className={classes.mainCard}>
- <Alert severity="info" variant="outlined">
- If you have sent your tokens but have not redeemed them, you may paste
- in the Source Transaction ID (from Step 3) to resume your transfer.
- </Alert>
- <TextField
- select
- variant="outlined"
- label="Type"
- disabled={!!recoverySignedVAA}
- value={type}
- onChange={handleTypeChange}
- fullWidth
- margin="normal"
- >
- <MenuItem value="Token">Token</MenuItem>
- <MenuItem value="NFT">NFT</MenuItem>
- </TextField>
- <ChainSelect
- select
- variant="outlined"
- label="Source Chain"
- disabled={!!recoverySignedVAA}
- value={recoverySourceChain}
- onChange={handleSourceChainChange}
- fullWidth
- margin="normal"
- chains={isNFT ? CHAINS_WITH_NFT_SUPPORT : CHAINS}
- />
- {isEVMChain(recoverySourceChain) ? (
- <KeyAndBalance chainId={recoverySourceChain} />
- ) : null}
- <TextField
- variant="outlined"
- label="Source Tx (paste here)"
- disabled={
- !!recoverySignedVAA ||
- recoverySourceTxIsLoading ||
- !!walletConnectError
- }
- value={recoverySourceTx}
- onChange={handleSourceTxChange}
- error={!!recoverySourceTxError || !!walletConnectError}
- helperText={recoverySourceTxError || walletConnectError}
- fullWidth
- margin="normal"
- />
- <RelayerRecovery
- parsedPayload={parsedPayload}
- signedVaa={recoverySignedVAA}
- onClick={handleRecoverWithRelayerClick}
- />
- <ButtonWithLoader
- onClick={handleRecoverClick}
- disabled={!enableRecovery}
- showLoader={recoverySourceTxIsLoading}
- >
- Recover
- </ButtonWithLoader>
- <div className={classes.advancedContainer}>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMore />}>
- Advanced
- </AccordionSummary>
- <AccordionDetails>
- <div>
- <Box position="relative">
- <TextField
- variant="outlined"
- label="Signed VAA (Hex)"
- disabled={recoverySourceTxIsLoading}
- value={recoverySignedVAA || ""}
- onChange={handleSignedVAAChange}
- fullWidth
- margin="normal"
- />
- {recoverySourceTxIsLoading ? (
- <Box
- position="absolute"
- style={{
- top: 0,
- right: 0,
- left: 0,
- bottom: 0,
- backgroundColor: "rgba(0,0,0,0.5)",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- }}
- >
- <CircularProgress />
- </Box>
- ) : null}
- </Box>
- <Box my={4}>
- <Divider />
- </Box>
- <TextField
- variant="outlined"
- label="Emitter Chain"
- disabled
- value={recoveryParsedVAA?.emitter_chain || ""}
- fullWidth
- margin="normal"
- />
- <TextField
- variant="outlined"
- label="Emitter Address"
- disabled
- value={
- (recoveryParsedVAA &&
- hexToNativeString(
- recoveryParsedVAA.emitter_address,
- recoveryParsedVAA.emitter_chain
- )) ||
- ""
- }
- fullWidth
- margin="normal"
- />
- <TextField
- variant="outlined"
- label="Sequence"
- disabled
- value={recoveryParsedVAA?.sequence || ""}
- fullWidth
- margin="normal"
- />
- <TextField
- variant="outlined"
- label="Timestamp"
- disabled
- value={
- (recoveryParsedVAA &&
- new Date(
- recoveryParsedVAA.timestamp * 1000
- ).toLocaleString()) ||
- ""
- }
- fullWidth
- margin="normal"
- />
- <Box my={4}>
- <Divider />
- </Box>
- <TextField
- variant="outlined"
- label="Origin Chain"
- disabled
- value={parsedPayload?.originChain.toString() || ""}
- fullWidth
- margin="normal"
- />
- <TextField
- variant="outlined"
- label="Origin Token Address"
- disabled
- value={
- (parsedPayload &&
- hexToNativeString(
- parsedPayload.originAddress,
- parsedPayload.originChain
- )) ||
- ""
- }
- fullWidth
- margin="normal"
- />
- {isNFT ? (
- <TextField
- variant="outlined"
- label="Origin Token ID"
- disabled
- // @ts-ignore
- value={parsedPayload?.tokenId || ""}
- fullWidth
- margin="normal"
- />
- ) : null}
- <TextField
- variant="outlined"
- label="Target Chain"
- disabled
- value={parsedPayload?.targetChain.toString() || ""}
- fullWidth
- margin="normal"
- />
- <TextField
- variant="outlined"
- label="Target Address"
- disabled
- value={
- (parsedPayload &&
- hexToNativeString(
- parsedPayload.targetAddress,
- parsedPayload.targetChain
- )) ||
- ""
- }
- fullWidth
- margin="normal"
- />
- {isNFT ? null : (
- <>
- <TextField
- variant="outlined"
- label="Amount"
- disabled
- // @ts-ignore
- value={parsedPayload?.amount.toString() || ""}
- fullWidth
- margin="normal"
- />
- <TextField
- variant="outlined"
- label="Relayer Fee"
- disabled
- // @ts-ignore
- value={parsedPayload?.fee.toString() || ""}
- fullWidth
- margin="normal"
- />
- </>
- )}
- </div>
- </AccordionDetails>
- </Accordion>
- </div>
- </Card>
- </Container>
- );
- }
|