bridge_ui: ethereum migration functions
Change-Id: I39d12adcdfd5036283572f006a1442a26a3fc143
This commit is contained in:
parent
363d83f9c6
commit
7f83984410
|
@ -24,6 +24,8 @@ import NFTOriginVerifier from "./components/NFTOriginVerifier";
|
|||
import Transfer from "./components/Transfer";
|
||||
import wormholeLogo from "./icons/wormhole.svg";
|
||||
import { CLUSTER } from "./utils/consts";
|
||||
import EthereumQuickMigrate from "./components/Migration/EthereumQuickMigrate";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
|
@ -185,8 +187,14 @@ function App() {
|
|||
<Route exact path="/register">
|
||||
<Attest />
|
||||
</Route>
|
||||
<Route exact path="/migrate/:legacyAsset/:fromTokenAccount">
|
||||
<Migration />
|
||||
<Route exact path="/migrate/Solana/:legacyAsset/:fromTokenAccount">
|
||||
<Migration chainId={CHAIN_ID_SOLANA} />
|
||||
</Route>
|
||||
<Route exact path="/migrate/Ethereum/:legacyAsset/">
|
||||
<Migration chainId={CHAIN_ID_ETH} />
|
||||
</Route>
|
||||
<Route exact path="/migrate/Ethereum/">
|
||||
<EthereumQuickMigrate />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
|
|
|
@ -0,0 +1,366 @@
|
|||
import {
|
||||
CHAIN_ID_ETH,
|
||||
TokenImplementation__factory,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Signer } from "@ethersproject/abstract-signer";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import {
|
||||
CircularProgress,
|
||||
Container,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import ArrowRightAltIcon from "@material-ui/icons/ArrowRightAlt";
|
||||
import { parseUnits } from "ethers/lib/utils";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import useEthereumMigratorInformation from "../../hooks/useEthereumMigratorInformation";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import { COLORS } from "../../muiTheme";
|
||||
import { ETH_MIGRATION_ASSET_MAP } from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import EthereumSignerKey from "../EthereumSignerKey";
|
||||
import ShowTx from "../ShowTx";
|
||||
import SmartAddress from "../SmartAddress";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
spacer: {
|
||||
height: "2rem",
|
||||
},
|
||||
containerDiv: {
|
||||
textAlign: "center",
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
lineItem: {
|
||||
display: "flex",
|
||||
flexWrap: "nowrap",
|
||||
justifyContent: "space-between",
|
||||
"& > *": {
|
||||
alignSelf: "flex-start",
|
||||
width: "max-content",
|
||||
},
|
||||
},
|
||||
flexGrow: {
|
||||
flewGrow: 1,
|
||||
},
|
||||
mainPaper: {
|
||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||
textAlign: "center",
|
||||
padding: "2rem",
|
||||
"& > h, p ": {
|
||||
margin: ".5rem",
|
||||
},
|
||||
},
|
||||
hidden: {
|
||||
display: "none",
|
||||
},
|
||||
divider: {
|
||||
margin: "2rem 0rem 2rem 0rem",
|
||||
},
|
||||
balance: {
|
||||
display: "inline-block",
|
||||
},
|
||||
convertButton: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
}));
|
||||
|
||||
//TODO move elsewhere
|
||||
export const compareWithDecimalOffset = (
|
||||
valueA: string,
|
||||
decimalsA: number,
|
||||
valueB: string,
|
||||
decimalsB: number
|
||||
) => {
|
||||
//find which is larger, and offset by that amount
|
||||
const decimalsBasis = decimalsA > decimalsB ? decimalsA : decimalsB;
|
||||
const normalizedA = parseUnits(valueA, decimalsBasis).toBigInt();
|
||||
const normalizedB = parseUnits(valueB, decimalsBasis).toBigInt();
|
||||
|
||||
if (normalizedA < normalizedB) {
|
||||
return -1;
|
||||
} else if (normalizedA === normalizedB) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
function EthereumMigrationLineItem({
|
||||
migratorAddress,
|
||||
onLoadComplete,
|
||||
}: {
|
||||
migratorAddress: string;
|
||||
onLoadComplete: () => void;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { signer, signerAddress } = useEthereumProvider();
|
||||
const poolInfo = useEthereumMigratorInformation(
|
||||
migratorAddress,
|
||||
signer,
|
||||
signerAddress,
|
||||
false
|
||||
);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
|
||||
const [transaction, setTransaction] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const sufficientPoolBalance =
|
||||
poolInfo.data &&
|
||||
compareWithDecimalOffset(
|
||||
poolInfo.data.fromWalletBalance,
|
||||
poolInfo.data.fromDecimals,
|
||||
poolInfo.data.toPoolBalance,
|
||||
poolInfo.data.toDecimals
|
||||
) !== 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded && (poolInfo.data || poolInfo.error)) {
|
||||
console.log("mounted & finished loading");
|
||||
onLoadComplete();
|
||||
setLoaded(true);
|
||||
}
|
||||
}, [loaded, poolInfo, onLoadComplete]);
|
||||
|
||||
//TODO use transaction loader
|
||||
const migrateTokens = useCallback(async () => {
|
||||
if (!poolInfo.data) {
|
||||
enqueueSnackbar("Could not migrate the tokens.", { variant: "error" }); //Should never be hit
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const migrationAmountAbs = parseUnits(
|
||||
poolInfo.data.fromWalletBalance,
|
||||
poolInfo.data.fromDecimals
|
||||
);
|
||||
setMigrationIsProcessing(true);
|
||||
await poolInfo.data.fromToken.approve(
|
||||
poolInfo.data.migrator.address,
|
||||
migrationAmountAbs
|
||||
);
|
||||
const transaction = await poolInfo.data.migrator.migrate(
|
||||
migrationAmountAbs
|
||||
);
|
||||
await transaction.wait();
|
||||
setTransaction(transaction.hash);
|
||||
enqueueSnackbar(`Successfully migrated the tokens.`, {
|
||||
variant: "success",
|
||||
});
|
||||
setMigrationIsProcessing(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
enqueueSnackbar("Could not migrate the tokens.", { variant: "error" });
|
||||
setMigrationIsProcessing(false);
|
||||
setError("Failed to send the transaction.");
|
||||
}
|
||||
}, [poolInfo.data, enqueueSnackbar]);
|
||||
|
||||
if (!poolInfo.data) {
|
||||
return null;
|
||||
} else if (transaction) {
|
||||
return (
|
||||
<div className={classes.lineItem}>
|
||||
<div>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Successfully migrated your tokens. They will become available once
|
||||
this transaction confirms.
|
||||
</Typography>
|
||||
<ShowTx chainId={CHAIN_ID_ETH} tx={{ id: transaction, block: 1 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={classes.lineItem}>
|
||||
<div>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Current Token
|
||||
</Typography>
|
||||
<Typography className={classes.balance}>
|
||||
{poolInfo.data.fromWalletBalance}
|
||||
</Typography>
|
||||
<SmartAddress
|
||||
chainId={CHAIN_ID_ETH}
|
||||
address={poolInfo.data.fromAddress}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
will become
|
||||
</Typography>
|
||||
<ArrowRightAltIcon fontSize="large" />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Wormhole Token
|
||||
</Typography>
|
||||
<Typography className={classes.balance}>
|
||||
{poolInfo.data.fromWalletBalance}
|
||||
</Typography>
|
||||
<SmartAddress
|
||||
chainId={CHAIN_ID_ETH}
|
||||
address={poolInfo.data.toAddress}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.convertButton}>
|
||||
<ButtonWithLoader
|
||||
showLoader={migrationIsProcessing}
|
||||
onClick={migrateTokens}
|
||||
error={
|
||||
error
|
||||
? error
|
||||
: !sufficientPoolBalance
|
||||
? "The swap pool has insufficient funds."
|
||||
: ""
|
||||
}
|
||||
disabled={!sufficientPoolBalance}
|
||||
>
|
||||
Convert
|
||||
</ButtonWithLoader>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getAddressBalances = async (
|
||||
signer: Signer,
|
||||
signerAddress: string,
|
||||
addresses: string[]
|
||||
): Promise<Map<string, BigNumber | null>> => {
|
||||
try {
|
||||
const promises: Promise<any>[] = [];
|
||||
const output = new Map<string, BigNumber | null>();
|
||||
addresses.forEach((address) => {
|
||||
const factory = TokenImplementation__factory.connect(address, signer);
|
||||
promises.push(
|
||||
factory.balanceOf(signerAddress).then(
|
||||
(result) => {
|
||||
output.set(address, result);
|
||||
},
|
||||
(error) => {
|
||||
output.set(address, null);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
return output;
|
||||
} catch (e) {
|
||||
return Promise.reject("Unable to retrieve token balances.");
|
||||
}
|
||||
};
|
||||
|
||||
export default function EthereumQuickMigrate() {
|
||||
const classes = useStyles();
|
||||
const { signer, signerAddress } = useEthereumProvider();
|
||||
const { isReady } = useIsWalletReady(CHAIN_ID_ETH);
|
||||
const eligibleTokens = useMemo(
|
||||
() => Array.from(ETH_MIGRATION_ASSET_MAP.keys()),
|
||||
[]
|
||||
);
|
||||
const [migrators, setMigrators] = useState<string[] | null>(null);
|
||||
const [migratorsError, setMigratorsError] = useState("");
|
||||
const [migratorsLoading, setMigratorsLoading] = useState(false);
|
||||
|
||||
//This is for a callback into the line items, so a loader can be displayed while
|
||||
//they are loading
|
||||
//TODO don't just swallow loading errors.
|
||||
const [migratorsFinishedLoading, setMigratorsFinishedLoading] = useState(0);
|
||||
const reportLoadComplete = useCallback(() => {
|
||||
setMigratorsFinishedLoading((prevState) => prevState + 1);
|
||||
}, []);
|
||||
const isLoading =
|
||||
migratorsLoading ||
|
||||
(migrators &&
|
||||
migrators.length &&
|
||||
migratorsFinishedLoading < migrators.length);
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && signer && signerAddress) {
|
||||
let cancelled = false;
|
||||
setMigratorsLoading(true);
|
||||
setMigratorsError("");
|
||||
getAddressBalances(signer, signerAddress, eligibleTokens).then(
|
||||
(result) => {
|
||||
if (!cancelled) {
|
||||
const migratorAddresses = [];
|
||||
for (const tokenAddress of result.keys()) {
|
||||
if (result.get(tokenAddress) && result.get(tokenAddress)?.gt(0)) {
|
||||
const migratorAddress =
|
||||
ETH_MIGRATION_ASSET_MAP.get(tokenAddress);
|
||||
if (migratorAddress) {
|
||||
migratorAddresses.push(migratorAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
setMigratorsFinishedLoading(0);
|
||||
setMigrators(migratorAddresses);
|
||||
setMigratorsLoading(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
setMigratorsLoading(false);
|
||||
setMigratorsError(
|
||||
"Failed to retrieve available token information."
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
}, [isReady, signer, signerAddress, eligibleTokens]);
|
||||
|
||||
const hasEligibleAssets = migrators && migrators.length > 0;
|
||||
|
||||
const content = (
|
||||
<div className={classes.containerDiv}>
|
||||
<Typography variant="h5">
|
||||
This page allows you to convert certain wrapped tokens on Ethereum into
|
||||
Wormhole V2 tokens.
|
||||
</Typography>
|
||||
<EthereumSignerKey />
|
||||
{!isReady ? (
|
||||
<Typography variant="body1">Please connect your wallet.</Typography>
|
||||
) : migratorsError ? (
|
||||
<Typography variant="h6">{migratorsError}</Typography>
|
||||
) : (
|
||||
<>
|
||||
<div className={classes.spacer} />
|
||||
<CircularProgress className={isLoading ? "" : classes.hidden} />
|
||||
<div className={!isLoading ? "" : classes.hidden}>
|
||||
<Typography>
|
||||
{hasEligibleAssets
|
||||
? "You have some assets that are eligible for migration! Click the 'Convert' button to swap them for Wormhole tokens."
|
||||
: "You don't have any assets eligible for migration."}
|
||||
</Typography>
|
||||
<div className={classes.spacer} />
|
||||
{migrators?.map((address) => {
|
||||
return (
|
||||
<EthereumMigrationLineItem
|
||||
key={address}
|
||||
migratorAddress={address}
|
||||
onLoadComplete={reportLoadComplete}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Paper className={classes.mainPaper}>{content}</Paper>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
|
||||
import {
|
||||
CircularProgress,
|
||||
makeStyles,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { parseUnits } from "ethers/lib/utils";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import useEthereumMigratorInformation from "../../hooks/useEthereumMigratorInformation";
|
||||
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import EthereumSignerKey from "../EthereumSignerKey";
|
||||
import ShowTx from "../ShowTx";
|
||||
import SmartAddress from "../SmartAddress";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
spacer: {
|
||||
height: "2rem",
|
||||
},
|
||||
containerDiv: {
|
||||
textAlign: "center",
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function EthereumWorkflow({
|
||||
migratorAddress,
|
||||
}: {
|
||||
migratorAddress: string;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { signer, signerAddress } = useEthereumProvider();
|
||||
const { isReady } = useIsWalletReady(CHAIN_ID_ETH);
|
||||
const [toggleRefresh, setToggleRefresh] = useState(false);
|
||||
const forceRefresh = useCallback(
|
||||
() => setToggleRefresh((prevState) => !prevState),
|
||||
[]
|
||||
);
|
||||
const poolInfo = useEthereumMigratorInformation(
|
||||
migratorAddress,
|
||||
signer,
|
||||
signerAddress,
|
||||
toggleRefresh
|
||||
);
|
||||
|
||||
const [migrationAmount, setMigrationAmount] = useState("");
|
||||
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [transaction, setTransaction] = useState<string | null>(null);
|
||||
|
||||
const fromParse = (amount: string) => {
|
||||
try {
|
||||
if (!poolInfo.data?.fromDecimals || !migrationAmount) {
|
||||
return BigInt(0);
|
||||
}
|
||||
return parseUnits(amount, poolInfo.data.fromDecimals).toBigInt();
|
||||
} catch (e) {
|
||||
return BigInt(0);
|
||||
}
|
||||
};
|
||||
|
||||
const hasRequisiteData = poolInfo.data;
|
||||
const amountGreaterThanZero = fromParse(migrationAmount) > BigInt(0);
|
||||
const sufficientFromTokens =
|
||||
poolInfo.data?.fromWalletBalance &&
|
||||
migrationAmount &&
|
||||
fromParse(migrationAmount) <= fromParse(poolInfo.data.fromWalletBalance);
|
||||
const sufficientPoolBalance =
|
||||
poolInfo.data?.toPoolBalance &&
|
||||
migrationAmount &&
|
||||
parseFloat(migrationAmount) <= parseFloat(poolInfo.data.toPoolBalance);
|
||||
|
||||
const isReadyToTransfer =
|
||||
isReady &&
|
||||
amountGreaterThanZero &&
|
||||
sufficientFromTokens &&
|
||||
sufficientPoolBalance &&
|
||||
hasRequisiteData;
|
||||
|
||||
const getNotReadyCause = () => {
|
||||
if (!isReady) {
|
||||
return "Connect your wallet to proceed.";
|
||||
} else if (poolInfo.error) {
|
||||
return "Unable to retrieve necessary information. This asset may not be supported.";
|
||||
} else if (!migrationAmount) {
|
||||
return "Enter an amount to transfer.";
|
||||
} else if (!amountGreaterThanZero) {
|
||||
return "The transfer amount must be greater than zero.";
|
||||
} else if (!sufficientFromTokens) {
|
||||
return "There are not sufficient funds in your wallet for this transfer.";
|
||||
} else if (!sufficientPoolBalance) {
|
||||
return "There are not sufficient funds in the pool for this transfer.";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleAmountChange = useCallback(
|
||||
(event) => setMigrationAmount(event.target.value),
|
||||
[setMigrationAmount]
|
||||
);
|
||||
|
||||
const migrateTokens = useCallback(async () => {
|
||||
if (!poolInfo.data) {
|
||||
enqueueSnackbar("Could not migrate the tokens.", { variant: "error" }); //Should never be hit
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setMigrationIsProcessing(true);
|
||||
setError("");
|
||||
await poolInfo.data.fromToken.approve(
|
||||
poolInfo.data.migrator.address,
|
||||
parseUnits(migrationAmount, poolInfo.data.fromDecimals)
|
||||
);
|
||||
const transaction = await poolInfo.data.migrator.migrate(
|
||||
parseUnits(migrationAmount, poolInfo.data.fromDecimals)
|
||||
);
|
||||
await transaction.wait();
|
||||
setTransaction(transaction.hash);
|
||||
forceRefresh();
|
||||
enqueueSnackbar(`Successfully migrated the tokens.`, {
|
||||
variant: "success",
|
||||
});
|
||||
setMigrationIsProcessing(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
enqueueSnackbar("Could not migrate the tokens.", { variant: "error" });
|
||||
setMigrationIsProcessing(false);
|
||||
setError("Failed to send the transaction.");
|
||||
}
|
||||
}, [poolInfo.data, migrationAmount, enqueueSnackbar, forceRefresh]);
|
||||
|
||||
//TODO tokenName
|
||||
const toTokenPretty = (
|
||||
<SmartAddress
|
||||
chainId={CHAIN_ID_ETH}
|
||||
address={poolInfo.data?.toAddress}
|
||||
symbol={poolInfo.data?.toSymbol}
|
||||
/>
|
||||
);
|
||||
const fromTokenPretty = (
|
||||
<SmartAddress
|
||||
chainId={CHAIN_ID_ETH}
|
||||
address={poolInfo.data?.fromAddress}
|
||||
symbol={poolInfo.data?.fromSymbol}
|
||||
/>
|
||||
);
|
||||
const poolPretty = (
|
||||
<SmartAddress chainId={CHAIN_ID_ETH} address={poolInfo.data?.poolAddress} />
|
||||
);
|
||||
|
||||
const fatalError = poolInfo.error
|
||||
? "Unable to retrieve necessary information. This asset may not be supported."
|
||||
: null;
|
||||
|
||||
const explainerContent = (
|
||||
<div>
|
||||
<Typography>This action will convert</Typography>
|
||||
<Typography variant="h6">
|
||||
{fromTokenPretty}{" "}
|
||||
{`(Balance: ${poolInfo.data?.fromWalletBalance || ""})`}
|
||||
</Typography>
|
||||
<div className={classes.spacer} />
|
||||
<Typography>to</Typography>
|
||||
<Typography variant="h6">
|
||||
{toTokenPretty} {`(Balance: ${poolInfo.data?.toWalletBalance || ""})`}
|
||||
</Typography>
|
||||
<div className={classes.spacer} />
|
||||
<Typography>Utilizing this pool</Typography>
|
||||
<Typography variant="h6">
|
||||
{poolPretty} {`(Balance: ${poolInfo.data?.toPoolBalance || ""})`}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
const mainWorkflow = (
|
||||
<>
|
||||
{explainerContent}
|
||||
<div className={classes.spacer} />
|
||||
<TextField
|
||||
value={migrationAmount}
|
||||
type="number"
|
||||
onChange={handleAmountChange}
|
||||
label={"Amount"}
|
||||
disabled={!!migrationIsProcessing || !!transaction}
|
||||
></TextField>
|
||||
|
||||
{!transaction && (
|
||||
<ButtonWithLoader
|
||||
disabled={!isReadyToTransfer || migrationIsProcessing}
|
||||
showLoader={migrationIsProcessing}
|
||||
onClick={migrateTokens}
|
||||
>
|
||||
{migrationAmount && isReadyToTransfer
|
||||
? "Migrate " + migrationAmount + " Tokens"
|
||||
: "Migrate"}
|
||||
</ButtonWithLoader>
|
||||
)}
|
||||
|
||||
{(error || !isReadyToTransfer) && (
|
||||
<Typography color="error">{error || getNotReadyCause()}</Typography>
|
||||
)}
|
||||
{transaction ? (
|
||||
<>
|
||||
<Typography>
|
||||
Successfully migrated your tokens! They will be available once this
|
||||
transaction confirms.
|
||||
</Typography>
|
||||
<ShowTx tx={{ id: transaction, block: 1 }} chainId={CHAIN_ID_ETH} />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.containerDiv}>
|
||||
<EthereumSignerKey />
|
||||
{!isReady ? (
|
||||
<Typography variant="body1">Please connect your wallet.</Typography>
|
||||
) : poolInfo.isLoading ? (
|
||||
<CircularProgress />
|
||||
) : fatalError ? (
|
||||
<Typography variant="h6">{fatalError}</Typography>
|
||||
) : (
|
||||
mainWorkflow
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -2,14 +2,7 @@ import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
|||
import migrateTokensTx from "@certusone/wormhole-sdk/lib/migration/migrateTokens";
|
||||
import getPoolAddress from "@certusone/wormhole-sdk/lib/migration/poolAddress";
|
||||
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
|
||||
import {
|
||||
Container,
|
||||
Divider,
|
||||
makeStyles,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { makeStyles, TextField, Typography } from "@material-ui/core";
|
||||
import {
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
Token,
|
||||
|
@ -400,13 +393,7 @@ export default function Workflow({
|
|||
);
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Paper className={classes.mainPaper}>
|
||||
<Typography variant="h5">Migrate Legacy Assets</Typography>
|
||||
<Typography variant="subtitle2">
|
||||
Convert assets from legacy bridges to Wormhole V2 tokens
|
||||
</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<div>
|
||||
<SolanaWalletKey />
|
||||
<div className={classes.spacer} />
|
||||
{fromTokenAccount && toTokenAccount ? (
|
||||
|
@ -435,10 +422,7 @@ export default function Workflow({
|
|||
variant="h5"
|
||||
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
|
||||
>
|
||||
<SmartAddress
|
||||
address={toTokenAccount}
|
||||
chainId={CHAIN_ID_SOLANA}
|
||||
/>
|
||||
<SmartAddress address={toTokenAccount} chainId={CHAIN_ID_SOLANA} />
|
||||
<span>
|
||||
{toTokenAccountExists
|
||||
? ` (Balance: ${toTokenAccountBalance}${
|
||||
|
@ -458,10 +442,7 @@ export default function Workflow({
|
|||
<div className={classes.spacer} />
|
||||
<Typography variant="body2" component="div">
|
||||
<span>Using pool </span>
|
||||
<SmartAddress
|
||||
address={poolAddress}
|
||||
chainId={CHAIN_ID_SOLANA}
|
||||
/>
|
||||
<SmartAddress address={poolAddress} chainId={CHAIN_ID_SOLANA} />
|
||||
<span> holding tokens in this account:</span>
|
||||
</Typography>
|
||||
<Typography variant="h5">
|
||||
|
@ -503,8 +484,8 @@ export default function Workflow({
|
|||
{transaction ? (
|
||||
<>
|
||||
<Typography>
|
||||
Successfully migrated your tokens! They will be available once
|
||||
this transaction confirms.
|
||||
Successfully migrated your tokens! They will be available once this
|
||||
transaction confirms.
|
||||
</Typography>
|
||||
<ShowTx
|
||||
tx={{ id: transaction, block: 1 }}
|
||||
|
@ -512,7 +493,6 @@ export default function Workflow({
|
|||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Paper>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,18 +1,53 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import {
|
||||
Container,
|
||||
Divider,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { MIGRATION_ASSET_MAP } from "../../utils/consts";
|
||||
import Workflow from "./Workflow";
|
||||
import {
|
||||
ETH_MIGRATION_ASSET_MAP,
|
||||
MIGRATION_ASSET_MAP,
|
||||
} from "../../utils/consts";
|
||||
import SolanaWorkflow from "./SolanaWorkflow";
|
||||
import { withRouter } from "react-router";
|
||||
import { COLORS } from "../../muiTheme";
|
||||
import {
|
||||
ChainId,
|
||||
CHAIN_ID_ETH,
|
||||
CHAIN_ID_SOLANA,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import EthereumWorkflow from "./EthereumWorkflow";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
mainPaper: {
|
||||
backgroundColor: COLORS.nearBlackWithMinorTransparency,
|
||||
textAlign: "center",
|
||||
padding: "2rem",
|
||||
"& > h, p ": {
|
||||
margin: ".5rem",
|
||||
},
|
||||
},
|
||||
divider: {
|
||||
margin: "2rem 0rem 2rem 0rem",
|
||||
},
|
||||
spacer: {
|
||||
height: "2rem",
|
||||
},
|
||||
}));
|
||||
|
||||
interface RouteParams {
|
||||
legacyAsset: string;
|
||||
fromTokenAccount: string;
|
||||
}
|
||||
|
||||
interface Migration extends RouteComponentProps<RouteParams> {}
|
||||
interface Migration extends RouteComponentProps<RouteParams> {
|
||||
chainId: ChainId;
|
||||
}
|
||||
|
||||
const MigrationRoot: React.FC<Migration> = (props) => {
|
||||
const SolanaRoot: React.FC<Migration> = (props) => {
|
||||
const legacyAsset: string = props.match.params.legacyAsset;
|
||||
const fromTokenAccount: string = props.match.params.fromTokenAccount;
|
||||
const targetAsset: string | undefined = MIGRATION_ASSET_MAP.get(legacyAsset);
|
||||
|
@ -27,27 +62,73 @@ const MigrationRoot: React.FC<Migration> = (props) => {
|
|||
fromTokenAccount && new PublicKey(fromTokenAccount).toString();
|
||||
} catch (e) {}
|
||||
|
||||
let content = null;
|
||||
|
||||
if (!fromMint || !toMint) {
|
||||
return (
|
||||
content = (
|
||||
<Typography style={{ textAlign: "center" }}>
|
||||
This asset is not eligible for migration.
|
||||
</Typography>
|
||||
);
|
||||
} else if (!fromTokenAcct) {
|
||||
return (
|
||||
content = (
|
||||
<Typography style={{ textAlign: "center" }}>
|
||||
Invalid token account.
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Workflow
|
||||
content = (
|
||||
<SolanaWorkflow
|
||||
fromMint={fromMint}
|
||||
toMint={toMint}
|
||||
fromTokenAccount={fromTokenAcct}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const EthereumRoot: React.FC<Migration> = (props) => {
|
||||
const legacyAsset: string = props.match.params.legacyAsset;
|
||||
const targetPool = ETH_MIGRATION_ASSET_MAP.get(legacyAsset);
|
||||
|
||||
let content = null;
|
||||
if (!legacyAsset || !targetPool) {
|
||||
content = (
|
||||
<Typography style={{ textAlign: "center" }}>
|
||||
This asset is not eligible for migration.
|
||||
</Typography>
|
||||
);
|
||||
} else {
|
||||
content = <EthereumWorkflow migratorAddress={targetPool} />;
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const MigrationRoot: React.FC<Migration> = (props) => {
|
||||
const classes = useStyles();
|
||||
let content = null;
|
||||
|
||||
if (props.chainId === CHAIN_ID_SOLANA) {
|
||||
content = <SolanaRoot {...props} />;
|
||||
} else if (props.chainId === CHAIN_ID_ETH) {
|
||||
content = <EthereumRoot {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Paper className={classes.mainPaper}>
|
||||
<Typography variant="h5">Migrate Assets</Typography>
|
||||
<Typography variant="subtitle2">
|
||||
Convert assets from other bridges to Wormhole V2 tokens
|
||||
</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
{content}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(MigrationRoot);
|
||||
|
|
|
@ -6,13 +6,16 @@ import {
|
|||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Autocomplete, createFilterOptions } from "@material-ui/lab";
|
||||
import { Alert, Autocomplete, createFilterOptions } from "@material-ui/lab";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||
import { CovalentData } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||
import { DataWrapper } from "../../store/helpers";
|
||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
||||
import { WORMHOLE_V1_ETH_ADDRESS } from "../../utils/consts";
|
||||
import {
|
||||
ETH_MIGRATION_ASSET_MAP,
|
||||
WORMHOLE_V1_ETH_ADDRESS,
|
||||
} from "../../utils/consts";
|
||||
import {
|
||||
ethNFTToNFTParsedTokenAccount,
|
||||
ethTokenToParsedTokenAccount,
|
||||
|
@ -57,6 +60,12 @@ const useStyles = makeStyles((theme) =>
|
|||
tokenImage: {
|
||||
maxHeight: "2.5rem", //Eyeballing this based off the text size
|
||||
},
|
||||
migrationAlert: {
|
||||
width: "100%",
|
||||
"& .MuiAlert-message": {
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -82,6 +91,10 @@ const isWormholev1 = (provider: any, address: string) => {
|
|||
return connection.isWrappedAsset(address);
|
||||
};
|
||||
|
||||
const isMigrationEligible = (address: string) => {
|
||||
return !!ETH_MIGRATION_ASSET_MAP.get(address);
|
||||
};
|
||||
|
||||
type EthereumSourceTokenSelectorProps = {
|
||||
value: ParsedTokenAccount | null;
|
||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||
|
@ -100,7 +113,7 @@ const renderAccount = (
|
|||
const mintPrettyString = shortenAddress(account.mintKey);
|
||||
const uri = getLogo(account);
|
||||
const symbol = getSymbol(account) || "Unknown";
|
||||
return (
|
||||
const content = (
|
||||
<div className={classes.tokenOverviewContainer}>
|
||||
<div className={classes.tokenImageContainer}>
|
||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||
|
@ -121,6 +134,19 @@ const renderAccount = (
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const migrationRender = (
|
||||
<div className={classes.migrationAlert}>
|
||||
<Alert severity="warning">
|
||||
<Typography variant="body2">
|
||||
This is a legacy asset eligible for migration.
|
||||
</Typography>
|
||||
<div>{content}</div>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
|
||||
return isMigrationEligible(account.mintKey) ? migrationRender : content;
|
||||
};
|
||||
|
||||
const renderNFTAccount = (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||
import { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||
import { Restore } from "@material-ui/icons";
|
||||
import { useCallback } from "react";
|
||||
|
@ -19,7 +19,11 @@ import {
|
|||
setAmount,
|
||||
setSourceChain,
|
||||
} from "../../store/transferSlice";
|
||||
import { CHAINS, MIGRATION_ASSET_MAP } from "../../utils/consts";
|
||||
import {
|
||||
CHAINS,
|
||||
ETH_MIGRATION_ASSET_MAP,
|
||||
MIGRATION_ASSET_MAP,
|
||||
} from "../../utils/consts";
|
||||
import ButtonWithLoader from "../ButtonWithLoader";
|
||||
import KeyAndBalance from "../KeyAndBalance";
|
||||
import LowBalanceWarning from "../LowBalanceWarning";
|
||||
|
@ -46,10 +50,15 @@ function Source({
|
|||
selectTransferSourceParsedTokenAccount
|
||||
);
|
||||
const hasParsedTokenAccount = !!parsedTokenAccount;
|
||||
const isMigrationAsset =
|
||||
const isSolanaMigration =
|
||||
sourceChain === CHAIN_ID_SOLANA &&
|
||||
!!parsedTokenAccount &&
|
||||
!!MIGRATION_ASSET_MAP.get(parsedTokenAccount.mintKey);
|
||||
const isEthereumMigration =
|
||||
sourceChain === CHAIN_ID_ETH &&
|
||||
!!parsedTokenAccount &&
|
||||
!!ETH_MIGRATION_ASSET_MAP.get(parsedTokenAccount.mintKey);
|
||||
const isMigrationAsset = isSolanaMigration || isEthereumMigration;
|
||||
const uiAmountString = useSelector(selectTransferSourceBalanceString);
|
||||
const amount = useSelector(selectTransferAmount);
|
||||
const error = useSelector(selectTransferSourceError);
|
||||
|
@ -57,10 +66,14 @@ function Source({
|
|||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||
const handleMigrationClick = useCallback(() => {
|
||||
if (sourceChain === CHAIN_ID_SOLANA) {
|
||||
history.push(
|
||||
`/migrate/${parsedTokenAccount?.mintKey}/${parsedTokenAccount?.publicKey}`
|
||||
`/migrate/Solana/${parsedTokenAccount?.mintKey}/${parsedTokenAccount?.publicKey}`
|
||||
);
|
||||
}, [history, parsedTokenAccount]);
|
||||
} else if (sourceChain === CHAIN_ID_ETH) {
|
||||
history.push(`/migrate/Ethereum/${parsedTokenAccount?.mintKey}`);
|
||||
}
|
||||
}, [history, parsedTokenAccount, sourceChain]);
|
||||
const handleSourceChange = useCallback(
|
||||
(event) => {
|
||||
dispatch(setSourceChain(event.target.value));
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import {
|
||||
Migrator,
|
||||
Migrator__factory,
|
||||
TokenImplementation,
|
||||
TokenImplementation__factory,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { Signer } from "@ethersproject/abstract-signer";
|
||||
import { formatUnits } from "@ethersproject/units";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export type EthMigrationInfo = {
|
||||
isLoading: boolean;
|
||||
error: string;
|
||||
data: RequisiteData | null;
|
||||
};
|
||||
|
||||
export type RequisiteData = {
|
||||
poolAddress: string;
|
||||
fromAddress: string;
|
||||
toAddress: string;
|
||||
fromToken: TokenImplementation;
|
||||
toToken: TokenImplementation;
|
||||
migrator: Migrator;
|
||||
fromSymbol: string;
|
||||
toSymbol: string;
|
||||
fromDecimals: number;
|
||||
toDecimals: number;
|
||||
sharesDecimals: number;
|
||||
fromWalletBalance: string;
|
||||
toWalletBalance: string;
|
||||
fromPoolBalance: string;
|
||||
toPoolBalance: string;
|
||||
walletSharesBalance: string;
|
||||
};
|
||||
|
||||
const getRequisiteData = async (
|
||||
migrator: Migrator,
|
||||
signer: Signer,
|
||||
signerAddress: string
|
||||
): Promise<RequisiteData> => {
|
||||
try {
|
||||
const poolAddress = migrator.address;
|
||||
const fromAddress = await migrator.fromAsset();
|
||||
const toAddress = await migrator.toAsset();
|
||||
|
||||
const fromToken = TokenImplementation__factory.connect(fromAddress, signer);
|
||||
const toToken = TokenImplementation__factory.connect(toAddress, signer);
|
||||
|
||||
const fromSymbol = await fromToken.symbol();
|
||||
const toSymbol = await toToken.symbol();
|
||||
|
||||
const fromDecimals = await (await migrator.fromDecimals()).toNumber();
|
||||
const toDecimals = await (await migrator.toDecimals()).toNumber();
|
||||
const sharesDecimals = await migrator.decimals();
|
||||
|
||||
const fromWalletBalance = formatUnits(
|
||||
await fromToken.balanceOf(signerAddress),
|
||||
fromDecimals
|
||||
);
|
||||
const toWalletBalance = formatUnits(
|
||||
await toToken.balanceOf(signerAddress),
|
||||
toDecimals
|
||||
);
|
||||
|
||||
const fromPoolBalance = formatUnits(
|
||||
await fromToken.balanceOf(poolAddress),
|
||||
fromDecimals
|
||||
);
|
||||
const toPoolBalance = formatUnits(
|
||||
await toToken.balanceOf(poolAddress),
|
||||
toDecimals
|
||||
);
|
||||
|
||||
const walletSharesBalance = formatUnits(
|
||||
await migrator.balanceOf(signerAddress),
|
||||
sharesDecimals
|
||||
);
|
||||
|
||||
return {
|
||||
poolAddress,
|
||||
fromAddress,
|
||||
toAddress,
|
||||
fromToken,
|
||||
toToken,
|
||||
migrator,
|
||||
fromSymbol,
|
||||
toSymbol,
|
||||
fromDecimals,
|
||||
toDecimals,
|
||||
fromWalletBalance,
|
||||
toWalletBalance,
|
||||
fromPoolBalance,
|
||||
toPoolBalance,
|
||||
walletSharesBalance,
|
||||
sharesDecimals,
|
||||
};
|
||||
} catch (e) {
|
||||
return Promise.reject("Failed to retrieve required data.");
|
||||
}
|
||||
};
|
||||
|
||||
function useEthereumMigratorInformation(
|
||||
migratorAddress: string | undefined,
|
||||
signer: Signer | undefined,
|
||||
signerAddress: string | undefined,
|
||||
toggleRefresh: boolean
|
||||
): EthMigrationInfo {
|
||||
const migrator = useMemo(
|
||||
() =>
|
||||
migratorAddress &&
|
||||
signer &&
|
||||
Migrator__factory.connect(migratorAddress, signer),
|
||||
[migratorAddress, signer]
|
||||
);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!signer || !migrator || !signerAddress) {
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setIsLoading(true);
|
||||
getRequisiteData(migrator, signer, signerAddress).then(
|
||||
(result) => {
|
||||
if (!cancelled) {
|
||||
setData(result);
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
setError("Failed to retrieve necessary data.");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
return;
|
||||
};
|
||||
}, [migrator, signer, signerAddress, toggleRefresh]);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!migratorAddress || !signer || !signerAddress) {
|
||||
return {
|
||||
isLoading: false,
|
||||
error:
|
||||
!signer || !signerAddress
|
||||
? "Wallet not connected"
|
||||
: !migratorAddress
|
||||
? "No contract address"
|
||||
: "Error",
|
||||
data: null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
data,
|
||||
};
|
||||
}
|
||||
}, [isLoading, error, data, migratorAddress, signer, signerAddress]);
|
||||
}
|
||||
|
||||
export default useEthereumMigratorInformation;
|
|
@ -336,3 +336,16 @@ export const MIGRATION_ASSET_MAP = new Map<string, string>(
|
|||
// ],
|
||||
]
|
||||
);
|
||||
|
||||
export const ETH_MIGRATION_ASSET_MAP = new Map<string, string>(
|
||||
CLUSTER === "mainnet"
|
||||
? []
|
||||
: CLUSTER === "testnet"
|
||||
? []
|
||||
: [
|
||||
// [
|
||||
// "0x2D8BE6BF0baA74e0A907016679CaE9190e80dD0A",
|
||||
// "0xFcCeD5E997E7fb1D0594518D3eD57245bB8ed17E",
|
||||
// ],
|
||||
]
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue