diff --git a/lp_ui/src/components/ButtonWithLoader.tsx b/lp_ui/src/components/ButtonWithLoader.tsx
new file mode 100644
index 00000000..be00a62e
--- /dev/null
+++ b/lp_ui/src/components/ButtonWithLoader.tsx
@@ -0,0 +1,72 @@
+import {
+ Button,
+ CircularProgress,
+ makeStyles,
+ Typography,
+} from "@material-ui/core";
+import { ReactChild } from "react";
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ position: "relative",
+ },
+ button: {
+ marginTop: theme.spacing(2),
+ textTransform: "none",
+ width: "100%",
+ },
+ loader: {
+ position: "absolute",
+ bottom: 0,
+ left: "50%",
+ marginLeft: -12,
+ marginBottom: 6,
+ },
+ error: {
+ marginTop: theme.spacing(1),
+ textAlign: "center",
+ },
+}));
+
+export default function ButtonWithLoader({
+ disabled,
+ onClick,
+ showLoader,
+ error,
+ children,
+}: {
+ disabled?: boolean;
+ onClick: () => void;
+ showLoader?: boolean;
+ error?: string;
+ children: ReactChild;
+}) {
+ const classes = useStyles();
+ return (
+ <>
+
+
+ {showLoader ? (
+
+ ) : null}
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ >
+ );
+}
diff --git a/lp_ui/src/components/ErrorBoundary.js b/lp_ui/src/components/ErrorBoundary.js
new file mode 100644
index 00000000..99eb901b
--- /dev/null
+++ b/lp_ui/src/components/ErrorBoundary.js
@@ -0,0 +1,29 @@
+import { Typography } from "@material-ui/core";
+import React from "react";
+
+export default class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ console.error(error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+ "An unexpected error has occurred. Please refresh the page."
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/lp_ui/src/components/SolanaCreateAssociatedAddress.tsx b/lp_ui/src/components/SolanaCreateAssociatedAddress.tsx
new file mode 100644
index 00000000..160803c4
--- /dev/null
+++ b/lp_ui/src/components/SolanaCreateAssociatedAddress.tsx
@@ -0,0 +1,146 @@
+import { ChainId, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
+import { Typography } from "@material-ui/core";
+import {
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ Token,
+ TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useSolanaWallet } from "../contexts/SolanaWalletContext";
+import { SOLANA_URL } from "../utils/consts";
+import { signSendAndConfirm } from "../utils/solana";
+import ButtonWithLoader from "./ButtonWithLoader";
+
+export function useAssociatedAccountExistsState(
+ mintAddress: string | null | undefined,
+ readableTargetAddress: string | undefined
+) {
+ const [associatedAccountExists, setAssociatedAccountExists] = useState(true); // for now, assume it exists until we confirm it doesn't
+ const solanaWallet = useSolanaWallet();
+ const solPK = solanaWallet?.publicKey;
+ useEffect(() => {
+ setAssociatedAccountExists(true);
+ if (!mintAddress || !readableTargetAddress || !solPK) return;
+ let cancelled = false;
+ (async () => {
+ const connection = new Connection(SOLANA_URL, "confirmed");
+ const mintPublicKey = new PublicKey(mintAddress);
+ const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
+ const associatedAddress = await Token.getAssociatedTokenAddress(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ mintPublicKey,
+ payerPublicKey
+ );
+ const match = associatedAddress.toString() === readableTargetAddress;
+ if (match) {
+ const associatedAddressInfo = await connection.getAccountInfo(
+ associatedAddress
+ );
+ if (!associatedAddressInfo) {
+ if (!cancelled) {
+ setAssociatedAccountExists(false);
+ }
+ }
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [mintAddress, readableTargetAddress, solPK]);
+ return useMemo(
+ () => ({ associatedAccountExists, setAssociatedAccountExists }),
+ [associatedAccountExists]
+ );
+}
+
+export default function SolanaCreateAssociatedAddress({
+ mintAddress,
+ readableTargetAddress,
+ associatedAccountExists,
+ setAssociatedAccountExists,
+}: {
+ mintAddress: string | undefined;
+ readableTargetAddress: string | undefined;
+ associatedAccountExists: boolean;
+ setAssociatedAccountExists: (associatedAccountExists: boolean) => void;
+}) {
+ const [isCreating, setIsCreating] = useState(false);
+ const solanaWallet = useSolanaWallet();
+ const solPK = solanaWallet?.publicKey;
+ const handleClick = useCallback(() => {
+ if (
+ associatedAccountExists ||
+ !mintAddress ||
+ !readableTargetAddress ||
+ !solPK
+ )
+ return;
+ (async () => {
+ try {
+ const connection = new Connection(SOLANA_URL, "confirmed");
+ const mintPublicKey = new PublicKey(mintAddress);
+ const payerPublicKey = new PublicKey(solPK); // currently assumes the wallet is the owner
+ const associatedAddress = await Token.getAssociatedTokenAddress(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ mintPublicKey,
+ payerPublicKey
+ );
+ const match = associatedAddress.toString() === readableTargetAddress;
+ if (match) {
+ const associatedAddressInfo = await connection.getAccountInfo(
+ associatedAddress
+ );
+ if (!associatedAddressInfo) {
+ setIsCreating(true);
+ const transaction = new Transaction().add(
+ await Token.createAssociatedTokenAccountInstruction(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ mintPublicKey,
+ associatedAddress,
+ payerPublicKey, // owner
+ payerPublicKey // payer
+ )
+ );
+ const { blockhash } = await connection.getRecentBlockhash();
+ transaction.recentBlockhash = blockhash;
+ transaction.feePayer = new PublicKey(payerPublicKey);
+ await signSendAndConfirm(solanaWallet, connection, transaction);
+ setIsCreating(false);
+ setAssociatedAccountExists(true);
+ }
+ }
+ } catch (e) {
+ console.log("cannot create specified spl token account");
+ console.error(e);
+ }
+ })();
+ }, [
+ associatedAccountExists,
+ setAssociatedAccountExists,
+ mintAddress,
+ solPK,
+ readableTargetAddress,
+ solanaWallet,
+ ]);
+ if (associatedAccountExists) return null;
+ return (
+ <>
+
+ This associated token account doesn't exist.
+
+
+ Create Associated Token Account
+
+ >
+ );
+}
diff --git a/lp_ui/src/index.js b/lp_ui/src/index.js
index 05760f4e..a26384d2 100644
--- a/lp_ui/src/index.js
+++ b/lp_ui/src/index.js
@@ -2,17 +2,20 @@ import { CssBaseline } from "@material-ui/core";
import { ThemeProvider } from "@material-ui/core/styles";
import ReactDOM from "react-dom";
import App from "./App";
+import ErrorBoundary from "./components/ErrorBoundary";
import { LoggerProvider } from "./contexts/Logger";
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext";
import { theme } from "./muiTheme";
ReactDOM.render(
-
-
-
-
-
-
-
- ,
+
+
+
+
+
+
+
+
+
+ ,
document.getElementById("root")
);
diff --git a/lp_ui/src/views/Main.tsx b/lp_ui/src/views/Main.tsx
index 6a132e60..f1bc9606 100644
--- a/lp_ui/src/views/Main.tsx
+++ b/lp_ui/src/views/Main.tsx
@@ -5,6 +5,7 @@ import {
Paper,
TextField,
Button,
+ Divider,
} from "@material-ui/core";
//import { pool_address } from "@certusone/wormhole-sdk/lib/solana/migration/wormhole_migration";
import { useCallback, useEffect, useState } from "react";
@@ -24,6 +25,19 @@ import getPoolAddress from "@certusone/wormhole-sdk/lib/migration/poolAddress";
import getFromCustodyAddress from "@certusone/wormhole-sdk/lib/migration/fromCustodyAddress";
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
import getShareMintAddress from "@certusone/wormhole-sdk/lib/migration/shareMintAddress";
+import parsePool from "@certusone/wormhole-sdk/lib/migration/parsePool";
+import addLiquidityTx from "@certusone/wormhole-sdk/lib/migration/addLiquidity";
+import claimSharesTx from "@certusone/wormhole-sdk/lib/migration/claimShares";
+import migrateTokensTx from "@certusone/wormhole-sdk/lib/migration/migrateTokens";
+
+import SolanaCreateAssociatedAddress, {
+ useAssociatedAccountExistsState,
+} from "../components/SolanaCreateAssociatedAddress";
+import {
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ Token,
+ TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
const useStyles = makeStyles(() => ({
rootContainer: {},
@@ -33,6 +47,9 @@ const useStyles = makeStyles(() => ({
},
padding: "2rem",
},
+ divider: {
+ margin: "2rem",
+ },
}));
function Main() {
@@ -43,20 +60,56 @@ function Main() {
const [fromMint, setFromMint] = useState("");
const [toMint, setToMint] = useState("");
+ const [shareMintAddress, setShareMintAddress] = useState(
+ undefined
+ );
const [poolAddress, setPoolAddress] = useState("");
- const [poolExists, setPoolExists] = useState(null);
- const [poolAccountInfo, setPoolAccountInfo] = useState(null);
- const [shareTokenMint, setShareTokenMint] = useState(null);
- const [toTokenAccount, setToTokenAccount] = useState(null);
- const [fromTokenAccount, setFromTokenAccount] = useState(null);
- const [shareTokenAccount, setShareTokenAccount] = useState(null);
+ const [poolExists, setPoolExists] = useState(undefined);
+ const [poolAccountInfo, setPoolAccountInfo] = useState(undefined);
+ const [parsedPoolData, setParsedPoolData] = useState(undefined);
+
+ //These are the user's personal token accounts corresponding to the mints for the connected wallet
+ const [fromTokenAccount, setFromTokenAccount] = useState(
+ undefined
+ );
+ const [toTokenAccount, setToTokenAccount] = useState(
+ undefined
+ );
+ const [shareTokenAccount, setShareTokenAccount] = useState<
+ string | undefined
+ >(undefined);
+
+ //These hooks detect if the connected wallet has the requisite token accounts
+ const {
+ associatedAccountExists: fromTokenAccountExists,
+ setAssociatedAccountExists: setFromTokenAccountExists,
+ } = useAssociatedAccountExistsState(fromMint, fromTokenAccount);
+ const {
+ associatedAccountExists: toTokenAccountExists,
+ setAssociatedAccountExists: setToTokenAccountExists,
+ } = useAssociatedAccountExistsState(toMint, toTokenAccount);
+ const {
+ associatedAccountExists: shareTokenAccountExists,
+ setAssociatedAccountExists: setShareTokenAccountExists,
+ } = useAssociatedAccountExistsState(shareMintAddress, shareTokenAccount);
//these are all the other derived information
- const [authorityAddress, setAuthorityAddress] = useState(null);
- const [fromCustodyAddress, setFromCustodyAddress] = useState(null);
- const [toCustodyAddress, setToCustodyAddress] = useState(null);
- const [shareMintAddress, setShareMintAddress] = useState(null);
+ const [authorityAddress, setAuthorityAddress] = useState(
+ undefined
+ );
+ const [fromCustodyAddress, setFromCustodyAddress] = useState<
+ string | undefined
+ >(undefined);
+ const [toCustodyAddress, setToCustodyAddress] = useState(
+ undefined
+ );
+
+ const [toggleAllData, setToggleAllData] = useState(false);
+
+ const [liquidityAmount, setLiquidityAmount] = useState("");
+ const [migrationAmount, setMigrationAmount] = useState("");
+ const [redeemAmount, setRedeemAmount] = useState("");
/*
Effects***
@@ -82,8 +135,8 @@ function Main() {
//Retrieve the poolAccount every time the pool address changes.
useEffect(() => {
if (poolAddress) {
- setPoolAccountInfo(null);
- setPoolExists(null);
+ setPoolAccountInfo(undefined);
+ setPoolExists(undefined);
try {
getMultipleAccounts(
connection,
@@ -92,6 +145,13 @@ function Main() {
).then((result) => {
if (result.length && result[0] !== null) {
setPoolAccountInfo(result[0]);
+ parsePool(result[0].data).then(
+ (parsed) => setParsedPoolData(parsed),
+ (error) => {
+ logger.log("Failed to parse the pool data.");
+ console.error(error);
+ }
+ );
setPoolExists(true);
logger.log("Successfully found account info for the pool.");
} else if (result.length && result[0] === null) {
@@ -109,21 +169,71 @@ function Main() {
}
}, [poolAddress]);
+ //Set all the addresses which derive from poolAddress
useEffect(() => {
getAuthorityAddress(MIGRATION_PROGRAM_ADDRESS).then((result: any) =>
- setAuthorityAddress(result)
+ setAuthorityAddress(new PublicKey(result).toString())
);
getToCustodyAddress(MIGRATION_PROGRAM_ADDRESS, poolAddress).then(
- (result: any) => setToCustodyAddress(result)
+ (result: any) => setToCustodyAddress(new PublicKey(result).toString())
);
getFromCustodyAddress(MIGRATION_PROGRAM_ADDRESS, poolAddress).then(
- (result: any) => setFromCustodyAddress(result)
+ (result: any) => setFromCustodyAddress(new PublicKey(result).toString())
);
getShareMintAddress(MIGRATION_PROGRAM_ADDRESS, poolAddress).then(
- (result: any) => setShareMintAddress(result)
+ (result: any) => setShareMintAddress(new PublicKey(result).toString())
);
}, [poolAddress]);
+
+ //Set the associated token accounts when the designated mint changes
+ useEffect(() => {
+ if (wallet?.publicKey && fromMint) {
+ Token.getAssociatedTokenAddress(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ new PublicKey(fromMint),
+ wallet?.publicKey || new PublicKey([])
+ ).then(
+ (result) => {
+ setFromTokenAccount(result.toString());
+ },
+ (error) => {}
+ );
+ }
+ }, [fromMint, wallet?.publicKey]);
+
+ useEffect(() => {
+ if (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?.publicKey]);
+
+ useEffect(() => {
+ if (wallet?.publicKey && shareMintAddress) {
+ Token.getAssociatedTokenAddress(
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ TOKEN_PROGRAM_ID,
+ new PublicKey(shareMintAddress || ""),
+ wallet?.publicKey || new PublicKey([])
+ ).then(
+ (result) => {
+ setShareTokenAccount(result.toString());
+ },
+ (error) => {}
+ );
+ }
+ }, [shareMintAddress, wallet?.publicKey]);
/*
End Effects!
*/
@@ -136,6 +246,14 @@ function Main() {
*/
const createPool = async () => {
+ console.log(
+ "createPool with these args",
+ connection,
+ wallet?.publicKey?.toString(),
+ MIGRATION_PROGRAM_ADDRESS,
+ fromMint,
+ toMint
+ );
try {
const instruction = await createPoolAccount(
connection,
@@ -148,8 +266,8 @@ function Main() {
signSendAndConfirm(wallet, connection, instruction).then(
(transaction: any) => {
- setPoolExists(null); //Set these to null to force a fetch on them
- setPoolAccountInfo(null);
+ setPoolExists(undefined); //Set these to null to force a fetch on them
+ setPoolAccountInfo(undefined);
logger.log("Successfully created the pool.");
},
(error) => {
@@ -162,6 +280,96 @@ function Main() {
console.error(e);
}
};
+
+ const addLiquidity = async () => {
+ try {
+ const instruction = await addLiquidityTx(
+ connection,
+ wallet?.publicKey?.toString() || "",
+ MIGRATION_PROGRAM_ADDRESS,
+ fromMint,
+ toMint,
+ toTokenAccount || "",
+ shareTokenAccount || "",
+ BigInt(liquidityAmount)
+ );
+
+ signSendAndConfirm(wallet, connection, instruction).then(
+ (transaction: any) => {
+ setPoolExists(undefined); //Set these to null to force a fetch on them
+ setPoolAccountInfo(undefined);
+ logger.log("Successfully added liquidity to the pool.");
+ },
+ (error) => {
+ logger.log("Could not complete the addLiquidity transaction");
+ console.error(error);
+ }
+ );
+ } catch (e) {
+ logger.log("Could not complete the addLiquidity transaction");
+ console.error(e);
+ }
+ };
+
+ const migrateTokens = async () => {
+ try {
+ const instruction = await migrateTokensTx(
+ connection,
+ wallet?.publicKey?.toString() || "",
+ MIGRATION_PROGRAM_ADDRESS,
+ fromMint,
+ toMint,
+ fromTokenAccount || "",
+ toTokenAccount || "",
+ BigInt(migrationAmount)
+ );
+
+ signSendAndConfirm(wallet, connection, instruction).then(
+ (transaction: any) => {
+ setPoolExists(undefined); //Set these to null to force a fetch on them
+ setPoolAccountInfo(undefined);
+ logger.log("Successfully migrated the tokens.");
+ },
+ (error) => {
+ logger.log("Could not complete the migrateTokens transaction.");
+ console.error(error);
+ }
+ );
+ } catch (e) {
+ logger.log("Could not complete the migrateTokens transaction.");
+ console.error(e);
+ }
+ };
+
+ const redeemShares = async () => {
+ try {
+ const instruction = await claimSharesTx(
+ connection,
+ wallet?.publicKey?.toString() || "",
+ MIGRATION_PROGRAM_ADDRESS,
+ fromMint,
+ toMint,
+ toTokenAccount || "",
+ shareTokenAccount || "",
+ BigInt(redeemAmount)
+ );
+
+ signSendAndConfirm(wallet, connection, instruction).then(
+ (transaction: any) => {
+ setPoolExists(undefined); //Set these to null to force a fetch on them
+ setPoolAccountInfo(undefined);
+ logger.log("Successfully redeemed the shares.");
+ },
+ (error) => {
+ logger.log("Could not complete the claimShares transaction.");
+ console.error(error);
+ }
+ );
+ } catch (e) {
+ logger.log("Could not complete the claimShares transaction.");
+ console.error(e);
+ }
+ };
/*
End actions!
*/
@@ -194,35 +402,156 @@ function Main() {
);
- const addLiquidity = (
+ const addLiquidityUI = (
<>
-
- Add 'to' tokens to this pool, and receive liquidity tokens.
+ Add Liquidity
+
+ This will remove 'To' tokens from your wallet, and give you an equal
+ number of 'Share' tokens.
setToMint(event.target.value)}
- label={"To Token"}
+ value={liquidityAmount}
+ type="number"
+ onChange={(event) => setLiquidityAmount(event.target.value)}
+ label={"Amount to add"}
>
+
>
);
+ const migrateTokensUI = (
+ <>
+ Migrate Tokens
+
+ This will remove 'From' tokens from your wallet, and give you an equal
+ number of 'To' tokens.
+
+ setMigrationAmount(event.target.value)}
+ label={"Amount to add"}
+ >
+
+ >
+ );
+
+ const redeemSharesUI = (
+ <>
+ Redeem Shares
+
+ This will remove 'Share' tokens from your wallet, and give you an equal
+ number of 'From' tokens.
+
+ setRedeemAmount(event.target.value)}
+ label={"Amount to add"}
+ >
+
+ >
+ );
+
+ const relevantTokenAccounts = (
+ <>
+ Your Relevant Token Accounts:
+
+ {"'From' SPL Token Account: " + fromTokenAccount}
+
+
+
+ {"'To' SPL Token Account: " + toTokenAccount}
+
+
+
+ {"Share SPL Token Account: " + shareTokenAccount}
+
+
+ >
+ );
+
+ const poolInfo = (
+
+ {
+
+ }
+ {toggleAllData ? (
+ <>
+ {"Pool Address: " + poolAddress}
+ {"Pool has been instantiated: " + poolExists}
+ {"'From' Token Mint Address: " + fromMint}
+ {"'To' Token Mint Address: " + toMint}
+ {"Share Token Mint: " + shareMintAddress}
+ {"Authority Address: " + authorityAddress}
+
+ {"'From' Custody Mint: " + fromCustodyAddress}
+
+ {"'To' Custody Mint: " + toCustodyAddress}
+
+ {"Full Parsed Data for Pool: " + JSON.stringify(parsedPoolData)}
+
+ >
+ ) : null}
+
+ );
+
const mainContent = (
<>
{toAndFromSelector}
+
+ {poolInfo}
{createPoolButton}
+
+ {relevantTokenAccounts}
+
+ {addLiquidityUI}
+
+ {redeemSharesUI}
+
+ {migrateTokensUI}
>
);
const content = !wallet.publicKey ? (
Please connect your wallet.
+ ) : !poolAddress ? (
+ toAndFromSelector
) : (
mainContent
);