diff --git a/packages/caip/src/adapters/banxa/index.ts b/packages/caip/src/adapters/banxa/index.ts index cb38dbc3874..af7cb707e57 100644 --- a/packages/caip/src/adapters/banxa/index.ts +++ b/packages/caip/src/adapters/banxa/index.ts @@ -1,5 +1,4 @@ import entries from 'lodash/entries' -import toLower from 'lodash/toLower' import type { AssetId } from '../../assetId/assetId' import type { ChainId } from '../../chainId/chainId' @@ -24,6 +23,7 @@ import { optimismChainId, polygonAssetId, polygonChainId, + solanaChainId, solAssetId, thorchainAssetId, thorchainChainId, @@ -106,8 +106,8 @@ export const getSupportedBanxaAssets = () => ticker, })) -export const assetIdToBanxaTicker = (assetId: string): string | undefined => - AssetIdToBanxaTickerMap[toLower(assetId)] +export const assetIdToBanxaTicker = (assetId: AssetId): string | undefined => + AssetIdToBanxaTickerMap[assetId] /** * map ChainIds to Banxa blockchain codes (ETH, BTC, COSMOS), @@ -129,7 +129,7 @@ const chainIdToBanxaBlockchainCodeMap: Record = { [arbitrumChainId]: 'ARB', [baseChainId]: 'BASE', [thorchainChainId]: 'THORCHAIN', - [solAssetId]: 'SOL', + [solanaChainId]: 'SOL', } as const /** diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index f2738e2bbfc..4a5c906ea54 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -1021,7 +1021,15 @@ "previewOrder": "Preview Order", "youGet": "You Get", "whenPriceReaches": "When price reaches", - "expiry": "Expiry" + "expiry": "Expiry", + "expiryOption": { + "oneHour": "1 hour", + "oneDay": "1 day", + "threeDays": "3 days", + "sevenDays": "7 days", + "twentyEightDays": "28 days", + "custom": "Custom" + } }, "modals": { "assetSearch": { diff --git a/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx b/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx index a69a65504b0..d5eed145f12 100644 --- a/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx +++ b/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx @@ -50,6 +50,7 @@ export const TradeAssetSearchModalBase: FC = ({ ) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx index bcdf42e3eb6..619fac02dfb 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfig.tsx @@ -12,18 +12,71 @@ import { } from '@chakra-ui/react' import type { Asset } from '@shapeshiftoss/types' import { bn, bnOrZero } from '@shapeshiftoss/utils' -import { noop } from 'lodash' -import { useCallback, useMemo, useState } from 'react' -import { Amount } from 'components/Amount/Amount' +import { useCallback, useMemo, useRef, useState } from 'react' +import type { NumberFormatValues } from 'react-number-format' +import NumberFormat from 'react-number-format' import { StyledAssetMenuButton } from 'components/AssetSelection/components/AssetMenuButton' import { SwapIcon } from 'components/Icons/SwapIcon' -import { RawText, Text } from 'components/Text' +import { Text } from 'components/Text' +import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' +import { assertUnreachable } from 'lib/utils' +import { allowedDecimalSeparators } from 'state/slices/preferencesSlice/preferencesSlice' -const EXPIRY_TIME_PERIODS = ['1 hour', '1 day', '3 days', '7 days', '28 days'] as const +import { AmountInput } from '../../TradeAmountInput' + +enum PriceDirection { + Default = 'default', + Reversed = 'reversed', +} + +enum PresetLimit { + Market = 'market', + OnePercent = 'onePercent', + TwoPercent = 'twoPercent', + FivePercent = 'fivePercent', + TenPercent = 'tenPercent', +} + +enum ExpiryOption { + OneHour = 'oneHour', + OneDay = 'oneDay', + ThreeDays = 'threeDays', + SevenDays = 'sevenDays', + TwentyEightDays = 'twentyEightDays', + // TODO: implement custom expiry + // Custom = 'custom', +} + +const EXPIRY_OPTIONS = [ + ExpiryOption.OneHour, + ExpiryOption.OneDay, + ExpiryOption.ThreeDays, + ExpiryOption.SevenDays, + ExpiryOption.TwentyEightDays, +] as const + +const getExpiryOptionTranslation = (expiryOption: ExpiryOption) => { + switch (expiryOption) { + case ExpiryOption.OneHour: + return `limitOrder.expiryOption.${expiryOption}` + case ExpiryOption.OneDay: + return `limitOrder.expiryOption.${expiryOption}` + case ExpiryOption.ThreeDays: + return `limitOrder.expiryOption.${expiryOption}` + case ExpiryOption.SevenDays: + return `limitOrder.expiryOption.${expiryOption}` + case ExpiryOption.TwentyEightDays: + return `limitOrder.expiryOption.${expiryOption}` + // TODO: implement custom expiry + // case ExpiryOption.Custom: + // return `limitOrder.expiryOption.${expiryOption}` + default: + assertUnreachable(expiryOption) + } +} const timePeriodRightIcon = const swapIcon = - const swapPriceButtonProps = { pr: 4 } type LimitOrderConfigProps = { @@ -34,19 +87,6 @@ type LimitOrderConfigProps = { setLimitPriceBuyAssetCryptoPrecision: (priceBuyAssetCryptoPrecision: string) => void } -enum PriceDirection { - Sell = 'sell', - Buy = 'buy', -} - -enum PresetLimit { - Market = 'market', - OnePercent = 'onePercent', - TwoPercent = 'twoPercent', - FivePercent = 'fivePercent', - TenPercent = 'tenPercent', -} - export const LimitOrderConfig = ({ sellAsset, buyAsset, @@ -54,71 +94,143 @@ export const LimitOrderConfig = ({ limitPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision, }: LimitOrderConfigProps) => { - const [priceDirection, setPriceDirection] = useState(PriceDirection.Sell) - const [presetLimit, setPresetLimit] = useState(PresetLimit.Market) + const priceAmountRef = useRef(null) - const renderedChains = useMemo(() => { - return EXPIRY_TIME_PERIODS.map(timePeriod => { + const [priceDirection, setPriceDirection] = useState(PriceDirection.Default) + const [presetLimit, setPresetLimit] = useState(PresetLimit.Market) + const [expiryOption, setExpiryOption] = useState(ExpiryOption.SevenDays) + + const { + number: { localeParts }, + } = useLocaleFormatter() + + const expiryOptions = useMemo(() => { + return EXPIRY_OPTIONS.map(expiryOption => { return ( - - {timePeriod} + + ) }) }, []) const priceAsset = useMemo(() => { - return priceDirection === PriceDirection.Sell ? sellAsset : buyAsset + return priceDirection === PriceDirection.Default ? buyAsset : sellAsset }, [buyAsset, priceDirection, sellAsset]) - const priceCryptoPrecision = useMemo(() => { - if (bnOrZero(limitPriceBuyAssetCryptoPrecision).isZero()) { - return '0' - } - - return priceDirection === PriceDirection.Sell - ? bn(1).div(limitPriceBuyAssetCryptoPrecision).toFixed() - : limitPriceBuyAssetCryptoPrecision - }, [limitPriceBuyAssetCryptoPrecision, priceDirection]) + // Lower the decimal places when the integer is greater than 8 significant digits for better UI + const priceCryptoFormatted = useMemo(() => { + const cryptoAmountIntegerCount = bnOrZero( + bnOrZero(limitPriceBuyAssetCryptoPrecision).toFixed(0), + ).precision(true) - const handleTogglePriceDirection = useCallback(() => { - setPriceDirection( - priceDirection === PriceDirection.Sell ? PriceDirection.Buy : PriceDirection.Sell, - ) - }, [priceDirection]) + return cryptoAmountIntegerCount <= 8 + ? limitPriceBuyAssetCryptoPrecision + : bnOrZero(limitPriceBuyAssetCryptoPrecision).toFixed(3) + }, [limitPriceBuyAssetCryptoPrecision]) const arrow = useMemo(() => { - return priceDirection === PriceDirection.Sell ? '↑' : '↓' + return priceDirection === PriceDirection.Default ? '↑' : '↓' }, [priceDirection]) + const handleSetPresetLimit = useCallback( + (presetLimit: PresetLimit, priceDirection: PriceDirection) => { + setPresetLimit(presetLimit) + const multiplier = (() => { + switch (presetLimit) { + case PresetLimit.Market: + return '1.00' + case PresetLimit.OnePercent: + return '1.01' + case PresetLimit.TwoPercent: + return '1.02' + case PresetLimit.FivePercent: + return '1.05' + case PresetLimit.TenPercent: + return '1.10' + default: + assertUnreachable(presetLimit) + } + })() + const adjustedLimitPrice = bn(marketPriceBuyAssetCryptoPrecision).times(multiplier).toFixed() + const maybeReversedPrice = + priceDirection === PriceDirection.Reversed + ? bn(1).div(adjustedLimitPrice).toFixed() + : adjustedLimitPrice + setLimitPriceBuyAssetCryptoPrecision(maybeReversedPrice) + }, + [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision], + ) + const handleSetMarketLimit = useCallback(() => { - setPresetLimit(PresetLimit.Market) - setLimitPriceBuyAssetCryptoPrecision(marketPriceBuyAssetCryptoPrecision) - }, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision]) + handleSetPresetLimit(PresetLimit.Market, priceDirection) + }, [handleSetPresetLimit, priceDirection]) const handleSetOnePercentLimit = useCallback(() => { - setPresetLimit(PresetLimit.OnePercent) - const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.01').toFixed() - setLimitPriceBuyAssetCryptoPrecision(price) - }, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision]) + handleSetPresetLimit(PresetLimit.OnePercent, priceDirection) + }, [handleSetPresetLimit, priceDirection]) const handleSetTwoPercentLimit = useCallback(() => { - setPresetLimit(PresetLimit.TwoPercent) - const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.02').toFixed() - setLimitPriceBuyAssetCryptoPrecision(price) - }, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision]) + handleSetPresetLimit(PresetLimit.TwoPercent, priceDirection) + }, [handleSetPresetLimit, priceDirection]) const handleSetFivePercentLimit = useCallback(() => { - setPresetLimit(PresetLimit.FivePercent) - const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.05').toFixed() - setLimitPriceBuyAssetCryptoPrecision(price) - }, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision]) + handleSetPresetLimit(PresetLimit.FivePercent, priceDirection) + }, [handleSetPresetLimit, priceDirection]) const handleSetTenPercentLimit = useCallback(() => { - setPresetLimit(PresetLimit.TenPercent) - const price = bn(marketPriceBuyAssetCryptoPrecision).div('1.10').toFixed() - setLimitPriceBuyAssetCryptoPrecision(price) - }, [marketPriceBuyAssetCryptoPrecision, setLimitPriceBuyAssetCryptoPrecision]) + handleSetPresetLimit(PresetLimit.TenPercent, priceDirection) + }, [handleSetPresetLimit, priceDirection]) + + const handleTogglePriceDirection = useCallback(() => { + const newPriceDirection = + priceDirection === PriceDirection.Default ? PriceDirection.Reversed : PriceDirection.Default + setPriceDirection(newPriceDirection) + + const isCustomLimit = presetLimit === undefined + + if (isCustomLimit) { + // For custom limit, just take the reciprocal as we don't know what the original input value was + setLimitPriceBuyAssetCryptoPrecision(bn(1).div(limitPriceBuyAssetCryptoPrecision).toFixed()) + } else { + // Otherwise set it to the precise value based on the original market price + handleSetPresetLimit(presetLimit, newPriceDirection) + } + }, [ + handleSetPresetLimit, + limitPriceBuyAssetCryptoPrecision, + presetLimit, + priceDirection, + setLimitPriceBuyAssetCryptoPrecision, + ]) + + const handlePriceChange = useCallback(() => { + // onChange will send us the formatted value + // To get around this we need to get the value from the onChange using a ref + // Now when the max buttons are clicked the onChange will not fire + setLimitPriceBuyAssetCryptoPrecision(priceAmountRef.current ?? '0') + + // Unset the preset limit, as this is a custom value + setPresetLimit(undefined) + }, [setLimitPriceBuyAssetCryptoPrecision]) + + const handleValueChange = useCallback( + (values: NumberFormatValues) => { + // This fires anytime value changes including setting it on max click + // Store the value in a ref to send when we actually want the onChange to fire + priceAmountRef.current = values.value + setLimitPriceBuyAssetCryptoPrecision(values.value) + }, + [setLimitPriceBuyAssetCryptoPrecision], + ) + + const expiryOptionTranslation = useMemo(() => { + return getExpiryOptionTranslation(expiryOption) + }, [expiryOption]) + + const handleChangeExpiryOption = useCallback((newExpiry: string | string[]) => { + setExpiryOption(newExpiry as ExpiryOption) + }, []) return ( @@ -128,18 +240,33 @@ export const LimitOrderConfig = ({ - 1 hour + - - {renderedChains} + + {expiryOptions} - + + selectFirstAccountIdByChainId(state, sellAsset.chainId), + ) + + const [buyAssetAccountId, setBuyAssetAccountId] = useState(defaultAccountId) + const [sellAssetAccountId, setSellAssetAccountId] = useState(defaultAccountId) const [isInputtingFiatSellAmount, setIsInputtingFiatSellAmount] = useState(false) const [isConfirmationLoading, setIsConfirmationLoading] = useState(false) @@ -73,8 +85,6 @@ export const LimitOrderInput = ({ const isTradeQuoteRequestAborted = useAppSelector(selectIsTradeQuoteRequestAborted) const hasUserEnteredAmount = true const votingPower = useAppSelector(state => selectVotingPower(state, votingPowerParams)) - const sellAsset = ethereum // TODO: Implement me - const buyAsset = fox // TODO: Implement me const activeQuote = useAppSelector(selectActiveQuote) const isAnyAccountMetadataLoadedForChainIdFilter = useMemo( () => ({ chainId: sellAsset.chainId }), @@ -108,10 +118,13 @@ export const LimitOrderInput = ({ ], ) + const assetMarketDataUserCurrency = useAppSelector(state => + selectMarketDataByAssetIdUserCurrency(state, sellAsset.assetId), + ) + const sellAmountUserCurrency = useMemo(() => { - // TODO: Implement me - return '0' - }, []) + return bnOrZero(sellAmountCryptoPrecision).times(assetMarketDataUserCurrency.price).toFixed() + }, [assetMarketDataUserCurrency.price, sellAmountCryptoPrecision]) const warningAcknowledgementMessage = useMemo(() => { // TODO: Implement me @@ -123,15 +136,33 @@ export const LimitOrderInput = ({ return <> }, []) - const setBuyAsset = useCallback((_asset: Asset) => { - // TODO: Implement me - }, []) - const setSellAsset = useCallback((_asset: Asset) => { - // TODO: Implement me - }, []) const handleSwitchAssets = useCallback(() => { - // TODO: Implement me - }, []) + setSellAsset(buyAsset) + setBuyAsset(sellAsset) + setSellAmountCryptoPrecision('0') + }, [buyAsset, sellAsset]) + + const handleSetSellAsset = useCallback( + (newSellAsset: Asset) => { + if (newSellAsset === sellAsset) { + handleSwitchAssets() + return + } + setSellAsset(newSellAsset) + }, + [handleSwitchAssets, sellAsset], + ) + + const handleSetBuyAsset = useCallback( + (newBuyAsset: Asset) => { + if (newBuyAsset === buyAsset) { + handleSwitchAssets() + return + } + setBuyAsset(newBuyAsset) + }, + [buyAsset, handleSwitchAssets], + ) const handleConnect = useCallback(() => { walletDispatch({ type: WalletActions.SET_WALLET_MODAL, payload: true }) @@ -186,7 +217,7 @@ export const LimitOrderInput = ({ handleSwitchAssets={handleSwitchAssets} onChangeIsInputtingFiatSellAmount={setIsInputtingFiatSellAmount} onChangeSellAmountCryptoPrecision={setSellAmountCryptoPrecision} - setSellAsset={setSellAsset} + setSellAsset={handleSetSellAsset} setSellAssetAccountId={setSellAssetAccountId} > @@ -195,7 +226,7 @@ export const LimitOrderInput = ({ accountId={buyAssetAccountId} isInputtingFiatSellAmount={isInputtingFiatSellAmount} onAccountIdChange={setBuyAssetAccountId} - onSetBuyAsset={setBuyAsset} + onSetBuyAsset={handleSetBuyAsset} /> { +export const AmountInput = (props: InputProps) => { const translate = useTranslate() return ( void formProps?: BoxProps allowWalletUnsupportedAssets?: boolean + isSwapper?: boolean } export const TradeAssetSearch: FC = ({ onAssetClick, formProps, allowWalletUnsupportedAssets, + isSwapper, }) => { const { walletInfo } = useWallet().state const hasWallet = useMemo(() => Boolean(walletInfo?.deviceId), [walletInfo?.deviceId]) @@ -104,7 +106,9 @@ export const TradeAssetSearch: FC = ({ const handleSubmit = useCallback((e: FormEvent) => e.preventDefault(), []) const popularAssets = useMemo(() => { - const unfilteredPopularAssets = popularAssetsByChainId?.[activeChainId] ?? [] + const unfilteredPopularAssets = (popularAssetsByChainId?.[activeChainId] ?? []).filter(asset => + isSwapper ? asset.chainId !== KnownChainIds.SolanaMainnet : true, + ) if (allowWalletUnsupportedAssets || !hasWallet) return unfilteredPopularAssets return unfilteredPopularAssets.filter(asset => walletConnectedChainIds.includes(asset.chainId)) }, [ @@ -113,6 +117,7 @@ export const TradeAssetSearch: FC = ({ allowWalletUnsupportedAssets, hasWallet, walletConnectedChainIds, + isSwapper, ]) const quickAccessAssets = useMemo(() => { @@ -140,11 +145,13 @@ export const TradeAssetSearch: FC = ({ const portfolioAssetsSortedByBalanceForChain = useMemo(() => { if (activeChainId === 'All') { - return portfolioAssetsSortedByBalance + return portfolioAssetsSortedByBalance.filter(asset => + isSwapper ? asset.chainId !== KnownChainIds.SolanaMainnet : true, + ) } return portfolioAssetsSortedByBalance.filter(asset => asset.chainId === activeChainId) - }, [activeChainId, portfolioAssetsSortedByBalance]) + }, [activeChainId, portfolioAssetsSortedByBalance, isSwapper]) const chainIds: (ChainId | 'All')[] = useMemo(() => { const unsortedChainIds = (() => { @@ -155,10 +162,12 @@ export const TradeAssetSearch: FC = ({ return walletConnectedChainIds })() - const sortedChainIds = sortChainIdsByDisplayName(unsortedChainIds) + const sortedChainIds = sortChainIdsByDisplayName(unsortedChainIds).filter(chainId => + isSwapper ? chainId !== KnownChainIds.SolanaMainnet : true, + ) return ['All', ...sortedChainIds] - }, [allowWalletUnsupportedAssets, hasWallet, walletConnectedChainIds]) + }, [allowWalletUnsupportedAssets, hasWallet, walletConnectedChainIds, isSwapper]) const quickAccessAssetButtons = useMemo(() => { if (isPopularAssetIdsLoading) { @@ -237,6 +246,7 @@ export const TradeAssetSearch: FC = ({ onAssetClick={handleAssetClick} onImportClick={handleImportIntent} isLoading={isPopularAssetIdsLoading} + isSwapper={isSwapper} allowWalletUnsupportedAssets={!hasWallet || allowWalletUnsupportedAssets} /> ) : ( diff --git a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx index d2e78940a2c..438d1b7195f 100644 --- a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx +++ b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx @@ -1,6 +1,6 @@ import { ASSET_NAMESPACE, bscChainId, type ChainId, toAssetId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' -import type { Asset } from '@shapeshiftoss/types' +import { type Asset, KnownChainIds } from '@shapeshiftoss/types' import { bnOrZero, makeAsset, type MinimalAsset } from '@shapeshiftoss/utils' import { orderBy } from 'lodash' import { useMemo } from 'react' @@ -23,6 +23,7 @@ export type SearchTermAssetListProps = { activeChainId: ChainId | 'All' searchString: string allowWalletUnsupportedAssets: boolean | undefined + isSwapper?: boolean onAssetClick: (asset: Asset) => void onImportClick: (asset: Asset) => void } @@ -32,6 +33,7 @@ export const SearchTermAssetList = ({ activeChainId, searchString, allowWalletUnsupportedAssets, + isSwapper, onAssetClick: handleAssetClick, onImportClick, }: SearchTermAssetListProps) => { @@ -56,15 +58,18 @@ export const SearchTermAssetList = ({ const assetsForChain = useMemo(() => { if (activeChainId === 'All') { - if (allowWalletUnsupportedAssets) return assets - return assets.filter(asset => walletConnectedChainIds.includes(asset.chainId)) + const _assets = assets.filter(asset => + isSwapper ? asset.chainId !== KnownChainIds.SolanaMainnet : true, + ) + if (allowWalletUnsupportedAssets) return _assets + return _assets.filter(asset => walletConnectedChainIds.includes(asset.chainId)) } // Should never happen, but paranoia. if (!allowWalletUnsupportedAssets && !walletConnectedChainIds.includes(activeChainId)) return [] return assets.filter(asset => asset.chainId === activeChainId) - }, [activeChainId, allowWalletUnsupportedAssets, assets, walletConnectedChainIds]) + }, [activeChainId, allowWalletUnsupportedAssets, assets, walletConnectedChainIds, isSwapper]) const customAssets: Asset[] = useMemo( () =>