lp_ui: ethereum migration pool functionality

Change-Id: Ibdf24e1f90e711e5284016045c0c7d9d413be4ac
This commit is contained in:
Chase Moran 2021-09-28 23:23:51 -04:00 committed by Evan Gray
parent 7e6123a3a8
commit d8a8d5722a
17 changed files with 1257 additions and 32 deletions

View File

@ -7,12 +7,12 @@
"": {
"name": "lp_ui",
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@certusone/wormhole-sdk": "file:..\\sdk\\js",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@solana/spl-token": "^0.1.6",
"@solana/spl-token-registry": "^0.2.216",
"@solana/wallet-adapter-base": "^0.5.2",
@ -25,6 +25,7 @@
"@types/node": "^16.9.1",
"@types/react": "^17.0.20",
"@types/react-dom": "^17.0.9",
"clsx": "^1.1.1",
"ethers": "^5.4.6",
"notistack": "^1.0.10",
"react": "^17.0.2",
@ -39,7 +40,7 @@
},
"../sdk/js": {
"name": "@certusone/wormhole-sdk",
"version": "0.0.2",
"version": "0.0.5",
"license": "Apache-2.0",
"dependencies": {
"@improbable-eng/grpc-web": "^0.14.0",
@ -3542,6 +3543,14 @@
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@metamask/detect-provider": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@metamask/detect-provider/-/detect-provider-1.2.0.tgz",
"integrity": "sha512-ocA76vt+8D0thgXZ7LxFPyqw3H7988qblgzddTDA6B8a/yU0uKV42QR/DhA+Jh11rJjxW0jKvwb5htA6krNZDQ==",
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -26318,6 +26327,11 @@
"react-is": "^16.8.0 || ^17.0.0"
}
},
"@metamask/detect-provider": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@metamask/detect-provider/-/detect-provider-1.2.0.tgz",
"integrity": "sha512-ocA76vt+8D0thgXZ7LxFPyqw3H7988qblgzddTDA6B8a/yU0uKV42QR/DhA+Jh11rJjxW0jKvwb5htA6krNZDQ=="
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@ -7,6 +7,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
"@metamask/detect-provider": "^1.2.0",
"@solana/spl-token": "^0.1.6",
"@solana/spl-token-registry": "^0.2.216",
"@solana/wallet-adapter-base": "^0.5.2",
@ -19,6 +20,7 @@
"@types/node": "^16.9.1",
"@types/react": "^17.0.20",
"@types/react-dom": "^17.0.9",
"clsx": "^1.1.1",
"ethers": "^5.4.6",
"notistack": "^1.0.10",
"react": "^17.0.2",

View File

@ -1,7 +1,7 @@
import Main from "./views/Main";
import Home from "./views/Home";
function App() {
return <Main />;
return <Home />;
}
export default App;

View File

@ -0,0 +1,25 @@
import { Typography } from "@material-ui/core";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import ToggleConnectedButton from "./ToggleConnectedButton";
const EthereumSignerKey = () => {
const { connect, disconnect, signerAddress, providerError } =
useEthereumProvider();
return (
<>
<ToggleConnectedButton
connect={connect}
disconnect={disconnect}
connected={!!signerAddress}
pk={signerAddress || ""}
/>
{providerError ? (
<Typography variant="body2" color="error">
{providerError}
</Typography>
) : null}
</>
);
};
export default EthereumSignerKey;

View File

