lp_ui: ethereum migration pool functionality
Change-Id: Ibdf24e1f90e711e5284016045c0c7d9d413be4ac
This commit is contained in:
parent
7e6123a3a8
commit
d8a8d5722a
|
@ -7,12 +7,12 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "lp_ui",
|
"name": "lp_ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@certusone/wormhole-sdk": "file:..\\sdk\\js",
|
"@certusone/wormhole-sdk": "file:..\\sdk\\js",
|
||||||
"@material-ui/core": "^4.12.2",
|
"@material-ui/core": "^4.12.2",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||||
|
"@metamask/detect-provider": "^1.2.0",
|
||||||
"@solana/spl-token": "^0.1.6",
|
"@solana/spl-token": "^0.1.6",
|
||||||
"@solana/spl-token-registry": "^0.2.216",
|
"@solana/spl-token-registry": "^0.2.216",
|
||||||
"@solana/wallet-adapter-base": "^0.5.2",
|
"@solana/wallet-adapter-base": "^0.5.2",
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
"@types/node": "^16.9.1",
|
"@types/node": "^16.9.1",
|
||||||
"@types/react": "^17.0.20",
|
"@types/react": "^17.0.20",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
"ethers": "^5.4.6",
|
"ethers": "^5.4.6",
|
||||||
"notistack": "^1.0.10",
|
"notistack": "^1.0.10",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
@ -39,7 +40,7 @@
|
||||||
},
|
},
|
||||||
"../sdk/js": {
|
"../sdk/js": {
|
||||||
"name": "@certusone/wormhole-sdk",
|
"name": "@certusone/wormhole-sdk",
|
||||||
"version": "0.0.2",
|
"version": "0.0.5",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@improbable-eng/grpc-web": "^0.14.0",
|
"@improbable-eng/grpc-web": "^0.14.0",
|
||||||
|
@ -3542,6 +3543,14 @@
|
||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
"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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"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"
|
"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": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"@material-ui/core": "^4.12.2",
|
"@material-ui/core": "^4.12.2",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||||
|
"@metamask/detect-provider": "^1.2.0",
|
||||||
"@solana/spl-token": "^0.1.6",
|
"@solana/spl-token": "^0.1.6",
|
||||||
"@solana/spl-token-registry": "^0.2.216",
|
"@solana/spl-token-registry": "^0.2.216",
|
||||||
"@solana/wallet-adapter-base": "^0.5.2",
|
"@solana/wallet-adapter-base": "^0.5.2",
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
"@types/node": "^16.9.1",
|
"@types/node": "^16.9.1",
|
||||||
"@types/react": "^17.0.20",
|
"@types/react": "^17.0.20",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
"ethers": "^5.4.6",
|
"ethers": "^5.4.6",
|
||||||
"notistack": "^1.0.10",
|
"notistack": "^1.0.10",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Main from "./views/Main";
|
import Home from "./views/Home";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <Main />;
|
return <Home />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -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;
|
|
@ -1,13 +1,8 @@
|
||||||
import { Button, Paper, Typography } from "@material-ui/core";
|
import { Button, Paper, Typography } from "@material-ui/core";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useLogger } from "../contexts/Logger";
|
import { useLogger } from "../contexts/Logger";
|
||||||
|
|
||||||
function LogWatcher() {
|
function LogWatcher() {
|
||||||
const { logs, clear, log } = useLogger();
|
const { logs, clear } = useLogger();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
log("Instantiated the logger.");
|
|
||||||
}, [log]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper style={{ padding: "1rem", maxHeight: "600px", overflow: "auto" }}>
|
<Paper style={{ padding: "1rem", maxHeight: "600px", overflow: "auto" }}>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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);
|
||||||
|
};
|
|
@ -20,7 +20,7 @@ const LoggerProviderContext = React.createContext<LoggerContext>({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LoggerProvider = ({ children }: { children: ReactChildren }) => {
|
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 clear = useCallback(() => setLogs([]), [setLogs]);
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
|
}
|
|
@ -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;
|
|
@ -7,16 +7,19 @@ import { LoggerProvider } from "./contexts/Logger";
|
||||||
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext";
|
import { SolanaWalletProvider } from "./contexts/SolanaWalletContext";
|
||||||
import { theme } from "./muiTheme";
|
import { theme } from "./muiTheme";
|
||||||
import { SnackbarProvider } from "notistack";
|
import { SnackbarProvider } from "notistack";
|
||||||
|
import { EthereumProviderProvider } from "./contexts/EthereumProviderContext";
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<SolanaWalletProvider>
|
<SolanaWalletProvider>
|
||||||
<SnackbarProvider maxSnack={3}>
|
<EthereumProviderProvider>
|
||||||
<LoggerProvider>
|
<SnackbarProvider maxSnack={3}>
|
||||||
<App />
|
<LoggerProvider>
|
||||||
</LoggerProvider>
|
<App />
|
||||||
</SnackbarProvider>
|
</LoggerProvider>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</EthereumProviderProvider>
|
||||||
</SolanaWalletProvider>
|
</SolanaWalletProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function pushToClipboard(content: any) {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
// Clipboard API not available
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return navigator.clipboard.writeText(content);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,24 +1,23 @@
|
||||||
import addLiquidityTx from "@certusone/wormhole-sdk/lib/migration/addLiquidity";
|
import addLiquidityTx from "@certusone/wormhole-sdk/lib/migration/addLiquidity";
|
||||||
import getAuthorityAddress from "@certusone/wormhole-sdk/lib/migration/authorityAddress";
|
import getAuthorityAddress from "@certusone/wormhole-sdk/lib/migration/authorityAddress";
|
||||||
import claimSharesTx from "@certusone/wormhole-sdk/lib/migration/claimShares";
|
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 createPoolAccount from "@certusone/wormhole-sdk/lib/migration/createPool";
|
||||||
import getFromCustodyAddress from "@certusone/wormhole-sdk/lib/migration/fromCustodyAddress";
|
import getFromCustodyAddress from "@certusone/wormhole-sdk/lib/migration/fromCustodyAddress";
|
||||||
import migrateTokensTx from "@certusone/wormhole-sdk/lib/migration/migrateTokens";
|
import migrateTokensTx from "@certusone/wormhole-sdk/lib/migration/migrateTokens";
|
||||||
import parsePool from "@certusone/wormhole-sdk/lib/migration/parsePool";
|
import parsePool from "@certusone/wormhole-sdk/lib/migration/parsePool";
|
||||||
import getPoolAddress from "@certusone/wormhole-sdk/lib/migration/poolAddress";
|
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 getShareMintAddress from "@certusone/wormhole-sdk/lib/migration/shareMintAddress";
|
||||||
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
|
import getToCustodyAddress from "@certusone/wormhole-sdk/lib/migration/toCustodyAddress";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
CircularProgress,
|
|
||||||
AppBar,
|
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import {
|
import {
|
||||||
ASSOCIATED_TOKEN_PROGRAM_ID,
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
@ -36,11 +35,7 @@ import SolanaCreateAssociatedAddress, {
|
||||||
import SolanaWalletKey from "../components/SolanaWalletKey";
|
import SolanaWalletKey from "../components/SolanaWalletKey";
|
||||||
import { useLogger } from "../contexts/Logger";
|
import { useLogger } from "../contexts/Logger";
|
||||||
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
import { useSolanaWallet } from "../contexts/SolanaWalletContext";
|
||||||
import {
|
import { MIGRATION_PROGRAM_ADDRESS, SOLANA_URL } from "../utils/consts";
|
||||||
CLUSTER,
|
|
||||||
MIGRATION_PROGRAM_ADDRESS,
|
|
||||||
SOLANA_URL,
|
|
||||||
} from "../utils/consts";
|
|
||||||
import { getMultipleAccounts, signSendAndConfirm } from "../utils/solana";
|
import { getMultipleAccounts, signSendAndConfirm } from "../utils/solana";
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
|
@ -59,7 +54,7 @@ const useStyles = makeStyles(() => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const compareWithDecimalOffset = (
|
export const compareWithDecimalOffset = (
|
||||||
valueA: string,
|
valueA: string,
|
||||||
decimalsA: number,
|
decimalsA: number,
|
||||||
valueB: string,
|
valueB: string,
|
||||||
|
@ -1002,13 +997,6 @@ function Main() {
|
||||||
|
|
||||||
return (
|
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}>
|
<Container maxWidth="md" className={classes.rootContainer}>
|
||||||
<Paper className={classes.mainPaper}>
|
<Paper className={classes.mainPaper}>
|
||||||
<SolanaWalletKey />
|
<SolanaWalletKey />
|
||||||
|
|
|
@ -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;
|
Loading…
Reference in New Issue