bridge_ui: Solana quick migrate page
Change-Id: Ide559e50de4fab0a45c0a0e41a91eb52cee75215
This commit is contained in:
parent
7f5740754b
commit
d9513f9c39
|
@ -42,6 +42,7 @@ import { COLORS } from "./muiTheme";
|
||||||
import { CLUSTER } from "./utils/consts";
|
import { CLUSTER } from "./utils/consts";
|
||||||
import Stats from "./components/Stats";
|
import Stats from "./components/Stats";
|
||||||
import TokenOriginVerifier from "./components/TokenOriginVerifier";
|
import TokenOriginVerifier from "./components/TokenOriginVerifier";
|
||||||
|
import SolanaQuickMigrate from "./components/Migration/SolanaQuickMigrate";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
appBar: {
|
appBar: {
|
||||||
|
@ -292,6 +293,9 @@ function App() {
|
||||||
<Route exact path="/migrate/BinanceSmartChain/">
|
<Route exact path="/migrate/BinanceSmartChain/">
|
||||||
<EvmQuickMigrate chainId={CHAIN_ID_BSC} />
|
<EvmQuickMigrate chainId={CHAIN_ID_BSC} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/migrate/Solana/">
|
||||||
|
<SolanaQuickMigrate />
|
||||||
|
</Route>
|
||||||
<Route exact path="/stats">
|
<Route exact path="/stats">
|
||||||
<Stats />
|
<Stats />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -0,0 +1,373 @@
|
||||||
|
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
|
import {
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
makeStyles,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import ArrowRightAltIcon from "@material-ui/icons/ArrowRightAlt";
|
||||||
|
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
||||||
|
import {
|
||||||
|
AccountInfo,
|
||||||
|
Connection,
|
||||||
|
ParsedAccountData,
|
||||||
|
PublicKey,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import useIsWalletReady from "../../hooks/useIsWalletReady";
|
||||||
|
import useSolanaMigratorInformation from "../../hooks/useSolanaMigratorInformation";
|
||||||
|
import { COLORS } from "../../muiTheme";
|
||||||
|
import {
|
||||||
|
CHAINS_BY_ID,
|
||||||
|
getMigrationAssetMap,
|
||||||
|
SOLANA_HOST,
|
||||||
|
} from "../../utils/consts";
|
||||||
|
import ButtonWithLoader from "../ButtonWithLoader";
|
||||||
|
import ShowTx from "../ShowTx";
|
||||||
|
import SmartAddress from "../SmartAddress";
|
||||||
|
import SolanaCreateAssociatedAddress from "../SolanaCreateAssociatedAddress";
|
||||||
|
import SolanaWalletKey from "../SolanaWalletKey";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
spacer: {
|
||||||
|
height: "2rem",
|
||||||
|
},
|
||||||
|
containerDiv: {
|
||||||
|
textAlign: "center",
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
},
|
||||||
|
centered: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function SolanaMigrationLineItem({
|
||||||
|
migratorInfo,
|
||||||
|
onLoadComplete,
|
||||||
|
}: {
|
||||||
|
migratorInfo: DefaultAssociatedTokenAccountInfo;
|
||||||
|
onLoadComplete: () => void;
|
||||||
|
}) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const poolInfo = useSolanaMigratorInformation(
|
||||||
|
migratorInfo.fromMintKey,
|
||||||
|
migratorInfo.toMintKey,
|
||||||
|
migratorInfo.defaultFromTokenAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
|
||||||
|
const [transaction, setTransaction] = useState("");
|
||||||
|
const [migrationError, setMigrationError] = useState("");
|
||||||
|
|
||||||
|
const handleMigrateClick = useCallback(() => {
|
||||||
|
if (!poolInfo.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMigrationIsProcessing(true);
|
||||||
|
setMigrationError("");
|
||||||
|
poolInfo.data
|
||||||
|
.migrateTokens(poolInfo.data.fromAssociatedTokenAccountBalance)
|
||||||
|
.then((result) => {
|
||||||
|
setMigrationIsProcessing(false);
|
||||||
|
setTransaction(result);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
setMigrationError("Unable to perform migration.");
|
||||||
|
setMigrationIsProcessing(false);
|
||||||
|
});
|
||||||
|
}, [poolInfo.data]);
|
||||||
|
|
||||||
|
const precheckError =
|
||||||
|
poolInfo.data &&
|
||||||
|
poolInfo.data.getNotReadyCause(
|
||||||
|
poolInfo.data.fromAssociatedTokenAccountBalance
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (poolInfo.data || poolInfo.error) {
|
||||||
|
onLoadComplete();
|
||||||
|
}
|
||||||
|
}, [poolInfo, onLoadComplete]);
|
||||||
|
|
||||||
|
if (!poolInfo.data) {
|
||||||
|
return (
|
||||||
|
<div className={classes.centered}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Failed to load migration information for token
|
||||||
|
</Typography>
|
||||||
|
<SmartAddress
|
||||||
|
chainId={CHAIN_ID_SOLANA}
|
||||||
|
address={migratorInfo.fromMintKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (transaction) {
|
||||||
|
return (
|
||||||
|
<div className={classes.centered}>
|
||||||
|
<div>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Successfully migrated your tokens. They will become available once
|
||||||
|
this transaction confirms.
|
||||||
|
</Typography>
|
||||||
|
<ShowTx
|
||||||
|
chainId={CHAIN_ID_SOLANA}
|
||||||
|
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.fromAssociatedTokenAccountBalance}
|
||||||
|
</Typography>
|
||||||
|
<SmartAddress
|
||||||
|
chainId={CHAIN_ID_SOLANA}
|
||||||
|
address={poolInfo.data.fromAssociatedTokenAccount}
|
||||||
|
symbol={poolInfo.data.fromSymbol || undefined}
|
||||||
|
tokenName={poolInfo.data.fromName || undefined}
|
||||||
|
/>
|
||||||
|
</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.fromAssociatedTokenAccountBalance}
|
||||||
|
</Typography>
|
||||||
|
<SmartAddress
|
||||||
|
chainId={CHAIN_ID_SOLANA}
|
||||||
|
address={poolInfo.data.toAssociatedTokenAccount}
|
||||||
|
symbol={poolInfo.data.toSymbol || undefined}
|
||||||
|
tokenName={poolInfo.data.toName || undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!poolInfo.data.toAssociatedTokenAccountExists ? (
|
||||||
|
<div className={classes.convertButton}>
|
||||||
|
<SolanaCreateAssociatedAddress
|
||||||
|
mintAddress={migratorInfo.toMintKey}
|
||||||
|
readableTargetAddress={poolInfo.data?.toAssociatedTokenAccount}
|
||||||
|
associatedAccountExists={
|
||||||
|
poolInfo.data.toAssociatedTokenAccountExists
|
||||||
|
}
|
||||||
|
setAssociatedAccountExists={poolInfo.data.setToTokenAccountExists}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={classes.convertButton}>
|
||||||
|
<ButtonWithLoader
|
||||||
|
showLoader={migrationIsProcessing}
|
||||||
|
onClick={handleMigrateClick}
|
||||||
|
error={
|
||||||
|
poolInfo.error
|
||||||
|
? poolInfo.error
|
||||||
|
: migrationError
|
||||||
|
? migrationError
|
||||||
|
: precheckError
|
||||||
|
? precheckError
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
!!poolInfo.error || !!precheckError || migrationIsProcessing
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Convert
|
||||||
|
</ButtonWithLoader>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultAssociatedTokenAccountInfo = {
|
||||||
|
fromMintKey: string;
|
||||||
|
toMintKey: string;
|
||||||
|
defaultFromTokenAccount: string;
|
||||||
|
fromAccountInfo: AccountInfo<ParsedAccountData> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTokenBalances = async (
|
||||||
|
walletAddress: string,
|
||||||
|
migrationMap: Map<string, string>
|
||||||
|
): Promise<DefaultAssociatedTokenAccountInfo[]> => {
|
||||||
|
try {
|
||||||
|
const connection = new Connection(SOLANA_HOST);
|
||||||
|
const output: DefaultAssociatedTokenAccountInfo[] = [];
|
||||||
|
const tokenAccounts = await connection.getParsedTokenAccountsByOwner(
|
||||||
|
new PublicKey(walletAddress),
|
||||||
|
{ programId: TOKEN_PROGRAM_ID },
|
||||||
|
"confirmed"
|
||||||
|
);
|
||||||
|
tokenAccounts.value.forEach((item) => {
|
||||||
|
if (
|
||||||
|
item.account != null &&
|
||||||
|
item.account.data?.parsed?.info?.tokenAmount?.uiAmountString &&
|
||||||
|
item.account.data?.parsed.info?.tokenAmount?.amount !== "0"
|
||||||
|
) {
|
||||||
|
const fromMintKey = item.account.data.parsed.info.mint;
|
||||||
|
const toMintKey = migrationMap.get(fromMintKey);
|
||||||
|
if (toMintKey) {
|
||||||
|
output.push({
|
||||||
|
fromMintKey,
|
||||||
|
toMintKey: toMintKey,
|
||||||
|
defaultFromTokenAccount: item.pubkey.toString(),
|
||||||
|
fromAccountInfo: item.account,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return Promise.reject("Unable to retrieve token balances.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SolanaQuickMigrate() {
|
||||||
|
const chainId = CHAIN_ID_SOLANA;
|
||||||
|
const classes = useStyles();
|
||||||
|
const { isReady, walletAddress } = useIsWalletReady(chainId);
|
||||||
|
const migrationMap = useMemo(() => getMigrationAssetMap(chainId), [chainId]);
|
||||||
|
const [migrators, setMigrators] = useState<
|
||||||
|
DefaultAssociatedTokenAccountInfo[] | 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 && walletAddress) {
|
||||||
|
let cancelled = false;
|
||||||
|
setMigratorsLoading(true);
|
||||||
|
setMigratorsError("");
|
||||||
|
getTokenBalances(walletAddress, migrationMap).then(
|
||||||
|
(result) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setMigratorsFinishedLoading(0);
|
||||||
|
setMigrators(result.filter((x) => x.fromAccountInfo && x));
|
||||||
|
setMigratorsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setMigratorsLoading(false);
|
||||||
|
setMigratorsError(
|
||||||
|
"Failed to retrieve available token information."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isReady, walletAddress, migrationMap]);
|
||||||
|
|
||||||
|
const hasEligibleAssets = migrators && migrators.length > 0;
|
||||||
|
const chainName = CHAINS_BY_ID[chainId]?.name;
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div className={classes.containerDiv}>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{`This page allows you to convert certain wrapped tokens ${
|
||||||
|
chainName ? "on " + chainName : ""
|
||||||
|
} into
|
||||||
|
Wormhole V2 tokens.`}
|
||||||
|
</Typography>
|
||||||
|
<SolanaWalletKey />
|
||||||
|
{!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((info) => {
|
||||||
|
return (
|
||||||
|
<SolanaMigrationLineItem
|
||||||
|
migratorInfo={info}
|
||||||
|
onLoadComplete={reportLoadComplete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="md">
|
||||||
|
<Paper className={classes.mainPaper}>{content}</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,430 @@
|
||||||
|
import { CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
|
||||||
|
import migrateTokensTx from "@certusone/wormhole-sdk/lib/esm/migration/migrateTokens";
|
||||||
|
import getPoolAddress from "@certusone/wormhole-sdk/lib/esm/migration/poolAddress";
|
||||||
|
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/esm/migration/toCustodyAddress";
|
||||||
|
import {
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
Token,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
} from "@solana/spl-token";
|
||||||
|
import { Connection, PublicKey } from "@solana/web3.js";
|
||||||
|
import { parseUnits } from "ethers/lib/utils";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useAssociatedAccountExistsState } from "../components/SolanaCreateAssociatedAddress";
|
||||||
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
|
import useIsWalletReady from "../hooks/useIsWalletReady";
|
||||||
|
import useMetaplexData from "../hooks/useMetaplexData";
|
||||||
|
import useSolanaTokenMap from "../hooks/useSolanaTokenMap";
|
||||||
|
import { DataWrapper } from "../store/helpers";
|
||||||
|
import { MIGRATION_PROGRAM_ADDRESS, SOLANA_HOST } from "../utils/consts";
|
||||||
|
import { getMultipleAccounts, signSendAndConfirm } from "../utils/solana";
|
||||||
|
|
||||||
|
const getDecimals = async (
|
||||||
|
connection: Connection,
|
||||||
|
mint: string,
|
||||||
|
setter: (decimals: number | undefined) => void
|
||||||
|
) => {
|
||||||
|
setter(undefined);
|
||||||
|
if (mint) {
|
||||||
|
try {
|
||||||
|
const pk = new PublicKey(mint);
|
||||||
|
const info = await connection.getParsedAccountInfo(pk);
|
||||||
|
// @ts-ignore
|
||||||
|
const decimals = info.value?.data.parsed.info.decimals;
|
||||||
|
setter(decimals);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Unable to determine decimals of ${mint}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBalance = async (
|
||||||
|
connection: Connection,
|
||||||
|
address: string | undefined,
|
||||||
|
setter: (balance: string | undefined) => void
|
||||||
|
) => {
|
||||||
|
setter(undefined);
|
||||||
|
if (address) {
|
||||||
|
try {
|
||||||
|
const pk = new PublicKey(address);
|
||||||
|
const info = await connection.getParsedAccountInfo(pk);
|
||||||
|
// @ts-ignore
|
||||||
|
const balance = info.value?.data.parsed.info.tokenAmount.uiAmountString;
|
||||||
|
setter(balance);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Unable to determine balance of ${address}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//If the pool doesn't exist in this app, it's an error.
|
||||||
|
export type SolanaMigratorInformation = {
|
||||||
|
poolAddress: string;
|
||||||
|
fromMint: string;
|
||||||
|
toMint: string;
|
||||||
|
fromMintDecimals: number;
|
||||||
|
fromAssociatedTokenAccountExists: boolean;
|
||||||
|
toAssociatedTokenAccountExists: boolean;
|
||||||
|
setToTokenAccountExists: any;
|
||||||
|
fromAssociatedTokenAccount: string;
|
||||||
|
toAssociatedTokenAccount: string;
|
||||||
|
fromAssociatedTokenAccountBalance: string;
|
||||||
|
toAssociatedTokenAccountBalance: string | null;
|
||||||
|
toCustodyAddress: string;
|
||||||
|
toCustodyBalance: string;
|
||||||
|
|
||||||
|
fromName: string | null;
|
||||||
|
fromSymbol: string | null;
|
||||||
|
fromLogo: string | null;
|
||||||
|
toName: string | null;
|
||||||
|
toSymbol: string | null;
|
||||||
|
toLogo: string | null;
|
||||||
|
|
||||||
|
getNotReadyCause: (amount: string) => string | null;
|
||||||
|
|
||||||
|
migrateTokens: (amount: string) => Promise<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
//TODO refactor the workflow page to use this hook
|
||||||
|
export default function useSolanaMigratorInformation(
|
||||||
|
fromMint: string,
|
||||||
|
toMint: string,
|
||||||
|
fromTokenAccount: string
|
||||||
|
): DataWrapper<SolanaMigratorInformation> {
|
||||||
|
const connection = useMemo(
|
||||||
|
() => new Connection(SOLANA_HOST, "confirmed"),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const wallet = useSolanaWallet();
|
||||||
|
const { isReady } = useIsWalletReady(CHAIN_ID_SOLANA, false);
|
||||||
|
const solanaTokenMap = useSolanaTokenMap();
|
||||||
|
const metaplexArray = useMemo(() => [fromMint, toMint], [fromMint, toMint]);
|
||||||
|
const metaplexData = useMetaplexData(metaplexArray);
|
||||||
|
|
||||||
|
const [poolAddress, setPoolAddress] = useState("");
|
||||||
|
const [poolExists, setPoolExists] = useState<boolean | undefined>(undefined);
|
||||||
|
const [fromTokenAccountBalance, setFromTokenAccountBalance] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [toTokenAccount, setToTokenAccount] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [toTokenAccountBalance, setToTokenAccountBalance] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [fromMintDecimals, setFromMintDecimals] = useState<number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
associatedAccountExists: fromTokenAccountExists,
|
||||||
|
//setAssociatedAccountExists: setFromTokenAccountExists,
|
||||||
|
} = useAssociatedAccountExistsState(
|
||||||
|
CHAIN_ID_SOLANA,
|
||||||
|
fromMint,
|
||||||
|
fromTokenAccount
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
associatedAccountExists: toTokenAccountExists,
|
||||||
|
setAssociatedAccountExists: setToTokenAccountExists,
|
||||||
|
} = useAssociatedAccountExistsState(CHAIN_ID_SOLANA, toMint, toTokenAccount);
|
||||||
|
|
||||||
|
const [toCustodyAddress, setToCustodyAddress] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const [toCustodyBalance, setToCustodyBalance] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
/* Effects
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
getDecimals(connection, fromMint, setFromMintDecimals);
|
||||||
|
}, [connection, fromMint]);
|
||||||
|
|
||||||
|
//Retrieve user balance when fromTokenAccount changes
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: cancellable
|
||||||
|
if (fromTokenAccount && fromTokenAccountExists) {
|
||||||
|
getBalance(connection, fromTokenAccount, setFromTokenAccountBalance);
|
||||||
|
} else {
|
||||||
|
setFromTokenAccountBalance(undefined);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
connection,
|
||||||
|
fromTokenAccountExists,
|
||||||
|
fromTokenAccount,
|
||||||
|
setFromTokenAccountBalance,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: cancellable
|
||||||
|
if (toTokenAccount && toTokenAccountExists) {
|
||||||
|
getBalance(connection, toTokenAccount, setToTokenAccountBalance);
|
||||||
|
} else {
|
||||||
|
setToTokenAccountBalance(undefined);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
connection,
|
||||||
|
toTokenAccountExists,
|
||||||
|
toTokenAccount,
|
||||||
|
setFromTokenAccountBalance,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// TODO: cancellable
|
||||||
|
if (toCustodyAddress) {
|
||||||
|
getBalance(connection, toCustodyAddress, setToCustodyBalance);
|
||||||
|
} else {
|
||||||
|
setToCustodyBalance(undefined);
|
||||||
|
}
|
||||||
|
}, [connection, toCustodyAddress, setToCustodyBalance]);
|
||||||
|
|
||||||
|
//Retrieve pool address on selectedTokens change
|
||||||
|
useEffect(() => {
|
||||||
|
if (toMint && fromMint) {
|
||||||
|
setPoolAddress("");
|
||||||
|
setPoolExists(undefined);
|
||||||
|
getPoolAddress(MIGRATION_PROGRAM_ADDRESS, fromMint, toMint).then(
|
||||||
|
(result) => {
|
||||||
|
const key = new PublicKey(result).toString();
|
||||||
|
setPoolAddress(key);
|
||||||
|
},
|
||||||
|
(error) => console.log("Could not calculate pool address.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [toMint, fromMint, setPoolAddress]);
|
||||||
|
|
||||||
|
//Retrieve the poolAccount every time the pool address changes.
|
||||||
|
useEffect(() => {
|
||||||
|
if (poolAddress) {
|
||||||
|
setPoolExists(undefined);
|
||||||
|
try {
|
||||||
|
getMultipleAccounts(
|
||||||
|
connection,
|
||||||
|
[new PublicKey(poolAddress)],
|
||||||
|
"confirmed"
|
||||||
|
).then((result) => {
|
||||||
|
if (result.length && result[0] !== null) {
|
||||||
|
setPoolExists(true);
|
||||||
|
} else if (result.length && result[0] === null) {
|
||||||
|
setPoolExists(false);
|
||||||
|
setError("There is no swap pool for this token.");
|
||||||
|
} else {
|
||||||
|
setError(
|
||||||
|
"unexpected error in fetching pool address. Please reload and try again"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setError("Could not fetch pool address");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [connection, poolAddress]);
|
||||||
|
|
||||||
|
//Set relevant information derived from poolAddress
|
||||||
|
useEffect(() => {
|
||||||
|
if (poolAddress) {
|
||||||
|
getToCustodyAddress(MIGRATION_PROGRAM_ADDRESS, poolAddress)
|
||||||
|
.then((result: any) =>
|
||||||
|
setToCustodyAddress(new PublicKey(result).toString())
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
setToCustodyAddress(undefined);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setToCustodyAddress(undefined);
|
||||||
|
}
|
||||||
|
}, [poolAddress]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wallet && wallet.publicKey && toMint) {
|
||||||
|
Token.getAssociatedTokenAddress(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
new PublicKey(toMint),
|
||||||
|
wallet.publicKey || new PublicKey([])
|
||||||
|
).then(
|
||||||
|
(result) => {
|
||||||
|
setToTokenAccount(result.toString());
|
||||||
|
},
|
||||||
|
(error) => {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [toMint, wallet]);
|
||||||
|
/*
|
||||||
|
End effects
|
||||||
|
*/
|
||||||
|
|
||||||
|
const migrateTokens = useCallback(
|
||||||
|
async (amount) => {
|
||||||
|
const instruction = await migrateTokensTx(
|
||||||
|
connection,
|
||||||
|
wallet.publicKey?.toString() || "",
|
||||||
|
MIGRATION_PROGRAM_ADDRESS,
|
||||||
|
fromMint,
|
||||||
|
toMint,
|
||||||
|
fromTokenAccount || "",
|
||||||
|
toTokenAccount || "",
|
||||||
|
parseUnits(amount, fromMintDecimals).toBigInt()
|
||||||
|
);
|
||||||
|
return await signSendAndConfirm(wallet, connection, instruction);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
connection,
|
||||||
|
fromMint,
|
||||||
|
fromTokenAccount,
|
||||||
|
toMint,
|
||||||
|
toTokenAccount,
|
||||||
|
wallet,
|
||||||
|
fromMintDecimals,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fromParse = useCallback(
|
||||||
|
(amount: string) => {
|
||||||
|
try {
|
||||||
|
return parseUnits(amount, fromMintDecimals).toBigInt();
|
||||||
|
} catch (e) {
|
||||||
|
return BigInt(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fromMintDecimals]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNotReadyCause = useCallback(
|
||||||
|
(amount: string) => {
|
||||||
|
const hasRequisiteData = fromMint && toMint && poolAddress && poolExists;
|
||||||
|
const accountsReady = fromTokenAccountExists && toTokenAccountExists;
|
||||||
|
const amountGreaterThanZero = fromParse(amount) > BigInt(0);
|
||||||
|
const sufficientFromTokens =
|
||||||
|
fromTokenAccountBalance &&
|
||||||
|
amount &&
|
||||||
|
fromParse(amount) <= fromParse(fromTokenAccountBalance);
|
||||||
|
const sufficientPoolBalance =
|
||||||
|
toCustodyBalance &&
|
||||||
|
amount &&
|
||||||
|
parseFloat(amount) <= parseFloat(toCustodyBalance);
|
||||||
|
|
||||||
|
if (!hasRequisiteData) {
|
||||||
|
return "This asset is not supported.";
|
||||||
|
} else if (!isReady) {
|
||||||
|
return "Wallet is not connected.";
|
||||||
|
} else if (!accountsReady) {
|
||||||
|
return "You have not created the necessary token accounts.";
|
||||||
|
} else if (!amount) {
|
||||||
|
return "Enter an amount to transfer.";
|
||||||
|
} else if (!amountGreaterThanZero) {
|
||||||
|
return "Enter an amount 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 "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
fromMint,
|
||||||
|
fromParse,
|
||||||
|
fromTokenAccountBalance,
|
||||||
|
fromTokenAccountExists,
|
||||||
|
isReady,
|
||||||
|
poolAddress,
|
||||||
|
poolExists,
|
||||||
|
toCustodyBalance,
|
||||||
|
toMint,
|
||||||
|
toTokenAccountExists,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getMetadata = useCallback(
|
||||||
|
(address: string) => {
|
||||||
|
const tokenMapItem = solanaTokenMap.data?.find(
|
||||||
|
(x) => x.address === address
|
||||||
|
);
|
||||||
|
const metaplexItem = metaplexData.data?.get(address);
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol: tokenMapItem?.symbol || metaplexItem?.data?.symbol || undefined,
|
||||||
|
name: tokenMapItem?.name || metaplexItem?.data?.name || undefined,
|
||||||
|
logo: tokenMapItem?.logoURI || metaplexItem?.data?.uri || undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[metaplexData.data, solanaTokenMap.data]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFetching = solanaTokenMap.isFetching || metaplexData.isFetching; //TODO add loading state on the actual Solana information
|
||||||
|
const hasRequisiteData = !!(
|
||||||
|
fromMintDecimals !== null &&
|
||||||
|
fromMintDecimals !== undefined &&
|
||||||
|
toTokenAccount &&
|
||||||
|
fromTokenAccountBalance &&
|
||||||
|
toCustodyAddress &&
|
||||||
|
toCustodyBalance
|
||||||
|
);
|
||||||
|
|
||||||
|
const output: DataWrapper<SolanaMigratorInformation> = useMemo(() => {
|
||||||
|
let data: SolanaMigratorInformation | null = null;
|
||||||
|
if (hasRequisiteData) {
|
||||||
|
data = {
|
||||||
|
poolAddress,
|
||||||
|
fromMint,
|
||||||
|
toMint,
|
||||||
|
fromMintDecimals,
|
||||||
|
fromAssociatedTokenAccountExists: fromTokenAccountExists,
|
||||||
|
toAssociatedTokenAccountExists: toTokenAccountExists,
|
||||||
|
fromAssociatedTokenAccount: fromTokenAccount,
|
||||||
|
toAssociatedTokenAccount: toTokenAccount,
|
||||||
|
fromAssociatedTokenAccountBalance: fromTokenAccountBalance,
|
||||||
|
toAssociatedTokenAccountBalance: toTokenAccountBalance || null,
|
||||||
|
toCustodyAddress,
|
||||||
|
toCustodyBalance,
|
||||||
|
|
||||||
|
fromName: getMetadata(fromMint)?.name || null,
|
||||||
|
fromSymbol: getMetadata(fromMint)?.symbol || null,
|
||||||
|
fromLogo: getMetadata(fromMint)?.logo || null,
|
||||||
|
toName: getMetadata(toMint)?.name || null,
|
||||||
|
toSymbol: getMetadata(toMint)?.symbol || null,
|
||||||
|
toLogo: getMetadata(toMint)?.logo || null,
|
||||||
|
|
||||||
|
setToTokenAccountExists,
|
||||||
|
|
||||||
|
getNotReadyCause: getNotReadyCause,
|
||||||
|
|
||||||
|
migrateTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching: isFetching,
|
||||||
|
error: error || !hasRequisiteData,
|
||||||
|
receivedAt: null,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
error,
|
||||||
|
isFetching,
|
||||||
|
hasRequisiteData,
|
||||||
|
poolAddress,
|
||||||
|
fromMint,
|
||||||
|
toMint,
|
||||||
|
fromMintDecimals,
|
||||||
|
fromTokenAccountExists,
|
||||||
|
toTokenAccountExists,
|
||||||
|
fromTokenAccount,
|
||||||
|
toTokenAccount,
|
||||||
|
fromTokenAccountBalance,
|
||||||
|
toTokenAccountBalance,
|
||||||
|
toCustodyAddress,
|
||||||
|
toCustodyBalance,
|
||||||
|
getMetadata,
|
||||||
|
getNotReadyCause,
|
||||||
|
migrateTokens,
|
||||||
|
setToTokenAccountExists,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
Loading…
Reference in New Issue