all the ui components for lp_ui

Change-Id: I60b6f95f480c28f245e87f35fd7ae4ebdced535d
This commit is contained in:
Chase Moran 2021-09-13 01:35:00 -04:00 committed by Evan Gray
parent 8a90b50aeb
commit 1230ea6fa3
5 changed files with 613 additions and 34 deletions

View File

@ -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 (
<>
<div className={classes.root}>
<Button
color="primary"
variant="contained"
className={classes.button}
disabled={disabled}
onClick={onClick}
>
{children}
</Button>
{showLoader ? (
<CircularProgress
size={24}
color="inherit"
className={classes.loader}
/>
) : null}
</div>
{error ? (
<Typography color="error" className={classes.error}>
{error}
</Typography>
) : null}
</>
);
}

View File

@ -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 (
<Typography variant="h5" style={{ textAlign: "center", marginTop: 24 }}>
"An unexpected error has occurred. Please refresh the page."
</Typography>
);
}
return this.props.children;
}
}

View File

@ -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 (
<>
<Typography color="error" variant="body2">
This associated token account doesn't exist.
</Typography>
<ButtonWithLoader
disabled={
!mintAddress || !readableTargetAddress || !solPK || isCreating
}
onClick={handleClick}
showLoader={isCreating}
>
Create Associated Token Account
</ButtonWithLoader>
</>
);
}

View File

@ -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(
<ThemeProvider theme={theme}>
<CssBaseline />
<SolanaWalletProvider>
<LoggerProvider>
<App />
</LoggerProvider>
</SolanaWalletProvider>
</ThemeProvider>,
<ErrorBoundary>
<ThemeProvider theme={theme}>
<CssBaseline />
<SolanaWalletProvider>
<LoggerProvider>
<App />
</LoggerProvider>
</SolanaWalletProvider>
</ThemeProvider>
</ErrorBoundary>,
document.getElementById("root")
);

View File

@ -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<string | undefined>(
undefined
);
const [poolAddress, setPoolAddress] = useState("");
const [poolExists, setPoolExists] = useState<boolean | null>(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<boolean | undefined>(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<string | undefined>(
undefined
);
const [toTokenAccount, setToTokenAccount] = useState<string | undefined>(
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<string | undefined>(
undefined
);
const [fromCustodyAddress, setFromCustodyAddress] = useState<
string | undefined
>(undefined);
const [toCustodyAddress, setToCustodyAddress] = useState<string | undefined>(
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() {
<Button
variant="contained"
onClick={() => createPool()}
disabled={poolExists || !poolAddress}
disabled={poolExists}
>
Click here to instantiate the pool for these tokens.
{poolExists
? "This Pool is instantiated."
: "This pool has not been instantiated! Click here to create it."}
</Button>
</div>
);
const addLiquidity = (
const addLiquidityUI = (
<>
<Typography>
Add 'to' tokens to this pool, and receive liquidity tokens.
<Typography variant="h4">Add Liquidity</Typography>
<Typography variant="body1">
This will remove 'To' tokens from your wallet, and give you an equal
number of 'Share' tokens.
</Typography>
<TextField
value={toMint}
onChange={(event) => setToMint(event.target.value)}
label={"To Token"}
value={liquidityAmount}
type="number"
onChange={(event) => setLiquidityAmount(event.target.value)}
label={"Amount to add"}
></TextField>
<Button variant="contained" onClick={addLiquidity}>
Add Liquidity
</Button>
</>
);
const migrateTokensUI = (
<>
<Typography variant="h4">Migrate Tokens</Typography>
<Typography variant="body1">
This will remove 'From' tokens from your wallet, and give you an equal
number of 'To' tokens.
</Typography>
<TextField
value={migrationAmount}
type="number"
onChange={(event) => setMigrationAmount(event.target.value)}
label={"Amount to add"}
></TextField>
<Button variant="contained" onClick={migrateTokens}>
Migrate Tokens
</Button>
</>
);
const redeemSharesUI = (
<>
<Typography variant="h4">Redeem Shares</Typography>
<Typography variant="body1">
This will remove 'Share' tokens from your wallet, and give you an equal
number of 'From' tokens.
</Typography>
<TextField
type="number"
value={redeemAmount}
onChange={(event) => setRedeemAmount(event.target.value)}
label={"Amount to add"}
></TextField>
<Button variant="contained" onClick={redeemShares}>
Redeem Shares
</Button>
</>
);
const relevantTokenAccounts = (
<>
<Typography variant="h4">Your Relevant Token Accounts: </Typography>
<Typography variant="body1">
{"'From' SPL Token Account: " + fromTokenAccount}
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={fromMint}
readableTargetAddress={fromTokenAccount}
associatedAccountExists={fromTokenAccountExists}
setAssociatedAccountExists={setFromTokenAccountExists}
/>
<Typography variant="body1">
{"'To' SPL Token Account: " + toTokenAccount}
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={toMint}
readableTargetAddress={toTokenAccount}
associatedAccountExists={toTokenAccountExists}
setAssociatedAccountExists={setToTokenAccountExists}
/>
<Typography variant="body1">
{"Share SPL Token Account: " + shareTokenAccount}
</Typography>
<SolanaCreateAssociatedAddress
mintAddress={shareMintAddress}
readableTargetAddress={shareTokenAccount}
associatedAccountExists={shareTokenAccountExists}
setAssociatedAccountExists={setShareTokenAccountExists}
/>
</>
);
const poolInfo = (
<div>
{
<Button
variant="outlined"
onClick={() => setToggleAllData(!toggleAllData)}
>
{toggleAllData ? "Hide Verbose Pool Data" : "Show Verbose Pool Data"}
</Button>
}
{toggleAllData ? (
<>
<Typography>{"Pool Address: " + poolAddress}</Typography>
<Typography>{"Pool has been instantiated: " + poolExists}</Typography>
<Typography>{"'From' Token Mint Address: " + fromMint}</Typography>
<Typography>{"'To' Token Mint Address: " + toMint}</Typography>
<Typography>{"Share Token Mint: " + shareMintAddress}</Typography>
<Typography>{"Authority Address: " + authorityAddress}</Typography>
<Typography>
{"'From' Custody Mint: " + fromCustodyAddress}
</Typography>
<Typography>{"'To' Custody Mint: " + toCustodyAddress}</Typography>
<Typography>
{"Full Parsed Data for Pool: " + JSON.stringify(parsedPoolData)}
</Typography>
</>
) : null}
</div>
);
const mainContent = (
<>
{toAndFromSelector}
<Divider className={classes.divider} />
{poolInfo}
{createPoolButton}
<Divider className={classes.divider} />
{relevantTokenAccounts}
<Divider className={classes.divider} />
{addLiquidityUI}
<Divider className={classes.divider} />
{redeemSharesUI}
<Divider className={classes.divider} />
{migrateTokensUI}
</>
);
const content = !wallet.publicKey ? (
<Typography>Please connect your wallet.</Typography>
) : !poolAddress ? (
toAndFromSelector
) : (
mainContent
);