From bd8b1085c4ea65ccae53d7d3286106c523b989d5 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Fri, 1 Apr 2022 20:29:52 +1300 Subject: [PATCH 1/9] Add `Link` component --- components/Link.tsx | 38 ++++++++++++++++++++++++++++++++++++++ types/index.d.ts | 2 ++ 2 files changed, 40 insertions(+) create mode 100644 components/Link.tsx diff --git a/components/Link.tsx b/components/Link.tsx new file mode 100644 index 00000000..5281d7eb --- /dev/null +++ b/components/Link.tsx @@ -0,0 +1,38 @@ +import { IntrinsicElements } from "@/types"; +import NextLink, { LinkProps as NextLinkProps } from "next/link"; +import { FC } from "react"; + +/* eslint-disable react/jsx-no-target-blank */ +const Link: FC = ({ + className, + href, + children, + ...props +}) => { + if (!href) href = "#"; + const internal = /^\/(?!\/)/.test(href) || (href && href.indexOf("#") === 0); + + if (!internal) { + const nofollow = /lithoverse\.xyz/.test(href) + ? null + : { rel: "nofollow noreferrer" }; + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; + +export default Link; diff --git a/types/index.d.ts b/types/index.d.ts index cb08fa40..b9c63c66 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,7 @@ import { ethers } from "ethers"; import { Balance } from "@/utils"; import { + AnchorHTMLAttributes, ButtonHTMLAttributes, FormHTMLAttributes, HTMLAttributes, @@ -50,6 +51,7 @@ export interface IntrinsicElements { form: FormHTMLAttributes; button: ButtonHTMLAttributes; input: InputHTMLAttributes; + a: AnchorHTMLAttributes; } export type PoolAction = "Add" | "Remove"; From 3f9bdfc59ea3af70a9375cbea714434135c72a00 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Sat, 2 Apr 2022 22:58:48 +1300 Subject: [PATCH 2/9] Add `CENNZTransaction` class and utilise it in `signAndSendTx2` --- package.json | 1 + utils/CENNZTransaction.ts | 60 +++++++++++++++++++++++++++++++++++++++ utils/index.ts | 6 +++- utils/signAndSendTx.ts | 25 +++++++++++++++- yarn.lock | 8 ++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 utils/CENNZTransaction.ts diff --git a/package.json b/package.json index 0223a39c..c52b220d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "axios": "^0.26.1", "big.js": "^6.1.1", "bignumber.js": "^9.0.2", + "emittery": "^0.10.1", "ethers": "^5.6.0", "lodash": "^4.17.21", "next": "^12.1.0", diff --git a/utils/CENNZTransaction.ts b/utils/CENNZTransaction.ts new file mode 100644 index 00000000..6167c1d8 --- /dev/null +++ b/utils/CENNZTransaction.ts @@ -0,0 +1,60 @@ +import { Api, SubmittableResult } from "@cennznet/api"; +import Emittery from "emittery"; +import { CENNZ_EXPLORER_URL } from "@/constants"; + +interface EmitEvents { + txCreated: undefined; + txHashed: string; + txPending: SubmittableResult; + txSucceeded: SubmittableResult; + txFailed: SubmittableResult; + txCancelled: undefined; +} + +export default class CENNZTransaction extends Emittery { + public hash: string; + + constructor() { + super(); + this.emit("txCreated"); + } + + setHash(hash: string) { + const shouldEmit = this.hash !== hash; + this.hash = hash; + if (shouldEmit) this.emit("txHashed", hash); + } + + setResult(result: SubmittableResult) { + const { status, dispatchError } = result; + + if (status.isInBlock) return this.emit("txPending", result); + + if (status.isFinalized && !dispatchError) + return this.emit("txSucceeded", result); + + if (status.isFinalized && dispatchError) + return this.emit("txFailed", result); + } + + setCancel() { + this.emit("txCancelled"); + } + + decodeError(api: Api, result: SubmittableResult): string { + const { dispatchError } = result; + console.log(dispatchError); + if (!dispatchError?.isModule) return null; + const { index, error } = dispatchError.asModule.toJSON(); + const errorMeta = api.registry.findMetaError( + new Uint8Array([index as number, error as number]) + ); + return errorMeta?.section && errorMeta?.name + ? `${errorMeta.section}.${errorMeta.name}` + : `I${index}E${error}`; + } + + getExplorerLink(hash: string): string { + return `${CENNZ_EXPLORER_URL}/extrinsic/${hash}`; + } +} diff --git a/utils/index.ts b/utils/index.ts index 8a5bec12..09ac200d 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -9,7 +9,10 @@ export { default as getTokenLogo } from "@/utils/getTokenLogo"; export { default as fetchSellPrice } from "@/utils/fetchSellPrice"; export { default as fetchGasFee } from "@/utils/fetchGasFee"; export { default as getBuyAssetExtrinsic } from "@/utils/getBuyAssetExtrinsic"; -export { default as signAndSendTx } from "@/utils/signAndSendTx"; +export { + default as signAndSendTx, + signAndSendTx2, +} from "@/utils/signAndSendTx"; export { default as fetchPoolExchangeInfo } from "@/utils/fetchPoolExchangeInfo"; export { default as fetchPoolUserInfo } from "@/utils/fetchPoolUserInfo"; export { default as getAddLiquidityExtrinsic } from "@/utils/getAddLiquidityExtrinsic"; @@ -38,3 +41,4 @@ export { default as sendWithdrawEthereumRequest } from "@/utils/sendWithdrawEthe export { default as trackPageView } from "@/utils/trackPageView"; export { default as getSellAssetExtrinsic } from "@/utils/getSellAssetExtrinsic"; export { default as selectMap } from "@/utils/selectMap"; +export { default as CENNZTransaction } from "@/utils/CENNZTransaction"; diff --git a/utils/signAndSendTx.ts b/utils/signAndSendTx.ts index 6d7f7d34..6e48f976 100644 --- a/utils/signAndSendTx.ts +++ b/utils/signAndSendTx.ts @@ -1,4 +1,5 @@ -import { Api } from "@cennznet/api"; +import { CENNZTransaction } from "@/utils"; +import { Api, SubmittableResult } from "@cennznet/api"; import { Signer, SubmittableExtrinsic } from "@cennznet/api/types"; interface TxReceipt { @@ -57,3 +58,25 @@ export default async function signAndSendTx( throw err; } } + +export async function signAndSendTx2( + extrinsic: SubmittableExtrinsic<"promise", any>, + address: string, + signer: Signer +): Promise { + const tx = new CENNZTransaction(); + + extrinsic + .signAndSend(address, { signer }, (result: SubmittableResult) => { + const { txHash } = result; + console.info("Transaction", txHash.toString()); + tx.setHash(txHash.toString()); + tx.setResult(result); + }) + .catch((error) => { + if (error?.message !== "Cancelled") throw error; + tx.setCancel(); + }); + + return tx; +} diff --git a/yarn.lock b/yarn.lock index c1c4ad4f..6f058402 100644 --- a/yarn.lock +++ b/yarn.lock @@ -553,6 +553,7 @@ __metadata: babel-jest: ^27.5.1 big.js: ^6.1.1 bignumber.js: ^9.0.2 + emittery: ^0.10.1 eslint: 8.11.0 eslint-config-next: 12.1.0 eslint-config-prettier: ^8.5.0 @@ -4007,6 +4008,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^0.10.1": + version: 0.10.1 + resolution: "emittery@npm:0.10.1" + checksum: 75b27db7696aec22fb9b6a92cebd1ba407e13f5dedb512f0f40b896090fe6d4a949329b198d3f5a570abeb45d48651714b9c2f16e910cfacd3a043e419ffa8b9 + languageName: node + linkType: hard + "emittery@npm:^0.8.1": version: 0.8.1 resolution: "emittery@npm:0.8.1" From 11c722719d8262d89a39e7fd060164df014d4668 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Sun, 3 Apr 2022 21:31:23 +1200 Subject: [PATCH 3/9] Update SwapProgress with improved layout, transaction link --- .env.development | 1 + .env.production | 1 + components/Link.tsx | 5 +- components/SwapForm.tsx | 81 ++++++----- components/SwapProgress.tsx | 186 +++++++++++++++----------- components/SwapStats.tsx | 2 +- components/shared/ProgressOverlay.tsx | 111 +++++++++++++++ constants.ts | 3 + hooks/index.ts | 2 + hooks/useTxStatus.ts | 39 ++++++ providers/SwapProvider.tsx | 99 ++------------ utils/CENNZTransaction.ts | 28 +++- utils/selectMap.ts | 11 +- 13 files changed, 357 insertions(+), 212 deletions(-) create mode 100644 components/shared/ProgressOverlay.tsx create mode 100644 hooks/useTxStatus.ts diff --git a/.env.development b/.env.development index 8684996e..d6dff5c9 100644 --- a/.env.development +++ b/.env.development @@ -5,3 +5,4 @@ NEXT_PUBLIC_API_URL="wss://nikau.centrality.me/public/ws" NEXT_PUBLIC_ETH_CHAIN_ID=42 NEXT_PUBLIC_BRIDGE_RELAYER_URL="https://bridge-contracts.nikau.centrality.me" NEXT_PUBLIC_GA_ID="G-8YJT9T1YTT" +NEXT_PUBLIC_CENNZ_EXPLORER_URL="https://uncoverexplorer.com" diff --git a/.env.production b/.env.production index 71dc19e2..b6fb50ca 100644 --- a/.env.production +++ b/.env.production @@ -5,3 +5,4 @@ NEXT_PUBLIC_API_URL="wss://cennznet.unfrastructure.io/public/ws" NEXT_PUBLIC_ETH_CHAIN_ID=1 NEXT_PUBLIC_BRIDGE_RELAYER_URL="https://bridge-contracts.centralityapp.com" NEXT_PUBLIC_GA_ID="G-8YJT9T1YTT" +NEXT_PUBLIC_CENNZ_EXPLORER_URL="https://uncoverexplorer.com" diff --git a/components/Link.tsx b/components/Link.tsx index 5281d7eb..1b078f8a 100644 --- a/components/Link.tsx +++ b/components/Link.tsx @@ -4,7 +4,6 @@ import { FC } from "react"; /* eslint-disable react/jsx-no-target-blank */ const Link: FC = ({ - className, href, children, ...props @@ -13,14 +12,14 @@ const Link: FC = ({ const internal = /^\/(?!\/)/.test(href) || (href && href.indexOf("#") === 0); if (!internal) { - const nofollow = /lithoverse\.xyz/.test(href) + const nofollow = /app\.cennz\.net/.test(href) ? null : { rel: "nofollow noreferrer" }; return ( {children} diff --git a/components/SwapForm.tsx b/components/SwapForm.tsx index 2db14e01..62608d0f 100644 --- a/components/SwapForm.tsx +++ b/components/SwapForm.tsx @@ -1,12 +1,12 @@ import { IntrinsicElements } from "@/types"; -import { FC, useCallback } from "react"; +import { FC, useCallback, useEffect } from "react"; import { css } from "@emotion/react"; import { Theme } from "@mui/material"; import SubmitButton from "@/components/shared/SubmitButton"; import { useSwap } from "@/providers/SwapProvider"; import { useCENNZApi } from "@/providers/CENNZApiProvider"; import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; -import { Balance, getSellAssetExtrinsic, signAndSendTx } from "@/utils"; +import { Balance, getSellAssetExtrinsic, signAndSendTx2 } from "@/utils"; interface SwapFormProps {} @@ -22,10 +22,10 @@ const SwapForm: FC = ({ exchangeInput: { value: exValue, setValue: setExValue }, receiveInput: { value: reValue }, slippage, - setTxStatus, - setSuccessStatus, - setProgressStatus, - setFailStatus, + setTxIdle, + setTxPending, + setTxSuccess, + setTxFailure, } = useSwap(); const { selectedAccount, wallet, updateBalances } = useCENNZWallet(); @@ -34,35 +34,56 @@ const SwapForm: FC = ({ event.preventDefault(); if (!api) return; - setProgressStatus(); - - const extrinsic = getSellAssetExtrinsic( - api, - exchangeAsset.assetId, - Balance.fromInput(exValue, exchangeAsset), - receiveAsset.assetId, - Balance.fromInput(reValue, receiveAsset), - Number(slippage) - ); - - let status: Awaited>; + try { - status = await signAndSendTx( + setTxPending(); + const extrinsic = getSellAssetExtrinsic( api, + exchangeAsset.assetId, + Balance.fromInput(exValue, exchangeAsset), + receiveAsset.assetId, + Balance.fromInput(reValue, receiveAsset), + Number(slippage) + ); + + const tx = await signAndSendTx2( extrinsic, selectedAccount.address, wallet.signer ); + + tx.on("txCancelled", () => setTxIdle()); + + tx.on("txHashed", () => { + setTxPending({ + txHashLink: tx.getHashLink(), + }); + }); + + tx.on("txFailed", (result) => + setTxFailure({ + errorCode: tx.decodeError(result), + txHashLink: tx.getHashLink(), + }) + ); + + tx.on("txSucceeded", (result) => { + const event = tx.findEvent(result, "cennzx", "AssetSold"); + const exchangeValue = Balance.fromCodec(event.data[3], exchangeAsset); + const receiveValue = Balance.fromCodec(event.data[4], receiveAsset); + + updateBalances(); + setExValue(""); + setTxSuccess({ + exchangeValue, + receiveValue, + txHashLink: tx.getHashLink(), + }); + }); } catch (error) { console.info(error); - return setFailStatus(error?.code); + return setTxFailure({ errorCode: error?.code as string }); } - - if (status === "cancelled") return setTxStatus(null); - - setSuccessStatus(); - setExValue(""); - updateBalances(); }, [ api, @@ -73,12 +94,12 @@ const SwapForm: FC = ({ slippage, selectedAccount?.address, wallet?.signer, - setTxStatus, updateBalances, setExValue, - setSuccessStatus, - setFailStatus, - setProgressStatus, + setTxFailure, + setTxPending, + setTxSuccess, + setTxIdle, ] ); diff --git a/components/SwapProgress.tsx b/components/SwapProgress.tsx index 0c5ebad2..1814b295 100644 --- a/components/SwapProgress.tsx +++ b/components/SwapProgress.tsx @@ -1,4 +1,4 @@ -import { VFC } from "react"; +import { VFC, ReactNode } from "react"; import { IntrinsicElements } from "@/types"; import { css } from "@emotion/react"; import { useSwap } from "@/providers/SwapProvider"; @@ -6,76 +6,123 @@ import { Theme, CircularProgress } from "@mui/material"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined"; import StandardButton from "@/components/shared/StandardButton"; +import { Balance, selectMap } from "@/utils"; +import Link from "@/components/Link"; +import ProgressOverlay from "@/components/shared/ProgressOverlay"; interface SwapProgressProps {} const SwapProgress: VFC = ( props ) => { - const { txStatus, setTxStatus } = useSwap(); + const { txStatus, setTxIdle } = useSwap(); + const { txHashLink, ...txProps } = txStatus?.props ?? {}; return ( -
- {!!txStatus && ( -
- {txStatus.status === "in-progress" && ( - - )} - {txStatus.status === "success" && ( - - )} - {txStatus.status === "fail" && ( - - )} - -
{txStatus.title}
-
{txStatus.message}
+ + {selectMap( + txStatus?.status, + { + Pending: , + Success: , + Failure: , + }, + null + )} - {txStatus.status !== "in-progress" && ( - setTxStatus(null)} - > - Dismiss - - )} -
+ {!!txHashLink && ( + + View Transaction + )} -
+ ); }; export default SwapProgress; -const styles = { - root: - (show: boolean) => - ({ transitions }: Theme) => - css` - position: absolute; - inset: 0; - background-color: rgba(255, 255, 255, 0.9); - z-index: 100; - opacity: ${show ? 1 : 0}; - pointer-events: ${show ? "all" : "none"}; - transition: opacity ${transitions.duration.short}ms; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(2px); - padding: 5em; - text-align: center; - font-size: 14px; - `, +interface TxPendingProps {} - status: css` - margin-bottom: 1.5em; - `, +const TxPending: VFC = (props) => { + return ( +
+ +

Transaction In Progress

+

+ Please sign the transaction when prompted and wait until it's + completed +

+
+ ); +}; + +interface TxSuccessProps { + txHash: string; + exchangeValue: Balance; + receiveValue: Balance; +} + +const TxSuccess: VFC = ({ + txHash, + exchangeValue, + receiveValue, + ...props +}) => { + return ( +
+ +

Transaction Completed

+

+ You successfully swapped{" "} + + {exchangeValue.toBalance()}{" "} + {exchangeValue.getSymbol()} + {" "} + for{" "} + + {receiveValue.toBalance()}{" "} + {receiveValue.getSymbol()} + + . +

+
+ ); +}; +interface TxFailureProps { + errorCode?: string; +} + +const TxFailure: VFC = ({ + errorCode, + ...props +}) => { + return ( +
+ +

Transaction Failed

+

+ An error occurred while processing your transaction. It might have gone + through, check your balances before trying again. +

+ {!!errorCode && ( +
+
+						#{errorCode}
+					
+
+ )} +
+ ); +}; + +const styles = { statusSuccess: ({ palette }: Theme) => css` width: 4em; height: 4em; @@ -83,41 +130,18 @@ const styles = { color: ${palette.success.main}; `, - statusFail: ({ palette }: Theme) => css` + statusFailure: ({ palette }: Theme) => css` width: 4em; height: 4em; font-size: 14px; color: ${palette.warning.main}; `, - title: ({ palette }: Theme) => css` - font-weight: bold; - font-size: 20px; - line-height: 1; - text-align: center; - text-transform: uppercase; - color: ${palette.primary.main}; - `, - - message: ({ palette }: Theme) => css` + button: css` margin-top: 1em; - line-height: 1.5; - - small { - font-size: 0.85em; - display: inline-block; - padding: 0.25em 0.5em; - margin-top: 0.5em; - } - - em { - font-weight: bold; - font-style: normal; - color: ${palette.primary.main}; - } `, - button: css` - margin-top: 2em; + errorCode: css` + margin-top: 0.5em; `, }; diff --git a/components/SwapStats.tsx b/components/SwapStats.tsx index edb10aa8..1f882bd4 100644 --- a/components/SwapStats.tsx +++ b/components/SwapStats.tsx @@ -24,7 +24,7 @@ const SwapStats: VFC = (props) => { const { gasFee, updatingGasFee, updateGasFee } = useSwapGasFee(); useEffect(() => { - if (txStatus?.status !== "success") return; + if (txStatus?.status !== "Success") return; updateExchangeRate(); updateGasFee(); }, [txStatus?.status, updateExchangeRate, updateGasFee]); diff --git a/components/shared/ProgressOverlay.tsx b/components/shared/ProgressOverlay.tsx new file mode 100644 index 00000000..8f9c2b7c --- /dev/null +++ b/components/shared/ProgressOverlay.tsx @@ -0,0 +1,111 @@ +import { IntrinsicElements } from "@/types"; +import { css } from "@emotion/react"; +import { Theme } from "@mui/material"; +import { FC, useEffect } from "react"; +import CloseIcon from "@mui/icons-material/Close"; + +interface ProgressOverlayProps { + show: boolean; + dismissible: boolean; + onRequestClose?: () => void; +} + +const ProgressOverlay: FC = ({ + show, + dismissible, + onRequestClose, + children, + ...props +}) => { + useEffect(() => { + if (!dismissible) return; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + onRequestClose?.(); + }; + + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [onRequestClose, dismissible]); + + return ( +
+ {dismissible && ( + + )} + {children} +
+ ); +}; + +export default ProgressOverlay; + +const styles = { + root: + (show: boolean) => + ({ transitions, palette }: Theme) => + css` + position: absolute; + inset: 0; + background-color: rgba(255, 255, 255, 0.9); + z-index: 100; + opacity: ${show ? 1 : 0}; + pointer-events: ${show ? "all" : "none"}; + transition: opacity ${transitions.duration.short}ms; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + backdrop-filter: blur(2px); + padding: 5em; + text-align: center; + font-size: 14px; + + h1 { + font-weight: bold; + font-size: 20px; + line-height: 1; + text-align: center; + text-transform: uppercase; + color: ${palette.primary.main}; + } + + p { + margin-top: 1em; + line-height: 1.5; + + small { + font-size: 0.85em; + display: inline-block; + padding: 0.25em 0.5em; + margin-top: 0.5em; + } + + em { + font-family: "Roboto Mono", monospace; + display: inline; + letter-spacing: -0.025em; + font-weight: bold; + font-style: normal; + color: ${palette.primary.main}; + font-size: 0.5em; + span { + font-size: 2em; + letter-spacing: -0.025em; + } + } + } + `, + closeIcon: ({ palette }: Theme) => css` + position: absolute; + top: 1em; + right: 1em; + cursor: pointer; + transition: color 0.2s; + color: ${palette.grey["600"]}; + + &:hover { + color: ${palette.primary.main}; + } + `, +}; diff --git a/constants.ts b/constants.ts index 439da2ba..a074176a 100644 --- a/constants.ts +++ b/constants.ts @@ -33,3 +33,6 @@ export const MAINNET_PEG_CONTRACT: string = export const KOVAN_PEG_CONTRACT: string = "0xa39E871e6e24f2d1Dd6AdA830538aBBE7b30F78F"; + +export const CENNZ_EXPLORER_URL: string = + process.env.NEXT_PUBLIC_CENNZ_EXPLORER_URL; diff --git a/hooks/index.ts b/hooks/index.ts index 377f42de..709e5e0a 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -12,7 +12,9 @@ export { default as useBalanceValidation } from "@/hooks/useBalanceValidation"; export { default as useBridgeGasFee } from "@/hooks/useBridgeGasFee"; export { default as useBridgeVerificationFee } from "@/hooks/useBridgeVerificationFee"; export { default as useBlockHashValidation } from "@/hooks/useBlockHashValidation"; +export { default as useTxStatus } from "@/hooks/useTxStatus"; export type { TokenInputHook } from "@/hooks/useTokenInput"; export type { PoolExchangeInfoHook } from "@/hooks/usePoolExchangeInfo"; export type { PoolUserInfoHook } from "@/hooks/usePoolUserInfo"; +export type { TxStatusHook } from "@/hooks/useTxStatus"; diff --git a/hooks/useTxStatus.ts b/hooks/useTxStatus.ts new file mode 100644 index 00000000..b7cf6f33 --- /dev/null +++ b/hooks/useTxStatus.ts @@ -0,0 +1,39 @@ +import { useCallback, useState } from "react"; + +type TxType = "Idle" | "Pending" | "Success" | "Failure"; + +interface TxStatus { + status: TxType; + props?: any; +} + +export interface TxStatusHook { + txStatus: TxStatus; + setTxIdle: (props?: any) => void; + setTxPending: (props?: any) => void; + setTxFailure: (props?: any) => void; + setTxSuccess: (props?: any) => void; +} + +export default function useTxStatus(defaultValue: TxStatus = null) { + const [txStatus, setTxStatus] = useState(defaultValue); + + const createTxStatusTrigger = useCallback((status: TxType) => { + return (props?: any) => { + if (status === "Idle") return setTxStatus(null); + + setTxStatus({ + status, + props, + }); + }; + }, []); + + return { + txStatus, + setTxIdle: createTxStatusTrigger("Idle"), + setTxPending: createTxStatusTrigger("Pending"), + setTxSuccess: createTxStatusTrigger("Success"), + setTxFailure: createTxStatusTrigger("Failure"), + }; +} diff --git a/providers/SwapProvider.tsx b/providers/SwapProvider.tsx index 1b9ff0cc..274d577a 100644 --- a/providers/SwapProvider.tsx +++ b/providers/SwapProvider.tsx @@ -5,17 +5,21 @@ import { SetStateAction, Dispatch, FC, - useCallback, } from "react"; -import { CENNZAsset, TxStatus } from "@/types"; -import { Balance, fetchSwapAssets } from "@/utils"; -import { useTokensFetcher } from "@/hooks"; +import { CENNZAsset } from "@/types"; +import { fetchSwapAssets } from "@/utils"; import { CENNZ_ASSET_ID, CPAY_ASSET_ID } from "@/constants"; -import { useTokenInput, TokenInputHook } from "@/hooks"; +import { + useTokenInput, + TokenInputHook, + TxStatusHook, + useTokensFetcher, + useTxStatus, +} from "@/hooks"; type CENNZAssetId = CENNZAsset["assetId"]; -interface SwapContextType { +interface SwapContextType extends TxStatusHook { exchangeAssets: CENNZAsset[]; receiveAssets: CENNZAsset[]; cpayAsset: CENNZAsset; @@ -28,13 +32,6 @@ interface SwapContextType { receiveAsset: CENNZAsset; slippage: string; setSlippage: Dispatch>; - - txStatus: TxStatus; - setTxStatus: Dispatch>; - - setProgressStatus: () => void; - setSuccessStatus: () => void; - setFailStatus: (errorCode?: string) => void; } const SwapContext = createContext({} as SwapContextType); @@ -69,75 +66,6 @@ const SwapProvider: FC = ({ supportedAssets, children }) => { ); const [slippage, setSlippage] = useState("5"); - const [txStatus, setTxStatus] = useState(null); - - const setProgressStatus = useCallback(() => { - setTxStatus({ - status: "in-progress", - title: "Transaction In Progress", - message: ( -
- Please sign the transaction when prompted and wait until it's - completed -
- ), - }); - }, []); - - const setFailStatus = useCallback((errorCode?: string) => { - setTxStatus({ - status: "fail", - title: "Transaction Failed", - message: ( -
- An error occurred while processing your transaction - {!!errorCode && ( - <> -
-
-								#{errorCode}
-							
- - )} -
- ), - }); - }, []); - - const setSuccessStatus = useCallback(() => { - const exValue = Balance.format(exchangeInput.value); - const exSymbol = exchangeAsset.symbol; - - const reValue = Balance.format(receiveInput.value); - const reSymbol = receiveAsset.symbol; - - setTxStatus({ - status: "success", - title: "Transaction Completed", - message: ( -
- You successfully swapped{" "} -
-						
-							{exValue} {exSymbol}
-						
-					
{" "} - for{" "} -
-						
-							{reValue} {reSymbol}
-						
-					
- . -
- ), - }); - }, [ - exchangeInput.value, - exchangeAsset.symbol, - receiveInput.value, - receiveAsset.symbol, - ]); return ( = ({ supportedAssets, children }) => { cpayAsset, slippage, setSlippage, - txStatus, - setTxStatus, - setProgressStatus, - setSuccessStatus, - setFailStatus, + + ...useTxStatus(), }} > {children} diff --git a/utils/CENNZTransaction.ts b/utils/CENNZTransaction.ts index 6167c1d8..d3da8483 100644 --- a/utils/CENNZTransaction.ts +++ b/utils/CENNZTransaction.ts @@ -1,6 +1,7 @@ import { Api, SubmittableResult } from "@cennznet/api"; import Emittery from "emittery"; import { CENNZ_EXPLORER_URL } from "@/constants"; +import { Event } from "@polkadot/types/interfaces"; interface EmitEvents { txCreated: undefined; @@ -41,20 +42,33 @@ export default class CENNZTransaction extends Emittery { this.emit("txCancelled"); } - decodeError(api: Api, result: SubmittableResult): string { + decodeError(result: SubmittableResult): string { const { dispatchError } = result; - console.log(dispatchError); if (!dispatchError?.isModule) return null; const { index, error } = dispatchError.asModule.toJSON(); - const errorMeta = api.registry.findMetaError( + const errorMeta = dispatchError.registry.findMetaError( new Uint8Array([index as number, error as number]) ); - return errorMeta?.section && errorMeta?.name - ? `${errorMeta.section}.${errorMeta.name}` + return errorMeta?.section && errorMeta?.method + ? `${errorMeta.section}.${errorMeta.method}` : `I${index}E${error}`; } - getExplorerLink(hash: string): string { - return `${CENNZ_EXPLORER_URL}/extrinsic/${hash}`; + findEvent( + result: SubmittableResult, + eventSection: string, + eventMethod: string + ): Event { + const { events: records } = result; + const record = records.find((record) => { + const { event } = record; + return event?.section === eventSection && event?.method === eventMethod; + }); + + return record?.event; + } + + getHashLink(): string { + return this.hash ? `${CENNZ_EXPLORER_URL}/extrinsic/${this.hash}` : null; } } diff --git a/utils/selectMap.ts b/utils/selectMap.ts index 756161d4..741b7a0b 100644 --- a/utils/selectMap.ts +++ b/utils/selectMap.ts @@ -2,15 +2,20 @@ * A utility that return value by key from a map * * @param {K} key - * @param {Map} map + * @param {Map | Record} object * @param {V} defaultValue * @return {V} */ -export default function selectMap( +export default function selectMap( key: K, - map: Map, + object: Map | Partial>, defaultValue?: V ): V { + const map: Map = + object instanceof Map + ? object + : new Map(Object.entries(object) as [K, V][]); + if (!map.has(key)) return defaultValue; return map.get(key); } From 51dc6483ff7cae97c1939c3c1007c56ad3ea96da Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Mon, 4 Apr 2022 16:23:55 +1200 Subject: [PATCH 4/9] Update Pool section to use new ProgressOverlay system --- components/PoolForm.tsx | 73 +++++++++----- components/PoolProgress.tsx | 192 ++++++++++++++++++++---------------- providers/PoolProvider.tsx | 97 ++---------------- providers/SwapProvider.tsx | 4 +- 4 files changed, 171 insertions(+), 195 deletions(-) diff --git a/components/PoolForm.tsx b/components/PoolForm.tsx index f49e1d95..4bec8935 100644 --- a/components/PoolForm.tsx +++ b/components/PoolForm.tsx @@ -9,7 +9,7 @@ import { Balance, getAddLiquidityExtrinsic, getRemoveLiquidityExtrinsic, - signAndSendTx, + signAndSendTx2, } from "@/utils"; import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; @@ -23,10 +23,10 @@ const PoolForm: FC = ({ const [buttonLabel, setButtonLabel] = useState("Add to Pool"); const { poolAction, - setTxStatus, - setSuccessStatus, - setProgressStatus, - setFailStatus, + setTxIdle, + setTxPending, + setTxSuccess, + setTxFailure, slippage, @@ -86,34 +86,61 @@ const PoolForm: FC = ({ event.preventDefault(); if (!extrinsic || !api) return; - setProgressStatus(); - let status: Awaited>; try { - status = await signAndSendTx( - api, + setTxPending(); + const tx = await signAndSendTx2( extrinsic, selectedAccount.address, wallet.signer ); + + tx.on("txCancelled", () => setTxIdle()); + + tx.on("txHashed", () => { + setTxPending({ + txHashLink: tx.getHashLink(), + }); + }); + + tx.on("txFailed", (result) => + setTxFailure({ + errorCode: tx.decodeError(result), + txHashLink: tx.getHashLink(), + }) + ); + + tx.on("txSucceeded", (result) => { + const event = tx.findEvent( + result, + "cennzx", + poolAction === "Remove" ? "RemoveLiquidity" : "AddLiquidity" + ); + + const coreValue = Balance.fromCodec(event.data[1], coreAsset); + const tradeValue = Balance.fromCodec(event.data[3], tradeAsset); + + setTrValue(""); + updateBalances(); + updatePoolUserInfo(); + updateExchangeRate(); + setTxSuccess({ + coreValue, + tradeValue, + txHashLink: tx.getHashLink(), + }); + }); } catch (error) { console.info(error); - return setFailStatus(error?.code); + return setTxFailure({ errorCode: error?.code as string }); } - - if (status === "cancelled") return setTxStatus(null); - - setSuccessStatus(); - setTrValue(""); - updateBalances(); - updatePoolUserInfo(); - updateExchangeRate(); }, [ extrinsic, - setProgressStatus, - setTxStatus, - setSuccessStatus, + setTxIdle, + setTxPending, + setTxSuccess, + setTxFailure, setTrValue, updateBalances, updatePoolUserInfo, @@ -121,7 +148,9 @@ const PoolForm: FC = ({ api, selectedAccount?.address, wallet?.signer, - setFailStatus, + coreAsset, + tradeAsset, + poolAction, ] ); diff --git a/components/PoolProgress.tsx b/components/PoolProgress.tsx index 1fdee07b..30f78884 100644 --- a/components/PoolProgress.tsx +++ b/components/PoolProgress.tsx @@ -1,4 +1,4 @@ -import { VFC } from "react"; +import { VFC, ReactNode } from "react"; import { IntrinsicElements } from "@/types"; import { css } from "@emotion/react"; import { usePool } from "@/providers/PoolProvider"; @@ -6,76 +6,125 @@ import { Theme, CircularProgress } from "@mui/material"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined"; import StandardButton from "@/components/shared/StandardButton"; +import Link from "@/components/Link"; +import ProgressOverlay from "@/components/shared/ProgressOverlay"; +import { Balance, selectMap } from "@/utils"; interface PoolProgressProps {} const PoolProgress: VFC = ( props ) => { - const { txStatus, setTxStatus } = usePool(); + const { txStatus, setTxIdle } = usePool(); + const { txHashLink, ...txProps } = txStatus?.props ?? {}; return ( -
- {!!txStatus && ( -
- {txStatus.status === "in-progress" && ( - - )} - {txStatus.status === "success" && ( - - )} - {txStatus.status === "fail" && ( - - )} - -
{txStatus.title}
-
{txStatus.message}
- - {txStatus.status !== "in-progress" && ( - setTxStatus(null)} - > - Dismiss - - )} -
+ + {selectMap( + txStatus?.status, + { + Pending: , + Success: , + Failure: , + }, + null )} -
+ + {!!txHashLink && ( + + View Transaction + + )} + ); }; export default PoolProgress; -const styles = { - root: - (show: boolean) => - ({ transitions }: Theme) => - css` - position: absolute; - inset: 0; - background-color: rgba(255, 255, 255, 0.9); - z-index: 100; - opacity: ${show ? 1 : 0}; - pointer-events: ${show ? "all" : "none"}; - transition: opacity ${transitions.duration.short}ms; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(2px); - padding: 5em; - text-align: center; - font-size: 14px; - `, - - status: css` - margin-bottom: 1.5em; - `, +interface TxPendingProps {} + +const TxPending: VFC = (props) => { + return ( +
+ +

Transaction In Progress

+

+ Please sign the transaction when prompted and wait until it's + completed +

+
+ ); +}; + +interface TxSuccessProps { + txHash: string; + tradeValue: Balance; + coreValue: Balance; +} +const TxSuccess: VFC = ({ + txHash, + tradeValue, + coreValue, + ...props +}) => { + const { poolAction } = usePool(); + + return ( +
+ +

Transaction Completed

+

+ You successfully {poolAction === "Remove" ? "withdrew" : "added"}{" "} + + {tradeValue.toBalance()}{" "} + {tradeValue.getSymbol()} + {" "} + and{" "} + + {coreValue.toBalance()}{" "} + {coreValue.getSymbol()} + {" "} + {poolAction === "Remove" ? "from" : "to"} the Liquidity Pool. +

+
+ ); +}; + +interface TxFailureProps { + errorCode?: string; +} + +const TxFailure: VFC = ({ + errorCode, + ...props +}) => { + return ( +
+ +

Transaction Failed

+

+ An error occurred while processing your transaction. It might have gone + through, check your balances before trying again. +

+ {!!errorCode && ( +
+
+						#{errorCode}
+					
+
+ )} +
+ ); +}; + +const styles = { statusSuccess: ({ palette }: Theme) => css` width: 4em; height: 4em; @@ -83,41 +132,18 @@ const styles = { color: ${palette.success.main}; `, - statusFail: ({ palette }: Theme) => css` + statusFailure: ({ palette }: Theme) => css` width: 4em; height: 4em; font-size: 14px; color: ${palette.warning.main}; `, - title: ({ palette }: Theme) => css` - font-weight: bold; - font-size: 20px; - line-height: 1; - text-align: center; - text-transform: uppercase; - color: ${palette.primary.main}; - `, - - message: ({ palette }: Theme) => css` + button: css` margin-top: 1em; - line-height: 1.5; - - small { - font-size: 0.85em; - display: inline-block; - padding: 0.25em 0.5em; - margin-top: 0.5em; - } - - em { - font-weight: bold; - font-style: normal; - color: ${palette.primary.main}; - } `, - button: css` - margin-top: 2em; + errorCode: css` + margin-top: 0.5em; `, }; diff --git a/providers/PoolProvider.tsx b/providers/PoolProvider.tsx index 5c198b23..22c72281 100644 --- a/providers/PoolProvider.tsx +++ b/providers/PoolProvider.tsx @@ -6,23 +6,26 @@ import { SetStateAction, useContext, useState, - useCallback, useEffect, } from "react"; import { useTokenInput, - TokenInputHook, usePoolExchangeInfo, - PoolExchangeInfoHook, usePoolUserInfo, + useTxStatus, + TokenInputHook, + PoolExchangeInfoHook, PoolUserInfoHook, + TxStatusHook, } from "@/hooks"; -import { Balance } from "@/utils"; import { CENNZ_ASSET_ID, CPAY_ASSET_ID } from "@/constants"; type CENNZAssetId = CENNZAsset["assetId"]; -interface PoolContextType extends PoolExchangeInfoHook, PoolUserInfoHook { +interface PoolContextType + extends PoolExchangeInfoHook, + PoolUserInfoHook, + TxStatusHook { poolAction: PoolAction; setPoolAction: Dispatch>; tradeAssets: CENNZAsset[]; @@ -38,13 +41,6 @@ interface PoolContextType extends PoolExchangeInfoHook, PoolUserInfoHook { slippage: string; setSlippage: Dispatch>; - - txStatus: TxStatus; - setTxStatus: Dispatch>; - - setProgressStatus: () => void; - setSuccessStatus: () => void; - setFailStatus: (errorCode?: string) => void; } const PoolContext = createContext({} as PoolContextType); @@ -85,76 +81,6 @@ const PoolProvider: FC = ({ supportedAssets, children }) => { }, [poolAction, updatePoolUserInfo]); const [slippage, setSlippage] = useState("5"); - const [txStatus, setTxStatus] = useState(null); - - const setProgressStatus = useCallback(() => { - setTxStatus({ - status: "in-progress", - title: "Transaction In Progress", - message: ( -
- Please sign the transaction when prompted and wait until it's - completed -
- ), - }); - }, []); - - const setFailStatus = useCallback((errorCode?: string) => { - setTxStatus({ - status: "fail", - title: "Transaction Failed", - message: ( -
- An error occurred while processing your transaction - {!!errorCode && ( - <> -
-
-								#{errorCode}
-							
- - )} -
- ), - }); - }, []); - - const setSuccessStatus = useCallback(() => { - const trValue = Balance.format(tradeInput.value); - const trSymbol = tradeAsset.symbol; - - const crValue = Balance.format(coreInput.value); - const crSymbol = coreAsset.symbol; - - setTxStatus({ - status: "success", - title: "Transaction Completed", - message: ( -
- You successfully {poolAction === "Remove" ? "withdrew" : "added"}{" "} -
-						
-							{trValue} {trSymbol}
-						
-					
{" "} - and{" "} -
-						
-							{crValue} {crSymbol}
-						
-					
{" "} - to the Liquidity Pool. -
- ), - }); - }, [ - tradeInput.value, - tradeAsset.symbol, - coreInput.value, - coreAsset.symbol, - poolAction, - ]); return ( = ({ supportedAssets, children }) => { slippage, setSlippage, - txStatus, - setTxStatus, - - setProgressStatus, - setSuccessStatus, - setFailStatus, + ...useTxStatus(), }} > {children} diff --git a/providers/SwapProvider.tsx b/providers/SwapProvider.tsx index 274d577a..3a2c01bc 100644 --- a/providers/SwapProvider.tsx +++ b/providers/SwapProvider.tsx @@ -11,10 +11,10 @@ import { fetchSwapAssets } from "@/utils"; import { CENNZ_ASSET_ID, CPAY_ASSET_ID } from "@/constants"; import { useTokenInput, - TokenInputHook, - TxStatusHook, useTokensFetcher, useTxStatus, + TokenInputHook, + TxStatusHook, } from "@/hooks"; type CENNZAssetId = CENNZAsset["assetId"]; From 56aca648f5433983005798c9a27978d511d619c4 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Tue, 5 Apr 2022 15:51:18 +1200 Subject: [PATCH 5/9] Update Bridge section to use new ProgressOverlay system --- .env.development | 1 + .env.production | 1 + components/BridgeForm.tsx | 216 ++------------------------ components/BridgeProgress.tsx | 213 ++++++++++++++----------- components/PoolProgress.tsx | 2 - components/SwapProgress.tsx | 1 - components/shared/ProgressOverlay.tsx | 4 + constants.ts | 3 + hooks/index.ts | 3 + hooks/useDepositRequest.ts | 105 +++++++++++++ hooks/useHistoricalWithdrawRequest.ts | 121 +++++++++++++++ hooks/useWithdrawRequest.ts | 157 +++++++++++++++++++ providers/BridgeProvider.tsx | 120 ++------------ utils/CENNZTransaction.ts | 3 - utils/EthereumTransaction.ts | 42 +++++ utils/ensureRelayerDepositDone.ts | 8 +- utils/index.ts | 2 + utils/selectMap.ts | 8 +- utils/sendDepositRequest.ts | 67 ++++---- utils/sendWithdrawCENNZRequest.ts | 45 ++---- utils/sendWithdrawEthereumRequest.ts | 33 ++-- utils/waitForEventProof.ts | 26 ++++ 22 files changed, 681 insertions(+), 500 deletions(-) create mode 100644 hooks/useDepositRequest.ts create mode 100644 hooks/useHistoricalWithdrawRequest.ts create mode 100644 hooks/useWithdrawRequest.ts create mode 100644 utils/EthereumTransaction.ts create mode 100644 utils/waitForEventProof.ts diff --git a/.env.development b/.env.development index d6dff5c9..a1ba4e6f 100644 --- a/.env.development +++ b/.env.development @@ -6,3 +6,4 @@ NEXT_PUBLIC_ETH_CHAIN_ID=42 NEXT_PUBLIC_BRIDGE_RELAYER_URL="https://bridge-contracts.nikau.centrality.me" NEXT_PUBLIC_GA_ID="G-8YJT9T1YTT" NEXT_PUBLIC_CENNZ_EXPLORER_URL="https://uncoverexplorer.com" +NEXT_PUBLIC_ETH_EXPLORER_URL="https://kovan.etherscan.io" diff --git a/.env.production b/.env.production index b6fb50ca..689c4723 100644 --- a/.env.production +++ b/.env.production @@ -6,3 +6,4 @@ NEXT_PUBLIC_ETH_CHAIN_ID=1 NEXT_PUBLIC_BRIDGE_RELAYER_URL="https://bridge-contracts.centralityapp.com" NEXT_PUBLIC_GA_ID="G-8YJT9T1YTT" NEXT_PUBLIC_CENNZ_EXPLORER_URL="https://uncoverexplorer.com" +NEXT_PUBLIC_ETH_EXPLORER_URL="https://etherscan.io" diff --git a/components/BridgeForm.tsx b/components/BridgeForm.tsx index 15944883..a201b077 100644 --- a/components/BridgeForm.tsx +++ b/components/BridgeForm.tsx @@ -1,27 +1,16 @@ -import { BridgedEthereumToken, IntrinsicElements } from "@/types"; +import { IntrinsicElements } from "@/types"; import { FC, useCallback, useEffect, useState } from "react"; import SubmitButton from "@/components/shared/SubmitButton"; import { css } from "@emotion/react"; import { Theme } from "@mui/material"; import { useBridge } from "@/providers/BridgeProvider"; import useBridgeStatus from "@/hooks/useBridgeStatus"; -import { useCENNZApi } from "@/providers/CENNZApiProvider"; -import { useMetaMaskWallet } from "@/providers/MetaMaskWalletProvider"; -import { - Balance, - ensureBridgeDepositActive, - ensureBridgeWithdrawActive, - ensureEthereumChain, - ensureRelayerDepositDone, - sendDepositRequest, - sendWithdrawCENNZRequest, - sendWithdrawEthereumRequest, -} from "@/utils"; -import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; -import { useMetaMaskExtension } from "@/providers/MetaMaskExtensionProvider"; import HistoricalWithdrawal from "@/components/HistoricalWithdrawal"; -import { EthyEventId } from "@cennznet/types"; -import { EthEventProof } from "@cennznet/api/derives/ethBridge/types"; +import { + useDepositRequest, + useHistoricalWithdrawRequest, + useWithdrawRequest, +} from "@/hooks"; interface BridgeFormProps {} @@ -29,200 +18,15 @@ const BridgeForm: FC = ({ children, ...props }) => { - const { api } = useCENNZApi(); - const { wallet: metaMaskWallet } = useMetaMaskWallet(); - const { - bridgeAction, - transferInput, - transferAsset, - transferCENNZAddress, - setProgressStatus, - setSuccessStatus, - setFailStatus, - setTxStatus, - updateMetaMaskBalances, - transferMetaMaskAddress, - historicalBlockHash, - historicalEventProofId, - } = useBridge(); + const { bridgeAction } = useBridge(); const [buttonLabel, setButtonLabel] = useState("Deposit"); const [advancedExpanded, setAdvancedExpanded] = useState(false); - const { - updateBalances: updateCENNZBalances, - selectedAccount: cennzAccount, - wallet: cennzWallet, - } = useCENNZWallet(); - const { extension } = useMetaMaskExtension(); - - const processDepositRequest = useCallback(async () => { - const setTrValue = transferInput.setValue; - const transferAmount = Balance.fromInput( - transferInput.value, - transferAsset - ); - setProgressStatus(); - - let tx: Awaited>; - - try { - await ensureEthereumChain(extension); - await ensureBridgeDepositActive(api, metaMaskWallet); - tx = await sendDepositRequest( - transferAmount, - transferAsset, - transferCENNZAddress, - metaMaskWallet.getSigner() - ); - - if (tx !== "cancelled") - await ensureRelayerDepositDone(tx.hash, 600000, setProgressStatus); - } catch (error) { - console.info(error); - return setFailStatus(error?.code); - } - - if (tx === "cancelled") return setTxStatus(null); - - setSuccessStatus(); - setTrValue(""); - updateMetaMaskBalances(); - updateCENNZBalances(); - }, [ - api, - transferInput, - transferAsset, - transferCENNZAddress, - metaMaskWallet, - setProgressStatus, - setSuccessStatus, - setFailStatus, - setTxStatus, - updateMetaMaskBalances, - updateCENNZBalances, - extension, - ]); - - const processWithdrawRequest = useCallback(async () => { - const setTrValue = transferInput.setValue; - const transferAmount = Balance.fromInput( - transferInput.value, - transferAsset - ); - setProgressStatus(); - - let eventProof: Awaited>; - try { - await ensureEthereumChain(extension); - await ensureBridgeWithdrawActive(api, metaMaskWallet); - setProgressStatus("CennznetConfirming"); - eventProof = await sendWithdrawCENNZRequest( - api, - transferAmount, - transferAsset as BridgedEthereumToken, - cennzAccount.address, - transferMetaMaskAddress, - cennzWallet.signer - ); - } catch (error) { - console.info(error); - return setFailStatus(error?.code); - } - - if (eventProof === "cancelled") return setTxStatus(null); - - let tx: Awaited>; - try { - setProgressStatus("EthereumConfirming"); - tx = await sendWithdrawEthereumRequest( - api, - eventProof, - transferAmount, - transferAsset as BridgedEthereumToken, - transferMetaMaskAddress, - metaMaskWallet.getSigner() - ); - } catch (error) { - console.info(error); - return setFailStatus(error?.code); - } - - if (tx === "cancelled") return setTxStatus(null); - - setSuccessStatus(); - setTrValue(""); - updateMetaMaskBalances(); - updateCENNZBalances(); - }, [ - api, - transferAsset, - cennzAccount?.address, - cennzWallet?.signer, - setProgressStatus, - setSuccessStatus, - setFailStatus, - setTxStatus, - transferInput, - transferMetaMaskAddress, - updateMetaMaskBalances, - updateCENNZBalances, - metaMaskWallet, - extension, - ]); - - const processHistoricalWithdrawRequest = useCallback(async () => { - const setTrValue = transferInput.setValue; - const transferAmount = Balance.fromInput( - transferInput.value, - transferAsset - ); - setProgressStatus(); - - const eventProof: Awaited = - await api.derive.ethBridge.eventProof( - historicalEventProofId as unknown as EthyEventId - ); - - let tx: Awaited>; - try { - setProgressStatus("EthereumConfirming"); - tx = await sendWithdrawEthereumRequest( - api, - eventProof, - transferAmount, - transferAsset as BridgedEthereumToken, - transferMetaMaskAddress, - metaMaskWallet.getSigner(), - historicalBlockHash - ); - } catch (error) { - console.info(error); - return setFailStatus(error?.code); - } + const processDepositRequest = useDepositRequest(); - if (tx === "cancelled") return setTxStatus(null); + const processWithdrawRequest = useWithdrawRequest(); - setSuccessStatus(); - setTrValue(""); - updateMetaMaskBalances(); - updateCENNZBalances(); - setAdvancedExpanded(false); - }, [ - api, - transferAsset, - setProgressStatus, - setSuccessStatus, - setFailStatus, - setTxStatus, - transferInput, - transferMetaMaskAddress, - updateMetaMaskBalances, - updateCENNZBalances, - metaMaskWallet, - historicalBlockHash, - historicalEventProofId, - setAdvancedExpanded, - ]); + const processHistoricalWithdrawRequest = useHistoricalWithdrawRequest(); const onFormSubmit = useCallback( async (event) => { diff --git a/components/BridgeProgress.tsx b/components/BridgeProgress.tsx index f6866867..d6148666 100644 --- a/components/BridgeProgress.tsx +++ b/components/BridgeProgress.tsx @@ -1,81 +1,140 @@ -import { VFC } from "react"; -import { IntrinsicElements } from "@/types"; +import { ReactNode, VFC } from "react"; +import { IntrinsicElements, RelayerConfirmingStatus } from "@/types"; import { css } from "@emotion/react"; import { useBridge } from "@/providers/BridgeProvider"; import { Theme, CircularProgress } from "@mui/material"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import CheckCircleOutlinedIcon from "@mui/icons-material/CheckCircleOutlined"; import StandardButton from "@/components/shared/StandardButton"; +import ProgressOverlay from "@/components/shared/ProgressOverlay"; +import Link from "@/components/Link"; +import { Balance, selectMap } from "@/utils"; interface BridgeProgressProps {} const BridgeProgress: VFC = ( props ) => { - const { txStatus, setTxStatus } = useBridge(); + const { txStatus, setTxIdle } = useBridge(); + const { txHashLink, ...txProps } = txStatus?.props ?? {}; return ( -
- {!!txStatus && ( -
- {txStatus.status === "in-progress" && ( - - )} - {txStatus.status === "success" && ( - - )} - {txStatus.status === "fail" && ( - - )} - -
{txStatus.title}
-
{txStatus.message}
- - {txStatus.status !== "in-progress" && ( - setTxStatus(null)} - > - Dismiss - - )} -
+ + {selectMap( + txStatus?.status, + { + Pending: , + Success: , + Failure: , + }, + null )} -
+ + {!!txHashLink && ( + + View Transaction + + )} + ); }; export default BridgeProgress; -const styles = { - root: - (show: boolean) => - ({ transitions }: Theme) => - css` - position: absolute; - inset: 0; - background-color: rgba(255, 255, 255, 0.9); - z-index: 100; - opacity: ${show ? 1 : 0}; - pointer-events: ${show ? "all" : "none"}; - transition: opacity ${transitions.duration.short}ms; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(2px); - padding: 5em; - text-align: center; - font-size: 14px; - `, - - status: css` - margin-bottom: 1.5em; - `, +interface TxPendingProps { + relayerStatus: RelayerConfirmingStatus; +} +const TxPending: VFC = ({ + relayerStatus, + ...props +}) => { + return ( +
+ +

+ {selectMap( + relayerStatus, + { + EthereumConfirming: "Confirming on Ethereum", + CennznetConfirming: ( + <> + Confirming on CENNZnet + + ), + }, + "Transaction In Progress" + )} +

+

+ Please sign the transaction when prompted and wait until it's + completed +

+
+ ); +}; + +interface TxSuccessProps { + transferValue: Balance; +} + +const TxSuccess: VFC = ({ + transferValue, + ...props +}) => { + const { bridgeAction } = useBridge(); + + return ( +
+ +

Transaction Completed

+

+ You successfully{" "} + {bridgeAction === "Withdraw" ? "withdrew" : "deposited"}{" "} + + {transferValue.toBalance()}{" "} + {transferValue.getSymbol()} + {" "} + {bridgeAction === "Withdraw" ? "from" : "to"} CENNZnet. +

+
+ ); +}; + +interface TxFailureProps { + errorCode?: string; +} + +const TxFailure: VFC = ({ + errorCode, + ...props +}) => { + return ( +
+ +

Transaction Failed

+

+ An error occurred while processing your transaction. It might have gone + through, check your balances before trying again. +

+ {!!errorCode && ( +
+
+						#{errorCode}
+					
+
+ )} +
+ ); +}; + +const styles = { statusSuccess: ({ palette }: Theme) => css` width: 4em; height: 4em; @@ -83,50 +142,18 @@ const styles = { color: ${palette.success.main}; `, - statusFail: ({ palette }: Theme) => css` + statusFailure: ({ palette }: Theme) => css` width: 4em; height: 4em; font-size: 14px; color: ${palette.warning.main}; `, - title: ({ palette }: Theme) => css` - font-weight: bold; - font-size: 20px; - line-height: 1; - text-align: center; - text-transform: uppercase; - color: ${palette.primary.main}; - - span { - text-transform: none !important; - } - `, - - message: ({ palette }: Theme) => css` + button: css` margin-top: 1em; - line-height: 1.5; - - small { - font-size: 0.85em; - display: inline-block; - padding: 0.25em 0.5em; - margin-top: 0.5em; - } - - em { - font-weight: bold; - font-style: normal; - color: ${palette.primary.main}; - font-size: 0.5em; - span { - font-size: 2em; - letter-spacing: -0.025em; - } - } `, - button: css` - margin-top: 2em; + errorCode: css` + margin-top: 0.5em; `, }; diff --git a/components/PoolProgress.tsx b/components/PoolProgress.tsx index 30f78884..141b35af 100644 --- a/components/PoolProgress.tsx +++ b/components/PoolProgress.tsx @@ -63,13 +63,11 @@ const TxPending: VFC = (props) => { }; interface TxSuccessProps { - txHash: string; tradeValue: Balance; coreValue: Balance; } const TxSuccess: VFC = ({ - txHash, tradeValue, coreValue, ...props diff --git a/components/SwapProgress.tsx b/components/SwapProgress.tsx index 1814b295..7c393929 100644 --- a/components/SwapProgress.tsx +++ b/components/SwapProgress.tsx @@ -69,7 +69,6 @@ interface TxSuccessProps { } const TxSuccess: VFC = ({ - txHash, exchangeValue, receiveValue, ...props diff --git a/components/shared/ProgressOverlay.tsx b/components/shared/ProgressOverlay.tsx index 8f9c2b7c..285a09a2 100644 --- a/components/shared/ProgressOverlay.tsx +++ b/components/shared/ProgressOverlay.tsx @@ -68,6 +68,10 @@ const styles = { text-align: center; text-transform: uppercase; color: ${palette.primary.main}; + + span { + text-transform: none; + } } p { diff --git a/constants.ts b/constants.ts index a074176a..74f9029d 100644 --- a/constants.ts +++ b/constants.ts @@ -36,3 +36,6 @@ export const KOVAN_PEG_CONTRACT: string = export const CENNZ_EXPLORER_URL: string = process.env.NEXT_PUBLIC_CENNZ_EXPLORER_URL; + +export const ETH_EXPLORER_URL: string = + process.env.NEXT_PUBLIC_ETH_EXPLORER_URL; diff --git a/hooks/index.ts b/hooks/index.ts index 709e5e0a..ba384314 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -13,6 +13,9 @@ export { default as useBridgeGasFee } from "@/hooks/useBridgeGasFee"; export { default as useBridgeVerificationFee } from "@/hooks/useBridgeVerificationFee"; export { default as useBlockHashValidation } from "@/hooks/useBlockHashValidation"; export { default as useTxStatus } from "@/hooks/useTxStatus"; +export { default as useDepositRequest } from "@/hooks/useDepositRequest"; +export { default as useWithdrawRequest } from "@/hooks/useWithdrawRequest"; +export { default as useHistoricalWithdrawRequest } from "@/hooks/useHistoricalWithdrawRequest"; export type { TokenInputHook } from "@/hooks/useTokenInput"; export type { PoolExchangeInfoHook } from "@/hooks/usePoolExchangeInfo"; diff --git a/hooks/useDepositRequest.ts b/hooks/useDepositRequest.ts new file mode 100644 index 00000000..be87eeb5 --- /dev/null +++ b/hooks/useDepositRequest.ts @@ -0,0 +1,105 @@ +import { useBridge } from "@/providers/BridgeProvider"; +import { useCENNZApi } from "@/providers/CENNZApiProvider"; +import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; +import { useMetaMaskExtension } from "@/providers/MetaMaskExtensionProvider"; +import { useMetaMaskWallet } from "@/providers/MetaMaskWalletProvider"; +import { + Balance, + ensureBridgeDepositActive, + ensureEthereumChain, + ensureRelayerDepositDone, + sendDepositRequest, +} from "@/utils"; +import { useCallback } from "react"; + +export default function useDepositRequest(): () => Promise { + const { api } = useCENNZApi(); + const { wallet: metaMaskWallet } = useMetaMaskWallet(); + const { extension } = useMetaMaskExtension(); + const { updateBalances: updateCENNZBalances } = useCENNZWallet(); + const { + transferInput, + transferAsset, + transferCENNZAddress, + setTxIdle, + setTxPending, + setTxSuccess, + setTxFailure, + updateMetaMaskBalances, + } = useBridge(); + + return useCallback(async () => { + const setTrValue = transferInput.setValue; + const transferAmount = Balance.fromInput( + transferInput.value, + transferAsset + ); + + try { + setTxPending(); + await ensureEthereumChain(extension); + await ensureBridgeDepositActive(api, metaMaskWallet); + const tx = await sendDepositRequest( + transferAmount, + transferAsset, + transferCENNZAddress, + metaMaskWallet.getSigner() + ); + + tx.on("txCancelled", () => setTxIdle()); + + tx.on("txHashed", () => { + setTxPending({ + txHashLink: tx.getHashLink(), + }); + }); + + tx.on("txFailed", (errorCode) => + setTxFailure({ + errorCode, + txHashLink: tx.getHashLink(), + }) + ); + + tx.on("txSucceeded", () => { + ensureRelayerDepositDone(tx.hash, 600000, (status) => + setTxPending({ relayerStatus: status, txHashLink: tx.getHashLink() }) + ) + .then(() => { + setTrValue(""); + updateMetaMaskBalances(); + updateCENNZBalances(); + setTxSuccess({ + transferValue: transferAmount, + txHashLink: tx.getHashLink(), + }); + }) + .catch((error) => { + console.info(error); + return setTxFailure({ + errorCode: error?.code, + txHashLink: tx.getHashLink(), + }); + }); + }); + } catch (error) { + console.info(error); + return setTxFailure({ + errorCode: error?.code, + }); + } + }, [ + api, + transferInput, + transferAsset, + transferCENNZAddress, + metaMaskWallet, + updateMetaMaskBalances, + updateCENNZBalances, + extension, + setTxIdle, + setTxFailure, + setTxPending, + setTxSuccess, + ]); +} diff --git a/hooks/useHistoricalWithdrawRequest.ts b/hooks/useHistoricalWithdrawRequest.ts new file mode 100644 index 00000000..9a13deab --- /dev/null +++ b/hooks/useHistoricalWithdrawRequest.ts @@ -0,0 +1,121 @@ +import { useBridge } from "@/providers/BridgeProvider"; +import { useCENNZApi } from "@/providers/CENNZApiProvider"; +import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; +import { useMetaMaskExtension } from "@/providers/MetaMaskExtensionProvider"; +import { useMetaMaskWallet } from "@/providers/MetaMaskWalletProvider"; +import { BridgedEthereumToken } from "@/types"; +import { + Balance, + ensureBridgeWithdrawActive, + ensureEthereumChain, + sendWithdrawEthereumRequest, +} from "@/utils"; +import { EthEventProof } from "@cennznet/api/derives/ethBridge/types"; +import { EthyEventId } from "@cennznet/types"; +import { useCallback } from "react"; + +export default function useHistoricalWithdrawRequest(): () => Promise { + const { + transferInput, + transferAsset, + transferMetaMaskAddress, + setTxIdle, + setTxPending, + setTxSuccess, + setTxFailure, + updateMetaMaskBalances, + historicalEventProofId, + historicalBlockHash, + } = useBridge(); + const { api } = useCENNZApi(); + const { updateBalances: updateCENNZBalances } = useCENNZWallet(); + const { wallet: metaMaskWallet } = useMetaMaskWallet(); + const { extension } = useMetaMaskExtension(); + + return useCallback(async () => { + const setTrValue = transferInput.setValue; + const transferAmount = Balance.fromInput( + transferInput.value, + transferAsset + ); + + try { + setTxPending(); + await ensureEthereumChain(extension); + await ensureBridgeWithdrawActive(api, metaMaskWallet); + const eventProof: EthEventProof = await api.derive.ethBridge.eventProof( + historicalEventProofId as unknown as EthyEventId + ); + + console.log({ eventProof }); + + setTxPending({ + relayerStatus: "EthereumConfirming", + }); + + sendWithdrawEthereumRequest( + api, + eventProof, + transferAmount, + transferAsset as BridgedEthereumToken, + transferMetaMaskAddress, + metaMaskWallet.getSigner(), + historicalBlockHash + ) + .then((withdrawTx) => { + withdrawTx.on("txHashed", (hash) => { + setTxPending({ + relayerStatus: "EthereumConfirming", + txHashLink: withdrawTx.getHashLink(), + }); + }); + + withdrawTx.on("txSucceeded", () => { + setTrValue(""); + updateMetaMaskBalances(); + updateCENNZBalances(); + setTxSuccess({ + transferValue: transferAmount, + txHashLink: withdrawTx.getHashLink(), + }); + }); + + withdrawTx.on("txFailed", (errorCode) => { + return setTxFailure({ + errorCode, + txHashLink: withdrawTx.getHashLink(), + }); + }); + + withdrawTx.on("txCancelled", () => setTxIdle()); + }) + .catch((error) => { + console.info(error); + return setTxFailure({ + errorCode: error?.code, + }); + }); + } catch (error) { + console.info(error); + return setTxFailure({ + errorCode: error?.code, + }); + } + }, [ + transferInput.setValue, + transferInput.value, + transferAsset, + setTxPending, + extension, + api, + metaMaskWallet, + historicalEventProofId, + transferMetaMaskAddress, + historicalBlockHash, + updateMetaMaskBalances, + updateCENNZBalances, + setTxSuccess, + setTxFailure, + setTxIdle, + ]); +} diff --git a/hooks/useWithdrawRequest.ts b/hooks/useWithdrawRequest.ts new file mode 100644 index 00000000..b968c73a --- /dev/null +++ b/hooks/useWithdrawRequest.ts @@ -0,0 +1,157 @@ +import { useBridge } from "@/providers/BridgeProvider"; +import { useCENNZApi } from "@/providers/CENNZApiProvider"; +import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; +import { useMetaMaskExtension } from "@/providers/MetaMaskExtensionProvider"; +import { useMetaMaskWallet } from "@/providers/MetaMaskWalletProvider"; +import { BridgedEthereumToken } from "@/types"; +import { + Balance, + ensureBridgeWithdrawActive, + ensureEthereumChain, + sendWithdrawCENNZRequest, + sendWithdrawEthereumRequest, + waitForEventProof, +} from "@/utils"; +import { EthyEventId } from "@cennznet/types"; +import { useCallback } from "react"; + +export default function useWithdrawRequest(): () => Promise { + const { + transferInput, + transferAsset, + transferMetaMaskAddress, + setTxIdle, + setTxPending, + setTxSuccess, + setTxFailure, + updateMetaMaskBalances, + } = useBridge(); + const { api } = useCENNZApi(); + const { + selectedAccount: cennzAccount, + wallet: cennzWallet, + updateBalances: updateCENNZBalances, + } = useCENNZWallet(); + const { wallet: metaMaskWallet } = useMetaMaskWallet(); + const { extension } = useMetaMaskExtension(); + + return useCallback(async () => { + const setTrValue = transferInput.setValue; + const transferAmount = Balance.fromInput( + transferInput.value, + transferAsset + ); + + try { + setTxPending(); + await ensureEthereumChain(extension); + await ensureBridgeWithdrawActive(api, metaMaskWallet); + const tx = await sendWithdrawCENNZRequest( + api, + transferAmount, + transferAsset as BridgedEthereumToken, + cennzAccount.address, + transferMetaMaskAddress, + cennzWallet.signer + ); + + tx.on("txCancelled", () => setTxIdle()); + + tx.on("txHashed", () => { + setTxPending({ + relayerStatus: "CennznetConfirming", + }); + }); + + tx.on("txFailed", (errorCode) => + setTxFailure({ + errorCode, + }) + ); + + tx.on("txSucceeded", (result) => { + const erc20WithdrawEvent = tx.findEvent( + result, + "erc20Peg", + "Erc20Withdraw" + ); + + const eventProofId = + erc20WithdrawEvent?.data?.[0].toJSON() as unknown as EthyEventId; + + if (!eventProofId) + return setTxFailure({ errorCode: "erc20Peg.EventProofIdNotFound" }); + + waitForEventProof(api, eventProofId) + .then((eventProof) => { + setTxPending({ + relayerStatus: "EthereumConfirming", + }); + + return sendWithdrawEthereumRequest( + api, + eventProof, + transferAmount, + transferAsset as BridgedEthereumToken, + transferMetaMaskAddress, + metaMaskWallet.getSigner() + ); + }) + .then((withdrawTx) => { + withdrawTx.on("txHashed", (hash) => { + setTxPending({ + relayerStatus: "EthereumConfirming", + txHashLink: withdrawTx.getHashLink(), + }); + }); + + withdrawTx.on("txSucceeded", () => { + setTrValue(""); + updateMetaMaskBalances(); + updateCENNZBalances(); + setTxSuccess({ + transferValue: transferAmount, + txHashLink: withdrawTx.getHashLink(), + }); + }); + + withdrawTx.on("txFailed", (errorCode) => { + return setTxFailure({ + errorCode, + txHashLink: withdrawTx.getHashLink(), + }); + }); + + withdrawTx.on("txCancelled", () => setTxIdle()); + }) + .catch((error) => { + console.info(error); + return setTxFailure({ + errorCode: error?.code, + }); + }); + }); + } catch (error) { + console.info(error); + return setTxFailure({ + errorCode: error?.code, + }); + } + }, [ + transferInput.setValue, + transferInput.value, + transferAsset, + setTxPending, + extension, + api, + metaMaskWallet, + cennzAccount?.address, + transferMetaMaskAddress, + cennzWallet?.signer, + setTxIdle, + setTxFailure, + updateMetaMaskBalances, + updateCENNZBalances, + setTxSuccess, + ]); +} diff --git a/providers/BridgeProvider.tsx b/providers/BridgeProvider.tsx index 35c23c45..d02c1cb1 100644 --- a/providers/BridgeProvider.tsx +++ b/providers/BridgeProvider.tsx @@ -1,14 +1,13 @@ import { ETH_TOKEN_ADDRESS } from "@/constants"; -import { TokenInputHook, useTokenInput } from "@/hooks"; -import useMetaMaskBalances from "@/hooks/useMetaMaskBalances"; import { - BridgeAction, - BridgedEthereumToken, - EthereumToken, - RelayerConfirmingStatus, - TxStatus, -} from "@/types"; -import { Balance, selectMap } from "@/utils"; + useTokenInput, + useTxStatus, + TokenInputHook, + TxStatusHook, +} from "@/hooks"; +import useMetaMaskBalances from "@/hooks/useMetaMaskBalances"; +import { BridgeAction, BridgedEthereumToken, EthereumToken } from "@/types"; +import { Balance } from "@/utils"; import { createContext, Dispatch, @@ -22,7 +21,7 @@ import { type ERC20TokenAddress = EthereumToken["address"]; -interface BridgeContextType { +interface BridgeContextType extends TxStatusHook { bridgeAction: BridgeAction; setBridgeAction: Dispatch>; @@ -40,13 +39,6 @@ interface BridgeContextType { transferMetaMaskAddress: string; setTransferMetaMaskAddress: Dispatch>; - txStatus: TxStatus; - setTxStatus: Dispatch>; - - setProgressStatus: (status?: RelayerConfirmingStatus) => void; - setSuccessStatus: () => void; - setFailStatus: (errorCode?: string) => void; - metaMaskBalance: Balance; updateMetaMaskBalances: () => void; @@ -90,92 +82,6 @@ const BridgeProvider: FC = ({ (token) => token.address === transferSelect.tokenId ) || ethAsset; - const [txStatus, setTxStatus] = useState(null); - - const setProgressStatus = useCallback((status?: RelayerConfirmingStatus) => { - const title = selectMap( - status, - new Map([ - ["EthereumConfirming", <>Confirming on Ethereum], - [ - "CennznetConfirming", - <> - Confirming on CENNZnet - , - ], - ]), - "Transaction In Progress" - ); - - setTxStatus({ - status: "in-progress", - title, - message: ( -
- Please sign the transaction when prompted and wait until it's - completed -
- ), - }); - }, []); - - const setFailStatus = useCallback((errorCode?: string) => { - setTxStatus({ - status: "fail", - title: "Transaction Failed", - message: ( -
- An error occurred while processing your transaction - {!!errorCode && ( - <> -
-
-								#{errorCode}
-							
- - )} -
- ), - }); - }, []); - - const setSuccessStatus = useCallback(() => { - const trValue = Balance.format(transferInput.value); - const trSymbol = transferAsset.symbol; - - setTxStatus({ - status: "success", - title: "Transaction Completed", - ...(bridgeAction === "Withdraw" && { - message: ( -
- You successfully withdrew{" "} -
-							
-								{trValue} {trSymbol}
-							
-						
{" "} - from CENNZnet. -
- ), - }), - - ...(bridgeAction === "Deposit" && { - message: ( -
- You successfully deposited{" "} -
-							
-								{trValue} {trSymbol}
-							
-						
{" "} - to CENNZnet. -
- ), - }), - }); - }, [transferInput.value, transferAsset?.symbol, bridgeAction]); - const [metaMaskBalance, , updateMetaMaskBalances] = useMetaMaskBalances(transferAsset); @@ -215,12 +121,6 @@ const BridgeProvider: FC = ({ transferMetaMaskAddress, setTransferMetaMaskAddress, - txStatus, - setTxStatus, - setProgressStatus, - setSuccessStatus, - setFailStatus, - metaMaskBalance, updateMetaMaskBalances, @@ -228,6 +128,8 @@ const BridgeProvider: FC = ({ setHistoricalBlockHash, historicalEventProofId, setHistoricalEventProofId, + + ...useTxStatus(), }} > {children} diff --git a/utils/CENNZTransaction.ts b/utils/CENNZTransaction.ts index d3da8483..a21fc725 100644 --- a/utils/CENNZTransaction.ts +++ b/utils/CENNZTransaction.ts @@ -6,7 +6,6 @@ import { Event } from "@polkadot/types/interfaces"; interface EmitEvents { txCreated: undefined; txHashed: string; - txPending: SubmittableResult; txSucceeded: SubmittableResult; txFailed: SubmittableResult; txCancelled: undefined; @@ -29,8 +28,6 @@ export default class CENNZTransaction extends Emittery { setResult(result: SubmittableResult) { const { status, dispatchError } = result; - if (status.isInBlock) return this.emit("txPending", result); - if (status.isFinalized && !dispatchError) return this.emit("txSucceeded", result); diff --git a/utils/EthereumTransaction.ts b/utils/EthereumTransaction.ts new file mode 100644 index 00000000..4a7a5341 --- /dev/null +++ b/utils/EthereumTransaction.ts @@ -0,0 +1,42 @@ +import Emittery from "emittery"; +import { ETH_EXPLORER_URL } from "@/constants"; + +interface EmitEvents { + txCreated: undefined; + txHashed: string; + txSucceeded: undefined; + txFailed: string | number; + txCancelled: undefined; +} + +export default class EthereumTransaction extends Emittery { + public hash: string; + + constructor() { + super(); + this.emit("txCreated"); + } + + setHash(hash: string) { + console.log("setHash", hash); + const shouldEmit = this.hash !== hash; + this.hash = hash; + if (shouldEmit) this.emit("txHashed", hash); + } + + setSuccess() { + this.emit("txSucceeded"); + } + + setFailure(errorCode?: string | number) { + this.emit("txFailed", errorCode); + } + + setCancel() { + this.emit("txCancelled"); + } + + getHashLink(): string { + return this.hash ? `${ETH_EXPLORER_URL}/tx/${this.hash}` : null; + } +} diff --git a/utils/ensureRelayerDepositDone.ts b/utils/ensureRelayerDepositDone.ts index 86595dbd..cf698e34 100644 --- a/utils/ensureRelayerDepositDone.ts +++ b/utils/ensureRelayerDepositDone.ts @@ -1,14 +1,14 @@ import { fetchDepositRelayerStatus, waitUntil } from "@/utils"; import { RelayerStatus, RelayerConfirmingStatus } from "@/types"; -type TimoutReturn = Awaited>; +type TimeoutReturn = Awaited>; // TODO: Needs test export default async function ensureRelayerDepositDone( txHash: string, timeout: number = 60000, confirmingCallback?: (status: RelayerConfirmingStatus) => void -): Promise { +): Promise { const status = await waitUntilDepositDone( txHash, timeout, @@ -17,15 +17,13 @@ export default async function ensureRelayerDepositDone( if (status === "timeout") throw { code: "RELAYER_TIMEOUT" }; if (status === "Failed") throw { code: "RELAYER_STATUS_FAILED" }; - - return status; } export async function waitUntilDepositDone( txHash: string, timeout: number = 60000, confirmingCallback?: (status: RelayerConfirmingStatus) => void -): Promise { +): Promise { let timedOut = false; const pollDepositRelayerStatus = () => { diff --git a/utils/index.ts b/utils/index.ts index 09ac200d..eaf57a00 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -42,3 +42,5 @@ export { default as trackPageView } from "@/utils/trackPageView"; export { default as getSellAssetExtrinsic } from "@/utils/getSellAssetExtrinsic"; export { default as selectMap } from "@/utils/selectMap"; export { default as CENNZTransaction } from "@/utils/CENNZTransaction"; +export { default as EthereumTransaction } from "@/utils/EthereumTransaction"; +export { default as waitForEventProof } from "@/utils/waitForEventProof"; diff --git a/utils/selectMap.ts b/utils/selectMap.ts index 741b7a0b..c83139b5 100644 --- a/utils/selectMap.ts +++ b/utils/selectMap.ts @@ -8,14 +8,10 @@ */ export default function selectMap( key: K, - object: Map | Partial>, + object: Partial>, defaultValue?: V ): V { - const map: Map = - object instanceof Map - ? object - : new Map(Object.entries(object) as [K, V][]); - + const map: Map = new Map(Object.entries(object) as [K, V][]); if (!map.has(key)) return defaultValue; return map.get(key); } diff --git a/utils/sendDepositRequest.ts b/utils/sendDepositRequest.ts index 9e752a52..eaff85af 100644 --- a/utils/sendDepositRequest.ts +++ b/utils/sendDepositRequest.ts @@ -1,34 +1,29 @@ import { BridgedEthereumToken, EthereumToken } from "@/types"; -import { Balance, getERC20TokenContract, getERC20PegContract } from "@/utils"; +import { + Balance, + getERC20TokenContract, + getERC20PegContract, + EthereumTransaction, +} from "@/utils"; import { ethers } from "ethers"; import { ETH_TOKEN_ADDRESS } from "@/constants"; import { decodeAddress } from "@polkadot/keyring"; import { TransactionResponse } from "@ethersproject/abstract-provider"; +// TODO: Needs test export default async function sendDepositRequest( transferAmount: Balance, transferToken: EthereumToken | BridgedEthereumToken, cennzAddress: string, signer: ethers.Signer -): Promise { +): Promise { const pegContract = getERC20PegContract<"OnBehalf">(signer); const decodedAddress = decodeAddress(cennzAddress); const transferValue = transferAmount.toBigNumber(); - - try { - if (transferToken.address === ETH_TOKEN_ADDRESS) { - const tx: TransactionResponse = await pegContract.deposit( - transferToken.address, - transferValue, - decodedAddress, - { - value: transferValue, - } - ); - - await tx.wait(); - return tx; - } + const isERC20Contract = transferToken.address !== ETH_TOKEN_ADDRESS; + const tx = new EthereumTransaction(); + const requestContractApproval = async () => { + if (!isERC20Contract) return Promise.resolve(); const tokenContract = getERC20TokenContract<"OnBehalf">( transferToken, @@ -40,18 +35,36 @@ export default async function sendDepositRequest( transferValue ); - await approveTx.wait(); + return await approveTx.wait(); + }; - const tx: TransactionResponse = await pegContract.deposit( + await requestContractApproval(); + + pegContract + .deposit( transferToken.address, transferValue, - decodedAddress - ); - await tx.wait(); + decodedAddress, + isERC20Contract + ? null + : { + value: transferValue, + } + ) + .then((pegTx: TransactionResponse) => { + tx.setHash(pegTx.hash); + return pegTx.wait(2); + }) + .then(() => { + tx.setSuccess(); + }) + .catch((error) => { + if (error?.code === 4001) { + tx.setCancel(); + return; + } + tx.setFailure(error?.code); + }); - return tx; - } catch (error) { - if (error?.code === 4001) return "cancelled"; - throw error; - } + return tx; } diff --git a/utils/sendWithdrawCENNZRequest.ts b/utils/sendWithdrawCENNZRequest.ts index 9eeefa3f..4adcfe53 100644 --- a/utils/sendWithdrawCENNZRequest.ts +++ b/utils/sendWithdrawCENNZRequest.ts @@ -1,9 +1,12 @@ import { CENNZAsset } from "@/types"; import { Api } from "@cennznet/api"; -import { Balance, getPegWithdrawExtrinsic, waitUntil } from "@/utils"; -import signAndSendTx from "@/utils/signAndSendTx"; +import { + Balance, + getPegWithdrawExtrinsic, + signAndSendTx2, + CENNZTransaction, +} from "@/utils"; import { Signer } from "@cennznet/api/types"; -import { EthEventProof } from "@cennznet/api/derives/ethBridge/types"; // TODO: Needs test export default async function sendWithdrawCENNZRequest( @@ -13,44 +16,14 @@ export default async function sendWithdrawCENNZRequest( cennzAddress: string, ethereumAddress: string, signer: Signer -): Promise { +): Promise { const extrinsic = getPegWithdrawExtrinsic( api, transferAsset.assetId, transferAmount, ethereumAddress ); + const pegTx = await signAndSendTx2(extrinsic, cennzAddress, signer); - const status = await signAndSendTx(api, extrinsic, cennzAddress, signer); - - if (status === "cancelled") return status; - - const erc20WithdrawEvent = status.events?.find((event) => { - const { - event: { method, section }, - } = event; - - return section === "erc20Peg" && method === "Erc20Withdraw"; - }); - - const eventProofId = erc20WithdrawEvent?.event?.data?.[0]; - - if (!eventProofId) throw { code: "erc20Peg.EventProofIdNotFound" }; - - const eventProof = await Promise.race([ - waitUntil(10000), - new Promise(async (resolve) => { - const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => { - api.derive.ethBridge.eventProof(eventProofId).then((eventProof) => { - if (!eventProof) return; - unsubscribe(); - resolve(eventProof); - }); - }); - }), - ]); - - if (eventProof === "timeout") throw { code: "erc20Peg.EventProofTimeout" }; - - return eventProof; + return pegTx; } diff --git a/utils/sendWithdrawEthereumRequest.ts b/utils/sendWithdrawEthereumRequest.ts index 7a393887..070681e7 100644 --- a/utils/sendWithdrawEthereumRequest.ts +++ b/utils/sendWithdrawEthereumRequest.ts @@ -1,5 +1,5 @@ import { BridgedEthereumToken } from "@/types"; -import { Balance, getBridgeContract } from "@/utils"; +import { Balance, EthereumTransaction, getBridgeContract } from "@/utils"; import getERC20PegContract from "@/utils/getERC20PegContract"; import { Api } from "@cennznet/api"; import { EthEventProof } from "@cennznet/api/derives/ethBridge/types"; @@ -14,7 +14,7 @@ export default async function sendWithdrawEthereumRequest( ethereumAddress: string, signer: ethers.Signer, blockHash?: string -): Promise { +): Promise { const notaryKeys = !!blockHash ? (( await api.query.ethBridge.notaryKeys.at(blockHash) @@ -41,20 +41,29 @@ export default async function sendWithdrawEthereumRequest( { ...eventProof, validators }, { value: verificationFee } ); - - try { - const tx: TransactionResponse = await pegContract.withdraw( + const tx = new EthereumTransaction(); + pegContract + .withdraw( transferAsset.address, transferAmount.toBigNumber(), ethereumAddress, { ...eventProof, validators }, { value: verificationFee, gasLimit: (gasFee.toNumber() * 1.02).toFixed() } - ); + ) + .then((pegTx: TransactionResponse) => { + tx.setHash(pegTx.hash); + return pegTx.wait(2); + }) + .then(() => { + tx.setSuccess(); + }) + .catch((error) => { + if (error?.code === 4001) { + tx.setCancel(); + return; + } + tx.setFailure(error?.code); + }); - await tx.wait(); - return tx; - } catch (error) { - if (error?.code === 4001) return "cancelled"; - throw error; - } + return tx; } diff --git a/utils/waitForEventProof.ts b/utils/waitForEventProof.ts new file mode 100644 index 00000000..0c934282 --- /dev/null +++ b/utils/waitForEventProof.ts @@ -0,0 +1,26 @@ +import { waitUntil } from "@/utils"; +import { Api } from "@cennznet/api"; +import { EthEventProof } from "@cennznet/api/derives/ethBridge/types"; +import { EthyEventId } from "@cennznet/types"; + +export default async function waitForEventProof( + api: Api, + eventProofId: EthyEventId +): Promise { + const eventProof = await Promise.race([ + waitUntil(10000), + new Promise(async (resolve) => { + const unsubscribe = await api.rpc.chain.subscribeNewHeads(() => { + api.derive.ethBridge.eventProof(eventProofId).then((eventProof) => { + if (!eventProof) return; + unsubscribe(); + resolve(eventProof); + }); + }); + }), + ]); + + if (eventProof === "timeout") throw { code: "erc20Peg.EventProofTimeout" }; + + return eventProof; +} From ccc38e2585af2a4cdd37d75568705160f386b50a Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Tue, 5 Apr 2022 16:07:44 +1200 Subject: [PATCH 6/9] Re-add `Dismiss` button back --- components/SwapProgress.tsx | 24 +++++++++++++++++++----- components/shared/StandardButton.tsx | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/components/SwapProgress.tsx b/components/SwapProgress.tsx index 7c393929..03151d1b 100644 --- a/components/SwapProgress.tsx +++ b/components/SwapProgress.tsx @@ -17,14 +17,14 @@ const SwapProgress: VFC = ( ) => { const { txStatus, setTxIdle } = useSwap(); const { txHashLink, ...txProps } = txStatus?.props ?? {}; + const dismissible = + txStatus?.status === "Success" || txStatus?.status === "Failure"; return ( {selectMap( txStatus?.status, @@ -37,10 +37,20 @@ const SwapProgress: VFC = ( )} {!!txHashLink && ( - + View Transaction )} + + {dismissible && ( + + Dismiss + + )} ); }; @@ -136,10 +146,14 @@ const styles = { color: ${palette.warning.main}; `, - button: css` + viewButton: css` margin-top: 1em; `, + dismissButton: css` + margin-top: 0.5em; + `, + errorCode: css` margin-top: 0.5em; `, diff --git a/components/shared/StandardButton.tsx b/components/shared/StandardButton.tsx index 1ff3547a..4c873f69 100644 --- a/components/shared/StandardButton.tsx +++ b/components/shared/StandardButton.tsx @@ -46,7 +46,7 @@ const styles = { secondary: ({ palette }: Theme) => css` border: 1px solid white; - color: ${palette.grey["800"]}; + color: ${palette.grey["600"]}; &: hover { background-color: white; From 7c9ceed9ce36117b1a983dfa27b03285c8651ca8 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Tue, 5 Apr 2022 16:12:48 +1200 Subject: [PATCH 7/9] Deprecate the old `signAndSend` function --- components/PoolForm.tsx | 4 +-- components/SwapForm.tsx | 4 +-- utils/index.ts | 5 +-- utils/sendWithdrawCENNZRequest.ts | 4 +-- utils/signAndSendTx.ts | 53 +------------------------------ 5 files changed, 8 insertions(+), 62 deletions(-) diff --git a/components/PoolForm.tsx b/components/PoolForm.tsx index 4bec8935..b29ccc0a 100644 --- a/components/PoolForm.tsx +++ b/components/PoolForm.tsx @@ -9,7 +9,7 @@ import { Balance, getAddLiquidityExtrinsic, getRemoveLiquidityExtrinsic, - signAndSendTx2, + signAndSendTx, } from "@/utils"; import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; @@ -89,7 +89,7 @@ const PoolForm: FC = ({ try { setTxPending(); - const tx = await signAndSendTx2( + const tx = await signAndSendTx( extrinsic, selectedAccount.address, wallet.signer diff --git a/components/SwapForm.tsx b/components/SwapForm.tsx index 62608d0f..b705b975 100644 --- a/components/SwapForm.tsx +++ b/components/SwapForm.tsx @@ -6,7 +6,7 @@ import SubmitButton from "@/components/shared/SubmitButton"; import { useSwap } from "@/providers/SwapProvider"; import { useCENNZApi } from "@/providers/CENNZApiProvider"; import { useCENNZWallet } from "@/providers/CENNZWalletProvider"; -import { Balance, getSellAssetExtrinsic, signAndSendTx2 } from "@/utils"; +import { Balance, getSellAssetExtrinsic, signAndSendTx } from "@/utils"; interface SwapFormProps {} @@ -46,7 +46,7 @@ const SwapForm: FC = ({ Number(slippage) ); - const tx = await signAndSendTx2( + const tx = await signAndSendTx( extrinsic, selectedAccount.address, wallet.signer diff --git a/utils/index.ts b/utils/index.ts index eaf57a00..8da1f2e8 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -9,10 +9,7 @@ export { default as getTokenLogo } from "@/utils/getTokenLogo"; export { default as fetchSellPrice } from "@/utils/fetchSellPrice"; export { default as fetchGasFee } from "@/utils/fetchGasFee"; export { default as getBuyAssetExtrinsic } from "@/utils/getBuyAssetExtrinsic"; -export { - default as signAndSendTx, - signAndSendTx2, -} from "@/utils/signAndSendTx"; +export { default as signAndSendTx } from "@/utils/signAndSendTx"; export { default as fetchPoolExchangeInfo } from "@/utils/fetchPoolExchangeInfo"; export { default as fetchPoolUserInfo } from "@/utils/fetchPoolUserInfo"; export { default as getAddLiquidityExtrinsic } from "@/utils/getAddLiquidityExtrinsic"; diff --git a/utils/sendWithdrawCENNZRequest.ts b/utils/sendWithdrawCENNZRequest.ts index 4adcfe53..7acd9034 100644 --- a/utils/sendWithdrawCENNZRequest.ts +++ b/utils/sendWithdrawCENNZRequest.ts @@ -3,7 +3,7 @@ import { Api } from "@cennznet/api"; import { Balance, getPegWithdrawExtrinsic, - signAndSendTx2, + signAndSendTx, CENNZTransaction, } from "@/utils"; import { Signer } from "@cennznet/api/types"; @@ -23,7 +23,7 @@ export default async function sendWithdrawCENNZRequest( transferAmount, ethereumAddress ); - const pegTx = await signAndSendTx2(extrinsic, cennzAddress, signer); + const pegTx = await signAndSendTx(extrinsic, cennzAddress, signer); return pegTx; } diff --git a/utils/signAndSendTx.ts b/utils/signAndSendTx.ts index 6e48f976..184f915c 100644 --- a/utils/signAndSendTx.ts +++ b/utils/signAndSendTx.ts @@ -7,59 +7,8 @@ interface TxReceipt { events: any[]; } +// TODO: Needs test export default async function signAndSendTx( - api: Api, - extrinsic: SubmittableExtrinsic<"promise", any>, - address: string, - signer: Signer -): Promise { - const signAndSend = async () => { - return new Promise((resolve, reject) => { - extrinsic - .signAndSend(address, { signer }, (progress) => { - const { dispatchError, status, events } = progress; - if (dispatchError && dispatchError?.isModule && status.isFinalized) { - const { index, error } = dispatchError.asModule.toJSON(); - const errorMeta = api.registry.findMetaError( - new Uint8Array([index, error]) - ); - const errorCode = - errorMeta?.section && errorMeta?.name - ? `${errorMeta.section}.${errorMeta.name}` - : `I${index}E${error}`; - - return reject( - new Error(`${errorCode}:${status?.asFinalized?.toString()}`) - ); - } - if (status.isFinalized) - return resolve({ - hash: status.asFinalized.toString(), - events, - } as TxReceipt); - }) - .catch((error) => reject(error)); - }); - }; - - try { - return await signAndSend().then((receipt) => { - console.info(`Transaction Finalized: ${receipt.hash}`); - return receipt; - }); - } catch (error) { - if (error?.message === "Cancelled") return "cancelled"; - const err = new Error( - "An error occured while sending your transaction request." - ); - const infoPair = error?.message?.split?.(":"); - (err as any).code = infoPair?.[0].trim(); - console.info(`Transaction Failed: ${infoPair?.[1]}`); - throw err; - } -} - -export async function signAndSendTx2( extrinsic: SubmittableExtrinsic<"promise", any>, address: string, signer: Signer From a8eae2b69202f5c14add8320718ffe8b2d17b770 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Wed, 6 Apr 2022 09:20:07 +1200 Subject: [PATCH 8/9] Add `Dismiss` button into Pool and Bridge sections --- components/BridgeProgress.tsx | 16 ++++++++++++++++ components/PoolProgress.tsx | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/components/BridgeProgress.tsx b/components/BridgeProgress.tsx index d6148666..381c85bd 100644 --- a/components/BridgeProgress.tsx +++ b/components/BridgeProgress.tsx @@ -17,6 +17,8 @@ const BridgeProgress: VFC = ( ) => { const { txStatus, setTxIdle } = useBridge(); const { txHashLink, ...txProps } = txStatus?.props ?? {}; + const dismissible = + txStatus?.status === "Success" || txStatus?.status === "Failure"; return ( = ( View Transaction )} + + {dismissible && ( + + Dismiss + + )} ); }; @@ -153,6 +165,10 @@ const styles = { margin-top: 1em; `, + dismissButton: css` + margin-top: 0.5em; + `, + errorCode: css` margin-top: 0.5em; `, diff --git a/components/PoolProgress.tsx b/components/PoolProgress.tsx index 141b35af..5c5a7656 100644 --- a/components/PoolProgress.tsx +++ b/components/PoolProgress.tsx @@ -17,6 +17,8 @@ const PoolProgress: VFC = ( ) => { const { txStatus, setTxIdle } = usePool(); const { txHashLink, ...txProps } = txStatus?.props ?? {}; + const dismissible = + txStatus?.status === "Success" || txStatus?.status === "Failure"; return ( = ( View Transaction )} + + {dismissible && ( + + Dismiss + + )} ); }; @@ -141,6 +153,10 @@ const styles = { margin-top: 1em; `, + dismissButton: css` + margin-top: 0.5em; + `, + errorCode: css` margin-top: 0.5em; `, From acb79de602ee43dd82f352b0e018bb927b75a4e0 Mon Sep 17 00:00:00 2001 From: Ken Vu Date: Wed, 6 Apr 2022 10:27:01 +1200 Subject: [PATCH 9/9] Append `?network=Nikau` for Nikau transaction --- .env.development | 2 +- utils/CENNZTransaction.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.env.development b/.env.development index a1ba4e6f..02ea4bb2 100644 --- a/.env.development +++ b/.env.development @@ -5,5 +5,5 @@ NEXT_PUBLIC_API_URL="wss://nikau.centrality.me/public/ws" NEXT_PUBLIC_ETH_CHAIN_ID=42 NEXT_PUBLIC_BRIDGE_RELAYER_URL="https://bridge-contracts.nikau.centrality.me" NEXT_PUBLIC_GA_ID="G-8YJT9T1YTT" -NEXT_PUBLIC_CENNZ_EXPLORER_URL="https://uncoverexplorer.com" +NEXT_PUBLIC_CENNZ_EXPLORER_URL="https://nikau.uncoverexplorer.com" NEXT_PUBLIC_ETH_EXPLORER_URL="https://kovan.etherscan.io" diff --git a/utils/CENNZTransaction.ts b/utils/CENNZTransaction.ts index a21fc725..38c5cb69 100644 --- a/utils/CENNZTransaction.ts +++ b/utils/CENNZTransaction.ts @@ -66,6 +66,16 @@ export default class CENNZTransaction extends Emittery { } getHashLink(): string { - return this.hash ? `${CENNZ_EXPLORER_URL}/extrinsic/${this.hash}` : null; + let isNikau: boolean; + const explorerUrl = CENNZ_EXPLORER_URL.replace("nikau.", (match) => { + isNikau = true; + return ""; + }); + const link = this.hash + ? `${explorerUrl}/extrinsic/${this.hash}?${ + isNikau ? "?network=Nikau" : "" + }` + : null; + return link; } }