{
diff --git a/packages/widget-v2/src/pages/SwapPage/SwapPageBridge.tsx b/packages/widget-v2/src/pages/SwapPage/SwapPageBridge.tsx
index 8690697d..1e83653f 100644
--- a/packages/widget-v2/src/pages/SwapPage/SwapPageBridge.tsx
+++ b/packages/widget-v2/src/pages/SwapPage/SwapPageBridge.tsx
@@ -10,7 +10,7 @@ export const SwapPageBridge = () => {
const [spin, setSpin] = useState(false);
const invertSwap = useSetAtom(invertSwapAtom);
const onInvertSwap = () => {
- invertSwap("swap-out");
+ invertSwap();
let spinTimeout = undefined;
clearTimeout(spinTimeout);
diff --git a/packages/widget-v2/src/pages/SwapPage/SwapPageFooter.tsx b/packages/widget-v2/src/pages/SwapPage/SwapPageFooter.tsx
index 2c4b0092..3561ae36 100644
--- a/packages/widget-v2/src/pages/SwapPage/SwapPageFooter.tsx
+++ b/packages/widget-v2/src/pages/SwapPage/SwapPageFooter.tsx
@@ -19,17 +19,33 @@ export const SwapPageFooterItems = ({
showRouteInfo,
}: SwapPageFooterItemsProps) => {
const { data: route, isLoading } = useAtomValue(skipRouteAtom);
- const estimatedTime = convertSecondsToMinutesOrHours(route?.estimatedRouteDurationSeconds);
+ const estimatedTime = convertSecondsToMinutesOrHours(
+ route?.estimatedRouteDurationSeconds
+ );
const renderRightContent = useMemo(() => {
if (showRouteInfo && route) {
return (
- {isLoading ? : <>{route?.txsRequired} tx required>}
+ {isLoading ? (
+
+ ) : (
+ <>
+
+ {route?.txsRequired} tx required
+ >
+ )}
- {isLoading ? : estimatedTime ? <>{estimatedTime}> : null}
+ {isLoading ? (
+
+ ) : estimatedTime ? (
+ <>
+
+ {estimatedTime}
+ >
+ ) : null}
);
@@ -49,18 +65,24 @@ export const SwapPageFooterItems = ({
export const SwapPageFooter = ({
onClick,
+ rightContent,
+ showRouteInfo,
...props
}: {
onClick?: () => void;
-} & SwapPageFooterItemsProps) => {
+} & SwapPageFooterItemsProps & React.ButtonHTMLAttributes
) => {
return (
-
+
);
};
diff --git a/packages/widget-v2/src/pages/SwapPage/useSetMaxAmount.ts b/packages/widget-v2/src/pages/SwapPage/useSetMaxAmount.ts
new file mode 100644
index 00000000..059ad2ad
--- /dev/null
+++ b/packages/widget-v2/src/pages/SwapPage/useSetMaxAmount.ts
@@ -0,0 +1,75 @@
+
+import { convertHumanReadableAmountToCryptoAmount, convertTokenAmountToHumanReadableAmount } from "@/utils/crypto";
+import { useGetAssetDetails } from "@/hooks/useGetAssetDetails";
+import { sourceAssetAmountAtom, sourceAssetAtom } from "@/state/swapPage";
+import { useAtom, useSetAtom } from "jotai";
+import { skipChainsAtom } from "@/state/skipClient";
+import { useSourceBalance } from "./useSourceBalance";
+import { BigNumber } from "bignumber.js";
+
+
+const ETH_GAS_FEE = 0.01;
+const COSMOS_GAS_FEE = 2_000_000;
+
+export const useGasFeeTokenAmount = () => {
+ const [sourceAsset] = useAtom(sourceAssetAtom);
+ const [{ data: chains }] = useAtom(skipChainsAtom);
+
+ const sourceDetails = useGetAssetDetails({
+ assetDenom: sourceAsset?.denom,
+ amount: sourceAsset?.amount,
+ chainId: sourceAsset?.chainID,
+ });
+
+ const feeAsset = chains?.find(chain => chain.chainID === sourceAsset?.chainID)?.feeAssets?.[0];
+
+ const chainType = sourceDetails?.chain?.chainType;
+
+ switch (chainType) {
+ case "evm":
+ return Number(convertHumanReadableAmountToCryptoAmount(ETH_GAS_FEE, sourceDetails.asset?.decimals));
+ case "cosmos":
+ if (!feeAsset?.gasPrice?.average || feeAsset.denom !== sourceAsset?.denom) return 0;
+ return Number(feeAsset.gasPrice.average) * (COSMOS_GAS_FEE);
+ case "svm":
+ default:
+ return 0;
+ }
+};
+
+export const useMaxAmountTokenMinusFees = () => {
+ const sourceBalance = useSourceBalance();
+ const gasFeeTokenAmount = useGasFeeTokenAmount();
+ const maxTokenAmount = sourceBalance?.amount;
+
+ if (gasFeeTokenAmount && maxTokenAmount) {
+ const maxTokenAmountMinusGasFees = BigNumber(maxTokenAmount).minus(gasFeeTokenAmount).toString();
+ const maxAmountMinusGasFees = convertTokenAmountToHumanReadableAmount(maxTokenAmountMinusGasFees);
+
+ return maxAmountMinusGasFees;
+ }
+};
+
+export const useSetMaxAmount = () => {
+ const maxAmountTokenMinusFees = useMaxAmountTokenMinusFees();
+ const setSourceAssetAmount = useSetAtom(sourceAssetAmountAtom);
+
+ return () => {
+ if (maxAmountTokenMinusFees) {
+ setSourceAssetAmount(maxAmountTokenMinusFees);
+ }
+ };
+};
+
+export const useInsufficientSourceBalance = () => {
+ const maxAmountTokenMinusFees = useMaxAmountTokenMinusFees();
+ const [sourceAsset] = useAtom(sourceAssetAtom);
+
+ if (!maxAmountTokenMinusFees || !sourceAsset?.amount) return true;
+
+ if (BigNumber(maxAmountTokenMinusFees).isGreaterThanOrEqualTo(BigNumber(sourceAsset?.amount))) {
+ return false;
+ }
+
+ return true;
+};
\ No newline at end of file
diff --git a/packages/widget-v2/src/pages/SwapPage/useSourceBalance.ts b/packages/widget-v2/src/pages/SwapPage/useSourceBalance.ts
new file mode 100644
index 00000000..4dbf0886
--- /dev/null
+++ b/packages/widget-v2/src/pages/SwapPage/useSourceBalance.ts
@@ -0,0 +1,16 @@
+import { useAccount } from "@/hooks/useAccount";
+import { skipBalancesAtom } from "@/state/skipClient";
+import { sourceAssetAtom } from "@/state/swapPage";
+import { useAtom, useAtomValue } from "jotai";
+
+export const useSourceBalance = () => {
+ const [sourceAsset] = useAtom(sourceAssetAtom);
+ const sourceAccount = useAccount(sourceAsset?.chainID);
+ const { data: skipBalances } = useAtomValue(skipBalancesAtom);
+
+ if (!sourceAsset || !sourceAccount || !skipBalances) return;
+ const { chainID, denom } = sourceAsset;
+ if (!denom || !chainID) return;
+
+ return skipBalances?.chains?.[chainID]?.denoms?.[denom];
+};
diff --git a/packages/widget-v2/src/state/skipClient.ts b/packages/widget-v2/src/state/skipClient.ts
index ba0726a0..fdb8651f 100644
--- a/packages/widget-v2/src/state/skipClient.ts
+++ b/packages/widget-v2/src/state/skipClient.ts
@@ -5,9 +5,10 @@ import {
Chain,
RouteRequest,
SkipClientOptions,
+ BalanceRequest,
} from "@skip-go/client";
import { atomWithQuery } from "jotai-tanstack-query";
-import { apiURL, endpointOptions } from "@/constants/skipClientDefault";
+import { devApiUrl, endpointOptions } from "@/constants/skipClientDefault";
import {
debouncedDestinationAssetAmountAtom,
debouncedSourceAssetAmountAtom,
@@ -17,7 +18,8 @@ import {
sourceAssetAtom,
swapDirectionAtom,
} from "./swapPage";
-import { getAmountWei } from "@/utils/number";
+import { currentPageAtom, Routes } from "./router";
+import { convertHumanReadableAmountToCryptoAmount } from "@/utils/crypto";
import { walletsAtom } from "./wallets";
import { getWallet, WalletType } from "graz";
import { getWalletClient } from "@wagmi/core";
@@ -25,12 +27,15 @@ import { config } from "@/constants/wagmi";
import { WalletClient } from "viem";
import { solanaWallets } from "@/constants/solana";
import { Adapter } from "@solana/wallet-adapter-base";
+import { defaultTheme, Theme } from "@/widget/theme";
export const skipClientConfigAtom = atom({
- apiURL,
+ apiURL: devApiUrl,
endpointOptions,
});
+export const themeAtom = atom(defaultTheme);
+
export const skipClient = atom((get) => {
const options = get(skipClientConfigAtom);
const wallets = get(walletsAtom);
@@ -142,10 +147,29 @@ export const skipSwapVenuesAtom = atomWithQuery((get) => {
};
});
+export const skipBalancesRequestAtom = atom();
+
+export const skipBalancesAtom = atomWithQuery((get) => {
+ const skip = get(skipClient);
+ const params = get(skipBalancesRequestAtom);
+
+ return {
+ queryKey: ["skipBalances", params],
+ queryFn: async () => {
+ if (!params) {
+ throw new Error("No balance request provided");
+ }
+
+ return skip.balances(params);
+ },
+ retry: 1,
+ };
+});
+
type SkipTransactionStatusProps = {
txsRequired: number;
txs: { chainID: string; txHash: string }[] | undefined;
-}
+};
export const skipTransactionStatusPropsAtom = atom({
txsRequired: 0,
@@ -181,14 +205,12 @@ const skipRouteRequestAtom = atom((get) => {
const direction = get(swapDirectionAtom);
const sourceAssetAmount = get(debouncedSourceAssetAmountAtom);
const destinationAssetAmount = get(debouncedDestinationAssetAmountAtom);
- const isInvertingSwap = get(isInvertingSwapAtom);
if (
!sourceAsset?.chainID ||
!sourceAsset.denom ||
!destinationAsset?.chainID ||
- !destinationAsset.denom ||
- isInvertingSwap
+ !destinationAsset.denom
) {
return undefined;
}
@@ -196,12 +218,11 @@ const skipRouteRequestAtom = atom((get) => {
direction === "swap-in"
? {
amountIn:
- getAmountWei(sourceAssetAmount, sourceAsset.decimals) || "0",
+ convertHumanReadableAmountToCryptoAmount(sourceAssetAmount ?? "0", sourceAsset.decimals),
}
: {
amountOut:
- getAmountWei(destinationAssetAmount, destinationAsset.decimals) ||
- "0",
+ convertHumanReadableAmountToCryptoAmount(destinationAssetAmount ?? "0", destinationAsset.decimals),
};
return {
@@ -216,9 +237,17 @@ const skipRouteRequestAtom = atom((get) => {
export const skipRouteAtom = atomWithQuery((get) => {
const skip = get(skipClient);
const params = get(skipRouteRequestAtom);
+ const currentPage = get(currentPageAtom);
+ const isInvertingSwap = get(isInvertingSwapAtom);
get(routeAmountEffect);
+ const queryEnabled =
+ params !== undefined &&
+ (Number(params.amountIn) > 0 || Number(params.amountOut) > 0) &&
+ !isInvertingSwap &&
+ currentPage === Routes.SwapPage;
+
return {
queryKey: ["skipRoute", params],
queryFn: async () => {
@@ -238,8 +267,7 @@ export const skipRouteAtom = atomWithQuery((get) => {
});
},
retry: 1,
- enabled:
- !!params && (Number(params.amountIn) > 0 || Number(params.amountOut) > 0),
+ enabled: queryEnabled,
refetchInterval: 1000 * 30,
};
});
diff --git a/packages/widget-v2/src/state/swapPage.ts b/packages/widget-v2/src/state/swapPage.ts
index a0286d68..2c145f15 100644
--- a/packages/widget-v2/src/state/swapPage.ts
+++ b/packages/widget-v2/src/state/swapPage.ts
@@ -1,9 +1,9 @@
import { atom } from "jotai";
import { ClientAsset, skipRouteAtom } from "./skipClient";
import { atomEffect } from "jotai-effect";
-import { parseAmountWei } from "@/utils/number";
import { atomWithDebounce } from "@/utils/atomWithDebounce";
import { MinimalWallet } from "./wallets";
+import { convertTokenAmountToHumanReadableAmount } from "@/utils/crypto";
export type AssetAtom = Partial & {
amount?: string;
@@ -11,15 +11,11 @@ export type AssetAtom = Partial & {
const ROUTE_REQUEST_DEBOUNCE_DELAY = 500;
-export const { debouncedValueAtom: debouncedSourceAssetAmountAtom } = atomWithDebounce(
- undefined,
- ROUTE_REQUEST_DEBOUNCE_DELAY,
-);
+export const { debouncedValueAtom: debouncedSourceAssetAmountAtom } =
+ atomWithDebounce(undefined, ROUTE_REQUEST_DEBOUNCE_DELAY);
-export const { debouncedValueAtom: debouncedDestinationAssetAmountAtom } = atomWithDebounce(
- undefined,
- ROUTE_REQUEST_DEBOUNCE_DELAY,
-);
+export const { debouncedValueAtom: debouncedDestinationAssetAmountAtom } =
+ atomWithDebounce(undefined, ROUTE_REQUEST_DEBOUNCE_DELAY);
export const sourceAssetAtom = atom();
@@ -29,51 +25,51 @@ export const sourceAssetAmountAtom = atom(
const oldSourceAsset = get(sourceAssetAtom);
set(sourceAssetAtom, { ...oldSourceAsset, amount: newAmount });
set(debouncedSourceAssetAmountAtom, newAmount);
- },
+ set(swapDirectionAtom, "swap-in");
+ }
);
export const destinationAssetAtom = atom();
-export const isWaitingForNewRouteAtom = atom(
- (get) => {
- const sourceAmount = get(sourceAssetAmountAtom);
- const destinationAmount = get(destinationAssetAmountAtom);
- const debouncedSourceAmount = get(debouncedSourceAssetAmountAtom);
- const debouncedDestinationAmount = get(debouncedDestinationAssetAmountAtom);
-
- const { isLoading } = get(skipRouteAtom);
- const direction = get(swapDirectionAtom);
-
- if (direction === "swap-in") {
- return (sourceAmount !== debouncedSourceAmount) || isLoading;
- } else if (direction === "swap-out") {
- return (destinationAmount !== debouncedDestinationAmount) || isLoading;
- }
- }
-);
-
export const destinationAssetAmountAtom = atom(
(get) => get(destinationAssetAtom)?.amount,
- (get, set, newAmount: string) => {
+ (get, set, newAmount: string, callback?: () => void) => {
const oldDestinationAsset = get(destinationAssetAtom);
set(destinationAssetAtom, { ...oldDestinationAsset, amount: newAmount });
set(debouncedDestinationAssetAmountAtom, newAmount);
- },
+ set(swapDirectionAtom, "swap-out");
+ callback?.();
+ }
);
+export const isWaitingForNewRouteAtom = atom((get) => {
+ const sourceAmount = get(sourceAssetAmountAtom);
+ const destinationAmount = get(destinationAssetAmountAtom);
+ const debouncedSourceAmount = get(debouncedSourceAssetAmountAtom);
+ const debouncedDestinationAmount = get(debouncedDestinationAssetAmountAtom);
+
+ const { isLoading } = get(skipRouteAtom);
+ const direction = get(swapDirectionAtom);
+
+ if (direction === "swap-in") {
+ return sourceAmount !== debouncedSourceAmount || isLoading;
+ } else if (direction === "swap-out") {
+ return destinationAmount !== debouncedDestinationAmount || isLoading;
+ }
+});
+
export type SwapDirection = "swap-in" | "swap-out";
export const swapDirectionAtom = atom("swap-in");
export const isInvertingSwapAtom = atom(false);
-export const invertSwapAtom = atom(null, (get, set, swapDirection: SwapDirection) => {
+export const invertSwapAtom = atom(null, (get, set) => {
const sourceAsset = get(sourceAssetAtom);
const destinationAsset = get(destinationAssetAtom);
+ const swapDirection = get(swapDirectionAtom);
set(isInvertingSwapAtom, true);
- set(swapDirectionAtom, swapDirection);
-
set(sourceAssetAtom, destinationAsset);
if (destinationAsset?.amount) {
set(sourceAssetAmountAtom, destinationAsset?.amount);
@@ -81,10 +77,13 @@ export const invertSwapAtom = atom(null, (get, set, swapDirection: SwapDirection
set(destinationAssetAtom, sourceAsset);
if (sourceAsset?.amount) {
- set(destinationAssetAmountAtom, sourceAsset?.amount);
+ set(destinationAssetAmountAtom, sourceAsset?.amount, () => {
+ const newSwapDirection =
+ swapDirection === "swap-in" ? "swap-out" : "swap-in";
+ set(swapDirectionAtom, newSwapDirection);
+ set(isInvertingSwapAtom, false);
+ });
}
-
- set(isInvertingSwapAtom, false);
});
export const connectedWalletAtom = atom();
@@ -99,8 +98,14 @@ export const routeAmountEffect = atomEffect((get, set) => {
if (!route.data || !sourceAsset || !destinationAsset) return;
- const swapInAmount = parseAmountWei(route.data.amountOut, destinationAsset.decimals);
- const swapOutAmount = parseAmountWei(route.data.amountIn, sourceAsset.decimals);
+ const swapInAmount = convertTokenAmountToHumanReadableAmount(
+ route.data.amountOut,
+ destinationAsset.decimals
+ );
+ const swapOutAmount = convertTokenAmountToHumanReadableAmount(
+ route.data.amountIn,
+ sourceAsset.decimals
+ );
const swapInAmountChanged = swapInAmount !== destinationAsset.amount;
const swapOutAmountChanged = swapOutAmount !== sourceAsset.amount;
diff --git a/packages/widget-v2/src/stories/Widget.stories.tsx b/packages/widget-v2/src/stories/Widget.stories.tsx
index a6f604fd..4e52d985 100644
--- a/packages/widget-v2/src/stories/Widget.stories.tsx
+++ b/packages/widget-v2/src/stories/Widget.stories.tsx
@@ -3,7 +3,7 @@ import { ShowSwapWidget, SwapWidget, SwapWidgetProps } from "@/widget/Widget";
import { defaultTheme, lightTheme, Theme } from "@/widget/theme";
import NiceModal from "@ebay/nice-modal-react";
import { styled } from "styled-components";
-import { ReactElement, useEffect } from "react";
+import { ReactElement, useEffect, useMemo } from "react";
import { destinationAssetAtom, sourceAssetAmountAtom, sourceAssetAtom } from "@/state/swapPage";
import { useAtom, useSetAtom } from "jotai";
import { skipAssetsAtom } from "@/state/skipClient";
@@ -29,13 +29,15 @@ export const Widget = (props: Props) => {
setSourceAssetAmount("1");
}, [destinationAsset, setDestinationAsset, setSourceAsset, setSourceAssetAmount, sourceAsset]);
- return ;
+ return useMemo(() => {
+ return ;
+ }, [props]);
};
export const Modal = (props: Props) => {
return (
-
+
);
};
diff --git a/packages/widget-v2/src/utils/number.ts b/packages/widget-v2/src/utils/number.ts
index abaf6040..e0ff00a5 100644
--- a/packages/widget-v2/src/utils/number.ts
+++ b/packages/widget-v2/src/utils/number.ts
@@ -1,5 +1,4 @@
import { BigNumber } from "bignumber.js";
-import { formatUnits } from "viem";
export function formatNumberWithCommas(str: string | number) {
const parts = str.toString().split(".");
@@ -11,24 +10,6 @@ export function formatNumberWithoutCommas(str: string | number) {
return str.toString().replace(/,/g, "");
}
-export function getAmountWei(amount?: string, decimals = 6) {
- if (!amount || !amount) return "0";
- try {
- return new BigNumber(formatNumberWithoutCommas(amount)).shiftedBy(decimals ?? 6).toFixed(0);
- } catch (_err) {
- return "0";
- }
-}
-
-export function parseAmountWei(amount?: string, decimals = 6) {
- if (!amount) return "0";
- try {
- return formatUnits(BigInt(amount.replace(/,/g, "")), decimals ?? 6);
- } catch (_err) {
- return "0";
- }
-}
-
export function calculatePercentageDifference(numberA: number | string, numberB: number | string, absoluteValue?: boolean) {
const bigNumberA = BigNumber(numberA);
const bigNumberB = BigNumber(numberB);
diff --git a/packages/widget-v2/src/widget/Widget.tsx b/packages/widget-v2/src/widget/Widget.tsx
index 3cd91e97..f6365c67 100644
--- a/packages/widget-v2/src/widget/Widget.tsx
+++ b/packages/widget-v2/src/widget/Widget.tsx
@@ -3,12 +3,12 @@ import NiceModal from "@ebay/nice-modal-react";
import { styled } from "styled-components";
import { createModal, useModal } from "@/components/Modal";
import { cloneElement, ReactElement, useEffect } from "react";
-import { PartialTheme } from "./theme";
+import { defaultTheme, PartialTheme } from "./theme";
import { Router } from "./Router";
import { useResetAtom } from "jotai/utils";
import { numberOfModalsOpenAtom } from "@/state/modal";
import { useSetAtom } from "jotai";
-import { skipClientConfigAtom } from "@/state/skipClient";
+import { skipClientConfigAtom, themeAtom } from "@/state/skipClient";
import { SkipClientOptions } from "@skip-go/client";
export type SwapWidgetProps = {
@@ -17,9 +17,11 @@ export type SwapWidgetProps = {
export const SwapWidget = ({ theme, ...skipClientConfig }: SwapWidgetProps) => {
const setSkipClientConfig = useSetAtom(skipClientConfigAtom);
+ const setTheme = useSetAtom(themeAtom);
useEffect(() => {
setSkipClientConfig(skipClientConfig);
- }, [setSkipClientConfig, skipClientConfig]);
+ setTheme({ ...defaultTheme, ...theme });
+ }, [setSkipClientConfig, setTheme, skipClientConfig, theme]);
return (
@@ -34,9 +36,11 @@ export const SwapWidget = ({ theme, ...skipClientConfig }: SwapWidgetProps) => {
const SwapWidgetWithoutNiceModalProvider = ({ theme, ...skipClientConfig }: SwapWidgetProps) => {
const setSkipClientConfig = useSetAtom(skipClientConfigAtom);
+ const setTheme = useSetAtom(themeAtom);
useEffect(() => {
setSkipClientConfig(skipClientConfig);
- }, [setSkipClientConfig, skipClientConfig]);
+ setTheme({ ...defaultTheme, ...theme });
+ }, [setSkipClientConfig, setTheme, skipClientConfig, theme]);
return (