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 Stats from "./components/Stats";
|
||||
import TokenOriginVerifier from "./components/TokenOriginVerifier";
|
||||
import SolanaQuickMigrate from "./components/Migration/SolanaQuickMigrate";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
appBar: {
|
||||
|
@ -292,6 +293,9 @@ function App() {
|
|||
<Route exact path="/migrate/BinanceSmartChain/">
|
||||
<EvmQuickMigrate chainId={CHAIN_ID_BSC} />
|
||||
</Route>
|
||||
<Route exact path="/migrate/Solana/">
|
||||
<SolanaQuickMigrate />
|
||||
</Route>
|
||||
<Route exact path="/stats">
|
||||
<Stats />
|
||||
</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