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,
},
);