@ -1,13 +1,8 @@
import { Button, Paper, Typography } from "@material-ui/core";
import { useEffect } from "react";
import { useLogger } from "../contexts/Logger";
function LogWatcher() {
const { logs, clear, log } = useLogger();
useEffect(() => {
log("Instantiated the logger.");
}, [log]);
const { logs, clear } = useLogger();
return (
<Paper style={{ padding: "1rem", maxHeight: "600px", overflow: "auto" }}>

View File

@ -0,0 +1,151 @@
import {
ChainId,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
} from "@certusone/wormhole-sdk";
import { Button, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { FileCopy, OpenInNew } from "@material-ui/icons";
import { withStyles } from "@material-ui/styles";
import clsx from "clsx";
import useCopyToClipboard from "../hooks/useCopyToClipboard";
import { CLUSTER } from "../utils/consts";
import { shortenAddress } from "../utils/solana";
const useStyles = makeStyles((theme) => ({
mainTypog: {
display: "inline-block",
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
textDecoration: "underline",
textUnderlineOffset: "2px",
},
noGutter: {
marginLeft: 0,
marginRight: 0,
},
noUnderline: {
textDecoration: "none",
},
buttons: {
marginLeft: ".5rem",
marginRight: ".5rem",
},
}));
const tooltipStyles = {
tooltip: {
minWidth: "max-content",
textAlign: "center",
"& > *": {
margin: ".25rem",
},
},
};
// @ts-ignore
const StyledTooltip = withStyles(tooltipStyles)(Tooltip);
export default function SmartAddress({
chainId,
address,
symbol,
tokenName,
variant,
noGutter,
noUnderline,
}: {
chainId: ChainId;
address?: string;
logo?: string;
tokenName?: string;
symbol?: string;
variant?: any;
noGutter?: boolean;
noUnderline?: boolean;
}) {
const classes = useStyles();
const useableAddress = address || "";
const useableSymbol = symbol || "";
const isNative = false;
const addressShort = shortenAddress(useableAddress) || "";
const useableName = tokenName || "";
//TODO terra
const explorerAddress = isNative
? null
: chainId === CHAIN_ID_ETH
? `https://${
CLUSTER === "testnet" ? "goerli." : ""
}etherscan.io/address/${useableAddress}`
: chainId === CHAIN_ID_SOLANA
? `https://explorer.solana.com/address/${useableAddress}${
CLUSTER === "testnet"
? "?cluster=testnet"
: CLUSTER === "devnet"
? "?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899"
: ""
}`
: undefined;
const explorerName = chainId === CHAIN_ID_ETH ? "Etherscan" : "Explorer";
const copyToClipboard = useCopyToClipboard(useableAddress);
const explorerButton = !explorerAddress ? null : (
<Button
size="small"
variant="outlined"
endIcon={<OpenInNew />}
className={classes.buttons}
href={explorerAddress}
target="_blank"
>
{"View on " + explorerName}
</Button>
);
//TODO add icon here
const copyButton = isNative ? null : (
<Button
size="small"
variant="outlined"
endIcon={<FileCopy />}
onClick={copyToClipboard}
className={classes.buttons}
>
Copy
</Button>
);
const tooltipContent = (
<>
{useableName && <Typography>{useableName}</Typography>}
{useableSymbol && !isNative && (
<Typography noWrap variant="body2">
{addressShort}
</Typography>
)}
<div>
{explorerButton}
{copyButton}
</div>
</>
);
return (
<StyledTooltip
title={tooltipContent}
interactive={true}
className={classes.mainTypog}
>
<Typography
variant={variant || "body1"}
className={clsx(classes.mainTypog, {
[classes.noGutter]: noGutter,
[classes.noUnderline]: noUnderline,
})}
component="div"
>
{useableSymbol || addressShort}
</Typography>
</StyledTooltip>
);
}

View File

@ -0,0 +1,51 @@
import { Button, makeStyles, Tooltip } from "@material-ui/core";
const useStyles = makeStyles((theme) => ({
button: {
display: "block",
margin: `${theme.spacing(1)}px auto`,
width: "100%",
maxWidth: 400,
},
}));
const ToggleConnectedButton = ({
connect,
disconnect,
connected,
pk,
}: {
connect(): any;
disconnect(): any;
connected: boolean;
pk: string;
}) => {
const classes = useStyles();
const is0x = pk.startsWith("0x");
return connected ? (
<Tooltip title={pk}>
<Button
color="secondary"
variant="contained"
size="small"
onClick={disconnect}
className={classes.button}
>
Disconnect {pk.substring(0, is0x ? 6 : 3)}...
{pk.substr(pk.length - (is0x ? 4 : 3))}
</Button>
</Tooltip>
) : (
<Button
color="primary"
variant="contained"
size="small"
onClick={connect}
className={classes.button}
>
Connect
</Button>
);
};
export default ToggleConnectedButton;

View File

@ -0,0 +1,158 @@
import detectEthereumProvider from "@metamask/detect-provider";
import { BigNumber, ethers } from "ethers";
import React, {
ReactChildren,
useCallback,
useContext,
useMemo,
useState,
} from "react";
export type Provider = ethers.providers.Web3Provider | undefined;
export type Signer = ethers.Signer | undefined;
interface IEthereumProviderContext {
connect(): void;
disconnect(): void;
provider: Provider;
chainId: number | undefined;
signer: Signer;
signerAddress: string | undefined;
providerError: string | null;
}
const EthereumProviderContext = React.createContext<IEthereumProviderContext>({
connect: () => {},
disconnect: () => {},
provider: undefined,
chainId: undefined,
signer: undefined,
signerAddress: undefined,
providerError: null,
});
export const EthereumProviderProvider = ({
children,
}: {
children: ReactChildren;
}) => {
const [providerError, setProviderError] = useState<string | null>(null);
const [provider, setProvider] = useState<Provider>(undefined);
const [chainId, setChainId] = useState<number | undefined>(undefined);
const [signer, setSigner] = useState<Signer>(undefined);
const [signerAddress, setSignerAddress] = useState<string | undefined>(
undefined
);
const connect = useCallback(() => {
setProviderError(null);
detectEthereumProvider()
.then((detectedProvider) => {
if (detectedProvider) {
const provider = new ethers.providers.Web3Provider(
// @ts-ignore
detectedProvider,
"any"
);
provider
.send("eth_requestAccounts", [])
.then(() => {
setProviderError(null);
setProvider(provider);
provider
.getNetwork()
.then((network) => {
setChainId(network.chainId);
})
.catch(() => {
setProviderError(
"An error occurred while getting the network"
);
});
const signer = provider.getSigner();
setSigner(signer);
signer
.getAddress()
.then((address) => {
setSignerAddress(address);
})
.catch(() => {
setProviderError(
"An error occurred while getting the signer address"
);
});
// TODO: try using ethers directly
// @ts-ignore
if (detectedProvider && detectedProvider.on) {
// @ts-ignore
detectedProvider.on("chainChanged", (chainId) => {
try {
setChainId(BigNumber.from(chainId).toNumber());
} catch (e) {}
});
// @ts-ignore
detectedProvider.on("accountsChanged", (accounts) => {
try {
const signer = provider.getSigner();
setSigner(signer);
signer
.getAddress()
.then((address) => {
setSignerAddress(address);
})
.catch(() => {
setProviderError(
"An error occurred while getting the signer address"
);
});
} catch (e) {}
});
}
})
.catch(() => {
setProviderError(
"An error occurred while requesting eth accounts"
);
});
} else {
setProviderError("Please install MetaMask");
}
})
.catch(() => {
setProviderError("Please install MetaMask");
});
}, []);
const disconnect = useCallback(() => {
setProviderError(null);
setProvider(undefined);
setChainId(undefined);
setSigner(undefined);
setSignerAddress(undefined);
}, []);
const contextValue = useMemo(
() => ({
connect,
disconnect,
provider,
chainId,
signer,
signerAddress,
providerError,
}),
[
connect,
disconnect,
provider,
chainId,
signer,
signerAddress,
providerError,
]
);
return (
<EthereumProviderContext.Provider value={contextValue}>
{children}
</EthereumProviderContext.Provider>
);
};
export const useEthereumProvider = () => {
return useContext(EthereumProviderContext);
};

View File

@ -20,7 +20,7 @@ const LoggerProviderContext = React.createContext<LoggerContext>({
});
export const LoggerProvider = ({ children }: { children: ReactChildren }) => {
const [logs, setLogs] = useState<string[]>([]);
const [logs, setLogs] = useState<string[]>(["Instantiated the logger."]);
const clear = useCallback(() => setLogs([]), [setLogs]);
const { enqueueSnackbar } = useSnackbar();

View File

@ -0,0 +1,12 @@
import { useSnackbar } from "notistack";
import { useCallback } from "react";
import pushToClipboard from "../utils/pushToClipboard";
export default function useCopyToClipboard(content: string) {
const { enqueueSnackbar } = useSnackbar();
return useCallback(() => {
pushToClipboard(content)?.then(() => {
enqueueSnackbar("Copied", { variant: "success" });
});
}, [content, enqueueSnackbar]);
}

View File

@ -0,0 +1,168 @@
import {
Migrator,
Migrator__factory,
TokenImplementation,
TokenImplementation__factory,
} from "@certusone/wormhole-sdk";
import { Signer } from "@ethersproject/abstract-signer";
import { formatUnits } from "@ethersproject/units";
import { useEffect, useMemo, useState } from "react";
export type EthMigrationInfo = {
isLoading: boolean;
error: string;
data: RequisiteData | null;
};
export type RequisiteData = {
poolAddress: string;
fromAddress: string;
toAddress: string;
fromToken: TokenImplementation;
toToken: TokenImplementation;
migrator: Migrator;
fromSymbol: string;
toSymbol: string;
fromDecimals: number;
toDecimals: number;
sharesDecimals: number;
fromWalletBalance: string;
toWalletBalance: string;
fromPoolBalance: string;
toPoolBalance: string;
walletSharesBalance: string;
};
const getRequisiteData = async (
migrator: Migrator,
signer: Signer,
signerAddress: string
): Promise<RequisiteData> => {
try {
const poolAddress = migrator.address;
const fromAddress = await migrator.fromAsset();
const toAddress = await migrator.toAsset();
const fromToken = TokenImplementation__factory.connect(fromAddress, signer);
const toToken = TokenImplementation__factory.connect(toAddress, signer);
const fromSymbol = await fromToken.symbol();
const toSymbol = await toToken.symbol();
const fromDecimals = await (await migrator.fromDecimals()).toNumber();
const toDecimals = await (await migrator.toDecimals()).toNumber();
const sharesDecimals = await migrator.decimals();
const fromWalletBalance = formatUnits(
await fromToken.balanceOf(signerAddress),
fromDecimals
);
const toWalletBalance = formatUnits(
await toToken.balanceOf(signerAddress),
toDecimals
);
const fromPoolBalance = formatUnits(
await fromToken.balanceOf(poolAddress),
fromDecimals
);
const toPoolBalance = formatUnits(
await toToken.balanceOf(poolAddress),
toDecimals
);
const walletSharesBalance = formatUnits(
await migrator.balanceOf(signerAddress),
sharesDecimals
);
return {
poolAddress,
fromAddress,
toAddress,
fromToken,
toToken,
migrator,
fromSymbol,
toSymbol,
fromDecimals,
toDecimals,
fromWalletBalance,
toWalletBalance,
fromPoolBalance,
toPoolBalance,
walletSharesBalance,
sharesDecimals,
};
} catch (e) {
return Promise.reject("Failed to retrieve required data.");
}
};
function useEthereumMigratorInformation(
migratorAddress: string | undefined,
signer: Signer | undefined,
signerAddress: string | undefined,
toggleRefresh: boolean
): EthMigrationInfo {
const migrator = useMemo(
() =>
migratorAddress &&
signer &&
Migrator__factory.connect(migratorAddress, signer),
[migratorAddress, signer]
);
const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
useEffect(() => {
if (!signer || !migrator || !signerAddress) {
return;
}
let cancelled = false;
setIsLoading(true);
getRequisiteData(migrator, signer, signerAddress).then(
(result) => {
if (!cancelled) {
setData(result);
setIsLoading(false);
}
},
(error) => {
if (!cancelled) {
setIsLoading(false);
setError("Failed to retrieve necessary data.");
}
}
);
return () => {
cancelled = true;
return;
};
}, [migrator, signer, signerAddress, toggleRefresh]);
return useMemo(() => {
if (!migratorAddress || !signer || !signerAddress) {
return {
isLoading: false,
error:
!signer || !signerAddress
? "Wallet not connected"
: !migratorAddress
? "No contract address"
: "Error",
data: null,
};
} else {
return {
isLoading,
error,
data,
};
}
}, [isLoading, error, data, migratorAddress, signer, signerAddress]);
}
export default useEthereumMigratorInformation;

View File

@ -7,16 +7,19 @@ import { LoggerProvider } from "./contexts/Logger";
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext";
import { theme } from "./muiTheme";
import { SnackbarProvider } from "notistack";
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
ReactDOM.render(
<ErrorBoundary>
<ThemeProvider theme={theme}>
<CssBaseline />
<SolanaWalletProvider>
<SnackbarProvider maxSnack={3}>
<LoggerProvider>
<App />
</LoggerProvider>
</SnackbarProvider>
<EthereumProviderProvider>
<SnackbarProvider maxSnack={3}>
<LoggerProvider>
<App />
</LoggerProvider>
</SnackbarProvider>
</EthereumProviderProvider>
</SolanaWalletProvider>
</ThemeProvider>
</ErrorBoundary>,

View File

@ -0,0 +1,7 @@
export default function pushToClipboard(content: any) {
if (!navigator.clipboard) {
// Clipboard API not available
return;
}
return navigator.clipboard.writeText(content);
}

View File

@ -0,0 +1,116 @@
import { Migrator__factory } from "@certusone/wormhole-sdk";
import {
Button,
Container,
makeStyles,
Paper,
TextField,
Typography,
} from "@material-ui/core";
import { ethers } from "ethers";
import { useState } from "react";
import EthereumSignerKey from "../components/EthereumSignerKey";
import LogWatcher from "../components/LogWatcher";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useLogger } from "../contexts/Logger";
const useStyles = makeStyles(() => ({
rootContainer: {},
mainPaper: {
"& > *": {
margin: "1rem",
},
padding: "2rem",
},
divider: {
margin: "2rem",
},
spacer: {
height: "1rem",
},
}));
function DeployNewEthereum() {
const classes = useStyles();
const { signer, provider } = useEthereumProvider();
const { log } = useLogger();
const [migratorAddress, setMigratorAddress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [fromAddress, setFromAddress] = useState<string | null>(null);
const [toAddress, setToAddress] = useState<string | null>(null);
const errorMessage =
error ||
(!provider && "Wallet not connected") ||
(!fromAddress && "No 'from' address") ||
(!toAddress && "No 'to' address");
const deployPool = async () => {
if (fromAddress && toAddress) {
const contractInterface = Migrator__factory.createInterface();
const bytecode = Migrator__factory.bytecode;
const factory = new ethers.ContractFactory(
contractInterface,
bytecode,
signer
);
const contract = await factory.deploy(fromAddress, toAddress);
contract.deployed().then(
(result) => {
log("Successfully deployed contract at " + result.address);
setMigratorAddress(result.address);
},
(error) => {
log("Failed to deploy the contract");
setError((error && error.toString()) || "Unable to create the pool.");
}
);
} else {
}
};
return (
<>
<Container maxWidth="md" className={classes.rootContainer}>
<Paper className={classes.mainPaper}>
<Typography variant="h6">
Create a new Ethereum Liquidity Pool
</Typography>
<EthereumSignerKey />
<TextField
value={fromAddress}
onChange={(event) => setFromAddress(event.target.value)}
label={"From Token"}
fullWidth
style={{ display: "block" }}
/>
<TextField
value={toAddress}
onChange={(event) => setToAddress(event.target.value)}
label={"To Token"}
fullWidth
style={{ display: "block" }}
/>
<Button disabled={!!errorMessage} onClick={deployPool}>
Create
</Button>
{errorMessage && <Typography>{errorMessage}</Typography>}
{migratorAddress !== null && (
<>
<Typography>Successfully created a new pool at:</Typography>
<Typography variant="h5">{migratorAddress}</Typography>
<Typography>
You may now populate the pool from the Ethereum pool management
page.
</Typography>
</>
)}
</Paper>
<LogWatcher />
</Container>
</>
);
}
export default DeployNewEthereum;

104
lp_ui/src/views/Home.tsx Normal file
View File

@ -0,0 +1,104 @@
import { AppBar, Button, Divider, Typography } from "@material-ui/core";
import { useCallback, useState } from "react";
import { default as DeployNewEthereum } from "./DeployNewEthereum";
import MigrateEthereum from "./MigrateEthereum";
import Main from "./Main";
import { CLUSTER } from "../utils/consts";
const ETH = "Interact with an existing Ethereum pool";
const NEW_ETH = "Create a New Ethereum Pool";
const SOL = "Manage Solana Liquidity pools.";
function Home() {
const [displayedView, setDisplayedView] = useState<string | null>(null);
const setEth = useCallback(() => {
setDisplayedView(ETH);
}, []);
const setNewEth = useCallback(() => {
setDisplayedView(NEW_ETH);
}, []);
const setSol = useCallback(() => {
setDisplayedView(SOL);
}, []);
const clear = useCallback(() => {
setDisplayedView(null);
}, []);
const backHeader = (
<>
<div style={{ padding: ".5rem", textAlign: "center" }}>
<Typography variant="h5">{displayedView}</Typography>
<Button onClick={clear} variant="contained" color="default">
Back
</Button>
</div>
<Divider />
</>
);
const content =
displayedView === null ? (
<div style={{ textAlign: "center", padding: "1rem" }}>
<Typography variant="h5">
Which action would you like to perform?
</Typography>
<div style={{ margin: "2rem" }}>
<Button
style={{ margin: ".5rem" }}
variant="contained"
onClick={setEth}
>
{ETH}
</Button>
<Button
style={{ margin: ".5rem" }}
variant="contained"
onClick={setNewEth}
>
{NEW_ETH}
</Button>
<Button
style={{ margin: ".5rem" }}
variant="contained"
onClick={setSol}
>
{SOL}
</Button>
</div>
</div>
) : displayedView === ETH ? (
<>
{backHeader}
<MigrateEthereum />
</>
) : displayedView === NEW_ETH ? (
<>
{backHeader}
<DeployNewEthereum />
</>
) : displayedView === SOL ? (
<>
{backHeader}
<Main />
</>
) : null;
return (
<>
{CLUSTER === "mainnet" ? null : (
<AppBar position="static" color="secondary">
<Typography style={{ textAlign: "center" }}>
Caution! You are using the {CLUSTER} build of this app.
</Typography>
</AppBar>
)}
{content}
</>
);
}
export default Home;

View File

@ -1,24 +1,23 @@
import addLiquidityTx from "@certusone/wormhole-sdk/lib/migration/addLiquidity";
import getAuthorityAddress from "@certusone/wormhole-sdk/lib/migration/authorityAddress";
import claimSharesTx from "@certusone/wormhole-sdk/lib/migration/claimShares";
import removeLiquidityTx from "@certusone/wormhole-sdk/lib/migration/removeLiquidity";
import createPoolAccount from "@certusone/wormhole-sdk/lib/migration/createPool";
import getFromCustodyAddress from "@certusone/wormhole-sdk/lib/migration/fromCustodyAddress";
import migrateTokensTx from "@certusone/wormhole-sdk/lib/migration/migrateTokens";
import parsePool from "@certusone/wormhole-sdk/lib/migration/parsePool";
import getPoolAddress from "@certusone/wormhole-sdk/lib/migration/poolAddress";
import removeLiquidityTx from "@certusone/wormhole-sdk/lib/migration/removeLiquidity";
import getShareMintAddress from "@certusone/wormhole-sdk/lib/migration/shareMintAddress";
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
import {
Button,
CircularProgress,
Container,
Divider,
makeStyles,
Paper,
TextField,
Typography,
CircularProgress,
AppBar,
} from "@material-ui/core";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
@ -36,11 +35,7 @@ import SolanaCreateAssociatedAddress, {
import SolanaWalletKey from "../components/SolanaWalletKey";
import { useLogger } from "../contexts/Logger";
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
import {
CLUSTER,
MIGRATION_PROGRAM_ADDRESS,
SOLANA_URL,
} from "../utils/consts";
import { MIGRATION_PROGRAM_ADDRESS, SOLANA_URL } from "../utils/consts";
import { getMultipleAccounts, signSendAndConfirm } from "../utils/solana";
const useStyles = makeStyles(() => ({
@ -59,7 +54,7 @@ const useStyles = makeStyles(() => ({
},
}));
const compareWithDecimalOffset = (
export const compareWithDecimalOffset = (
valueA: string,
decimalsA: number,
valueB: string,
@ -1002,13 +997,6 @@ function Main() {
return (
<>
{CLUSTER === "mainnet" ? null : (
<AppBar position="static" color="secondary">
<Typography style={{ textAlign: "center" }}>
Caution! You are using the {CLUSTER} build of this app.
</Typography>
</AppBar>
)}
<Container maxWidth="md" className={classes.rootContainer}>
<Paper className={classes.mainPaper}>
<SolanaWalletKey />

View File

@ -0,0 +1,431 @@
import { CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
import {
Button,
CircularProgress,
Container,
Divider,
makeStyles,
Paper,
TextField,
Typography,
} from "@material-ui/core";
//import { pool_address } from "@certusone/wormhole-sdk/lib/solana/migration/wormhole_migration";
import { parseUnits } from "ethers/lib/utils";
import { useCallback, useState } from "react";
import EthereumSignerKey from "../components/EthereumSignerKey";
import LogWatcher from "../components/LogWatcher";
import SmartAddress from "../components/SmartAddress";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
import { useLogger } from "../contexts/Logger";
import useEthereumMigratorInformation from "../hooks/useEthereumMigratorInformation";
import { compareWithDecimalOffset } from "./Main";
const useStyles = makeStyles(() => ({
rootContainer: {},
mainPaper: {
"& > *": {
margin: "1rem",
},
padding: "2rem",
},
divider: {
margin: "2rem",
},
spacer: {
height: "1rem",
},
}));
function MigrateEthereum() {
const classes = useStyles();
const { signer, signerAddress } = useEthereumProvider();
const { log } = useLogger();
const [migratorAddress, setMigratorAddress] = useState("");
const [refresher, setRefresher] = useState(false);
const forceRefresh = useCallback(() => {
setRefresher((prevState) => !prevState);
}, []);
const poolInfo = useEthereumMigratorInformation(
migratorAddress,
signer,
signerAddress,
refresher
);
const info = poolInfo.data;
const [liquidityAmount, setLiquidityAmount] = useState("");
const [removeLiquidityAmount, setRemoveLiquidityAmount] = useState("");
const [migrationAmount, setMigrationAmount] = useState("");
const [redeemAmount, setRedeemAmount] = useState("");
const [liquidityIsProcessing, setLiquidityIsProcessing] = useState(false);
const [removeLiquidityIsProcessing, setRemoveLiquidityIsProcessing] =
useState(false);
const [migrationIsProcessing, setMigrationIsProcessing] = useState(false);
const [redeemIsProcessing, setRedeemIsProcessing] = useState(false);
const addLiquidity = useCallback(async () => {
if (!info) {
return;
}
try {
setLiquidityIsProcessing(true);
await info.toToken.approve(
info.migrator.address,
parseUnits(liquidityAmount, info.toDecimals)
);
const transaction = await info.migrator.add(
parseUnits(liquidityAmount, info.toDecimals)
);
await transaction.wait();
forceRefresh();
log(`Successfully added liquidity to the pool.`, "success");
setLiquidityIsProcessing(false);
} catch (e) {
console.error(e);
log(`Could not add liquidity to the pool.`, "error");
setLiquidityIsProcessing(false);
}
}, [info, liquidityAmount, log, forceRefresh]);
const removeLiquidity = useCallback(async () => {
if (!info) {
return;
}
try {
setRemoveLiquidityIsProcessing(true);
const transaction = await info.migrator.remove(
parseUnits(removeLiquidityAmount, info.sharesDecimals)
);
await transaction.wait();
forceRefresh();
log(`Successfully removed liquidity from the pool.`, "success");
setRemoveLiquidityIsProcessing(false);
} catch (e) {
console.error(e);
log(`Could not remove liquidity from the pool.`, "error");
setRemoveLiquidityIsProcessing(false);
}
}, [info, removeLiquidityAmount, log, forceRefresh]);
const migrateTokens = useCallback(async () => {
if (!info) {
return;
}
try {
setMigrationIsProcessing(true);
await info.fromToken.approve(
info.migrator.address,
parseUnits(migrationAmount, info.fromDecimals)
);
const transaction = await info.migrator.migrate(
parseUnits(migrationAmount, info.fromDecimals)
);
await transaction.wait();
forceRefresh();
log(`Successfully migrated tokens.`, "success");
setMigrationIsProcessing(false);
} catch (e) {
console.error(e);
log(`Could not migrate the tokens.`, "error");
setMigrationIsProcessing(false);
}
}, [info, migrationAmount, log, forceRefresh]);
const redeemShares = useCallback(async () => {
if (!info) {
return;
}
try {
setRedeemIsProcessing(true);
const transaction = await info.migrator.claim(
parseUnits(redeemAmount, info.sharesDecimals)
);
await transaction.wait();
forceRefresh();
log(`Successfully redeemed shares.`, "success");
setRedeemIsProcessing(false);
} catch (e) {
console.error(e);
log(`Could not redeem shares.`, "error");
setRedeemIsProcessing(false);
}
}, [info, redeemAmount, log, forceRefresh]);
const addToTokensInWallet =
info &&
liquidityAmount &&
compareWithDecimalOffset(
liquidityAmount,
info.toDecimals,
info.toWalletBalance,
info.toDecimals
) !== 1;
const addLiquidityIsReady = addToTokensInWallet;
const addLiquidityUI = (
<>
<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={liquidityAmount}
type="number"
onChange={(event) => setLiquidityAmount(event.target.value)}
label={"Amount to add"}
></TextField>
<Button
variant="contained"
onClick={addLiquidity}
disabled={liquidityIsProcessing || !addLiquidityIsReady}
>
Add Liquidity
</Button>
{liquidityIsProcessing ? <CircularProgress /> : null}
</>
);
const removeToTokensInPool =
info &&
removeLiquidityAmount &&
compareWithDecimalOffset(
removeLiquidityAmount,
info.sharesDecimals,
info.toPoolBalance,
info.toDecimals
) !== 1;
const removeShareTokensInWallet =
info &&
removeLiquidityAmount &&
compareWithDecimalOffset(
removeLiquidityAmount,
info.sharesDecimals,
info.walletSharesBalance,
info.sharesDecimals
) !== 1;
const removeLiquidityIsReady =
removeShareTokensInWallet && removeToTokensInPool;
const removeLiquidityUI = (
<>
<Typography variant="h4">Remove Liquidity</Typography>
<Typography variant="body1">
This will remove 'Share' tokens from your wallet, and give you an equal
number of 'To' tokens.
</Typography>
<TextField
value={removeLiquidityAmount}
type="number"
onChange={(event) => setRemoveLiquidityAmount(event.target.value)}
label={"Amount to remove"}
></TextField>
<Button
variant="contained"
onClick={removeLiquidity}
disabled={removeLiquidityIsProcessing || !removeLiquidityIsReady}
>
Remove Liquidity
</Button>
{removeLiquidityIsProcessing ? <CircularProgress /> : null}
</>
);
const migrateToTokensInPool =
info &&
migrationAmount &&
compareWithDecimalOffset(
migrationAmount,
info.fromDecimals,
info.toPoolBalance,
info.toDecimals
) !== 1;
const migrateFromTokensInWallet =
info &&
migrationAmount &&
compareWithDecimalOffset(
migrationAmount,
info.fromDecimals,
info.fromWalletBalance,
info.fromDecimals
) !== 1;
const migrateIsReady = migrateFromTokensInWallet && migrateToTokensInPool;
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 migrate"}
></TextField>
<Button
variant="contained"
onClick={migrateTokens}
disabled={migrationIsProcessing || !migrateIsReady}
>
Migrate Tokens
</Button>
{migrationIsProcessing ? <CircularProgress /> : null}
</>
);
const redeemSharesInWallet =
info &&
redeemAmount &&
compareWithDecimalOffset(
redeemAmount,
info.sharesDecimals,
info.walletSharesBalance,
info.sharesDecimals
) !== 1;
const redeemFromTokensInPool =
info &&
redeemAmount &&
compareWithDecimalOffset(
redeemAmount,
info.sharesDecimals,
info.fromPoolBalance,
info.fromDecimals
) !== 1;
const redeemIsReady = redeemSharesInWallet && redeemFromTokensInPool;
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 redeem"}
></TextField>
<Button
variant="contained"
onClick={redeemShares}
disabled={redeemIsProcessing || !redeemIsReady}
>
Redeem Shares
</Button>
{redeemIsProcessing ? <CircularProgress /> : null}
</>
);
const topContent = (
<>
<Typography variant="h6">Manage an Ethereum Pool</Typography>
<EthereumSignerKey />
<TextField
value={migratorAddress}
onChange={(event) => setMigratorAddress(event.target.value)}
label={"Migrator Address"}
fullWidth
style={{ display: "block" }}
/>
</>
);
const infoDisplay = poolInfo.isLoading ? (
<CircularProgress />
) : poolInfo.error ? (
<Typography>{poolInfo.error}</Typography>
) : !poolInfo.data ? null : (
<>
<div style={{ display: "flex" }}>
<div>
<Typography variant="h5">Pool Balances</Typography>
<Typography>
{`'From' Asset: `}
{info?.fromPoolBalance}
<SmartAddress
chainId={CHAIN_ID_ETH}
address={info?.fromAddress}
symbol={info?.fromSymbol}
/>
</Typography>
<Typography>
{`'To' Asset: `}
{info?.toPoolBalance}
<SmartAddress
chainId={CHAIN_ID_ETH}
address={info?.toAddress}
symbol={info?.toSymbol}
/>
</Typography>
</div>
<div style={{ flexGrow: 1 }} />
<div>
<Typography variant="h5">Connected Wallet Balances</Typography>
<Typography>
{`'From' Asset: `}
{info?.fromWalletBalance}
<SmartAddress
chainId={CHAIN_ID_ETH}
address={info?.fromAddress}
symbol={info?.fromSymbol}
/>
</Typography>
<Typography>
{`'To' Asset: `}
{info?.toWalletBalance}
<SmartAddress
chainId={CHAIN_ID_ETH}
address={info?.toAddress}
symbol={info?.toSymbol}
/>
</Typography>
<Typography>
{`'Shares' Asset: `}
{info?.walletSharesBalance}
<SmartAddress chainId={CHAIN_ID_ETH} address={info?.poolAddress} />
</Typography>
</div>
</div>
<Button onClick={forceRefresh} variant="contained" color="primary">
Force Refresh
</Button>
</>
);
const actionPanel = poolInfo.data ? (
<>
{addLiquidityUI}
<Divider className={classes.divider} />
{removeLiquidityUI}
<Divider className={classes.divider} />
{redeemSharesUI}
<Divider className={classes.divider} />
{migrateTokensUI}
</>
) : null;
return (
<>
<Container maxWidth="md" className={classes.rootContainer}>
<Paper className={classes.mainPaper}>
{topContent}
{infoDisplay && (
<>
<Divider className={classes.divider} />
{infoDisplay}
</>
)}
{actionPanel && (
<>
<Divider className={classes.divider} />
{actionPanel}
</>
)}
</Paper>
<LogWatcher />
</Container>
</>
);
}
export default MigrateEthereum;