bridge_ui: ethereum migration functions

Change-Id: I39d12adcdfd5036283572f006a1442a26a3fc143
This commit is contained in:
Chase Moran 2021-10-03 18:35:24 -04:00 committed by Evan Gray
parent 363d83f9c6
commit 7f83984410
9 changed files with 1029 additions and 141 deletions

View File

@ -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 />

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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,119 +393,106 @@ 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} />
<SolanaWalletKey />
<div className={classes.spacer} />
{fromTokenAccount && toTokenAccount ? (
<>
<Typography variant="body2" component="div">
<span>This will migrate</span>
{fromMintPretty}
<span>tokens in this account:</span>
</Typography>
<Typography variant="h5">
<SmartAddress
address={fromTokenAccount}
chainId={CHAIN_ID_SOLANA}
/>
{`(Balance: ${fromTokenAccountBalance}${
fromMetadata.symbol && " " + fromMetadata.symbol
})`}
</Typography>
<div className={classes.spacer} />
<Typography variant="body2" component="div">
<span>into </span>
{toMintPretty}
<span> tokens in this account:</span>
</Typography>
<Typography
variant="h5"
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
>
<SmartAddress
address={toTokenAccount}
chainId={CHAIN_ID_SOLANA}
/>
<span>
{toTokenAccountExists
? ` (Balance: ${toTokenAccountBalance}${
(toMetadata.symbol && " " + toMetadata.symbol) || ""
})`
: " (Not created yet)"}
</span>
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={toMint}
readableTargetAddress={toTokenAccount}
associatedAccountExists={toTokenAccountExists}
setAssociatedAccountExists={setToTokenAccountExists}
/>
{poolAddress && toCustodyAddress && toCustodyBalance ? (
<>
<div className={classes.spacer} />
<Typography variant="body2" component="div">
<span>Using pool </span>
<SmartAddress
address={poolAddress}
chainId={CHAIN_ID_SOLANA}
/>
<span> holding tokens in this account:</span>
</Typography>
<Typography variant="h5">
<SmartAddress
address={toCustodyAddress}
chainId={CHAIN_ID_SOLANA}
/>
<span>{` (Balance: ${toCustodyBalance}${
toMetadata.symbol && " " + toMetadata.symbol
})`}</span>
</Typography>
</>
) : null}
</>
) : null}
<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 }}
<div>
<SolanaWalletKey />
<div className={classes.spacer} />
{fromTokenAccount && toTokenAccount ? (
<>
<Typography variant="body2" component="div">
<span>This will migrate</span>
{fromMintPretty}
<span>tokens in this account:</span>
</Typography>
<Typography variant="h5">
<SmartAddress
address={fromTokenAccount}
chainId={CHAIN_ID_SOLANA}
/>
</>
) : null}
</Paper>
</Container>
{`(Balance: ${fromTokenAccountBalance}${
fromMetadata.symbol && " " + fromMetadata.symbol
})`}
</Typography>
<div className={classes.spacer} />
<Typography variant="body2" component="div">
<span>into </span>
{toMintPretty}
<span> tokens in this account:</span>
</Typography>
<Typography
variant="h5"
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
>
<SmartAddress address={toTokenAccount} chainId={CHAIN_ID_SOLANA} />
<span>
{toTokenAccountExists
? ` (Balance: ${toTokenAccountBalance}${
(toMetadata.symbol && " " + toMetadata.symbol) || ""
})`
: " (Not created yet)"}
</span>
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={toMint}
readableTargetAddress={toTokenAccount}
associatedAccountExists={toTokenAccountExists}
setAssociatedAccountExists={setToTokenAccountExists}
/>
{poolAddress && toCustodyAddress && toCustodyBalance ? (
<>
<div className={classes.spacer} />
<Typography variant="body2" component="div">
<span>Using pool </span>
<SmartAddress address={poolAddress} chainId={CHAIN_ID_SOLANA} />
<span> holding tokens in this account:</span>
</Typography>
<Typography variant="h5">
<SmartAddress
address={toCustodyAddress}
chainId={CHAIN_ID_SOLANA}
/>
<span>{` (Balance: ${toCustodyBalance}${
toMetadata.symbol && " " + toMetadata.symbol
})`}</span>
</Typography>
</>
) : null}
</>
) : null}
<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_SOLANA}
/>
</>
) : null}
</div>
);
}

View File

@ -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);

View File

@ -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 = (

View File

@ -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(() => {
history.push(
`/migrate/${parsedTokenAccount?.mintKey}/${parsedTokenAccount?.publicKey}`
);
}, [history, parsedTokenAccount]);
if (sourceChain === CHAIN_ID_SOLANA) {
history.push(
`/migrate/Solana/${parsedTokenAccount?.mintKey}/${parsedTokenAccount?.publicKey}`
);
} else if (sourceChain === CHAIN_ID_ETH) {
history.push(`/migrate/Ethereum/${parsedTokenAccount?.mintKey}`);
}
}, [history, parsedTokenAccount, sourceChain]);
const handleSourceChange = useCallback(
(event) => {
dispatch(setSourceChain(event.target.value));

View File

@ -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;

View File

@ -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",
// ],
]
);