diff --git a/package-lock.json b/package-lock.json index b12316ed..19d64289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "@radix-ui/react-tooltip": "^1.0.7", "@react-stately/table": "^3.11.4", "@sentry/nextjs": "^7.94.1", - "@skip-router/core": "1.2.3", + "@skip-router/core": "1.2.7", "@tailwindcss/forms": "^0.5.7", "@tanstack/query-sync-storage-persister": "^5.17.19", "@tanstack/react-query": "^5.17.19", @@ -7860,9 +7860,9 @@ } }, "node_modules/@skip-router/core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@skip-router/core/-/core-1.2.3.tgz", - "integrity": "sha512-byH0lsex+9P3kGavqNxqaLuASeAX4objs/EEsNUGNGMr+7YpwxPrXlgvJ8Xk6u+XmBQplpgsYGh/EjfIZnqHSg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@skip-router/core/-/core-1.2.7.tgz", + "integrity": "sha512-Zbz7wH57yJPU2hJ3DICnCDDxFYYqWq9aF4yCTAOTMdOA6HPx6EJqiO2xAaSsN6OwtkTazNqUCkZ0MS41r0Kyjw==", "dependencies": { "@axelar-network/axelarjs-sdk": "^0.13.6", "@cosmjs/amino": "^0.31.1", @@ -7875,7 +7875,7 @@ "@injectivelabs/core-proto-ts": "^0.0.18", "@injectivelabs/sdk-ts": "^1.12.1", "axios": "^1.4.0", - "chain-registry": "^1.19.0", + "chain-registry": "^1.25.4", "cosmjs-types": "^0.8.0", "faker": "^6.6.6", "keccak256": "^1.0.6", @@ -8809,9 +8809,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.11.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", - "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "version": "20.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.6.tgz", + "integrity": "sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q==", "dependencies": { "undici-types": "~5.26.4" } @@ -23371,9 +23371,9 @@ } }, "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.0.tgz", - "integrity": "sha512-NPaKJsb4wyJ16qc8zBQrWswLKv/YirgBFykvUQ1Iajt2wd+twC8E4hFXdlIXqiMl6kWA0zY8tUJ9ELVAdu5h7w==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.1.tgz", + "integrity": "sha512-7ZnJYTp6uc04uYRISWtiX3DSKB/fxNQT0B5o1OUeCqiQiwF+JC9+rJiZIDrPrNCLLuTqyQmh4VdQqh/ZOkv9MQ==", "dev": true, "engines": { "node": ">=16" diff --git a/package.json b/package.json index 95ec612b..0c094818 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@react-stately/table": "^3.11.4", - "@skip-router/core": "1.2.3", + "@skip-router/core": "1.2.7", "@sentry/nextjs": "^7.94.1", "@tailwindcss/forms": "^0.5.7", "@tanstack/query-sync-storage-persister": "^5.17.19", diff --git a/src/components/AssetInput.tsx b/src/components/AssetInput.tsx index 731e09ce..6cdf57a3 100644 --- a/src/components/AssetInput.tsx +++ b/src/components/AssetInput.tsx @@ -133,7 +133,9 @@ function AssetInput({ if (!onAmountChange) return; if (event.key === "Escape") { - onAmountChange?.(""); + if (event.currentTarget.selectionStart === event.currentTarget.selectionEnd) { + event.currentTarget.select(); + } return; } diff --git a/src/components/RouteDisplay.tsx b/src/components/RouteDisplay.tsx index d3db8eb4..29a501b9 100644 --- a/src/components/RouteDisplay.tsx +++ b/src/components/RouteDisplay.tsx @@ -456,7 +456,12 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT return; } - const sourceChain = operation.transfer.chainID; + let sourceChain = ""; + if ("cctpTransfer" in operation) { + sourceChain = operation.cctpTransfer.fromChainID; + } else { + sourceChain = operation.transfer.chainID; + } let destinationChain = ""; if (i === route.operations.length - 1) { @@ -473,6 +478,8 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT } } else if ("axelarTransfer" in nextOperation) { destinationChain = nextOperation.axelarTransfer.toChainID; + } else if ("cctpTransfer" in nextOperation) { + destinationChain = nextOperation.cctpTransfer.toChainID; } else { destinationChain = nextOperation.transfer.chainID; } @@ -486,7 +493,11 @@ function RouteDisplay({ route, isRouteExpanded, setIsRouteExpanded, broadcastedT id: `transfer-${transferCount}-${i}`, }); - asset = operation.transfer.destDenom; + if ("cctpTransfer" in operation) { + asset = operation.cctpTransfer.burnToken; + } else { + asset = operation.transfer.destDenom; + } transferCount++; }); diff --git a/src/components/SettingsDialog/GasSetting.tsx b/src/components/SettingsDialog/GasSetting.tsx index 9a2ebb77..d6d0549d 100644 --- a/src/components/SettingsDialog/GasSetting.tsx +++ b/src/components/SettingsDialog/GasSetting.tsx @@ -1,3 +1,5 @@ +import { BigNumber } from "bignumber.js"; + import { useSettingsStore } from "@/context/settings"; import { formatNumberWithCommas, formatNumberWithoutCommas } from "@/utils/number"; @@ -6,7 +8,7 @@ export const GasSetting = () => { return (
-

Gas Multiplier

+

Gas Amount

@@ -25,6 +27,36 @@ export const GasSetting = () => { const value = Math.max(0, +latest); useSettingsStore.setState({ gasAmount: value.toString() }); }} + onKeyDown={(event) => { + if (event.key === "Escape") { + event.currentTarget.select(); + return; + } + + let value = BigNumber(formatNumberWithoutCommas(event.currentTarget.value) || "0"); + + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault(); + if (event.key === "ArrowUp") { + if (event.shiftKey) { + value = value.plus(1_000); + } else { + value = value.plus(1); + } + } + if (event.key === "ArrowDown") { + if (event.shiftKey) { + value = value.minus(1_000); + } else { + value = value.minus(1); + } + } + if (value.isNegative()) { + value = BigNumber(0); + } + useSettingsStore.setState({ gasAmount: value.toString() }); + } + }} />
diff --git a/src/components/SettingsDialog/SlippageSetting.tsx b/src/components/SettingsDialog/SlippageSetting.tsx index d454894a..c5c3b61a 100644 --- a/src/components/SettingsDialog/SlippageSetting.tsx +++ b/src/components/SettingsDialog/SlippageSetting.tsx @@ -1,7 +1,8 @@ +import { BigNumber } from "bignumber.js"; import { clsx } from "clsx"; import { useSettingsStore } from "@/context/settings"; - +import { formatNumberWithCommas, formatNumberWithoutCommas } from "@/utils/number"; const OPTION_VALUES = ["1", "3", "5"]; export const SlippageSetting = () => { @@ -18,13 +19,56 @@ export const SlippageSetting = () => { "rounded-lg border px-2 py-1 text-end tabular-nums transition", "w-full pe-5 number-input-arrows-hide", )} - type="number" - value={currentValue} - min={0} - max={100} + type="text" + inputMode="numeric" + value={formatNumberWithCommas(currentValue)} onChange={(event) => { - const value = Math.max(0, Math.min(100, +event.target.value)); - useSettingsStore.setState({ slippage: value.toString() }); + let latest = event.target.value; + + if (latest.match(/^[.,]/)) latest = `0.${latest}`; // Handle first character being a period or comma + latest = latest.replace(/^[0]{2,}/, "0"); // Remove leading zeros + latest = latest.replace(/[^\d.,]/g, ""); // Remove non-numeric and non-decimal characters + latest = latest.replace(/[.]{2,}/g, "."); // Remove multiple decimals + latest = latest.replace(/[,]{2,}/g, ","); // Remove multiple commas + + if (!latest.endsWith(".")) { + latest = Math.max(0, Math.min(100, +formatNumberWithoutCommas(latest))).toString(); + } + useSettingsStore.setState({ slippage: latest }); + }} + onKeyDown={(event) => { + if (event.key === "Escape") { + event.currentTarget.select(); + return; + } + + let value = BigNumber(formatNumberWithoutCommas(event.currentTarget.value) || "0"); + + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + event.preventDefault(); + if (event.key === "ArrowUp") { + if (event.shiftKey) { + value = value.plus(10); + } else if (event.altKey || event.ctrlKey || event.metaKey || value.lt(1)) { + value = value.plus(0.1); + } else { + value = value.plus(1); + } + } + if (event.key === "ArrowDown") { + if (event.shiftKey) { + value = value.minus(10); + } else if (event.altKey || event.ctrlKey || event.metaKey || value.lte(1)) { + value = value.minus(0.1); + } else { + value = value.minus(1); + } + } + if (value.isNegative()) { + value = BigNumber(0); + } + useSettingsStore.setState({ slippage: value.toString() }); + } }} />
%
diff --git a/src/components/SwapWidget/SwapWidget.tsx b/src/components/SwapWidget/SwapWidget.tsx index bf577f3a..f1244331 100644 --- a/src/components/SwapWidget/SwapWidget.tsx +++ b/src/components/SwapWidget/SwapWidget.tsx @@ -5,7 +5,6 @@ import { ElementRef, useEffect, useRef } from "react"; import type {} from "typed-query-selector"; import { disclosure } from "@/context/disclosures"; -import { useSettingsStore } from "@/context/settings"; import { useAccount } from "@/hooks/useAccount"; import { useChains as useSkipChains } from "@/hooks/useChains"; @@ -50,6 +49,7 @@ export function SwapWidget() { onSourceAmountChange, onSourceAmountMax, onInvertDirection, + onAllTransactionComplete, priceImpactThresholdReached, route, routeError, @@ -78,13 +78,6 @@ export function SwapWidget() { useEffect(() => { document.querySelector("[data-testid='source'] input")?.focus(); - return useSettingsStore.subscribe((state) => { - if (+state.slippage < 0 || +state.slippage > 100) { - useSettingsStore.setState({ - slippage: Math.max(0, Math.min(100, +state.slippage)).toString(), - }); - } - }); }, []); const invertButtonRef = useRef>(null); @@ -283,6 +276,7 @@ export function SwapWidget() { shouldShowPriceImpactWarning={!!routeWarningTitle && !!routeWarningMessage} routeWarningTitle={routeWarningTitle} routeWarningMessage={routeWarningMessage} + onAllTransactionComplete={onAllTransactionComplete} />
)} diff --git a/src/components/SwapWidget/useSwapWidget.ts b/src/components/SwapWidget/useSwapWidget.ts index 02d7c956..31dc789f 100644 --- a/src/components/SwapWidget/useSwapWidget.ts +++ b/src/components/SwapWidget/useSwapWidget.ts @@ -12,6 +12,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 { AssetWithMetadata, useAssets } from "@/context/assets"; import { useAnyDisclosureOpen } from "@/context/disclosures"; import { useSettingsStore } from "@/context/settings"; @@ -134,12 +135,17 @@ export function useSwapWidget() { if (srcFeeAsset) { const parsedFeeBalance = BigNumber(balances[srcFeeAsset.denom] ?? "0").shiftedBy(-(srcFeeAsset.decimals ?? 6)); - if (parsedFeeBalance.lt(gasRequired || "0")) { + const parsedGasRequired = BigNumber(gasRequired || "0"); + if ( + srcFeeAsset.denom === srcAsset.denom + ? parsedAmount.isGreaterThan(parsedBalance.minus(parsedGasRequired)) + : parsedFeeBalance.minus(parsedGasRequired).isLessThanOrEqualTo(0) + ) { return `Insufficient balance. You need ≈${gasRequired} ${srcFeeAsset.recommendedSymbol} to accomodate gas fees.`; } } - if (parsedBalance.lt(parsedAmount)) { + if (parsedAmount.isGreaterThan(parsedBalance)) { return `Insufficient balance.`; } @@ -357,7 +363,10 @@ export function useSwapWidget() { * (would be impossible since max button is disabled if no balance) */ if (!balance) { - useSwapWidgetStore.setState({ amountIn: "0" }); + useSwapWidgetStore.setState({ + amountIn: "0", + direction: "swap-in", + }); return; } @@ -372,7 +381,10 @@ export function useSwapWidget() { */ if (event.shiftKey || isDifferentAsset || isNotCosmos) { const newAmountIn = formatUnits(balance, decimals); - useSwapWidgetStore.setState({ amountIn: newAmountIn }); + useSwapWidgetStore.setState({ + amountIn: newAmountIn, + direction: "swap-in", + }); return; } @@ -382,17 +394,34 @@ export function useSwapWidget() { if (gasRequired && srcFeeAsset && srcFeeAsset.denom === srcAsset.denom) { let newAmountIn = BigNumber(balance).shiftedBy(-decimals).minus(gasRequired); newAmountIn = newAmountIn.isNegative() ? BigNumber(0) : newAmountIn; - useSwapWidgetStore.setState({ amountIn: newAmountIn.toFixed(decimals) }); + useSwapWidgetStore.setState({ + amountIn: newAmountIn.toFixed(decimals), + direction: "swap-in", + }); return; } // otherwise, max balance const newAmountIn = formatUnits(balance, decimals); - useSwapWidgetStore.setState({ amountIn: newAmountIn }); + useSwapWidgetStore.setState({ + amountIn: newAmountIn, + direction: "swap-in", + }); }, [balances, gasRequired, srcAsset, srcChain, srcFeeAsset], ); + /** + * Handle clearing amount values when all transactions are complete + */ + const onAllTransactionComplete = useCallback(() => { + useSwapWidgetStore.setState({ + amountIn: "", + amountOut: "", + direction: "swap-in", + }); + }, []); + // #endregion ///////////////////////////////////////////////////////////////////////////// @@ -431,10 +460,11 @@ export function useSwapWidget() { } const decimals = srcFeeAsset.decimals ?? 6; + const actualGasAmount = isChainIdEvmos(srcChain.chainID) ? EVMOS_GAS_AMOUNT : gasAmount; useSwapWidgetStore.setState({ gasRequired: BigNumber(feeDenomPrices.gasPrice.average) - .multipliedBy(gasAmount) + .multipliedBy(actualGasAmount) .shiftedBy(-decimals) .toString(), sourceFeeAsset: srcFeeAsset, @@ -702,6 +732,7 @@ export function useSwapWidget() { onSourceAmountChange, onInvertDirection, onSourceAmountMax, + onAllTransactionComplete, priceImpactThresholdReached, route, routeError: errorMessage, @@ -744,16 +775,8 @@ const useSwapWidgetStore = create( subscribeWithSelector( persist(() => defaultValues, { name: "SwapWidgetState", - version: 1, + version: 2, storage: createJSONStorage(() => window.sessionStorage), - partialize: (state): Partial => ({ - amountIn: state.amountIn, - amountOut: state.amountOut, - sourceChain: state.sourceChain, - sourceAsset: state.sourceAsset, - destinationChain: state.destinationChain, - destinationAsset: state.destinationAsset, - }), skipHydration: true, }), ), diff --git a/src/components/TransactionDialog/TransactionDialogContent.tsx b/src/components/TransactionDialog/TransactionDialogContent.tsx index 8c7cbea2..416c6b5c 100644 --- a/src/components/TransactionDialog/TransactionDialogContent.tsx +++ b/src/components/TransactionDialog/TransactionDialogContent.tsx @@ -32,6 +32,7 @@ interface Props { transactionCount: number; isAmountError?: boolean | string; onClose: () => void; + onAllTransactionComplete?: () => void; } export interface BroadcastedTx { @@ -40,7 +41,13 @@ export interface BroadcastedTx { explorerLink: string; } -function TransactionDialogContent({ route, onClose, isAmountError, transactionCount }: Props) { +function TransactionDialogContent({ + route, + onClose, + isAmountError, + transactionCount, + onAllTransactionComplete, +}: Props) { const { data: chains = [] } = useChains(); const skipClient = useSkipClient(); @@ -210,6 +217,7 @@ function TransactionDialogContent({ route, onClose, isAmountError, transactionCo historyId && txHistory.success(historyId); setTxComplete(true); + onAllTransactionComplete?.(); } catch (err: unknown) { if (process.env.NODE_ENV === "development") { console.error(err); diff --git a/src/components/TransactionDialog/index.tsx b/src/components/TransactionDialog/index.tsx index 3eb8c58e..3df6052d 100644 --- a/src/components/TransactionDialog/index.tsx +++ b/src/components/TransactionDialog/index.tsx @@ -17,6 +17,7 @@ interface Props { shouldShowPriceImpactWarning?: boolean; routeWarningMessage?: string; routeWarningTitle?: string; + onAllTransactionComplete?: () => void; } function TransactionDialog({ @@ -27,6 +28,7 @@ function TransactionDialog({ shouldShowPriceImpactWarning, routeWarningMessage, routeWarningTitle, + onAllTransactionComplete, }: Props) { const [hasDisplayedWarning, setHasDisplayedWarning] = useState(false); const [isOpen, { set: setIsOpen }] = useDisclosureKey("confirmSwapDialog"); @@ -71,6 +73,7 @@ function TransactionDialog({ onClose={() => setIsOpen(false)} isAmountError={isAmountError} transactionCount={transactionCount} + onAllTransactionComplete={onAllTransactionComplete} /> )}
diff --git a/src/components/TransactionSuccessView.tsx b/src/components/TransactionSuccessView.tsx index 325c9eb8..9b5f77ca 100644 --- a/src/components/TransactionSuccessView.tsx +++ b/src/components/TransactionSuccessView.tsx @@ -90,7 +90,7 @@ const TransactionSuccessView: FC<{ className="w-full rounded-md bg-[#FF486E] py-4 font-semibold text-white outline-none transition-transform enabled:hover:rotate-1 enabled:hover:scale-105 disabled:cursor-not-allowed disabled:opacity-75" onClick={onClose} > - {route.doesSwap ? "Swap" : "Transfer"} Again + Create New {route.doesSwap ? "Swap" : "Transfer"} diff --git a/src/constants/finality.ts b/src/constants/finality.ts index 00312980..15a1fd0e 100644 --- a/src/constants/finality.ts +++ b/src/constants/finality.ts @@ -32,6 +32,6 @@ const finalityTimeMap: Record = { }; /** @see https://docs.axelar.dev/learn/txduration#common-finality-time-for-interchain-transactions */ -export const getFinalityTime = (id: string | number) => { +export function getFinalityTime(id: string | number) { return finalityTimeMap[`${id}`] || "~30 minutes"; -}; +} diff --git a/src/constants/gas.ts b/src/constants/gas.ts new file mode 100644 index 00000000..5eb36a19 --- /dev/null +++ b/src/constants/gas.ts @@ -0,0 +1,7 @@ +export const DEFAULT_GAS_AMOUNT = (200_000).toString(); + +export const EVMOS_GAS_AMOUNT = (280_000).toString(); + +export function isChainIdEvmos(chainID: string) { + return chainID === "evmos_9001-2" || chainID.includes("evmos"); +} diff --git a/src/context/settings.ts b/src/context/settings.ts index 5123442e..c95b52c3 100644 --- a/src/context/settings.ts +++ b/src/context/settings.ts @@ -1,13 +1,15 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { DEFAULT_GAS_AMOUNT } from "@/constants/gas"; + interface SettingsStore { gasAmount: string; slippage: string; } export const defaultValues: SettingsStore = { - gasAmount: (200_000).toString(), + gasAmount: DEFAULT_GAS_AMOUNT, slippage: (3).toString(), }; diff --git a/src/solve/queries.ts b/src/solve/queries.ts index 34bddbd6..634641d5 100644 --- a/src/solve/queries.ts +++ b/src/solve/queries.ts @@ -102,6 +102,7 @@ export function useRoute({ destAssetDenom: destinationAsset, destAssetChainID: destinationAssetChainID, swapVenue, + allowUnsafe: true, // experimentalFeatures, } : { @@ -111,6 +112,7 @@ export function useRoute({ destAssetDenom: destinationAsset, destAssetChainID: destinationAssetChainID, swapVenue, + allowUnsafe: true, // experimentalFeatures, }, );