From dd5533d6b67808d546f57e7544278f9a07385856 Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Fri, 16 Feb 2024 03:39:24 +0700 Subject: [PATCH 01/29] refactor: fre-599-create-reusable-components --- src/components/RouteDisplay.tsx | 127 ++++----------------------- src/components/RouteDisplay/Step.tsx | 22 +++++ src/components/common/Gap.tsx | 22 +++++ src/components/common/Spinner.tsx | 22 +++++ 4 files changed, 83 insertions(+), 110 deletions(-) create mode 100644 src/components/RouteDisplay/Step.tsx create mode 100644 src/components/common/Gap.tsx create mode 100644 src/components/common/Spinner.tsx diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index 4a1c7b2f..50c40969 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -1,15 +1,15 @@ -import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid"; import { BridgeType, RouteResponse } from "@skip-router/core"; -import { ComponentProps, Dispatch, Fragment, SetStateAction, SyntheticEvent, useMemo } from "react"; +import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useMemo } from "react"; import { formatUnits } from "viem"; import { useAssets } from "@/context/assets"; import { useBridgeByID } from "@/hooks/useBridges"; import { useChainByID } from "@/hooks/useChains"; import { useBroadcastedTxsStatus } from "@/solve"; -import { cn } from "@/utils/ui"; import { AdaptiveLink } from "./AdaptiveLink"; +import { Gap } from "./common/Gap"; +import { Step } from "./RouteDisplay/Step"; import { SimpleTooltip } from "./SimpleTooltip"; import { BroadcastedTx } from "./TransactionDialog/TransactionDialogContent"; @@ -134,47 +134,23 @@ function TransferStep({ action, actions, id, statusData }: TransferStepProps) { const renderTransferState = useMemo(() => { if (isFirstOpSwap) { if (transferStatus?.state === "TRANSFER_FAILURE") { - return ( -
- -
- ); + return ; } if (transferStatus?.state === "TRANSFER_SUCCESS") { - return ( -
- -
- ); + return ; } - return
; + return ; } switch (transferStatus?.state) { case "TRANSFER_SUCCESS": - return ( -
- -
- ); + return ; case "TRANSFER_RECEIVED": - return ( -
- -
- ); + return ; case "TRANSFER_FAILURE": - return ( -
- -
- ); + return ; case "TRANSFER_PENDING": - return ( -
- -
- ); + return ; default: return
; @@ -374,51 +350,26 @@ function SwapStep({ action, actions, id, statusData }: SwapStepProps) { const renderSwapState = useMemo(() => { if (isSwapFirstStep) { if (swapStatus?.state === "TRANSFER_PENDING") { - return ( -
- -
- ); + return ; } if (swapStatus?.state === "TRANSFER_SUCCESS") { - return ( -
- -
- ); + return ; } if (swapStatus?.state === "TRANSFER_FAILURE") { - return ( -
- -
- ); + return ; } return
; } switch (swapStatus?.state) { case "TRANSFER_RECEIVED": - return ( -
- -
- ); + return ; case "TRANSFER_SUCCESS": - return ( -
- -
- ); + return ; case "TRANSFER_FAILURE": - return ( -
- -
- ); - + return ; default: - return
; + return ; } }, [isSwapFirstStep, swapStatus?.state]); @@ -777,52 +728,8 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT ); } -function Spinner() { - return ( - - - - - ); -} - export default RouteDisplay; -const Gap = { - Parent({ className, ...props }: ComponentProps<"div">) { - return ( -
- ); - }, - Child({ className, ...props }: ComponentProps<"div">) { - return ( -
- ); - }, -}; - function makeExplorerLink(link: string) { return { link, diff --git a/src/components/RouteDisplay/Step.tsx b/src/components/RouteDisplay/Step.tsx new file mode 100644 index 00000000..0a1cf417 --- /dev/null +++ b/src/components/RouteDisplay/Step.tsx @@ -0,0 +1,22 @@ +import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid"; + +import { Spinner } from "../common/Spinner"; + +export const Step = { + SuccessState: () => ( +
+ +
+ ), + FailureState: () => ( +
+ +
+ ), + LoadingState: () => ( +
+ +
+ ), + DefaultState: () =>
, +}; diff --git a/src/components/common/Gap.tsx b/src/components/common/Gap.tsx new file mode 100644 index 00000000..e21db2ae --- /dev/null +++ b/src/components/common/Gap.tsx @@ -0,0 +1,22 @@ +import { ComponentProps } from "react"; + +import { cn } from "@/utils/ui"; + +export const Gap = { + Parent({ className, ...props }: ComponentProps<"div">) { + return ( +
+ ); + }, + Child({ className, ...props }: ComponentProps<"div">) { + return ( +
+ ); + }, +}; diff --git a/src/components/common/Spinner.tsx b/src/components/common/Spinner.tsx new file mode 100644 index 00000000..cc6f82e3 --- /dev/null +++ b/src/components/common/Spinner.tsx @@ -0,0 +1,22 @@ +export const Spinner = () => ( + + + + +); From 9552bb09657cc84d35a0ac3d43b5267aa1ac565a Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Fri, 16 Feb 2024 05:24:45 +0700 Subject: [PATCH 02/29] Update src/components/common/Spinner.tsx Co-authored-by: Griko Nibras --- src/components/common/Spinner.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/common/Spinner.tsx b/src/components/common/Spinner.tsx index cc6f82e3..d4a53a35 100644 --- a/src/components/common/Spinner.tsx +++ b/src/components/common/Spinner.tsx @@ -1,9 +1,14 @@ -export const Spinner = () => ( +import { ComponentProps } from "react"; + +import { cn } from "@/utils/ui"; + +export const Spinner = ({ className, ...props }: ComponentProps<"svg">) => ( Date: Fri, 16 Feb 2024 05:24:54 +0700 Subject: [PATCH 03/29] Update src/components/RouteDisplay/Step.tsx Co-authored-by: Griko Nibras --- src/components/RouteDisplay/Step.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RouteDisplay/Step.tsx b/src/components/RouteDisplay/Step.tsx index 0a1cf417..dddcd2ef 100644 --- a/src/components/RouteDisplay/Step.tsx +++ b/src/components/RouteDisplay/Step.tsx @@ -15,7 +15,7 @@ export const Step = { ), LoadingState: () => (
- +
), DefaultState: () =>
, From cb955fcdba568cd05fad745b8d7e3c6b1e8aa607 Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Fri, 16 Feb 2024 06:46:46 +0700 Subject: [PATCH 04/29] refactor: extract component --- src/components/Icons/ExpandArrow.tsx | 17 +++++++++++++++++ src/components/{common => Icons}/Spinner.tsx | 0 src/components/RouteDisplay.tsx | 18 ++++-------------- src/components/RouteDisplay/Step.tsx | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 src/components/Icons/ExpandArrow.tsx rename src/components/{common => Icons}/Spinner.tsx (100%) diff --git a/src/components/Icons/ExpandArrow.tsx b/src/components/Icons/ExpandArrow.tsx new file mode 100644 index 00000000..99fae931 --- /dev/null +++ b/src/components/Icons/ExpandArrow.tsx @@ -0,0 +1,17 @@ +import { ComponentProps } from "react"; + +export const ExpandArrow = ({ className, ...props }: ComponentProps<"svg">) => ( + + + +); diff --git a/src/components/common/Spinner.tsx b/src/components/Icons/Spinner.tsx similarity index 100% rename from src/components/common/Spinner.tsx rename to src/components/Icons/Spinner.tsx diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index 50c40969..a7204084 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -9,6 +9,7 @@ import { useBroadcastedTxsStatus } from "@/solve"; import { AdaptiveLink } from "./AdaptiveLink"; import { Gap } from "./common/Gap"; +import { ExpandArrow } from "./Icons/ExpandArrow"; import { Step } from "./RouteDisplay/Step"; import { SimpleTooltip } from "./SimpleTooltip"; import { BroadcastedTx } from "./TransactionDialog/TransactionDialogContent"; @@ -153,7 +154,7 @@ function TransferStep({ action, actions, id, statusData }: TransferStepProps) { return ; default: - return
; + return ; } }, [isFirstOpSwap, transferStatus?.state]); @@ -359,7 +360,7 @@ function SwapStep({ action, actions, id, statusData }: SwapStepProps) { return ; } - return
; + return ; } switch (swapStatus?.state) { case "TRANSFER_RECEIVED": @@ -702,18 +703,7 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT className="rounded-full border-2 border-neutral-200 bg-white p-1 text-neutral-400 transition-transform hover:scale-110" onClick={() => setIsRouteExpanded(true)} > - - - +
)} diff --git a/src/components/RouteDisplay/Step.tsx b/src/components/RouteDisplay/Step.tsx index dddcd2ef..c159c4b9 100644 --- a/src/components/RouteDisplay/Step.tsx +++ b/src/components/RouteDisplay/Step.tsx @@ -1,6 +1,6 @@ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid"; -import { Spinner } from "../common/Spinner"; +import { Spinner } from "../Icons/Spinner"; export const Step = { SuccessState: () => ( From 70df515f926da1ac62af2fde63e8d40f3cdea6b7 Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Fri, 16 Feb 2024 03:59:02 +0700 Subject: [PATCH 05/29] refactor: fre-600-extract-components-to-a-separated-file --- src/components/RouteDisplay.tsx | 732 ------------------ src/components/RouteDisplay/RouteEnd.tsx | 30 + src/components/RouteDisplay/SwapStep.tsx | 216 ++++++ src/components/RouteDisplay/TransferStep.tsx | 230 ++++++ src/components/RouteDisplay/index.tsx | 224 ++++++ .../TransactionDialogContent.tsx | 2 +- src/constants/swap-venues.ts | 43 + src/utils/image.ts | 5 + src/utils/link.ts | 6 + 9 files changed, 755 insertions(+), 733 deletions(-) delete mode 100644 src/components/RouteDisplay.tsx create mode 100644 src/components/RouteDisplay/RouteEnd.tsx create mode 100644 src/components/RouteDisplay/SwapStep.tsx create mode 100644 src/components/RouteDisplay/TransferStep.tsx create mode 100644 src/components/RouteDisplay/index.tsx create mode 100644 src/constants/swap-venues.ts create mode 100644 src/utils/image.ts create mode 100644 src/utils/link.ts diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx deleted file mode 100644 index a7204084..00000000 --- a/src/components/RouteDisplay.tsx +++ /dev/null @@ -1,732 +0,0 @@ -import { BridgeType, RouteResponse } from "@skip-router/core"; -import { Dispatch, Fragment, SetStateAction, SyntheticEvent, useMemo } from "react"; -import { formatUnits } from "viem"; - -import { useAssets } from "@/context/assets"; -import { useBridgeByID } from "@/hooks/useBridges"; -import { useChainByID } from "@/hooks/useChains"; -import { useBroadcastedTxsStatus } from "@/solve"; - -import { AdaptiveLink } from "./AdaptiveLink"; -import { Gap } from "./common/Gap"; -import { ExpandArrow } from "./Icons/ExpandArrow"; -import { Step } from "./RouteDisplay/Step"; -import { SimpleTooltip } from "./SimpleTooltip"; -import { BroadcastedTx } from "./TransactionDialog/TransactionDialogContent"; - -export interface SwapVenueConfig { - name: string; - imageURL: string; -} - -export const SWAP_VENUES: Record = { - "neutron-astroport": { - name: "Neutron Astroport", - imageURL: "https://avatars.githubusercontent.com/u/87135340", - }, - "terra-astroport": { - name: "Terra Astroport", - imageURL: "https://avatars.githubusercontent.com/u/87135340", - }, - "injective-astroport": { - name: "Injective Astroport", - imageURL: "https://avatars.githubusercontent.com/u/87135340", - }, - "sei-astroport": { - name: "Sei Astroport", - imageURL: "https://avatars.githubusercontent.com/u/87135340", - }, - "osmosis-poolmanager": { - name: "Osmosis", - imageURL: "https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/osmosis/dappImg/app.png", - }, - "neutron-lido-satellite": { - name: "Neutron Lido Satellite", - imageURL: "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/wsteth.svg", - }, - "migaloo-white-whale": { - name: "Migaloo White Whale", - imageURL: "https://whitewhale.money/logo.svg", - }, - "chihuahua-white-whale": { - name: "Chihuahua White Whale", - imageURL: "https://whitewhale.money/logo.svg", - }, - "terra-white-whale": { - name: "Terra White Whale", - imageURL: "https://whitewhale.money/logo.svg", - }, -}; - -interface TransferAction { - type: "TRANSFER"; - asset: string; - sourceChain: string; - destinationChain: string; - id: string; - bridgeID: BridgeType; -} - -interface SwapAction { - type: "SWAP"; - sourceAsset: string; - destinationAsset: string; - chain: string; - venue: string; - id: string; -} - -type Action = TransferAction | SwapAction; - -interface RouteEndProps { - amount: string; - symbol: string; - chain: string; - logo: string; -} - -function RouteEnd({ amount, symbol, logo, chain }: RouteEndProps) { - return ( -
-
- {chain} -
-
- -
- {parseFloat(amount).toLocaleString("en-US", { maximumFractionDigits: 8 })} {symbol} -
-
-
On {chain}
-
-
- ); -} - -interface TransferStepProps { - actions: Action[]; - action: TransferAction; - id: string; - statusData?: ReturnType["data"]; -} - -function TransferStep({ action, actions, id, statusData }: TransferStepProps) { - const { data: bridge } = useBridgeByID(action.bridgeID); - const { data: sourceChain } = useChainByID(action.sourceChain); - const { data: destinationChain } = useChainByID(action.destinationChain); - - // format: operationType-- - const operationTypeCount = Number(id.split("-")[1]); - const operationIndex = Number(id.split("-")[2]); - const isFirstOpSwap = actions[0]?.type === "SWAP"; - const transferStatus = statusData?.transferSequence[operationTypeCount]; - const isNextOpSwap = - actions - // We can assume that the swap operation by the previous transfer - .find((x) => Number(x.id.split("-")[2]) === operationIndex + 1) - ?.id.split("-")[0] === "swap"; - const isPrevOpTransfer = actions[operationIndex - 1]?.type === "TRANSFER"; - - // We can assume that the transfer is successful when the state is TRANSFER_SUCCESS or TRANSFER_RECEIVED - const renderTransferState = useMemo(() => { - if (isFirstOpSwap) { - if (transferStatus?.state === "TRANSFER_FAILURE") { - return ; - } - if (transferStatus?.state === "TRANSFER_SUCCESS") { - return ; - } - - return ; - } - switch (transferStatus?.state) { - case "TRANSFER_SUCCESS": - return ; - case "TRANSFER_RECEIVED": - return ; - case "TRANSFER_FAILURE": - return ; - case "TRANSFER_PENDING": - return ; - - default: - return ; - } - }, [isFirstOpSwap, transferStatus?.state]); - - const explorerLink = useMemo(() => { - const packetTx = (() => { - if (operationIndex === 0) return transferStatus?.txs.sendTx; - if (isNextOpSwap) return transferStatus?.txs.sendTx; - if (isPrevOpTransfer) return transferStatus?.txs.sendTx; - return transferStatus?.txs.receiveTx; - })(); - if (!packetTx?.explorerLink) { - return null; - } - return makeExplorerLink(packetTx.explorerLink); - }, [isNextOpSwap, isPrevOpTransfer, operationIndex, transferStatus?.txs.receiveTx, transferStatus?.txs.sendTx]); - - const { getAsset } = useAssets(); - - const asset = (() => { - const currentAsset = getAsset(action.asset, action.sourceChain); - if (currentAsset) return currentAsset; - const prevAction = actions[operationIndex - 1]; - if (!prevAction || prevAction.type !== "TRANSFER") return; - const prevAsset = getAsset(prevAction.asset, prevAction.sourceChain); - return prevAsset; - })(); - - if (!sourceChain || !destinationChain) { - // this should be unreachable - return null; - } - - if (!asset) { - return ( -
-
{renderTransferState}
-
- - Transfer - from - - {sourceChain.prettyName} - {sourceChain.prettyName} - - - - to - - {destinationChain.prettyName} - {destinationChain.prettyName} - - {bridge && ( - <> - with - - {bridge.name.toLowerCase() !== "ibc" && ( - {bridge.name} - )} - - {bridge.name} - - - )} - - {explorerLink && ( - - {explorerLink.shorthand} - - )} -
-
- ); - } - - return ( -
-
{renderTransferState}
-
- - Transfer - - {asset.name} - {asset.recommendedSymbol} - - from - - {sourceChain.prettyName} - {sourceChain.prettyName} - - - - to - - {destinationChain.prettyName} - {destinationChain.prettyName} - - {bridge && ( - <> - with - - {bridge.name.toLowerCase() !== "ibc" && ( - {bridge.name} - )} - - {bridge.name} - - - )} - - {explorerLink && ( - - {explorerLink.shorthand} - - )} -
-
- ); -} - -interface SwapStepProps { - action: SwapAction; - actions: Action[]; - id: string; - statusData?: ReturnType["data"]; -} - -function SwapStep({ action, actions, id, statusData }: SwapStepProps) { - const { getAsset } = useAssets(); - - const assetIn = useMemo(() => { - return getAsset(action.sourceAsset, action.chain); - }, [action.chain, action.sourceAsset, getAsset]); - - const assetOut = useMemo(() => { - return getAsset(action.destinationAsset, action.chain); - }, [action.chain, action.destinationAsset, getAsset]); - - const venue = SWAP_VENUES[action.venue]; - - // format: operationType-- - const operationIndex = Number(id.split("-")[2]); - const operationTypeCount = Number(id.split("-")[1]); - const isSwapFirstStep = operationIndex === 0 && operationTypeCount === 0; - - const sequenceIndex = Number( - actions - // We can assume that the swap operation by the previous transfer - .find((x) => Number(x.id.split("-")[2]) === operationIndex - 1) - ?.id.split("-")[1], - ); - const swapStatus = statusData?.transferSequence[isSwapFirstStep ? 0 : sequenceIndex]; - - // as for swap operations, we can assume that the swap is successful if the previous transfer state is TRANSFER_SUCCESS - const renderSwapState = useMemo(() => { - if (isSwapFirstStep) { - if (swapStatus?.state === "TRANSFER_PENDING") { - return ; - } - if (swapStatus?.state === "TRANSFER_SUCCESS") { - return ; - } - if (swapStatus?.state === "TRANSFER_FAILURE") { - return ; - } - - return ; - } - switch (swapStatus?.state) { - case "TRANSFER_RECEIVED": - return ; - case "TRANSFER_SUCCESS": - return ; - case "TRANSFER_FAILURE": - return ; - default: - return ; - } - }, [isSwapFirstStep, swapStatus?.state]); - - const explorerLink = useMemo(() => { - const tx = isSwapFirstStep ? swapStatus?.txs.sendTx : swapStatus?.txs.receiveTx; - if (!tx) return; - if (swapStatus?.state !== "TRANSFER_SUCCESS") return; - return makeExplorerLink(tx.explorerLink); - }, [isSwapFirstStep, swapStatus?.state, swapStatus?.txs.receiveTx, swapStatus?.txs.sendTx]); - - if (!assetIn && assetOut) { - return ( -
-
{renderSwapState}
-
- - Swap to - - {assetOut.name} - {assetOut.recommendedSymbol} - - on - - {venue.name} - {venue.name} - - - {explorerLink && ( - - {explorerLink.shorthand} - - )} -
-
- ); - } - - if (assetIn && !assetOut) { - return ( -
-
{renderSwapState}
-
- - Swap - - {assetIn.name} - {assetIn.recommendedSymbol} - - on - - {venue.name} - {venue.name} - - - {explorerLink && ( - - {explorerLink.shorthand} - - )} -
-
- ); - } - - if (!assetIn || !assetOut) { - return null; - } - - return ( -
-
{renderSwapState}
-
- - Swap - - {assetIn.name} - {assetIn.recommendedSymbol} - - for - - {assetOut.name} - {assetOut.recommendedSymbol} - - - on - {venue.name} - {venue.name} - - - {explorerLink && ( - - {explorerLink.shorthand} - - )} -
-
- ); -} - -interface RouteDisplayProps { - route: RouteResponse; - isRouteExpanded: boolean; - setIsRouteExpanded: Dispatch>; - broadcastedTxs?: BroadcastedTx[]; -} - -function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedTxs }: RouteDisplayProps) { - const { getAsset } = useAssets(); - - const sourceAsset = getAsset(route.sourceAssetDenom, route.sourceAssetChainID); - - const destinationAsset = getAsset(route.destAssetDenom, route.destAssetChainID); - - const { data: sourceChain } = useChainByID(route.sourceAssetChainID); - const { data: destinationChain } = useChainByID(route.destAssetChainID); - - const amountIn = useMemo(() => { - try { - return formatUnits(BigInt(route.amountIn), sourceAsset?.decimals ?? 6); - } catch { - return "0"; - } - }, [route.amountIn, sourceAsset?.decimals]); - - const amountOut = useMemo(() => { - try { - return formatUnits(BigInt(route.amountOut), destinationAsset?.decimals ?? 6); - } catch { - return "0"; - } - }, [route.amountOut, destinationAsset?.decimals]); - - const actions = useMemo(() => { - const _actions: Action[] = []; - - let swapCount = 0; - let transferCount = 0; - let asset = route.sourceAssetDenom; - - route.operations.forEach((operation, i) => { - if ("swap" in operation) { - if ("swapIn" in operation.swap) { - _actions.push({ - type: "SWAP", - sourceAsset: operation.swap.swapIn.swapOperations[0].denomIn, - destinationAsset: - operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut, - chain: operation.swap.swapIn.swapVenue.chainID, - venue: operation.swap.swapIn.swapVenue.name, - id: `swap-${swapCount}-${i}`, - }); - - asset = operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut; - } - - if ("swapOut" in operation.swap) { - _actions.push({ - type: "SWAP", - sourceAsset: operation.swap.swapOut.swapOperations[0].denomIn, - destinationAsset: - operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut, - chain: operation.swap.swapOut.swapVenue.chainID, - venue: operation.swap.swapOut.swapVenue.name, - id: `swap-${swapCount}-${i}`, - }); - - asset = operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut; - } - swapCount++; - return; - } - - if ("axelarTransfer" in operation) { - _actions.push({ - type: "TRANSFER", - asset, - sourceChain: operation.axelarTransfer.fromChainID, - destinationChain: operation.axelarTransfer.toChainID, - id: `transfer-${transferCount}-${i}`, - bridgeID: operation.axelarTransfer.bridgeID, - }); - - asset = operation.axelarTransfer.asset; - transferCount++; - return; - } - - if ("cctpTransfer" in operation) { - _actions.push({ - type: "TRANSFER", - asset, - sourceChain: operation.cctpTransfer.fromChainID, - destinationChain: operation.cctpTransfer.toChainID, - id: `transfer-${transferCount}-${i}`, - bridgeID: operation.cctpTransfer.bridgeID, - }); - - asset = operation.cctpTransfer.burnToken; - transferCount++; - return; - } - - const sourceChain = operation.transfer.chainID; - - let destinationChain = ""; - if (i === route.operations.length - 1) { - destinationChain = route.destAssetChainID; - } else { - const nextOperation = route.operations[i + 1]; - if ("swap" in nextOperation) { - if ("swapIn" in nextOperation.swap) { - destinationChain = nextOperation.swap.swapIn.swapVenue.chainID; - } - - if ("swapOut" in nextOperation.swap) { - destinationChain = nextOperation.swap.swapOut.swapVenue.chainID; - } - } else if ("axelarTransfer" in nextOperation) { - destinationChain = nextOperation.axelarTransfer.fromChainID; - } else if ("cctpTransfer" in nextOperation) { - destinationChain = nextOperation.cctpTransfer.fromChainID; - } else { - destinationChain = nextOperation.transfer.chainID; - } - } - - _actions.push({ - type: "TRANSFER", - asset, - sourceChain, - destinationChain, - id: `transfer-${transferCount}-${i}`, - bridgeID: operation.transfer.bridgeID, - }); - - asset = operation.transfer.destDenom; - transferCount++; - }); - - return _actions; - }, [route]); - - const { data: statusData } = useBroadcastedTxsStatus({ txsRequired: route.txsRequired, txs: broadcastedTxs }); - - return ( -
-
-
-
-
-
- - {isRouteExpanded && ( - - )} -
- {isRouteExpanded && - actions.map((action, i) => ( - - {action.type === "SWAP" && ( - - )} - {action.type === "TRANSFER" && ( - - )} - - ))} - {!isRouteExpanded && ( -
- -
- )} - -
-
- ); -} - -export default RouteDisplay; - -function makeExplorerLink(link: string) { - return { - link, - shorthand: `${link.split("/").at(-1)?.slice(0, 6)}…${link.split("/").at(-1)?.slice(-6)}`, - }; -} - -function onImageError(event: SyntheticEvent) { - event.currentTarget.src = "https://api.dicebear.com/6.x/shapes/svg"; -} diff --git a/src/components/RouteDisplay/RouteEnd.tsx b/src/components/RouteDisplay/RouteEnd.tsx new file mode 100644 index 00000000..5c71c856 --- /dev/null +++ b/src/components/RouteDisplay/RouteEnd.tsx @@ -0,0 +1,30 @@ +import { SimpleTooltip } from "../SimpleTooltip"; + +export interface RouteEndProps { + amount: string; + symbol: string; + chain: string; + logo: string; +} + +export const RouteEnd = ({ amount, symbol, logo, chain }: RouteEndProps) => { + return ( +
+
+ {chain} +
+
+ +
+ {parseFloat(amount).toLocaleString("en-US", { maximumFractionDigits: 8 })} {symbol} +
+
+
On {chain}
+
+
+ ); +}; diff --git a/src/components/RouteDisplay/SwapStep.tsx b/src/components/RouteDisplay/SwapStep.tsx new file mode 100644 index 00000000..2319b436 --- /dev/null +++ b/src/components/RouteDisplay/SwapStep.tsx @@ -0,0 +1,216 @@ +import { useMemo } from "react"; + +import { SWAP_VENUES } from "@/constants/swap-venues"; +import { useAssets } from "@/context/assets"; +import { useBroadcastedTxsStatus } from "@/solve"; +import { onImageError } from "@/utils/image"; +import { makeExplorerLink } from "@/utils/link"; + +import { AdaptiveLink } from "../AdaptiveLink"; +import { Gap } from "../common/Gap"; +import { Action } from "."; +import { Step } from "./Step"; + +export interface SwapAction { + type: "SWAP"; + sourceAsset: string; + destinationAsset: string; + chain: string; + venue: string; + id: string; +} + +export interface SwapStepProps { + action: SwapAction; + actions: Action[]; + id: string; + statusData?: ReturnType["data"]; +} + +export const SwapStep = ({ action, actions, id, statusData }: SwapStepProps) => { + const { getAsset } = useAssets(); + + const assetIn = useMemo(() => { + return getAsset(action.sourceAsset, action.chain); + }, [action.chain, action.sourceAsset, getAsset]); + + const assetOut = useMemo(() => { + return getAsset(action.destinationAsset, action.chain); + }, [action.chain, action.destinationAsset, getAsset]); + + const venue = SWAP_VENUES[action.venue]; + + // format: operationType-- + const operationIndex = Number(id.split("-")[2]); + const operationTypeCount = Number(id.split("-")[1]); + const isSwapFirstStep = operationIndex === 0 && operationTypeCount === 0; + + const sequenceIndex = Number( + actions + // We can assume that the swap operation by the previous transfer + .find((x) => Number(x.id.split("-")[2]) === operationIndex - 1) + ?.id.split("-")[1], + ); + const swapStatus = statusData?.transferSequence[isSwapFirstStep ? 0 : sequenceIndex]; + + // as for swap operations, we can assume that the swap is successful if the previous transfer state is TRANSFER_SUCCESS + const renderSwapState = useMemo(() => { + if (isSwapFirstStep) { + if (swapStatus?.state === "TRANSFER_PENDING") { + return ; + } + if (swapStatus?.state === "TRANSFER_SUCCESS") { + return ; + } + if (swapStatus?.state === "TRANSFER_FAILURE") { + return ; + } + + return
; + } + switch (swapStatus?.state) { + case "TRANSFER_RECEIVED": + return ; + case "TRANSFER_SUCCESS": + return ; + case "TRANSFER_FAILURE": + return ; + default: + return ; + } + }, [isSwapFirstStep, swapStatus?.state]); + + const explorerLink = useMemo(() => { + const tx = isSwapFirstStep ? swapStatus?.txs.sendTx : swapStatus?.txs.receiveTx; + if (!tx) return; + if (swapStatus?.state !== "TRANSFER_SUCCESS") return; + return makeExplorerLink(tx.explorerLink); + }, [isSwapFirstStep, swapStatus?.state, swapStatus?.txs.receiveTx, swapStatus?.txs.sendTx]); + + if (!assetIn && assetOut) { + return ( +
+
{renderSwapState}
+
+ + Swap to + + {assetOut.name} + {assetOut.recommendedSymbol} + + on + + {venue.name} + {venue.name} + + + {explorerLink && ( + + {explorerLink.shorthand} + + )} +
+
+ ); + } + + if (assetIn && !assetOut) { + return ( +
+
{renderSwapState}
+
+ + Swap + + {assetIn.name} + {assetIn.recommendedSymbol} + + on + + {venue.name} + {venue.name} + + + {explorerLink && ( + + {explorerLink.shorthand} + + )} +
+
+ ); + } + + if (!assetIn || !assetOut) { + return null; + } + + return ( +
+
{renderSwapState}
+
+ + Swap + + {assetIn.name} + {assetIn.recommendedSymbol} + + for + + {assetOut.name} + {assetOut.recommendedSymbol} + + + on + {venue.name} + {venue.name} + + + {explorerLink && ( + + {explorerLink.shorthand} + + )} +
+
+ ); +}; diff --git a/src/components/RouteDisplay/TransferStep.tsx b/src/components/RouteDisplay/TransferStep.tsx new file mode 100644 index 00000000..001a8364 --- /dev/null +++ b/src/components/RouteDisplay/TransferStep.tsx @@ -0,0 +1,230 @@ +import { BridgeType } from "@skip-router/core"; +import { useMemo } from "react"; + +import { useAssets } from "@/context/assets"; +import { useBridgeByID } from "@/hooks/useBridges"; +import { useChainByID } from "@/hooks/useChains"; +import { useBroadcastedTxsStatus } from "@/solve"; +import { onImageError } from "@/utils/image"; +import { makeExplorerLink } from "@/utils/link"; + +import { AdaptiveLink } from "../AdaptiveLink"; +import { Gap } from "../common/Gap"; +import { Action } from "."; +import { Step } from "./Step"; + +export interface TransferAction { + type: "TRANSFER"; + asset: string; + sourceChain: string; + destinationChain: string; + id: string; + bridgeID: BridgeType; +} + +interface TransferStepProps { + actions: Action[]; + action: TransferAction; + id: string; + statusData?: ReturnType["data"]; +} + +export const TransferStep = ({ action, actions, id, statusData }: TransferStepProps) => { + const { data: bridge } = useBridgeByID(action.bridgeID); + const { data: sourceChain } = useChainByID(action.sourceChain); + const { data: destinationChain } = useChainByID(action.destinationChain); + + // format: operationType-- + const operationTypeCount = Number(id.split("-")[1]); + const operationIndex = Number(id.split("-")[2]); + const isFirstOpSwap = actions[0]?.type === "SWAP"; + const transferStatus = statusData?.transferSequence[operationTypeCount]; + const isNextOpSwap = + actions + // We can assume that the swap operation by the previous transfer + .find((x) => Number(x.id.split("-")[2]) === operationIndex + 1) + ?.id.split("-")[0] === "swap"; + const isPrevOpTransfer = actions[operationIndex - 1]?.type === "TRANSFER"; + + // We can assume that the transfer is successful when the state is TRANSFER_SUCCESS or TRANSFER_RECEIVED + const renderTransferState = useMemo(() => { + if (isFirstOpSwap) { + if (transferStatus?.state === "TRANSFER_FAILURE") { + return ; + } + if (transferStatus?.state === "TRANSFER_SUCCESS") { + return ; + } + + return ; + } + switch (transferStatus?.state) { + case "TRANSFER_SUCCESS": + return ; + case "TRANSFER_RECEIVED": + return ; + case "TRANSFER_FAILURE": + return ; + case "TRANSFER_PENDING": + return ; + + default: + return
; + } + }, [isFirstOpSwap, transferStatus?.state]); + + const explorerLink = useMemo(() => { + const packetTx = (() => { + if (operationIndex === 0) return transferStatus?.txs.sendTx; + if (isNextOpSwap) return transferStatus?.txs.sendTx; + if (isPrevOpTransfer) return transferStatus?.txs.sendTx; + return transferStatus?.txs.receiveTx; + })(); + if (!packetTx?.explorerLink) { + return null; + } + return makeExplorerLink(packetTx.explorerLink); + }, [isNextOpSwap, isPrevOpTransfer, operationIndex, transferStatus?.txs.receiveTx, transferStatus?.txs.sendTx]); + + const { getAsset } = useAssets(); + + const asset = (() => { + const currentAsset = getAsset(action.asset, action.sourceChain); + if (currentAsset) return currentAsset; + const prevAction = actions[operationIndex - 1]; + if (!prevAction || prevAction.type !== "TRANSFER") return; + const prevAsset = getAsset(prevAction.asset, prevAction.sourceChain); + return prevAsset; + })(); + + if (!sourceChain || !destinationChain) { + // this should be unreachable + return null; + } + + if (!asset) { + return ( +
+
{renderTransferState}
+
+ + Transfer + from + + {sourceChain.prettyName} + {sourceChain.prettyName} + + + + to + + {destinationChain.prettyName} + {destinationChain.prettyName} + + {bridge && ( + <> + with + + {bridge.name.toLowerCase() !== "ibc" && ( + {bridge.name} + )} + + {bridge.name} + + + )} + + {explorerLink && ( + + {explorerLink.shorthand} + + )} +
+
+ ); + } + + return ( +
+
{renderTransferState}
+
+ + Transfer + + {asset.name} + {asset.recommendedSymbol} + + from + + {sourceChain.prettyName} + {sourceChain.prettyName} + + + + to + + {destinationChain.prettyName} + {destinationChain.prettyName} + + {bridge && ( + <> + with + + {bridge.name.toLowerCase() !== "ibc" && ( + {bridge.name} + )} + + {bridge.name} + + + )} + + {explorerLink && ( + + {explorerLink.shorthand} + + )} +
+
+ ); +}; diff --git a/src/components/RouteDisplay/index.tsx b/src/components/RouteDisplay/index.tsx new file mode 100644 index 00000000..b331b508 --- /dev/null +++ b/src/components/RouteDisplay/index.tsx @@ -0,0 +1,224 @@ +import { RouteResponse } from "@skip-router/core"; +import { Dispatch, Fragment, SetStateAction, useMemo } from "react"; +import { formatUnits } from "viem"; + +import { useAssets } from "@/context/assets"; +import { useChainByID } from "@/hooks/useChains"; +import { useBroadcastedTxsStatus } from "@/solve"; + +import { ExpandArrow } from "../Icons/ExpandArrow"; +import { BroadcastedTx } from "../TransactionDialog/TransactionDialogContent"; +import { RouteEnd } from "./RouteEnd"; +import { SwapAction, SwapStep } from "./SwapStep"; +import { TransferAction, TransferStep } from "./TransferStep"; + +export type Action = TransferAction | SwapAction; + +interface RouteDisplayProps { + route: RouteResponse; + isRouteExpanded: boolean; + setIsRouteExpanded: Dispatch>; + broadcastedTxs?: BroadcastedTx[]; +} + +export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broadcastedTxs }: RouteDisplayProps) => { + const { getAsset } = useAssets(); + + const sourceAsset = getAsset(route.sourceAssetDenom, route.sourceAssetChainID); + + const destinationAsset = getAsset(route.destAssetDenom, route.destAssetChainID); + + const { data: sourceChain } = useChainByID(route.sourceAssetChainID); + const { data: destinationChain } = useChainByID(route.destAssetChainID); + + const amountIn = useMemo(() => { + try { + return formatUnits(BigInt(route.amountIn), sourceAsset?.decimals ?? 6); + } catch { + return "0"; + } + }, [route.amountIn, sourceAsset?.decimals]); + + const amountOut = useMemo(() => { + try { + return formatUnits(BigInt(route.amountOut), destinationAsset?.decimals ?? 6); + } catch { + return "0"; + } + }, [route.amountOut, destinationAsset?.decimals]); + + const actions = useMemo(() => { + const _actions: Action[] = []; + + let swapCount = 0; + let transferCount = 0; + let asset = route.sourceAssetDenom; + + route.operations.forEach((operation, i) => { + if ("swap" in operation) { + if ("swapIn" in operation.swap) { + _actions.push({ + type: "SWAP", + sourceAsset: operation.swap.swapIn.swapOperations[0].denomIn, + destinationAsset: + operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut, + chain: operation.swap.swapIn.swapVenue.chainID, + venue: operation.swap.swapIn.swapVenue.name, + id: `swap-${swapCount}-${i}`, + }); + + asset = operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut; + } + + if ("swapOut" in operation.swap) { + _actions.push({ + type: "SWAP", + sourceAsset: operation.swap.swapOut.swapOperations[0].denomIn, + destinationAsset: + operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut, + chain: operation.swap.swapOut.swapVenue.chainID, + venue: operation.swap.swapOut.swapVenue.name, + id: `swap-${swapCount}-${i}`, + }); + + asset = operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut; + } + swapCount++; + return; + } + + if ("axelarTransfer" in operation) { + _actions.push({ + type: "TRANSFER", + asset, + sourceChain: operation.axelarTransfer.fromChainID, + destinationChain: operation.axelarTransfer.toChainID, + id: `transfer-${transferCount}-${i}`, + bridgeID: operation.axelarTransfer.bridgeID, + }); + + asset = operation.axelarTransfer.asset; + transferCount++; + return; + } + + if ("cctpTransfer" in operation) { + _actions.push({ + type: "TRANSFER", + asset, + sourceChain: operation.cctpTransfer.fromChainID, + destinationChain: operation.cctpTransfer.toChainID, + id: `transfer-${transferCount}-${i}`, + bridgeID: operation.cctpTransfer.bridgeID, + }); + + asset = operation.cctpTransfer.burnToken; + transferCount++; + return; + } + + const sourceChain = operation.transfer.chainID; + + let destinationChain = ""; + if (i === route.operations.length - 1) { + destinationChain = route.destAssetChainID; + } else { + const nextOperation = route.operations[i + 1]; + if ("swap" in nextOperation) { + if ("swapIn" in nextOperation.swap) { + destinationChain = nextOperation.swap.swapIn.swapVenue.chainID; + } + + if ("swapOut" in nextOperation.swap) { + destinationChain = nextOperation.swap.swapOut.swapVenue.chainID; + } + } else if ("axelarTransfer" in nextOperation) { + destinationChain = nextOperation.axelarTransfer.fromChainID; + } else if ("cctpTransfer" in nextOperation) { + destinationChain = nextOperation.cctpTransfer.fromChainID; + } else { + destinationChain = nextOperation.transfer.chainID; + } + } + + _actions.push({ + type: "TRANSFER", + asset, + sourceChain, + destinationChain, + id: `transfer-${transferCount}-${i}`, + bridgeID: operation.transfer.bridgeID, + }); + + asset = operation.transfer.destDenom; + transferCount++; + }); + + return _actions; + }, [route]); + + const { data: statusData } = useBroadcastedTxsStatus({ txsRequired: route.txsRequired, txs: broadcastedTxs }); + + return ( +
+
+
+
+
+
+ + {isRouteExpanded && ( + + )} +
+ {isRouteExpanded && + actions.map((action, i) => ( + + {action.type === "SWAP" && ( + + )} + {action.type === "TRANSFER" && ( + + )} + + ))} + {!isRouteExpanded && ( +
+ +
+ )} + +
+
+ ); +}; diff --git a/src/components/TransactionDialog/TransactionDialogContent.tsx b/src/components/TransactionDialog/TransactionDialogContent.tsx index 88fdb265..08f65242 100644 --- a/src/components/TransactionDialog/TransactionDialogContent.tsx +++ b/src/components/TransactionDialog/TransactionDialogContent.tsx @@ -17,7 +17,7 @@ import { isCCTPLedgerBrokenInOperation, isEthermintLedgerInOperation } from "@/u import { randomId } from "@/utils/random"; import { cn } from "@/utils/ui"; -import RouteDisplay from "../RouteDisplay"; +import { RouteDisplay } from "../RouteDisplay"; import { SpinnerIcon } from "../SpinnerIcon"; import TransactionSuccessView from "../TransactionSuccessView"; import * as AlertCollapse from "./AlertCollapse"; diff --git a/src/constants/swap-venues.ts b/src/constants/swap-venues.ts new file mode 100644 index 00000000..882d5be6 --- /dev/null +++ b/src/constants/swap-venues.ts @@ -0,0 +1,43 @@ +export interface SwapVenueConfig { + name: string; + imageURL: string; +} + +export const SWAP_VENUES: Record = { + "neutron-astroport": { + name: "Neutron Astroport", + imageURL: "https://avatars.githubusercontent.com/u/87135340", + }, + "terra-astroport": { + name: "Terra Astroport", + imageURL: "https://avatars.githubusercontent.com/u/87135340", + }, + "injective-astroport": { + name: "Injective Astroport", + imageURL: "https://avatars.githubusercontent.com/u/87135340", + }, + "sei-astroport": { + name: "Sei Astroport", + imageURL: "https://avatars.githubusercontent.com/u/87135340", + }, + "osmosis-poolmanager": { + name: "Osmosis", + imageURL: "https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/osmosis/dappImg/app.png", + }, + "neutron-lido-satellite": { + name: "Neutron Lido Satellite", + imageURL: "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/ethereum/images/wsteth.svg", + }, + "migaloo-white-whale": { + name: "Migaloo White Whale", + imageURL: "https://whitewhale.money/logo.svg", + }, + "chihuahua-white-whale": { + name: "Chihuahua White Whale", + imageURL: "https://whitewhale.money/logo.svg", + }, + "terra-white-whale": { + name: "Terra White Whale", + imageURL: "https://whitewhale.money/logo.svg", + }, +}; diff --git a/src/utils/image.ts b/src/utils/image.ts new file mode 100644 index 00000000..966a1d52 --- /dev/null +++ b/src/utils/image.ts @@ -0,0 +1,5 @@ +import { SyntheticEvent } from "react"; + +export const onImageError = (event: SyntheticEvent) => { + event.currentTarget.src = "https://api.dicebear.com/6.x/shapes/svg"; +}; diff --git a/src/utils/link.ts b/src/utils/link.ts new file mode 100644 index 00000000..72983644 --- /dev/null +++ b/src/utils/link.ts @@ -0,0 +1,6 @@ +export const makeExplorerLink = (link: string) => { + return { + link, + shorthand: `${link.split("/").at(-1)?.slice(0, 6)}…${link.split("/").at(-1)?.slice(-6)}`, + }; +}; From 80b9ad86d24a34327e063a8da4a2858d2919254f Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Fri, 16 Feb 2024 16:44:35 +0700 Subject: [PATCH 06/29] fix: add expand route size --- src/components/RouteDisplay/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RouteDisplay/index.tsx b/src/components/RouteDisplay/index.tsx index b331b508..c1b5ed41 100644 --- a/src/components/RouteDisplay/index.tsx +++ b/src/components/RouteDisplay/index.tsx @@ -208,7 +208,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad className="rounded-full border-2 border-neutral-200 bg-white p-1 text-neutral-400 transition-transform hover:scale-110" onClick={() => setIsRouteExpanded(true)} > - +
)} From 392fa6af7873c7089c9f3f96715ed9605b7bcf1b Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Sat, 17 Feb 2024 04:21:24 +0700 Subject: [PATCH 07/29] refactor: extract and unify lifecycle tracking logic --- src/components/RouteDisplay/SwapStep.tsx | 41 ++++------ src/components/RouteDisplay/TransferStep.tsx | 45 +++-------- src/components/RouteDisplay/index.tsx | 12 ++- .../RouteDisplay/operation-state.ts | 79 +++++++++++++++++++ 4 files changed, 109 insertions(+), 68 deletions(-) create mode 100644 src/components/RouteDisplay/operation-state.ts diff --git a/src/components/RouteDisplay/SwapStep.tsx b/src/components/RouteDisplay/SwapStep.tsx index 2319b436..327ab566 100644 --- a/src/components/RouteDisplay/SwapStep.tsx +++ b/src/components/RouteDisplay/SwapStep.tsx @@ -4,11 +4,11 @@ import { SWAP_VENUES } from "@/constants/swap-venues"; import { useAssets } from "@/context/assets"; import { useBroadcastedTxsStatus } from "@/solve"; import { onImageError } from "@/utils/image"; -import { makeExplorerLink } from "@/utils/link"; import { AdaptiveLink } from "../AdaptiveLink"; import { Gap } from "../common/Gap"; import { Action } from "."; +import { makeOperationState } from "./operation-state"; import { Step } from "./Step"; export interface SwapAction { @@ -23,11 +23,10 @@ export interface SwapAction { export interface SwapStepProps { action: SwapAction; actions: Action[]; - id: string; statusData?: ReturnType["data"]; } -export const SwapStep = ({ action, actions, id, statusData }: SwapStepProps) => { +export const SwapStep = ({ action, actions, statusData }: SwapStepProps) => { const { getAsset } = useAssets(); const assetIn = useMemo(() => { @@ -40,35 +39,30 @@ export const SwapStep = ({ action, actions, id, statusData }: SwapStepProps) => const venue = SWAP_VENUES[action.venue]; - // format: operationType-- - const operationIndex = Number(id.split("-")[2]); - const operationTypeCount = Number(id.split("-")[1]); - const isSwapFirstStep = operationIndex === 0 && operationTypeCount === 0; + const { explorerLink, state, operationIndex, operationTypeIndex } = makeOperationState({ + actions, + action, + statusData, + }); - const sequenceIndex = Number( - actions - // We can assume that the swap operation by the previous transfer - .find((x) => Number(x.id.split("-")[2]) === operationIndex - 1) - ?.id.split("-")[1], - ); - const swapStatus = statusData?.transferSequence[isSwapFirstStep ? 0 : sequenceIndex]; + const isSwapFirstStep = operationIndex === 0 && operationTypeIndex === 0; // as for swap operations, we can assume that the swap is successful if the previous transfer state is TRANSFER_SUCCESS const renderSwapState = useMemo(() => { if (isSwapFirstStep) { - if (swapStatus?.state === "TRANSFER_PENDING") { + if (state === "TRANSFER_PENDING") { return ; } - if (swapStatus?.state === "TRANSFER_SUCCESS") { + if (state === "TRANSFER_SUCCESS") { return ; } - if (swapStatus?.state === "TRANSFER_FAILURE") { + if (state === "TRANSFER_FAILURE") { return ; } - return
; + return ; } - switch (swapStatus?.state) { + switch (state) { case "TRANSFER_RECEIVED": return ; case "TRANSFER_SUCCESS": @@ -78,14 +72,7 @@ export const SwapStep = ({ action, actions, id, statusData }: SwapStepProps) => default: return ; } - }, [isSwapFirstStep, swapStatus?.state]); - - const explorerLink = useMemo(() => { - const tx = isSwapFirstStep ? swapStatus?.txs.sendTx : swapStatus?.txs.receiveTx; - if (!tx) return; - if (swapStatus?.state !== "TRANSFER_SUCCESS") return; - return makeExplorerLink(tx.explorerLink); - }, [isSwapFirstStep, swapStatus?.state, swapStatus?.txs.receiveTx, swapStatus?.txs.sendTx]); + }, [isSwapFirstStep, state]); if (!assetIn && assetOut) { return ( diff --git a/src/components/RouteDisplay/TransferStep.tsx b/src/components/RouteDisplay/TransferStep.tsx index 001a8364..a75e86d8 100644 --- a/src/components/RouteDisplay/TransferStep.tsx +++ b/src/components/RouteDisplay/TransferStep.tsx @@ -6,11 +6,11 @@ import { useBridgeByID } from "@/hooks/useBridges"; import { useChainByID } from "@/hooks/useChains"; import { useBroadcastedTxsStatus } from "@/solve"; import { onImageError } from "@/utils/image"; -import { makeExplorerLink } from "@/utils/link"; import { AdaptiveLink } from "../AdaptiveLink"; import { Gap } from "../common/Gap"; import { Action } from "."; +import { makeOperationState } from "./operation-state"; import { Step } from "./Step"; export interface TransferAction { @@ -25,40 +25,32 @@ export interface TransferAction { interface TransferStepProps { actions: Action[]; action: TransferAction; - id: string; statusData?: ReturnType["data"]; } -export const TransferStep = ({ action, actions, id, statusData }: TransferStepProps) => { +export const TransferStep = ({ action, actions, statusData }: TransferStepProps) => { + const { getAsset } = useAssets(); const { data: bridge } = useBridgeByID(action.bridgeID); const { data: sourceChain } = useChainByID(action.sourceChain); const { data: destinationChain } = useChainByID(action.destinationChain); - // format: operationType-- - const operationTypeCount = Number(id.split("-")[1]); - const operationIndex = Number(id.split("-")[2]); + const { explorerLink, state, operationIndex } = makeOperationState({ actions, action, statusData }); + const isFirstOpSwap = actions[0]?.type === "SWAP"; - const transferStatus = statusData?.transferSequence[operationTypeCount]; - const isNextOpSwap = - actions - // We can assume that the swap operation by the previous transfer - .find((x) => Number(x.id.split("-")[2]) === operationIndex + 1) - ?.id.split("-")[0] === "swap"; - const isPrevOpTransfer = actions[operationIndex - 1]?.type === "TRANSFER"; - // We can assume that the transfer is successful when the state is TRANSFER_SUCCESS or TRANSFER_RECEIVED const renderTransferState = useMemo(() => { + // We don't show loading state if first operation is swap operation, loading will be in swap operation if (isFirstOpSwap) { - if (transferStatus?.state === "TRANSFER_FAILURE") { + if (state === "TRANSFER_FAILURE") { return ; } - if (transferStatus?.state === "TRANSFER_SUCCESS") { + if (state === "TRANSFER_SUCCESS") { return ; } - return ; } - switch (transferStatus?.state) { + // We can assume that the transfer operation is successful when the state is TRANSFER_SUCCESS or TRANSFER_RECEIVED + switch (state) { case "TRANSFER_SUCCESS": return ; case "TRANSFER_RECEIVED": @@ -71,22 +63,7 @@ export const TransferStep = ({ action, actions, id, statusData }: TransferStepPr default: return
; } - }, [isFirstOpSwap, transferStatus?.state]); - - const explorerLink = useMemo(() => { - const packetTx = (() => { - if (operationIndex === 0) return transferStatus?.txs.sendTx; - if (isNextOpSwap) return transferStatus?.txs.sendTx; - if (isPrevOpTransfer) return transferStatus?.txs.sendTx; - return transferStatus?.txs.receiveTx; - })(); - if (!packetTx?.explorerLink) { - return null; - } - return makeExplorerLink(packetTx.explorerLink); - }, [isNextOpSwap, isPrevOpTransfer, operationIndex, transferStatus?.txs.receiveTx, transferStatus?.txs.sendTx]); - - const { getAsset } = useAssets(); + }, [isFirstOpSwap, state]); const asset = (() => { const currentAsset = getAsset(action.asset, action.sourceChain); diff --git a/src/components/RouteDisplay/index.tsx b/src/components/RouteDisplay/index.tsx index c1b5ed41..bee15a1f 100644 --- a/src/components/RouteDisplay/index.tsx +++ b/src/components/RouteDisplay/index.tsx @@ -64,7 +64,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut, chain: operation.swap.swapIn.swapVenue.chainID, venue: operation.swap.swapIn.swapVenue.name, - id: `swap-${swapCount}-${i}`, + id: `SWAP-${swapCount}-${i}`, }); asset = operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut; @@ -78,7 +78,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut, chain: operation.swap.swapOut.swapVenue.chainID, venue: operation.swap.swapOut.swapVenue.name, - id: `swap-${swapCount}-${i}`, + id: `SWAP-${swapCount}-${i}`, }); asset = operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut; @@ -93,7 +93,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad asset, sourceChain: operation.axelarTransfer.fromChainID, destinationChain: operation.axelarTransfer.toChainID, - id: `transfer-${transferCount}-${i}`, + id: `TRANSFER-${transferCount}-${i}`, bridgeID: operation.axelarTransfer.bridgeID, }); @@ -108,7 +108,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad asset, sourceChain: operation.cctpTransfer.fromChainID, destinationChain: operation.cctpTransfer.toChainID, - id: `transfer-${transferCount}-${i}`, + id: `TRANSFER-${transferCount}-${i}`, bridgeID: operation.cctpTransfer.bridgeID, }); @@ -146,7 +146,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad asset, sourceChain, destinationChain, - id: `transfer-${transferCount}-${i}`, + id: `TRANSFER-${transferCount}-${i}`, bridgeID: operation.transfer.bridgeID, }); @@ -188,7 +188,6 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad )} @@ -196,7 +195,6 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad )} diff --git a/src/components/RouteDisplay/operation-state.ts b/src/components/RouteDisplay/operation-state.ts new file mode 100644 index 00000000..cbbc86b8 --- /dev/null +++ b/src/components/RouteDisplay/operation-state.ts @@ -0,0 +1,79 @@ +import { useBroadcastedTxsStatus } from "@/solve"; +import { makeExplorerLink } from "@/utils/link"; + +import { Action } from "."; + +export const makeOperationState = ({ + actions, + action, + statusData, +}: { + actions: Action[]; + action: Action; + statusData?: ReturnType["data"]; +}) => { + // operations from router and tx/status response are not one to one + // tx/status only tracks transfer operations + + // format: -- + const _id = action.id.split("-"); + const operationType = action.type; + const operationTypeIndex = Number(_id[1]); + const operationIndex = Number(_id[2]); + + // swap operation + if (operationType === "SWAP") { + // Swap operation is not tracked by tx/status + // so we got the state from the previous transfer operation + // or next transfer operation if swap is the first operation + // ┌───────────┐ ┌───────────┐ + // │ Transfer │◀─┐ │ Swap │──┐ (first operation) + // └───────────┘ │ └───────────┘ │ + // │ state │ state + // ┌───────────┐ │ ┌───────────┐ │ + // │ Swap │──┘ │ Transfer │◀─┘ + // └───────────┘ └───────────┘ + const isSwapFirstStep = operationIndex === 0 && operationTypeIndex === 0; + const prevTransferOpIndex = Number( + actions.find((x) => Number(x.id.split("-")[2]) === operationIndex - 1)?.id.split("-")[1], + ); + const swapSequence = statusData?.transferSequence[isSwapFirstStep ? 0 : prevTransferOpIndex]; + const explorerLink = (() => { + const tx = isSwapFirstStep ? swapSequence?.txs.sendTx : swapSequence?.txs.receiveTx; + if (!tx) return; + if (swapSequence?.state !== "TRANSFER_SUCCESS") return; + return makeExplorerLink(tx.explorerLink); + })(); + return { + state: swapSequence?.state, + explorerLink, + operationIndex, + operationTypeIndex, + }; + } + + // transfer operation + const isNextOpSwap = + actions.find((x) => Number(x.id.split("-")[2]) === operationIndex + 1)?.id.split("-")[0] === "SWAP"; + const isPrevOpTransfer = actions[operationIndex - 1]?.type === "TRANSFER"; + const transferSequence = statusData?.transferSequence[operationTypeIndex]; + const explorerLink = (() => { + const packetTx = (() => { + if (operationIndex === 0) return transferSequence?.txs.sendTx; + if (isNextOpSwap) return transferSequence?.txs.sendTx; + if (isPrevOpTransfer) return transferSequence?.txs.sendTx; + return transferSequence?.txs.receiveTx; + })(); + if (!packetTx?.explorerLink) { + return null; + } + return makeExplorerLink(packetTx.explorerLink); + })(); + + return { + state: transferSequence?.state, + explorerLink, + operationIndex, + operationTypeIndex, + }; +}; From d7e2801f1d5ee34dae8009d242d52fde65a26a90 Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Sat, 17 Feb 2024 04:34:57 +0700 Subject: [PATCH 08/29] refactor: rename fn and file --- src/components/RouteDisplay/SwapStep.tsx | 4 ++-- src/components/RouteDisplay/TransferStep.tsx | 4 ++-- .../RouteDisplay/{operation-state.ts => make-step-state.ts} | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/components/RouteDisplay/{operation-state.ts => make-step-state.ts} (98%) diff --git a/src/components/RouteDisplay/SwapStep.tsx b/src/components/RouteDisplay/SwapStep.tsx index 327ab566..63e2d4ac 100644 --- a/src/components/RouteDisplay/SwapStep.tsx +++ b/src/components/RouteDisplay/SwapStep.tsx @@ -8,7 +8,7 @@ import { onImageError } from "@/utils/image"; import { AdaptiveLink } from "../AdaptiveLink"; import { Gap } from "../common/Gap"; import { Action } from "."; -import { makeOperationState } from "./operation-state"; +import { makeStepState } from "./make-step-state"; import { Step } from "./Step"; export interface SwapAction { @@ -39,7 +39,7 @@ export const SwapStep = ({ action, actions, statusData }: SwapStepProps) => { const venue = SWAP_VENUES[action.venue]; - const { explorerLink, state, operationIndex, operationTypeIndex } = makeOperationState({ + const { explorerLink, state, operationIndex, operationTypeIndex } = makeStepState({ actions, action, statusData, diff --git a/src/components/RouteDisplay/TransferStep.tsx b/src/components/RouteDisplay/TransferStep.tsx index a75e86d8..2f480e88 100644 --- a/src/components/RouteDisplay/TransferStep.tsx +++ b/src/components/RouteDisplay/TransferStep.tsx @@ -10,7 +10,7 @@ import { onImageError } from "@/utils/image"; import { AdaptiveLink } from "../AdaptiveLink"; import { Gap } from "../common/Gap"; import { Action } from "."; -import { makeOperationState } from "./operation-state"; +import { makeStepState } from "./make-step-state"; import { Step } from "./Step"; export interface TransferAction { @@ -34,7 +34,7 @@ export const TransferStep = ({ action, actions, statusData }: TransferStepProps) const { data: sourceChain } = useChainByID(action.sourceChain); const { data: destinationChain } = useChainByID(action.destinationChain); - const { explorerLink, state, operationIndex } = makeOperationState({ actions, action, statusData }); + const { explorerLink, state, operationIndex } = makeStepState({ actions, action, statusData }); const isFirstOpSwap = actions[0]?.type === "SWAP"; diff --git a/src/components/RouteDisplay/operation-state.ts b/src/components/RouteDisplay/make-step-state.ts similarity index 98% rename from src/components/RouteDisplay/operation-state.ts rename to src/components/RouteDisplay/make-step-state.ts index cbbc86b8..a865d915 100644 --- a/src/components/RouteDisplay/operation-state.ts +++ b/src/components/RouteDisplay/make-step-state.ts @@ -3,7 +3,7 @@ import { makeExplorerLink } from "@/utils/link"; import { Action } from "."; -export const makeOperationState = ({ +export const makeStepState = ({ actions, action, statusData, From 2d1f62454cf70dfc4d4b0e5a7705693fc72635eb Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Tue, 20 Feb 2024 23:41:06 +0700 Subject: [PATCH 09/29] [FRE-602] extract actions composing (#184) * refactor: [FRE-602] extract actions composing * refactor: change switch to if * refactor: bringback actions old logic --- src/components/RouteDisplay/SwapStep.tsx | 2 +- src/components/RouteDisplay/TransferStep.tsx | 2 +- src/components/RouteDisplay/index.tsx | 118 +----------------- src/components/RouteDisplay/make-actions.ts | 116 +++++++++++++++++ .../RouteDisplay/make-step-state.ts | 2 +- 5 files changed, 123 insertions(+), 117 deletions(-) create mode 100644 src/components/RouteDisplay/make-actions.ts diff --git a/src/components/RouteDisplay/SwapStep.tsx b/src/components/RouteDisplay/SwapStep.tsx index 63e2d4ac..b4a0d895 100644 --- a/src/components/RouteDisplay/SwapStep.tsx +++ b/src/components/RouteDisplay/SwapStep.tsx @@ -7,7 +7,7 @@ import { onImageError } from "@/utils/image"; import { AdaptiveLink } from "../AdaptiveLink"; import { Gap } from "../common/Gap"; -import { Action } from "."; +import { Action } from "./make-actions"; import { makeStepState } from "./make-step-state"; import { Step } from "./Step"; diff --git a/src/components/RouteDisplay/TransferStep.tsx b/src/components/RouteDisplay/TransferStep.tsx index 2f480e88..136113d2 100644 --- a/src/components/RouteDisplay/TransferStep.tsx +++ b/src/components/RouteDisplay/TransferStep.tsx @@ -9,7 +9,7 @@ import { onImageError } from "@/utils/image"; import { AdaptiveLink } from "../AdaptiveLink"; import { Gap } from "../common/Gap"; -import { Action } from "."; +import { Action } from "./make-actions"; import { makeStepState } from "./make-step-state"; import { Step } from "./Step"; diff --git a/src/components/RouteDisplay/index.tsx b/src/components/RouteDisplay/index.tsx index bee15a1f..e53e94d3 100644 --- a/src/components/RouteDisplay/index.tsx +++ b/src/components/RouteDisplay/index.tsx @@ -8,11 +8,10 @@ import { useBroadcastedTxsStatus } from "@/solve"; import { ExpandArrow } from "../Icons/ExpandArrow"; import { BroadcastedTx } from "../TransactionDialog/TransactionDialogContent"; +import { makeActions } from "./make-actions"; import { RouteEnd } from "./RouteEnd"; -import { SwapAction, SwapStep } from "./SwapStep"; -import { TransferAction, TransferStep } from "./TransferStep"; - -export type Action = TransferAction | SwapAction; +import { SwapStep } from "./SwapStep"; +import { TransferStep } from "./TransferStep"; interface RouteDisplayProps { route: RouteResponse; @@ -47,116 +46,7 @@ export const RouteDisplay = ({ route, isRouteExpanded, setIsRouteExpanded, broad } }, [route.amountOut, destinationAsset?.decimals]); - const actions = useMemo(() => { - const _actions: Action[] = []; - - let swapCount = 0; - let transferCount = 0; - let asset = route.sourceAssetDenom; - - route.operations.forEach((operation, i) => { - if ("swap" in operation) { - if ("swapIn" in operation.swap) { - _actions.push({ - type: "SWAP", - sourceAsset: operation.swap.swapIn.swapOperations[0].denomIn, - destinationAsset: - operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut, - chain: operation.swap.swapIn.swapVenue.chainID, - venue: operation.swap.swapIn.swapVenue.name, - id: `SWAP-${swapCount}-${i}`, - }); - - asset = operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut; - } - - if ("swapOut" in operation.swap) { - _actions.push({ - type: "SWAP", - sourceAsset: operation.swap.swapOut.swapOperations[0].denomIn, - destinationAsset: - operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut, - chain: operation.swap.swapOut.swapVenue.chainID, - venue: operation.swap.swapOut.swapVenue.name, - id: `SWAP-${swapCount}-${i}`, - }); - - asset = operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut; - } - swapCount++; - return; - } - - if ("axelarTransfer" in operation) { - _actions.push({ - type: "TRANSFER", - asset, - sourceChain: operation.axelarTransfer.fromChainID, - destinationChain: operation.axelarTransfer.toChainID, - id: `TRANSFER-${transferCount}-${i}`, - bridgeID: operation.axelarTransfer.bridgeID, - }); - - asset = operation.axelarTransfer.asset; - transferCount++; - return; - } - - if ("cctpTransfer" in operation) { - _actions.push({ - type: "TRANSFER", - asset, - sourceChain: operation.cctpTransfer.fromChainID, - destinationChain: operation.cctpTransfer.toChainID, - id: `TRANSFER-${transferCount}-${i}`, - bridgeID: operation.cctpTransfer.bridgeID, - }); - - asset = operation.cctpTransfer.burnToken; - transferCount++; - return; - } - - const sourceChain = operation.transfer.chainID; - - let destinationChain = ""; - if (i === route.operations.length - 1) { - destinationChain = route.destAssetChainID; - } else { - const nextOperation = route.operations[i + 1]; - if ("swap" in nextOperation) { - if ("swapIn" in nextOperation.swap) { - destinationChain = nextOperation.swap.swapIn.swapVenue.chainID; - } - - if ("swapOut" in nextOperation.swap) { - destinationChain = nextOperation.swap.swapOut.swapVenue.chainID; - } - } else if ("axelarTransfer" in nextOperation) { - destinationChain = nextOperation.axelarTransfer.fromChainID; - } else if ("cctpTransfer" in nextOperation) { - destinationChain = nextOperation.cctpTransfer.fromChainID; - } else { - destinationChain = nextOperation.transfer.chainID; - } - } - - _actions.push({ - type: "TRANSFER", - asset, - sourceChain, - destinationChain, - id: `TRANSFER-${transferCount}-${i}`, - bridgeID: operation.transfer.bridgeID, - }); - - asset = operation.transfer.destDenom; - transferCount++; - }); - - return _actions; - }, [route]); - + const actions = useMemo(() => makeActions({ route }), [route]); const { data: statusData } = useBroadcastedTxsStatus({ txsRequired: route.txsRequired, txs: broadcastedTxs }); return ( diff --git a/src/components/RouteDisplay/make-actions.ts b/src/components/RouteDisplay/make-actions.ts new file mode 100644 index 00000000..1c28f149 --- /dev/null +++ b/src/components/RouteDisplay/make-actions.ts @@ -0,0 +1,116 @@ +import { RouteResponse } from "@skip-router/core"; + +import { SwapAction } from "./SwapStep"; +import { TransferAction } from "./TransferStep"; + +export type Action = TransferAction | SwapAction; + +export const makeActions = ({ route }: { route: RouteResponse }): Action[] => { + const _actions: Action[] = []; + + let swapCount = 0; + let transferCount = 0; + let asset = route.sourceAssetDenom; + + route.operations.forEach((operation, i) => { + if ("swap" in operation) { + if ("swapIn" in operation.swap) { + _actions.push({ + type: "SWAP", + sourceAsset: operation.swap.swapIn.swapOperations[0].denomIn, + destinationAsset: + operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut, + chain: operation.swap.swapIn.swapVenue.chainID, + venue: operation.swap.swapIn.swapVenue.name, + id: `SWAP-${swapCount}-${i}`, + }); + + asset = operation.swap.swapIn.swapOperations[operation.swap.swapIn.swapOperations.length - 1].denomOut; + } + + if ("swapOut" in operation.swap) { + _actions.push({ + type: "SWAP", + sourceAsset: operation.swap.swapOut.swapOperations[0].denomIn, + destinationAsset: + operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut, + chain: operation.swap.swapOut.swapVenue.chainID, + venue: operation.swap.swapOut.swapVenue.name, + id: `SWAP-${swapCount}-${i}`, + }); + + asset = operation.swap.swapOut.swapOperations[operation.swap.swapOut.swapOperations.length - 1].denomOut; + } + swapCount++; + return; + } + + if ("axelarTransfer" in operation) { + _actions.push({ + type: "TRANSFER", + asset, + sourceChain: operation.axelarTransfer.fromChainID, + destinationChain: operation.axelarTransfer.toChainID, + id: `TRANSFER-${transferCount}-${i}`, + bridgeID: operation.axelarTransfer.bridgeID, + }); + + asset = operation.axelarTransfer.asset; + transferCount++; + return; + } + + if ("cctpTransfer" in operation) { + _actions.push({ + type: "TRANSFER", + asset, + sourceChain: operation.cctpTransfer.fromChainID, + destinationChain: operation.cctpTransfer.toChainID, + id: `TRANSFER-${transferCount}-${i}`, + bridgeID: operation.cctpTransfer.bridgeID, + }); + + asset = operation.cctpTransfer.burnToken; + transferCount++; + return; + } + + const sourceChain = operation.transfer.chainID; + + let destinationChain = ""; + if (i === route.operations.length - 1) { + destinationChain = route.destAssetChainID; + } else { + const nextOperation = route.operations[i + 1]; + if ("swap" in nextOperation) { + if ("swapIn" in nextOperation.swap) { + destinationChain = nextOperation.swap.swapIn.swapVenue.chainID; + } + + if ("swapOut" in nextOperation.swap) { + destinationChain = nextOperation.swap.swapOut.swapVenue.chainID; + } + } else if ("axelarTransfer" in nextOperation) { + destinationChain = nextOperation.axelarTransfer.fromChainID; + } else if ("cctpTransfer" in nextOperation) { + destinationChain = nextOperation.cctpTransfer.fromChainID; + } else { + destinationChain = nextOperation.transfer.chainID; + } + } + + _actions.push({ + type: "TRANSFER", + asset, + sourceChain, + destinationChain, + id: `TRANSFER-${transferCount}-${i}`, + bridgeID: operation.transfer.bridgeID, + }); + + asset = operation.transfer.destDenom; + transferCount++; + }); + + return _actions; +}; diff --git a/src/components/RouteDisplay/make-step-state.ts b/src/components/RouteDisplay/make-step-state.ts index a865d915..3832fd95 100644 --- a/src/components/RouteDisplay/make-step-state.ts +++ b/src/components/RouteDisplay/make-step-state.ts @@ -1,7 +1,7 @@ import { useBroadcastedTxsStatus } from "@/solve"; import { makeExplorerLink } from "@/utils/link"; -import { Action } from "."; +import { Action } from "./make-actions"; export const makeStepState = ({ actions, From 1afccb0c79e6a789a822c7475e54c73001ac3a5b Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Sat, 24 Feb 2024 02:32:11 +0700 Subject: [PATCH 10/29] chore(deps): add dotenv --- package-lock.json | 12 ++++++++++++ package.json | 1 + 2 files changed, 13 insertions(+) diff --git a/package-lock.json b/package-lock.json index aa07369e..bc990662 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "clsx": "^2.1.0", "cosmjs-types": "0.8.x", "date-fns": "^3.3.1", + "dotenv": "^16.4.5", "download": "^8.0.0", "match-sorter": "^6.3.3", "next": "^14.1.0", @@ -12631,6 +12632,17 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/download": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/download/-/download-8.0.0.tgz", diff --git a/package.json b/package.json index ca51485e..2ee62106 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "clsx": "^2.1.0", "cosmjs-types": "0.8.x", "date-fns": "^3.3.1", + "dotenv": "^16.4.5", "download": "^8.0.0", "match-sorter": "^6.3.3", "next": "^14.1.0", From d7249c830515d92cd0ed5f6ca0c17df8c34b296a Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Sat, 24 Feb 2024 02:34:14 +0700 Subject: [PATCH 11/29] chore: add make-actions and make-step unit test --- .github/workflows/unit-test.yml | 31 +++++ .../__test__/make-actions.test.tsx | 118 ++++++++++++++++++ .../RouteDisplay/__test__/make-step.test.tsx | 71 +++++++++++ .../RouteDisplay/__test__/route-to-test.ts | 59 +++++++++ src/components/RouteDisplay/__test__/utils.ts | 28 +++++ 5 files changed, 307 insertions(+) create mode 100644 .github/workflows/unit-test.yml create mode 100644 src/components/RouteDisplay/__test__/make-actions.test.tsx create mode 100644 src/components/RouteDisplay/__test__/make-step.test.tsx create mode 100644 src/components/RouteDisplay/__test__/route-to-test.ts create mode 100644 src/components/RouteDisplay/__test__/utils.ts diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 00000000..8e9cfa7a --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,31 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Unit test + +on: + push: + branches: ["staging"] + pull_request: + branches: ["staging"] + +jobs: + build: + runs-on: ${{matrix.os}} + + strategy: + matrix: + node-version: [14.x, 16.x, 18.x] + os: [ubuntu-latest, windows-latest, macos-latest] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + - run: npm ci + - run: npm test + - run: npm run build --if-present diff --git a/src/components/RouteDisplay/__test__/make-actions.test.tsx b/src/components/RouteDisplay/__test__/make-actions.test.tsx new file mode 100644 index 00000000..57b7647f --- /dev/null +++ b/src/components/RouteDisplay/__test__/make-actions.test.tsx @@ -0,0 +1,118 @@ +import { makeActions } from "../make-actions"; +import { + cosmosHubAtomToAkashAKT, + cosmoshubATOMToAkashATOM, + cosmoshubATOMToArbitrumARB, + nobleUSDCToEthereumUSDC, + RouteArgs, +} from "./route-to-test"; +import { createRoute } from "./utils"; + +const makeActionsTest = async (_route: RouteArgs) => { + const { direction, amount, sourceAsset, sourceAssetChainID, destinationAsset, destinationAssetChainID, swapVenue } = + _route; + + const route = await createRoute( + direction === "swap-in" + ? { + amountIn: amount, + sourceAssetDenom: sourceAsset, + sourceAssetChainID: sourceAssetChainID, + destAssetDenom: destinationAsset, + destAssetChainID: destinationAssetChainID, + swapVenue, + allowMultiTx: true, + allowUnsafe: true, + experimentalFeatures: ["cctp"], + } + : { + amountOut: amount, + sourceAssetDenom: sourceAsset, + sourceAssetChainID: sourceAssetChainID, + destAssetDenom: destinationAsset, + destAssetChainID: destinationAssetChainID, + swapVenue, + allowMultiTx: true, + allowUnsafe: true, + experimentalFeatures: ["cctp"], + }, + ); + const actions = makeActions({ route }); + + expect(actions).toBeTruthy(); + expect(actions.length).toBeGreaterThan(0); + + const totalActions = actions.length; + + actions.forEach((currentAction, i) => { + const nextAction = i === totalActions - 1 ? undefined : actions[i + 1]; + const prevAction = i === 0 ? undefined : actions[i - 1]; + const bridgeId = (() => { + if ("transfer" in route.operations[i]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return route.operations[i].transfer.bridgeID; + } + if ("axelarTransfer" in route.operations[i]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return route.operations[i].axelarTransfer.bridgeID; + } + if ("cctpTransfer" in route.operations[i]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return route.operations[i].cctpTransfer.bridgeID; + } + return undefined; + })(); + if (currentAction.type === "TRANSFER") { + expect(currentAction.bridgeID).toBe(bridgeId); + if (actions.length === 1) { + expect(currentAction.sourceChain).toBe(_route.sourceAssetChainID); + expect(currentAction.asset).toBe(_route.sourceAsset); + expect(currentAction.destinationChain).toBe(_route.destinationAssetChainID); + return; + } + if (i === 0) { + expect(currentAction.sourceChain).toBe(_route.sourceAssetChainID); + expect(currentAction.asset).toBe(_route.sourceAsset); + return; + } + if (nextAction) { + if (nextAction.type === "SWAP") { + expect(currentAction.destinationChain).toBe(nextAction.chain); + return; + } + if (nextAction.type === "TRANSFER") { + expect(currentAction.destinationChain).toBe(nextAction.sourceChain); + return; + } + } + } + if (currentAction.type === "SWAP") { + if (prevAction) { + if (prevAction.type === "TRANSFER") { + expect(currentAction.chain).toBe(prevAction.destinationChain); + return; + } + } + } + }); + return actions; +}; + +test("make-actions: Cosmoshub ATOM -> Akash AKT", async () => { + await makeActionsTest(cosmosHubAtomToAkashAKT); +}); + +test("make-actions: Cosmoshub ATOM to Akash ATOM", async () => { + await makeActionsTest(cosmoshubATOMToAkashATOM); +}); + +test("make-actions: Noble USDC to Ethereum USDC", async () => { + await makeActionsTest(nobleUSDCToEthereumUSDC); +}); + +test("make-actions: Cosmoshub ATOM to Arbitrum ARB", async () => { + await makeActionsTest(cosmoshubATOMToArbitrumARB); +}); diff --git a/src/components/RouteDisplay/__test__/make-step.test.tsx b/src/components/RouteDisplay/__test__/make-step.test.tsx new file mode 100644 index 00000000..09aa37ba --- /dev/null +++ b/src/components/RouteDisplay/__test__/make-step.test.tsx @@ -0,0 +1,71 @@ +import { useBroadcastedTxsStatus } from "@/solve"; +import { AllTheProviders, renderHook, waitFor } from "@/test"; + +import { makeActions } from "../make-actions"; +import { makeStepState } from "../make-step-state"; +import { nobleUSDCtoInjectiveINJ } from "./route-to-test"; +import { createRoute } from "./utils"; + +test("make-step: Noble USDC to Injective INJ", async () => { + const { direction, amount, sourceAsset, sourceAssetChainID, destinationAsset, destinationAssetChainID, swapVenue } = + nobleUSDCtoInjectiveINJ; + + const route = await createRoute( + direction === "swap-in" + ? { + amountIn: amount, + sourceAssetDenom: sourceAsset, + sourceAssetChainID: sourceAssetChainID, + destAssetDenom: destinationAsset, + destAssetChainID: destinationAssetChainID, + swapVenue, + allowMultiTx: true, + allowUnsafe: true, + experimentalFeatures: ["cctp"], + } + : { + amountOut: amount, + sourceAssetDenom: sourceAsset, + sourceAssetChainID: sourceAssetChainID, + destAssetDenom: destinationAsset, + destAssetChainID: destinationAssetChainID, + swapVenue, + allowMultiTx: true, + allowUnsafe: true, + experimentalFeatures: ["cctp"], + }, + ); + const actions = makeActions({ route }); + const { result } = renderHook( + () => + useBroadcastedTxsStatus({ + txsRequired: route.txsRequired, + txs: [ + { + chainID: "noble-1", + txHash: "40C220E06B22435842A1DDA80ED2D38917228D8A77419A9F4B885C9E48D6B228", + }, + ], + enabled: true, + }), + { + wrapper: AllTheProviders, + }, + ); + + await waitFor(() => expect(result.current.isLoading).toBeFalsy(), { + timeout: 10000, + }); + + actions.forEach((action, i) => { + const { explorerLink, operationIndex, operationTypeIndex, state } = makeStepState({ + action, + actions, + statusData: result.current.data, + }); + expect(operationIndex).toEqual(i); + expect(state).toBeDefined(); + expect(operationTypeIndex).toBeDefined(); + expect(explorerLink).toBeDefined(); + }); +}); diff --git a/src/components/RouteDisplay/__test__/route-to-test.ts b/src/components/RouteDisplay/__test__/route-to-test.ts new file mode 100644 index 00000000..a1d1c843 --- /dev/null +++ b/src/components/RouteDisplay/__test__/route-to-test.ts @@ -0,0 +1,59 @@ +export interface RouteArgs { + direction: string; + amount: string; + sourceAsset: string; + sourceAssetChainID: string; + destinationAsset: string; + destinationAssetChainID: string; + swapVenue: undefined; +} + +export const cosmosHubAtomToAkashAKT = { + direction: "swap-in", + amount: "1000000", + sourceAsset: "uatom", + sourceAssetChainID: "cosmoshub-4", + destinationAsset: "uakt", + destinationAssetChainID: "akashnet-2", + swapVenue: undefined, +}; + +export const cosmoshubATOMToAkashATOM = { + direction: "swap-in", + amount: "1000000", + sourceAsset: "uatom", + sourceAssetChainID: "cosmoshub-4", + destinationAsset: "ibc/2E5D0AC026AC1AFA65A23023BA4F24BB8DDF94F118EDC0BAD6F625BFC557CDED", + destinationAssetChainID: "akashnet-2", + swapVenue: undefined, +}; + +export const nobleUSDCToEthereumUSDC = { + direction: "swap-in", + amount: "11000000", + sourceAsset: "uusdc", + sourceAssetChainID: "noble-1", + destinationAsset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + destinationAssetChainID: "1", + swapVenue: undefined, +}; + +export const cosmoshubATOMToArbitrumARB = { + direction: "swap-in", + amount: "11000000", + sourceAsset: "uatom", + sourceAssetChainID: "cosmoshub-4", + destinationAsset: "0x912CE59144191C1204E64559FE8253a0e49E6548", + destinationAssetChainID: "42161", + swapVenue: undefined, +}; + +export const nobleUSDCtoInjectiveINJ = { + direction: "swap-in", + amount: "1000000", + sourceAsset: "uusdc", + sourceAssetChainID: "noble-1", + destinationAsset: "inj", + destinationAssetChainID: "injective-1", + swapVenue: undefined, +}; diff --git a/src/components/RouteDisplay/__test__/utils.ts b/src/components/RouteDisplay/__test__/utils.ts new file mode 100644 index 00000000..9b8943db --- /dev/null +++ b/src/components/RouteDisplay/__test__/utils.ts @@ -0,0 +1,28 @@ +import { RouteRequest, SkipRouter } from "@skip-router/core"; +import { waitFor } from "@testing-library/react"; + +import { API_URL, APP_URL } from "@/constants/api"; + +export const createRoute = async (options: RouteRequest) => { + const skipClient = new SkipRouter({ + clientID: process.env.NEXT_PUBLIC_CLIENT_ID, + apiURL: API_URL, + endpointOptions: { + getRpcEndpointForChain: async (chainID) => { + return `${APP_URL}/api/rpc/${chainID}`; + }, + getRestEndpointForChain: async (chainID) => { + return `${APP_URL}/api/rest/${chainID}`; + }, + }, + }); + const route = await skipClient.route(options); + await waitFor(() => expect(route).toBeTruthy(), { + timeout: 10000, + }); + + if (!route) { + throw new Error("useRoute hook returned no data"); + } + return route; +}; From 30ee5ab66b3f72a564e411959ead05e6c9971837 Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Sat, 24 Feb 2024 02:46:08 +0700 Subject: [PATCH 12/29] feat: automated e2e test --- .github/workflows/unit-test.yml | 31 -- env.d.ts | 1 + playwright.config.ts | 9 +- .../AssetSelect/AssetSelectContent.tsx | 1 + src/components/AssetSelect/index.tsx | 1 + .../ChainSelect/ChainSelectContent.tsx | 1 + .../ChainSelect/ChainSelectTrigger.tsx | 1 + src/components/RouteDisplay/Step.tsx | 22 +- src/components/RouteDisplay/SwapStep.tsx | 23 +- src/components/RouteDisplay/TransferStep.tsx | 23 +- src/components/RouteDisplay/index.tsx | 6 +- .../TransactionDialogContent.tsx | 13 +- src/components/TransactionSuccessView.tsx | 2 +- .../ConnectWalletButtonSmall.test.tsx | 21 - src/components/__tests__/SwapWidget.test.tsx | 406 ------------------ src/components/__tests__/WalletModal.test.tsx | 97 ----- tests/example.spec.ts | 18 - tests/lib/commands/keplr.ts | 4 +- tests/lib/commands/playwright.ts | 27 +- tests/lib/globalSetup.ts | 14 + tests/transactions.spec.ts | 59 +++ tests/utils.ts | 134 ++++++ 22 files changed, 324 insertions(+), 590 deletions(-) delete mode 100644 .github/workflows/unit-test.yml delete mode 100644 src/components/__tests__/ConnectWalletButtonSmall.test.tsx delete mode 100644 src/components/__tests__/SwapWidget.test.tsx delete mode 100644 src/components/__tests__/WalletModal.test.tsx delete mode 100644 tests/example.spec.ts create mode 100644 tests/lib/globalSetup.ts create mode 100644 tests/transactions.spec.ts create mode 100644 tests/utils.ts diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml deleted file mode 100644 index 8e9cfa7a..00000000 --- a/.github/workflows/unit-test.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs - -name: Unit test - -on: - push: - branches: ["staging"] - pull_request: - branches: ["staging"] - -jobs: - build: - runs-on: ${{matrix.os}} - - strategy: - matrix: - node-version: [14.x, 16.x, 18.x] - os: [ubuntu-latest, windows-latest, macos-latest] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: "npm" - - run: npm ci - - run: npm test - - run: npm run build --if-present diff --git a/env.d.ts b/env.d.ts index 5fc94372..aca3e5bc 100644 --- a/env.d.ts +++ b/env.d.ts @@ -11,6 +11,7 @@ declare namespace NodeJS { readonly NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID?: string; readonly RESEND_API_KEY?: string; readonly WALLETCONNECT_VERIFY_KEY?: string; + readonly WORD_PHRASE_KEY?: string; } } diff --git a/playwright.config.ts b/playwright.config.ts index af40ce00..133fe25b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,9 +2,11 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./tests", + globalSetup: "./tests/lib/globalSetup.ts", + timeout: 600000, fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 2 : 2, workers: 1, reporter: "html", use: { @@ -17,4 +19,9 @@ export default defineConfig({ use: { ...devices["Desktop Chrome"] }, }, ], + webServer: { + command: "npm run dev", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, }); diff --git a/src/components/AssetSelect/AssetSelectContent.tsx b/src/components/AssetSelect/AssetSelectContent.tsx index a7de78af..44a2f0ba 100644 --- a/src/components/AssetSelect/AssetSelectContent.tsx +++ b/src/components/AssetSelect/AssetSelectContent.tsx @@ -89,6 +89,7 @@ function AssetSelectContent({ assets = [], balances, onChange, onClose, showChai {filteredAssets.map((asset) => (