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 Transfer from "./components/Transfer";
|
||||||
import wormholeLogo from "./icons/wormhole.svg";
|
import wormholeLogo from "./icons/wormhole.svg";
|
||||||
import { CLUSTER } from "./utils/consts";
|
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) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
appBar: {
|
appBar: {
|
||||||
|
@ -185,8 +187,14 @@ function App() {
|
||||||
<Route exact path="/register">
|
<Route exact path="/register">
|
||||||
<Attest />
|
<Attest />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/migrate/:legacyAsset/:fromTokenAccount">
|
<Route exact path="/migrate/Solana/:legacyAsset/:fromTokenAccount">
|
||||||
<Migration />
|
<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>
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Home />
|
<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 migrateTokensTx from "@certusone/wormhole-sdk/lib/migration/migrateTokens";
|
||||||
import getPoolAddress from "@certusone/wormhole-sdk/lib/migration/poolAddress";
|
import getPoolAddress from "@certusone/wormhole-sdk/lib/migration/poolAddress";
|
||||||
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
|
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
|
||||||
import {
|
import { makeStyles, TextField, Typography } from "@material-ui/core";
|
||||||
Container,
|
|
||||||
Divider,
|
|
||||||
makeStyles,
|
|
||||||
Paper,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import {
|
import {
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
Token,
|
Token,
|
||||||
|
@ -400,13 +393,7 @@ export default function Workflow({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="md">
|
<div>
|
||||||
<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 />
|
<SolanaWalletKey />
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
{fromTokenAccount && toTokenAccount ? (
|
{fromTokenAccount && toTokenAccount ? (
|
||||||
|
@ -435,10 +422,7 @@ export default function Workflow({
|
||||||
variant="h5"
|
variant="h5"
|
||||||
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
|
color={toTokenAccountExists ? "textPrimary" : "textSecondary"}
|
||||||
>
|
>
|
||||||
<SmartAddress
|
<SmartAddress address={toTokenAccount} chainId={CHAIN_ID_SOLANA} />
|
||||||
address={toTokenAccount}
|
|
||||||
chainId={CHAIN_ID_SOLANA}
|
|
||||||
/>
|
|
||||||
<span>
|
<span>
|
||||||
{toTokenAccountExists
|
{toTokenAccountExists
|
||||||
? ` (Balance: ${toTokenAccountBalance}${
|
? ` (Balance: ${toTokenAccountBalance}${
|
||||||
|
@ -458,10 +442,7 @@ export default function Workflow({
|
||||||
<div className={classes.spacer} />
|
<div className={classes.spacer} />
|
||||||
<Typography variant="body2" component="div">
|
<Typography variant="body2" component="div">
|
||||||
<span>Using pool </span>
|
<span>Using pool </span>
|
||||||
<SmartAddress
|
<SmartAddress address={poolAddress} chainId={CHAIN_ID_SOLANA} />
|
||||||
address={poolAddress}
|
|
||||||
chainId={CHAIN_ID_SOLANA}
|
|
||||||
/>
|
|
||||||
<span> holding tokens in this account:</span>
|
<span> holding tokens in this account:</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
|
@ -503,8 +484,8 @@ export default function Workflow({
|
||||||
{transaction ? (
|
{transaction ? (
|
||||||
<>
|
<>
|
||||||
<Typography>
|
<Typography>
|
||||||
Successfully migrated your tokens! They will be available once
|
Successfully migrated your tokens! They will be available once this
|
||||||
this transaction confirms.
|
transaction confirms.
|
||||||
</Typography>
|
</Typography>
|
||||||
<ShowTx
|
<ShowTx
|
||||||
tx={{ id: transaction, block: 1 }}
|
tx={{ id: transaction, block: 1 }}
|
||||||
|
@ -512,7 +493,6 @@ export default function Workflow({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</Paper>
|
</div>
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -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 { PublicKey } from "@solana/web3.js";
|
||||||
import { RouteComponentProps } from "react-router-dom";
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
import { MIGRATION_ASSET_MAP } from "../../utils/consts";
|
import {
|
||||||
import Workflow from "./Workflow";
|
ETH_MIGRATION_ASSET_MAP,
|
||||||
|
MIGRATION_ASSET_MAP,
|
||||||
|
} from "../../utils/consts";
|
||||||
|
import SolanaWorkflow from "./SolanaWorkflow";
|
||||||
import { withRouter } from "react-router";
|
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 {
|
interface RouteParams {
|
||||||
legacyAsset: string;
|
legacyAsset: string;
|
||||||
fromTokenAccount: 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 legacyAsset: string = props.match.params.legacyAsset;
|
||||||
const fromTokenAccount: string = props.match.params.fromTokenAccount;
|
const fromTokenAccount: string = props.match.params.fromTokenAccount;
|
||||||
const targetAsset: string | undefined = MIGRATION_ASSET_MAP.get(legacyAsset);
|
const targetAsset: string | undefined = MIGRATION_ASSET_MAP.get(legacyAsset);
|
||||||
|
@ -27,27 +62,73 @@ const MigrationRoot: React.FC<Migration> = (props) => {
|
||||||
fromTokenAccount && new PublicKey(fromTokenAccount).toString();
|
fromTokenAccount && new PublicKey(fromTokenAccount).toString();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
let content = null;
|
||||||
|
|
||||||
if (!fromMint || !toMint) {
|
if (!fromMint || !toMint) {
|
||||||
return (
|
content = (
|
||||||
<Typography style={{ textAlign: "center" }}>
|
<Typography style={{ textAlign: "center" }}>
|
||||||
This asset is not eligible for migration.
|
This asset is not eligible for migration.
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
} else if (!fromTokenAcct) {
|
} else if (!fromTokenAcct) {
|
||||||
return (
|
content = (
|
||||||
<Typography style={{ textAlign: "center" }}>
|
<Typography style={{ textAlign: "center" }}>
|
||||||
Invalid token account.
|
Invalid token account.
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
content = (
|
||||||
<Workflow
|
<SolanaWorkflow
|
||||||
fromMint={fromMint}
|
fromMint={fromMint}
|
||||||
toMint={toMint}
|
toMint={toMint}
|
||||||
fromTokenAccount={fromTokenAcct}
|
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);
|
export default withRouter(MigrationRoot);
|
||||||
|
|
|
@ -6,13 +6,16 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} 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 React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
import { useEthereumProvider } from "../../contexts/EthereumProviderContext";
|
||||||
import { CovalentData } from "../../hooks/useGetSourceParsedTokenAccounts";
|
import { CovalentData } from "../../hooks/useGetSourceParsedTokenAccounts";
|
||||||
import { DataWrapper } from "../../store/helpers";
|
import { DataWrapper } from "../../store/helpers";
|
||||||
import { ParsedTokenAccount } from "../../store/transferSlice";
|
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 {
|
import {
|
||||||
ethNFTToNFTParsedTokenAccount,
|
ethNFTToNFTParsedTokenAccount,
|
||||||
ethTokenToParsedTokenAccount,
|
ethTokenToParsedTokenAccount,
|
||||||
|
@ -57,6 +60,12 @@ const useStyles = makeStyles((theme) =>
|
||||||
tokenImage: {
|
tokenImage: {
|
||||||
maxHeight: "2.5rem", //Eyeballing this based off the text size
|
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);
|
return connection.isWrappedAsset(address);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMigrationEligible = (address: string) => {
|
||||||
|
return !!ETH_MIGRATION_ASSET_MAP.get(address);
|
||||||
|
};
|
||||||
|
|
||||||
type EthereumSourceTokenSelectorProps = {
|
type EthereumSourceTokenSelectorProps = {
|
||||||
value: ParsedTokenAccount | null;
|
value: ParsedTokenAccount | null;
|
||||||
onChange: (newValue: ParsedTokenAccount | null) => void;
|
onChange: (newValue: ParsedTokenAccount | null) => void;
|
||||||
|
@ -100,7 +113,7 @@ const renderAccount = (
|
||||||
const mintPrettyString = shortenAddress(account.mintKey);
|
const mintPrettyString = shortenAddress(account.mintKey);
|
||||||
const uri = getLogo(account);
|
const uri = getLogo(account);
|
||||||
const symbol = getSymbol(account) || "Unknown";
|
const symbol = getSymbol(account) || "Unknown";
|
||||||
return (
|
const content = (
|
||||||
<div className={classes.tokenOverviewContainer}>
|
<div className={classes.tokenOverviewContainer}>
|
||||||
<div className={classes.tokenImageContainer}>
|
<div className={classes.tokenImageContainer}>
|
||||||
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
{uri && <img alt="" className={classes.tokenImage} src={uri} />}
|
||||||
|
@ -121,6 +134,19 @@ const renderAccount = (
|
||||||
</div>
|
</div>
|
||||||
</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 = (
|
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 { Button, makeStyles, MenuItem, TextField } from "@material-ui/core";
|
||||||
import { Restore } from "@material-ui/icons";
|
import { Restore } from "@material-ui/icons";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
@ -19,7 +19,11 @@ import {
|
||||||
setAmount,
|
setAmount,
|
||||||
setSourceChain,
|
setSourceChain,
|
||||||
} from "../../store/transferSlice";
|
} 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 ButtonWithLoader from "../ButtonWithLoader";
|
||||||
import KeyAndBalance from "../KeyAndBalance";
|
import KeyAndBalance from "../KeyAndBalance";
|
||||||
import LowBalanceWarning from "../LowBalanceWarning";
|
import LowBalanceWarning from "../LowBalanceWarning";
|
||||||
|
@ -46,10 +50,15 @@ function Source({
|
||||||
selectTransferSourceParsedTokenAccount
|
selectTransferSourceParsedTokenAccount
|
||||||
);
|
);
|
||||||
const hasParsedTokenAccount = !!parsedTokenAccount;
|
const hasParsedTokenAccount = !!parsedTokenAccount;
|
||||||
const isMigrationAsset =
|
const isSolanaMigration =
|
||||||
sourceChain === CHAIN_ID_SOLANA &&
|
sourceChain === CHAIN_ID_SOLANA &&
|
||||||
!!parsedTokenAccount &&
|
!!parsedTokenAccount &&
|
||||||
!!MIGRATION_ASSET_MAP.get(parsedTokenAccount.mintKey);
|
!!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 uiAmountString = useSelector(selectTransferSourceBalanceString);
|
||||||
const amount = useSelector(selectTransferAmount);
|
const amount = useSelector(selectTransferAmount);
|
||||||
const error = useSelector(selectTransferSourceError);
|
const error = useSelector(selectTransferSourceError);
|
||||||
|
@ -57,10 +66,14 @@ function Source({
|
||||||
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
const shouldLockFields = useSelector(selectTransferShouldLockFields);
|
||||||
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
const { isReady, statusMessage } = useIsWalletReady(sourceChain);
|
||||||
const handleMigrationClick = useCallback(() => {
|
const handleMigrationClick = useCallback(() => {
|
||||||
|
if (sourceChain === CHAIN_ID_SOLANA) {
|
||||||
history.push(
|
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(
|
const handleSourceChange = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
dispatch(setSourceChain(event.target.value));
|
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