bridge_ui: QoL improvements
Change-Id: I4221ff5d7757e099b8dad55de6ca2765137e5e38
This commit is contained in:
parent
03a373676b
commit
26b6ee22bb
|
@ -63,7 +63,7 @@ export default function ButtonWithLoader({
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<Typography color="error" className={classes.error}>
|
<Typography variant="body2" color="error" className={classes.error}>
|
||||||
{error}
|
{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
getEmitterAddressEth,
|
getEmitterAddressEth,
|
||||||
getEmitterAddressSolana,
|
getEmitterAddressSolana,
|
||||||
getSignedVAA,
|
|
||||||
parseSequenceFromLogEth,
|
parseSequenceFromLogEth,
|
||||||
parseSequenceFromLogSolana,
|
parseSequenceFromLogSolana,
|
||||||
} from "@certusone/wormhole-sdk";
|
} from "@certusone/wormhole-sdk";
|
||||||
|
@ -31,11 +30,11 @@ import { BigNumber, ethers } from "ethers";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
|
import { setSignedVAAHex, setStep, setTargetChain } from "../../store/nftSlice";
|
||||||
import {
|
import {
|
||||||
selectNFTSignedVAAHex,
|
selectNFTSignedVAAHex,
|
||||||
selectNFTSourceChain,
|
selectNFTSourceChain,
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { setSignedVAAHex, setStep, setTargetChain } from "../../store/nftSlice";
|
|
||||||
import {
|
import {
|
||||||
hexToNativeString,
|
hexToNativeString,
|
||||||
hexToUint8Array,
|
hexToUint8Array,
|
||||||
|
@ -49,9 +48,9 @@ import {
|
||||||
SOL_NFT_BRIDGE_ADDRESS,
|
SOL_NFT_BRIDGE_ADDRESS,
|
||||||
WORMHOLE_RPC_HOSTS,
|
WORMHOLE_RPC_HOSTS,
|
||||||
} from "../../utils/consts";
|
} from "../../utils/consts";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
|
||||||
import { METADATA_REPLACE } from "../../utils/metaplex";
|
import { METADATA_REPLACE } from "../../utils/metaplex";
|
||||||
import { getNextRpcHost } from "../../utils/getSignedVAAWithRetry";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
fab: {
|
fab: {
|
||||||
|
@ -66,11 +65,11 @@ async function eth(provider: ethers.providers.Web3Provider, tx: string) {
|
||||||
const receipt = await provider.getTransactionReceipt(tx);
|
const receipt = await provider.getTransactionReceipt(tx);
|
||||||
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
||||||
const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
|
const emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
|
||||||
const { vaaBytes } = await getSignedVAA(
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
|
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
emitterAddress,
|
emitterAddress,
|
||||||
sequence.toString()
|
sequence.toString(),
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return uint8ArrayToHex(vaaBytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -90,11 +89,11 @@ async function solana(tx: string) {
|
||||||
const emitterAddress = await getEmitterAddressSolana(
|
const emitterAddress = await getEmitterAddressSolana(
|
||||||
SOL_NFT_BRIDGE_ADDRESS
|
SOL_NFT_BRIDGE_ADDRESS
|
||||||
);
|
);
|
||||||
const { vaaBytes } = await getSignedVAA(
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
|
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
emitterAddress,
|
emitterAddress,
|
||||||
sequence.toString()
|
sequence.toString(),
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return uint8ArrayToHex(vaaBytes);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -199,10 +198,10 @@ function RecoveryDialogContent({
|
||||||
setRecoverySourceChain(event.target.value);
|
setRecoverySourceChain(event.target.value);
|
||||||
}, []);
|
}, []);
|
||||||
const handleSourceTxChange = useCallback((event) => {
|
const handleSourceTxChange = useCallback((event) => {
|
||||||
setRecoverySourceTx(event.target.value);
|
setRecoverySourceTx(event.target.value.trim());
|
||||||
}, []);
|
}, []);
|
||||||
const handleSignedVAAChange = useCallback((event) => {
|
const handleSignedVAAChange = useCallback((event) => {
|
||||||
setRecoverySignedVAA(event.target.value);
|
setRecoverySignedVAA(event.target.value.trim());
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
|
@ -40,7 +40,7 @@ function Send() {
|
||||||
Transfer the NFT to the Wormhole Token Bridge.
|
Transfer the NFT to the Wormhole Token Bridge.
|
||||||
</StepDescription>
|
</StepDescription>
|
||||||
<KeyAndBalance chainId={sourceChain} />
|
<KeyAndBalance chainId={sourceChain} />
|
||||||
<Alert severity="warning">
|
<Alert severity="info">
|
||||||
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
|
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
|
||||||
wait for finalization. If you navigate away from this page before
|
wait for finalization. If you navigate away from this page before
|
||||||
completing Step 4, you will have to perform the recovery workflow to
|
completing Step 4, you will have to perform the recovery workflow to
|
||||||
|
|
|
@ -25,7 +25,10 @@ export default function ShowTx({
|
||||||
tx: Transaction;
|
tx: Transaction;
|
||||||
}) {
|
}) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const showExplorerLink = CLUSTER === "testnet" || CLUSTER === "mainnet";
|
const showExplorerLink =
|
||||||
|
CLUSTER === "testnet" ||
|
||||||
|
CLUSTER === "mainnet" ||
|
||||||
|
(CLUSTER === "devnet" && chainId === CHAIN_ID_SOLANA);
|
||||||
const explorerAddress =
|
const explorerAddress =
|
||||||
chainId === CHAIN_ID_ETH
|
chainId === CHAIN_ID_ETH
|
||||||
? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${
|
? `https://${CLUSTER === "testnet" ? "goerli." : ""}etherscan.io/tx/${
|
||||||
|
@ -33,7 +36,11 @@ export default function ShowTx({
|
||||||
}`
|
}`
|
||||||
: chainId === CHAIN_ID_SOLANA
|
: chainId === CHAIN_ID_SOLANA
|
||||||
? `https://explorer.solana.com/tx/${tx?.id}${
|
? `https://explorer.solana.com/tx/${tx?.id}${
|
||||||
CLUSTER === "testnet" ? "?cluster=testnet" : ""
|
CLUSTER === "testnet"
|
||||||
|
? "?cluster=testnet"
|
||||||
|
: CLUSTER === "devnet"
|
||||||
|
? "?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899"
|
||||||
|
: ""
|
||||||
}`
|
}`
|
||||||
: undefined;
|
: undefined;
|
||||||
const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer";
|
const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer";
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
|
||||||
|
import { Button, makeStyles } from "@material-ui/core";
|
||||||
|
import detectEthereumProvider from "@metamask/detect-provider";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
|
import {
|
||||||
|
selectTransferTargetAsset,
|
||||||
|
selectTransferTargetChain,
|
||||||
|
} from "../../store/selectors";
|
||||||
|
import {
|
||||||
|
ethTokenToParsedTokenAccount,
|
||||||
|
getEthereumToken,
|
||||||
|
} from "../../utils/ethereum";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
addButton: {
|
||||||
|
display: "block",
|
||||||
|
margin: `${theme.spacing(1)}px auto 0px`,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function AddToMetamask() {
|
||||||
|
const classes = useStyles();
|
||||||
|
const targetChain = useSelector(selectTransferTargetChain);
|
||||||
|
const targetAsset = useSelector(selectTransferTargetAsset);
|
||||||
|
const { provider, signerAddress } = useEthereumProvider();
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (provider && targetAsset && signerAddress) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = await getEthereumToken(targetAsset, provider);
|
||||||
|
const { symbol, decimals } = await ethTokenToParsedTokenAccount(
|
||||||
|
token,
|
||||||
|
signerAddress
|
||||||
|
);
|
||||||
|
const ethereum = (await detectEthereumProvider()) as any;
|
||||||
|
ethereum.request({
|
||||||
|
method: "wallet_watchAsset",
|
||||||
|
params: {
|
||||||
|
type: "ERC20", // In the future, other standards will be supported
|
||||||
|
options: {
|
||||||
|
address: targetAsset, // The address of the token contract
|
||||||
|
symbol, // A ticker symbol or shorthand, up to 5 characters
|
||||||
|
decimals, // The number of token decimals
|
||||||
|
// image: string; // A string url of the token logo
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, [provider, targetAsset, signerAddress]);
|
||||||
|
return provider &&
|
||||||
|
signerAddress &&
|
||||||
|
targetAsset &&
|
||||||
|
targetChain === CHAIN_ID_ETH ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleClick}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
className={classes.addButton}
|
||||||
|
>
|
||||||
|
Add to Metamask
|
||||||
|
</Button>
|
||||||
|
) : null;
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ import {
|
||||||
getEmitterAddressEth,
|
getEmitterAddressEth,
|
||||||
getEmitterAddressSolana,
|
getEmitterAddressSolana,
|
||||||
getEmitterAddressTerra,
|
getEmitterAddressTerra,
|
||||||
getSignedVAA,
|
|
||||||
parseSequenceFromLogEth,
|
parseSequenceFromLogEth,
|
||||||
parseSequenceFromLogSolana,
|
parseSequenceFromLogSolana,
|
||||||
parseSequenceFromLogTerra,
|
parseSequenceFromLogTerra,
|
||||||
|
@ -15,6 +14,7 @@ import {
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
@ -32,6 +32,7 @@ import { Alert } from "@material-ui/lab";
|
||||||
import { Connection } from "@solana/web3.js";
|
import { Connection } from "@solana/web3.js";
|
||||||
import { LCDClient } from "@terra-money/terra.js";
|
import { LCDClient } from "@terra-money/terra.js";
|
||||||
import { BigNumber, ethers } from "ethers";
|
import { BigNumber, ethers } from "ethers";
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
|
@ -59,7 +60,8 @@ import {
|
||||||
TERRA_TOKEN_BRIDGE_ADDRESS,
|
TERRA_TOKEN_BRIDGE_ADDRESS,
|
||||||
WORMHOLE_RPC_HOSTS,
|
WORMHOLE_RPC_HOSTS,
|
||||||
} from "../../utils/consts";
|
} from "../../utils/consts";
|
||||||
import { getNextRpcHost } from "../../utils/getSignedVAAWithRetry";
|
import { getSignedVAAWithRetry } from "../../utils/getSignedVAAWithRetry";
|
||||||
|
import parseError from "../../utils/parseError";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
@ -70,25 +72,30 @@ const useStyles = makeStyles((theme) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function eth(provider: ethers.providers.Web3Provider, tx: string) {
|
async function eth(
|
||||||
|
provider: ethers.providers.Web3Provider,
|
||||||
|
tx: string,
|
||||||
|
enqueueSnackbar: any
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const receipt = await provider.getTransactionReceipt(tx);
|
const receipt = await provider.getTransactionReceipt(tx);
|
||||||
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
|
||||||
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
|
||||||
const { vaaBytes } = await getSignedVAA(
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
|
|
||||||
CHAIN_ID_ETH,
|
CHAIN_ID_ETH,
|
||||||
emitterAddress,
|
emitterAddress,
|
||||||
sequence.toString()
|
sequence.toString(),
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function solana(tx: string) {
|
async function solana(tx: string, enqueueSnackbar: any) {
|
||||||
try {
|
try {
|
||||||
const connection = new Connection(SOLANA_HOST, "confirmed");
|
const connection = new Connection(SOLANA_HOST, "confirmed");
|
||||||
const info = await connection.getTransaction(tx);
|
const info = await connection.getTransaction(tx);
|
||||||
|
@ -99,20 +106,21 @@ async function solana(tx: string) {
|
||||||
const emitterAddress = await getEmitterAddressSolana(
|
const emitterAddress = await getEmitterAddressSolana(
|
||||||
SOL_TOKEN_BRIDGE_ADDRESS
|
SOL_TOKEN_BRIDGE_ADDRESS
|
||||||
);
|
);
|
||||||
const { vaaBytes } = await getSignedVAA(
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
|
|
||||||
CHAIN_ID_SOLANA,
|
CHAIN_ID_SOLANA,
|
||||||
emitterAddress,
|
emitterAddress,
|
||||||
sequence.toString()
|
sequence.toString(),
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function terra(tx: string) {
|
async function terra(tx: string, enqueueSnackbar: any) {
|
||||||
try {
|
try {
|
||||||
const lcd = new LCDClient(TERRA_HOST);
|
const lcd = new LCDClient(TERRA_HOST);
|
||||||
const info = await lcd.tx.txInfo(tx);
|
const info = await lcd.tx.txInfo(tx);
|
||||||
|
@ -123,17 +131,18 @@ async function terra(tx: string) {
|
||||||
const emitterAddress = await getEmitterAddressTerra(
|
const emitterAddress = await getEmitterAddressTerra(
|
||||||
TERRA_TOKEN_BRIDGE_ADDRESS
|
TERRA_TOKEN_BRIDGE_ADDRESS
|
||||||
);
|
);
|
||||||
const { vaaBytes } = await getSignedVAA(
|
const { vaaBytes } = await getSignedVAAWithRetry(
|
||||||
WORMHOLE_RPC_HOSTS[getNextRpcHost()],
|
|
||||||
CHAIN_ID_TERRA,
|
CHAIN_ID_TERRA,
|
||||||
emitterAddress,
|
emitterAddress,
|
||||||
sequence
|
sequence,
|
||||||
|
WORMHOLE_RPC_HOSTS.length
|
||||||
);
|
);
|
||||||
return uint8ArrayToHex(vaaBytes);
|
return { vaa: uint8ArrayToHex(vaaBytes), error: null };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
enqueueSnackbar(parseError(e), { variant: "error" });
|
||||||
|
return { vaa: null, error: parseError(e) };
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0 u256 amount
|
// 0 u256 amount
|
||||||
|
@ -159,12 +168,16 @@ function RecoveryDialogContent({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { provider } = useEthereumProvider();
|
const { provider } = useEthereumProvider();
|
||||||
const currentSourceChain = useSelector(selectTransferSourceChain);
|
const currentSourceChain = useSelector(selectTransferSourceChain);
|
||||||
const [recoverySourceChain, setRecoverySourceChain] =
|
const [recoverySourceChain, setRecoverySourceChain] =
|
||||||
useState(currentSourceChain);
|
useState(currentSourceChain);
|
||||||
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
const [recoverySourceTx, setRecoverySourceTx] = useState("");
|
||||||
|
const [recoverySourceTxIsLoading, setRecoverySourceTxIsLoading] =
|
||||||
|
useState(false);
|
||||||
|
const [recoverySourceTxError, setRecoverySourceTxError] = useState("");
|
||||||
const currentSignedVAA = useSelector(selectTransferSignedVAAHex);
|
const currentSignedVAA = useSelector(selectTransferSignedVAAHex);
|
||||||
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
const [recoverySignedVAA, setRecoverySignedVAA] = useState(currentSignedVAA);
|
||||||
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
const [recoveryParsedVAA, setRecoveryParsedVAA] = useState<any>(null);
|
||||||
|
@ -178,24 +191,55 @@ function RecoveryDialogContent({
|
||||||
if (recoverySourceTx) {
|
if (recoverySourceTx) {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
if (recoverySourceChain === CHAIN_ID_ETH && provider) {
|
if (recoverySourceChain === CHAIN_ID_ETH && provider) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
(async () => {
|
(async () => {
|
||||||
const vaa = await eth(provider, recoverySourceTx);
|
const { vaa, error } = await eth(
|
||||||
|
provider,
|
||||||
|
recoverySourceTx,
|
||||||
|
enqueueSnackbar
|
||||||
|
);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRecoverySignedVAA(vaa);
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
} else if (recoverySourceChain === CHAIN_ID_SOLANA) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
(async () => {
|
(async () => {
|
||||||
const vaa = await solana(recoverySourceTx);
|
const { vaa, error } = await solana(
|
||||||
|
recoverySourceTx,
|
||||||
|
enqueueSnackbar
|
||||||
|
);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRecoverySignedVAA(vaa);
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
} else if (recoverySourceChain === CHAIN_ID_TERRA) {
|
} else if (recoverySourceChain === CHAIN_ID_TERRA) {
|
||||||
|
setRecoverySourceTxError("");
|
||||||
|
setRecoverySourceTxIsLoading(true);
|
||||||
(async () => {
|
(async () => {
|
||||||
const vaa = await terra(recoverySourceTx);
|
const { vaa, error } = await terra(recoverySourceTx, enqueueSnackbar);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setRecoverySignedVAA(vaa);
|
setRecoverySourceTxIsLoading(false);
|
||||||
|
if (vaa) {
|
||||||
|
setRecoverySignedVAA(vaa);
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
setRecoverySourceTxError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -203,7 +247,7 @@ function RecoveryDialogContent({
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [recoverySourceChain, recoverySourceTx, provider]);
|
}, [recoverySourceChain, recoverySourceTx, provider, enqueueSnackbar]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecoverySignedVAA(currentSignedVAA);
|
setRecoverySignedVAA(currentSignedVAA);
|
||||||
}, [currentSignedVAA]);
|
}, [currentSignedVAA]);
|
||||||
|
@ -212,10 +256,10 @@ function RecoveryDialogContent({
|
||||||
setRecoverySourceChain(event.target.value);
|
setRecoverySourceChain(event.target.value);
|
||||||
}, []);
|
}, []);
|
||||||
const handleSourceTxChange = useCallback((event) => {
|
const handleSourceTxChange = useCallback((event) => {
|
||||||
setRecoverySourceTx(event.target.value);
|
setRecoverySourceTx(event.target.value.trim());
|
||||||
}, []);
|
}, []);
|
||||||
const handleSignedVAAChange = useCallback((event) => {
|
const handleSignedVAAChange = useCallback((event) => {
|
||||||
setRecoverySignedVAA(event.target.value);
|
setRecoverySignedVAA(event.target.value.trim());
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
@ -292,23 +336,45 @@ function RecoveryDialogContent({
|
||||||
<KeyAndBalance chainId={recoverySourceChain} />
|
<KeyAndBalance chainId={recoverySourceChain} />
|
||||||
) : null}
|
) : null}
|
||||||
<TextField
|
<TextField
|
||||||
label="Source Tx"
|
label="Source Tx (paste here)"
|
||||||
disabled={!!recoverySignedVAA}
|
disabled={!!recoverySignedVAA || recoverySourceTxIsLoading}
|
||||||
value={recoverySourceTx}
|
value={recoverySourceTx}
|
||||||
onChange={handleSourceTxChange}
|
onChange={handleSourceTxChange}
|
||||||
|
error={!!recoverySourceTxError}
|
||||||
|
helperText={recoverySourceTxError}
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<Box mt={4}>
|
<Box position="relative">
|
||||||
<Typography>or</Typography>
|
<Box mt={4}>
|
||||||
|
<Typography>or</Typography>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
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>
|
||||||
<TextField
|
|
||||||
label="Signed VAA (Hex)"
|
|
||||||
value={recoverySignedVAA || ""}
|
|
||||||
onChange={handleSignedVAAChange}
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Box my={4}>
|
<Box my={4}>
|
||||||
<Divider />
|
<Divider />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { reset } from "../../store/transferSlice";
|
import { reset } from "../../store/transferSlice";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import ShowTx from "../ShowTx";
|
import ShowTx from "../ShowTx";
|
||||||
|
import AddToMetamask from "./AddToMetamask";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
description: {
|
description: {
|
||||||
|
@ -37,6 +38,7 @@ export default function RedeemPreview() {
|
||||||
{explainerString}
|
{explainerString}
|
||||||
</Typography>
|
</Typography>
|
||||||
{redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null}
|
{redeemTx ? <ShowTx chainId={targetChain} tx={redeemTx} /> : null}
|
||||||
|
<AddToMetamask />
|
||||||
<ButtonWithLoader onClick={handleResetClick}>
|
<ButtonWithLoader onClick={handleResetClick}>
|
||||||
Transfer More Tokens!
|
Transfer More Tokens!
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import ButtonWithLoader from "../ButtonWithLoader";
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
|
import ShowTx from "../ShowTx";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
import TransactionProgress from "../TransactionProgress";
|
import TransactionProgress from "../TransactionProgress";
|
||||||
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
import WaitingForWalletMessage from "./WaitingForWalletMessage";
|
||||||
|
@ -111,7 +112,7 @@ function Send() {
|
||||||
Transfer the tokens to the Wormhole Token Bridge.
|
Transfer the tokens to the Wormhole Token Bridge.
|
||||||
</StepDescription>
|
</StepDescription>
|
||||||
<KeyAndBalance chainId={sourceChain} />
|
<KeyAndBalance chainId={sourceChain} />
|
||||||
<Alert severity="warning">
|
<Alert severity="info">
|
||||||
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
|
This will initiate the transfer on {CHAINS_BY_ID[sourceChain].name} and
|
||||||
wait for finalization. If you navigate away from this page before
|
wait for finalization. If you navigate away from this page before
|
||||||
completing Step 4, you will have to perform the recovery workflow to
|
completing Step 4, you will have to perform the recovery workflow to
|
||||||
|
@ -153,6 +154,7 @@ function Send() {
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
)}
|
)}
|
||||||
<WaitingForWalletMessage />
|
<WaitingForWalletMessage />
|
||||||
|
{transferTx ? <ShowTx chainId={sourceChain} tx={transferTx} /> : null}
|
||||||
<TransactionProgress
|
<TransactionProgress
|
||||||
chainId={sourceChain}
|
chainId={sourceChain}
|
||||||
tx={transferTx}
|
tx={transferTx}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
import useTokenBlacklistWarning from "../../hooks/useTokenBlacklistWarning";
|
|
||||||
import {
|
import {
|
||||||
selectTransferAmount,
|
selectTransferAmount,
|
||||||
selectTransferIsSourceComplete,
|
selectTransferIsSourceComplete,
|
||||||
|
@ -25,6 +24,7 @@ import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import StepDescription from "../StepDescription";
|
import StepDescription from "../StepDescription";
|
||||||
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
|
import { TokenSelector } from "../TokenSelectors/SourceTokenSelector";
|
||||||
|
import TokenBlacklistWarning from "./TokenBlacklistWarning";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
transferField: {
|
transferField: {
|
||||||
|
@ -55,10 +55,6 @@ function Source({
|
||||||
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
|
const isSourceComplete = useSelector(selectTransferIsSourceComplete);
|
||||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||||
const tokenBlacklistWarning = useTokenBlacklistWarning(
|
|
||||||
sourceChain,
|
|
||||||
parsedTokenAccount?.mintKey
|
|
||||||
);
|
|
||||||
const handleMigrationClick = useCallback(() => {
|
const handleMigrationClick = useCallback(() => {
|
||||||
parsedTokenAccount?.mintKey &&
|
parsedTokenAccount?.mintKey &&
|
||||||
history.push("/migrate/" + parsedTokenAccount.mintKey);
|
history.push("/migrate/" + parsedTokenAccount.mintKey);
|
||||||
|
@ -124,6 +120,11 @@ function Source({
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<TokenBlacklistWarning
|
||||||
|
sourceChain={sourceChain}
|
||||||
|
tokenAddress={parsedTokenAccount?.mintKey}
|
||||||
|
symbol={parsedTokenAccount?.symbol}
|
||||||
|
/>
|
||||||
{hasParsedTokenAccount ? (
|
{hasParsedTokenAccount ? (
|
||||||
<TextField
|
<TextField
|
||||||
label="Amount"
|
label="Amount"
|
||||||
|
@ -139,7 +140,7 @@ function Source({
|
||||||
disabled={!isSourceComplete}
|
disabled={!isSourceComplete}
|
||||||
onClick={handleNextClick}
|
onClick={handleNextClick}
|
||||||
showLoader={false}
|
showLoader={false}
|
||||||
error={statusMessage || error || tokenBlacklistWarning}
|
error={statusMessage || error}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</ButtonWithLoader>
|
</ButtonWithLoader>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from "../../store/selectors";
|
} from "../../store/selectors";
|
||||||
import { CHAINS_BY_ID } from "../../utils/consts";
|
import { CHAINS_BY_ID } from "../../utils/consts";
|
||||||
import { shortenAddress } from "../../utils/solana";
|
import { shortenAddress } from "../../utils/solana";
|
||||||
|
import TokenBlacklistWarning from "./TokenBlacklistWarning";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
description: {
|
description: {
|
||||||
|
@ -35,12 +36,19 @@ export default function SourcePreview() {
|
||||||
: "Step complete.";
|
: "Step complete.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography
|
<>
|
||||||
component="div"
|
<Typography
|
||||||
variant="subtitle2"
|
component="div"
|
||||||
className={classes.description}
|
variant="subtitle2"
|
||||||
>
|
className={classes.description}
|
||||||
{explainerString}
|
>
|
||||||
</Typography>
|
{explainerString}
|
||||||
|
</Typography>
|
||||||
|
<TokenBlacklistWarning
|
||||||
|
sourceChain={sourceChain}
|
||||||
|
tokenAddress={sourceParsedTokenAccount?.mintKey}
|
||||||
|
symbol={sourceParsedTokenAccount?.symbol}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ChainId } from "@certusone/wormhole-sdk";
|
||||||
|
import { Alert } from "@material-ui/lab";
|
||||||
|
import useTokenBlacklistWarning from "../../hooks/useTokenBlacklistWarning";
|
||||||
|
|
||||||
|
export default function TokenBlacklistWarning({
|
||||||
|
sourceChain,
|
||||||
|
tokenAddress,
|
||||||
|
symbol,
|
||||||
|
}: {
|
||||||
|
sourceChain: ChainId;
|
||||||
|
tokenAddress: string | undefined;
|
||||||
|
symbol: string | undefined;
|
||||||
|
}) {
|
||||||
|
const tokenBlacklistWarning = useTokenBlacklistWarning(
|
||||||
|
sourceChain,
|
||||||
|
tokenAddress,
|
||||||
|
symbol
|
||||||
|
);
|
||||||
|
return tokenBlacklistWarning ? (
|
||||||
|
<Alert severity="warning">{tokenBlacklistWarning}</Alert>
|
||||||
|
) : null;
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ import {
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Signer } from "../../../sdk/js/node_modules/ethers/lib";
|
import { Signer } from "ethers";
|
||||||
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -11,7 +11,8 @@ import {
|
||||||
|
|
||||||
export default function useTokenBlacklistWarning(
|
export default function useTokenBlacklistWarning(
|
||||||
chainId: ChainId,
|
chainId: ChainId,
|
||||||
tokenAddress: string | undefined
|
tokenAddress: string | undefined,
|
||||||
|
symbol: string | undefined
|
||||||
) {
|
) {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -20,8 +21,12 @@ export default function useTokenBlacklistWarning(
|
||||||
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)) ||
|
SOLANA_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)) ||
|
||||||
(chainId === CHAIN_ID_ETH &&
|
(chainId === CHAIN_ID_ETH &&
|
||||||
ETH_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)))
|
ETH_TOKENS_THAT_EXIST_ELSEWHERE.includes(tokenAddress)))
|
||||||
? "This token exists on multiple chains! Bridging the token via Wormhole will produce a wrapped version which might have no liquidity on the target chain."
|
? `Bridging ${
|
||||||
|
symbol ? symbol : "the token"
|
||||||
|
} via Wormhole will not produce native ${
|
||||||
|
symbol ? symbol : "assets"
|
||||||
|
}. It will produce a wrapped version which might have no liquidity or utility on the target chain.`
|
||||||
: undefined,
|
: undefined,
|
||||||
[chainId, tokenAddress]
|
[chainId, tokenAddress, symbol]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,11 +197,12 @@ export const SOLANA_TOKENS_THAT_EXIST_ELSEWHERE = [
|
||||||
"ArUkYE2XDKzqy77PRRGjo4wREWwqk6RXTfM9NeqzPvjU", // renDOGE
|
"ArUkYE2XDKzqy77PRRGjo4wREWwqk6RXTfM9NeqzPvjU", // renDOGE
|
||||||
"E99CQ2gFMmbiyK2bwiaFNWUUmwz4r8k2CVEFxwuvQ7ue", // renZEC
|
"E99CQ2gFMmbiyK2bwiaFNWUUmwz4r8k2CVEFxwuvQ7ue", // renZEC
|
||||||
"De2bU64vsXKU9jq4bCjeDxNRGPn8nr3euaTK8jBYmD3J", // renFIL
|
"De2bU64vsXKU9jq4bCjeDxNRGPn8nr3euaTK8jBYmD3J", // renFIL
|
||||||
|
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", // USDT
|
||||||
];
|
];
|
||||||
export const ETH_TOKENS_THAT_EXIST_ELSEWHERE = [
|
export const ETH_TOKENS_THAT_EXIST_ELSEWHERE = [
|
||||||
getAddress("0x476c5E26a75bd202a9683ffD34359C0CC15be0fF"), // SRM
|
getAddress("0x476c5E26a75bd202a9683ffD34359C0CC15be0fF"), // SRM
|
||||||
getAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC
|
getAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), // USDC
|
||||||
getAddress("0x818fc6c2ec5986bc6e2cbf00939d90556ab12ce5"), // KIN
|
getAddress("0x818fc6c2ec5986bc6e2cbf00939d90556ab12ce5"), // KIN
|
||||||
getAddress("0xeb4c2781e4eba804ce9a9803c67d0893436bb27d"), // renBTC
|
getAddress("0xeb4c2781e4eba804ce9a9803c67d0893436bb27d"), // renBTC
|
||||||
getAddress("0x52d87F22192131636F93c5AB18d0127Ea52CB641"), // renLUNA
|
getAddress("0x52d87F22192131636F93c5AB18d0127Ea52CB641"), // renLUNA
|
||||||
getAddress("0x459086f2376525bdceba5bdda135e4e9d3fef5bf"), // renBCH
|
getAddress("0x459086f2376525bdceba5bdda135e4e9d3fef5bf"), // renBCH
|
||||||
|
@ -209,6 +210,7 @@ export const ETH_TOKENS_THAT_EXIST_ELSEWHERE = [
|
||||||
getAddress("0x3832d2F059E55934220881F831bE501D180671A7"), // renDOGE
|
getAddress("0x3832d2F059E55934220881F831bE501D180671A7"), // renDOGE
|
||||||
getAddress("0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2"), // renZEC
|
getAddress("0x1c5db575e2ff833e46a2e9864c22f4b22e0b37c2"), // renZEC
|
||||||
getAddress("0xD5147bc8e386d91Cc5DBE72099DAC6C9b99276F5"), // renFIL
|
getAddress("0xD5147bc8e386d91Cc5DBE72099DAC6C9b99276F5"), // renFIL
|
||||||
|
getAddress("0xdac17f958d2ee523a2206206994597c13d831ec7"), // USDT
|
||||||
];
|
];
|
||||||
|
|
||||||
export const MIGRATION_PROGRAM_ADDRESS =
|
export const MIGRATION_PROGRAM_ADDRESS =
|
||||||
|
|
|
@ -9,10 +9,13 @@ export const getNextRpcHost = () =>
|
||||||
export async function getSignedVAAWithRetry(
|
export async function getSignedVAAWithRetry(
|
||||||
emitterChain: ChainId,
|
emitterChain: ChainId,
|
||||||
emitterAddress: string,
|
emitterAddress: string,
|
||||||
sequence: string
|
sequence: string,
|
||||||
|
retryAttempts?: number
|
||||||
) {
|
) {
|
||||||
let result;
|
let result;
|
||||||
|
let attempts = 0;
|
||||||
while (!result) {
|
while (!result) {
|
||||||
|
attempts++;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
try {
|
try {
|
||||||
result = await getSignedVAA(
|
result = await getSignedVAA(
|
||||||
|
@ -22,7 +25,10 @@ export async function getSignedVAAWithRetry(
|
||||||
sequence
|
sequence
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(`Attempt ${attempts}: `, e);
|
||||||
|
if (retryAttempts !== undefined && attempts > retryAttempts) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
Loading…
Reference in New Issue