diff --git a/react/src/swapper/swapper.ts b/react/src/swapper/swapper.ts index ae11198..394d17a 100644 --- a/react/src/swapper/swapper.ts +++ b/react/src/swapper/swapper.ts @@ -43,7 +43,7 @@ import { swapExactOutFromVaaNativeV3, swapExactOutFromVaaTokenV2, swapExactOutFromVaaTokenV3, -} from "util"; +} from "./util"; import { abi as SWAP_CONTRACT_V2_ABI } from "../abi/contracts/CrossChainSwapV2.json"; import { abi as SWAP_CONTRACT_V3_ABI } from "../abi/contracts/CrossChainSwapV3.json"; import { SWAP_CONTRACT_ADDRESS as CROSSCHAINSWAP_CONTRACT_ADDRESS_ETHEREUM } from "../addresses/goerli"; diff --git a/react/src/utils/consts.ts b/react/src/utils/consts.ts index 07c46a6..d93824f 100644 --- a/react/src/utils/consts.ts +++ b/react/src/utils/consts.ts @@ -7,30 +7,51 @@ import ethIcon from "../icons/eth.svg"; import polygonIcon from "../icons/polygon.svg"; export interface TokenInfo { - id: string; name: string; address: string; chainId: ChainId; logo: string; + isNative: boolean; } +export const MATIC_TOKEN_INFO: TokenInfo = { + name: "MATIC", + address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", // used to compute quote + chainId: CHAIN_ID_POLYGON, + logo: polygonIcon, + isNative: true, +}; + export const WMATIC_TOKEN_INFO: TokenInfo = { - id: "WMATIC", name: "WMATIC", address: "0x9c3c9283d3e44854697cd22d3faa240cfb032889", chainId: CHAIN_ID_POLYGON, logo: polygonIcon, + isNative: false, +}; + +export const ETH_TOKEN_INFO: TokenInfo = { + name: "ETH", + address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", // used to compute quote + chainId: CHAIN_ID_ETH, + logo: ethIcon, + isNative: true, }; export const WETH_TOKEN_INFO: TokenInfo = { - id: "WETH", name: "WETH", address: "0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6", chainId: CHAIN_ID_ETH, logo: ethIcon, + isNative: false, }; -export const TOKEN_INFOS = [WMATIC_TOKEN_INFO, WETH_TOKEN_INFO]; +export const TOKEN_INFOS = [ + MATIC_TOKEN_INFO, + WMATIC_TOKEN_INFO, + ETH_TOKEN_INFO, + WETH_TOKEN_INFO, +]; export const ETH_NETWORK_CHAIN_ID = 5; @@ -43,7 +64,7 @@ export const getEvmChainId = (chainId: ChainId) => ? POLYGON_NETWORK_CHAIN_ID : undefined; -export const RELAYER_FEE_UST = "0.0001"; +export const RELAYER_FEE_UST = "0.25"; export const WORMHOLE_RPC_HOSTS = [ "https://wormhole-v2-testnet-api.certus.one", diff --git a/react/src/utils/getIsTransferCompletedWithRetry.ts b/react/src/utils/getIsTransferCompletedWithRetry.ts new file mode 100644 index 0000000..39463a8 --- /dev/null +++ b/react/src/utils/getIsTransferCompletedWithRetry.ts @@ -0,0 +1,31 @@ +import { getIsTransferCompletedEth } from "@certusone/wormhole-sdk"; +import { ethers } from "ethers"; + +export default async function getIsTransferCompletedEvmWithRetry( + tokenBridgeAddress: string, + provider: ethers.providers.Provider, + signedVAA: Uint8Array, + retryTimeoutMs: number, + retryAttempts: number +) { + let result = false; + let attempts = 0; + while (attempts < retryAttempts) { + try { + result = await getIsTransferCompletedEth( + tokenBridgeAddress, + provider, + signedVAA + ); + console.log("getIsTransferCompletedEth", result); + } catch (e) { + console.error(e); + } + if (result) { + break; + } + await new Promise((resolve) => setTimeout(resolve, retryTimeoutMs)); + attempts++; + } + return result; +} diff --git a/react/src/views/Home.tsx b/react/src/views/Home.tsx index 532889c..3c76666 100644 --- a/react/src/views/Home.tsx +++ b/react/src/views/Home.tsx @@ -1,22 +1,26 @@ import { Container, + Link, makeStyles, Paper, TextField, Typography, } from "@material-ui/core"; -import { ChainId } from "@certusone/wormhole-sdk"; +import { ChainId, getSignedVAAWithRetry } from "@certusone/wormhole-sdk"; import { useCallback, useEffect, useState } from "react"; import ButtonWithLoader from "../components/ButtonWithLoader"; import EthereumSignerKey from "../components/EthereumSignerKey"; import TokenSelect from "../components/TokenSelect"; import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { + ETH_TOKEN_INFO, getEvmChainId, + MATIC_TOKEN_INFO, RELAYER_FEE_UST, TOKEN_INFOS, WETH_TOKEN_INFO, WMATIC_TOKEN_INFO, + WORMHOLE_RPC_HOSTS, } from "../utils/consts"; import { COLORS } from "../muiTheme"; import Wormhole from "../icons/wormhole-network.svg"; @@ -28,6 +32,7 @@ import { useSnackbar } from "notistack"; import { Alert } from "@material-ui/lab"; import parseError from "../utils/parseError"; import Settings from "../components/Settings"; +import getIsTransferCompletedEvmWithRetry from "../utils/getIsTransferCompletedWithRetry"; const useStyles = makeStyles((theme) => ({ bg: { @@ -160,7 +165,8 @@ export default function Home() { const executor = new UniswapToUniswapExecutor(); await executor.initialize( sourceTokenInfo.address, - targetTokenInfo.address + targetTokenInfo.address, + sourceTokenInfo.isNative ); await executor.computeAndVerifySrcPoolAddress().catch((e) => { throw new Error("failed to verify source pool address"); @@ -225,12 +231,19 @@ export default function Home() { }, []); const handleSourceChange = useCallback((event) => { + // NOTE: only native-to-native or wrapped-to-wrapped swaps are currently supported if (event.target.value === WMATIC_TOKEN_INFO.name) { setSourceTokenInfo(WMATIC_TOKEN_INFO); setTargetTokenInfo(WETH_TOKEN_INFO); - } else { + } else if (event.target.value === WETH_TOKEN_INFO.name) { setSourceTokenInfo(WETH_TOKEN_INFO); setTargetTokenInfo(WMATIC_TOKEN_INFO); + } else if (event.target.value === ETH_TOKEN_INFO.name) { + setSourceTokenInfo(ETH_TOKEN_INFO); + setTargetTokenInfo(MATIC_TOKEN_INFO); + } else { + setSourceTokenInfo(MATIC_TOKEN_INFO); + setTargetTokenInfo(ETH_TOKEN_INFO); } setAmountIn("0.0"); setAmountOut("0.0"); @@ -242,13 +255,53 @@ export default function Home() { setIsSwapping(true); await switchProviderNetwork(provider, sourceTokenInfo.chainId); const sourceReceipt = await executor.approveAndSwap(signer); - console.info(`src transaction: ${sourceReceipt.transactionHash}`); - await switchProviderNetwork(provider, targetTokenInfo.chainId); - const targetReceipt = await executor.fetchVaaAndSwap(signer); - console.info(`dst transaction: ${targetReceipt.transactionHash}`); + console.info( + "firstSwapTransactionHash:", + sourceReceipt.transactionHash + ); + + // Wait for the guardian network to reach consensus and emit the signedVAA enqueueSnackbar(null, { - content: Success!, + content: Fetching VAA, }); + const { vaaBytes } = await getSignedVAAWithRetry( + WORMHOLE_RPC_HOSTS, + executor.srcExecutionParams.wormhole.chainId, + executor.vaaSearchParams.emitterAddress, + executor.vaaSearchParams.sequence + ); + // Check if the signedVAA has redeemed by the relayer + enqueueSnackbar(null, { + content: ( + + Fetched the Signed VAA, waiting for relayer to redeem it + + ), + }); + const isCompleted = await getIsTransferCompletedEvmWithRetry( + executor.dstExecutionParams.wormhole.tokenBridgeAddress, + executor.quoter.dstProvider, //provider, + vaaBytes, + // retry for two minutes + 3000, + 40 + ); + if (isCompleted) { + enqueueSnackbar(null, { + content: Swap completed, + }); + } else { + // If the relayer hasn't redeemed the signedVAA, then manually redeem it ourselves + await switchProviderNetwork(provider, targetTokenInfo.chainId); + const targetReceipt = await executor.fetchVaaAndSwap(signer); + enqueueSnackbar(null, { + content: Swap completed, + }); + console.info( + "secondSwapTransactionHash:", + targetReceipt.transactionHash + ); + } } catch (e: any) { console.error(e); enqueueSnackbar(null, { @@ -329,6 +382,12 @@ export default function Home() { {"powered by wormhole"} Wormhole +
+ + Goerli faucet + +
+ Mumbai faucet
);