diff --git a/contracts/.gitignore b/contracts/.gitignore
index 4c49bd7..79b531a 100644
--- a/contracts/.gitignore
+++ b/contracts/.gitignore
@@ -1 +1,3 @@
.env
+build/
+node_modules/
diff --git a/react/package.json b/react/package.json
index 8ec29e4..6e094f4 100644
--- a/react/package.json
+++ b/react/package.json
@@ -1,5 +1,5 @@
{
- "name": "Cross Chain Swap",
+ "name": "NativeSwap",
"version": "0.1.0",
"private": true,
"dependencies": {
diff --git a/react/public/index.html b/react/public/index.html
index 149fdf7..a3f3ac9 100644
--- a/react/public/index.html
+++ b/react/public/index.html
@@ -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`.
-->
-
Cross Chain Swap
+ NativeSwap
diff --git a/react/src/components/CircleLoader.tsx b/react/src/components/CircleLoader.tsx
new file mode 100644
index 0000000..9d3a179
--- /dev/null
+++ b/react/src/components/CircleLoader.tsx
@@ -0,0 +1,11 @@
+import "../css/CircleLoader.css";
+
+export default function CircleLoader() {
+ return (
+
+ );
+}
diff --git a/react/src/components/SwapProgress.tsx b/react/src/components/SwapProgress.tsx
new file mode 100644
index 0000000..8087fab
--- /dev/null
+++ b/react/src/components/SwapProgress.tsx
@@ -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 (
+
+
+
+ {message}
+
+
+ );
+}
diff --git a/react/src/css/CircleLoader.css b/react/src/css/CircleLoader.css
new file mode 100644
index 0000000..eb11e05
--- /dev/null
+++ b/react/src/css/CircleLoader.css
@@ -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;
+}
diff --git a/react/src/utils/getIsTransferCompletedWithRetry.ts b/react/src/utils/getIsTransferCompletedWithRetry.ts
index 39463a8..cae60f1 100644
--- a/react/src/utils/getIsTransferCompletedWithRetry.ts
+++ b/react/src/utils/getIsTransferCompletedWithRetry.ts
@@ -17,7 +17,6 @@ export default async function getIsTransferCompletedEvmWithRetry(
provider,
signedVAA
);
- console.log("getIsTransferCompletedEth", result);
} catch (e) {
console.error(e);
}
diff --git a/react/src/views/Home.tsx b/react/src/views/Home.tsx
index cac2ca8..f56f646 100644
--- a/react/src/views/Home.tsx
+++ b/react/src/views/Home.tsx
@@ -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(
@@ -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: Fetching VAA,
- });
const { vaaBytes } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
executor.srcExecutionParams.wormhole.chainId,
executor.vaaSearchParams.emitterAddress,
executor.vaaSearchParams.sequence
);
- enqueueSnackbar(null, {
- content: (
-
- Fetched the Signed VAA, waiting for relayer to redeem it
-
- ),
- });
+ 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: Swap completed,
- });
- } 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: Swap completed,
- });
console.info(
"secondSwapTransactionHash:",
targetReceipt.transactionHash
);
}
+ setIsSecondSwapComplete(true);
} catch (e: any) {
+ reset();
console.error(e);
enqueueSnackbar(null, {
content: {parseError(e)},
});
}
- 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() {
- Cross Chain Swap Demo
+ Wormhole NativeSwap Demo
-
-
- Send
-
+
+
+
+ Send
+
+
+ {}}
+ disabled={true}
+ >
+ Receive (estimated)
+
+ {`Slippage tolerance: ${slippage}%`}
+ {!isSwapping && }
+
+ Swap
+
+
+
+
+
+
+
+ {`Your ${sourceTokenInfo.name} is being swapped to ${targetTokenInfo.name}`}
+
+
+
+
+
+
+ Swap completed!
+ reset()}>
+ Swap more tokens!
+
+
+
- {}}
- disabled={true}
- >
- Receive (estimated)
-
- {`Slippage tolerance: ${slippage}%`}
- {!isSwapping && }
-
- Swap
-
+ {hasQuote && (
+
+ {`${amountIn} ${sourceTokenInfo.name} `}
+
+ {` ${amountInUST} UST `}
+ {` ${amountOut} ${targetTokenInfo.name}`}
+
+ )}
+ {isFirstSwapComplete &&
+ !isSecondSwapComplete &&
+ !relayerTimeoutString && (
+
+ )}
+ {relayerTimeoutString && (
+ {relayerTimeoutString}
+ )}
WARNING: this is a Testnet release only