diff --git a/components/MarketNavItem.tsx b/components/MarketNavItem.tsx index 827502a1..ad58bab7 100644 --- a/components/MarketNavItem.tsx +++ b/components/MarketNavItem.tsx @@ -91,7 +91,7 @@ const MarketNavItem: FunctionComponent = ({ ) : null} -
+
diff --git a/components/MarketsTable.tsx b/components/MarketsTable.tsx index 8303e501..635b2874 100644 --- a/components/MarketsTable.tsx +++ b/components/MarketsTable.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useMemo } from 'react' import Link from 'next/link' import { formatUsdValue, perpContractPrecision, usdFormatter } from '../utils' import { Table, Td, Th, TrBody, TrHead } from './TableElements' @@ -6,18 +6,29 @@ import { useViewport } from '../hooks/useViewport' import { breakpoints } from './TradePageGrid' import { useTranslation } from 'next-i18next' import useMangoStore from '../stores/useMangoStore' -import MobileTableHeader from './mobile/MobileTableHeader' -import { ExpandableRow } from './TableElements' import { FavoriteMarketButton } from './TradeNavMenu' import { useSortableData } from '../hooks/useSortableData' import { LinkButton } from './Button' import { ArrowSmDownIcon } from '@heroicons/react/solid' +import { useRouter } from 'next/router' +import { AreaChart, Area, XAxis, YAxis } from 'recharts' +import { InformationCircleIcon } from '@heroicons/react/outline' +import Tooltip from './Tooltip' const MarketsTable = ({ isPerpMarket }) => { const { t } = useTranslation('common') const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false const marketsInfo = useMangoStore((s) => s.marketsInfo) + const actions = useMangoStore((s) => s.actions) + const coingeckoPrices = useMangoStore((s) => s.coingeckoPrices) + const router = useRouter() + + useEffect(() => { + if (coingeckoPrices.length === 0) { + actions.fetchCoingeckoPrices() + } + }, [coingeckoPrices]) const perpMarketsInfo = useMemo( () => @@ -49,7 +60,9 @@ const MarketsTable = ({ isPerpMarket }) => { className="flex items-center font-normal no-underline" onClick={() => requestSort('name')} > - {t('market')} + + {t('market')} + { className="flex items-center font-normal no-underline" onClick={() => requestSort('last')} > - {t('price')} + + {t('price')} + { className="flex items-center font-normal no-underline" onClick={() => requestSort('change24h')} > - + {t('rolling-change')} { className="flex items-center font-normal no-underline" onClick={() => requestSort('volumeUsd24h')} > - + {t('daily-volume')} { className="flex items-center font-normal no-underline" onClick={() => requestSort('funding1h')} > - + {t('average-funding')} { className="flex items-center no-underline" onClick={() => requestSort('openInterestUsd')} > - + {t('open-interest')} { const fundingApr = funding1h ? (funding1h * 24 * 365).toFixed(2) : '-' - + const coingeckoData = coingeckoPrices.find( + (asset) => asset.symbol === baseSymbol + ) + const chartData = coingeckoData ? coingeckoData.prices : undefined return ( @@ -197,12 +215,36 @@ const MarketsTable = ({ isPerpMarket }) => { - - {last ? ( - formatUsdValue(last) - ) : ( - Unavailable - )} + +
+ {last ? ( + formatUsdValue(last) + ) : ( + {t('unavailable')} + )} +
+
+ {chartData !== undefined ? ( + + + + + + ) : ( + t('unavailable') + )} +
{ {change24h || change24h === 0 ? ( `${(change24h * 100).toFixed(2)}%` ) : ( - Unavailable + {t('unavailable')} )} @@ -219,7 +261,7 @@ const MarketsTable = ({ isPerpMarket }) => { {volumeUsd24h ? ( usdFormatter(volumeUsd24h, 0) ) : ( - Unavailable + {t('unavailable')} )} {isPerpMarket ? ( @@ -231,7 +273,9 @@ const MarketsTable = ({ isPerpMarket }) => { {`(${fundingApr}% APR)`} ) : ( - Unavailable + + {t('unavailable')} + )} @@ -249,7 +293,9 @@ const MarketsTable = ({ isPerpMarket }) => { ) : null} ) : ( - Unavailable + + {t('unavailable')} + )} @@ -265,128 +311,89 @@ const MarketsTable = ({ isPerpMarket }) => { ) : ( -
- - {items.map((market, index) => { - const { - baseSymbol, - change24h, - funding1h, - high24h, - last, - low24h, - name, - openInterest, - volumeUsd24h, - } = market - const fundingApr = funding1h ? (funding1h * 24 * 365).toFixed(2) : '-' + items.map((market) => { + const { baseSymbol, change24h, funding1h, last, name } = market + const fundingApr = funding1h ? (funding1h * 24 * 365).toFixed(2) : '-' + const coingeckoData = coingeckoPrices.find( + (asset) => asset.symbol === baseSymbol + ) + const chartData = coingeckoData ? coingeckoData.prices : undefined + return ( + + ) + }) ) ) : null } -export default MarketsTable +export default MarketsTable as any diff --git a/components/Orderbook.tsx b/components/Orderbook.tsx index 965836eb..705511e1 100644 --- a/components/Orderbook.tsx +++ b/components/Orderbook.tsx @@ -523,27 +523,18 @@ export default function Orderbook({ depth = 8 }) {
- { + setDisplayCumulativeSize(!displayCumulativeSize) + }} + className="flex h-8 w-8 items-center justify-center rounded-full bg-th-bkg-3 hover:text-th-primary focus:outline-none" > - - + {displayCumulativeSize ? ( + + ) : ( + + )} +
{ leaveTo="opacity-0" > -
+
onSearch(e.target.value)} prefix={} @@ -134,7 +134,7 @@ const SwitchMarketDropdown = () => {

{t('futures')}

-

+

{t('favorite')}

@@ -148,7 +148,7 @@ const SwitchMarketDropdown = () => { ))}

{t('spot')}

-

+

{t('favorite')}

diff --git a/components/mobile/BottomBar.tsx b/components/mobile/BottomBar.tsx index 4197228e..3feb8dfd 100644 --- a/components/mobile/BottomBar.tsx +++ b/components/mobile/BottomBar.tsx @@ -36,12 +36,12 @@ const BottomBar = () => {
diff --git a/components/mobile/MobileTradePage.tsx b/components/mobile/MobileTradePage.tsx index c040c8ed..77eb6dbd 100644 --- a/components/mobile/MobileTradePage.tsx +++ b/components/mobile/MobileTradePage.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react' import { Disclosure } from '@headlessui/react' import dynamic from 'next/dynamic' -import { SwitchHorizontalIcon, XIcon } from '@heroicons/react/outline' +import { XIcon } from '@heroicons/react/outline' import useMangoStore from '../../stores/useMangoStore' import { getWeights, PerpMarket } from '@blockworks-foundation/mango-client' import { CandlesIcon } from '../icons' @@ -16,8 +16,8 @@ import RecentMarketTrades from '../RecentMarketTrades' import FloatingElement from '../FloatingElement' import Swipeable from './Swipeable' import { useTranslation } from 'next-i18next' -import Link from 'next/link' import { useWallet } from '@solana/wallet-adapter-react' +import SwitchMarketDropdown from 'components/SwitchMarketDropdown' const TVChartContainer = dynamic( () => import('../../components/TradingView/index'), @@ -31,9 +31,6 @@ const MobileTradePage = () => { const selectedMarket = useMangoStore((s) => s.selectedMarket.current) const marketConfig = useMangoStore((s) => s.selectedMarket.config) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) - const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) - const baseSymbol = marketConfig.baseSymbol - const isPerpMarket = marketConfig.kind === 'perp' const handleChangeViewIndex = (index) => { setViewIndex(index) @@ -57,30 +54,10 @@ const MobileTradePage = () => {
- -
-
{baseSymbol}
- - {isPerpMarket ? '-' : '/'} - -
- {isPerpMarket ? 'PERP' : groupConfig.quoteSymbol} -
-
+ {initLeverage}x - -
- -
-
{({ open }) => ( diff --git a/components/trade_form/AdvancedTradeForm.tsx b/components/trade_form/AdvancedTradeForm.tsx index b97e51b0..abfb8b5b 100644 --- a/components/trade_form/AdvancedTradeForm.tsx +++ b/components/trade_form/AdvancedTradeForm.tsx @@ -15,11 +15,7 @@ import { InformationCircleIcon, } from '@heroicons/react/outline' import { notify } from '../../utils/notifications' -import { - calculateTradePrice, - getDecimalCount, - percentFormat, -} from '../../utils' +import { calculateTradePrice, getDecimalCount } from '../../utils' import { floorToDecimal } from '../../utils/index' import useMangoStore, { Orderbook } from '../../stores/useMangoStore' import Button, { LinkButton } from '../Button' @@ -79,7 +75,7 @@ export default function AdvancedTradeForm({ const [spotMargin, setSpotMargin] = useState(defaultSpotMargin) const [positionSizePercent, setPositionSizePercent] = useState('') const [insufficientSol, setInsufficientSol] = useState(false) - const { takerFee, makerFee } = useFees() + const { takerFee } = useFees() const { totalMsrm } = useSrmAccount() const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) @@ -947,7 +943,7 @@ export default function AdvancedTradeForm({
) : null ) : null} -
+
{isLimitOrder ? (
@@ -989,42 +985,44 @@ export default function AdvancedTradeForm({ auto updating the reduceOnly state when doing a market order: && showReduceOnly(perpAccount?.basePosition.toNumber()) */} - {marketConfig.kind === 'perp' || isLuna ? ( -
- - reduceOnChange(e.target.checked)} - disabled={isTriggerOrder || isLuna} +
+ {marketConfig.kind === 'perp' || isLuna ? ( +
+ - Reduce Only - - -
- ) : null} - {marketConfig.kind === 'perp' && tradeType === 'Limit' ? ( -
- - postOnlySlideOnChange(e.target.checked)} - disabled={isTriggerOrder} + reduceOnChange(e.target.checked)} + disabled={isTriggerOrder || isLuna} + > + Reduce Only + + +
+ ) : null} + {marketConfig.kind === 'perp' && tradeType === 'Limit' ? ( +
+ - Slide - - -
- ) : null} + postOnlySlideOnChange(e.target.checked)} + disabled={isTriggerOrder} + > + Slide + + +
+ ) : null} +
{marketConfig.kind === 'spot' ? (
{t('slippage-warning')}
) : null} -
+
{canTrade ? (
- ) : ( -
-
- {t('maker-fee')}: {percentFormat.format(makerFee)}{' '} -
- | -
- {' '} - {t('taker-fee')}: {percentFormat.format(takerFee)} -
-
- )} + ) : null}
diff --git a/pages/markets.tsx b/pages/markets.tsx index 8cf9655e..fb266137 100644 --- a/pages/markets.tsx +++ b/pages/markets.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next' import MarketsTable from '../components/MarketsTable' import Tabs from '../components/Tabs' -const TABS = ['perp', 'spot'] +const TABS = ['futures', 'spot'] export async function getStaticProps({ locale }) { return { @@ -18,15 +18,13 @@ export async function getStaticProps({ locale }) { } export default function Markets() { - const [activeTab, setActiveTab] = useState('perp') + const [activeTab, setActiveTab] = useState('futures') const { t } = useTranslation(['common']) const handleTabChange = (tabName) => { setActiveTab(tabName) } - const isPerp = activeTab === 'perp' - return (
@@ -35,13 +33,14 @@ export default function Markets() {

{t('markets')}

- -

- {isPerp - ? `${t('perp')} ${t('markets')}` - : `${t('spot')} ${t('markets')}`} -

- +
+ +
+
diff --git a/pages/select.tsx b/pages/select.tsx deleted file mode 100644 index b1481736..00000000 --- a/pages/select.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { useEffect, useState } from 'react' -import { ChevronRightIcon } from '@heroicons/react/solid' -import useMangoStore from '../stores/useMangoStore' -import Link from 'next/link' -import { formatUsdValue } from '../utils' -import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -import PageBodyContainer from '../components/PageBodyContainer' -import TopBar from '../components/TopBar' - -export async function getStaticProps({ locale }) { - return { - props: { - ...(await serverSideTranslations(locale, [ - 'common', - 'tv-chart', - 'profile', - ])), - // Will be passed to the page component as props - }, - } -} - -const SelectMarket = () => { - const { t } = useTranslation('common') - const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) - const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) - const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache) - - const [markets, setMarkets] = useState([]) - useEffect(() => { - const markets: any[] = [] - const allMarkets = - groupConfig?.spotMarkets && groupConfig?.perpMarkets - ? [...groupConfig.spotMarkets, ...groupConfig.perpMarkets] - : [] - allMarkets.forEach((market) => { - const base = market.name.slice(0, -5) - const found = markets.find((b) => b.baseAsset === base) - if (!found) { - markets.push({ baseAsset: base, markets: [market] }) - } else { - found.markets.push(market) - } - }) - setMarkets(markets) - }, []) - - if (!mangoCache) { - return null - } - - return ( -
- - -
- {t('markets')} -
- {markets.map((mkt) => { - return ( -
-
-
- - {mkt.baseAsset} -
-
- -
- ) - })} - {/* spacer so last market can be selected albeit bottom bar overlay */} -

-
-
- ) -} - -export default SelectMarket diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 9e4a227c..865ba7e4 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -431,6 +431,7 @@ "trigger-price": "Trigger Price", "try-again": "Try again", "type": "Type", + "unavailable": "Unavailable", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", "unsettled-balance": "Redeemable Value", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index df7f9fdb..2e5f4cc0 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -431,6 +431,7 @@ "trigger-price": "Precio de activación", "try-again": "Inténtalo de nuevo", "type": "Tipo", + "unavailable": "Unavailable", "unrealized-pnl": "PnL no realizado", "unsettled": "Inestable", "unsettled-balance": "Saldo pendiente", diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 3bb63b41..28e2a27d 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -431,6 +431,7 @@ "trigger-price": "触发价格", "try-again": "请再试一次", "type": "类型", + "unavailable": "无资料", "unrealized-pnl": "未实现盈亏", "unsettled": "未结清", "unsettled-balance": "可领取价值", diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index 499373a8..848c5fb7 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -431,6 +431,7 @@ "trigger-price": "觸發價格", "try-again": "請再試一次", "type": "類型", + "unavailable": "無資料", "unrealized-pnl": "未實現盈虧", "unsettled": "未結清", "unsettled-balance": "可領取價值", diff --git a/stores/useMangoStore.tsx b/stores/useMangoStore.tsx index ef6399ae..b1d2082e 100644 --- a/stores/useMangoStore.tsx +++ b/stores/useMangoStore.tsx @@ -40,6 +40,7 @@ import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp' import { decodeBook } from '../hooks/useHydrateStore' import { IOrderLineAdapter } from '../public/charting_library/charting_library' import { Wallet } from '@solana/wallet-adapter-react' +import { coingeckoIds } from 'utils/tokens' import { getTokenAccountsByMint } from 'utils/tokens' import { getParsedNftAccountsByOwner } from 'utils/getParsedNftAccountsByOwner' @@ -264,6 +265,7 @@ export type MangoStore = { deleteAlert: (id: string) => void loadAlerts: (pk: PublicKey) => void fetchMarketsInfo: () => void + fetchCoingeckoPrices: () => void } alerts: { activeAlerts: Array @@ -277,6 +279,7 @@ export type MangoStore = { tradingView: { orderLines: Map } + coingeckoPrices: any[] } const useMangoStore = create< @@ -408,6 +411,7 @@ const useMangoStore = create< tradingView: { orderLines: new Map(), }, + coingeckoPrices: [], set: (fn) => set(produce(fn)), actions: { async fetchWalletTokens(wallet: Wallet) { @@ -1125,6 +1129,29 @@ const useMangoStore = create< console.log('ERORR: Unable to load all market info') } }, + async fetchCoingeckoPrices() { + const set = get().set + try { + const promises: any = [] + for (const asset of coingeckoIds) { + promises.push( + fetch( + `https://api.coingecko.com/api/v3/coins/${asset.id}/market_chart?vs_currency=usd&days=2` + ).then((res) => res.json()) + ) + } + + const data = await Promise.all(promises) + for (let i = 0; i < data.length; i++) { + data[i].symbol = coingeckoIds[i].symbol + } + set((state) => { + state.coingeckoPrices = data + }) + } catch (e) { + console.log('ERORR: Unable to load Coingecko prices') + } + }, }, } }) diff --git a/utils/tokens.ts b/utils/tokens.ts index b7a5d8ef..49b62ec3 100644 --- a/utils/tokens.ts +++ b/utils/tokens.ts @@ -23,6 +23,23 @@ export function parseTokenAccountData(data: Buffer): { } } +export const coingeckoIds = [ + { id: 'bitcoin', symbol: 'BTC' }, + { id: 'ethereum', symbol: 'ETH' }, + { id: 'solana', symbol: 'SOL' }, + { id: 'mango-markets', symbol: 'MNGO' }, + { id: 'binancecoin', symbol: 'BNB' }, + { id: 'serum', symbol: 'SRM' }, + { id: 'raydium', symbol: 'RAY' }, + { id: 'ftx-token', symbol: 'FTT' }, + { id: 'avalanche-2', symbol: 'AVAX' }, + { id: 'terra-luna', symbol: 'LUNA' }, + { id: 'cope', symbol: 'COPE' }, + { id: 'cardano', symbol: 'ADA' }, + { id: 'msol', symbol: 'MSOL' }, + { id: 'tether', symbol: 'USDT' }, +] + export async function getTokenAccountsByMint( connection: Connection, mint: string