Skip to content

Commit

Permalink
Merge pull request #130 from skip-mev/jw/price-warning
Browse files Browse the repository at this point in the history
  • Loading branch information
bpiv400 authored Dec 28, 2023
2 parents 649618f + 38d064e commit 41fcac7
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 42 deletions.
2 changes: 1 addition & 1 deletion chain-registry
Submodule chain-registry updated 82 files
+31 −0 _IBC/andromeda-kujira.json
+31 −0 _IBC/andromeda-secretnetwork.json
+31 −0 _IBC/andromeda-terra2.json
+31 −0 _IBC/axelar-fxcore.json
+31 −0 _IBC/bandchain-coreum.json
+31 −0 _IBC/comdex-kujira.json
+31 −0 _IBC/composable-cosmoshub.json
+32 −0 _IBC/coreum-doravota.json
+31 −0 _IBC/coreum-dydx.json
+31 −0 _IBC/coreum-kava.json
+31 −0 _IBC/coreum-kujira.json
+31 −0 _IBC/coreum-noble.json
+31 −0 _IBC/coreum-secretnetwork.json
+31 −0 _IBC/coreum-sei.json
+32 −0 _IBC/cosmoshub-doravota.json
+32 −0 _IBC/doravota-nolus.json
+31 −0 _IBC/fxcore-pundix.json
+31 −0 _IBC/gateway-kujira.json
+31 −0 _IBC/juno-kujira.json
+16 −0 _IBC/kujira-terra2.json
+2 −2 _IBC/neutron-osmosis.json
+31 −0 _IBC/neutron-secretnetwork.json
+17 −1 _IBC/nois-osmosis.json
+17 −1 _IBC/nois-stargaze.json
+32 −0 _IBC/osmosis-pundix.json
+28 −0 _non-cosmos/binancesmartchain/assetlist.json
+30 −0 _non-cosmos/ethereum/assetlist.json
+ _non-cosmos/ethereum/images/page.png
+1 −0 _non-cosmos/ethereum/images/page.svg
+44 −0 _non-cosmos/polygon/assetlist.json
+12 −11 celestia/chain.json
+4 −0 chain.schema.json
+22 −5 chain4energy/chain.json
+23 −0 composable/chain.json
+5 −13 dyson/chain.json
+23 −0 empowerchain/chain.json
+27 −0 evmos/chain.json
+30 −0 fxcore/assetlist.json
+42 −0 gravitybridge/assetlist.json
+593 −2 kujira/assetlist.json
+118 −29 kujira/chain.json
+ kujira/images/PLNK_drk.png
+133 −1 neutron/assetlist.json
+9 −9 neutron/chain.json
+ neutron/images/astropepe.png
+ neutron/images/babycorgi.png
+ neutron/images/newt.png
+34 −0 nibiru/assetlist.json
+151 −0 nibiru/chain.json
+ nibiru/images/nibiru.png
+19 −0 nibiru/images/nibiru.svg
+26 −4 nolus/chain.json
+54 −2 osmosis/assetlist.json
+18 −0 osmosis/images/milktia.svg
+162 −0 osmosis/versions.json
+93 −0 pundix/assetlist.json
+171 −0 pundix/chain.json
+ pundix/images/pundi-x-chain-logo.png
+1 −0 pundix/images/pundi-x-chain-logo.svg
+ pundix/images/pundi-x-token-logo.png
+24 −0 pundix/images/pundi-x-token-logo.svg
+ pundix/images/purse-token-logo.png
+1 −0 pundix/images/purse-token-logo.svg
+31 −4 qwoyn/chain.json
+10 −4 sei/chain.json
+150 −0 teritori/assetlist.json
+ terra2/images/astro.png
+3 −0 terra2/images/astro.svg
+33 −0 testnets/auratestnet/assetlist.json
+122 −0 testnets/auratestnet/chain.json
+3 −3 testnets/celestiatestnet2/chain.json
+3 −3 testnets/celestiatestnet3/chain.json
+26 −10 testnets/cosmoshubtestnet/chain.json
+1 −1 testnets/deardogetestnet/assetlist.json
+31 −0 testnets/doravotatestnet2/assetlist.json
+88 −0 testnets/doravotatestnet2/chain.json
+15 −1 testnets/lavatestnet/chain.json
+26 −10 testnets/rsprovidertestnet/chain.json
+20 −1 testnets/sgetestnet/chain.json
+23 −0 umee/chain.json
+145 −0 versions.schema.json
+31 −4 xpla/chain.json
59 changes: 21 additions & 38 deletions src/components/AssetInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ import { PencilSquareIcon } from "@heroicons/react/20/solid";
import { BigNumber } from "bignumber.js";
import { clsx } from "clsx";
import { ethers } from "ethers";
import { FC, Fragment, useEffect, useMemo, useState } from "react";
import { FC, Fragment, useMemo, useState } from "react";

