UI - added circle loader and swap progress bar

This commit is contained in:
Kevin Peters 2022-01-23 18:21:23 +00:00
parent 76f4c63198
commit 89c118a37f
8 changed files with 344 additions and 78 deletions

View File

@ -1 +1,3 @@
.env
build/
node_modules/

View File

@ -1,5 +1,5 @@
{
"name": "Cross Chain Swap",
"name": "NativeSwap",
"version": "0.1.0",
"private": true,
"dependencies": {

View File

@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Cross Chain Swap</title>
<title>NativeSwap</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -0,0 +1,11 @@
import "../css/CircleLoader.css";
export default function CircleLoader() {
return (
<div id={"loaderContainer"}>
<div id={"circle"}>
<div id={"inner"}></div>
</div>
</div>
);
}

View File

@ -0,0 +1,89 @@
import { ChainId, CHAIN_ID_POLYGON, isEVMChain } from "@certusone/wormhole-sdk";
import { LinearProgress, makeStyles, Typography } from "@material-ui/core";
import { useEffect, useState } from "react";
import { useEthereumProvider } from "../contexts/EthereumProviderContext";
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(2),
textAlign: "center",
},
message: {
marginTop: theme.spacing(1),
},
}));
export default function TransactionProgress({
chainId,
txBlockNumber,
step,
}: {
chainId: ChainId;
txBlockNumber: number | undefined;
step: number;
}) {
const classes = useStyles();
const { provider } = useEthereumProvider();
const [currentBlock, setCurrentBlock] = useState(0);
useEffect(() => {
if (step !== 1 || !txBlockNumber) return;
if (isEVMChain(chainId) && provider) {
let cancelled = false;
(async () => {
while (!cancelled) {
await new Promise((resolve) => setTimeout(resolve, 500));
try {
const newBlock = await provider.getBlockNumber();
if (!cancelled) {
setCurrentBlock(newBlock);
}
} catch (e) {
console.error(e);
}
}
})();
return () => {
cancelled = true;
};
}
}, [step, chainId, provider, txBlockNumber]);
const blockDiff =
txBlockNumber !== undefined && txBlockNumber && currentBlock
? currentBlock - txBlockNumber
: 0;
const expectedBlocks = 15;
let value;
let valueBuffer;
let message;
switch (step) {
case 1:
value = (blockDiff / expectedBlocks) * 50;
valueBuffer = 50;
message = `Waiting for ${blockDiff} / ${expectedBlocks} confirmations on ${
chainId === CHAIN_ID_POLYGON ? "Polygon" : "Ethereum"
}...`;
break;
case 2:
value = 50;
valueBuffer = 100;
message = "Waiting for relayer to complete swap...";
break;
case 3:
value = 100;
valueBuffer = 100;
message = "";
break;
}
return (
<div className={classes.root}>
<LinearProgress
variant="buffer"
value={value}
valueBuffer={valueBuffer}
/>
<Typography variant="body2" className={classes.message}>
{message}
</Typography>
</div>
);
}

View File

@ -0,0 +1,90 @@
:root {
--basis: linear-gradient(
160deg,
rgba(69, 74, 117, 0.473) 0%,
rgba(98, 104, 143, 0.445) 100%
),
linear-gradient(
45deg,
rgba(153, 69, 255, 0.411) 0%,
rgba(0, 209, 139, 0.404) 100%
);
/* --gradient1: rgb(26, 212, 150); */
/* --gradient2: rgb(176, 139, 221); */
--gradient1: rgb(117, 228, 187);
--gradient2: rgb(193, 164, 230);
}
#loaderContainer {
display: flex;
width: max-content;
height: max-content;
justify-content: center;
align-items: center;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes switch {
0% {
top: 50%;
transform: translateX(-50%) translateY(-50%);
width: 200px;
height: 200px;
box-shadow: 0 -130px 0 -75px var(--gradient1);
}
25% {
top: 50%;
transform: translateX(-50%) translateY(-50%);
width: 200px;
height: 200px;
box-shadow: 0 -130px 0 -75px var(--gradient1);
}
50% {
top: calc(100% - 55px);
width: 50px;
height: 50px;
box-shadow: 0 -130px 0 75px var(--gradient1);
transform: translateX(-50%) translateY(0);
}
75% {
top: calc(100% - 55px);
width: 50px;
height: 50px;
box-shadow: 0 -130px 0 75px var(--gradient1);
transform: translateX(-50%) translateY(0);
}
100% {
top: 50%;
transform: translateX(-50%) translateY(-50%);
width: 200px;
height: 200px;
box-shadow: 0 -130px 0 -75px var(--gradient1);
}
}
#circle {
width: 325px;
height: 325px;
display: block;
background: var(--basis);
border-radius: 500%;
position: relative;
animation: rotation 2s linear infinite;
}
#inner {
width: 200px;
height: 200px;
background: var(--gradient2);
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
border-radius: 100%;
box-shadow: 0 -130px 0 -75px #222;
animation: switch 8s ease-in-out infinite;
}

