diff --git a/apps/kyberswap-interface/src/assets/svg/gas_less_icon.svg b/apps/kyberswap-interface/src/assets/svg/gas_less_icon.svg index a22379a4a4..a573ed2278 100644 --- a/apps/kyberswap-interface/src/assets/svg/gas_less_icon.svg +++ b/apps/kyberswap-interface/src/assets/svg/gas_less_icon.svg @@ -1,10 +1,10 @@ - - + + - + diff --git a/apps/kyberswap-interface/src/components/Announcement/Popups/SimplePopup.tsx b/apps/kyberswap-interface/src/components/Announcement/Popups/SimplePopup.tsx index 1ec5ff72c6..310239c059 100644 --- a/apps/kyberswap-interface/src/components/Announcement/Popups/SimplePopup.tsx +++ b/apps/kyberswap-interface/src/components/Announcement/Popups/SimplePopup.tsx @@ -7,7 +7,6 @@ import { AutoColumn } from 'components/Column' import { CheckCircle } from 'components/Icons' import IconFailure from 'components/Icons/Failed' import WarningIcon from 'components/Icons/WarningIcon' -import { AutoRow } from 'components/Row' import useTheme from 'hooks/useTheme' import { cn } from 'utils/cn' @@ -40,8 +39,8 @@ export default function SimplePopup({ type === NotificationType.SUCCESS ? 'text-primary' : type === NotificationType.WARNING ? 'text-warning' : 'text-red' const mapIcon = { [NotificationType.SUCCESS]: , - [NotificationType.WARNING]: , - [NotificationType.ERROR]: , + [NotificationType.WARNING]: , + [NotificationType.ERROR]: , } const navigate = useNavigate() @@ -50,9 +49,9 @@ export default function SimplePopup({ onRemove?.() } return ( - -
{icon || mapIcon[type]}
- +
+
{icon || mapIcon[type]}
+ {title} {summary && {summary}} {link && ( @@ -61,6 +60,6 @@ export default function SimplePopup({ )} - +
) } diff --git a/apps/kyberswap-interface/src/components/Announcement/Popups/TopRightPopup.tsx b/apps/kyberswap-interface/src/components/Announcement/Popups/TopRightPopup.tsx index 82656e85f4..05426dc385 100644 --- a/apps/kyberswap-interface/src/components/Announcement/Popups/TopRightPopup.tsx +++ b/apps/kyberswap-interface/src/components/Announcement/Popups/TopRightPopup.tsx @@ -1,6 +1,5 @@ import { motion } from 'framer-motion' import { CSSProperties, useCallback, useEffect, useState } from 'react' -import { X } from 'react-feather' import getPopupTopRightDescriptionByType from 'components/Announcement/Popups/PopupTopRightDescriptions' import SimplePopup from 'components/Announcement/Popups/SimplePopup' @@ -16,6 +15,7 @@ import { import { useSuccessSound } from 'hooks/useSuccessSound' import useTheme from 'hooks/useTheme' import { useRemovePopup } from 'state/application/hooks' +import { CloseIcon } from 'theme' const delta = typeof window !== 'undefined' ? window.innerWidth + 'px' : '0px' @@ -105,17 +105,17 @@ export default function PopupItem({ popup, hasOverlay }: { popup: PopupItemType;
) : (
-
+
{popupContent} - +
{removeAfterMs && }
diff --git a/apps/kyberswap-interface/src/components/Announcement/Popups/TransactionPopup.tsx b/apps/kyberswap-interface/src/components/Announcement/Popups/TransactionPopup.tsx index 2f88ac6601..5f0255dc8c 100644 --- a/apps/kyberswap-interface/src/components/Announcement/Popups/TransactionPopup.tsx +++ b/apps/kyberswap-interface/src/components/Announcement/Popups/TransactionPopup.tsx @@ -209,6 +209,7 @@ const SUMMARY: { [type in TRANSACTION_TYPE]: SummaryFunction } = { [TRANSACTION_TYPE.ELASTIC_WITHDRAW_LIQUIDITY]: summaryTypeOnly, [TRANSACTION_TYPE.ELASTIC_FORCE_WITHDRAW_LIQUIDITY]: summaryTypeOnly, + [TRANSACTION_TYPE.FILL_LIMIT_ORDER]: summary2Token, [TRANSACTION_TYPE.CANCEL_LIMIT_ORDER]: summaryCancelLimitOrder, [TRANSACTION_TYPE.TRANSFER_TOKEN]: summaryTransferToken, @@ -277,19 +278,19 @@ export default function TransactionPopup({ hash, notiType }: { hash: string; not const { title, summary } = getSummary(transaction) return ( -
-
-
- {success ? : } -
- - - {title} - - {summary} - +
+
+ {success ? ( + + ) : ( + + )}
- + + {title} + {summary} + + = MAX_NOTIFICATION || totalTopRightPopup > 1 return ( <> {topRightPopups.length > 0 && (
-
- {totalTopRightPopup >= MAX_NOTIFICATION && ( - - See All - - )} - {totalTopRightPopup > 1 && ( - - Clear All - - )} -
+ {showTopRightPopupActions && ( +
+ {totalTopRightPopup >= MAX_NOTIFICATION && ( + + See All + + )} + {totalTopRightPopup > 1 && ( + + Clear All + + )} +
+ )} {topRightPopups.slice(0, MAX_NOTIFICATION).map((item, i) => ( diff --git a/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/InboxItemLimitOrder.tsx b/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/InboxItemLimitOrder.tsx index e757025e0d..b06d1501a4 100644 --- a/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/InboxItemLimitOrder.tsx +++ b/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/InboxItemLimitOrder.tsx @@ -21,8 +21,8 @@ import { } from 'components/Announcement/PrivateAnnoucement/styled' import { AnnouncementTemplateLimitOrder } from 'components/Announcement/type' import { CheckCircle } from 'components/Icons' +import { LimitOrderStatus } from 'components/LimitOrder/types' import { TokenLogoWithShadow } from 'components/Logo' -import { LimitOrderStatus } from 'components/swapv2/LimitOrder/type' import { APP_PATHS } from 'constants/index' import { NETWORKS_INFO } from 'constants/networks' import { useNavigateToUrl } from 'utils/redirect' @@ -135,7 +135,9 @@ function InboxItemLimitOrder({ const navigate = useNavigateToUrl() const onClick = () => { const route = NETWORKS_INFO[chainId]?.route ?? NETWORKS_INFO[ChainId.MAINNET].route - navigate(`${APP_PATHS.LIMIT}/${route}?activeTab=my_order`, chainId) + const orderTab = + isFilled || isPartialFilled || isCancelled || status === LimitOrderStatus.EXPIRED ? 'closed' : 'active' + navigate(`${APP_PATHS.LIMIT}/${route}?tab=my_order&orderTab=${orderTab}`, chainId) onRead(announcement, statusMessage) } diff --git a/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/NotificationCenter/LimitOrder.tsx b/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/NotificationCenter/LimitOrder.tsx index de321e071d..49b41bba1c 100644 --- a/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/NotificationCenter/LimitOrder.tsx +++ b/apps/kyberswap-interface/src/components/Announcement/PrivateAnnoucement/NotificationCenter/LimitOrder.tsx @@ -6,8 +6,8 @@ import InboxIcon from 'components/Announcement/PrivateAnnoucement/Icon' import { PrivateAnnouncementPropCenter } from 'components/Announcement/PrivateAnnoucement/NotificationCenter' import { Desc, Time, Title, Wrapper } from 'components/Announcement/PrivateAnnoucement/NotificationCenter/styled' import { AnnouncementTemplateLimitOrder } from 'components/Announcement/type' +import { LimitOrderStatus } from 'components/LimitOrder/types' import Logo from 'components/Logo' -import { LimitOrderStatus } from 'components/swapv2/LimitOrder/type' import { APP_PATHS } from 'constants/index' import { formatTime } from 'utils/time' diff --git a/apps/kyberswap-interface/src/components/Announcement/helpers.ts b/apps/kyberswap-interface/src/components/Announcement/helpers.ts index 3351df4cf1..7b3e3e36fb 100644 --- a/apps/kyberswap-interface/src/components/Announcement/helpers.ts +++ b/apps/kyberswap-interface/src/components/Announcement/helpers.ts @@ -9,7 +9,7 @@ import { SmartExitReason, SmartExitStatus, } from 'components/Announcement/type' -import { LimitOrderStatus } from 'components/swapv2/LimitOrder/type' +import { LimitOrderStatus } from 'components/LimitOrder/types' import { EnvKeys, NOTI_ENV } from 'constants/env' import { Metric, SmartExitCondition } from 'pages/Earns/types' import { formatDisplayNumber } from 'utils/numbers' diff --git a/apps/kyberswap-interface/src/components/Announcement/type.ts b/apps/kyberswap-interface/src/components/Announcement/type.ts index d7802ec137..8e6d25462f 100644 --- a/apps/kyberswap-interface/src/components/Announcement/type.ts +++ b/apps/kyberswap-interface/src/components/Announcement/type.ts @@ -1,6 +1,6 @@ import { ReactNode } from 'react' -import { LimitOrderStatus } from 'components/swapv2/LimitOrder/type' +import { LimitOrderStatus } from 'components/LimitOrder/types' import { SmartExitDexType } from 'pages/Earns/components/SmartExit/constants' import { SmartExitCondition } from 'pages/Earns/types' import { HistoricalPriceAlert } from 'pages/NotificationCenter/const' diff --git a/apps/kyberswap-interface/src/components/CurrencyInputPanel/index.tsx b/apps/kyberswap-interface/src/components/CurrencyInputPanel/index.tsx index 49c7f39ebc..ec79ea23d5 100644 --- a/apps/kyberswap-interface/src/components/CurrencyInputPanel/index.tsx +++ b/apps/kyberswap-interface/src/components/CurrencyInputPanel/index.tsx @@ -9,7 +9,7 @@ import Card from 'components/Card' import TokenInfo from 'components/CurrencyInputPanel/TokenInfo' import CurrencyLogo from 'components/CurrencyLogo' import Wallet from 'components/Icons/Wallet' -import { Input as NumericalInput } from 'components/NumericalInput' +import NumericalInput from 'components/NumericalInput' import { RowFixed } from 'components/Row' import TokenSelectorModal from 'components/TokenSelectorModal' import { useActiveWeb3React } from 'hooks' diff --git a/apps/kyberswap-interface/src/components/DatePicker.tsx b/apps/kyberswap-interface/src/components/DatePicker.tsx index 104a640a74..55baf062f3 100644 --- a/apps/kyberswap-interface/src/components/DatePicker.tsx +++ b/apps/kyberswap-interface/src/components/DatePicker.tsx @@ -3,9 +3,12 @@ import Picker from 'react-date-picker' export default function DatePicker({ onChange, value }: { value: Date; onChange: (date: Date) => void }) { const today = new Date() const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()) + const dateKey = `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}` + return (
[ + { value: TIMES_IN_SECS.ONE_HOUR, label: t`1 Hour` }, + { value: TIMES_IN_SECS.ONE_DAY, label: t`1 Day` }, + { value: 7 * TIMES_IN_SECS.ONE_DAY, label: t`7 Days` }, + { value: 30 * TIMES_IN_SECS.ONE_DAY, label: t`30 Days` }, + { value: 36500 * TIMES_IN_SECS.ONE_DAY, label: t`Never Expires` }, +] const HOURS = Array.from({ length: 24 }, (_, i) => ({ label: i, value: i })) const MINS = Array.from({ length: 60 }, (_, i) => ({ label: i, value: i })) @@ -29,6 +41,7 @@ export default function DateTimePicker({ expire, defaultDate, defaultOptions, + returnPresetValue, title, }: { isOpen: boolean @@ -36,8 +49,9 @@ export default function DateTimePicker({ onSetDate: (val: Date | number) => void expire: number defaultDate?: Date - title?: ReactNode defaultOptions?: { label: string; value: number }[] + returnPresetValue?: boolean + title?: ReactNode }) { const today = new Date() const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate()) @@ -45,6 +59,7 @@ export default function DateTimePicker({ const [min, setMin] = useState(0) const [hour, setHour] = useState(0) const [defaultExpire, setDefaultExpire] = useState(null) + const options = useMemo(() => defaultOptions || getDefaultOptions(), [defaultOptions]) const setCustomDate = (date: Date, hour: number, min: number) => { let newMin = min @@ -65,18 +80,17 @@ export default function DateTimePicker({ setDefaultExpire(null) } - const onSelectDefaultOption = useCallback((value: number) => { - // `value` is either a preset duration in seconds (up to ~3.15e9 for the 100-year "Forever" - // option) or an absolute millisecond timestamp from a custom-picked date (always ≥ ~1.7e12). - // Only a real ms timestamp is treated as an absolute date; everything else is a duration offset - // from now — otherwise the large "Forever" duration would be misread as a 1970 timestamp. - const isTimestamp = value >= 1e12 - if (!isTimestamp) setDefaultExpire(value) - const date = isTimestamp ? new Date(value) : new Date(Date.now() + value * 1000) - setDate(date) - setHour(date.getHours()) - setMin(date.getMinutes()) - }, []) + const onSelectDefaultOption = useCallback( + (value: number) => { + const isDefaultOption = options.some(opt => opt.value === value) + setDefaultExpire(isDefaultOption ? value : null) + const date = isDefaultOption ? new Date(Date.now() + value * 1000) : new Date(value) + setDate(date) + setHour(date.getHours()) + setMin(date.getMinutes()) + }, + [options], + ) useEffect(() => { if (isOpen) { @@ -97,15 +111,20 @@ export default function DateTimePicker({ const theme = useTheme() const propsSelect: Partial = { - style: { width: 120, borderRadius: 20 }, - className: 'bg-background', + style: { width: 100, borderRadius: 20 }, + className: 'bg-background px-3 py-1', menuStyle: { height: 250, overflow: 'scroll', textAlign: 'center', - width: 'fit-content', + width: 100, + }, + optionStyle: { + padding: '8px 8px', + fontSize: 14, + textAlign: 'left', }, - placement: 'left', + placement: 'bottom', } const expireResult = defaultExpire ? Date.now() + defaultExpire * 1000 : date @@ -123,33 +142,48 @@ export default function DateTimePicker({ return MINS }, [date, hour]) + const handleSetDate = () => { + onSetDate(returnPresetValue && defaultExpire ? defaultExpire : date) + onDismiss() + } + return ( - +
{title || Customize the Expiry Time} - +
-
- +
+ Default Options - {(defaultOptions || getExpireOptions()).map(opt => ( - opt.value && onSelectDefaultOption(Number(opt.value))} - > - {opt.label} - - ))} +
+ {options.map(opt => { + const active = opt.value === defaultExpire + + return ( + + ) + })} +
setCustomDate(date, hour, min)} /> -
+
+ {dayjs(date).format('DD/MM/YYYY')} + +
+ +
+ +
+
+ {orders.map(order => ( + + ))} +
+ {showPagination && ( +
+ +
+ )} + {showNoOrders && ( + + + + {keyword ? ( + No orders found. + ) : isTabActive ? ( + You don't have any open orders yet. + ) : ( + You don't have any order history. + )} + + + )} + + {selectedCancelOrders.length > 0 && ( + + )} +
+ ) +} + +export default MyOrders diff --git a/apps/kyberswap-interface/src/components/LimitOrder/MyOrders/utils.ts b/apps/kyberswap-interface/src/components/LimitOrder/MyOrders/utils.ts new file mode 100644 index 0000000000..9ae72e224c --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/MyOrders/utils.ts @@ -0,0 +1,119 @@ +import { t } from '@lingui/macro' + +import { LimitOrder, LimitOrderStatus } from 'components/LimitOrder/types' +import { formatAmountOrder, formatRateLimitOrder, isActiveStatus } from 'components/LimitOrder/utils' + +export const PAGE_SIZE = 10 + +export const LIST_ORDER_TABS = [LimitOrderStatus.ACTIVE, LimitOrderStatus.CLOSED] as const + +type ListOrderTab = (typeof LIST_ORDER_TABS)[number] + +type OrderTypeOption = { + label: string + value: LimitOrderStatus +} + +export const getActiveOrderOptions = (): OrderTypeOption[] => [ + { + label: t`All Active Orders`, + value: LimitOrderStatus.ACTIVE, + }, + { + label: t`Open Orders`, + value: LimitOrderStatus.OPEN, + }, + { + label: t`Partially Filled Orders`, + value: LimitOrderStatus.PARTIALLY_FILLED, + }, +] + +export const getCloseOrderOptions = (): OrderTypeOption[] => [ + { + label: t`All Closed Orders`, + value: LimitOrderStatus.CLOSED, + }, + { + label: t`Filled Orders`, + value: LimitOrderStatus.FILLED, + }, + { + label: t`Cancelled Orders`, + value: LimitOrderStatus.CANCELLED, + }, + { + label: t`Expired Orders`, + value: LimitOrderStatus.EXPIRED, + }, +] + +export const getOrderTypeOptions = (orderType: LimitOrderStatus): OrderTypeOption[] => + isActiveStatus(orderType) ? getActiveOrderOptions() : getCloseOrderOptions() + +export const getActiveTabByOrderType = (orderType: LimitOrderStatus): ListOrderTab => + isActiveStatus(orderType) ? LimitOrderStatus.ACTIVE : LimitOrderStatus.CLOSED + +export const formatStatus = (status: LimitOrderStatus): string => { + switch (status) { + case LimitOrderStatus.ACTIVE: + case LimitOrderStatus.OPEN: + return t`Active` + case LimitOrderStatus.PARTIALLY_FILLED: + return t`Active` + case LimitOrderStatus.FILLED: + return t`Filled` + case LimitOrderStatus.CANCELLING: + return t`Cancelling` + case LimitOrderStatus.CANCELLED: + return t`Cancelled` + case LimitOrderStatus.EXPIRED: + return t`Expired` + case LimitOrderStatus.INSUFFICIENT_FUNDS: + return t`Insufficient funds` + default: + return status.replace('_', ' ') + } +} + +export const getSearchParamsWithKeyword = (searchParams: URLSearchParams, keyword: string): URLSearchParams => { + const nextSearchParams = new URLSearchParams(searchParams) + + if (keyword) { + nextSearchParams.set('search', keyword) + } else { + nextSearchParams.delete('search') + } + + return nextSearchParams +} + +export const getCancelledOrderTrackingPayload = (order: LimitOrder, chainName: string) => ({ + order_id: order.id, + side: 'sell', + from_token: order.makerAssetSymbol, + to_token: order.takerAssetSymbol, + pair: `${order.makerAssetSymbol}/${order.takerAssetSymbol}`, + limit_price: formatRateLimitOrder(order, false), + amount_in: formatAmountOrder(order.makingAmount, order.makerAssetDecimals), + time_active_minutes: Math.round((Date.now() / 1000 - order.createdAt) / 60), + chain: chainName, +}) + +export const getFilledOrderTrackingPayload = (order: LimitOrder, chainName: string) => { + const lastTx = order.transactions?.[order.transactions.length - 1] + + return { + order_id: order.id, + side: 'sell', + from_token: order.makerAssetSymbol, + to_token: order.takerAssetSymbol, + pair: `${order.makerAssetSymbol}/${order.takerAssetSymbol}`, + limit_price: formatRateLimitOrder(order, false), + fill_price: formatRateLimitOrder(order, false), + amount_in: formatAmountOrder(order.makingAmount, order.makerAssetDecimals), + amount_out_actual: formatAmountOrder(order.filledTakingAmount, order.takerAssetDecimals), + tx_hash: lastTx?.txHash, + chain: chainName, + } +} diff --git a/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/OrderBookSkeleton.tsx b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/OrderBookSkeleton.tsx new file mode 100644 index 0000000000..2503795e96 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/OrderBookSkeleton.tsx @@ -0,0 +1,67 @@ +import TableHeader, { RowWrapper } from 'components/LimitOrder/OrderBook/TableHeader' +import Skeleton from 'components/Skeleton' + +const Amount = () => ( +
+ +
+) + +const RowSkeleton = () => ( + + + + +
+ + +
+ + + +
+ + +
+ + + + + + + +
+) + +const Rows = ({ count }: { count: number }) => ( + <> + {Array.from({ length: count }, (_, i) => ( + + ))} + +) + +const OrderBookSkeleton = () => ( + <> + + + + + + + + + + + + + + + + + + + +) + +export default OrderBookSkeleton diff --git a/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/OrderItem.tsx b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/OrderItem.tsx new file mode 100644 index 0000000000..72309881fa --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/OrderItem.tsx @@ -0,0 +1,90 @@ +import { Trans } from '@lingui/macro' +import { useMedia } from 'react-use' + +import CopyHelper from 'components/Copy' +import { RowWrapper } from 'components/LimitOrder/OrderBook/TableHeader' +import { AmountWithSymbol, ClippedText, SizeInfo } from 'components/LimitOrder/components' +import { LimitOrderFromTokenPairFormatted } from 'components/LimitOrder/types' +import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import { useLimitState } from 'state/limit/hooks' +import { MEDIA_WIDTHS } from 'theme' +import { cn } from 'utils/cn' + +type RateTextProps = { + order: LimitOrderFromTokenPairFormatted + className?: string + showInvertedRate?: boolean +} + +const RateText = ({ order, className, showInvertedRate }: RateTextProps) => { + const rate = showInvertedRate ? order.invertedRate : order.rate + const formattedRate = showInvertedRate ? order.formattedInvertedRate : order.formattedRate + const formattedMarketDiffPercent = showInvertedRate + ? order.formattedInvertedMarketDiffPercent + : order.formattedMarketDiffPercent + + return ( +
+ + {formattedRate} + + {formattedMarketDiffPercent &&
{formattedMarketDiffPercent}
} +
+ ) +} + +type OrderItemProps = { + reverse?: boolean + order: LimitOrderFromTokenPairFormatted + onTake?: (order: LimitOrderFromTokenPairFormatted) => void + showInvertedRate?: boolean +} + +const OrderItem = ({ reverse, order, onTake, showInvertedRate }: OrderItemProps) => { + const upToExtraSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToExtraSmall}px)`) + const { currencyIn: makerCurrency, currencyOut: takerCurrency } = useLimitState() + + const chain = NETWORKS_INFO[order.chainId] + const filledPercent = Math.max(0, Math.min(Number(order.filledPercent) || 0, 100)) + const sizeAmount = order.formattedMakerAmount + const availableAmount = order.formattedAvailableMakerAmount + const totalAmount = order.formattedTakerAmount + const sizeCurrency = !reverse ? makerCurrency : takerCurrency + const totalCurrency = !reverse ? takerCurrency : makerCurrency + const rateClassName = reverse ? 'text-primary' : 'text-red' + + return ( + + + Network + + + {!upToExtraSmall && } + + + {!upToExtraSmall && ( + + )} + {!upToExtraSmall && ( +
+ {order.hasAvailable && ( + + )} +
+ )} +
+ ) +} + +export default OrderItem diff --git a/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/TableHeader.tsx b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/TableHeader.tsx new file mode 100644 index 0000000000..7f3e9ec41f --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/TableHeader.tsx @@ -0,0 +1,56 @@ +import { Trans } from '@lingui/macro' +import { HTMLAttributes } from 'react' +import { useMedia } from 'react-use' + +import InfoHelper from 'components/InfoHelper' +import { MEDIA_WIDTHS } from 'theme' +import { cn } from 'utils/cn' + +export const RowWrapper = ({ children, className, ...rest }: HTMLAttributes) => ( +
+ {children} +
+) + +const TableHeader = () => { + const upToExtraSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToExtraSmall}px)`) + + return ( + + CHAIN + + Size + + {!upToExtraSmall && ( + + Available + Amount available to be taken from the order.} /> + + )} + + Rate + + + Total + + {!upToExtraSmall && ( + + ID + + )} + {!upToExtraSmall && ( + + Action + + )} + + ) +} + +export default TableHeader diff --git a/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/index.tsx b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/index.tsx new file mode 100644 index 0000000000..91b2ea6e4f --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/index.tsx @@ -0,0 +1,239 @@ +import { Trans } from '@lingui/macro' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { Repeat } from 'react-feather' +import { useGetOrdersByTokenPairQuery } from 'services/limitOrder' + +import { ReactComponent as NoDataIcon } from 'assets/svg/no_data.svg' +import OrderItem from 'components/LimitOrder/OrderBook/OrderItem' +import TableHeader, { RowWrapper } from 'components/LimitOrder/OrderBook/TableHeader' +import { formatOrders, getSchemaToken, invertRateValue } from 'components/LimitOrder/OrderBook/utils' +import TakeOrderConfirmModal from 'components/LimitOrder/TakeOrder/TakeOrderConfirmModal' +import { LimitOrderFromTokenPairFormatted } from 'components/LimitOrder/types' +import RefetchIndicator from 'components/RefetchIndicator' +import RefreshLoading from 'components/RefreshLoading' +import { useActiveWeb3React } from 'hooks' +import { useStableCoins } from 'hooks/Tokens' +import { useBaseTradeInfoLimitOrder } from 'hooks/useBaseTradeInfo' +import { getDefaultRevertPrice as getDefaultInvertPrice } from 'pages/Earns/utils' +import { useWalletModalToggle } from 'state/application/hooks' +import { useLimitState } from 'state/limit/hooks' +import { cn } from 'utils/cn' +import { formatDisplayNumber } from 'utils/numbers' + +const refetchSafely = (refetch: () => { catch?: (onRejected: () => void) => unknown } | void) => { + try { + refetch()?.catch?.(() => {}) + } catch {} +} + +const NoDataPanel = () => ( +
+ + No orders +
+) + +const SectionLabel = ({ color, label, symbol }: { color: string; label: React.ReactNode; symbol?: string }) => ( +
+ {label} {symbol} +
+) + +const OrderSide = ({ + children, + className, + reverse, + style, + ...rest +}: React.HTMLAttributes & { reverse?: boolean }) => { + return ( +
+ {children} +
+ ) +} + +const OrderBook = () => { + const { account, chainId, networkInfo } = useActiveWeb3React() + const toggleWalletModal = useWalletModalToggle() + const { currencyIn: makerCurrency, currencyOut: takerCurrency } = useLimitState() + const { isStableCoin } = useStableCoins(chainId) + + const [selectedOrderToTake, setSelectedOrderToTake] = useState() + const [isTakeOrderModalOpen, setIsTakeOrderModalOpen] = useState(false) + const [invertedRateOverride, setInvertedRateOverride] = useState<{ pairKey: string; value: boolean }>() + + const { + loading: loadingMarketRate, + tradeInfo: { marketRate = 0, priceUsdIn = 0, priceUsdOut = 0 } = {}, + refetch: refetchMarketRate, + } = useBaseTradeInfoLimitOrder(makerCurrency, takerCurrency, chainId) + + const { + data: { orders = [] } = {}, + isFetching: isFetchingOrders, + isSuccess: isOrdersLoaded, + refetch: refetchOrders, + } = useGetOrdersByTokenPairQuery({ + chainId, + makerAsset: makerCurrency?.wrapped?.address, + takerAsset: takerCurrency?.wrapped?.address, + }) + + const { + data: { orders: reversedOrders = [] } = {}, + isFetching: isFetchingReversedOrder, + isSuccess: isReversedOrdersLoaded, + refetch: refetchReversedOrders, + } = useGetOrdersByTokenPairQuery({ + chainId, + makerAsset: takerCurrency?.wrapped?.address, + takerAsset: makerCurrency?.wrapped?.address, + }) + + const refetchLoading = loadingMarketRate || isFetchingOrders || isFetchingReversedOrder + + const formattedOrders = useMemo( + () => formatOrders(orders, makerCurrency, takerCurrency, marketRate, priceUsdIn, priceUsdOut), + [orders, makerCurrency, takerCurrency, marketRate, priceUsdIn, priceUsdOut], + ) + const formattedReversedOrders = useMemo( + () => formatOrders(reversedOrders, takerCurrency, makerCurrency, marketRate, priceUsdOut, priceUsdIn, true), + [reversedOrders, takerCurrency, makerCurrency, marketRate, priceUsdOut, priceUsdIn], + ) + + const visibleSellOrders = formattedOrders.slice(-10).reverse() + const visibleBuyOrders = formattedReversedOrders.slice(0, 10) + + const ratePairKey = useMemo(() => { + const makerAddress = makerCurrency?.wrapped.address.toLowerCase() + const takerAddress = takerCurrency?.wrapped.address.toLowerCase() + + return makerAddress && takerAddress ? `${chainId}:${makerAddress}:${takerAddress}` : '' + }, [chainId, makerCurrency, takerCurrency]) + + const defaultShowInvertedRate = useMemo(() => { + const makerToken = getSchemaToken(makerCurrency, isStableCoin(makerCurrency?.wrapped.address)) + const takerToken = getSchemaToken(takerCurrency, isStableCoin(takerCurrency?.wrapped.address)) + if (!makerToken || !takerToken) return false + + return getDefaultInvertPrice({ token0: makerToken, token1: takerToken }, chainId) + }, [chainId, isStableCoin, makerCurrency, takerCurrency]) + + const showInvertedRate = + invertedRateOverride?.pairKey === ratePairKey ? invertedRateOverride.value : defaultShowInvertedRate + + const displayedMarketRate = showInvertedRate ? invertRateValue(marketRate) : marketRate + + const displayedRatePair = showInvertedRate + ? `${takerCurrency?.symbol}/${makerCurrency?.symbol}` + : `${makerCurrency?.symbol}/${takerCurrency?.symbol}` + + const onRefreshOrders = useCallback(() => { + refetchSafely(refetchMarketRate) + refetchSafely(refetchOrders) + refetchSafely(refetchReversedOrders) + }, [refetchMarketRate, refetchOrders, refetchReversedOrders]) + + useEffect(() => { + setInvertedRateOverride(undefined) + }, [ratePairKey]) + + const handleInvertRate = useCallback(() => { + if (!ratePairKey) return + + setInvertedRateOverride(current => ({ + pairKey: ratePairKey, + value: !(current?.pairKey === ratePairKey ? current.value : defaultShowInvertedRate), + })) + }, [defaultShowInvertedRate, ratePairKey]) + + const handleTakeOrder = (order: LimitOrderFromTokenPairFormatted) => { + if (!account) { + toggleWalletModal() + return + } + setSelectedOrderToTake(order) + setIsTakeOrderModalOpen(true) + } + + const handleDismissTakeOrderModal = () => { + setIsTakeOrderModalOpen(false) + setSelectedOrderToTake(undefined) + } + + return ( +
+ +
+ +
+ SELLING} symbol={makerCurrency?.symbol} /> + + + {visibleSellOrders.length > 0 + ? visibleSellOrders.map(order => ( + + )) + : isOrdersLoaded && } + + + + + Network + + + {displayedMarketRate ? formatDisplayNumber(displayedMarketRate, { significantDigits: 6 }) : '--'} + + + + +
+ +
+
+ + BUYING} symbol={makerCurrency?.symbol} /> + + + {visibleBuyOrders.length > 0 + ? visibleBuyOrders.map(order => ( + + )) + : isReversedOrdersLoaded && } + + + {selectedOrderToTake && ( + + )} +
+ ) +} + +export default OrderBook diff --git a/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/utils.ts b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/utils.ts new file mode 100644 index 0000000000..2940967aa9 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/OrderBook/utils.ts @@ -0,0 +1,124 @@ +import type { Token as SchemaToken } from '@kyber/schema' +import { Currency, CurrencyAmount, Token } from '@kyberswap/ks-sdk-core' +import JSBI from 'jsbi' + +import { LimitOrderFromTokenPair, LimitOrderFromTokenPairFormatted } from 'components/LimitOrder/types' +import { getMarketPriceDiff } from 'components/LimitOrder/utils' +import { formatDisplayNumber } from 'utils/numbers' + +const MIN_AVAILABLE_USD = 0.01 + +const safeDivide = (numerator: JSBI, denominator: JSBI) => + JSBI.equal(denominator, JSBI.BigInt(0)) ? JSBI.BigInt(0) : JSBI.divide(numerator, denominator) + +export const invertRateValue = (value: string | number | undefined) => { + const numberValue = Number(value) + if (!numberValue || !Number.isFinite(numberValue)) return undefined + return 1 / numberValue +} + +export const getSchemaToken = (currency: Currency | undefined, isStable: boolean): SchemaToken | undefined => { + if (!currency) return undefined + + return { + address: currency.wrapped.address, + symbol: currency.symbol || '', + name: currency.name || '', + decimals: currency.decimals, + isStable, + } +} + +export const formatOrders = ( + orders: LimitOrderFromTokenPair[], + makerCurrency: Currency | undefined, + takerCurrency: Currency | undefined, + marketRate: number, + makerPriceUsd: number, + takerPriceUsd: number, + reverse = false, +): LimitOrderFromTokenPairFormatted[] => { + if (!makerCurrency || !takerCurrency) return [] + + return orders + .map(order => { + const newMakerCurrency = new Token( + order.chainId, + order.makerAsset, + order.makerAssetDecimals, + order.makerAssetSymbol || makerCurrency.symbol, + ) + const newTakerCurrency = new Token( + order.chainId, + order.takerAsset, + order.takerAssetDecimals, + order.takerAssetSymbol || takerCurrency.symbol, + ) + + const makerCurrencyAmount = CurrencyAmount.fromRawAmount(newMakerCurrency, order.makingAmount) + const takerCurrencyAmount = CurrencyAmount.fromRawAmount(newTakerCurrency, order.takingAmount) + const availableMakerCurrencyAmount = CurrencyAmount.fromRawAmount(newMakerCurrency, order.availableMakingAmount) + + const rate = ( + !reverse + ? takerCurrencyAmount.divide(makerCurrencyAmount).multiply(makerCurrencyAmount.decimalScale) + : makerCurrencyAmount.divide(takerCurrencyAmount).multiply(takerCurrencyAmount.decimalScale) + ).toSignificant(100) + + const filledMakingAmount = CurrencyAmount.fromRawAmount(newMakerCurrency, order.filledMakingAmount) + const filledPercent = (parseFloat(filledMakingAmount.toExact()) / parseFloat(makerCurrencyAmount.toExact())) * 100 + const makerAmount = makerCurrencyAmount.toExact() + const takerAmount = takerCurrencyAmount.toExact() + const availableMakerAmount = availableMakerCurrencyAmount.toExact() + const availableTakerAmount = CurrencyAmount.fromRawAmount( + newTakerCurrency, + safeDivide( + JSBI.multiply(JSBI.BigInt(order.takingAmount), JSBI.BigInt(order.availableMakingAmount)), + JSBI.BigInt(order.makingAmount), + ), + ).toExact() + const availableAmount = reverse ? availableTakerAmount : availableMakerAmount + const availablePriceUsd = reverse ? takerPriceUsd : makerPriceUsd + const availableAmountNumber = Number(availableAmount) + const availableUsd = + availablePriceUsd && Number.isFinite(availableAmountNumber) + ? availableAmountNumber * availablePriceUsd + : undefined + + if (availableAmountNumber <= 0 || (availableUsd !== undefined && availableUsd < MIN_AVAILABLE_USD)) { + return undefined + } + + const hasAvailable = parseFloat(availableMakerAmount) > 0 + const marketDiff = getMarketPriceDiff(rate, marketRate) + const invertedRate = invertRateValue(rate) + const invertedMarketDiff = getMarketPriceDiff(invertedRate, invertRateValue(marketRate)) + + return { + id: order.id, + chainId: order.chainId, + rawOrder: order, + isReversed: reverse, + hasAvailable, + formattedMakerAmount: formatDisplayNumber(makerAmount, { significantDigits: 6 }), + formattedTakerAmount: formatDisplayNumber(takerAmount, { significantDigits: 6 }), + formattedAvailableMakerAmount: hasAvailable + ? formatDisplayNumber(availableMakerAmount, { significantDigits: 6 }) + : '', + formattedAvailableTakerAmount: hasAvailable + ? formatDisplayNumber(availableTakerAmount, { significantDigits: 6 }) + : '', + rate, + formattedRate: formatDisplayNumber(rate, { significantDigits: 6 }), + invertedRate: invertedRate?.toString() || '', + formattedInvertedRate: invertedRate ? formatDisplayNumber(invertedRate, { significantDigits: 6 }) : '--', + formattedMarketDiffPercent: marketDiff.displayPercent, + formattedInvertedMarketDiffPercent: invertedMarketDiff.displayPercent, + marketDiffPercent: reverse ? -marketDiff.rawPercent : marketDiff.rawPercent, + filledPercent: filledPercent > 99 ? '100' : filledPercent.toFixed(), + } + }) + .filter((order): order is LimitOrderFromTokenPairFormatted => Boolean(order)) + .filter(order => order.filledPercent !== '100') + .sort((a, b) => parseFloat(b.rate) - parseFloat(a.rate)) +} diff --git a/apps/kyberswap-interface/src/components/LimitOrder/OrderList/index.tsx b/apps/kyberswap-interface/src/components/LimitOrder/OrderList/index.tsx new file mode 100644 index 0000000000..3bac21725e --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/OrderList/index.tsx @@ -0,0 +1,113 @@ +import { ChainId } from '@kyberswap/ks-sdk-core' +import { Trans } from '@lingui/macro' +import { useGetNumberOfInsufficientFundOrdersQuery } from 'services/limitOrder' + +import { LimitOrderProvider, useLimitOrderContext } from 'components/LimitOrder/LimitOrderContext' +import MyOrders from 'components/LimitOrder/MyOrders' +import OrderBook from 'components/LimitOrder/OrderBook' +import { LimitOrderTab } from 'components/LimitOrder/types' +import { HStack, Stack } from 'components/Stack' +import { MouseoverTooltip } from 'components/Tooltip' +import { useActiveWeb3React } from 'hooks' +import useTab from 'hooks/useTab' +import TokenPriceChart from 'pages/SwapV3/Components/TokenPriceChart' +import { useLimitState } from 'state/limit/hooks' +import { cn } from 'utils/cn' + +const ORDER_LIST_TABS = [ + { id: LimitOrderTab.ORDER_BOOK, label: Open Limit Orders }, + { id: LimitOrderTab.MY_ORDER, label: My Order(s) }, + { id: LimitOrderTab.PRICE, label: Price }, +] as const + +const TabSelector = ({ + activeTab, + setActiveTab, +}: { + activeTab: LimitOrderTab + setActiveTab: (n: LimitOrderTab) => void +}) => { + const { account } = useActiveWeb3React() + const { chainId } = useLimitOrderContext() + + const { data: numberOfInsufficientFundOrders } = useGetNumberOfInsufficientFundOrdersQuery( + { chainId, maker: account || '' }, + { skip: !account, pollingInterval: 10_000 }, + ) + + return ( + +
+ {ORDER_LIST_TABS.map((tab, index) => { + const active = tab.id === activeTab + const isLast = index === ORDER_LIST_TABS.length - 1 + return ( + + ) + })} +
+
+ ) +} + +const OrderListContent = () => { + const { syncOrderListTabWithQuery } = useLimitOrderContext() + const { currencyIn, currencyOut } = useLimitState() + const { activeTab, setActiveTab } = useTab({ + tabs: ORDER_LIST_TABS.map(tab => tab.id), + defaultTab: LimitOrderTab.ORDER_BOOK, + syncQuery: syncOrderListTabWithQuery, + }) + const currentTab = activeTab || LimitOrderTab.ORDER_BOOK + + return ( + + + + + {currentTab === LimitOrderTab.ORDER_BOOK && } + {currentTab === LimitOrderTab.MY_ORDER && } + {currentTab === LimitOrderTab.PRICE && } + + + ) +} + +const OrderList = ({ customChainId }: { customChainId?: ChainId }) => ( + + + +) + +export default OrderList diff --git a/apps/kyberswap-interface/src/components/LimitOrder/ProcessingOrder/ProcessingOrderModal.tsx b/apps/kyberswap-interface/src/components/LimitOrder/ProcessingOrder/ProcessingOrderModal.tsx new file mode 100644 index 0000000000..28f4751814 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/ProcessingOrder/ProcessingOrderModal.tsx @@ -0,0 +1,213 @@ +import { ChainId, Currency } from '@kyberswap/ks-sdk-core' +import { Trans, t } from '@lingui/macro' +import { AlertCircle, RotateCw } from 'react-feather' + +import { ButtonLight, ButtonOutlined, ButtonPrimary } from 'components/Button' +import { CheckCircle } from 'components/Icons' +import type { ProcessingOrderStep } from 'components/LimitOrder/ProcessingOrder/useProcessingOrder' +import Loader from 'components/Loader' +import Modal from 'components/Modal' +import { Center, HStack, Stack } from 'components/Stack' +import { NativeCurrencies } from 'constants/tokens' +import { CloseIcon } from 'theme/components' +import { cn } from 'utils/cn' + +type ProcessingStepStatus = 'idle' | 'active' | 'success' | 'error' + +type ProcessingState = { + show: boolean + steps: Step[] + currentStep?: Step + errorStep?: Step + completedSteps: Step[] +} + +type ProcessingController = { + state: ProcessingState + dismiss: () => void + retryStep?: (step: Step) => void +} + +type ProcessingOrderModalProps = { + processing: ProcessingController + chainId?: ChainId + currencyIn?: Currency + onViewOrder?: () => void +} + +const getStepStatus = ({ + step, + currentStep, + errorStep, + completedSteps, +}: { + step: Step + currentStep: Step | undefined + errorStep: Step | undefined + completedSteps: Step[] +}): ProcessingStepStatus => { + if (errorStep === step) return 'error' + if (completedSteps.includes(step)) return 'success' + if (currentStep === step) return 'active' + return 'idle' +} + +const StepIcon = ({ index, status }: { index: number; status: ProcessingStepStatus }) => { + if (status === 'success') return + if (status === 'active') return + if (status === 'error') return + return ( +
+ {index + 1} +
+ ) +} + +const getStepLabel = ({ + step, + status, + chainId, + currencyIn, +}: { + step: ProcessingOrderStep + status: ProcessingStepStatus + chainId: ChainId | undefined + currencyIn: Currency | undefined +}) => { + if (step === 'wrap') { + const nativeSymbol = chainId ? NativeCurrencies[chainId].symbol : t`token` + if (status === 'active') return t`Wrapping ${nativeSymbol}` + if (status === 'success') return t`Wrapped ${nativeSymbol}` + return t`Wrap ${nativeSymbol}` + } + + if (step === 'approve') { + const inputSymbol = currencyIn?.wrapped.symbol + if (status === 'active') return inputSymbol ? t`Approving ${inputSymbol}` : t`Approving token` + if (status === 'success') return inputSymbol ? t`Approved ${inputSymbol}` : t`Approved token` + return inputSymbol ? t`Approve ${inputSymbol}` : t`Approve token` + } + + if (step === 'create') { + if (status === 'active') return t`Signing order` + if (status === 'success') return t`Order successfully listed` + return t`Sign order` + } + + if (status === 'active') return t`Filling order` + if (status === 'success') return t`Order filled` + return t`Fill order` +} + +const ProcessingStepRow = ({ + index, + step, + status, + chainId, + currencyIn, + onRetryStep, +}: { + index: number + step: Step + status: ProcessingStepStatus + chainId: ChainId | undefined + currencyIn: Currency | undefined + onRetryStep?: (step: Step) => void +}) => ( + + + + {getStepLabel({ step, status, chainId, currencyIn })} + + {status === 'error' && ( + onRetryStep?.(step)} width="auto" className="gap-1 px-2 py-1 text-xs"> + + {t`Retry`} + + )} + +) + +const ProcessingOrderModal = ({ + processing, + chainId, + currencyIn, + onViewOrder, +}: ProcessingOrderModalProps) => { + const { state, dismiss, retryStep } = processing + + const orderComplete = + state.show && + !!state.steps.length && + state.steps.every(step => state.completedSteps.includes(step)) && + !state.errorStep + + const isProcessing = state.show && !!state.currentStep && !state.errorStep && !orderComplete + + const handleDismiss = () => { + if (!isProcessing) { + dismiss() + } + } + + const handleViewOrder = () => { + dismiss() + onViewOrder?.() + } + + return ( + + + +
{t`Processing Order`}
+ +
+ + + + {state.steps.map((step, index) => { + const status = getStepStatus({ + step, + currentStep: state.currentStep, + errorStep: state.errorStep, + completedSteps: state.completedSteps, + }) + return ( + + ) + })} + + + {orderComplete && ( + + + Close + + + My Orders + + + )} + +
+
+ ) +} + +export default ProcessingOrderModal diff --git a/apps/kyberswap-interface/src/components/LimitOrder/ProcessingOrder/useProcessingOrder.ts b/apps/kyberswap-interface/src/components/LimitOrder/ProcessingOrder/useProcessingOrder.ts new file mode 100644 index 0000000000..39cf11c46e --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/ProcessingOrder/useProcessingOrder.ts @@ -0,0 +1,227 @@ +import { waitForTransactionReceipt } from '@wagmi/core' +import { Dispatch, SetStateAction } from 'react' + +import { wagmiConfig } from 'components/Web3Provider' +import { ApprovalState, ApprovalStatus } from 'hooks/useApproveCallback' + +export type ProcessingOrderStep = 'wrap' | 'approve' | 'create' | 'fill' +export type ProcessingOrderStepStatus = 'idle' | 'active' | 'success' | 'error' + +export type ProcessingOrderState = { + show: boolean + steps: ProcessingOrderStep[] + currentStep?: ProcessingOrderStep + errorStep?: ProcessingOrderStep + completedSteps: ProcessingOrderStep[] +} + +export type ProcessingOrderController = { + state: ProcessingOrderState + start: () => void + dismiss: () => void + retryStep: (step: ProcessingOrderStep) => void +} + +type UseProcessingOrderProps = { + processingOrder: ProcessingOrderState + setProcessingOrder: Dispatch> + chainId: number + approval: ApprovalState + approveCallback: () => Promise + checkApprovalManually: () => Promise + steps: ProcessingOrderStep[] + onWrap: (() => Promise) | undefined + onWrapSuccess?: () => void + finalStep: Extract + onFinalStep: () => Promise + onError?: (error: unknown, step: ProcessingOrderStep) => void + onStart?: () => void +} + +export const DEFAULT_PROCESSING_ORDER: ProcessingOrderState = { + show: false, + steps: [], + completedSteps: [], +} + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +export const useProcessingOrder = ({ + processingOrder, + setProcessingOrder, + chainId, + approval, + approveCallback, + checkApprovalManually, + steps, + onWrap, + onWrapSuccess, + finalStep, + onFinalStep, + onError, + onStart, +}: UseProcessingOrderProps): ProcessingOrderController => { + const markProcessingStepSuccess = (step: ProcessingOrderStep) => { + setProcessingOrder(state => { + if (!state.show || state.currentStep !== step) return state + const completedSteps = state.completedSteps.includes(step) + ? state.completedSteps + : [...state.completedSteps, step] + const nextStep = state.steps[state.steps.indexOf(step) + 1] + return { + ...state, + currentStep: nextStep, + completedSteps, + } + }) + } + + const markProcessingStepError = (step: ProcessingOrderStep) => { + setProcessingOrder(state => { + if (!state.show || state.currentStep !== step) return state + return { + ...state, + errorStep: step, + } + }) + } + + const hideProcessingOrder = () => { + setProcessingOrder(DEFAULT_PROCESSING_ORDER) + } + + const waitForManualApproval = async () => { + for (let attempt = 0; attempt < 8; attempt++) { + const hasEnoughAllowance = await checkApprovalManually() + if (hasEnoughAllowance) return true + await sleep(2000) + } + + return false + } + + const runWrapStep = async () => { + try { + const hash = await onWrap?.() + if (!hash) { + markProcessingStepError('wrap') + return false + } + const receipt = await waitForTransactionReceipt(wagmiConfig, { + chainId: chainId as (typeof wagmiConfig)['chains'][number]['id'], + hash: hash as `0x${string}`, + }) + if (receipt.status === 'reverted') { + markProcessingStepError('wrap') + return false + } + onWrapSuccess?.() + markProcessingStepSuccess('wrap') + return true + } catch (error) { + onError?.(error, 'wrap') + markProcessingStepError('wrap') + return false + } + } + + const runApproveStep = async () => { + try { + if (await checkApprovalManually()) { + markProcessingStepSuccess('approve') + return true + } + + if (approval !== ApprovalState.PENDING) { + const approvalStatus = await approveCallback() + if (approvalStatus === ApprovalStatus.REJECTED || approvalStatus === ApprovalStatus.FAILED) { + markProcessingStepError('approve') + return false + } + } + + const hasEnoughAllowance = await waitForManualApproval() + if (hasEnoughAllowance) { + markProcessingStepSuccess('approve') + return true + } + + markProcessingStepError('approve') + return false + } catch (error) { + onError?.(error, 'approve') + markProcessingStepError('approve') + return false + } + } + + const runFinalStep = async () => { + try { + const success = await onFinalStep() + if (success) { + markProcessingStepSuccess(finalStep) + return true + } + markProcessingStepError(finalStep) + return false + } catch (error) { + onError?.(error, finalStep) + markProcessingStepError(finalStep) + return false + } + } + + const runProcessingStep = (step: ProcessingOrderStep) => { + if (step === 'wrap') { + return runWrapStep() + } + + if (step === 'approve') { + return runApproveStep() + } + + return runFinalStep() + } + + const runProcessingSequence = async (firstStep: ProcessingOrderStep, processingSteps: ProcessingOrderStep[]) => { + const startIndex = processingSteps.indexOf(firstStep) + if (startIndex < 0) return + + for (const step of processingSteps.slice(startIndex)) { + setProcessingOrder(state => (state.show ? { ...state, currentStep: step, errorStep: undefined } : state)) + + const isStepSuccess = await runProcessingStep(step) + if (!isStepSuccess) return + } + } + + const retryProcessingStep = (step: ProcessingOrderStep) => { + setProcessingOrder(state => ({ + ...state, + currentStep: step, + errorStep: undefined, + })) + void runProcessingSequence(step, processingOrder.steps) + } + + const startProcessingOrder = () => { + const firstStep = steps[0] + if (!firstStep) return + + onStart?.() + setProcessingOrder({ + show: true, + steps, + currentStep: firstStep, + completedSteps: [], + }) + void runProcessingSequence(firstStep, steps) + } + + return { + state: processingOrder, + start: startProcessingOrder, + dismiss: hideProcessingOrder, + retryStep: retryProcessingStep, + } +} diff --git a/apps/kyberswap-interface/src/components/LimitOrder/TEST_SCRIPT.md b/apps/kyberswap-interface/src/components/LimitOrder/TEST_SCRIPT.md new file mode 100644 index 0000000000..1b4ae445ec --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/TEST_SCRIPT.md @@ -0,0 +1,222 @@ +# Limit Order QA Test Script + +Use the examples below as concrete scenarios. Token names are examples; any pair with the same balance/allowance setup is fine. + +Expected processing pattern: +- Create order: `Wrap native? -> Approve token -> Sign order` +- Take/fill order: `Wrap native? -> Approve token -> Fill order` +- `Wrap native` appears only when the required wrapped token balance is missing and native balance can cover the deficit. +- `Approve token` always appears. If allowance is already enough, it should auto-pass without wallet popup. + +## Create Order - ERC20 Input + +### 1. ERC20 balance enough, allowance already enough +- Example: create order `100 USDT -> 0.04 ETH`. +- Wallet balance: `500 USDT`. +- Existing allowance: `200 USDT`. +- Reserved active making amount: `0 USDT`. +- Expected processing steps: `Approve USDT`, `Sign order`. +- Expected tx/signature: approve step auto-passes; no approve tx; order signing/API create runs. + +### 2. ERC20 balance enough, allowance missing +- Example: create order `100 USDT -> 0.04 ETH`. +- Wallet balance: `500 USDT`. +- Existing allowance: `20 USDT`. +- Reserved active making amount: `0 USDT`. +- Expected processing steps: `Approve USDT`, `Sign order`. +- Expected tx/signature: approve USDT tx is requested; order signing starts after approval is detected. + +### 3. ERC20 allowance enough for input but not reserved active amount +- Example: create order `100 USDT -> 0.04 ETH`. +- Wallet balance: `500 USDT`. +- Existing allowance: `120 USDT`. +- Reserved active making amount: `50 USDT`. +- Required allowance check: `100 + 50 = 150 USDT`. +- Expected processing steps: `Approve USDT`, `Sign order`. +- Expected tx/signature: approve tx is requested because available allowance after reserved amount is only `70 USDT`. + +### 4. ERC20 invalid or zero amount +- Example: input amount is `0`, empty, or unparsable. +- Expected result: bottom button shows `Please enter a valid input amount`. +- Expected result: token input tooltip error is not shown separately. +- Expected result: review modal cannot open. + +## Create Order - Native / WETH Input + +### 5. Native ETH input, WETH balance and allowance already enough +- Example: create order `1 ETH -> 3,500 USDT`. +- Wallet balance: `2 ETH`, `1 WETH`. +- Existing WETH allowance: `2 WETH`. +- Reserved active making amount: `0 WETH`. +- Expected processing steps: `Approve WETH`, `Sign order`. +- Expected tx/signature: no wrap tx; approve step auto-passes; order signing/API create runs. + +### 6. Native ETH input, WETH balance enough but allowance missing +- Example: create order `1 ETH -> 3,500 USDT`. +- Wallet balance: `2 ETH`, `1 WETH`. +- Existing WETH allowance: `0 WETH`. +- Expected processing steps: `Approve WETH`, `Sign order`. +- Expected tx/signature: no wrap tx; approve WETH tx is requested; order signing starts after approval is detected. + +### 7. Native ETH input, WETH balance missing but ETH enough +- Example: create order `1 ETH -> 3,500 USDT`. +- Wallet balance: `2 ETH`, `0 WETH`. +- Existing WETH allowance: `0 WETH`. +- Expected wrap amount: `1 ETH`. +- Expected processing steps: `Wrap ETH`, `Approve WETH`, `Sign order`. +- Expected tx/signature: wrap `1 ETH`; approve WETH tx is requested; order signing starts after approval is detected. + +### 8. WETH input, WETH balance enough but allowance missing +- Example: create order `1 WETH -> 3,500 USDT`. +- Wallet balance: `1.5 WETH`, `0 ETH`. +- Existing WETH allowance: `0 WETH`. +- Expected processing steps: `Approve WETH`, `Sign order`. +- Expected tx/signature: no wrap tx; approve WETH tx is requested; order signing starts after approval is detected. + +### 9. WETH input, WETH balance partially missing but ETH enough +- Example: create order `1 WETH -> 3,500 USDT`. +- Wallet balance: `0.4 WETH`, `2 ETH`. +- Existing WETH allowance: `5 WETH`. +- Expected wrap amount: `0.6 ETH`. +- Expected processing steps: `Wrap ETH`, `Approve WETH`, `Sign order`. +- Expected tx/signature: wrap `0.6 ETH`; approve step auto-passes; order signing/API create runs. + +### 10. WETH input, WETH plus ETH insufficient +- Example: create order `1 WETH -> 3,500 USDT`. +- Wallet balance: `0.4 WETH`, `0.3 ETH`. +- Existing WETH allowance: any value. +- Required available amount: `0.4 + 0.3 = 0.7 WETH equivalent`. +- Expected result: bottom button shows `Insufficient Balance`. +- Expected result: review/processing cannot start. + +## Take / Fill Order - ERC20 Pay Token + +### 11. ERC20 pay token, allowance already enough +- Example: fill order requiring taker to pay `100 USDT` and receive `0.04 ETH`. +- Wallet balance: `500 USDT`. +- Existing USDT allowance: `200 USDT`. +- Expected processing steps: `Approve token`, `Fill order`. +- Expected tx: approve step auto-passes; no approve tx; fill tx is submitted. + +### 12. ERC20 pay token, allowance missing +- Example: fill order requiring taker to pay `100 USDT`. +- Wallet balance: `500 USDT`. +- Existing USDT allowance: `20 USDT`. +- Expected processing steps: `Approve token`, `Fill order`. +- Expected tx: approve USDT tx is requested; fill tx starts after approval is detected. + +### 13. Fill amount exceeds available amount +- Example: order has available amount `60 HYPE`. +- User enters fill amount `100 HYPE`. +- Expected result: submit button shows `Exceeds order available`. +- Expected result: processing modal cannot start. + +## Take / Fill Order - WETH Pay Token + +### 14. WETH pay token, WETH balance and allowance already enough +- Example: fill order requiring taker to pay `1 WETH`. +- Wallet balance: `2 WETH`, `0 ETH`. +- Existing WETH allowance: `2 WETH`. +- Expected processing steps: `Approve token`, `Fill order`. +- Expected tx: no wrap tx; approve step auto-passes; fill tx is submitted. + +### 15. WETH pay token, WETH balance enough but allowance missing +- Example: fill order requiring taker to pay `1 WETH`. +- Wallet balance: `1.5 WETH`, `0 ETH`. +- Existing WETH allowance: `0 WETH`. +- Expected processing steps: `Approve token`, `Fill order`. +- Expected tx: no wrap tx; approve WETH tx is requested; fill tx starts after approval is detected. + +### 16. WETH pay token, WETH balance missing but ETH enough +- Example: fill order requiring taker to pay `1 WETH`. +- Wallet balance: `0.4 WETH`, `2 ETH`. +- Existing WETH allowance: `5 WETH`. +- Expected wrap amount: `0.6 ETH`. +- Expected processing steps: `Wrap ETH`, `Approve token`, `Fill order`. +- Expected tx: wrap `0.6 ETH`; approve step auto-passes; fill tx is submitted. + +### 17. WETH pay token, WETH balance missing and allowance missing +- Example: fill order requiring taker to pay `1 WETH`. +- Wallet balance: `0.4 WETH`, `2 ETH`. +- Existing WETH allowance: `0 WETH`. +- Expected wrap amount: `0.6 ETH`. +- Expected processing steps: `Wrap ETH`, `Approve token`, `Fill order`. +- Expected tx: wrap `0.6 ETH`; approve WETH tx is requested; fill tx starts after approval is detected. + +### 18. WETH pay token, WETH plus ETH insufficient +- Example: fill order requiring taker to pay `1 WETH`. +- Wallet balance: `0.4 WETH`, `0.3 ETH`. +- Existing WETH allowance: any value. +- Required available amount: `0.7 WETH equivalent`, below `1 WETH`. +- Expected result: submit button shows `Insufficient Balance`. +- Expected result: processing modal cannot start. + +## Take / Fill Order - Fee and Encode Payload + +### 19. Fill threshold and taker fee display +- Example: fill order requiring taker to pay `100 HYPE`, order rate `1 HYPE = 35 USDT`, taker fee `0.4%`. +- Gross receive amount: `3,500 USDT`. +- UI `You Receive`: `3,486 USDT`. +- Expected encode payload: `thresholdAmount` equals raw amount for `3,500 USDT`, not `3,486 USDT`. + +## Processing Modal Behavior + +### 20. Approve step auto-pass +- Example: allowance is already enough before opening processing modal. +- Expected result: `Approve token` row is visible, then quickly changes to success without wallet popup. +- Applies to: create order and take order. + +### 21. Processing cannot be dismissed mid-step +- Example: start `Wrap ETH`, `Approve token`, `Sign order`, or `Fill order`. +- Expected result: clicking outside does not close modal. +- Expected result: close icon is disabled while the active step is running. +- Expected result: close works after success or error. + +### 22. Retry failed step +- Example: reject approve in wallet. +- Expected result: approve row becomes error. +- Expected result: `Retry` appears on the same row without layout jump. +- Expected result: retry reruns only approve step, not previous successful wrap step. + +### 23. Create success actions +- Example: create order finishes successfully. +- Expected result: modal shows `Close` and `View Order`. +- Expected result: `View Order` opens Limit page with `tab=my_order`. + +### 24. Take success actions +- Example: fill order tx is submitted successfully. +- Expected result: modal shows `Close`. +- Expected result: closing modal also closes the fill modal flow. + +## Cancel Order + +### 25. Gasless cancel refresh +- Example: cancel order `#123` with gasless cancel. +- Expected result: cancel request succeeds. +- Expected result: My Orders list refreshes. +- Expected result: insufficient-orders badge refreshes. + +### 26. Hard cancel refresh +- Example: cancel order `#123` with hard cancel. +- Expected result: cancel tx is submitted. +- Expected result: cancel tx is added to the transaction store with the cancelled order id. +- Expected result: My Orders shows the order as cancelling while the tx is pending. +- Expected result: My Orders list refreshes after tx/Firebase/order polling updates. +- Expected result: insufficient-orders badge refreshes. + +## URL / Tab Behavior + +### 27. Reserved order notice link +- Example: notice says `Some of your USDT is already reserved... here`. +- Action: click `here`. +- Expected result: confirm modal closes if it was open. +- Expected URL params: + - `tab=my_order` + - `orderTab=active` + - `search=` + +### 28. View created order +- Example: create order on Ethereum pair `USDT -> ETH`. +- Action: click `View Order` after success. +- Expected result: opens Limit route for the current network/pair. +- Expected URL param: `tab=my_order`. diff --git a/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/RateComparison.tsx b/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/RateComparison.tsx new file mode 100644 index 0000000000..e3ddf26f71 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/RateComparison.tsx @@ -0,0 +1,138 @@ +import { Currency, CurrencyAmount, Price } from '@kyberswap/ks-sdk-core' +import { Trans } from '@lingui/macro' +import { useEffect } from 'react' +import { parseGetRouteResponse } from 'services/route/utils' + +import { removeTrailingZero } from 'components/LimitOrder/utils' +import Skeleton from 'components/Skeleton' +import { HStack, Stack } from 'components/Stack' +import useGetRoute from 'components/SwapForm/hooks/useGetRoute' +import { cn } from 'utils/cn' +import { formatDisplayNumber } from 'utils/numbers' + +export const MARKET_DIFF_WARNING_THRESHOLD = 100 + +const formatPriceRate = (price: Price | undefined) => + price + ? `1 ${price.baseCurrency.symbol} = ${removeTrailingZero(price.toSignificant(8))} ${price.quoteCurrency.symbol}` + : '--' + +const DetailRow = ({ label, children }: { label: React.ReactNode; children: React.ReactNode }) => ( + + {label} +
{children}
+
+) + +type Props = { + marketDiffPercent: number + inputCurrency: Currency + outputCurrency: Currency + inputAmount: CurrencyAmount | undefined + outputAmount: CurrencyAmount | undefined + fallbackOrderPrice?: Price +} + +const RateComparison = ({ + marketDiffPercent, + inputCurrency, + outputCurrency, + inputAmount, + outputAmount, + fallbackOrderPrice, +}: Props) => { + const { + fetcher: getSwapRoute, + result: swapRouteResult, + isLoading: swapRouteLoading, + } = useGetRoute({ + currencyIn: inputCurrency, + currencyOut: outputCurrency, + parsedAmount: inputAmount, + customChain: inputCurrency.wrapped.chainId, + }) + + const swapRouteSummary = (() => { + if (!swapRouteResult.data?.data || swapRouteResult.error) return undefined + + return parseGetRouteResponse(swapRouteResult.data.data, inputCurrency, outputCurrency).routeSummary + })() + + const orderExecutionPrice = (() => { + if (!inputAmount || !outputAmount) return fallbackOrderPrice + if (inputAmount.equalTo(0) || outputAmount.equalTo(0)) return fallbackOrderPrice + + return new Price(inputCurrency, outputCurrency, inputAmount.quotient, outputAmount.quotient) + })() + + const swapRouteOutputDelta = (() => { + if (!swapRouteSummary?.parsedAmountOut || !outputAmount) return undefined + + const swapOutput = Number(swapRouteSummary.parsedAmountOut.toExact()) + const orderOutput = Number(outputAmount.toExact()) + if (!swapOutput || !orderOutput) return undefined + + return (swapOutput / orderOutput - 1) * 100 + })() + + const isSwapRouteBetter = + !!swapRouteSummary?.parsedAmountOut && !!outputAmount && swapRouteSummary.parsedAmountOut.greaterThan(outputAmount) + + const shouldWarningOrderRate = marketDiffPercent > MARKET_DIFF_WARNING_THRESHOLD + const shouldSuccessOrderRate = marketDiffPercent < 0 + const showSwapOfferNotice = swapRouteLoading ? marketDiffPercent > 0 : isSwapRouteBetter + + useEffect(() => { + if (!inputAmount || inputAmount.equalTo(0)) return + getSwapRoute() + }, [getSwapRoute, inputAmount]) + + return ( + + + Rate Comparison + + + + This order (after fee) + + } + > + + {formatPriceRate(orderExecutionPrice)} + + + Swap best route}> + + + {swapRouteLoading ? ( + + ) : ( + formatPriceRate(swapRouteSummary?.executionPrice) + )} + + {!swapRouteLoading && swapRouteOutputDelta !== undefined && ( + + {swapRouteOutputDelta > 0 ? '+' : ''} + {formatDisplayNumber(swapRouteOutputDelta, { + significantDigits: 4, + allowDisplayNegative: true, + })} + % + + )} + + + {showSwapOfferNotice && ( + + 💡 Swap offers a better rate. You can still fill this order directly if you prefer. + + )} + + + ) +} + +export default RateComparison diff --git a/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/TakeOrderConfirmModal.tsx b/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/TakeOrderConfirmModal.tsx new file mode 100644 index 0000000000..4f0b7db6ee --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/TakeOrderConfirmModal.tsx @@ -0,0 +1,467 @@ +import { Currency, CurrencyAmount, Price, Token } from '@kyberswap/ks-sdk-core' +import { Trans, t } from '@lingui/macro' +import JSBI from 'jsbi' +import { useEffect, useMemo, useState } from 'react' +import { Repeat } from 'react-feather' +import { useNavigate } from 'react-router-dom' + +import { ButtonOutlined, ButtonPrimary } from 'components/Button' +import CurrencyLogo from 'components/CurrencyLogo' +import WalletIcon from 'components/Icons/Wallet' +import ProcessingOrderModal from 'components/LimitOrder/ProcessingOrder/ProcessingOrderModal' +import { DEFAULT_PROCESSING_ORDER, useProcessingOrder } from 'components/LimitOrder/ProcessingOrder/useProcessingOrder' +import RateComparison, { MARKET_DIFF_WARNING_THRESHOLD } from 'components/LimitOrder/TakeOrder/RateComparison' +import { useTakeLimitOrder } from 'components/LimitOrder/TakeOrder/useTakeLimitOrder' +import { + LimitOrderFromTokenPairFormatted, + LimitOrderStatus, + LimitOrderTab, + LimitOrderTakeContext, +} from 'components/LimitOrder/types' +import { removeTrailingZero } from 'components/LimitOrder/utils' +import Modal from 'components/Modal' +import NumericalInput from 'components/NumericalInput' +import { HStack, Stack } from 'components/Stack' +import { APP_PATHS } from 'constants/index' +import { NETWORKS_INFO } from 'hooks/useChainsConfig' +import { useLimitState } from 'state/limit/hooks' +import { PriceType, useTokenPrices } from 'state/tokenPrices/hooks' +import { CloseIcon } from 'theme/components' +import { cn } from 'utils/cn' +import { formatDisplayNumber } from 'utils/numbers' + +const formatExact = (amount: CurrencyAmount | undefined, significantDigits = 6) => + amount ? formatDisplayNumber(amount.toExact(), { significantDigits }) : '--' + +const formatRate = (context: LimitOrderTakeContext) => { + const receiveAmount = CurrencyAmount.fromRawAmount(context.receiveCurrency, context.order.makingAmount) + const payAmount = CurrencyAmount.fromRawAmount(context.payCurrency, context.order.takingAmount) + const rate = receiveAmount.divide(payAmount).multiply(payAmount.decimalScale).toSignificant(8) + return `1 ${context.payCurrency.symbol} = ${removeTrailingZero(rate)} ${context.receiveCurrency.symbol}` +} + +const formatInvertedRate = (context: LimitOrderTakeContext) => { + const receiveAmount = CurrencyAmount.fromRawAmount(context.receiveCurrency, context.order.makingAmount) + const payAmount = CurrencyAmount.fromRawAmount(context.payCurrency, context.order.takingAmount) + const rate = payAmount.divide(receiveAmount).multiply(receiveAmount.decimalScale).toSignificant(8) + return `1 ${context.receiveCurrency.symbol} = ${removeTrailingZero(rate)} ${context.payCurrency.symbol}` +} + +const DetailRow = ({ label, children }: { label: React.ReactNode; children: React.ReactNode }) => ( + + {label} +
{children}
+
+) + +const TokenBadge = ({ + amount, + currency, + symbol, +}: { + amount?: CurrencyAmount + currency?: Currency + symbol?: string +}) => { + const badgeCurrency = currency || amount?.currency + + return ( + + {badgeCurrency && } + {symbol} + + ) +} + +const PairLogos = ({ payCurrency, receiveCurrency }: { payCurrency: Currency; receiveCurrency: Currency }) => ( + + + + + + + + +) + +const getPercentFillAmount = (amount: CurrencyAmount | undefined, percent: number) => { + if (!amount) return '' + + const rawAmount = JSBI.divide(JSBI.multiply(amount.quotient, JSBI.BigInt(percent)), JSBI.BigInt(100)) + return CurrencyAmount.fromRawAmount(amount.currency, rawAmount).toExact() +} + +const normalizeActionAmount = (nextAmount: string) => (parseFloat(nextAmount || '0') > 0 ? nextAmount : '') + +const QUICK_FILL_PERCENTS = [25, 50, 75, 100] +const FEE_BPS_BASE = JSBI.BigInt(10_000) + +const getSwapCurrencyId = (currency: Currency | undefined) => + currency ? (currency.isNative ? currency.symbol?.toLowerCase() || '' : currency.wrapped.address.toLowerCase()) : '' + +const ceilDivide = (numerator: JSBI, denominator: JSBI) => { + if (JSBI.equal(denominator, JSBI.BigInt(0))) return JSBI.BigInt(0) + return JSBI.divide(JSBI.add(numerator, JSBI.subtract(denominator, JSBI.BigInt(1))), denominator) +} + +const getOrderPriceAfterFee = (context: LimitOrderTakeContext, feeBps: number) => { + const payRaw = JSBI.BigInt(context.order.takingAmount) + const receiveRaw = JSBI.BigInt(context.order.makingAmount) + const adjustedPayRaw = + context.order.isTakerAssetFee && feeBps > 0 + ? ceilDivide(JSBI.multiply(payRaw, JSBI.BigInt(10_000 + feeBps)), FEE_BPS_BASE) + : payRaw + const adjustedReceiveRaw = + !context.order.isTakerAssetFee && feeBps > 0 + ? JSBI.divide(JSBI.multiply(receiveRaw, JSBI.BigInt(10_000 - feeBps)), FEE_BPS_BASE) + : receiveRaw + + if (JSBI.equal(adjustedPayRaw, JSBI.BigInt(0)) || JSBI.equal(adjustedReceiveRaw, JSBI.BigInt(0))) return undefined + + return new Price(context.payCurrency, context.receiveCurrency, adjustedPayRaw, adjustedReceiveRaw) +} + +type Props = { + isOpen: boolean + order: LimitOrderFromTokenPairFormatted + onDismiss?: () => void +} + +const TakeOrderConfirmModal = ({ isOpen, order, onDismiss }: Props) => { + const navigate = useNavigate() + const { currencyIn: makerCurrency, currencyOut: takerCurrency } = useLimitState() + + const [fillAmount, setFillAmount] = useState('') + const [showInvertedRate, setShowInvertedRate] = useState(false) + const [estimatedGasUsd, setEstimatedGasUsd] = useState('') + const [processingState, setProcessingState] = useState(DEFAULT_PROCESSING_ORDER) + + const context = useMemo(() => { + const rawOrder = order.rawOrder + const getOrderCurrencySymbol = (asset: string, fallback?: string) => { + const assetAddress = asset.toLowerCase() + if (makerCurrency && assetAddress === makerCurrency.wrapped.address.toLowerCase()) + return makerCurrency.wrapped.symbol + if (takerCurrency && assetAddress === takerCurrency.wrapped.address.toLowerCase()) + return takerCurrency.wrapped.symbol + return fallback + } + + const paySymbol = getOrderCurrencySymbol(rawOrder.takerAsset, rawOrder.takerAssetSymbol) + const receiveSymbol = getOrderCurrencySymbol(rawOrder.makerAsset, rawOrder.makerAssetSymbol) + const payCurrency = new Token(rawOrder.chainId, rawOrder.takerAsset, rawOrder.takerAssetDecimals, paySymbol) + const receiveCurrency = new Token(rawOrder.chainId, rawOrder.makerAsset, rawOrder.makerAssetDecimals, receiveSymbol) + + return { order: rawOrder, payCurrency, receiveCurrency } + }, [makerCurrency, order, takerCurrency]) + + const takeOrder = useTakeLimitOrder({ + context, + fillAmount, + }) + + const { + maxPayAmount, + maxBalancePayAmount, + parsedPayAmount, + requiredPayAmount, + receiveAmount, + receiveAmountAfterFee, + feeBps, + balance, + wrapAmount, + exceedsAvailableAmount, + insufficientBalance, + canSubmit, + } = takeOrder.amount + const { estimateTxGas } = takeOrder + const processing = useProcessingOrder({ + processingOrder: processingState, + setProcessingOrder: setProcessingState, + ...takeOrder.processing, + }) + + const payTokenAddress = context.payCurrency.wrapped.address + const tokenPrices = useTokenPrices([payTokenAddress], context.order.chainId, PriceType.Average) + + const isConfirmOpen = isOpen && !processing.state.show + const fillAmountUsd = parsedPayAmount ? Number(parsedPayAmount.toExact()) * tokenPrices[payTokenAddress] : 0 + const receiveAmountForComparison = receiveAmountAfterFee || receiveAmount + const orderRate = useMemo(() => formatRate(context), [context]) + const invertedRate = useMemo(() => formatInvertedRate(context), [context]) + const orderPriceAfterFee = useMemo(() => getOrderPriceAfterFee(context, feeBps), [context, feeBps]) + const shouldWarnMarketDiff = order.marketDiffPercent > MARKET_DIFF_WARNING_THRESHOLD + + const rate = (() => { + if (!showInvertedRate) return orderRate + + return invertedRate + })() + + const fillAmountError = insufficientBalance || exceedsAvailableAmount + const fillAmountErrorMessage = insufficientBalance + ? t`Insufficient Balance` + : exceedsAvailableAmount + ? t`Exceeds order available` + : '' + + const fillAmountHelperMessage = wrapAmount && ( + + You need to wrap {formatExact(wrapAmount)} {wrapAmount.currency.symbol} before filling this order + + ) + + const walletBalance = balance?.currency.equals(context.payCurrency) ? balance : undefined + const defaultPayAmount = useMemo(() => { + if (!maxPayAmount) return undefined + if (!maxBalancePayAmount) return maxPayAmount + if (JSBI.equal(maxBalancePayAmount.quotient, JSBI.BigInt(0))) return maxPayAmount + + return maxBalancePayAmount.lessThan(maxPayAmount) ? maxBalancePayAmount : maxPayAmount + }, [maxBalancePayAmount, maxPayAmount]) + + useEffect(() => { + setFillAmount(normalizeActionAmount(defaultPayAmount?.toExact() || '')) + }, [defaultPayAmount]) + + useEffect(() => { + const controller = new AbortController() + const fetchGas = async () => { + try { + if (!isConfirmOpen || !canSubmit) { + setEstimatedGasUsd('') + return + } + const gas = await estimateTxGas() + if (controller.signal.aborted) return + setEstimatedGasUsd(gas?.gasInUsd ? gas.gasInUsd.toString() : '') + } catch { + if (!controller.signal.aborted) setEstimatedGasUsd('') + } + } + fetchGas() + return () => controller.abort() + }, [canSubmit, estimateTxGas, isConfirmOpen]) + + const handleDismiss = () => { + onDismiss?.() + } + + const handleSubmit = () => { + if (!canSubmit) return + processing.start() + } + + const handleUseSwapInstead = () => { + const route = NETWORKS_INFO[context.order.chainId]?.route + const inputCurrency = getSwapCurrencyId(context.payCurrency) + const outputCurrency = getSwapCurrencyId(context.receiveCurrency) + if (!route || !inputCurrency || !outputCurrency) return + + const search = new URLSearchParams() + const input = requiredPayAmount?.toExact() || parsedPayAmount?.toExact() + if (input) search.set('input', input) + + navigate( + `${APP_PATHS.SWAP}/${route}/${encodeURIComponent(inputCurrency)}-to-${encodeURIComponent(outputCurrency)}${ + search.toString() ? `?${search.toString()}` : '' + }`, + ) + } + + const handleProcessingDismiss = () => { + processing.dismiss() + onDismiss?.() + } + + const handleViewOrder = () => { + const route = NETWORKS_INFO[context.order.chainId]?.route + if (!route) return + + const search = new URLSearchParams({ + tab: LimitOrderTab.MY_ORDER, + orderTab: LimitOrderStatus.CLOSED, + }).toString() + + navigate(`${APP_PATHS.LIMIT}/${route}?${search}`) + } + + return ( + <> + + + + + Fill Order + + + + + + + + + {context.payCurrency.symbol}/{context.receiveCurrency.symbol} + + + + + + + Order Rate + + setShowInvertedRate(value => !value)} + > + {rate} + + + + + + + + + + + Fill Amount + + + + + + {QUICK_FILL_PERCENTS.map(percent => { + const percentAmount = getPercentFillAmount(maxBalancePayAmount, percent) + return ( + + ) + })} + + + + + + + {!!fillAmountUsd && ( + + ~{formatDisplayNumber(fillAmountUsd, { significantDigits: 6, style: 'currency' })} + + )} + + + + + {fillAmountErrorMessage || fillAmountHelperMessage} + + + + + Protocol Fee}>{feeBps ? `${feeBps / 100}%` : '0%'} + You Receive}> + + {formatExact(receiveAmountAfterFee || receiveAmount)} {context.receiveCurrency.symbol} + + + Gas Fee}> + {estimatedGasUsd + ? `~${formatDisplayNumber(estimatedGasUsd, { style: 'currency', significantDigits: 4 })}` + : '--'} + + + + + + + {shouldWarnMarketDiff ? ( + <> + + Use Swap Instead + + + Fill order anyway + + + ) : ( + <> + + Use Swap Instead + + + Fill this order + + + )} + + + + + + + ) +} + +export default TakeOrderConfirmModal diff --git a/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/useTakeLimitOrder.ts b/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/useTakeLimitOrder.ts new file mode 100644 index 0000000000..802abef805 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/TakeOrder/useTakeLimitOrder.ts @@ -0,0 +1,299 @@ +import { Currency, CurrencyAmount } from '@kyberswap/ks-sdk-core' +import { t } from '@lingui/macro' +import JSBI from 'jsbi' +import { useCallback, useMemo } from 'react' +import { + FillOrderBody, + useEncodeFillOrderMutation, + useGetLOConfigQuery, + useLazyGetOperatorSignatureQuery, +} from 'services/limitOrder' + +import { NotificationType } from 'components/Announcement/type' +import { ProcessingOrderStep } from 'components/LimitOrder/ProcessingOrder/useProcessingOrder' +import { useLimitOrderApproval } from 'components/LimitOrder/hooks/useLimitOrderApproval' +import { useLimitOrderWrapStep } from 'components/LimitOrder/hooks/useLimitOrderWrapStep' +import { LimitOrderTakeContext } from 'components/LimitOrder/types' +import { getErrorMessage } from 'components/LimitOrder/utils' +import { RTK_QUERY_TAGS } from 'constants/index' +import { useActiveWeb3React, useWeb3React } from 'hooks' +import { useApproveCallback } from 'hooks/useApproveCallback' +import { useInvalidateTagLimitOrder } from 'hooks/useInvalidateTags' +import { useNotify } from 'state/application/hooks' +import { tryParseAmount } from 'state/swap/hooks' +import { useTransactionAdder } from 'state/transactions/hooks' +import { TRANSACTION_TYPE } from 'state/transactions/type' +import { useCurrencyBalance } from 'state/wallet/hooks' +import { sendEVMTransaction } from 'utils/sendTransaction' +import { ErrorName } from 'utils/transactionError' +import useEstimateGasTxs from 'utils/useEstimateGasTxs' + +const safeDivide = (numerator: JSBI, denominator: JSBI) => { + if (JSBI.equal(denominator, JSBI.BigInt(0))) return JSBI.BigInt(0) + return JSBI.divide(numerator, denominator) +} + +const BPS_BASE = JSBI.BigInt(10_000) + +const ceilDivide = (numerator: JSBI, denominator: JSBI) => { + if (JSBI.equal(denominator, JSBI.BigInt(0))) return JSBI.BigInt(0) + return JSBI.divide(JSBI.add(numerator, JSBI.subtract(denominator, JSBI.BigInt(1))), denominator) +} + +const getAvailablePayAmount = ({ order, payCurrency }: LimitOrderTakeContext) => { + const totalPayRaw = JSBI.BigInt(order.takingAmount) + const totalReceiveRaw = JSBI.BigInt(order.makingAmount) + const availableReceiveRaw = JSBI.BigInt(order.availableMakingAmount) + const availablePayRaw = safeDivide(JSBI.multiply(totalPayRaw, availableReceiveRaw), totalReceiveRaw) + return CurrencyAmount.fromRawAmount(payCurrency, availablePayRaw) +} + +type GetReceiveAmountProps = { + payAmount: CurrencyAmount | undefined + context: LimitOrderTakeContext +} + +const getReceiveAmount = ({ payAmount, context }: GetReceiveAmountProps) => { + if (!payAmount) return undefined + const { order, receiveCurrency } = context + const receiveRaw = safeDivide( + JSBI.multiply(payAmount.quotient, JSBI.BigInt(order.makingAmount)), + JSBI.BigInt(order.takingAmount), + ) + return CurrencyAmount.fromRawAmount(receiveCurrency, receiveRaw) +} + +const getFeeBps = (feePercent?: string) => { + const fee = Number(feePercent || 0) + if (!Number.isFinite(fee) || fee <= 0) return 0 + return Math.min(Math.round(fee > 1 ? fee : fee * 100), 10_000) +} + +const subtractFee = (amount: CurrencyAmount | undefined, feeBps: number) => { + if (!amount || feeBps <= 0) return amount + const raw = JSBI.divide(JSBI.multiply(amount.quotient, JSBI.BigInt(10_000 - feeBps)), BPS_BASE) + return CurrencyAmount.fromRawAmount(amount.currency, raw) +} + +const addFee = (amount: CurrencyAmount | undefined, feeBps: number) => { + if (!amount || feeBps <= 0) return amount + const raw = ceilDivide(JSBI.multiply(amount.quotient, JSBI.BigInt(10_000 + feeBps)), BPS_BASE) + return CurrencyAmount.fromRawAmount(amount.currency, raw) +} + +const getMaxAmountBeforeTakerFee = (amount: CurrencyAmount | undefined, feeBps: number) => { + if (!amount || feeBps <= 0) return amount + const raw = safeDivide(JSBI.multiply(amount.quotient, BPS_BASE), JSBI.BigInt(10_000 + feeBps)) + return CurrencyAmount.fromRawAmount(amount.currency, raw) +} + +type UseTakeLimitOrderProps = { + context: LimitOrderTakeContext + fillAmount: string +} + +export const useTakeLimitOrder = ({ context, fillAmount }: UseTakeLimitOrderProps) => { + const { account, walletKey } = useActiveWeb3React() + const { isSmartConnector } = useWeb3React() + const notify = useNotify() + const addTransactionWithType = useTransactionAdder() + const estimateGas = useEstimateGasTxs() + const invalidateLimitOrderTags = useInvalidateTagLimitOrder() + + const order = context.order + const payCurrency = context.payCurrency + const receiveCurrency = context.receiveCurrency + const chainId = order.chainId + + const { currentData: config } = useGetLOConfigQuery(chainId) + const contractAddress = order.contractAddress || config?.contract || '' + const [getOperatorSignature] = useLazyGetOperatorSignatureQuery() + const [encodeFillOrder] = useEncodeFillOrderMutation() + + const maxPayAmount = useMemo(() => getAvailablePayAmount(context), [context]) + const parsedPayAmount = useMemo(() => tryParseAmount(fillAmount, payCurrency), [fillAmount, payCurrency]) + const receiveAmount = useMemo( + () => getReceiveAmount({ payAmount: parsedPayAmount, context }), + [context, parsedPayAmount], + ) + const feeBps = getFeeBps(order.makerTokenFeePercent) + const receiveAmountAfterFee = useMemo( + () => (order.isTakerAssetFee ? receiveAmount : subtractFee(receiveAmount, feeBps)), + [feeBps, order.isTakerAssetFee, receiveAmount], + ) + const thresholdAmount = receiveAmount?.quotient.toString() || '0' + + const balance = useCurrencyBalance(payCurrency, chainId) + const requiredPayAmount = useMemo( + () => (order.isTakerAssetFee ? addFee(parsedPayAmount, feeBps) : parsedPayAmount), + [feeBps, order.isTakerAssetFee, parsedPayAmount], + ) + const maxBalancePayAmount = useMemo( + () => (order.isTakerAssetFee ? getMaxAmountBeforeTakerFee(balance, feeBps) : balance), + [balance, feeBps, order.isTakerAssetFee], + ) + const { + insufficientBalance, + onWrap, + wrapAmount: wrapAmountForOrder, + } = useLimitOrderWrapStep({ + chainId, + currency: payCurrency, + amount: requiredPayAmount, + balance, + }) + const exceedsAvailableAmount = (() => { + if (!parsedPayAmount || !maxPayAmount) return false + return parsedPayAmount.greaterThan(maxPayAmount) + })() + + const canSubmit = + !!contractAddress && + !!parsedPayAmount && + JSBI.greaterThan(parsedPayAmount.quotient, JSBI.BigInt(0)) && + !exceedsAvailableAmount && + !insufficientBalance + + const [approval, approveCallback] = useApproveCallback({ + amount: requiredPayAmount, + spender: contractAddress || undefined, + forceApprove: true, + }) + + const checkApprovalManually = useLimitOrderApproval({ + account, + amount: requiredPayAmount, + chainId, + currency: payCurrency, + spender: contractAddress, + passWhenInvalidInput: true, + }) + + const buildFillOrderBody = useCallback(async (): Promise => { + if (!account || !parsedPayAmount) throw new Error('Wrong input') + + const operatorSignatures = await getOperatorSignature({ chainId, orderIds: [order.id] }).unwrap() + const operatorSignature = operatorSignatures.find(item => item.id === order.id) + if (!operatorSignature?.operatorSignature) throw new Error('Missing operator signature') + + return { + orderId: order.id, + takingAmount: parsedPayAmount.quotient.toString(), + thresholdAmount, + target: account, + operatorSignature: operatorSignature.operatorSignature, + } + }, [account, getOperatorSignature, order, chainId, parsedPayAmount, thresholdAmount]) + + const submitFillOrder = useCallback(async () => { + if (!account || !parsedPayAmount || !contractAddress) { + throw new Error('Wrong input') + } + + const fillBody = await buildFillOrderBody() + const { encodedData } = await encodeFillOrder(fillBody).unwrap() + const response = await sendEVMTransaction({ + account, + contractAddress, + encodedData, + value: 0n, + isSmartConnector, + errorInfo: { + name: ErrorName.LimitOrderError, + wallet: walletKey, + }, + chainId, + }) + + if (!response?.hash) throw new Error('Transaction was not submitted') + + addTransactionWithType({ + hash: response.hash, + desiredChainId: chainId, + type: TRANSACTION_TYPE.FILL_LIMIT_ORDER, + extraInfo: { + tokenAddressIn: payCurrency.wrapped.address, + tokenAddressOut: receiveCurrency.wrapped.address, + tokenSymbolIn: payCurrency.symbol || '', + tokenSymbolOut: receiveCurrency.symbol || '', + tokenAmountIn: requiredPayAmount?.toExact() || parsedPayAmount.toExact(), + tokenAmountOut: receiveAmountAfterFee?.toExact() || receiveAmount?.toExact() || '', + arbitrary: { + order_id: order.id, + type: 'fill_limit_order', + }, + }, + }) + invalidateLimitOrderTags([ + RTK_QUERY_TAGS.GET_LIMIT_ORDER_LIST, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_BOOK, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_INSUFFICIENT, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_ACTIVE_MAKING_AMOUNT, + ]) + return true + }, [ + account, + addTransactionWithType, + buildFillOrderBody, + chainId, + contractAddress, + encodeFillOrder, + invalidateLimitOrderTags, + isSmartConnector, + order, + parsedPayAmount, + payCurrency, + receiveAmount, + receiveAmountAfterFee, + receiveCurrency, + requiredPayAmount, + walletKey, + ]) + + const processingSteps = useMemo(() => { + const steps: ProcessingOrderStep[] = [] + if (wrapAmountForOrder) steps.push('wrap') + steps.push('approve') + steps.push('fill') + return steps + }, [wrapAmountForOrder]) + + const estimateTxGas = useCallback(async () => { + if (!account || !order || !parsedPayAmount || !contractAddress) return null + const fillBody = await buildFillOrderBody() + const { encodedData } = await encodeFillOrder(fillBody).unwrap() + return estimateGas({ contractAddress, encodedData }) + }, [account, buildFillOrderBody, contractAddress, encodeFillOrder, estimateGas, order, parsedPayAmount]) + + return { + amount: { + maxPayAmount, + maxBalancePayAmount, + parsedPayAmount, + requiredPayAmount, + receiveAmount, + receiveAmountAfterFee, + feeBps, + balance, + wrapAmount: wrapAmountForOrder, + exceedsAvailableAmount, + insufficientBalance, + canSubmit, + }, + processing: { + chainId, + approval, + approveCallback: () => approveCallback(requiredPayAmount), + checkApprovalManually, + steps: processingSteps, + onWrap, + finalStep: 'fill' as const, + onFinalStep: submitFillOrder, + onError: (error: unknown, step: ProcessingOrderStep) => { + const title = step === 'wrap' ? t`Wrap Error` : step === 'approve' ? t`Approve Error` : t`Fill Order Error` + notify({ type: NotificationType.ERROR, title, summary: getErrorMessage(error) }) + }, + }, + estimateTxGas, + } +} diff --git a/apps/kyberswap-interface/src/components/LimitOrder/components.tsx b/apps/kyberswap-interface/src/components/LimitOrder/components.tsx new file mode 100644 index 0000000000..62497dd935 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/components.tsx @@ -0,0 +1,223 @@ +import { Currency } from '@kyberswap/ks-sdk-core' +import { Trans } from '@lingui/macro' +import { ReactNode, useState } from 'react' +import { Repeat } from 'react-feather' +import { Link } from 'react-router-dom' + +import { LimitOrder, RateInfo } from 'components/LimitOrder/types' +import { formatRateLimitOrder, removeTrailingZero } from 'components/LimitOrder/utils' +import { HStack, Stack } from 'components/Stack' +import { NativeCurrencies } from 'constants/tokens' +import { cn } from 'utils/cn' + +const Label = ({ children, className, style, ...rest }: React.HTMLAttributes) => ( +
+ {children} +
+) + +export const ReservedOrderNotice = ({ symbol, to }: { symbol: string | undefined; to: string }) => ( + + + Notice: Some of your {symbol} is already reserved by an open Limit Order - + review it here. + + +) + +type SummaryRowType = { label: ReactNode; content: ReactNode } + +type OrderSummaryProps = { + title?: ReactNode + inputCurrency: ReactNode + outputCurrency: ReactNode + currencyIn?: Currency + currencyOut?: Currency + rateInfo?: RateInfo + order?: LimitOrder + expires?: ReactNode + marketRate?: ReactNode + className?: string +} + +const SummaryRow = ({ label, content }: SummaryRowType) => ( + +
{label}
+ + {content} + +
+) + +const formatRateValue = (value?: string | number) => { + if (value === undefined || value === null || value === '') return '--' + const numberValue = typeof value === 'number' ? value : Number(value) + if (!Number.isFinite(numberValue)) return '--' + return removeTrailingZero(numberValue.toPrecision(6)) +} + +const RateValue = ({ + currencyIn, + currencyOut, + rateInfo, + order, +}: { + currencyIn?: Currency + currencyOut?: Currency + rateInfo?: RateInfo + order?: LimitOrder +}) => { + const [showInverted, setShowInverted] = useState(false) + + let baseSymbol: string | undefined + let quoteSymbol: string | undefined + let rate: string | undefined + let referenceRate: string | undefined + + if (order) { + const native = NativeCurrencies[order.chainId] + const isNative = + order.nativeOutput && order.takerAssetSymbol.toLowerCase() === native?.wrapped.symbol?.toLowerCase() + const takerSymbol = isNative ? native?.symbol || order.takerAssetSymbol : order.takerAssetSymbol + + baseSymbol = showInverted ? order.makerAssetSymbol : takerSymbol + quoteSymbol = showInverted ? takerSymbol : order.makerAssetSymbol + rate = formatRateLimitOrder(order, showInverted) + referenceRate = formatRateLimitOrder(order, !showInverted) + } else { + const baseCurrency = showInverted ? currencyIn : currencyOut + const quoteCurrency = showInverted ? currencyOut : currencyIn + + baseSymbol = baseCurrency?.symbol + quoteSymbol = quoteCurrency?.symbol + rate = showInverted ? rateInfo?.rate : rateInfo?.invertRate + referenceRate = showInverted ? rateInfo?.invertRate : rateInfo?.rate + } + + if (!baseSymbol || !quoteSymbol) return -- + + const displayRate = order ? rate || '--' : formatRateValue(rate) + const displayReferenceRate = order ? referenceRate || '--' : formatRateValue(referenceRate) + + return ( + setShowInverted(value => !value)} + > + + 1 {baseSymbol} = {displayRate} {quoteSymbol} + + ~{displayReferenceRate} + + + ) +} + +export const OrderSummary = ({ + title, + inputCurrency, + outputCurrency, + currencyIn, + currencyOut, + rateInfo, + order, + expires, + marketRate, + className, +}: OrderSummaryProps) => { + const rows = [ + { label: I pay, content: inputCurrency }, + { label: and receive, content: outputCurrency }, + { + label: when, + content: , + }, + ...(expires ? [{ label: before the order expires on, content: expires }] : []), + ] + + return ( + + {title && } + + {rows.map((item, index) => ( + + ))} + + {marketRate && ( + + Market Price} content={marketRate} /> + + )} + + ) +} + +const formatAmountWithSymbol = (amount: string, symbol?: string) => `${amount} ${symbol ?? ''}`.trim() + +type ClippedTextProps = { + children: ReactNode + className?: string + title?: string +} + +export const ClippedText = ({ children, className, title }: ClippedTextProps) => ( +
+ + {children} + +
+) + +type AmountWithSymbolProps = { + amount?: string + symbol?: string + muted?: boolean +} + +export const AmountWithSymbol = ({ amount, symbol, muted }: AmountWithSymbolProps) => ( +
+ {amount ? ( + <> + {amount} + {symbol && {symbol}} + + ) : ( + '--' + )} +
+) + +type SizeInfoProps = { + amount: string + symbol?: string + filledPercentText: string + filledProgressPercent: number +} + +export const SizeInfo = ({ amount, symbol, filledPercentText, filledProgressPercent }: SizeInfoProps) => ( +
+ +
+ + + + + Fill {filledPercentText}% + +
+
+) diff --git a/apps/kyberswap-interface/src/components/LimitOrder/hooks/useLimitOrderApproval.ts b/apps/kyberswap-interface/src/components/LimitOrder/hooks/useLimitOrderApproval.ts new file mode 100644 index 0000000000..5cbcd5199a --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/hooks/useLimitOrderApproval.ts @@ -0,0 +1,43 @@ +import { ChainId, Currency, CurrencyAmount, TokenAmount } from '@kyberswap/ks-sdk-core' +import { readContract } from '@wagmi/core' + +import { wagmiConfig } from 'components/Web3Provider' +import { ERC20_ABI } from 'constants/abis' +import { Address } from 'utils/viem' + +type UseLimitOrderApprovalProps = { + account: string | undefined + amount: CurrencyAmount | undefined + chainId: ChainId + currency: Currency | undefined + spender: string | undefined + isAllowanceEnough?: (allowance: TokenAmount) => boolean + passWhenInvalidInput?: boolean +} + +export const useLimitOrderApproval = ({ + account, + amount, + chainId, + currency, + spender, + isAllowanceEnough, + passWhenInvalidInput = false, +}: UseLimitOrderApprovalProps) => { + return async () => { + if (!currency || currency.isNative || !account || !spender || !amount) return passWhenInvalidInput + + const allowance = (await readContract(wagmiConfig, { + address: currency.wrapped.address as Address, + abi: ERC20_ABI, + functionName: 'allowance', + args: [account, spender], + chainId: chainId as number, + })) as bigint + + const allowanceAmount = TokenAmount.fromRawAmount(currency.wrapped, allowance.toString()) + return isAllowanceEnough + ? isAllowanceEnough(allowanceAmount) + : allowanceAmount.greaterThan(amount) || allowanceAmount.equalTo(amount) + } +} diff --git a/apps/kyberswap-interface/src/components/LimitOrder/hooks/useLimitOrderWrapStep.ts b/apps/kyberswap-interface/src/components/LimitOrder/hooks/useLimitOrderWrapStep.ts new file mode 100644 index 0000000000..a3aa8135e1 --- /dev/null +++ b/apps/kyberswap-interface/src/components/LimitOrder/hooks/useLimitOrderWrapStep.ts @@ -0,0 +1,50 @@ +import { ChainId, Currency, CurrencyAmount, WETH } from '@kyberswap/ks-sdk-core' +import JSBI from 'jsbi' +import { useMemo } from 'react' + +import { NativeCurrencies } from 'constants/tokens' +import useWrapCallback from 'hooks/useWrapCallback' +import { useCurrencyBalance } from 'state/wallet/hooks' + +type UseLimitOrderWrapStepProps = { + chainId: ChainId + currency?: Currency + amount?: CurrencyAmount + balance?: CurrencyAmount +} + +export const useLimitOrderWrapStep = ({ chainId, currency, amount, balance }: UseLimitOrderWrapStepProps) => { + const nativeCurrency = NativeCurrencies[chainId] + const nativeBalance = useCurrencyBalance(nativeCurrency, chainId) + const isWrappedNativeCurrency = !!currency?.equals(WETH[chainId]) + + const wrapAmount = useMemo(() => { + if (!currency || !isWrappedNativeCurrency || !amount || !balance?.currency.equals(currency)) { + return undefined + } + if (!balance.lessThan(amount)) return undefined + return CurrencyAmount.fromRawAmount(nativeCurrency, JSBI.subtract(amount.quotient, balance.quotient)) + }, [amount, balance, currency, isWrappedNativeCurrency, nativeCurrency]) + + const insufficientBalance = (() => { + if (!amount) return false + if (!balance?.currency.equals(amount.currency)) return false + if (!balance.lessThan(amount)) return false + if (!isWrappedNativeCurrency || !wrapAmount || !nativeBalance?.currency.equals(nativeCurrency)) return true + return nativeBalance.lessThan(wrapAmount) + })() + + const { execute: onWrap } = useWrapCallback( + wrapAmount ? nativeCurrency : undefined, + WETH[chainId], + wrapAmount?.toExact(), + true, + chainId, + ) + + return { + insufficientBalance, + onWrap, + wrapAmount, + } +} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/useNotificationLimitOrder.tsx b/apps/kyberswap-interface/src/components/LimitOrder/hooks/useNotificationLimitOrder.tsx similarity index 96% rename from apps/kyberswap-interface/src/components/swapv2/LimitOrder/useNotificationLimitOrder.tsx rename to apps/kyberswap-interface/src/components/LimitOrder/hooks/useNotificationLimitOrder.tsx index e6893c3b6d..e1fdff7831 100644 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/useNotificationLimitOrder.tsx +++ b/apps/kyberswap-interface/src/components/LimitOrder/hooks/useNotificationLimitOrder.tsx @@ -3,7 +3,8 @@ import { useCallback, useEffect, useRef } from 'react' import { useAckNotificationOrderMutation } from 'services/limitOrder' import { NotificationType } from 'components/Announcement/type' -import { LimitOrder, LimitOrderStatus } from 'components/swapv2/LimitOrder/type' +import { SummaryNotify } from 'components/LimitOrder/MyOrders/SummaryNotify' +import { LimitOrder, LimitOrderStatus } from 'components/LimitOrder/types' import { APP_PATHS } from 'constants/index' import { useActiveWeb3React } from 'hooks' import { useNotify } from 'state/application/hooks' @@ -17,14 +18,12 @@ import { } from 'utils/firebase' import { getTransactionStatus } from 'utils/transaction' -import SummaryNotify from './ListOrder/SummaryNotify' - const isTransactionFailed = (txHash: string, transactions: GroupedTxsByHash | undefined) => { const transactionInfo = findTx(transactions, txHash) return transactionInfo ? getTransactionStatus(transactionInfo).error : false } -const useNotificationLimitOrder = () => { +export const useNotificationLimitOrder = () => { const notify = useNotify() const { account, chainId } = useActiveWeb3React() const showedNotificationOrderIds = useRef<{ [id: string]: boolean }>({}) @@ -177,4 +176,3 @@ const useNotificationLimitOrder = () => { } }, [account, chainId, notify, ackNotificationOrder, ackNotiLocal, transactions]) } -export default useNotificationLimitOrder diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/listOrdersArgs.ts b/apps/kyberswap-interface/src/components/LimitOrder/listOrdersArgs.ts similarity index 92% rename from apps/kyberswap-interface/src/components/swapv2/LimitOrder/listOrdersArgs.ts rename to apps/kyberswap-interface/src/components/LimitOrder/listOrdersArgs.ts index 6c61f6e24b..ca83126ed4 100644 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/listOrdersArgs.ts +++ b/apps/kyberswap-interface/src/components/LimitOrder/listOrdersArgs.ts @@ -1,6 +1,6 @@ import { ChainId } from '@kyberswap/ks-sdk-core' -import { LimitOrderStatus } from 'components/swapv2/LimitOrder/type' +import { LimitOrderStatus } from 'components/LimitOrder/types' /** Page size for the limit-orders list — shared by the list page and the nav-intent prefetch. */ export const LIMIT_ORDERS_PAGE_SIZE = 10 diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/type.ts b/apps/kyberswap-interface/src/components/LimitOrder/types.ts similarity index 67% rename from apps/kyberswap-interface/src/components/swapv2/LimitOrder/type.ts rename to apps/kyberswap-interface/src/components/LimitOrder/types.ts index 0b287ea257..90133dd02f 100644 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/type.ts +++ b/apps/kyberswap-interface/src/components/LimitOrder/types.ts @@ -1,6 +1,9 @@ import { ChainId, Currency, Fraction } from '@kyberswap/ks-sdk-core' +import type { BaseTradeInfo } from 'hooks/useBaseTradeInfo' + export enum LimitOrderTab { + PRICE = 'price', ORDER_BOOK = 'order_book', MY_ORDER = 'my_order', } @@ -73,6 +76,7 @@ export type LimitOrderFromTokenPair = { feeConfig: string feeRecipient: string makerTokenFeePercent: string + isTakerAssetFee: boolean makerAssetData: string takerAssetData: string getMakerAmount: string @@ -86,15 +90,30 @@ export type LimitOrderFromTokenPair = { makerBalanceAllowance: string makerAssetDecimals: number takerAssetDecimals: number + makerAssetSymbol?: string + takerAssetSymbol?: string + makerAssetLogoURL?: string + takerAssetLogoURL?: string } export type LimitOrderFromTokenPairFormatted = { id: number chainId: ChainId + rawOrder: LimitOrderFromTokenPair + isReversed: boolean + hasAvailable: boolean + formattedMakerAmount: string + formattedTakerAmount: string + formattedAvailableMakerAmount: string + formattedAvailableTakerAmount: string rate: string - makerAmount: string - takerAmount: string - filled: string + formattedRate: string + invertedRate: string + formattedInvertedRate: string + formattedMarketDiffPercent: string + formattedInvertedMarketDiffPercent: string + marketDiffPercent: number + filledPercent: string } export enum CancelOrderType { @@ -105,28 +124,26 @@ export enum CancelOrderType { export type RateInfo = { rate: string // to store user input invertRate: string // to store user input - invert: boolean rateFraction?: Fraction // to calc with big number } +export type DeltaRateLimitOrder = { + rawPercent: number | undefined + percent: string + profit: boolean +} + export type CancelOrderFunction = (data: { orders: LimitOrder[] + isCancelAll: boolean cancelType: CancelOrderType - isEdit?: boolean -}) => Promise - -export type EditOrderInfo = { - cancelType?: CancelOrderType - gasFee?: string - isEdit?: boolean - renderCancelButtons: () => React.JSX.Element -} +}) => Promise export type CancelOrderResponse = { orders: { operatorSignatureExpiredAt: number }[] } -export type CreateOrderParam = { +export type CreateOrderParams = { currencyIn: Currency | undefined currencyOut: Currency | undefined chainId: ChainId @@ -134,9 +151,26 @@ export type CreateOrderParam = { inputAmount: string outputAmount: string expiredAt: number - nativeOutput: boolean - orderId?: number - signature?: string - salt?: string referral?: string } + +export type LimitOrderCreateContext = { + currencyIn: Currency | undefined + currencyOut: Currency | undefined + chainId: ChainId + networkName: string + inputAmount: string + outputAmount: string + displayRate: string + expiredAt: number + displayTime: string + rateInfo: RateInfo + tradeInfo: BaseTradeInfo | undefined + deltaRate: DeltaRateLimitOrder +} + +export type LimitOrderTakeContext = { + order: LimitOrderFromTokenPair + payCurrency: Currency + receiveCurrency: Currency +} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/helpers.ts b/apps/kyberswap-interface/src/components/LimitOrder/utils.ts similarity index 68% rename from apps/kyberswap-interface/src/components/swapv2/LimitOrder/helpers.ts rename to apps/kyberswap-interface/src/components/LimitOrder/utils.ts index 6209352809..5646b9d8ff 100644 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/helpers.ts +++ b/apps/kyberswap-interface/src/components/LimitOrder/utils.ts @@ -1,19 +1,28 @@ import { Currency, Fraction } from '@kyberswap/ks-sdk-core' import { t } from '@lingui/macro' import JSBI from 'jsbi' +import { CreateOrderBody } from 'services/limitOrder' -import { CreateOrderParam, LimitOrder, LimitOrderStatus } from 'components/swapv2/LimitOrder/type' +import { CreateOrderParams, LimitOrder, LimitOrderStatus } from 'components/LimitOrder/types' import { RESERVE_USD_DECIMALS } from 'constants/index' import { tryParseAmount } from 'state/swap/hooks' import { friendlyError } from 'utils/errorMessage' import { formatDisplayNumber, uint256ToFraction } from 'utils/numbers' import { parseUnits } from 'utils/viem' +const baseUrl = 'https://docs.kyberswap.com/kyberswap-solutions/limit-order' + +export const DOCS_LINKS = { + GASLESS_CANCEL: baseUrl + '/concepts/gasless-cancellation#gasless-cancel', + HARD_CANCEL: baseUrl + '/concepts/gasless-cancellation#hard-cancel', + CANCEL_GUIDE: baseUrl + '/user-guides/cancel-limit-orders', + USER_GUIDE: baseUrl, +} + export const isActiveStatus = (status: LimitOrderStatus) => [LimitOrderStatus.ACTIVE, LimitOrderStatus.OPEN, LimitOrderStatus.PARTIALLY_FILLED].includes(status) -// js number to fraction -export function parseFraction(value: string, decimals = RESERVE_USD_DECIMALS) { +export const parseFraction = (value: string, decimals = RESERVE_USD_DECIMALS) => { try { return new Fraction( parseUnits(value, decimals).toString(), @@ -24,17 +33,13 @@ export function parseFraction(value: string, decimals = RESERVE_USD_DECIMALS) { } } -// 1.00010000 => 1.0001 export const removeTrailingZero = (num: string) => { if (num === undefined || num === null) return '' num = String(num) - /** - * 15.23000: $1 is 15, $2 is ., $3 is 23000 => '$1$2$3' => 15.23 - */ return num.replace(/^([\d,]+)$|^([\d,]+)\.0*$|^([\d,]+\.[0-9]*?)0*$/, '$1$2$3') } -export function calcOutput(input: string, rate: string | Fraction, decimalsOut: number) { +export const calcOutput = (input: string, rate: string | Fraction, decimalsOut: number) => { try { const value = parseFraction(input).multiply(typeof rate === 'string' ? parseFraction(rate) : rate) return removeTrailingZero(value.toFixed(decimalsOut)) @@ -43,7 +48,7 @@ export function calcOutput(input: string, rate: string | Fraction, decimalsOut: } } -export function calcRate(input: string, output: string, decimalsOut: number) { +export const calcRate = (input: string, output: string, decimalsOut: number) => { try { if (input && input === output) return '1' const rate = parseFraction(output, decimalsOut).divide(parseFraction(input)) @@ -53,8 +58,7 @@ export function calcRate(input: string, output: string, decimalsOut: number) { } } -// calc 1/value -export function calcInvert(value: string) { +export const calcInvert = (value: string) => { try { if (parseFloat(value) === 1) return '1' return removeTrailingZero(new Fraction(1).divide(parseFraction(value)).toFixed(16)) @@ -93,6 +97,29 @@ export const calcUsdPrices = ({ } } +type MarketPriceDiff = { + rawPercent: number + displayPercent: string +} + +export const getMarketPriceDiff = ( + rate: string | number | undefined, + marketRate: number | undefined, +): MarketPriceDiff => { + const rateValue = typeof rate === 'number' ? rate : Number(rate) + if (!rateValue || !marketRate || !Number.isFinite(rateValue) || !Number.isFinite(marketRate)) { + return { rawPercent: 0, displayPercent: '' } + } + + const rawPercent = ((rateValue - marketRate) / marketRate) * 100 + const sign = rawPercent > 0 ? '+' : '' + + return { + rawPercent, + displayPercent: `${sign}${removeTrailingZero(rawPercent.toFixed(2))}%`, + } +} + export const formatAmountOrder = (value: string, decimals?: number) => { const isUint256 = decimals !== undefined return formatDisplayNumber(parseFloat(isUint256 ? uint256ToFraction(value, decimals).toFixed(16) : value), { @@ -126,19 +153,32 @@ export const calcPercentFilledOrder = (value: string, total: string, decimals: n } } -export const getErrorMessage = (error: any) => { +type LimitOrderError = { + code?: string | number + response?: { + data?: { + code?: string | number + } + } +} + +const isLimitOrderError = (error: unknown): error is LimitOrderError => typeof error === 'object' && error !== null + +export const getErrorMessage = (error: unknown) => { console.error('Limit order error: ', error) - const errorCode: string = error?.response?.data?.code || error.code || '' - const mapErrorMessageByErrCode: { [code: string]: string } = { + const errorCode = isLimitOrderError(error) ? error.response?.data?.code || error.code || '' : '' + const mapErrorMessageByErrCode: Record = { 4001: t`User denied message signature`, 4002: t`You don't have sufficient fund for this transaction.`, 4004: t`Invalid signature`, } - const msg = mapErrorMessageByErrCode[errorCode] - return msg?.toString?.() || friendlyError(error) + const msg = mapErrorMessageByErrCode[String(errorCode)] + return msg?.toString?.() || friendlyError(error instanceof Error || typeof error === 'string' ? error : '') } -export const getPayloadCreateOrder = (params: CreateOrderParam) => { +type CreateOrderSignatureBodyPayload = Omit + +export const getPayloadCreateOrder = (params: CreateOrderParams): CreateOrderSignatureBodyPayload => { const { currencyIn, currencyOut, chainId, account, inputAmount, outputAmount, expiredAt, referral } = params const parseInputAmount = tryParseAmount(inputAmount, currencyIn ?? undefined) return { @@ -154,7 +194,19 @@ export const getPayloadCreateOrder = (params: CreateOrderParam) => { } } -export const getPayloadTracking = (order: LimitOrder, networkName: string, payload = {}) => { +type LimitOrderTrackingPayload = { + from_token: string + to_token: string + from_network: string + trade_qty: string + order_id: number +} & Record + +export const getPayloadTracking = ( + order: LimitOrder, + networkName: string, + payload: Record = {}, +): LimitOrderTrackingPayload => { const { makerAssetSymbol, takerAssetSymbol, makingAmount, makerAssetDecimals, id } = order return { ...payload, diff --git a/apps/kyberswap-interface/src/components/NumericalInput/index.tsx b/apps/kyberswap-interface/src/components/NumericalInput/index.tsx index af8d7a2d62..cd7a99c0a7 100644 --- a/apps/kyberswap-interface/src/components/NumericalInput/index.tsx +++ b/apps/kyberswap-interface/src/components/NumericalInput/index.tsx @@ -1,11 +1,20 @@ import { CSSProperties } from 'react' -import { escapeRegExp } from 'utils' import { cn } from 'utils/cn' -const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group +const inputRegex = /^\d*\.?\d*$/ +const signedInputRegex = /^[+-]?\d*\.?\d*$/ -export const Input = function InnerInput({ +type Props = { + value: string | number + onUserInput?: (input: string) => void + error?: boolean + fontSize?: string + align?: 'right' | 'left' + allowNegative?: boolean +} & Omit, 'ref' | 'onChange' | 'as'> + +const NumericalInput = ({ value, onUserInput, placeholder, @@ -16,20 +25,24 @@ export const Input = function InnerInput({ className, style, disabled, + allowNegative, + onKeyDown, ...rest -}: { - value: string | number - onUserInput?: (input: string) => void - error?: boolean - fontSize?: string - align?: 'right' | 'left' -} & Omit, 'ref' | 'onChange' | 'as'>) { +}: Props) => { const enforcer = (nextUserInput: string) => { - if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { + const regex = allowNegative ? signedInputRegex : inputRegex + if (nextUserInput === '' || regex.test(nextUserInput)) { onUserInput?.(nextUserInput) } } + const handleStep = (step: 1 | -1) => { + const numericValue = Number(value || 0) + if (!Number.isFinite(numericValue)) return + const nextValue = numericValue + step + onUserInput?.(String(!allowNegative && nextValue < 0 ? 0 : nextValue)) + } + // Only set fontSize inline when caller explicitly passes one — otherwise leave // the size to the className (default `text-2xl` below) so consumers can // override with `text-xs` / `text-sm` via the className prop. @@ -52,12 +65,20 @@ export const Input = function InnerInput({ // replace commas with periods (period is the decimal separator) enforcer(event.target.value.replace(/,/g, '.')) }} + onKeyDown={event => { + onKeyDown?.(event) + if (event.defaultPrevented) return + if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { + event.preventDefault() + handleStep(event.key === 'ArrowUp' ? 1 : -1) + } + }} inputMode="decimal" title={value.toString()} autoComplete="off" autoCorrect="off" type="text" - pattern="^[0-9]*[.,]?[0-9]*$" + pattern={allowNegative ? '^[+\\-]?[0-9]*[.,]?[0-9]*$' : '^[0-9]*[.,]?[0-9]*$'} placeholder={placeholder || '0.0'} minLength={1} maxLength={maxLength} @@ -65,7 +86,7 @@ export const Input = function InnerInput({ className={cn( 'relative w-0 flex-1 truncate border-none bg-buttonBlack p-0 text-2xl font-medium outline-none placeholder:text-text4', '[-webkit-appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-search-decoration]:appearance-none', - error ? 'text-red1' : disabled ? 'cursor-not-allowed text-disableText opacity-100' : 'text-text', + error ? 'text-red1' : disabled ? 'cursor-auto text-disableText opacity-100' : 'text-text', className, )} style={inline} @@ -73,4 +94,4 @@ export const Input = function InnerInput({ ) } -export default Input +export default NumericalInput diff --git a/apps/kyberswap-interface/src/components/Pagination/styles.tsx b/apps/kyberswap-interface/src/components/Pagination/styles.tsx index 7d18ee37c8..ae60a5bc5a 100644 --- a/apps/kyberswap-interface/src/components/Pagination/styles.tsx +++ b/apps/kyberswap-interface/src/components/Pagination/styles.tsx @@ -20,7 +20,7 @@ export const PaginationItem = forwardRef(
  • void }) { @@ -63,16 +65,13 @@ export default function RefreshLoading({ }, [countdown, onRefresh, disableRefresh]) useEffect(() => { + if (!refreshOnMount) return onRefresh() - }, [onRefresh]) + }, [onRefresh, refreshOnMount]) return ( - + <> diff --git a/apps/kyberswap-interface/src/components/Select/index.tsx b/apps/kyberswap-interface/src/components/Select/index.tsx index 495aeba6ba..5c96d014d2 100644 --- a/apps/kyberswap-interface/src/components/Select/index.tsx +++ b/apps/kyberswap-interface/src/components/Select/index.tsx @@ -2,7 +2,7 @@ import { t } from '@lingui/macro' import { Placement } from '@popperjs/core' import { Portal } from '@reach/portal' import { AnimatePresence, motion } from 'framer-motion' -import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react' +import { CSSProperties, ReactNode, useEffect, useMemo, useRef, useState } from 'react' import { usePopper } from 'react-popper' import { DropdownArrowIcon } from 'components/ArrowRotate' @@ -43,6 +43,7 @@ export type SelectProps = { placement?: Placement withSearch?: boolean onHideMenu?: () => void // hide without changes + matchMenuWidth?: boolean } export default function Select({ @@ -64,6 +65,7 @@ export default function Select({ withSearch, placement = 'bottom', placeholder, + matchMenuWidth, }: SelectProps) { const hasPlaceholder = placeholder !== undefined && placeholder !== null const getInitialSelected = () => { @@ -99,11 +101,17 @@ export default function Select({ }, [selectedValue, options, hasPlaceholder]) const ref = useRef(null) + const popperRef = useRef(null) + const outsideRefs = useMemo(() => [ref, popperRef], []) - useOnClickOutside(ref, () => { - setShowMenu(false) - onHideMenu?.() - }) + useOnClickOutside( + outsideRefs, + () => { + setShowMenu(false) + onHideMenu?.() + }, + { ignoreReachPortal: false }, + ) const selectedInfo = options.find(item => getOptionValue(item) === selected) const shouldShowPlaceholder = hasPlaceholder && (selectedValue === null || selectedValue === undefined) && !selectedInfo @@ -136,11 +144,12 @@ export default function Select({ onClick={onClick} style={optionStyle} className={cn( - 'whitespace-nowrap rounded-lg p-2 text-xs', - item.disabled - ? 'cursor-not-allowed text-border opacity-50' - : 'cursor-pointer text-subText hover:bg-background', - isSelected ? 'font-medium' : 'font-normal', + 'whitespace-nowrap rounded-lg p-2 text-sm transition-colors', + item.disabled && 'cursor-not-allowed text-border opacity-50', + !item.disabled && + (isSelected + ? 'cursor-pointer bg-primary-10 font-medium text-primary' + : 'cursor-pointer text-subText hover:bg-white/[0.04] hover:text-text'), )} > {optionRender ? optionRender(item) : getOptionLabel(item)} @@ -150,6 +159,10 @@ export default function Select({ } const [popperElement, setPopperElement] = useState(null) + const setPopperRef = (node: HTMLDivElement | null) => { + popperRef.current = node + setPopperElement(node) + } const { styles } = usePopper(ref.current, popperElement, { placement: placement, @@ -173,12 +186,12 @@ export default function Select({
    {shouldShowPlaceholder ? placeholder : activeRender ? activeRender(selectedInfo) : getOptionLabel(selectedInfo)}
    - + {showMenu && (
    {withSearch && (
    e.stopPropagation()} - className="relative mb-2 flex items-center justify-center rounded-lg bg-buttonGray text-subText [transition:background-color_0.1s_ease,color_0.1s_ease] focus-within:bg-buttonBlack focus-within:text-text hover:bg-buttonBlack hover:text-text" + className="relative mb-2 flex items-center justify-center rounded-lg bg-white/[0.04] text-subText [transition:background-color_0.1s_ease,color_0.1s_ease] focus-within:bg-white/[0.08] focus-within:text-text hover:bg-white/[0.08] hover:text-text" > @@ -209,7 +222,9 @@ export default function Select({ />
    )} -
    {dropdownRender ? dropdownRender(renderMenu()) : renderMenu()}
    +
    + {dropdownRender ? dropdownRender(renderMenu()) : renderMenu()} +
    diff --git a/apps/kyberswap-interface/src/components/SwapForm/ReverseTokenSelectionButton.tsx b/apps/kyberswap-interface/src/components/SwapForm/ReverseTokenSelectionButton.tsx index 7c35345978..d89cd62b80 100644 --- a/apps/kyberswap-interface/src/components/SwapForm/ReverseTokenSelectionButton.tsx +++ b/apps/kyberswap-interface/src/components/SwapForm/ReverseTokenSelectionButton.tsx @@ -1,11 +1,13 @@ import { useState } from 'react' import ArrowRotate from 'components/ArrowRotate' +import { cn } from 'utils/cn' type Props = { + className?: string onClick: () => void } -const ReverseTokenSelectionButton: React.FC = ({ onClick }) => { +const ReverseTokenSelectionButton: React.FC = ({ className, onClick }) => { const [rotated, setRotated] = useState(false) const handleClick = () => { @@ -14,9 +16,11 @@ const ReverseTokenSelectionButton: React.FC = ({ onClick }) => { } return ( -
    - -
    + ) } diff --git a/apps/kyberswap-interface/src/components/SwapForm/SlippageSetting.tsx b/apps/kyberswap-interface/src/components/SwapForm/SlippageSetting.tsx index 4ef8b4495a..b7e6f726a0 100644 --- a/apps/kyberswap-interface/src/components/SwapForm/SlippageSetting.tsx +++ b/apps/kyberswap-interface/src/components/SwapForm/SlippageSetting.tsx @@ -26,7 +26,7 @@ export const DropdownIcon = ({ {...rest} style={{ width: size || 12, height: size || 12, ...rest.style }} className={cn( - 'relative z-0 ml-1 flex items-center justify-center overflow-visible rounded-full p-0.5 text-white2 transition-all duration-200 ease-in-out [&>svg]:relative [&>svg]:z-[1]', + 'relative z-0 flex items-center justify-center overflow-visible rounded-full text-white2 transition-all duration-200 ease-in-out [&>svg]:relative [&>svg]:z-[1]', 'data-[flip=true]:rotate-180', 'data-[highlight=true]:text-primary', 'data-[highlight=true]:after:pointer-events-none data-[highlight=true]:after:absolute data-[highlight=true]:after:-inset-px data-[highlight=true]:after:rounded-full data-[highlight=true]:after:bg-primary/25 data-[highlight=true]:after:content-[""]', @@ -110,7 +110,7 @@ const SlippageSetting = ({ rightComponent, tooltip, slippageInfo }: Props) => {
    @@ -131,7 +131,11 @@ const SlippageSetting = ({ rightComponent, tooltip, slippageInfo }: Props) => { Max Slippage: -
    setExpanded(e => !e)} className="flex cursor-pointer items-center gap-1"> +
    setExpanded(e => !e)} + className="flex cursor-pointer items-center gap-1 hover:brightness-[0.85]" + > {msg ? ( @@ -143,7 +147,7 @@ const SlippageSetting = ({ rightComponent, tooltip, slippageInfo }: Props) => { - +
    diff --git a/apps/kyberswap-interface/src/components/SwapForm/SwapActionButton/index.tsx b/apps/kyberswap-interface/src/components/SwapForm/SwapActionButton/index.tsx index df4f593978..f0805b9ce7 100644 --- a/apps/kyberswap-interface/src/components/SwapForm/SwapActionButton/index.tsx +++ b/apps/kyberswap-interface/src/components/SwapForm/SwapActionButton/index.tsx @@ -103,12 +103,11 @@ const SwapActionButton: React.FC = ({ [trackingHandler, networkInfo?.name], ) - const [approval, approveCallback, currentAllowance] = useApproveCallback( - parsedAmountFromTypedValue, - routeSummary?.routerAddress, - false, - handleApprovalError, - ) + const [approval, approveCallback, currentAllowance] = useApproveCallback({ + amount: parsedAmountFromTypedValue, + spender: routeSummary?.routerAddress, + onApprovalError: handleApprovalError, + }) const [approvalSubmitted, setApprovalSubmitted] = useState(false) diff --git a/apps/kyberswap-interface/src/components/SwapForm/SwapModal/ConfirmSwapModalContent.tsx b/apps/kyberswap-interface/src/components/SwapForm/SwapModal/ConfirmSwapModalContent.tsx index 9b9cf4ee30..c61dee5f3b 100644 --- a/apps/kyberswap-interface/src/components/SwapForm/SwapModal/ConfirmSwapModalContent.tsx +++ b/apps/kyberswap-interface/src/components/SwapForm/SwapModal/ConfirmSwapModalContent.tsx @@ -2,13 +2,16 @@ import { Currency, CurrencyAmount, Price } from '@kyberswap/ks-sdk-core' import { Trans, t } from '@lingui/macro' import { useEffect, useMemo, useState } from 'react' import { Check, Info, Repeat } from 'react-feather' -import { Link, useParams, useSearchParams } from 'react-router-dom' +import { useParams, useSearchParams } from 'react-router-dom' import { useGetListOrdersQuery, useGetTotalActiveMakingAmountQuery } from 'services/limitOrder' import { calculatePriceImpact } from 'services/route/utils' import { ButtonOutlined, ButtonPrimary } from 'components/Button' import Dots from 'components/Dots' import InfoHelper from 'components/InfoHelper' +import { ReservedOrderNotice } from 'components/LimitOrder/components' +import { LimitOrderStatus, LimitOrderTab } from 'components/LimitOrder/types' +import { calcPercentFilledOrder } from 'components/LimitOrder/utils' import Loader from 'components/Loader' import SlippageWarningNote from 'components/SlippageWarningNote' import { HStack, Stack } from 'components/Stack' @@ -23,8 +26,6 @@ import { BuildRouteResult } from 'components/SwapForm/hooks/useBuildRoute' import { MouseoverTooltip } from 'components/Tooltip' import { TransactionErrorContent } from 'components/TransactionConfirmationModal' import WarningNote from 'components/WarningNote' -import { calcPercentFilledOrder } from 'components/swapv2/LimitOrder/helpers' -import { LimitOrderStatus, LimitOrderTab } from 'components/swapv2/LimitOrder/type' import { StyledBalanceMaxMini } from 'components/swapv2/styleds' import { TOKEN_API_URL } from 'constants/env' import { APP_PATHS, PAIR_CATEGORY } from 'constants/index' @@ -513,15 +514,10 @@ export default function ConfirmSwapModalContent({ {errorWhileBuildRoute && } {showLOWwarning && ( - - Notice: Some of your {currencyIn?.symbol} is already - reserved by an open Limit Order—review it{' '} - - here. - - + )} {errorWhileBuildRoute ? ( diff --git a/apps/kyberswap-interface/src/components/SwapForm/index.tsx b/apps/kyberswap-interface/src/components/SwapForm/index.tsx index db2a2e8596..0dd5f23355 100644 --- a/apps/kyberswap-interface/src/components/SwapForm/index.tsx +++ b/apps/kyberswap-interface/src/components/SwapForm/index.tsx @@ -274,6 +274,7 @@ const SwapForm: React.FC = props => { /> { trackingHandler(TRACKING_EVENT_TYPE.TOKEN_PAIR_REVERSED, { from_token: currencyIn?.symbol, @@ -307,7 +308,7 @@ const SwapForm: React.FC = props => { {!isWrapOrUnwrap && }
    -
    +
    {!isWrapOrUnwrap && ( diff --git a/apps/kyberswap-interface/src/components/TokenSelectorModal/TokenSelectorContent.tsx b/apps/kyberswap-interface/src/components/TokenSelectorModal/TokenSelectorContent.tsx index 3560671393..c811f75153 100644 --- a/apps/kyberswap-interface/src/components/TokenSelectorModal/TokenSelectorContent.tsx +++ b/apps/kyberswap-interface/src/components/TokenSelectorModal/TokenSelectorContent.tsx @@ -341,21 +341,19 @@ export const TokenSelectorContent = ({ - + {title || Select a token} - - Find a token by searching for its name or symbol or by pasting its address below. -
    - You can select and trade any token on KyberSwap. -
    - + + Find a token by searching for its name or symbol or by pasting its address below. +
    + You can select and trade any token on KyberSwap. +
    ) } /> diff --git a/apps/kyberswap-interface/src/components/TransactionConfirmationModal/index.tsx b/apps/kyberswap-interface/src/components/TransactionConfirmationModal/index.tsx index 5b5e4199cf..c563f18143 100644 --- a/apps/kyberswap-interface/src/components/TransactionConfirmationModal/index.tsx +++ b/apps/kyberswap-interface/src/components/TransactionConfirmationModal/index.tsx @@ -147,7 +147,7 @@ export function TransactionSubmittedContent({
    {tokenAddToMetaMask?.address && } - + Close diff --git a/apps/kyberswap-interface/src/components/WalletPopup/Transactions/Status.tsx b/apps/kyberswap-interface/src/components/WalletPopup/Transactions/Status.tsx index 66c6e9d25e..0c2f567c67 100644 --- a/apps/kyberswap-interface/src/components/WalletPopup/Transactions/Status.tsx +++ b/apps/kyberswap-interface/src/components/WalletPopup/Transactions/Status.tsx @@ -1,97 +1,18 @@ import { t } from '@lingui/macro' -import debounce from 'lodash.debounce' -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { memo } from 'react' import { Repeat } from 'react-feather' -import { useDispatch } from 'react-redux' import { CheckCircle } from 'components/Icons' import IconFailure from 'components/Icons/Failed' import WarningIcon from 'components/Icons/WarningIcon' -import Loader from 'components/Loader' import { PrimaryText } from 'components/WalletPopup/Transactions/TransactionItem' import { isTxsPendingTooLong as isShowPendingWarning } from 'components/WalletPopup/Transactions/helper' -import { CancellingOrderInfo } from 'components/swapv2/LimitOrder/useCancellingOrders' -import { AppDispatch } from 'state' -import { modifyTransaction } from 'state/transactions/actions' -import { TRANSACTION_TYPE, TransactionDetails } from 'state/transactions/type' +import { TransactionDetails } from 'state/transactions/type' import { getTransactionStatus } from 'utils/transaction' -const MAX_TIME_CHECK_STATUS = 7 * 86_400_000 // the time that we don't need to interval check -const TYPE_NEED_CHECK_PENDING = [TRANSACTION_TYPE.CANCEL_LIMIT_ORDER] - -const isTxsActuallySuccess = (txs: TransactionDetails) => txs.extraInfo?.actuallySuccess - -// this component to interval call api/listen firebase to check transaction status actually done or not -function StatusIcon({ - transaction, - cancellingOrderInfo, -}: { - transaction: TransactionDetails - cancellingOrderInfo: CancellingOrderInfo -}) { - const { type, hash, extraInfo, chainId, addedTime } = transaction - const { pending: pendingRpc, success } = getTransactionStatus(transaction) - - const needCheckActuallyPending = - success && - TYPE_NEED_CHECK_PENDING.includes(type) && - !isTxsActuallySuccess(transaction) && - Date.now() - addedTime < MAX_TIME_CHECK_STATUS - +function StatusIcon({ transaction }: { transaction: TransactionDetails }) { + const { pending, success } = getTransactionStatus(transaction) const isPendingTooLong = isShowPendingWarning(transaction) - const [isPendingState, setIsPendingState] = useState(needCheckActuallyPending ? null : pendingRpc) - - const dispatch = useDispatch() - const { loading, isOrderCancelling } = cancellingOrderInfo - - const interval = useRef(undefined) - - const checkStatus = useCallback(async () => { - try { - if (isTxsActuallySuccess(transaction) && interval.current) { - clearInterval(interval.current) - return - } - - let isPending = false - const isLoadingRemoteData = type === TRANSACTION_TYPE.CANCEL_LIMIT_ORDER && loading - switch (type) { - case TRANSACTION_TYPE.CANCEL_LIMIT_ORDER: - const orderId = extraInfo?.arbitrary?.order_id - isPending = isOrderCancelling(orderId) - break - } - if (!isPending && !isLoadingRemoteData) { - dispatch( - modifyTransaction({ - chainId, - hash, - extraInfo: { ...extraInfo, actuallySuccess: true }, - }), - ) - } - setIsPendingState(isPending) - } catch (error) { - console.error('Checking txs status error: ', error) - interval.current && clearInterval(interval.current) - } - }, [isOrderCancelling, chainId, dispatch, transaction, extraInfo, hash, type, loading]) - - const checkStatusDebounced = useMemo(() => debounce(checkStatus, 1000), [checkStatus]) - - useEffect(() => { - if (!needCheckActuallyPending) { - setIsPendingState(pendingRpc) - return - } - checkStatusDebounced() - if (TYPE_NEED_CHECK_PENDING.includes(type)) { - interval.current = setInterval(checkStatusDebounced, 5000) - } - return () => interval.current && clearInterval(interval.current) - }, [needCheckActuallyPending, pendingRpc, checkStatusDebounced, type]) - - const checkingStatus = isPendingState === null const pendingText = isPendingTooLong ? t`Pending` : t`Processing` const pendingIcon = isPendingTooLong ? ( @@ -101,12 +22,8 @@ function StatusIcon({ ) return ( - - {checkingStatus ? t`Checking` : isPendingState ? pendingText : success ? t`Completed` : t`Failed`} - - {checkingStatus ? ( - - ) : isPendingState ? ( + {pending ? pendingText : success ? t`Completed` : t`Failed`} + {pending ? ( pendingIcon ) : success ? ( diff --git a/apps/kyberswap-interface/src/components/WalletPopup/Transactions/TransactionItem.tsx b/apps/kyberswap-interface/src/components/WalletPopup/Transactions/TransactionItem.tsx index b2c1386257..58f905b5fd 100644 --- a/apps/kyberswap-interface/src/components/WalletPopup/Transactions/TransactionItem.tsx +++ b/apps/kyberswap-interface/src/components/WalletPopup/Transactions/TransactionItem.tsx @@ -15,7 +15,6 @@ import PendingWarning from 'components/WalletPopup/Transactions/PendingWarning' import PoolFarmLink from 'components/WalletPopup/Transactions/PoolFarmLink' import Status from 'components/WalletPopup/Transactions/Status' import { isTxsPendingTooLong } from 'components/WalletPopup/Transactions/helper' -import { CancellingOrderInfo } from 'components/swapv2/LimitOrder/useCancellingOrders' import { APP_PATHS, ETHER_ADDRESS } from 'constants/index' import { TRANSACTION_TYPE, @@ -238,6 +237,7 @@ const DESCRIPTION_MAP: { [TRANSACTION_TYPE.SWAP]: Description2Token, [TRANSACTION_TYPE.KYBERDAO_MIGRATE]: Description2Token, + [TRANSACTION_TYPE.FILL_LIMIT_ORDER]: DescriptionLimitOrder, [TRANSACTION_TYPE.CANCEL_LIMIT_ORDER]: DescriptionLimitOrder, [TRANSACTION_TYPE.CLASSIC_CREATE_POOL]: DescriptionLiquidity, @@ -266,11 +266,10 @@ type Prop = { transaction: TransactionDetails style: CSSProperties isMinimal: boolean - cancellingOrderInfo: CancellingOrderInfo } const TransactionItem = forwardRef(function TransactionItem( - { transaction, style, isMinimal, cancellingOrderInfo }: Prop, + { transaction, style, isMinimal }: Prop, ref, ) { const { type, addedTime, hash, chainId } = transaction @@ -299,7 +298,7 @@ const TransactionItem = forwardRef(function TransactionIte {type} - +
    diff --git a/apps/kyberswap-interface/src/components/WalletPopup/Transactions/index.tsx b/apps/kyberswap-interface/src/components/WalletPopup/Transactions/index.tsx index ba6b1bda0e..f6ff0e9d0a 100644 --- a/apps/kyberswap-interface/src/components/WalletPopup/Transactions/index.tsx +++ b/apps/kyberswap-interface/src/components/WalletPopup/Transactions/index.tsx @@ -11,7 +11,6 @@ import Row, { RowBetween } from 'components/Row' import Tab from 'components/WalletPopup/Transactions/Tab' import TransactionItem from 'components/WalletPopup/Transactions/TransactionItem' import { NUMBERS } from 'components/WalletPopup/Transactions/helper' -import useCancellingOrders, { CancellingOrderInfo } from 'components/swapv2/LimitOrder/useCancellingOrders' import { useActiveWeb3React } from 'hooks' import { fetchListTokenByAddresses, findCacheToken, useIsLoadedTokenDefault } from 'hooks/Tokens' import { isSupportKyberDao } from 'hooks/kyberdao' @@ -31,14 +30,12 @@ function RowItem({ transaction, setRowHeight, isMinimal, - cancellingOrderInfo, }: { transaction: TransactionDetails style: CSSProperties index: number setRowHeight: (v: number, height: number) => void isMinimal: boolean - cancellingOrderInfo: CancellingOrderInfo }) { const rowRef = useRef(null) @@ -61,21 +58,12 @@ function RowItem({ } }, [rowRef, index, setRowHeight]) - return ( - - ) + return } let storedActiveTab = '' function ListTransaction({ isMinimal }: { isMinimal: boolean }) { const transactions = useSortRecentTransactions(false) - const cancellingOrderInfo = useCancellingOrders() const dispatch = useAppDispatch() const { chainId } = useActiveWeb3React() @@ -216,7 +204,6 @@ function ListTransaction({ isMinimal }: { isMinimal: boolean }) { index={index} key={data[index].hash} setRowHeight={setRowHeight} - cancellingOrderInfo={cancellingOrderInfo} /> )} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ActionButtonLimitOrder.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ActionButtonLimitOrder.tsx deleted file mode 100644 index 7a3a0dff01..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ActionButtonLimitOrder.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Currency, WETH } from '@kyberswap/ks-sdk-core' -import { Trans, t } from '@lingui/macro' - -import { - ButtonApprove, - ButtonError, - ButtonLight, - ButtonPrimary, - ButtonWarning, - ButtonWithInfoHelper, -} from 'components/Button' -import ProgressSteps from 'components/ProgressSteps' -import { RowBetween } from 'components/Row' -import { EditOrderInfo } from 'components/swapv2/LimitOrder/type' -import { useActiveWeb3React } from 'hooks' -import { ApprovalState } from 'hooks/useApproveCallback' -import { useWalletModalToggle } from 'state/application/hooks' -import { isTokenNative } from 'utils/tokenInfo' - -export default function ActionButtonLimitOrder({ - showWrap, - approval, - currencyIn, - currencyOut, - isWrappingEth, - wrapInputError, - approveCallback, - onWrapToken, - checkingAllowance, - showPreview, - isNotFillAllInput, - enoughAllowance, - hasInputError, - approvalSubmitted, - showApproveFlow, - showWarning, - editOrderInfo, -}: { - currencyIn: Currency | undefined - currencyOut: Currency | undefined - approval: ApprovalState - showWrap: boolean - isWrappingEth: boolean - isNotFillAllInput: boolean - hasInputError: boolean - approvalSubmitted: boolean - enoughAllowance: boolean - checkingAllowance: boolean - showApproveFlow: boolean - wrapInputError: any - showWarning: boolean - approveCallback: () => Promise - onWrapToken: () => Promise - showPreview: () => void - editOrderInfo?: EditOrderInfo -}) { - const { isEdit, renderCancelButtons } = editOrderInfo || {} - const disableBtnApproved = - approval === ApprovalState.PENDING || - !!hasInputError || - ((approval !== ApprovalState.NOT_APPROVED || approvalSubmitted) && enoughAllowance) - - const disableBtnReview = - checkingAllowance || - isNotFillAllInput || - !!hasInputError || - approval !== ApprovalState.APPROVED || - isWrappingEth || - (showWrap && !isWrappingEth) || - (currencyIn?.equals(WETH[currencyIn.chainId]) && isTokenNative(currencyOut)) - - const { account } = useActiveWeb3React() - const toggleWalletModal = useWalletModalToggle() - if (!account) - return ( - - Connect - - ) - - const inSymbol = currencyIn?.symbol - const wrapSymbol = currencyIn?.wrapped.symbol - if (showApproveFlow || showWrap) - return ( - <> - - {showWrap ? ( - - ) : ( - - )} - - - Review Order - - - - {showApproveFlow && } - - ) - - const contentButton = ( - - {checkingAllowance ? Checking Allowance... : Review Order} - - ) - - if (isEdit) { - return checkingAllowance ? {contentButton} : renderCancelButtons?.() || null - } - - if (showWarning && !disableBtnReview) return {contentButton} - - return ( - - {contentButton} - - ) -} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/DeltaRate.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/DeltaRate.tsx deleted file mode 100644 index f69080ff60..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/DeltaRate.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Trans } from '@lingui/macro' -import { useMemo } from 'react' - -import InfoHelper from 'components/InfoHelper' -import { Label } from 'components/swapv2/LimitOrder/LimitOrderForm' -import { RateInfo } from 'components/swapv2/LimitOrder/type' -import { BaseTradeInfo } from 'hooks/useBaseTradeInfo' -import useTheme from 'hooks/useTheme' -import { cn } from 'utils/cn' - -export type DeltaRateLimitOrder = { rawPercent: number | undefined; percent: string; profit: boolean } - -export function useGetDeltaRateLimitOrder({ - marketPrice, - rateInfo, -}: { - marketPrice: BaseTradeInfo | undefined - rateInfo: RateInfo -}): DeltaRateLimitOrder { - const { deltaText, percent } = useMemo(() => { - try { - if (marketPrice && rateInfo.rate && rateInfo.invertRate) { - const { rate, invert, invertRate } = rateInfo - const ourRate = Number(invert ? invertRate : rate) - const marketRate = Number(invert ? marketPrice.invertRate : marketPrice.marketRate) - let percent = ((ourRate - marketRate) / marketRate) * 100 - if (invert) percent = -percent - const delta = Number(percent) - const sign = delta > 0 ? '+' : '' - const deltaText = `${Math.abs(delta) > 100 ? '>100' : `${sign}${delta.toFixed(2)}`}%` - return { percent, deltaText } - } - } catch (error) { - console.log(error) - } - return { percent: undefined, deltaText: '' } - }, [marketPrice, rateInfo]) - - const percentText = Math.abs(Number(percent)) > 0.009 ? deltaText : '' - return { - rawPercent: percent, - percent: percentText, - profit: Boolean(percent && Number(percent) > 0), - } -} - -const DeltaRate = ({ - marketPrice, - rateInfo, - symbol, - invert, -}: { - marketPrice: BaseTradeInfo | undefined - rateInfo: RateInfo - symbol: string - invert: boolean -}) => { - const theme = useTheme() - - const { percent, profit } = useGetDeltaRateLimitOrder({ marketPrice, rateInfo }) - const color = profit ? theme.apr : theme.warning - const colorClass = profit ? 'text-apr' : 'text-warning' - const styledPercent = {percent} - return ( - - ) -} -export default DeltaRate diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/EditOrderModal.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/EditOrderModal.tsx deleted file mode 100644 index c700fd94c3..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/EditOrderModal.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { ChainId } from '@kyberswap/ks-sdk-core' -import { Trans } from '@lingui/macro' -import { useEffect, useMemo, useRef, useState } from 'react' -import { ChevronLeft, X } from 'react-feather' -import { useGetTotalActiveMakingAmountQuery } from 'services/limitOrder' - -import Column from 'components/Column' -import Modal from 'components/Modal' -import LimitOrderForm, { Label, LimitOrderFormHandle } from 'components/swapv2/LimitOrder/LimitOrderForm' -import { useEstimateFee, useProcessCancelOrder } from 'components/swapv2/LimitOrder/ListOrder/useRequestCancelOrder' -import CancelButtons from 'components/swapv2/LimitOrder/Modals/CancelButtons' -import { CancelStatus } from 'components/swapv2/LimitOrder/Modals/CancelOrderModal' -import CancelStatusCountDown from 'components/swapv2/LimitOrder/Modals/CancelStatusCountDown' -import { calcInvert, calcPercentFilledOrder, calcRate, removeTrailingZero } from 'components/swapv2/LimitOrder/helpers' -import { - CancelOrderFunction, - CancelOrderType, - EditOrderInfo, - LimitOrder, - LimitOrderStatus, - RateInfo, -} from 'components/swapv2/LimitOrder/type' -import { useIsSupportSoftCancelOrder } from 'components/swapv2/LimitOrder/useFetchActiveAllOrders' -import { Z_INDEXS } from 'constants/styles' -import { useActiveWeb3React } from 'hooks' -import { useCurrencyV2 } from 'hooks/Tokens' -import { TransactionFlowState } from 'types/TransactionFlowState' -import { formatUnits } from 'utils/viem' - -enum Steps { - EDIT_ORDER, - REVIEW_ORDER, -} -export default function EditOrderModal({ - onSubmit, - onDismiss, - customChainId, - order, - note, - isOpen, - flowState, - setFlowState, -}: { - onSubmit: CancelOrderFunction - onDismiss: () => void - customChainId?: ChainId - order: LimitOrder - note: string - isOpen: boolean - flowState: TransactionFlowState - setFlowState: React.Dispatch> -}) { - const { chainId, account } = useActiveWeb3React() - const [step, setStep] = useState(Steps.EDIT_ORDER) - - const { status, makingAmount, takingAmount, makerAsset, takerAsset, filledTakingAmount, expiredAt } = order - const currencyIn = useCurrencyV2(makerAsset, customChainId) ?? undefined - const currencyOut = useCurrencyV2(takerAsset, customChainId) ?? undefined - const inputAmount = currencyIn ? formatUnits(BigInt(makingAmount), currencyIn.decimals) : '' - const outputAmount = currencyOut ? formatUnits(BigInt(takingAmount), currencyOut.decimals) : '' - - const formatIn = inputAmount ? removeTrailingZero(inputAmount) : inputAmount - const formatOut = outputAmount ? removeTrailingZero(outputAmount) : outputAmount - const defaultExpire = new Date(expiredAt * 1000) - const rate = currencyOut ? calcRate(formatIn, formatOut, currencyOut.decimals) : '' - const defaultRate: RateInfo = { rate, invertRate: calcInvert(rate), invert: false } - const filled = currencyOut ? calcPercentFilledOrder(filledTakingAmount, takingAmount, currencyOut.decimals) : 0 - - const { data: defaultActiveMakingAmount } = useGetTotalActiveMakingAmountQuery( - { chainId, tokenAddress: currencyIn?.wrapped.address ?? '', account: account ?? '' }, - { skip: !currencyIn || !account }, - ) - - const { onClickGaslessCancel, onClickHardCancel, expiredTime, cancelStatus, setCancelStatus } = useProcessCancelOrder( - { - isOpen, - onDismiss, - onSubmit, - getOrders: () => (order ? [order] : []), - isEdit: true, - }, - ) - - const isSupportSoftCancelOrder = useIsSupportSoftCancelOrder() - const { orderSupportGasless: supportGasLessCancel, chainSupportGasless } = isSupportSoftCancelOrder(order) - const [cancelType, setCancelType] = useState(CancelOrderType.GAS_LESS_CANCEL) - useEffect(() => { - setCancelType(supportGasLessCancel ? CancelOrderType.GAS_LESS_CANCEL : CancelOrderType.HARD_CANCEL) - }, [supportGasLessCancel]) - - const orders = useMemo(() => (order ? [order] : []), [order]) - - const estimateGas = useEstimateFee({ orders }) - - const isReviewOrder = step === Steps.REVIEW_ORDER - const onBack = () => { - setStep(Steps.EDIT_ORDER) - setFlowState(v => ({ ...v, showConfirm: false })) - } - const onNext = () => { - setStep(Steps.REVIEW_ORDER) - setFlowState(v => ({ ...v, showConfirm: true })) - } - - const isWaiting = cancelStatus === CancelStatus.WAITING - const showReview = isReviewOrder && isWaiting - - const ref = useRef(null) - const renderCancelButtons = () => { - const hasChangeInfo = step === Steps.EDIT_ORDER ? ref.current?.hasChangedOrderInfo?.() : true - const disabledGasLessCancel = !hasChangeInfo || !supportGasLessCancel || flowState.attemptingTxn - const disabledHardCancel = !hasChangeInfo || flowState.attemptingTxn - return ( - <> - {isReviewOrder && ( - - )} - Gasless Edit, - hardCancelGasless: Hard Edit, - confirmBtnText: isReviewOrder ? Place Order : Edit Order, - disabledConfirm: flowState.attemptingTxn || (disabledGasLessCancel && disabledHardCancel), - }} - /> - - ) - } - - const editOrderInfo: EditOrderInfo = { isEdit: true, gasFee: estimateGas, cancelType, renderCancelButtons } - return ( - -
    -
    - {showReview ? :
    } - {showReview ? Review your order : Edit Order} - -
    - - - - {status === LimitOrderStatus.PARTIALLY_FILLED && ( - - )} - - - {isWaiting && ( - - )} -
    - - ) -} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/LimitOrderForm.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/LimitOrderForm.tsx deleted file mode 100644 index ecc5919487..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/LimitOrderForm.tsx +++ /dev/null @@ -1,1096 +0,0 @@ -import { ChainId, Currency, CurrencyAmount, Token, TokenAmount, WETH } from '@kyberswap/ks-sdk-core' -import { Trans, t } from '@lingui/macro' -import dayjs from 'dayjs' -import JSBI from 'jsbi' -import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' -import { Repeat } from 'react-feather' -import { useSearchParams } from 'react-router-dom' -import { useCreateOrderMutation, useGetLOConfigQuery, useGetTotalActiveMakingAmountQuery } from 'services/limitOrder' - -import { ReactComponent as DropdownSVG } from 'assets/svg/down.svg' -import { NotificationType } from 'components/Announcement/type' -import ArrowRotate from 'components/ArrowRotate' -import { ButtonLight } from 'components/Button' -import CurrencyInputPanel from 'components/CurrencyInputPanel' -import CurrencyLogo from 'components/CurrencyLogo' -import { NetworkSelector } from 'components/NetworkSelector' -import NumericalInput from 'components/NumericalInput' -import { RowBetween } from 'components/Row' -import { DefaultSlippageOption } from 'components/SlippageControl' -import { TextDashed } from 'components/Text' -import { getTipLinkAttribution } from 'components/TipLinkGeneratorModal/shared' -import Tooltip, { MouseoverTooltip } from 'components/Tooltip' -import ActionButtonLimitOrder from 'components/swapv2/LimitOrder/ActionButtonLimitOrder' -import DeltaRate, { useGetDeltaRateLimitOrder } from 'components/swapv2/LimitOrder/DeltaRate' -import ExpirePicker from 'components/swapv2/LimitOrder/ExpirePicker' -import { SummaryNotifyOrderPlaced } from 'components/swapv2/LimitOrder/ListOrder/SummaryNotify' -import ConfirmOrderModal from 'components/swapv2/LimitOrder/Modals/ConfirmOrderModal' -import TradePrice from 'components/swapv2/LimitOrder/TradePrice' -import { DEFAULT_EXPIRED, getExpireOptions } from 'components/swapv2/LimitOrder/const' -import { - calcInvert, - calcOutput, - calcRate, - calcUsdPrices, - formatAmountOrder, - getErrorMessage, - getPayloadCreateOrder, - parseFraction, - removeTrailingZero, -} from 'components/swapv2/LimitOrder/helpers' -import { CreateOrderParam, EditOrderInfo, LimitOrder, RateInfo } from 'components/swapv2/LimitOrder/type' -import useSignOrder from 'components/swapv2/LimitOrder/useSignOrder' -import useValidateInputError from 'components/swapv2/LimitOrder/useValidateInputError' -import useWarningCreateOrder from 'components/swapv2/LimitOrder/useWarningCreateOrder' -import useWrapEthStatus from 'components/swapv2/LimitOrder/useWrapEthStatus' -import { TRANSACTION_STATE_DEFAULT } from 'constants/index' -import { SUPPORTED_NETWORKS } from 'constants/networks' -import { Z_INDEXS } from 'constants/styles' -import { useTokenAllowance } from 'data/Allowances' -import { useActiveWeb3React } from 'hooks' -import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' -import { useBaseTradeInfoLimitOrder } from 'hooks/useBaseTradeInfo' -import { NETWORKS_INFO } from 'hooks/useChainsConfig' -import useTracking, { TRACKING_EVENT_TYPE } from 'hooks/useTracking' -import useWrapCallback from 'hooks/useWrapCallback' -import { useChangeNetwork } from 'hooks/web3/useChangeNetwork' -import ErrorWarningPanel from 'pages/Bridge/ErrorWarning' -import { useNotify } from 'state/application/hooks' -import { useLimitActionHandlers, useLimitState } from 'state/limit/hooks' -import { tryParseAmount } from 'state/swap/hooks' -import { useCurrencyBalance } from 'state/wallet/hooks' -import { TransactionFlowState } from 'types/TransactionFlowState' -import { getCookieValue } from 'utils' -import { cn } from 'utils/cn' -import { subscribeNotificationOrderCancelled, subscribeNotificationOrderExpired } from 'utils/firebase' -import { maxAmountSpend } from 'utils/maxAmountSpend' -import { formatTimeDuration } from 'utils/time' - -const DropdownIcon = ({ className, ...rest }: React.SVGProps & { 'data-flip'?: boolean }) => ( - -) - -export const Label = ({ children, className, ...rest }: React.HTMLAttributes) => ( -
    - {children} -
    -) - -const Set2Market = ({ children, className, ...rest }: React.HTMLAttributes) => ( - -) - -type Props = { - currencyIn: Currency | undefined - currencyOut: Currency | undefined - defaultInputAmount?: string - defaultOutputAmount?: string - defaultActiveMakingAmount?: string - defaultExpire?: Date - note?: string - orderInfo?: LimitOrder - flowState: TransactionFlowState - setFlowState: React.Dispatch> - zIndexToolTip?: number - defaultRate?: RateInfo - editOrderInfo?: EditOrderInfo - useUrlParams?: boolean -} - -const InputWrapper = ({ children, className, ...rest }: React.HTMLAttributes) => ( -
    - {children} -
    -) - -const useInputAmount = ({ - defaultInputAmount, - isEdit, -}: { - defaultInputAmount?: string - isEdit: boolean -}): [string, (v: string) => void] => { - const { inputAmount } = useLimitState() - const { setInputValue } = useLimitActionHandlers() - - const localState = useState(defaultInputAmount || '') - return isEdit ? localState : [inputAmount, setInputValue] -} - -export type LimitOrderFormHandle = { - hasChangedOrderInfo: () => boolean -} -const LimitOrderForm = forwardRef(function LimitOrderForm( - { - currencyIn, - currencyOut, - defaultInputAmount = '', - defaultOutputAmount = '', - defaultActiveMakingAmount = '', - defaultExpire, - defaultRate = { rate: '', invertRate: '', invert: false }, - note = '', - orderInfo, - flowState, - setFlowState, - zIndexToolTip = Z_INDEXS.TOOL_TIP_ERROR_INPUT_SWAP_FORM, - editOrderInfo, - useUrlParams, - }, - ref, -) { - const { changeNetwork } = useChangeNetwork() - const isEdit = editOrderInfo?.isEdit || false // else create - const { account, chainId: walletChainId, networkInfo } = useActiveWeb3React() - const [searchParams, setSearchParams] = useSearchParams() - const urlChainId = searchParams.get('chainId') - const chainId: ChainId = useUrlParams - ? urlChainId && SUPPORTED_NETWORKS.includes(+urlChainId) - ? +urlChainId - : walletChainId - : walletChainId - - const notify = useNotify() - const { trackingHandler } = useTracking() - - const { - setCurrencyIn: updateCurrencyIn, - setCurrencyOut: updateCurrencyOut, - switchCurrency: rotateCurrency, - removeOrderNeedCreated, - setOrderEditing, - } = useLimitActionHandlers() - - const setCurrencyIn = useCallback( - (currency: Currency | undefined) => { - if (useUrlParams) { - searchParams.set( - 'inputCurrency', - !currency ? '' : currency.isNative ? currency.symbol || '' : currency.wrapped.address, - ) - setSearchParams(searchParams) - } else updateCurrencyIn(currency) - autoFillMarketPrice.current = false - }, - [useUrlParams, searchParams, setSearchParams, updateCurrencyIn], - ) - - const setCurrencyOut = useCallback( - (currency: Currency | undefined) => { - if (useUrlParams) { - searchParams.set( - 'outputCurrency', - !currency ? '' : currency.isNative ? currency.symbol || '' : currency.wrapped.address, - ) - setSearchParams(searchParams) - } else updateCurrencyOut(currency) - autoFillMarketPrice.current = false - }, - [useUrlParams, searchParams, setSearchParams, updateCurrencyOut], - ) - - const switchCurrency = useCallback(() => { - if (useUrlParams) { - const cin = searchParams.get('inputCurrency') || '' - const cout = searchParams.get('outputCurrency') || '' - searchParams.set('outputCurrency', cin) - searchParams.set('inputCurrency', cout) - setSearchParams(searchParams) - } else rotateCurrency() - }, [useUrlParams, rotateCurrency, searchParams, setSearchParams]) - - const { ordersNeedCreated } = useLimitState() - - const [inputAmount, setInputAmount] = useInputAmount({ defaultInputAmount, isEdit }) - const [outputAmount, setOutputAmount] = useState(defaultOutputAmount) - - const [rateInfo, setRateInfo] = useState(defaultRate) - const displayRate = rateInfo.invert ? rateInfo.invertRate : rateInfo.rate - - const [expire, setExpire] = useState(DEFAULT_EXPIRED) - - const [showDatePicker, setShowDatePicker] = useState(false) - const [customDateExpire, setCustomDateExpire] = useState(defaultExpire) - - const [approvalSubmitted, setApprovalSubmitted] = useState(false) - - const { loading: loadingTrade, tradeInfo } = useBaseTradeInfoLimitOrder(currencyIn, currencyOut, chainId) - const deltaRate = useGetDeltaRateLimitOrder({ marketPrice: tradeInfo, rateInfo }) - - const { data: activeOrderMakingAmount = defaultActiveMakingAmount, refetch: getActiveMakingAmount } = - useGetTotalActiveMakingAmountQuery( - { chainId, tokenAddress: currencyIn?.wrapped.address ?? '', account: account ?? '' }, - { skip: !currencyIn || !account }, - ) - - const { execute: onWrap, inputError: wrapInputError } = useWrapCallback( - currencyIn, - currencyOut, - inputAmount, - true, - chainId, - ) - const showWrap = !!currencyIn?.isNative - - const onSetRate = useCallback( - (rate: string, invertRate: string) => { - if (!currencyIn || !currencyOut) return - const newRate: RateInfo = { ...rateInfo, rate, invertRate, rateFraction: parseFraction(rate) } - if (!rate && !invertRate) { - setRateInfo(newRate) - return - } - - if (rate) { - if (inputAmount) { - const output = calcOutput(inputAmount, newRate.rateFraction || rate, currencyOut.decimals) - setOutputAmount(output) - } - if (!invertRate) { - newRate.invertRate = calcInvert(rate) - } - setRateInfo(newRate) - return - } - if (invertRate) { - newRate.rate = calcInvert(invertRate) - newRate.rateFraction = parseFraction(invertRate).invert() - if (inputAmount) { - const output = calcOutput(inputAmount, newRate.rateFraction, currencyOut.decimals) - setOutputAmount(output) - } - setRateInfo(newRate) - return - } - }, - [currencyIn, currencyOut, inputAmount, rateInfo], - ) - - const onSetOutput = (output: string) => { - if (inputAmount && parseFloat(inputAmount) !== 0 && currencyOut && output) { - const rate = calcRate(inputAmount, output, currencyOut?.decimals) - setRateInfo({ - ...rateInfo, - rate, - rateFraction: parseFraction(rate), - invertRate: calcInvert(rate), - }) - } - setOutputAmount(output) - } - - const setPriceRateMarket = useCallback( - (autoFillInput = false) => { - try { - !autoFillInput && trackingHandler(TRACKING_EVENT_TYPE.LO_ENTER_DETAIL, 'set price') - if ((loadingTrade && !autoFillInput) || !tradeInfo) return - const marketRate = removeTrailingZero(tradeInfo.marketRate.toFixed(16)) ?? '' - onSetRate(marketRate, removeTrailingZero(tradeInfo.invertRate.toFixed(16)) ?? '') - if (!autoFillInput) { - trackingHandler(TRACKING_EVENT_TYPE.LO_PRICE_SET, { - side: rateInfo.invert ? 'buy' : 'sell', - limit_price: marketRate, - market_price: marketRate, - price_difference_pct: 0, - from_token: currencyIn?.symbol, - to_token: currencyOut?.symbol, - chain: networkInfo.name, - }) - } - } catch (error) {} - }, - [loadingTrade, trackingHandler, onSetRate, tradeInfo, rateInfo.invert, currencyIn, currencyOut, networkInfo.name], - ) - - const onChangeRate = (val: string) => { - if (currencyOut) { - onSetRate(rateInfo.invert ? '' : val, rateInfo.invert ? val : '') - } - } - - const onSetInput = useCallback( - (input: string) => { - setInputAmount(input) - if (rateInfo.rate && currencyIn && currencyOut && input) { - setOutputAmount(calcOutput(input, rateInfo.rateFraction || rateInfo.rate, currencyOut.decimals)) - } - }, - [rateInfo, currencyIn, currencyOut, setInputAmount], - ) - - const onInvertRate = (invert: boolean) => { - setRateInfo({ ...rateInfo, invert }) - } - - const handleInputSelect = useCallback( - (currency: Currency, resetRate = true) => { - if (currencyOut && currency?.equals(currencyOut)) { - switchCurrency() - return - } - setCurrencyIn(currency) - resetRate && setRateInfo(rateInfo => ({ ...rateInfo, invertRate: '', rate: '', rateFraction: undefined })) - }, - [currencyOut, setCurrencyIn, switchCurrency], - ) - - const switchToWeth = useCallback(() => { - handleInputSelect(currencyIn?.wrapped as Currency, false) - }, [currencyIn, handleInputSelect]) - - const { isWrappingEth, setTxHashWrapped } = useWrapEthStatus(switchToWeth) - - const handleOutputSelect = (currency: Currency) => { - if (currencyIn && currency?.equals(currencyIn)) { - switchCurrency() - return - } - setCurrencyOut(currency) - setRateInfo({ ...rateInfo, invertRate: '', rate: '', rateFraction: undefined }) - } - - const [rotate, setRotate] = useState(false) - const handleRotateClick = () => { - if (isEdit) return - trackingHandler(TRACKING_EVENT_TYPE.LO_SIDE_SELECTED, { - side: rateInfo.invert ? 'sell' : 'buy', - from_token: currencyOut?.symbol, - to_token: currencyIn?.symbol, - chain: networkInfo.name, - }) - setRotate(prev => !prev) - switchCurrency() - setInputAmount(outputAmount) - setOutputAmount(inputAmount) - if (currencyIn) { - const rate = calcRate(outputAmount, inputAmount, currencyIn?.decimals) - setRateInfo({ - ...rateInfo, - rate, - rateFraction: parseFraction(rate), - invertRate: calcInvert(rate), - }) - } - } - - const parseInputAmount = tryParseAmount(inputAmount, currencyIn ?? undefined) - const { currentData } = useGetLOConfigQuery(chainId) - const limitOrderContract = currentData?.contract - - const currentAllowance = useTokenAllowance( - currencyIn as Token, - account ?? undefined, - limitOrderContract, - ) as CurrencyAmount - - const parsedActiveOrderMakingAmount = useMemo(() => { - try { - if (currencyIn && activeOrderMakingAmount) { - if (currencyIn.isNative) { - return TokenAmount.fromRawAmount(currencyIn, JSBI.BigInt(0)) - } - const value = TokenAmount.fromRawAmount(currencyIn, JSBI.BigInt(activeOrderMakingAmount)) - if (isEdit && orderInfo) { - const makingAmount = TokenAmount.fromRawAmount(currencyIn, JSBI.BigInt(orderInfo.makingAmount)) - return value.greaterThan(makingAmount) - ? value.subtract(makingAmount) - : TokenAmount.fromRawAmount(currencyIn, 0) - } - return value - } - } catch (error) {} - return undefined - }, [currencyIn, activeOrderMakingAmount, isEdit, orderInfo]) - - const balance = useCurrencyBalance(currencyIn, chainId) - const maxAmountInput = useMemo(() => { - return maxAmountSpend(balance) - }, [balance]) - - const handleMaxInput = useCallback(() => { - if (!maxAmountInput) return - try { - onSetInput(maxAmountInput.toExact()) - } catch (error) {} - }, [maxAmountInput, onSetInput]) - - const missingAllowance = useMemo(() => { - if (currentAllowance?.equalTo(0)) return true - if (currencyIn?.isNative || !parseInputAmount) return false - const allowanceSubtracted = parsedActiveOrderMakingAmount - ? currentAllowance?.subtract(parsedActiveOrderMakingAmount) - : undefined - if ( - !allowanceSubtracted || - allowanceSubtracted.greaterThan(parseInputAmount) || - allowanceSubtracted.equalTo(parseInputAmount) - ) - return false - return parseInputAmount.subtract(allowanceSubtracted) - }, [currencyIn?.isNative, currentAllowance, parseInputAmount, parsedActiveOrderMakingAmount]) - - const enoughAllowance = !Boolean(missingAllowance) - - const [approval, approveCallback] = useApproveCallback( - parseInputAmount, - limitOrderContract || undefined, - !enoughAllowance, - ) - - const { inputError, outPutError } = useValidateInputError({ - inputAmount, - outputAmount, - balance, - displayRate, - parsedActiveOrderMakingAmount: undefined, - currencyIn, - wrapInputError, - showWrap, - currencyOut, - }) - - const hasInputError = Boolean(inputError || outPutError) - const checkingAllowance = - !(currencyIn && parsedActiveOrderMakingAmount?.currency?.equals(currencyIn)) || - !(currencyIn && currentAllowance?.currency?.equals(currencyIn)) - - const isNotFillAllInput = [outputAmount, inputAmount, currencyIn, currencyOut, displayRate].some(e => !e) - - const expiredAt = customDateExpire?.getTime() || Date.now() + expire * 1000 - - const displayTime = customDateExpire ? dayjs(customDateExpire).format('DD/MM/YYYY HH:mm') : formatTimeDuration(expire) - - const getTokenAddress = (currency: Currency | undefined) => - currency?.isNative ? 'NATIVE' : currency?.wrapped?.address - - const showPreview = () => { - if (!currencyIn || !currencyOut || !outputAmount || !inputAmount || !displayRate) return - setFlowState({ ...TRANSACTION_STATE_DEFAULT, showConfirm: true }) - if (!isEdit) { - trackingHandler(TRACKING_EVENT_TYPE.LO_CLICK_REVIEW_PLACE_ORDER, { - from_token: currencyIn.symbol, - to_token: currencyOut.symbol, - from_network: chainId, - trade_qty: inputAmount, - }) - trackingHandler(TRACKING_EVENT_TYPE.LO_REVIEW_OPENED, { - side: rateInfo.invert ? 'buy' : 'sell', - from_token: currencyIn.symbol, - from_token_address: getTokenAddress(currencyIn), - to_token: currencyOut.symbol, - to_token_address: getTokenAddress(currencyOut), - pair: `${currencyIn.symbol}/${currencyOut.symbol}`, - limit_price: displayRate, - amount_in: inputAmount, - amount_in_usd: estimateUSD.rawInput || undefined, - amount_out_estimated: outputAmount, - expiry: displayTime, - market_price: tradeInfo ? removeTrailingZero(tradeInfo.marketRate.toFixed(16)) : undefined, - price_difference_pct: deltaRate.rawPercent ? Number(deltaRate.rawPercent) : undefined, - chain: networkInfo.name, - }) - } - } - - const hidePreview = useCallback(() => { - setFlowState(state => ({ ...state, showConfirm: false })) - }, [setFlowState]) - - const toggleDatePicker = () => { - setShowDatePicker(!showDatePicker) - } - - const onChangeExpire = (val: Date | number) => { - const previousExpiry = displayTime - if (typeof val === 'number') { - setExpire(val) - setCustomDateExpire(undefined) - trackingHandler(TRACKING_EVENT_TYPE.LO_ENTER_DETAIL, 'choose date') - trackingHandler(TRACKING_EVENT_TYPE.LO_EXPIRY_CHANGED, { - previous_expiry: previousExpiry, - new_expiry: formatTimeDuration(val), - custom_expiry_minutes: null, - chain: networkInfo.name, - }) - } else { - setCustomDateExpire(val) - trackingHandler(TRACKING_EVENT_TYPE.LO_EXPIRY_CHANGED, { - previous_expiry: previousExpiry, - new_expiry: 'custom', - custom_expiry_minutes: Math.round((val.getTime() - Date.now()) / 60000), - chain: networkInfo.name, - }) - } - } - - const onResetForm = () => { - setInputAmount(defaultInputAmount) - setOutputAmount(defaultOutputAmount) - setRateInfo(defaultRate) - setExpire(DEFAULT_EXPIRED) - setCustomDateExpire(undefined) - refreshActiveMakingAmount() - } - - const handleError = useCallback( - (error: any) => { - const errorMessage = getErrorMessage(error) - const isUserRejected = - errorMessage.toLowerCase().includes('user denied') || errorMessage.toLowerCase().includes('user rejected') - trackingHandler(TRACKING_EVENT_TYPE.LO_ORDER_FAILED, { - side: rateInfo.invert ? 'buy' : 'sell', - from_token: currencyIn?.symbol, - to_token: currencyOut?.symbol, - pair: currencyIn && currencyOut ? `${currencyIn.symbol}/${currencyOut.symbol}` : undefined, - limit_price: displayRate, - amount_in: inputAmount, - error_type: isUserRejected ? 'user_rejected' : 'tx_failed', - error_message: errorMessage, - chain: networkInfo.name, - }) - setFlowState(state => ({ - ...state, - attemptingTxn: false, - errorMessage, - })) - }, - [ - setFlowState, - trackingHandler, - rateInfo.invert, - currencyIn, - currencyOut, - displayRate, - inputAmount, - networkInfo.name, - ], - ) - - const signOrder = useSignOrder(setFlowState) - - const [submitOrder] = useCreateOrderMutation() - const onSubmitCreateOrder = async (params: CreateOrderParam) => { - try { - const { currencyIn, currencyOut, account, inputAmount, outputAmount, expiredAt } = params - if (!currencyIn || !currencyOut || !account || !inputAmount || !outputAmount || !expiredAt) { - throw new Error('wrong input') - } - - const refCode = getCookieValue('refCode') - const clientId = searchParams.get('clientId') - - const { signature, salt } = await signOrder({ ...params, referral: refCode }) - const payload = getPayloadCreateOrder(params) - setFlowState(state => ({ ...state, pendingText: t`Placing order` })) - const response = await submitOrder({ ...payload, salt, signature, referral: refCode, clientId }).unwrap() - setFlowState(state => ({ ...state, showConfirm: false })) - - notify( - { - type: NotificationType.SUCCESS, - title: t`Order Placed`, - summary: , - }, - 10000, - ) - onResetForm() - return response?.id - } catch (error) { - handleError(error) - return - } - } - - const onWrapToken = async () => { - try { - if (isNotFillAllInput || wrapInputError || isWrappingEth || hasInputError) return - const amount = formatAmountOrder(inputAmount) - const wethSymbol = WETH[chainId].symbol - const inSymbol = currencyIn?.symbol - setFlowState(state => ({ - ...state, - attemptingTxn: true, - showConfirm: true, - pendingText: t`Wrapping ${amount} ${inSymbol} to ${amount} ${wethSymbol}`, - })) - const hash = await onWrap?.() - hash && setTxHashWrapped(hash) - setFlowState(state => ({ ...state, showConfirm: false })) - } catch (error) { - handleError(error) - } - } - - useEffect(() => { - if (approval === ApprovalState.PENDING) { - setApprovalSubmitted(true) - } - if (approval === ApprovalState.NOT_APPROVED) { - setApprovalSubmitted(false) - } - }, [approval, approvalSubmitted]) - - const refreshActiveMakingAmount = useCallback(() => { - try { - getActiveMakingAmount() - } catch (error) {} - }, [getActiveMakingAmount]) - - useEffect(() => { - if (!isEdit || !orderInfo?.id) return - setOrderEditing({ - orderId: orderInfo.id, - account, - chainId, - currencyIn, - currencyOut, - inputAmount, - outputAmount, - expiredAt, - nativeOutput: currencyOut?.isNative || false, - }) - }, [ - setOrderEditing, - account, - chainId, - currencyIn, - currencyOut, - inputAmount, - outputAmount, - expiredAt, - orderInfo?.id, - isEdit, - ]) - - // use ref to prevent too many api call when firebase update status - const refSubmitCreateOrder = useRef(onSubmitCreateOrder) - refSubmitCreateOrder.current = onSubmitCreateOrder - const [expanded, setExpanded] = useState(false) - - useEffect(() => { - if (!account) return - // call when cancel expired/cancelled - const unsubscribeCancelled = subscribeNotificationOrderCancelled(account, chainId, data => { - data?.orders.forEach(order => { - const findInfo = ordersNeedCreated.find(e => e.orderId === order.id) - if (!findInfo?.orderId) return - removeOrderNeedCreated(findInfo.orderId) - // when cancel order success => create a new order - if (order.isSuccessful && !isEdit) { - refSubmitCreateOrder.current(findInfo) - } - }) - refreshActiveMakingAmount() - }) - const unsubscribeExpired = subscribeNotificationOrderExpired(account, chainId, refreshActiveMakingAmount) - return () => { - unsubscribeCancelled?.() - unsubscribeExpired?.() - } - }, [account, chainId, ordersNeedCreated, removeOrderNeedCreated, refreshActiveMakingAmount, isEdit]) - - const autoFillMarketPrice = useRef(false) - useEffect(() => { - if (tradeInfo && !autoFillMarketPrice.current && !loadingTrade && !defaultRate?.rate) { - autoFillMarketPrice.current = true - setPriceRateMarket(true) - } - }, [tradeInfo, setPriceRateMarket, loadingTrade, defaultRate?.rate]) - - const trackingTouchInput = useCallback(() => { - trackingHandler(TRACKING_EVENT_TYPE.LO_ENTER_DETAIL, 'touch enter amount box') - }, [trackingHandler]) - - const trackingPriceSetOnBlur = useCallback(() => { - if (!displayRate || !currencyIn || !currencyOut) return - trackingHandler(TRACKING_EVENT_TYPE.LO_PRICE_SET, { - side: rateInfo.invert ? 'buy' : 'sell', - limit_price: displayRate, - market_price: tradeInfo ? removeTrailingZero(tradeInfo.marketRate.toFixed(16)) : undefined, - price_difference_pct: deltaRate.rawPercent ? Number(deltaRate.rawPercent) : undefined, - from_token: currencyIn.symbol, - to_token: currencyOut.symbol, - chain: networkInfo.name, - }) - }, [ - displayRate, - currencyIn, - currencyOut, - rateInfo.invert, - tradeInfo, - deltaRate.rawPercent, - networkInfo.name, - trackingHandler, - ]) - - const trackingTouchSelectToken = useCallback(() => { - trackingHandler(TRACKING_EVENT_TYPE.LO_ENTER_DETAIL, 'touch enter token box') - }, [trackingHandler]) - - const trackingPlaceOrder = (type: TRACKING_EVENT_TYPE, data = {}) => { - trackingHandler(type, { - from_token: currencyIn?.symbol, - to_token: currencyOut?.symbol, - from_network: networkInfo.name, - trade_qty: inputAmount, - ...data, - }) - } - - const onSubmitCreateOrderWithTracking = async () => { - trackingPlaceOrder(TRACKING_EVENT_TYPE.LO_CLICK_PLACE_ORDER) - const order_id = await onSubmitCreateOrder({ - currencyIn, - currencyOut, - chainId, - account, - inputAmount, - outputAmount, - expiredAt, - nativeOutput: currencyOut?.isNative || false, - }) - if (order_id) { - trackingPlaceOrder(TRACKING_EVENT_TYPE.LO_PLACE_ORDER_SUCCESS, { order_id }) - trackingHandler(TRACKING_EVENT_TYPE.LO_ORDER_PLACED, { - side: rateInfo.invert ? 'buy' : 'sell', - from_token: currencyIn?.symbol, - from_token_address: getTokenAddress(currencyIn), - to_token: currencyOut?.symbol, - to_token_address: getTokenAddress(currencyOut), - pair: currencyIn && currencyOut ? `${currencyIn.symbol}/${currencyOut.symbol}` : undefined, - limit_price: displayRate, - market_price: tradeInfo ? removeTrailingZero(tradeInfo.marketRate.toFixed(16)) : undefined, - price_difference_pct: deltaRate.rawPercent ? Number(deltaRate.rawPercent) : undefined, - amount_in: inputAmount, - amount_in_usd: estimateUSD.rawInput || undefined, - amount_out_estimated: outputAmount, - expiry: displayTime, - chain: networkInfo.name, - order_id, - volume: estimateUSD.rawInput || undefined, - }) - - // Tip is not charged on limit orders, so this attributes referral volume only - // (tracked at placement — `Limit Order Filled` fires off-page with no tip context). - const tipLink = getTipLinkAttribution(searchParams) - if (tipLink) { - trackingHandler(TRACKING_EVENT_TYPE.TIP_LINK_TRADE, { - trade_type: 'limit_order', - trade_status: 'placed', - tip_charged: false, - ...tipLink, - input_token: currencyIn?.symbol, - output_token: currencyOut?.symbol, - input_token_address: getTokenAddress(currencyIn), - output_token_address: getTokenAddress(currencyOut), - pair: currencyIn && currencyOut ? `${currencyIn.symbol}/${currencyOut.symbol}` : undefined, - chain: networkInfo.name, - chain_id: chainId, - volume: estimateUSD.rawInput || undefined, - order_id, - }) - } - } - } - - const styleTooltip = { maxWidth: '250px', zIndex: zIndexToolTip } - const estimateUSD = useMemo(() => { - return calcUsdPrices({ - inputAmount, - outputAmount, - priceUsdIn: tradeInfo?.priceUsdIn, - priceUsdOut: tradeInfo?.priceUsdOut, - currencyIn, - currencyOut, - }) - }, [inputAmount, outputAmount, tradeInfo, currencyIn, currencyOut]) - - const showApproveFlow = - !checkingAllowance && - !showWrap && - !isNotFillAllInput && - (approval === ApprovalState.NOT_APPROVED || - approval === ApprovalState.PENDING || - !enoughAllowance || - (approvalSubmitted && approval === ApprovalState.APPROVED)) - - const warningMessage = useWarningCreateOrder({ - estimateUSD: estimateUSD.rawInput, - currencyIn, - outputAmount, - displayRate, - deltaRate, - missingAllowance, - }) - - useImperativeHandle(ref, () => ({ - hasChangedOrderInfo() { - return ( - isEdit && - !hasInputError && - (defaultInputAmount !== inputAmount || - defaultRate?.rate !== rateInfo.rate || - defaultExpire?.getTime() !== expiredAt) - ) - }, - })) - - const renderActionBtn = () => - chainId !== walletChainId ? ( - changeNetwork(chainId)}> - Switch to {NETWORKS_INFO[chainId].name} - - ) : ( - 0, - editOrderInfo, - }} - /> - ) - const renderConfirmModal = (showConfirmContent = false) => ( - - ) - - if (isEdit && flowState.showConfirm) - return ( - <> - {renderConfirmModal(true)} - {renderActionBtn()} - - ) - return ( - <> -
    - {useUrlParams ? : null} - - - You Sell - - } - positionLabel="in" - customChainId={chainId} - trackingSource="limit_order" - /> - - - - -
    - - {tradeInfo && ( - setPriceRateMarket()}> - Market - - )} -
    -
    - - {currencyIn && currencyOut && ( -
    onInvertRate(!rateInfo.invert)}> - - - {rateInfo.invert ? currencyIn?.symbol : currencyOut?.symbol} - -
    - -
    -
    - )} -
    -
    -
    - - - {currencyIn && currencyOut ? ( - - ) : ( -
    - )} - - - - - - You Buy - - } - positionLabel="in" - customChainId={chainId} - trackingSource="limit_order" - /> - - -
    -
    - - - Expires in - - -
    setExpanded(e => !e)}> - {displayTime} - -
    -
    - -
    -
    - {[...getExpireOptions(), { label: 'Custom', onSelect: toggleDatePicker }].map((item: any) => { - const isActive = customDateExpire ? item.label === 'Custom' : item.value === expire - return ( - { - if (item.label === 'Custom') item.onSelect() - else onChangeExpire(item.value) - }} - className={cn( - 'whitespace-nowrap text-xs', - isActive - ? 'border-primary-50 bg-tabActive text-text hover:bg-buttonGray' - : 'border-transparent bg-transparent text-subText hover:border-border hover:bg-buttonGray', - )} - > - {item.label} - - ) - })} -
    -
    -
    - - {warningMessage.map((mess, i) => ( - - ))} - - {renderActionBtn()} -
    - - {renderConfirmModal()} - - - - ) -}) - -export default memo(LimitOrderForm) diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListLimitOrder/TabSelector.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListLimitOrder/TabSelector.tsx deleted file mode 100644 index b1bd124856..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListLimitOrder/TabSelector.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Trans, t } from '@lingui/macro' -import { useGetNumberOfInsufficientFundOrdersQuery } from 'services/limitOrder' - -import TabButton from 'components/TabButton' -import { MouseoverTooltip } from 'components/Tooltip' -import { LimitOrderTab } from 'components/swapv2/LimitOrder/type' -import { useActiveWeb3React } from 'hooks' - -const TAB_BUTTON_CLASS = 'h-fit w-fit !flex-none px-4 py-3 text-sm font-medium max-sm:w-1/2' - -export default function TabSelector({ - activeTab, - setActiveTab, -}: { - activeTab: LimitOrderTab - setActiveTab: (n: LimitOrderTab) => void -}) { - const { chainId, account } = useActiveWeb3React() - const { data: numberOfInsufficientFundOrders } = useGetNumberOfInsufficientFundOrdersQuery( - { chainId, maker: account || '' }, - { skip: !account, pollingInterval: 10_000 }, - ) - - return ( -
    - setActiveTab(LimitOrderTab.ORDER_BOOK)} - text={t`Open Limit Orders`} - /> - - My Order(s) - {!!numberOfInsufficientFundOrders && ( - - You have {numberOfInsufficientFundOrders} active orders that don't have sufficient funds. - - } - > - - {numberOfInsufficientFundOrders} - - - )} - - } - onClick={() => setActiveTab(LimitOrderTab.MY_ORDER)} - /> -
    - ) -} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListLimitOrder/index.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListLimitOrder/index.tsx deleted file mode 100644 index cbcf6c7ac7..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListLimitOrder/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ChainId } from '@kyberswap/ks-sdk-core' -import { useSearchParams } from 'react-router-dom' - -import TabSelector from 'components/swapv2/LimitOrder/ListLimitOrder/TabSelector' -import ListMyOrder from 'components/swapv2/LimitOrder/ListOrder' -import OrderBook from 'components/swapv2/LimitOrder/OrderBook' -import { LimitOrderTab } from 'components/swapv2/LimitOrder/type' - -export default function ListLimitOrder({ customChainId }: { customChainId?: ChainId }) { - const [searchParams, setSearchParams] = useSearchParams() - - const activeTab = (searchParams.get('activeTab') as LimitOrderTab) || LimitOrderTab.ORDER_BOOK - - const setActiveTab = (tab: LimitOrderTab) => { - searchParams.set('activeTab', tab) - setSearchParams(searchParams, { replace: true }) - } - - return ( -
    -
    - -
    - - {activeTab === LimitOrderTab.ORDER_BOOK ? : } -
    - ) -} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/ActionButtons.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/ActionButtons.tsx deleted file mode 100644 index 33fd5b204d..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/ActionButtons.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { t } from '@lingui/macro' -import { CSSProperties, useCallback, useEffect, useState } from 'react' -import { ExternalLink as LinkIcon, Trash } from 'react-feather' - -import { DropdownArrowIcon } from 'components/ArrowRotate' -import CopyHelper from 'components/Copy' -import WarningIcon from 'components/Icons/WarningIcon' -import { MouseoverTooltipDesktopOnly } from 'components/Tooltip' -import { isActiveStatus } from 'components/swapv2/LimitOrder/helpers' -import { LimitOrder, LimitOrderStatus } from 'components/swapv2/LimitOrder/type' -import { useActiveWeb3React } from 'hooks' -import useInterval from 'hooks/useInterval' -import useTheme from 'hooks/useTheme' -import { ExternalLink } from 'theme' -import { getEtherscanLink } from 'utils' -import { formatRemainTime } from 'utils/time' - -const IconWrap = ({ - children, - color, - isDisabled, - style, - onClick, -}: { - children: React.ReactNode - color: string - isDisabled?: boolean - style?: CSSProperties - onClick?: (e: React.MouseEvent) => void -}) => ( -
    - {children} -
    -) - -const CancelStatusButton = ({ expiredAt, style }: { expiredAt: number | undefined; style?: CSSProperties }) => { - const theme = useTheme() - const [remain, setRemain] = useState(0) - - useEffect(() => { - const delta = Math.floor((expiredAt || 0) - Date.now() / 1000) - setRemain(Math.max(0, delta)) - }, [expiredAt]) - - const countdown = useCallback(() => { - setRemain(v => v - 1) - }, []) - - useInterval(countdown, remain > 0 ? 1000 : null) - - if (remain <= 0) return null - return ( - - Gaslessly cancelling in {formatRemainTime(remain)} - - } - placement="top" - width="fit-content" - > - - - - - ) -} - -const ActionButtons = ({ - order, - expand, - onExpand, - txHash, - isChildren, - itemStyle = {}, - onCancelOrder, - isCancelling = false, -}: { - order: LimitOrder - expand?: boolean - onExpand?: () => void - txHash: string - isChildren?: boolean - itemStyle?: CSSProperties - onCancelOrder?: (order: LimitOrder) => void - onEditOrder?: (order: LimitOrder) => void - isCancelling?: boolean -}) => { - const { networkInfo } = useActiveWeb3React() - const theme = useTheme() - const { status, chainId, transactions = [], operatorSignatureExpiredAt } = order - const isActiveTab = isActiveStatus(status) - const numberTxs = transactions.length - - const iconExpand = - ((isActiveTab && numberTxs >= 1) || (!isActiveTab && numberTxs > 1)) && !isChildren ? ( - { - e.stopPropagation() - onExpand?.() - }} - style={itemStyle} - > - - - ) : null - - const iconCancelling = - !isChildren && status === LimitOrderStatus.CANCELLING ? ( - - ) : null - - const isDisabledCopy = - !isChildren && [LimitOrderStatus.CANCELLED, LimitOrderStatus.CANCELLING, LimitOrderStatus.EXPIRED].includes(status) - const disabledCancel = isCancelling - - return ( -
    - {iconCancelling} - {isActiveTab && !isChildren ? ( - - { - e.stopPropagation() - onCancelOrder?.(order) - }} - > - - - - ) : ( - (numberTxs <= 1 || isChildren) && ( - <> - - - - - - - - - - - - - - ) - )} - {iconExpand} -
    - ) -} -export default ActionButtons diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/ListOrderSkeleton.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/ListOrderSkeleton.tsx deleted file mode 100644 index 42b8226c72..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/ListOrderSkeleton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import Skeleton from 'components/Skeleton' -import { ItemWrapper } from 'components/swapv2/LimitOrder/ListOrder/OrderItem' -import TableHeader from 'components/swapv2/LimitOrder/ListOrder/TableHeader' -import { cn } from 'utils/cn' - -const Stack = ({ children, className }: { children: React.ReactNode; className?: string }) => ( -
    {children}
    -) - -// Mirrors OrderItem's 5-cell grid via the shared ItemWrapper, so the placeholders line up under the -// table headers (LIMIT ORDER(S) | RATE | CREATED·EXPIRY | FILLED%·STATUS | ACTION). -const RowSkeleton = ({ isLast }: { isLast?: boolean }) => ( - -
    - - - - - - -
    - - {/* RATE — `rate` class hides it on max-lg, matching the real row */} - - - - - {/* CREATED | EXPIRY */} - - - - - - {/* FILLED % | STATUS */} - - - - - - {/* ACTION */} -
    - - -
    -
    -) - -const ListOrderSkeleton = ({ rows = 5 }: { rows?: number }) => ( -
    - - {Array.from({ length: rows }, (_, i) => ( - - ))} -
    -) - -export default ListOrderSkeleton diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/OrderItem.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/OrderItem.tsx deleted file mode 100644 index 090c265711..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/OrderItem.tsx +++ /dev/null @@ -1,496 +0,0 @@ -import { Token } from '@kyberswap/ks-sdk-core' -import { Trans, t } from '@lingui/macro' -import dayjs from 'dayjs' -import { CSSProperties, useEffect, useMemo, useState } from 'react' -import { Repeat } from 'react-feather' -import { useNavigate, useSearchParams } from 'react-router-dom' -import { useMedia } from 'react-use' - -import InfoHelper from 'components/InfoHelper' -import Logo from 'components/Logo' -import ProgressBar from 'components/ProgressBar' -import ActionButtons from 'components/swapv2/LimitOrder/ListOrder/ActionButtons' -import { - calcPercentFilledOrder, - formatAmountOrder, - formatRateLimitOrder, - isActiveStatus, -} from 'components/swapv2/LimitOrder/helpers' -import { LimitOrder, LimitOrderStatus } from 'components/swapv2/LimitOrder/type' -import { NETWORKS_INFO } from 'constants/networks' -import { NativeCurrencies } from 'constants/tokens' -import useTheme from 'hooks/useTheme' -import { useTokenBalance } from 'state/wallet/hooks' -import { MEDIA_WIDTHS } from 'theme' -import { cn } from 'utils/cn' -import { toCurrencyAmount } from 'utils/currencyAmount' - -type Theme = ReturnType - -interface ItemWrapperProps extends React.HTMLAttributes { - hasBorder?: boolean - active?: boolean - highlight?: boolean -} - -export const ItemWrapper = ({ - hasBorder, - active, - highlight, - className, - children, - style, - ...rest -}: ItemWrapperProps) => ( -
    - {children} -
    -) - -const ItemWrapperMobile = ({ children, className, ...rest }: React.HTMLAttributes) => ( -
    - {children} -
    -) - -const DeltaAmount = ({ - color, - className, - style, - children, - ...rest -}: React.HTMLAttributes & { color?: string }) => ( -
    - {children} -
    -) - -const Colum = ({ children, className, ...rest }: React.HTMLAttributes) => ( -
    - {children} -
    -) - -const TimeText = ({ time, style = {} }: { time: number; style?: CSSProperties }) => { - return ( -
    - {dayjs(time * 1000).format('DD/MM/YYYY')} -   {dayjs(time * 1000).format('HH:mm')} -
    - ) -} - -const TokenLogo = ({ srcs }: { srcs: string[] }) => - -const SingleAmountInfo = ({ - amount, - color, - className, - logoUrl, - symbol, - plus = true, - hideLogo = false, - decimals, -}: { - amount: string - color?: string - className?: string - symbol: string - logoUrl: string - plus?: boolean - hideLogo?: boolean - decimals: number -}) => ( -
    - {!hideLogo && } - - {plus ? '+' : '-'} {formatAmountOrder(amount, decimals)} {symbol || '???'} - -
    -) -const AmountInfo = ({ order, takerSymbol }: { order: LimitOrder; takerSymbol: string }) => { - const { - makerAssetSymbol, - makerAssetLogoURL, - takerAssetLogoURL, - takerAssetSymbol, - makingAmount, - takingAmount, - makerAssetDecimals, - takerAssetDecimals, - nativeOutput, - chainId, - } = order - const native = NativeCurrencies[chainId] - const isNative = nativeOutput && takerAssetSymbol.toLowerCase() === native?.wrapped.symbol?.toLowerCase() - return ( - - - - - ) -} - -const TradeRateOrder = ({ - order, - symbolOut, - style = {}, -}: { - order: LimitOrder - symbolOut: string - style?: CSSProperties -}) => { - const [invert, setInvert] = useState(false) - const symbolIn = order.makerAssetSymbol || '???' - - const onInvert = (event: React.MouseEvent) => { - event.stopPropagation() - setInvert(!invert) - } - - return ( - event.stopPropagation()}> -
    - {!invert ? `${symbolOut}/${symbolIn}` : `${symbolIn}/${symbolOut}`} - -
    - {formatRateLimitOrder(order, invert)} -
    - ) -} - -function formatStatus(status: string) { - status = status.replace('_', ' ') - return status.charAt(0).toUpperCase() + status.slice(1) -} - -function getNeededMakingAmount(order: LimitOrder) { - const makingToken = new Token(order.chainId, order.makerAsset, order.makerAssetDecimals, order.makerAssetSymbol, '') - const makingAmount = toCurrencyAmount(makingToken, order.makingAmount) - const filledMakingAmount = toCurrencyAmount(makingToken, order.filledMakingAmount) - - return makingAmount.subtract(filledMakingAmount) -} - -function formatStatusLimitOrder(order: LimitOrder, isCancelling = false, isNotSufficientFund = false) { - const { takingAmount, filledTakingAmount, takerAssetDecimals } = order - const filledPercent = calcPercentFilledOrder(filledTakingAmount, takingAmount, takerAssetDecimals) - const status = isCancelling ? LimitOrderStatus.CANCELLING : order.status - const partiallyFilled = status === LimitOrderStatus.PARTIALLY_FILLED - const expandTitle = [LimitOrderStatus.EXPIRED, LimitOrderStatus.CANCELLED, LimitOrderStatus.CANCELLING].includes( - status, - ) - ? ` | ${formatStatus(status)}` - : isNotSufficientFund && status !== LimitOrderStatus.FILLED - ? `, ${t`insufficient funds`}` - : '' - return `${partiallyFilled ? t`Partially Filled` : t`Filled`} ${filledPercent}%${expandTitle}` -} - -const getColorStatus = (status: LimitOrderStatus, theme: Theme, isNotSufficientFund = false) => { - const MapStatusColor: { [key: string]: string } = { - [LimitOrderStatus.FILLED]: theme.primary, - [LimitOrderStatus.CANCELLED]: theme.red, - [LimitOrderStatus.CANCELLING]: theme.red, - [LimitOrderStatus.EXPIRED]: theme.warning, - [LimitOrderStatus.PARTIALLY_FILLED]: theme.warning, - } - - const color = MapStatusColor[status] - if (color) { - return color - } - - if (isNotSufficientFund) { - return theme.warning - } - - return undefined -} - -const IndexText = ({ children }: { children?: React.ReactNode }) => ( -
    {children}
    -) - -const WarningText = ({ children }: { children: React.ReactNode }) => {children} - -export default function OrderItem({ - order, - index, - onCancelOrder, - onEditOrder, - isOrderCancelling, - tokenPrices, - isLast, - hasOrderCancelling, -}: { - order: LimitOrder - onCancelOrder: (order: LimitOrder) => void - onEditOrder: (order: LimitOrder) => void - index: number - isOrderCancelling: (order: LimitOrder) => boolean - tokenPrices: Record - isLast: boolean - hasOrderCancelling: boolean -}) { - const [expand, setExpand] = useState(false) - const upToSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToSmall}px)`) - const isCancelling = isOrderCancelling(order) - const theme = useTheme() - - const { - createdAt = Date.now(), - expiredAt = Date.now(), - takingAmount, - filledTakingAmount, - transactions = [], - takerAssetSymbol, - takerAssetDecimals, - takerAsset, - makerAsset, - nativeOutput, - chainId, - } = order - const native = NativeCurrencies[chainId] - const isNative = nativeOutput && takerAssetSymbol.toLowerCase() === native?.wrapped.symbol?.toLowerCase() - const takerSymbol = isNative ? native?.symbol || takerAssetSymbol : takerAssetSymbol - - const status = isCancelling ? LimitOrderStatus.CANCELLING : order.status - const isOrderActive = isActiveStatus(order.status) - const filledPercent = calcPercentFilledOrder(filledTakingAmount, takingAmount, takerAssetDecimals) - - const makingToken = useMemo(() => { - return new Token(order.chainId, order.makerAsset, order.makerAssetDecimals, order.makerAssetSymbol, '') - }, [order.chainId, order.makerAsset, order.makerAssetDecimals, order.makerAssetSymbol]) - - const makingTokenBalance = useTokenBalance(makingToken) - const neededFund = getNeededMakingAmount(order) - const isNotSufficientFund = makingTokenBalance ? makingTokenBalance.lessThan(neededFund) : false - - const colorStatus = getColorStatus(status, theme, isNotSufficientFund) - const txHash = transactions[0]?.txHash ?? '' - const toggle = () => setExpand(prev => !prev) - - const marketPrice = tokenPrices[order.takerAsset] / tokenPrices[order.makerAsset] - const selectedPrice = Number(formatRateLimitOrder(order, false)) - const percent = ((marketPrice - selectedPrice) / marketPrice) * 100 - - const navigate = useNavigate() - const onClickOrder = () => { - navigate({ - search: new URLSearchParams({ - inputCurrency: makerAsset, - outputCurrency: takerAsset, - }).toString(), - }) - } - - const [searchParams, setSearchParams] = useSearchParams() - useEffect(() => { - const i = setTimeout(() => { - searchParams.delete('highlight') - setSearchParams(searchParams) - }, 5_000) // to ensure the searchParams is updated after the click - - return () => clearTimeout(i) - }, [searchParams, setSearchParams]) - - const renderProgressComponent = () => { - const getTooltipText = () => { - const texts = [Insufficient {order.makerAssetSymbol} balance for order execution.] - - if (Number.isFinite(percent) && percent < 0) { - texts.push(<> ) - texts.push( - - Once you add {order.makerAssetSymbol}, the order will be executed at{' '} - {percent.toFixed(2)}% below the market price. - , - ) - } - - return texts - } - - return ( - -
    - {isOrderActive && isNotSufficientFund && ( - - )}{' '} - {formatStatusLimitOrder(order, isCancelling, isNotSufficientFund)} -
    - -
    - ) - } - - if (upToSmall) { - return ( - -
    - - -
    -
    - {renderProgressComponent()} - -
    - {expand && ( -
    - {transactions.map(txs => { - return ( -
    - -
    - - -
    -
    - ) - })} -
    - )} -
    - - - Created - - - - - - Expiry - - - -
    -
    - ) - } - - const highlight = - searchParams.get('highlight') === 'true' && - order.makerAsset.toLowerCase() === searchParams.get('search')?.toLowerCase() && - isOrderActive - - return ( - <> - -
    - {index + 1} - -
    - - - - - - - - {renderProgressComponent()} - -
    - {expand && ( -
    - {transactions.map(txs => { - const filledPercent = calcPercentFilledOrder(txs.takingAmount, takingAmount, takerAssetDecimals) - return ( - -
    - -
    -
    - - + {formatAmountOrder(txs.takingAmount, takerAssetDecimals)} {takerSymbol} - -
    -
    - - - - - - {filledPercent}% - - - - ) - })} -
    - )} - - ) -} diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/TabSelector.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/TabSelector.tsx deleted file mode 100644 index c91275e909..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/TabSelector.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { t } from '@lingui/macro' - -import { LimitOrderStatus } from 'components/swapv2/LimitOrder/type' -import { cn } from 'utils/cn' - -const TabButton = ({ active, ...props }: { active: boolean } & React.HTMLAttributes) => ( -
    -) - -const TabSelector = ({ - activeTab, - setActiveTab, -}: { - activeTab: LimitOrderStatus - setActiveTab: (n: LimitOrderStatus) => void -}) => { - return ( -
    - setActiveTab(LimitOrderStatus.ACTIVE)}> - {t`Active Orders`} - - - | - - setActiveTab(LimitOrderStatus.CLOSED)}> - {t`Order History`} - -
    - ) -} -export default TabSelector diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/TableHeader.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/TableHeader.tsx deleted file mode 100644 index 596baba843..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/TableHeader.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Trans } from '@lingui/macro' -import { useMedia } from 'react-use' - -import { ItemWrapper } from 'components/swapv2/LimitOrder/ListOrder/OrderItem' -import { MEDIA_WIDTHS } from 'theme' - -const TableHeader = () => { - const upToSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToSmall}px)`) - return ( - - {!upToSmall ? ( - <> -
    - - LIMIT ORDER(S) - -
    - - RATE - - - CREATED | EXPIRY - - - FILLED % | STATUS - - - ACTION - - - ) : ( - - LIMIT ORDER(S) - - )} -
    - ) -} - -export default TableHeader diff --git a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/index.tsx b/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/index.tsx deleted file mode 100644 index a6185b78f0..0000000000 --- a/apps/kyberswap-interface/src/components/swapv2/LimitOrder/ListOrder/index.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import { ChainId } from '@kyberswap/ks-sdk-core' -import { Trans, t } from '@lingui/macro' -import { HTMLAttributes, ReactNode, useCallback, useEffect, useMemo, useState } from 'react' -import { Trash } from 'react-feather' -import { useNavigate, useSearchParams } from 'react-router-dom' -import { useGetListOrdersQuery } from 'services/limitOrder' - -import { ReactComponent as NoDataIcon } from 'assets/svg/no_data.svg' -import { ButtonLight } from 'components/Button' -import Column from 'components/Column' -import Pagination from 'components/Pagination' -import Row from 'components/Row' -import SearchInput from 'components/SearchInput' -import Select from 'components/Select' -import EditOrderModal from 'components/swapv2/LimitOrder/EditOrderModal' -import ListOrderSkeleton from 'components/swapv2/LimitOrder/ListOrder/ListOrderSkeleton' -import OrderItem from 'components/swapv2/LimitOrder/ListOrder/OrderItem' -import TabSelector from 'components/swapv2/LimitOrder/ListOrder/TabSelector' -import TableHeader from 'components/swapv2/LimitOrder/ListOrder/TableHeader' -import useRequestCancelOrder from 'components/swapv2/LimitOrder/ListOrder/useRequestCancelOrder' -import CancelOrderModal from 'components/swapv2/LimitOrder/Modals/CancelOrderModal' -import { ACTIVE_ORDER_OPTIONS, CLOSE_ORDER_OPTIONS } from 'components/swapv2/LimitOrder/const' -import { - calcPercentFilledOrder, - formatAmountOrder, - formatRateLimitOrder, - getPayloadTracking, - isActiveStatus, -} from 'components/swapv2/LimitOrder/helpers' -import { LIMIT_ORDERS_PAGE_SIZE } from 'components/swapv2/LimitOrder/listOrdersArgs' -import { LimitOrder, LimitOrderStatus } from 'components/swapv2/LimitOrder/type' -import useCancellingOrders from 'components/swapv2/LimitOrder/useCancellingOrders' -import { EMPTY_ARRAY, RTK_QUERY_TAGS, TRANSACTION_STATE_DEFAULT } from 'constants/index' -import { useActiveWeb3React } from 'hooks' -import { useInvalidateTagLimitOrder } from 'hooks/useInvalidateTags' -import usePageLocation from 'hooks/usePageLocation' -import useParsedQueryString from 'hooks/useParsedQueryString' -import useShowLoadingAtLeastTime from 'hooks/useShowLoadingAtLeastTime' -import useTracking, { TRACKING_EVENT_TYPE } from 'hooks/useTracking' -import { useLimitState } from 'state/limit/hooks' -import { useTokenPricesWithLoading } from 'state/tokenPrices/hooks' -import { cn } from 'utils/cn' -import { - subscribeNotificationOrderCancelled, - subscribeNotificationOrderExpired, - subscribeNotificationOrderFilled, -} from 'utils/firebase' - -// Shared with the nav-intent prefetch (utils/prefetch) so the page's initial query key can't drift. -const PAGE_SIZE = LIMIT_ORDERS_PAGE_SIZE - -export const NoResultWrapper = ({ className, ...rest }: HTMLAttributes) => ( -
    -) - -const TableFooter = ({ children = [], isTabActive }: { children: ReactNode[]; isTabActive: boolean }) => { - const totalChild = children.filter(Boolean).length - return totalChild ? ( -
    - {children} -
    - ) : null -} - -export default function ListMyOrder({ customChainId }: { customChainId?: ChainId }) { - const { account, chainId: walletChainId, networkInfo } = useActiveWeb3React() - const chainId = customChainId || walletChainId - const [curPage, setCurPage] = useState(1) - - const { tab, ...qs } = useParsedQueryString<{ tab: LimitOrderStatus }>() - const [orderType, setOrderType] = useState(LimitOrderStatus.ACTIVE) - - const [searchParams, setSearchParams] = useSearchParams() - const keyword = searchParams.get('search') || '' - - const setKeyword = useCallback( - (val: string) => { - searchParams.set('search', val) - setSearchParams(searchParams, { replace: true }) - }, - [searchParams, setSearchParams], - ) - - const [isOpenCancel, setIsOpenCancel] = useState(false) - const [isOpenEdit, setIsOpenEdit] = useState(false) - const { ordersNeedCreated: ordersUpdating } = useLimitState() - - const { isOrderCancelling } = useCancellingOrders() - const { trackingHandler } = useTracking() - - const { data: { orders = [], totalOrder = 0 } = {}, isFetching } = useGetListOrdersQuery( - { - chainId, - maker: account, - status: orderType, - query: keyword, - page: curPage, - pageSize: PAGE_SIZE, - }, - { skip: !account, refetchOnFocus: true }, - ) - - const loading = useShowLoadingAtLeastTime(isFetching) - - const [currentOrder, setCurrentOrder] = useState() - const [isCancelAll, setIsCancelAll] = useState(false) - - const tokenAddresses = useMemo(() => { - const activeOrders = orders.filter(e => isActiveStatus(e.status)) - if (!activeOrders.length) { - return EMPTY_ARRAY - } - return activeOrders.flatMap(order => [order.takerAsset, order.makerAsset]) - }, [orders]) - - const { refetch, data: tokenPrices } = useTokenPricesWithLoading(tokenAddresses, chainId) - - useEffect(() => { - // Refresh token prices each 10 seconds - const interval = setInterval(refetch, 10_000) - return () => { - clearInterval(interval) - } - }, [refetch]) - - const onPageChange = (page: number) => { - setCurPage(page) - } - - const onReset = useCallback(() => { - setCurPage(1) - }, []) - - const { isEmbeddedSwap } = usePageLocation() - const navigate = useNavigate() - const onSelectTab = (type: LimitOrderStatus) => { - setOrderType(type) - onReset() - if (!isEmbeddedSwap) { - navigate({ search: new URLSearchParams(qs).toString() }, { replace: true }) - } - } - - const onChangeKeyword = (val: string) => { - setKeyword(val) - setCurPage(1) - } - - useEffect(() => { - onReset() - }, [chainId, onReset, orderType]) - - const invalidateTag = useInvalidateTagLimitOrder() - const refetchOrders = useCallback(() => { - invalidateTag(RTK_QUERY_TAGS.GET_LIST_ORDERS) - }, [invalidateTag]) - - const refreshListOrder = useCallback(() => { - try { - onReset() - refetchOrders() - } catch (error) {} - }, [onReset, refetchOrders]) - - useEffect(() => { - if (!account) return - const callback = (data: any) => { - const orders: LimitOrder[] = data?.orders ?? [] - if (orders.length) refreshListOrder() - } - const unsubscribeCancelled = subscribeNotificationOrderCancelled(account, chainId, data => { - refreshListOrder() - const cancelledOrders: LimitOrder[] = data?.orders ?? [] - cancelledOrders.forEach(order => { - trackingHandler(TRACKING_EVENT_TYPE.LO_ORDER_CANCELLED, { - order_id: order.id, - side: 'sell', - from_token: order.makerAssetSymbol, - to_token: order.takerAssetSymbol, - pair: `${order.makerAssetSymbol}/${order.takerAssetSymbol}`, - limit_price: formatRateLimitOrder(order, false), - amount_in: formatAmountOrder(order.makingAmount, order.makerAssetDecimals), - time_active_minutes: Math.round((Date.now() / 1000 - order.createdAt) / 60), - chain: networkInfo.name, - }) - }) - }) - const unsubscribeExpired = subscribeNotificationOrderExpired(account, chainId, callback) - const unsubscribeFilled = subscribeNotificationOrderFilled(account, chainId, data => { - const filledOrders: LimitOrder[] = data?.orders ?? [] - if (filledOrders.length) refreshListOrder() - filledOrders.forEach(order => { - const lastTx = order.transactions?.[order.transactions.length - 1] - trackingHandler(TRACKING_EVENT_TYPE.LO_ORDER_FILLED, { - order_id: order.id, - side: 'sell', - from_token: order.makerAssetSymbol, - to_token: order.takerAssetSymbol, - pair: `${order.makerAssetSymbol}/${order.takerAssetSymbol}`, - limit_price: formatRateLimitOrder(order, false), - fill_price: formatRateLimitOrder(order, false), - amount_in: formatAmountOrder(order.makingAmount, order.makerAssetDecimals), - amount_out_actual: formatAmountOrder(order.filledTakingAmount, order.takerAssetDecimals), - tx_hash: lastTx?.txHash, - chain: networkInfo.name, - }) - }) - }) - return () => { - unsubscribeCancelled?.() - unsubscribeExpired?.() - unsubscribeFilled?.() - } - }, [account, chainId, refreshListOrder, trackingHandler, networkInfo.name]) - - const { flowState, setFlowState, onCancelOrder } = useRequestCancelOrder({ - orders, - isCancelAll, - totalOrder, - }) - - const hideConfirmCancel = useCallback(() => { - setFlowState(TRANSACTION_STATE_DEFAULT) - setIsOpenCancel(false) - setTimeout(() => { - setCurrentOrder(undefined) - }, 300) - }, [setFlowState]) - - const hideEditModal = useCallback(() => { - setFlowState(TRANSACTION_STATE_DEFAULT) - setCurrentOrder(undefined) - setIsOpenEdit(false) - }, [setFlowState]) - - const showConfirmCancel = useCallback( - (order?: LimitOrder) => { - setCurrentOrder(order) - setFlowState({ ...TRANSACTION_STATE_DEFAULT, showConfirm: true }) - setIsOpenCancel(true) - setIsCancelAll(false) - if (order) { - trackingHandler(TRACKING_EVENT_TYPE.LO_CLICK_CANCEL_ORDER, getPayloadTracking(order, networkInfo.name)) - } - }, - [trackingHandler, setFlowState, networkInfo], - ) - - const showEditOrderModal = useCallback( - (order: LimitOrder) => { - setFlowState({ ...TRANSACTION_STATE_DEFAULT }) - setCurrentOrder(order) - setIsOpenEdit(true) - setIsCancelAll(false) - trackingHandler(TRACKING_EVENT_TYPE.LO_CLICK_EDIT_ORDER, getPayloadTracking(order, networkInfo.name)) - }, - [trackingHandler, networkInfo.name, setFlowState], - ) - - const totalOrderNotCancelling = useMemo(() => { - return orders.filter(e => !isOrderCancelling(e)).length - }, [orders, isOrderCancelling]) - - const onCancelAllOrder = () => { - showConfirmCancel() - setIsCancelAll(true) - } - - const disabledBtnCancelAll = totalOrderNotCancelling === 0 - const isTabActive = isActiveStatus(orderType) - - useEffect(() => { - const orderCancelling = orders.length - totalOrderNotCancelling - window.onbeforeunload = () => (orderCancelling > 0 && ordersUpdating.length > 0 ? '' : null) // return null will not show confirm, else will show - }, [totalOrderNotCancelling, orders, ordersUpdating]) - - const filledPercent = - currentOrder && - calcPercentFilledOrder(currentOrder.filledTakingAmount, currentOrder.takingAmount, currentOrder.takerAssetDecimals) - - return ( -
    -
    - -
    - -
    - - setCustomGasPercent(v)} placeholder={t`Custom`} diff --git a/apps/kyberswap-interface/src/pages/Earns/components/SmartExit/Metrics/TimeInput.tsx b/apps/kyberswap-interface/src/pages/Earns/components/SmartExit/Metrics/TimeInput.tsx index 878dde6a41..d956b4466e 100644 --- a/apps/kyberswap-interface/src/pages/Earns/components/SmartExit/Metrics/TimeInput.tsx +++ b/apps/kyberswap-interface/src/pages/Earns/components/SmartExit/Metrics/TimeInput.tsx @@ -3,7 +3,7 @@ import dayjs from 'dayjs' import { useMemo, useState } from 'react' import { Calendar } from 'react-feather' -import DateTimePicker from 'components/swapv2/LimitOrder/ExpirePicker' +import DateTimePicker from 'components/DateTimePicker' import useTheme from 'hooks/useTheme' import { DEFAULT_TIME_OPTIONS } from 'pages/Earns/components/SmartExit/ExpireSetting' import { HighlightWrapper } from 'pages/Earns/components/SmartExit/GuidedHighlight' diff --git a/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/CurrencyInputForStake.tsx b/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/CurrencyInputForStake.tsx index 02b35c8f9a..e696fecf19 100644 --- a/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/CurrencyInputForStake.tsx +++ b/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/CurrencyInputForStake.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react' import KNC from 'assets/images/KNC.svg' import Wallet from 'components/Icons/Wallet' -import Input from 'components/NumericalInput' +import NumericalInput from 'components/NumericalInput' import { AutoRow, RowBetween } from 'components/Row' import useTokenBalance from 'hooks/useTokenBalance' import { KNCLogoWrapper, SmallButton } from 'pages/KyberDAO/StakeKNC/StakeKNCComponent' @@ -61,7 +61,7 @@ export default function CurrencyInputForStake({ - + ~${kncValueInUsd} {getTokenLogoURL(tokenAddress, ChainId.MAINNET) !== '' ? ( diff --git a/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/MigrateModal.tsx b/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/MigrateModal.tsx index b0fd577647..6f6287ff8f 100644 --- a/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/MigrateModal.tsx +++ b/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/MigrateModal.tsx @@ -46,7 +46,10 @@ export default function MigrateModal({ value, ) - const [approval, approveCallback] = useApproveCallback(parsedAmount, kyberDAOInfo?.KNCAddress) + const [approval, approveCallback] = useApproveCallback({ + amount: parsedAmount, + spender: kyberDAOInfo?.KNCAddress, + }) const oldKNCBalance = useTokenBalance(kyberDAOInfo?.KNCLAddress || '') useEffect(() => { diff --git a/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/StakeKNCComponent.tsx b/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/StakeKNCComponent.tsx index 201917e074..100b385353 100644 --- a/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/StakeKNCComponent.tsx +++ b/apps/kyberswap-interface/src/pages/KyberDAO/StakeKNC/StakeKNCComponent.tsx @@ -21,7 +21,7 @@ import VoteIcon from 'components/Icons/Vote' import Wallet from 'components/Icons/Wallet' import WarningIcon from 'components/Icons/WarningIcon' import InfoHelper from 'components/InfoHelper' -import Input from 'components/NumericalInput' +import NumericalInput from 'components/NumericalInput' import Row, { AutoRow, RowBetween, RowFit } from 'components/Row' import useParsedAmount from 'components/SwapForm/hooks/useParsedAmount' import { MouseoverTooltip } from 'components/Tooltip' @@ -149,10 +149,10 @@ export default function StakeKNCComponent() { inputValue, ) - const [approvalKNC, approveCallback] = useApproveCallback( - activeTab === STAKE_TAB.Stake && inputValue ? parsedAmount : undefined, - kyberDAOInfo?.staking, - ) + const [approvalKNC, approveCallback] = useApproveCallback({ + amount: activeTab === STAKE_TAB.Stake && inputValue ? parsedAmount : undefined, + spender: kyberDAOInfo?.staking, + }) const stakedBalanceFormatted = formatUnits(BigInt((stakedBalance || 0).toString()), 18) const currentVotingPower = calculateVotingPower(stakedBalanceFormatted) @@ -351,7 +351,7 @@ export default function StakeKNCComponent() { )} - + ~${kncValueInUsd} diff --git a/apps/kyberswap-interface/src/pages/NotificationCenter/CreateAlert/styleds.tsx b/apps/kyberswap-interface/src/pages/NotificationCenter/CreateAlert/styleds.tsx index 892fa13403..c29b05b7f8 100644 --- a/apps/kyberswap-interface/src/pages/NotificationCenter/CreateAlert/styleds.tsx +++ b/apps/kyberswap-interface/src/pages/NotificationCenter/CreateAlert/styleds.tsx @@ -1,5 +1,5 @@ import { ButtonPrimary } from 'components/Button' -import Input from 'components/NumericalInput' +import NumericalInput from 'components/NumericalInput' import Select from 'components/Select' import { cn } from 'utils/cn' @@ -47,8 +47,8 @@ export const RightColumn = ({ children, className, ...rest }: React.HTMLAttribut
    ) -export const StyledInputNumber = ({ className, ...rest }: React.ComponentProps) => ( - +export const StyledInputNumber = ({ className, ...rest }: React.ComponentProps) => ( + ) export const StyledInput = ({ className, ...rest }: React.TextareaHTMLAttributes) => ( diff --git a/apps/kyberswap-interface/src/pages/PartnerSwap/index.tsx b/apps/kyberswap-interface/src/pages/PartnerSwap/index.tsx index 7a3c116b98..df31bbfe58 100644 --- a/apps/kyberswap-interface/src/pages/PartnerSwap/index.tsx +++ b/apps/kyberswap-interface/src/pages/PartnerSwap/index.tsx @@ -1,22 +1,21 @@ import { ChainId, Currency, WETH } from '@kyberswap/ks-sdk-core' -import { t } from '@lingui/macro' import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { usePreviousDistinct } from 'react-use' import { useGetTipLinkQuery } from 'services/tipLink' import Banner from 'components/Banner' +import LimitOrderForm from 'components/LimitOrder/Form/LimitOrderForm' +import OrderList from 'components/LimitOrder/OrderList' import SwapForm, { SwapFormProps } from 'components/SwapForm' import { SwitchLocaleLink } from 'components/SwitchLocaleLink' import { DEFAULT_TIP, TIP_LINK_CLIENT_ID, isCreatorNameValid } from 'components/TipLinkGeneratorModal/shared' -import LimitOrderForm from 'components/swapv2/LimitOrder/LimitOrderForm' -import ListLimitOrder from 'components/swapv2/LimitOrder/ListLimitOrder' import LiquiditySourcesPanel from 'components/swapv2/LiquiditySourcesPanel' import SettingsPanel from 'components/swapv2/SwapSettingsPanel' import useRequiredDegenMode from 'components/swapv2/SwapSettingsPanel/useRequiredDegenMode' import TokenInfoTab from 'components/swapv2/TokenInfo' import { Container, InfoComponentsWrapper, PageWrapper, SwapFormWrapper } from 'components/swapv2/styleds' -import { MAX_FEE_IN_BIPS, TRANSACTION_STATE_DEFAULT } from 'constants/index' +import { MAX_FEE_IN_BIPS } from 'constants/index' import { SUPPORTED_NETWORKS } from 'constants/networks' import { DEFAULT_OUTPUT_TOKEN_BY_CHAIN, NativeCurrencies, PRICE_CHART_QUOTE_TOKEN_BY_CHAIN } from 'constants/tokens' import { useActiveWeb3React } from 'hooks' @@ -35,7 +34,6 @@ import { Field } from 'state/swap/actions' import { usePermitData } from 'state/swap/hooks' import { useDegenModeManager, useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks' import { useCurrencyBalances } from 'state/wallet/hooks' -import { TransactionFlowState } from 'types/TransactionFlowState' import { ChargeFeeBy, DetailedRouteSummary } from 'types/route' import { isAddress } from 'utils' import { getTradeComposition } from 'utils/aggregationRouting' @@ -289,11 +287,6 @@ export default function PartnerSwap({ mode = 'partner' }: Props) { omniView: true, } - // modal and loading - const [flowState, setFlowState] = useState(TRANSACTION_STATE_DEFAULT) - - const currencyName = currencyOut?.wrapped.name - const currencySymbol = currencyOut?.wrapped.symbol return ( <> @@ -307,7 +300,7 @@ export default function PartnerSwap({ mode = 'partner' }: Props) { activeMainTab={activeMainTab} /> - + {isSwapPage && } {activeTab === TAB.INFO && } {activeTab === TAB.SETTINGS && ( @@ -328,22 +321,7 @@ export default function PartnerSwap({ mode = 'partner' }: Props) { {activeTab === TAB.LIQUIDITY_SOURCES && ( setActiveTab(TAB.SETTINGS)} chainId={swapChainId} /> )} - {activeTab === TAB.LIMIT && ( -
    - -
    - )} + {activeTab === TAB.LIMIT && } {activeTab === TAB.CROSS_CHAIN && } {activeTab === TAB.CROSS_CHAIN_SOURCES && ( setActiveTab(TAB.SETTINGS)} /> @@ -364,7 +342,7 @@ export default function PartnerSwap({ mode = 'partner' }: Props) { isSmartSettlementActive={isSmartSettlementActive} /> )} - {isLimitPage && } + {isLimitPage && } {isCrossChainPage && } diff --git a/apps/kyberswap-interface/src/pages/RemoveLiquidity/TokenPair.tsx b/apps/kyberswap-interface/src/pages/RemoveLiquidity/TokenPair.tsx index 1fac247c58..ec47ada24c 100644 --- a/apps/kyberswap-interface/src/pages/RemoveLiquidity/TokenPair.tsx +++ b/apps/kyberswap-interface/src/pages/RemoveLiquidity/TokenPair.tsx @@ -133,7 +133,10 @@ export default function TokenPair({ // allowance handling const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null) - const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], contractAddress) + const [approval, approveCallback] = useApproveCallback({ + amount: parsedAmounts[Field.LIQUIDITY], + spender: contractAddress, + }) // if user liquidity change => remove signature useEffect(() => { @@ -150,7 +153,8 @@ export default function TokenPair({ if (!liquidityAmount) throw new Error('missing liquidity amount') if (isArgentWallet) { - return approveCallback() + await approveCallback() + return } // try to gather a signature for permission @@ -216,7 +220,7 @@ export default function TokenPair({ 8000, ) } else { - approveCallback() + await approveCallback() } } } diff --git a/apps/kyberswap-interface/src/pages/RemoveLiquidity/ZapOut.tsx b/apps/kyberswap-interface/src/pages/RemoveLiquidity/ZapOut.tsx index b70c101022..47b7f6550d 100644 --- a/apps/kyberswap-interface/src/pages/RemoveLiquidity/ZapOut.tsx +++ b/apps/kyberswap-interface/src/pages/RemoveLiquidity/ZapOut.tsx @@ -167,14 +167,14 @@ export default function ZapOut({ // allowance handling const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null) - const [approval, approveCallback] = useApproveCallback( - parsedAmounts[Field.LIQUIDITY], - isStaticFeePair + const [approval, approveCallback] = useApproveCallback({ + amount: parsedAmounts[Field.LIQUIDITY], + spender: isStaticFeePair ? isOldStaticFeeContract ? networkInfo.classic.oldStatic?.zap : networkInfo.classic.static.zap : networkInfo.classic.dynamic?.zap, - ) + }) // if user liquidity change => remove signature useEffect(() => { @@ -191,7 +191,8 @@ export default function ZapOut({ if (!liquidityAmount) throw new Error('missing liquidity amount') if (isArgentWallet) { - return approveCallback() + await approveCallback() + return } // try to gather a signature for permission @@ -261,7 +262,7 @@ export default function ZapOut({ 8000, ) } else { - approveCallback() + await approveCallback() } } } diff --git a/apps/kyberswap-interface/src/pages/SwapV3/Components/TokenPriceChart.tsx b/apps/kyberswap-interface/src/pages/SwapV3/Components/TokenPriceChart.tsx index 6fb24bb3a5..93429c552d 100644 --- a/apps/kyberswap-interface/src/pages/SwapV3/Components/TokenPriceChart.tsx +++ b/apps/kyberswap-interface/src/pages/SwapV3/Components/TokenPriceChart.tsx @@ -55,9 +55,10 @@ const countRecentTransactions = (candles: TokenChartCandle[], count: number) => type TokenPriceChartProps = { tokens?: Array + flatten?: boolean } -const TokenPriceChart = ({ tokens }: TokenPriceChartProps) => { +const TokenPriceChart = ({ tokens, flatten }: TokenPriceChartProps) => { const theme = useTheme() const upToSmall = useMedia(`(max-width: ${MEDIA_WIDTHS.upToSmall}px)`) const chartHeight = upToSmall ? 280 : 360 @@ -193,7 +194,7 @@ const TokenPriceChart = ({ tokens }: TokenPriceChartProps) => { ) return ( - +
    {filteredTokens.map((token, index) => { @@ -208,11 +209,18 @@ const TokenPriceChart = ({ tokens }: TokenPriceChartProps) => { setIsExpanded(true) }} className={cn( - 'relative flex shrink-0 cursor-pointer items-center gap-1.5 border-0 px-4 py-3 text-sm font-medium', + 'relative flex min-h-11 shrink-0 cursor-pointer items-center gap-1.5 border-0 px-4 py-3 text-sm font-medium', !isLast && 'border-r border-darkBorder', isActive - ? 'bg-primary/15 text-primary shadow-[inset_0_-2px_0_var(--ks-primary)] hover:bg-primary/15 hover:text-primary' - : 'bg-transparent text-subText hover:bg-tableHeader hover:text-text', + ? cn( + 'text-primary hover:text-primary', + !flatten && 'shadow-[inset_0_-2px_0_var(--ks-primary)]', + flatten ? 'bg-transparent hover:bg-transparent' : 'bg-primary/15 hover:bg-primary/15', + ) + : cn( + 'bg-transparent text-subText hover:text-text', + flatten ? 'hover:bg-transparent' : 'hover:bg-tableHeader', + ), )} > diff --git a/apps/kyberswap-interface/src/pages/SwapV3/Header.tsx b/apps/kyberswap-interface/src/pages/SwapV3/Header.tsx index 5a64158424..3c2ceb73c0 100644 --- a/apps/kyberswap-interface/src/pages/SwapV3/Header.tsx +++ b/apps/kyberswap-interface/src/pages/SwapV3/Header.tsx @@ -47,7 +47,7 @@ export default function Header({ Gasless & no slippage - Kyberswap Limit Order execute on-chain automatically when the market reaches your price. - {t`Buy or sell tokens at customized prices`} + {t`Buy or sell tokens at customized prices`} )} {isSwapPage && ( @@ -57,7 +57,7 @@ export default function Header({ An advanced aggregator splits your trade across hundreds of DEXs and liquidity sources for minimal slippage. - {t`Instantly buy or sell tokens at superior prices`} + {t`Instantly buy or sell tokens at superior prices`} )} {isCrossChainPage && ( @@ -66,7 +66,7 @@ export default function Header({ Swap tokens between EVMs, Bitcoin, Solana, and Near chains in one step - no manual bridging. Quotes from multiple providers, best rate picked automatically. - {t`Swap between tokens on different chains`} + {t`Swap between tokens on different chains`} )} diff --git a/apps/kyberswap-interface/src/pages/SwapV3/index.tsx b/apps/kyberswap-interface/src/pages/SwapV3/index.tsx index 6c2bb38912..022a655f21 100644 --- a/apps/kyberswap-interface/src/pages/SwapV3/index.tsx +++ b/apps/kyberswap-interface/src/pages/SwapV3/index.tsx @@ -3,11 +3,11 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import Banner from 'components/Banner' import { FarmingPoolBanner, TrendingPoolBanner } from 'components/EarnBanner' +import LimitOrderForm from 'components/LimitOrder/Form/LimitOrderForm' +import OrderList from 'components/LimitOrder/OrderList' import { HStack, Stack } from 'components/Stack' import { SwitchLocaleLink } from 'components/SwitchLocaleLink' import { TutorialIds } from 'components/Tutorial/TutorialSwap/constant' -import LimitOrder from 'components/swapv2/LimitOrder' -import ListLimitOrder from 'components/swapv2/LimitOrder/ListLimitOrder' import LiquiditySourcesPanel from 'components/swapv2/LiquiditySourcesPanel' import SettingsPanel from 'components/swapv2/SwapSettingsPanel' import useRequiredDegenMode from 'components/swapv2/SwapSettingsPanel/useRequiredDegenMode' @@ -132,7 +132,7 @@ export default function Swap() { {isSwapPage && ( )} {activeTab === TAB.LIQUIDITY_SOURCES && setActiveTab(TAB.SETTINGS)} />} - {activeTab === TAB.LIMIT && } + {activeTab === TAB.LIMIT && } {activeTab === TAB.CROSS_CHAIN && } {activeTab === TAB.CROSS_CHAIN_SOURCES && ( setActiveTab(TAB.SETTINGS)} /> @@ -180,7 +180,7 @@ export default function Swap() { /> )} - {isLimitPage && } + {isLimitPage && } {isCrossChainPage && ( diff --git a/apps/kyberswap-interface/src/services/limitOrder.ts b/apps/kyberswap-interface/src/services/limitOrder.ts index 1e37582c92..cde71121c5 100644 --- a/apps/kyberswap-interface/src/services/limitOrder.ts +++ b/apps/kyberswap-interface/src/services/limitOrder.ts @@ -1,7 +1,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' -import { LimitOrder, LimitOrderFromTokenPair, LimitOrderStatus } from 'components/swapv2/LimitOrder/type' +import { CancelOrderResponse, LimitOrder, LimitOrderFromTokenPair, LimitOrderStatus } from 'components/LimitOrder/types' import { LIMIT_ORDER_API } from 'constants/env' import { RTK_QUERY_TAGS } from 'constants/index' @@ -15,27 +15,126 @@ const mapPath: Partial> = { [LimitOrderStatus.FILLED]: 'filled', } -const transformResponse = (data: any) => data?.data +type ApiEnvelope = { + code: number + message: string + data: T +} + +type LimitOrderConfigResponse = { + latest?: string + features?: Record +} + +type LimitOrderConfig = { + contract: string + features: Record +} + +type ListOrdersResponse = { + orders?: LimitOrder[] + pagination?: { + totalItems?: number + } +} + +type TokenPairOrdersResponse = { + orders?: LimitOrderFromTokenPair[] +} + +type NumberOfInsufficientFundOrdersResponse = { + total?: number +} + +type ActiveMakingAmountResponse = { + activeMakingAmount?: string +} + +type EncodeDataResponse = { + encodedData: string +} + +export type CreateOrderBody = { + chainId: string + makerAsset?: string + takerAsset?: string + maker?: string + makingAmount?: string + takingAmount?: string + expiredAt: number + nativeOutput: boolean + referral?: string + salt: string + signature: string + clientId?: string | null +} + +export type CreateOrderSignatureBody = Omit + +export type CreateOrderSignatureResponse = { + domain: unknown + types: Record + primaryType: string + message: { salt?: string } & Record +} + +export type CreateCancelOrderSignatureResponse = { + domain: unknown + types: Record + primaryType: string + message: Record +} + +export type InsertCancellingOrderBody = { + orderIds?: number[] + nonce?: number + maker: string + chainId: string + txHash: string + contractAddress: string +} + +export type OperatorSignature = { + id: number + chainId: string + operatorSignature: string + operatorSignatureExpiredAt: number +} + +export type FillOrderBody = { + orderId: number + takingAmount: string + thresholdAmount: string + target: string + operatorSignature: string +} + +const transformResponse = (data: ApiEnvelope) => data.data const limitOrderApi = createApi({ reducerPath: 'limitOrderApi', baseQuery: fetchBaseQuery({ baseUrl: '' }), - tagTypes: [RTK_QUERY_TAGS.GET_LIST_ORDERS, RTK_QUERY_TAGS.GET_ORDERS_BY_TOKEN_PAIR], + tagTypes: [ + RTK_QUERY_TAGS.GET_LIMIT_ORDER_LIST, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_BOOK, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_INSUFFICIENT, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_ACTIVE_MAKING_AMOUNT, + ], endpoints: builder => ({ - getLOConfig: builder.query< - { contract: string; features: { [address: string]: { supportDoubleSignature: boolean } } }, - ChainId - >({ + getLOConfig: builder.query({ query: chainId => ({ url: `${LIMIT_ORDER_API_READ}/v1/configs/contract-address`, params: { chainId }, }), - transformResponse: (data: any) => { - const features = data?.data?.features || {} - Object.keys(features).forEach(key => { - features[key.toLowerCase()] = features[key] - }) - return { contract: data?.data?.latest?.toLowerCase?.() ?? '', features } + transformResponse: ({ data }: ApiEnvelope) => { + const features = Object.entries(data.features ?? {}).reduce( + (accumulator, [key, value]) => { + accumulator[key.toLowerCase()] = value + return accumulator + }, + {}, + ) + return { contract: data.latest?.toLowerCase() ?? '', features } }, }), getListOrders: builder.query< @@ -53,13 +152,13 @@ const limitOrderApi = createApi({ url: `${LIMIT_ORDER_API_READ}/v1/orders`, params, }), - transformResponse: ({ data }: any) => { - data.orders.forEach((order: any) => { + transformResponse: ({ data }: ApiEnvelope) => { + data.orders?.forEach(order => { order.chainId = Number(order.chainId) as ChainId }) return { orders: data?.orders || [], totalOrder: data?.pagination?.totalItems || 0 } }, - providesTags: [RTK_QUERY_TAGS.GET_LIST_ORDERS], + providesTags: [RTK_QUERY_TAGS.GET_LIMIT_ORDER_LIST], }), getOrdersByTokenPair: builder.query< { orders: LimitOrderFromTokenPair[] }, @@ -73,46 +172,35 @@ const limitOrderApi = createApi({ url: `${LIMIT_ORDER_API_READ_PARTNER}/v1/orders/allchains`, params, }), - transformResponse: ({ data }: any) => { - data.orders.forEach((order: any) => { + transformResponse: ({ data }: ApiEnvelope) => { + data.orders?.forEach(order => { order.chainId = Number(order.chainId) as ChainId }) return { orders: data?.orders || [] } }, - async onQueryStarted(agr, { dispatch, queryFulfilled }) { - try { - await queryFulfilled - } catch { - dispatch(limitOrderApi.util.upsertQueryData('getOrdersByTokenPair', agr, { orders: [] })) - } - }, - providesTags: [RTK_QUERY_TAGS.GET_ORDERS_BY_TOKEN_PAIR], + providesTags: [RTK_QUERY_TAGS.GET_LIMIT_ORDER_BOOK], }), getNumberOfInsufficientFundOrders: builder.query({ query: params => ({ url: `${LIMIT_ORDER_API_READ}/v1/orders/insufficient-funds`, params, }), - transformResponse: (data: any) => data?.data?.total || 0, + transformResponse: ({ data }: ApiEnvelope) => data.total || 0, + providesTags: [RTK_QUERY_TAGS.GET_LIMIT_ORDER_INSUFFICIENT], }), - insertCancellingOrder: builder.mutation< - any, - { - orderIds?: number[] - nonce?: number - maker: string - chainId: string - txHash: string - contractAddress: string - } - >({ + insertCancellingOrder: builder.mutation({ query: body => ({ url: `${LIMIT_ORDER_API_WRITE}/v1/orders/cancelling`, body, method: 'POST', }), + invalidatesTags: [ + RTK_QUERY_TAGS.GET_LIMIT_ORDER_LIST, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_INSUFFICIENT, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_ACTIVE_MAKING_AMOUNT, + ], }), - createOrder: builder.mutation<{ id: number }, any>({ + createOrder: builder.mutation<{ id: number }, CreateOrderBody>({ query: body => ({ url: `${LIMIT_ORDER_API_WRITE}/v1/orders`, body, @@ -122,9 +210,13 @@ const limitOrderApi = createApi({ }, }), transformResponse, - invalidatesTags: [RTK_QUERY_TAGS.GET_LIST_ORDERS], + invalidatesTags: [ + RTK_QUERY_TAGS.GET_LIMIT_ORDER_LIST, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_INSUFFICIENT, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_ACTIVE_MAKING_AMOUNT, + ], }), - createOrderSignature: builder.mutation({ + createOrderSignature: builder.mutation({ query: body => ({ url: `${LIMIT_ORDER_API_WRITE}/v1/orders/sign-message`, body, @@ -133,7 +225,7 @@ const limitOrderApi = createApi({ transformResponse, }), - getEncodeData: builder.mutation({ + getEncodeData: builder.mutation({ query: ({ orderIds, isCancelAll = false }) => ({ url: `${LIMIT_ORDER_API_READ}/v1/encode/${isCancelAll ? 'increase-nonce' : 'cancel-batch-orders'}`, body: isCancelAll ? {} : { orderIds }, @@ -142,7 +234,7 @@ const limitOrderApi = createApi({ transformResponse, }), ackNotificationOrder: builder.mutation< - any, + unknown, { docIds: string[]; maker: string; chainId: ChainId; type: LimitOrderStatus } >({ query: ({ maker, chainId, type, docIds }) => ({ @@ -161,10 +253,14 @@ const limitOrderApi = createApi({ maker: account, }, }), - transformResponse: (data: any) => data?.data?.activeMakingAmount, + transformResponse: ({ data }: ApiEnvelope) => data.activeMakingAmount || '', + providesTags: [RTK_QUERY_TAGS.GET_LIMIT_ORDER_ACTIVE_MAKING_AMOUNT], }), - createCancelOrderSignature: builder.mutation({ + createCancelOrderSignature: builder.mutation< + CreateCancelOrderSignatureResponse, + { chainId: string; maker: string; orderIds: number[] } + >({ query: body => ({ url: `${LIMIT_ORDER_API_WRITE}/v1/orders/cancel-sign`, body, @@ -172,14 +268,39 @@ const limitOrderApi = createApi({ }), transformResponse, }), - cancelOrders: builder.mutation({ + cancelOrders: builder.mutation< + CancelOrderResponse, + { chainId: string; maker: string; orderIds: number[]; signature: string } + >({ query: body => ({ url: `${LIMIT_ORDER_API_WRITE}/v1/orders/cancel`, body, method: 'POST', }), transformResponse, - invalidatesTags: [RTK_QUERY_TAGS.GET_LIST_ORDERS], + invalidatesTags: [ + RTK_QUERY_TAGS.GET_LIMIT_ORDER_LIST, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_INSUFFICIENT, + RTK_QUERY_TAGS.GET_LIMIT_ORDER_ACTIVE_MAKING_AMOUNT, + ], + }), + getOperatorSignature: builder.query({ + query: ({ chainId, orderIds }) => ({ + url: `${LIMIT_ORDER_API_READ_PARTNER}/v1/orders/operator-signature`, + params: { + chainId: chainId.toString(), + orderIds: orderIds.join(','), + }, + }), + transformResponse: ({ data }: ApiEnvelope<{ orders?: OperatorSignature[] }>) => data.orders || [], + }), + encodeFillOrder: builder.mutation({ + query: body => ({ + url: `${LIMIT_ORDER_API_READ}/v1/encode/fill-order-to`, + body, + method: 'POST', + }), + transformResponse, }), }), }) @@ -197,6 +318,8 @@ export const { useAckNotificationOrderMutation, useCreateCancelOrderSignatureMutation, useCancelOrdersMutation, + useLazyGetOperatorSignatureQuery, + useEncodeFillOrderMutation, } = limitOrderApi export default limitOrderApi diff --git a/apps/kyberswap-interface/src/state/index.ts b/apps/kyberswap-interface/src/state/index.ts index e35cd4ea8e..6c4efdea72 100644 --- a/apps/kyberswap-interface/src/state/index.ts +++ b/apps/kyberswap-interface/src/state/index.ts @@ -39,7 +39,6 @@ import burn from 'state/burn/reducer' import crossChainSwap from 'state/crossChainSwap' import customizeDexes from 'state/customizeDexes' import { updateVersion } from 'state/global/actions' -import limit from 'state/limit/reducer' import lists from 'state/lists/reducer' import mintV2 from 'state/mint/proamm/reducer' import mint from 'state/mint/reducer' @@ -87,7 +86,6 @@ const rootReducer = combineReducers({ transactions, crossChainSwap, swap, - limit, mint, mintV2, burn, diff --git a/apps/kyberswap-interface/src/state/limit/actions.ts b/apps/kyberswap-interface/src/state/limit/actions.ts deleted file mode 100644 index 20af8c78dd..0000000000 --- a/apps/kyberswap-interface/src/state/limit/actions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createAction } from '@reduxjs/toolkit' - -import { CreateOrderParam } from 'components/swapv2/LimitOrder/type' - -export const pushOrderNeedCreated = createAction('limit/pushOrderNeedCreated') -export const removeOrderNeedCreated = createAction('limit/removeOrderNeedCreated') - -export const setOrderEditing = createAction('limit/setOrderEditing') diff --git a/apps/kyberswap-interface/src/state/limit/hooks.ts b/apps/kyberswap-interface/src/state/limit/hooks.ts index 445a15fc4b..918b06dee9 100644 --- a/apps/kyberswap-interface/src/state/limit/hooks.ts +++ b/apps/kyberswap-interface/src/state/limit/hooks.ts @@ -1,20 +1,10 @@ import { Currency } from '@kyberswap/ks-sdk-core' import { useCallback } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { CreateOrderParam } from 'components/swapv2/LimitOrder/type' -import { AppDispatch, AppState } from 'state/index' import { Field } from 'state/swap/actions' import { useInputCurrency, useOutputCurrency, useSwapActionHandlers, useSwapState } from 'state/swap/hooks' -import { - pushOrderNeedCreated as pushOrderNeedCreatedAction, - removeOrderNeedCreated as removeOrderNeedCreatedAction, - setOrderEditing as setOrderEditingAction, -} from './actions' -import { LimitState } from './reducer' - -export function useLimitState(): LimitState & { +export function useLimitState(): { currencyIn: Currency | undefined currencyOut: Currency | undefined inputAmount: string @@ -24,12 +14,10 @@ export function useLimitState(): LimitState & { const currencyOut = useOutputCurrency() const { typedValue: inputAmount } = useSwapState() - const state = useSelector((state: AppState) => state.limit) - return { ...state, currencyIn, currencyOut, inputAmount } + return { currencyIn, currencyOut, inputAmount } } export function useLimitActionHandlers() { - const dispatch = useDispatch() const { onSwitchTokensV2, onCurrencySelection, onUserInput } = useSwapActionHandlers() const setInputValue = useCallback( @@ -53,34 +41,10 @@ export function useLimitActionHandlers() { [onCurrencySelection], ) - const pushOrderNeedCreated = useCallback( - (order: CreateOrderParam) => { - dispatch(pushOrderNeedCreatedAction(order)) - }, - [dispatch], - ) - - const removeOrderNeedCreated = useCallback( - (orderId: number) => { - dispatch(removeOrderNeedCreatedAction(orderId)) - }, - [dispatch], - ) - - const setOrderEditing = useCallback( - (order: CreateOrderParam) => { - dispatch(setOrderEditingAction(order)) - }, - [dispatch], - ) - return { switchCurrency: onSwitchTokensV2, setCurrencyIn, setCurrencyOut, - pushOrderNeedCreated, - removeOrderNeedCreated, - setOrderEditing, setInputValue, } } diff --git a/apps/kyberswap-interface/src/state/limit/reducer.ts b/apps/kyberswap-interface/src/state/limit/reducer.ts deleted file mode 100644 index bdc6409a80..0000000000 --- a/apps/kyberswap-interface/src/state/limit/reducer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createReducer } from '@reduxjs/toolkit' - -import { CreateOrderParam } from 'components/swapv2/LimitOrder/type' - -import { pushOrderNeedCreated, removeOrderNeedCreated, setOrderEditing } from './actions' - -export interface LimitState { - ordersNeedCreated: CreateOrderParam[] - orderEditing: CreateOrderParam | undefined -} - -const initialState: LimitState = { - ordersNeedCreated: [], // orders need to be created when cancel is completed - orderEditing: undefined, // order is editing -} - -export default createReducer(initialState, builder => - builder - .addCase(pushOrderNeedCreated, (state, { payload }) => { - state.ordersNeedCreated = [...state.ordersNeedCreated, payload] - }) - .addCase(removeOrderNeedCreated, (state, { payload: orderId }) => { - state.ordersNeedCreated = state.ordersNeedCreated.filter(e => e.orderId !== orderId) - }) - .addCase(setOrderEditing, (state, { payload: orderEditing }) => { - state.orderEditing = orderEditing - }), -) diff --git a/apps/kyberswap-interface/src/state/transactions/type.ts b/apps/kyberswap-interface/src/state/transactions/type.ts index dd9e5ce0d6..6e28de7c25 100644 --- a/apps/kyberswap-interface/src/state/transactions/type.ts +++ b/apps/kyberswap-interface/src/state/transactions/type.ts @@ -194,6 +194,7 @@ export enum TRANSACTION_TYPE { KYBERDAO_CLAIM = 'KyberDAO Claim Voting Reward', KYBERDAO_CLAIM_GAS_REFUND = 'Gas Refund', + FILL_LIMIT_ORDER = 'Fill Limit Order', CANCEL_LIMIT_ORDER = 'Cancel Limit Order', TRANSFER_TOKEN = 'Send', CLAIM = 'Claim', @@ -248,6 +249,7 @@ export const GROUP_TRANSACTION_BY_TYPE = { TRANSACTION_TYPE.APPROVE, TRANSACTION_TYPE.CLAIM_REWARD, TRANSACTION_TYPE.CLAIM, + TRANSACTION_TYPE.FILL_LIMIT_ORDER, TRANSACTION_TYPE.CANCEL_LIMIT_ORDER, TRANSACTION_TYPE.TRANSFER_TOKEN, ], diff --git a/apps/kyberswap-interface/src/tailwind.css b/apps/kyberswap-interface/src/tailwind.css index f5b4b62f47..2ff12bf32e 100644 --- a/apps/kyberswap-interface/src/tailwind.css +++ b/apps/kyberswap-interface/src/tailwind.css @@ -417,13 +417,42 @@ color: var(--ks-primary); font-weight: bold; } - .ks-date-picker .custom-calendar .react-calendar__decade-view button, - .ks-date-picker .custom-calendar .react-calendar__century-view button, - .ks-date-picker .custom-calendar .react-calendar__year-view button { - padding: 14px 0.5em; + .ks-date-picker .custom-calendar .react-calendar__decade-view button:not(:disabled):hover, + .ks-date-picker .custom-calendar .react-calendar__century-view button:not(:disabled):hover, + .ks-date-picker .custom-calendar .react-calendar__year-view button:not(:disabled):hover { + background-color: var(--ks-buttonGray); + } + .ks-date-picker + .custom-calendar + :is( + .react-calendar__year-view__months, + .react-calendar__decade-view__years, + .react-calendar__century-view__decades + ) { + gap: 4px; + } + .ks-date-picker + .custom-calendar + :is(.react-calendar__decade-view, .react-calendar__century-view, .react-calendar__year-view) + button { + border-radius: 8px; + padding: 10px 0.5em; + font-weight: bold; + } + .ks-date-picker .custom-calendar .react-calendar__year-view__months__month { + flex-basis: calc((100% - 8px) / 3) !important; + max-width: calc((100% - 8px) / 3); + } + .ks-date-picker + .custom-calendar + :is(.react-calendar__decade-view__years__year, .react-calendar__century-view__decades__decade) { + flex-basis: calc((100% - 16px) / 3) !important; + max-width: calc((100% - 16px) / 3); } .ks-date-picker .custom-calendar .react-calendar__navigation { - margin: 0; + display: flex; + gap: 2px; + margin-bottom: 8px; font-weight: 500; height: auto; } @@ -432,9 +461,12 @@ font-size: 14px; color: var(--ks-text); background-color: transparent; + margin-inline: 8px; + border-radius: 8px; } - .ks-date-picker .custom-calendar .react-calendar__navigation__label:hover { - background-color: transparent; + .ks-date-picker .custom-calendar .react-calendar__navigation__label:enabled:hover, + .ks-date-picker .custom-calendar .react-calendar__navigation__label:enabled:focus { + background-color: var(--ks-buttonBlack); } .ks-date-picker .custom-calendar .react-calendar__navigation__arrow { font-size: 20px; @@ -447,9 +479,16 @@ line-height: 1; min-width: unset; } + .ks-date-picker .custom-calendar .react-calendar__navigation__arrow:enabled:hover, + .ks-date-picker .custom-calendar .react-calendar__navigation__arrow:enabled:focus { + background-color: var(--ks-buttonBlack); + } .ks-date-picker .custom-calendar .react-calendar__navigation__arrow:disabled { background-color: rgba(28, 28, 28, 0.4); } + .ks-date-picker .custom-calendar .react-calendar__viewContainer { + min-height: 232px; + } .ks-date-picker .custom-calendar .react-calendar__month-view__days button { height: 34px; padding: 0; @@ -477,6 +516,7 @@ .react-calendar__month-view__days button.react-calendar__month-view__days__day--neighboringMonth { font-style: italic; + color: var(--ks-subText); } @media (max-width: 992px) { .ks-date-picker .custom-date-picker .custom-calendar .react-calendar__month-view__days button { @@ -809,19 +849,6 @@ } } - /* LimitOrder ListOrder — primary box-shadow pulse on highlighted row. */ - @keyframes ks-order-highlight { - 0% { - box-shadow: 0 0 0 0 var(--ks-primary); - } - 70% { - box-shadow: 0 0 0 2px var(--ks-primary); - } - 100% { - box-shadow: 0 0 0 0 var(--ks-primary); - } - } - /* OptionButton (KyberDAO Vote) — striped overlay animation for "Choosing" state. */ @keyframes ks-stripe-move { 0% { diff --git a/apps/kyberswap-interface/src/theme/components.tsx b/apps/kyberswap-interface/src/theme/components.tsx index bcbea53292..146f87c3e7 100644 --- a/apps/kyberswap-interface/src/theme/components.tsx +++ b/apps/kyberswap-interface/src/theme/components.tsx @@ -32,7 +32,7 @@ export function ButtonText({ } export function CloseIcon({ className, ...rest }: IconProps) { - return + return } export function LinkIcon({ color, className, style, ...rest }: IconProps & { color?: string }) { diff --git a/apps/kyberswap-interface/src/utils/firebase.ts b/apps/kyberswap-interface/src/utils/firebase.ts index 9c207412d2..af3e882399 100644 --- a/apps/kyberswap-interface/src/utils/firebase.ts +++ b/apps/kyberswap-interface/src/utils/firebase.ts @@ -2,7 +2,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core' import type { Firestore, collection, doc, onSnapshot, query } from 'firebase/firestore' import { PopupContentAnnouncement } from 'components/Announcement/type' -import { LimitOrder } from 'components/swapv2/LimitOrder/type' +import { LimitOrder } from 'components/LimitOrder/types' import { ENV_KEY, ENV_LEVEL, FIREBASE } from 'constants/env' import { ENV_TYPE } from 'constants/type' diff --git a/apps/kyberswap-interface/src/utils/prefetch.ts b/apps/kyberswap-interface/src/utils/prefetch.ts index 9eaf22d532..10842e41ea 100644 --- a/apps/kyberswap-interface/src/utils/prefetch.ts +++ b/apps/kyberswap-interface/src/utils/prefetch.ts @@ -2,7 +2,7 @@ import { ChainId } from '@kyberswap/ks-sdk-core' import limitOrderApi from 'services/limitOrder' import zapEarnServiceApi from 'services/zapEarn' -import { getInitialListOrdersArgs } from 'components/swapv2/LimitOrder/listOrdersArgs' +import { getInitialListOrdersArgs } from 'components/LimitOrder/listOrdersArgs' import { APP_PATHS } from 'constants/index' import { isSupportedChainId } from 'constants/networks' import { getInitialPositionQueryParams } from 'pages/Earns/UserPositions/positionsQuery' diff --git a/apps/kyberswap-interface/src/utils/useEstimateGasTxs.ts b/apps/kyberswap-interface/src/utils/useEstimateGasTxs.ts index ff6e64d98c..c214fcee47 100644 --- a/apps/kyberswap-interface/src/utils/useEstimateGasTxs.ts +++ b/apps/kyberswap-interface/src/utils/useEstimateGasTxs.ts @@ -8,7 +8,12 @@ import { useTokenPrices } from 'state/tokenPrices/hooks' import { createAccessListIfEnabled } from 'utils/accessList' import { Address, Hex, PublicClient, formatEther } from 'utils/viem' -type EstimateParams = { contractAddress: string; encodedData: string; value?: bigint } +type EstimateParams = { + contractAddress: string + encodedData: string + value?: bigint +} + function useEstimateGasTxs(): (v: EstimateParams) => Promise<{ gas: bigint | null; gasInUsd: number | null }> { const { account, chainId } = useActiveWeb3React() @@ -56,4 +61,5 @@ function useEstimateGasTxs(): (v: EstimateParams) => Promise<{ gas: bigint | nul [account, chainId, usdPriceNative], ) } + export default useEstimateGasTxs