import { Chain } from "@/api/queries";
import { AssetWithMetadata, useAssets } from "@/context/assets";
import { disclosure } from "@/context/disclosures";
import { useSettingsStore } from "@/context/settings";
import Toast from "@/elements/Toast";
import { useAccount } from "@/hooks/useAccount";
import { getFee, useBalancesByChain } from "@/utils/utils";
import { formatUSD, getFee, useBalancesByChain } from "@/utils/utils";

import AssetSelect from "./AssetSelect";
import ChainSelect from "./ChainSelect";
import { ClientOnly } from "./ClientOnly";
import { SimpleTooltip } from "./SimpleTooltip";
import { UsdDiff, UsdValue, useUsdDiffReset } from "./UsdValue";

interface Props {
amount: string;
amountUSD?: string;
diffPercentage?: number;
onAmountChange?: (amount: string) => void;
asset?: AssetWithMetadata;
onAssetChange?: (asset: AssetWithMetadata) => void;
Expand All @@ -33,6 +34,8 @@ interface Props {

const AssetInput: FC<Props> = ({
amount,
amountUSD,
diffPercentage = 0,
onAmountChange,
asset,
onAssetChange,
Expand Down Expand Up @@ -93,15 +96,6 @@ const AssetInput: FC<Props> = ({

const { slippage } = useSettingsStore();

const reset = useUsdDiffReset();
useEffect(() => {
const parsed = parseFloat(amount);

// hotfix side effect to prevent negative amounts
if (parsed < 0) onAmountChange?.("0.0");
if (parsed == 0) reset();
}, [amount, onAmountChange, reset]);

return (
<Fragment>
<div className="space-y-4 border border-neutral-200 p-4 rounded-lg">
Expand Down Expand Up @@ -188,33 +182,22 @@ const AssetInput: FC<Props> = ({
}}
/>
<div className="flex items-center space-x-2 tabular-nums h-8">
{asset && parseFloat(amount) > 0 && (
<div className="text-neutral-400 text-sm">
<UsdValue
error={null}
chainId={asset.originChainID}
denom={asset.originDenom}
coingeckoID={asset.coingeckoID}
value={amount}
context={context}
/>
</div>
)}
{context === "dest" && (
<UsdDiff.Value>
{({ isLoading, percentage }) => (
<div
className={clsx(
"text-sm",
isLoading && "hidden",
percentage > 0 ? "text-green-500" : "text-red-500",
)}
>
({percentage.toFixed(2)}%)
</div>
<p className="text-neutral-400 text-sm">
{amountUSD ? formatUSD(amountUSD) : null}
</p>
{amountUSD !== undefined && context === "dest" ? (
<p
className={clsx(
"text-sm",
diffPercentage >= 0 ? "text-green-500" : "text-red-500",
)}
</UsdDiff.Value>
)}
>
{new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 2,
}).format(diffPercentage)}
</p>
) : null}
<div className="flex-grow" />
{showBalance && address && selectedAssetBalance && asset && (
<div className="text-neutral-400 text-sm flex items-center">
Expand Down
63 changes: 63 additions & 0 deletions src/components/PriceImpactWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useDisclosureKey } from "@/context/disclosures";

interface Props {
onGoBack: () => void;
warningMessage?: string;
}

export const PriceImpactWarning = ({
onGoBack,
warningMessage = "",
}: Props) => {
const [isOpen, control] = useDisclosureKey("priceImpactWarning");

if (!isOpen) return null;

return (
<div className="absolute inset-0 bg-white rounded-3xl z-[999]">
<div className="h-full px-4 py-6 overflow-y-auto scrollbar-hide">
<div className="h-full flex flex-col">
<div className="flex-1 pt-8">
<div className="text-red-400 flex justify-center py-16">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="h-24 w-24"
>
<path
fillRule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"
clipRule="evenodd"
/>
</svg>
</div>
<p className="font-bold text-lg text-center text-red-500 mb-2">
Price Impact Warning
</p>
<p className="text-center text-lg px-4 leading-snug text-gray-500">
{warningMessage} Do you want to continue?
</p>
</div>
<div className="flex items-end gap-2">
<button
className="bg-[#FF486E] hover:bg-[#ed1149] transition-colors text-white font-semibold py-4 rounded-md w-full"
onClick={() => control.close()}
>
Continue
</button>
<button
className="border border-gray-400 text-gray-500 font-semibold py-4 rounded-md w-full transition-colors hover:bg-gray-50"
onClick={() => {
control.close();
onGoBack();
}}
>
Go Back
</button>
</div>
</div>
</div>
</div>
);
};
35 changes: 34 additions & 1 deletion src/components/SwapWidget/SwapDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChevronDownIcon, PencilSquareIcon } from "@heroicons/react/20/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { RouteResponse } from "@skip-router/core";
import { clsx } from "clsx";
import { useMemo } from "react";
import { Fragment, useEffect, useMemo } from "react";

import { disclosure, useDisclosureKey } from "@/context/disclosures";
import { useSettingsStore } from "@/context/settings";
Expand All @@ -15,6 +15,8 @@ import { FormValues } from "./useSwapWidget";
type Props = FormValues & {
amountOut: string;
route: RouteResponse;
priceImpactPercent: number;
priceImpactThresholdReached: boolean;
};

export const SwapDetails = ({
Expand All @@ -25,6 +27,8 @@ export const SwapDetails = ({
destinationChain,
destinationAsset,
route,
priceImpactPercent,
priceImpactThresholdReached,
}: Props) => {
const [open, control] = useDisclosureKey("swapDetailsCollapsible");

Expand All @@ -43,6 +47,12 @@ export const SwapDetails = ({
return +feeAmount / Math.pow(10, 18);
}, [axelarTransferOperation]);

useEffect(() => {
if (priceImpactThresholdReached) {
control.open();
}
}, [control, priceImpactThresholdReached]);

if (!(sourceChain && sourceAsset && destinationChain && destinationAsset)) {
return null;
}
Expand Down Expand Up @@ -110,6 +120,29 @@ export const SwapDetails = ({
"[&_dd]:text-end [&_dd]:tabular-nums",
)}
>
{priceImpactPercent ? (
<Fragment>
<dt>
<span
className={clsx(
priceImpactThresholdReached ? "text-red-500" : undefined,
)}
>
Price Impact
</span>
</dt>
<dd
className={clsx(
priceImpactThresholdReached ? "text-red-500" : undefined,
)}
>
{new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 2,
}).format(priceImpactPercent)}
</dd>
</Fragment>
) : null}
<dt>
Max Slippage{" "}
<SimpleTooltip label="Click to change max slippage">
Expand Down
22 changes: 21 additions & 1 deletion src/components/SwapWidget/SwapWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import TransactionDialog from "../TransactionDialog";
import { UsdDiff } from "../UsdValue";
import { useWalletModal, WalletModal } from "../WalletModal";
import { SwapDetails } from "./SwapDetails";
import { useSwapWidget } from "./useSwapWidget";
import { PRICE_IMPACT_THRESHOLD, useSwapWidget } from "./useSwapWidget";

export const SwapWidget: FC = () => {
const { openWalletModal } = useWalletModal();
Expand All @@ -47,9 +47,19 @@ export const SwapWidget: FC = () => {
onSourceAssetChange,
onDestinationChainChange,
onDestinationAssetChange,
swapPriceImpactPercent,
priceImpactThresholdReached,
routeError,
routeWarning,
} = useSwapWidget();

let usdDiffPercent = 0.0;
if (route?.usdAmountIn && route?.usdAmountOut) {
usdDiffPercent =
(parseFloat(route.usdAmountOut) - parseFloat(route.usdAmountIn)) /
parseFloat(route.usdAmountIn);
}

const {
address,
isWalletConnected: isSourceWalletConnected,
Expand Down Expand Up @@ -114,6 +124,7 @@ export const SwapWidget: FC = () => {
<div data-testid="source">
<AssetInput
amount={amountIn}
amountUSD={route?.usdAmountIn}
asset={sourceAsset}
chain={sourceChain}
chains={chains ?? []}
Expand Down Expand Up @@ -174,6 +185,8 @@ export const SwapWidget: FC = () => {
<div data-testid="destination">
<AssetInput
amount={amountOut}
amountUSD={route?.usdAmountOut}
diffPercentage={usdDiffPercent}
asset={destinationAsset}
chain={destinationChain}
chains={chains ?? []}
Expand All @@ -196,6 +209,8 @@ export const SwapWidget: FC = () => {
destinationChain={destinationChain}
destinationAsset={destinationAsset}
route={route}
priceImpactPercent={swapPriceImpactPercent ?? 0}
priceImpactThresholdReached={priceImpactThresholdReached}
/>
)}
{routeLoading && <RouteLoadingBanner />}
Expand Down Expand Up @@ -251,6 +266,11 @@ export const SwapWidget: FC = () => {
route={route}
transactionCount={numberOfTransactions}
insufficientBalance={insufficientBalance}
shouldShowPriceImpactWarning={
priceImpactThresholdReached ||
Math.abs(usdDiffPercent * 100) > PRICE_IMPACT_THRESHOLD
}
routeWarning={routeWarning}
/>
{insufficientBalance && (
<p className="text-center font-semibold text-sm text-red-500">
Expand Down
72 changes: 72 additions & 0 deletions src/components/SwapWidget/useSwapWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { useBalancesByChain } from "@/utils/utils";

export const LAST_SOURCE_CHAIN_KEY = "IBC_DOT_FUN_LAST_SOURCE_CHAIN";

export const PRICE_IMPACT_THRESHOLD = 0.1;

export function useSwapWidget() {
const {
onSourceChainChange,
Expand Down Expand Up @@ -174,6 +176,73 @@ export function useSwapWidget() {
}
}, [currentEvmChain, sourceChain, switchNetwork]);

const swapPriceImpactPercent = useMemo(() => {
if (!routeResponse?.swapPriceImpactPercent) return undefined;
return parseFloat(routeResponse.swapPriceImpactPercent) / 100;
}, [routeResponse]);

const priceImpactThresholdReached = useMemo(() => {
if (!swapPriceImpactPercent) return false;
return swapPriceImpactPercent > PRICE_IMPACT_THRESHOLD;
}, [swapPriceImpactPercent]);

const usdDiffPercent = useMemo(() => {
if (!routeResponse) {
return undefined;
}

if (!routeResponse.usdAmountIn || !routeResponse.usdAmountOut) {
return undefined;
}

const usdAmountIn = parseFloat(routeResponse.usdAmountIn);
const usdAmountOut = parseFloat(routeResponse.usdAmountOut);

return (usdAmountOut - usdAmountIn) / usdAmountIn;
}, [routeResponse]);

const routeWarning = useMemo(() => {
if (!routeResponse) {
return undefined;
}

if (
!routeResponse.swapPriceImpactPercent &&
(!routeResponse.usdAmountIn || !routeResponse.usdAmountOut)
) {
return "We were unable to calculate the price impact of this route.";
}

if (usdDiffPercent && Math.abs(usdDiffPercent) > PRICE_IMPACT_THRESHOLD) {
const amountInUSD = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(parseFloat(routeResponse.usdAmountIn ?? "0"));

const amountOutUSD = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(parseFloat(routeResponse.usdAmountOut ?? "0"));

const formattedUsdDiffPercent = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 2,
}).format(Math.abs(usdDiffPercent));
return `Your estimated output value (${amountOutUSD}) is ${formattedUsdDiffPercent} lower than your estimated input value (${amountInUSD}).`;
}

if (
swapPriceImpactPercent &&
swapPriceImpactPercent > PRICE_IMPACT_THRESHOLD
) {
const formattedPriceImpact = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 2,
}).format(swapPriceImpactPercent);
return `Your swap is expected to execute at a ${formattedPriceImpact} worse price than the current estimated on-chain price. It's likely there's not much liquidity available for this swap.`;
}
}, [routeResponse, swapPriceImpactPercent, usdDiffPercent]);

return {
amountIn,
amountOut,
Expand All @@ -193,6 +262,9 @@ export function useSwapWidget() {
onDestinationAssetChange,
noRouteFound: routeQueryIsError,
routeError: errorMessage,
swapPriceImpactPercent,
priceImpactThresholdReached,
routeWarning,
};
}

Expand Down
Loading

0 comments on commit 41fcac7

Please sign in to comment.