View File

@ -17,7 +17,6 @@ export default async function getIsTransferCompletedEvmWithRetry(
provider,
signedVAA
);
console.log("getIsTransferCompletedEth", result);
} catch (e) {
console.error(e);
}

View File

@ -1,4 +1,5 @@
import {
Collapse,
Container,
Link,
makeStyles,
@ -33,6 +34,9 @@ import { Alert } from "@material-ui/lab";
import parseError from "../utils/parseError";
import Settings from "../components/Settings";
import getIsTransferCompletedEvmWithRetry from "../utils/getIsTransferCompletedWithRetry";
import CircleLoader from "../components/CircleLoader";
import { ArrowForward, CheckCircleOutlineRounded } from "@material-ui/icons";
import SwapProgress from "../components/SwapProgress";
const useStyles = makeStyles((theme) => ({
bg: {
@ -115,6 +119,16 @@ const useStyles = makeStyles((theme) => ({
margin: "1rem",
display: "inline-block",
},
loaderHolder: {
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center",
},
successIcon: {
color: COLORS.green,
fontSize: "200px",
},
}));
const switchProviderNetwork = async (
@ -136,10 +150,11 @@ const switchProviderNetwork = async (
export default function Home() {
const classes = useStyles();
const [sourceTokenInfo, setSourceTokenInfo] = useState(WMATIC_TOKEN_INFO);
const [targetTokenInfo, setTargetTokenInfo] = useState(WETH_TOKEN_INFO);
const [sourceTokenInfo, setSourceTokenInfo] = useState(MATIC_TOKEN_INFO);
const [targetTokenInfo, setTargetTokenInfo] = useState(ETH_TOKEN_INFO);
const [amountIn, setAmountIn] = useState("");
const [amountOut, setAmountOut] = useState("0.0");
const [amountInUST, setAmountInUST] = useState("");
const [amountOut, setAmountOut] = useState("");
const [deadline, setDeadline] = useState("30");
const [slippage, setSlippage] = useState("1");
const [executor, setExecutor] = useState<UniswapToUniswapExecutor | null>(
@ -150,18 +165,25 @@ export default function Home() {
const [hasQuote, setHasQuote] = useState(false);
const { provider, signer } = useEthereumProvider();
const { enqueueSnackbar } = useSnackbar();
const [isFirstSwapComplete, setIsFirstSwapComplete] = useState(false);
const [isSecondSwapComplete, setIsSecondSwapComplete] = useState(false);
const [sourceTxBlockNumber, setSourceTxBlockNumber] = useState<
number | undefined
>(undefined);
const [hasSignedVAA, setHasSignedVAA] = useState(false);
const [relayerTimeoutString, setRelayerTimeoutString] = useState("");
const computeQuote = useCallback(() => {
(async () => {
setHasQuote(false);
setIsComputingQuote(true);
setAmountOut("");
try {
if (
parseFloat(amountIn) > 0 &&
!isNaN(parseFloat(deadline)) &&
!isNaN(parseFloat(slippage))
) {
setAmountOut("0.0");
const executor = new UniswapToUniswapExecutor();
await executor.initialize(
sourceTokenInfo.address,
@ -184,9 +206,12 @@ export default function Home() {
executor.tokens.dstOut.formatAmount(quote.dst.minAmountOut)
).toFixed(8)
);
setAmountInUST(
parseFloat(
executor.tokens.dstIn.formatAmount(quote.dst.amountIn)
).toFixed(2)
);
setHasQuote(true);
} else {
setAmountOut("0.0");
}
} catch (e) {
console.error(e);
@ -246,13 +271,28 @@ export default function Home() {
setTargetTokenInfo(ETH_TOKEN_INFO);
}
setAmountIn("");
setAmountOut("0.0");
setAmountOut("");
}, []);
const reset = useCallback(() => {
setIsSwapping(false);
setHasQuote(false);
setIsFirstSwapComplete(false);
setIsSecondSwapComplete(false);
setAmountIn("");
setAmountOut("");
setSourceTxBlockNumber(undefined);
setRelayerTimeoutString("");
}, []);
const handleSwapClick = useCallback(async () => {
if (provider && signer && executor) {
try {
setIsSwapping(true);
setIsFirstSwapComplete(false);
setHasSignedVAA(false);
setIsSecondSwapComplete(false);
setRelayerTimeoutString("");
await switchProviderNetwork(provider, sourceTokenInfo.chainId);
const sourceReceipt = await executor.approveAndSwap(signer);
@ -260,24 +300,17 @@ export default function Home() {
"firstSwapTransactionHash:",
sourceReceipt.transactionHash
);
setIsFirstSwapComplete(true);
setSourceTxBlockNumber(sourceReceipt.blockNumber);
// Wait for the guardian network to reach consensus and emit the signedVAA
enqueueSnackbar(null, {
content: <Alert severity="info">Fetching VAA</Alert>,
});
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
executor.srcExecutionParams.wormhole.chainId,
executor.vaaSearchParams.emitterAddress,
executor.vaaSearchParams.sequence
);
enqueueSnackbar(null, {
content: (
<Alert severity="info">
Fetched the Signed VAA, waiting for relayer to redeem it
</Alert>
),
});
setHasSignedVAA(true);
// Check if the signedVAA has redeemed by the relayer
const isCompleted = await getIsTransferCompletedEvmWithRetry(
executor.dstExecutionParams.wormhole.tokenBridgeAddress,
@ -287,30 +320,26 @@ export default function Home() {
3000,
40
);
if (isCompleted) {
enqueueSnackbar(null, {
content: <Alert severity="success">Swap completed</Alert>,
});
} else {
if (!isCompleted) {
// If the relayer hasn't redeemed the signedVAA, then manually redeem it ourselves
setRelayerTimeoutString(
"Timed out waiting for relayer to complete swap. You'll need to complete it yourself."
);
await switchProviderNetwork(provider, targetTokenInfo.chainId);
const targetReceipt = await executor.fetchVaaAndSwap(signer);
enqueueSnackbar(null, {
content: <Alert severity="success">Swap completed</Alert>,
});
console.info(
"secondSwapTransactionHash:",
targetReceipt.transactionHash
);
}
setIsSecondSwapComplete(true);
} catch (e: any) {
reset();
console.error(e);
enqueueSnackbar(null, {
content: <Alert severity="error">{parseError(e)}</Alert>,
});
}
setIsSwapping(false);
setAmountIn("");
}
}, [
provider,
@ -319,6 +348,7 @@ export default function Home() {
enqueueSnackbar,
sourceTokenInfo,
targetTokenInfo,
reset,
]);
const readyToSwap = provider && signer && hasQuote;
@ -328,58 +358,103 @@ export default function Home() {
<Container className={classes.centeredContainer} maxWidth="sm">
<div className={classes.titleBar}></div>
<Typography variant="h4" color="textSecondary">
Cross Chain Swap Demo
Wormhole NativeSwap Demo
</Typography>
<div className={classes.spacer} />
<Paper className={classes.mainPaper}>
<Settings
disabled={isSwapping || isComputingQuote}
slippage={slippage}
deadline={deadline}
onSlippageChange={handleSlippageChange}
onDeadlineChange={handleDeadlineChange}
/>
<TokenSelect
tokens={TOKEN_INFOS}
value={sourceTokenInfo.name}
onChange={handleSourceChange}
disabled={isSwapping || isComputingQuote}
></TokenSelect>
<Typography variant="subtitle1">Send</Typography>
<TextField
type="number"
value={amountIn}
disabled={isSwapping || isComputingQuote}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
onChange={handleAmountChange}
placeholder="0.0"
></TextField>
<Collapse in={!isFirstSwapComplete}>
<Settings
disabled={isSwapping || isComputingQuote}
slippage={slippage}
deadline={deadline}
onSlippageChange={handleSlippageChange}
onDeadlineChange={handleDeadlineChange}
/>
<TokenSelect
tokens={TOKEN_INFOS}
value={sourceTokenInfo.name}
onChange={handleSourceChange}
disabled={isSwapping || isComputingQuote}
></TokenSelect>
<Typography variant="subtitle1">Send</Typography>
<TextField
type="number"
value={amountIn}
disabled={isSwapping || isComputingQuote}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
onChange={handleAmountChange}
placeholder="0.0"
></TextField>
<div className={classes.spacer} />
<TokenSelect
tokens={TOKEN_INFOS}
value={targetTokenInfo.name}
onChange={() => {}}
disabled={true}
></TokenSelect>
<Typography variant="subtitle1">Receive (estimated)</Typography>
<TextField
type="number"
value={amountOut}
autoFocus={true}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
inputProps={{ readOnly: true }}
placeholder="0.0"
></TextField>
<Typography variant="subtitle2">{`Slippage tolerance: ${slippage}%`}</Typography>
{!isSwapping && <EthereumSignerKey />}
<ButtonWithLoader
disabled={!readyToSwap || isSwapping}
showLoader={isSwapping}
onClick={handleSwapClick}
>
Swap
</ButtonWithLoader>
</Collapse>
<Collapse in={isFirstSwapComplete && !isSecondSwapComplete}>
<div className={classes.loaderHolder}>
<CircleLoader />
<div className={classes.spacer} />
<Typography variant="h5">
{`Your ${sourceTokenInfo.name} is being swapped to ${targetTokenInfo.name}`}
</Typography>
</div>
</Collapse>
<Collapse in={isSecondSwapComplete}>
<div className={classes.loaderHolder}>
<CheckCircleOutlineRounded
className={classes.successIcon}
fontSize={"inherit"}
/>
<Typography>Swap completed!</Typography>
<ButtonWithLoader onClick={() => reset()}>
Swap more tokens!
</ButtonWithLoader>
</div>
</Collapse>
<div className={classes.spacer} />
<TokenSelect
tokens={TOKEN_INFOS}
value={targetTokenInfo.name}
onChange={() => {}}
disabled={true}
></TokenSelect>
<Typography variant="subtitle1">Receive (estimated)</Typography>
<TextField
type="number"
value={amountOut}
autoFocus={true}
InputProps={{ disableUnderline: true }}
className={classes.numberField}
inputProps={{ readOnly: true }}
></TextField>
<Typography variant="subtitle2">{`Slippage tolerance: ${slippage}%`}</Typography>
{!isSwapping && <EthereumSignerKey />}
<ButtonWithLoader
disabled={!readyToSwap || isSwapping}
showLoader={isSwapping}
onClick={handleSwapClick}
>
Swap
</ButtonWithLoader>
{hasQuote && (
<Typography variant="subtitle1">
{`${amountIn} ${sourceTokenInfo.name} `}
<ArrowForward fontSize="inherit" />
{` ${amountInUST} UST `} <ArrowForward fontSize="inherit" />
{` ${amountOut} ${targetTokenInfo.name}`}
</Typography>
)}
{isFirstSwapComplete &&
!isSecondSwapComplete &&
!relayerTimeoutString && (
<SwapProgress
chainId={sourceTokenInfo.chainId}
txBlockNumber={sourceTxBlockNumber}
step={!hasSignedVAA ? 1 : !isSecondSwapComplete ? 2 : 3}
/>
)}
{relayerTimeoutString && (
<Typography variant="subtitle1">{relayerTimeoutString}</Typography>
)}
<div className={classes.spacer} />
<Typography variant="subtitle2" color="error">
WARNING: this is a Testnet release only