Skip to content

Commit

Permalink
all the ui components for lp_ui
Browse files Browse the repository at this point in the history
Change-Id: I60b6f95f480c28f245e87f35fd7ae4ebdced535d
  • Loading branch information
chase-45 authored and evan-gray committed Sep 13, 2021
1 parent 8a90b50 commit 1230ea6
Show file tree
Hide file tree
Showing 5 changed files with 613 additions and 34 deletions.
72 changes: 72 additions & 0 deletions lp_ui/src/components/ButtonWithLoader.tsx
Original file line number Diff line number Diff line change
@@ -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}
</>
);
}
29 changes: 29 additions & 0 deletions lp_ui/src/components/ErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
146 changes: 146 additions & 0 deletions lp_ui/src/components/SolanaCreateAssociatedAddress.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
}
19 changes: 11 additions & 8 deletions lp_ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
Loading

0 comments on commit 1230ea6

Please sign in to comment.