From 4bd16e8895841d060f5aef438e80f53939d9604f Mon Sep 17 00:00:00 2001 From: Griko Nibras Date: Thu, 1 Feb 2024 04:47:30 +0700 Subject: [PATCH 1/7] fix: replace deprecated estimatedAmountOut with amountOut [FRE-588] --- src/components/RouteDisplay.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index b1ff72a8..752ef0f1 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -1,7 +1,7 @@ /* eslint-disable @next/next/no-img-element */ import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid"; import { BridgeType, RouteResponse } from "@skip-router/core"; -import { ethers } from "ethers"; +import { formatUnits } from "ethers"; import { ComponentProps, Dispatch, Fragment, SetStateAction, SyntheticEvent, useMemo } from "react"; import { useAssets } from "@/context/assets"; @@ -515,7 +515,7 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT const amountIn = useMemo(() => { try { - return ethers.formatUnits(route.amountIn, sourceAsset?.decimals ?? 6); + return formatUnits(route.amountIn, sourceAsset?.decimals ?? 6); } catch { return "0.0"; } @@ -523,11 +523,11 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT const amountOut = useMemo(() => { try { - return ethers.formatUnits(route.estimatedAmountOut ?? 0, destinationAsset?.decimals ?? 6); + return formatUnits(route.amountOut ?? 0, destinationAsset?.decimals ?? 6); } catch { return "0.0"; } - }, [route.estimatedAmountOut, destinationAsset?.decimals]); + }, [route.amountOut, destinationAsset?.decimals]); const actions = useMemo(() => { const _actions: Action[] = []; From be8ac44eaf7bb8bd1101094115177256d1079f23 Mon Sep 17 00:00:00 2001 From: Griko Nibras Date: Thu, 1 Feb 2024 05:49:28 +0700 Subject: [PATCH 2/7] feat: add useWalletAddresses hook Signed-off-by: Griko Nibras --- src/hooks/useWalletAddresses.ts | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/hooks/useWalletAddresses.ts diff --git a/src/hooks/useWalletAddresses.ts b/src/hooks/useWalletAddresses.ts new file mode 100644 index 00000000..f8586dd5 --- /dev/null +++ b/src/hooks/useWalletAddresses.ts @@ -0,0 +1,87 @@ +import { useManager } from "@cosmos-kit/react"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useAccount as useWagmiAccount } from "wagmi"; + +import { useAccount } from "@/hooks/useAccount"; +import { useChains } from "@/hooks/useChains"; + +export function useWalletAddresses(chainIDs: string[]) { + const { data: chains = [] } = useChains(); + + const { address: evmAddress } = useWagmiAccount(); + const { getWalletRepo } = useManager(); + + const srcAccount = useAccount("source"); + const dstAccount = useAccount("destination"); + + const queryKey = useMemo(() => ["USE_WALLET_ADDRESSES", chainIDs] as const, [chainIDs]); + + return useQuery({ + queryKey, + queryFn: async ({ queryKey: [, chainIDs] }) => { + const record: Record = {}; + + const srcChain = chains.find(({ chainID }) => { + return chainID === chainIDs.at(0); + }); + const dstChain = chains.find(({ chainID }) => { + return chainID === chainIDs.at(-1); + }); + + for (const currentChainID of chainIDs) { + const chain = chains.find(({ chainID }) => chainID === currentChainID); + if (!chain) { + throw new Error(`useWalletAddresses error: cannot find chain '${currentChainID}'`); + } + + if (chain.chainType === "cosmos") { + const { wallets } = getWalletRepo(chain.chainName); + + const currentWalletName = (() => { + // if `chainID` is the source or destination chain + if (srcChain?.chainID === currentChainID) { + return srcAccount?.wallet?.walletName; + } + if (dstChain?.chainID === currentChainID) { + return dstAccount?.wallet?.walletName; + } + + // if `chainID` isn't the source or destination chain + if (srcChain?.chainType === "cosmos") { + return srcAccount?.wallet?.walletName; + } + if (dstChain?.chainType === "cosmos") { + return dstAccount?.wallet?.walletName; + } + })(); + + if (!currentWalletName) { + throw new Error(`useWalletAddresses error: cannot find wallet for '${chain.chainName}'`); + } + + const wallet = wallets.find(({ walletName }) => walletName === currentWalletName); + if (!wallet) { + throw new Error(`useWalletAddresses error: cannot find wallet for '${chain.chainName}'`); + } + if (wallet.isWalletDisconnected || !wallet.isWalletConnected) { + await wallet.connect(); + } + if (!wallet.address) { + throw new Error(`useWalletAddresses error: cannot resolve wallet address for '${chain.chainName}'`); + } + record[currentChainID] = wallet.address; + } + + if (chain.chainType === "evm") { + if (!evmAddress) { + throw new Error(`useWalletAddresses error: evm wallet not connected`); + } + record[currentChainID] = evmAddress; + } + } + + return record; + }, + }); +} From d636ca3df6f80e99c984ca0aaac33814747f023b Mon Sep 17 00:00:00 2001 From: Griko Nibras Date: Thu, 1 Feb 2024 05:50:00 +0700 Subject: [PATCH 3/7] feat: make hotfixed gas price resolver Signed-off-by: Griko Nibras --- src/constants/gas.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/constants/gas.ts b/src/constants/gas.ts index 5eb36a19..7916522d 100644 --- a/src/constants/gas.ts +++ b/src/constants/gas.ts @@ -1,3 +1,5 @@ +import { GasPrice } from "@cosmjs/stargate"; + export const DEFAULT_GAS_AMOUNT = (200_000).toString(); export const EVMOS_GAS_AMOUNT = (280_000).toString(); @@ -5,3 +7,9 @@ export const EVMOS_GAS_AMOUNT = (280_000).toString(); export function isChainIdEvmos(chainID: string) { return chainID === "evmos_9001-2" || chainID.includes("evmos"); } + +export async function getHotfixedGasPrice(chainID: string) { + if (chainID === "noble-1") { + return GasPrice.fromString("0.0uusdc"); + } +} From dae2bdc3719e9afc4c7a47635a7f2e46010f155e Mon Sep 17 00:00:00 2001 From: Griko Nibras Date: Thu, 1 Feb 2024 05:50:35 +0700 Subject: [PATCH 4/7] refactor: attach hotfixed gas price resolver Signed-off-by: Griko Nibras --- src/components/SwapWidget/useSwapWidget.ts | 12 +-- .../TransactionDialogContent.tsx | 75 ++----------------- 2 files changed, 13 insertions(+), 74 deletions(-) diff --git a/src/components/SwapWidget/useSwapWidget.ts b/src/components/SwapWidget/useSwapWidget.ts index eaf5eb1a..731aa64d 100644 --- a/src/components/SwapWidget/useSwapWidget.ts +++ b/src/components/SwapWidget/useSwapWidget.ts @@ -13,7 +13,7 @@ import { createJSONStorage, persist, subscribeWithSelector } from "zustand/middl import { shallow } from "zustand/shallow"; import { createWithEqualityFn as create } from "zustand/traditional"; -import { EVMOS_GAS_AMOUNT, isChainIdEvmos } from "@/constants/gas"; +import { EVMOS_GAS_AMOUNT, getHotfixedGasPrice, isChainIdEvmos } from "@/constants/gas"; import { useAssets } from "@/context/assets"; import { useAnyDisclosureOpen } from "@/context/disclosures"; import { useSettingsStore } from "@/context/settings"; @@ -141,9 +141,10 @@ export function useSwapWidget() { const parsedFeeBalance = BigNumber(balances[srcFeeAsset.denom] ?? "0").shiftedBy(-(srcFeeAsset.decimals ?? 6)); const parsedGasRequired = BigNumber(gasRequired || "0"); if ( - srcFeeAsset.denom === srcAsset.denom + parsedGasRequired.gt(0) && + (srcFeeAsset.denom === srcAsset.denom ? parsedAmount.isGreaterThan(parsedBalance.minus(parsedGasRequired)) - : parsedFeeBalance.minus(parsedGasRequired).isLessThanOrEqualTo(0) + : parsedFeeBalance.minus(parsedGasRequired).isLessThanOrEqualTo(0)) ) { return `Insufficient balance. You need ≈${gasRequired} ${srcFeeAsset.recommendedSymbol} to accomodate gas fees.`; } @@ -453,12 +454,13 @@ export function useSwapWidget() { async ([srcChain, srcAsset, srcFeeAsset]) => { if (!(srcChain?.chainType === "cosmos" && srcAsset)) return; - const suggestedPrice = await skipClient.getRecommendedGasPrice(srcChain.chainID); + let suggestedPrice = await getHotfixedGasPrice(srcChain.chainID); + suggestedPrice ??= await skipClient.getRecommendedGasPrice(srcChain.chainID); if (!srcFeeAsset || srcFeeAsset.chainID !== srcChain.chainID) { if (suggestedPrice) { srcFeeAsset = assetsByChainID(srcChain.chainID).find(({ denom }) => { - return denom === suggestedPrice.denom; + return denom === suggestedPrice!.denom; }); } else { srcFeeAsset = await getFeeAsset(srcChain.chainID); diff --git a/src/components/TransactionDialog/TransactionDialogContent.tsx b/src/components/TransactionDialog/TransactionDialogContent.tsx index 3554b9dd..974d9b7f 100644 --- a/src/components/TransactionDialog/TransactionDialogContent.tsx +++ b/src/components/TransactionDialog/TransactionDialogContent.tsx @@ -1,16 +1,15 @@ -import { useManager } from "@cosmos-kit/react"; import { ArrowLeftIcon, CheckCircleIcon, InformationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid"; import * as Sentry from "@sentry/react"; import { RouteResponse } from "@skip-router/core"; import { useState } from "react"; import { toast } from "react-hot-toast"; -import { useAccount as useWagmiAccount } from "wagmi"; +import { getHotfixedGasPrice } from "@/constants/gas"; import { useSettingsStore } from "@/context/settings"; import { txHistory } from "@/context/tx-history"; import { useAccount } from "@/hooks/useAccount"; -import { useChains } from "@/hooks/useChains"; import { useFinalityTimeEstimate } from "@/hooks/useFinalityTimeEstimate"; +import { useWalletAddresses } from "@/hooks/useWalletAddresses"; import { useBroadcastedTxsStatus, useSkipClient } from "@/solve"; import { isUserRejectedRequestError } from "@/utils/error"; import { getExplorerUrl } from "@/utils/explorer"; @@ -37,10 +36,7 @@ export interface BroadcastedTx { } function TransactionDialogContent({ route, onClose, isAmountError, transactionCount }: Props) { - const { data: chains = [] } = useChains(); - const skipClient = useSkipClient(); - const { address: evmAddress } = useWagmiAccount(); const [isOngoing, setOngoing] = useState(false); @@ -53,82 +49,23 @@ function TransactionDialogContent({ route, onClose, isAmountError, transactionCo txsRequired: route.txsRequired, }); - const { getWalletRepo } = useManager(); - const srcAccount = useAccount("source"); const dstAccount = useAccount("destination"); + const { data: userAddresses } = useWalletAddresses(route.chainIDs); + async function onSubmit() { + if (!userAddresses) return; setOngoing(true); setIsExpanded(true); const historyId = randomId(); try { - const userAddresses: Record = {}; - - const srcChain = chains.find((c) => { - return c.chainID === route.sourceAssetChainID; - }); - const dstChain = chains.find((c) => { - return c.chainID === route.destAssetChainID; - }); - - for (const chainID of route.chainIDs) { - const chain = chains.find((c) => c.chainID === chainID); - if (!chain) { - throw new Error(`executeRoute error: cannot find chain '${chainID}'`); - } - - if (chain.chainType === "cosmos") { - const { wallets } = getWalletRepo(chain.chainName); - - const walletName = (() => { - // if `chainID` is the source or destination chain - if (srcChain?.chainID === chainID) { - return srcAccount?.wallet?.walletName; - } - if (dstChain?.chainID === chainID) { - return dstAccount?.wallet?.walletName; - } - - // if `chainID` isn't the source or destination chain - if (srcChain?.chainType === "cosmos") { - return srcAccount?.wallet?.walletName; - } - if (dstChain?.chainType === "cosmos") { - return dstAccount?.wallet?.walletName; - } - })(); - - if (!walletName) { - throw new Error(`executeRoute error: cannot find wallet for '${chain.chainName}'`); - } - - const wallet = wallets.find((w) => w.walletName === walletName); - if (!wallet) { - throw new Error(`executeRoute error: cannot find wallet for '${chain.chainName}'`); - } - if (wallet.isWalletDisconnected || !wallet.isWalletConnected) { - await wallet.connect(); - } - if (!wallet.address) { - throw new Error(`executeRoute error: cannot resolve wallet address for '${chain.chainName}'`); - } - userAddresses[chainID] = wallet.address; - } - - if (chain.chainType === "evm") { - if (!evmAddress) { - throw new Error(`executeRoute error: evm wallet not connected`); - } - userAddresses[chainID] = evmAddress; - } - } - await skipClient.executeRoute({ route, userAddresses, validateGasBalance: route.txsRequired === 1, slippageTolerancePercent: useSettingsStore.getState().slippage, + getGasPrice: getHotfixedGasPrice, onTransactionBroadcast: async (txStatus) => { const makeExplorerUrl = await getExplorerUrl(txStatus.chainID); const explorerLink = makeExplorerUrl?.(txStatus.txHash); From 1c0e1a0e811b6c4f1dc21bcd2c9e3d2bd557dc0b Mon Sep 17 00:00:00 2001 From: Griko Nibras Date: Thu, 1 Feb 2024 05:56:48 +0700 Subject: [PATCH 5/7] chore: update readme --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f1fc8f3a..80a7a4ac 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,15 @@ cp .env.example .env Make sure to set the following environment variables in `.env` file: ```bash -NEXT_PUBLIC_API_URL="https://api.skip.money" # required -NEXT_PUBLIC_CLIENT_ID= -POLKACHU_USER= # required -POLKACHU_PASSWORD= # required +NEXT_PUBLIC_API_URL="https://api.skip.money" +NEXT_PUBLIC_CLIENT_ID= # optional +POLKACHU_USER= # required +POLKACHU_PASSWORD= # required +NEXT_PUBLIC_EDGE_CONFIG= # required ``` +To retrieve `NEXT_PUBLIC_EDGE_CONFIG`, visit the [edge config token setup page](https://link.skip.money/ibc-fun-edge-config-token). + Read more on all available environment variables in [`.env.example`](.env.example) file. ## Script commands From 5785455742f7618f4e6d88b5a3803919c6a546de Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Thu, 1 Feb 2024 12:45:48 +0700 Subject: [PATCH 6/7] fix: fallback asset to prev transfer op --- src/components/RouteDisplay.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index 752ef0f1..d1526b8a 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -175,7 +175,14 @@ function TransferStep({ action, actions, id, statusData }: TransferStepProps) { const { getAsset } = useAssets(); - const asset = getAsset(action.asset, action.sourceChain); + 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 From a0d3eb45c617c20d633e2af4ce38d736778d1616 Mon Sep 17 00:00:00 2001 From: Nur Fikri Date: Thu, 1 Feb 2024 13:54:27 +0700 Subject: [PATCH 7/7] feat: undefined asset show bridge and src dest chain --- src/components/RouteDisplay.tsx | 53 ++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index d1526b8a..01b48dc0 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -191,17 +191,50 @@ function TransferStep({ action, actions, id, statusData }: TransferStepProps) { if (!asset) { return ( -
+
{renderTransferState}
-
- - Transfer to - {destinationChain.prettyName} - {destinationChain.prettyName} +
+ + Transfer + from + + {sourceChain.prettyName} + {sourceChain.prettyName} + + + + to + + {destinationChain.prettyName} + {destinationChain.prettyName} + + {bridge && ( + <> + with + + {bridge.name.toLowerCase() !== "ibc" && ( + {bridge.name} + )} + + {bridge.name} + + + )} {explorerLink && (