UI - added circle loader and swap progress bar
This commit is contained in:
parent
76f4c63198
commit
89c118a37f
|
@ -1 +1,3 @@
|
|||
.env
|
||||
build/
|
||||
node_modules/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Cross Chain Swap",
|
||||
"name": "NativeSwap",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -17,7 +17,6 @@ export default async function getIsTransferCompletedEvmWithRetry(
|
|||
provider,
|
||||
signedVAA
|
||||
);
|
||||
console.log("getIsTransferCompletedEth", result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue