add market select dropdown

This commit is contained in:
Tyler Shipe 2021-04-06 18:11:42 -04:00
parent ea376cc166
commit a0c0768473
18 changed files with 334 additions and 149 deletions

View File

@ -0,0 +1,53 @@
import xw from 'xwind'
import { Menu, Transition } from '@headlessui/react'
import useMarketList from '../hooks/useMarketList'
import useMarket from '../hooks/useMarket'
const DropdownExample = () => {
const { spotMarkets } = useMarketList()
const { marketName } = useMarket()
console.log('spot markets', spotMarkets)
// const handleChange = (e) => {
// console.log('new market selected', e)
// }
return (
<div css={xw`ml-4`}>
<Menu>
{({ open }) => (
<>
<Menu.Button>{marketName}</Menu.Button>
<Transition
show={open}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items>
{Object.entries(spotMarkets).map(([name, address]) => (
<Menu.Item key={address}>
{({ active }) => (
<a
className={`${active && 'bg-blue-500'}`}
href="/account-settings"
>
{name}
</a>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
)
}
export default DropdownExample

View File

@ -73,9 +73,8 @@ export default function MarginInfo() {
>(null)
const { tradeHistory } = useTradeHistory()
// Settle bororows
useEffect(() => {
console.log('HERE mangoGroup: ', mangoGroup)
console.log('marginInfo useEffect')
if (mangoGroup) {
mangoGroup.getPrices(connection).then((prices) => {

View File

@ -0,0 +1,76 @@
import xw from 'xwind'
import { Listbox, Transition } from '@headlessui/react'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
import useMarketList from '../hooks/useMarketList'
// import useMarket from '../hooks/useMarket'
import useMangoStore from '../stores/useMangoStore'
const MarketSelect = () => {
const { spotMarkets } = useMarketList()
const selectedMarket = useMangoStore((s) => s.selectedMarket)
const setMangoStore = useMangoStore((s) => s.set)
const handleChange = (mktName) => {
setMangoStore((state) => {
state.selectedMarket = { name: mktName, address: spotMarkets[mktName] }
})
}
return (
<div css={xw`ml-4 relative inline-block -mb-1`}>
<Listbox value={selectedMarket.name} onChange={handleChange}>
{({ open }) => (
<>
<Listbox.Button
css={xw`border border-mango-dark-lighter focus:outline-none focus:ring-1 focus:ring-mango-yellow p-2 w-56`}
>
<div
css={xw`flex items-center text-lg justify-between font-light`}
>
{selectedMarket.name}
{open ? (
<ChevronUpIcon css={xw`h-5 w-5 mr-1`} />
) : (
<ChevronDownIcon css={xw`h-5 w-5 mr-1`} />
)}
</div>
</Listbox.Button>
<Transition
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Listbox.Options
static
css={xw`z-20 p-1 absolute left-0 w-56 mt-1 bg-mango-dark-light origin-top-left divide-y divide-mango-dark-lighter shadow-lg outline-none`}
>
<div css={xw`opacity-50 p-2`}>Markets</div>
{Object.entries(spotMarkets).map(([name, address]) => (
<Listbox.Option key={address} value={name}>
{({ selected }) => (
<div
css={[
xw`p-2 text-base hover:bg-mango-dark-lighter hover:cursor-pointer tracking-wider font-light`,
selected &&
xw`text-mango-yellow bg-mango-dark-lighter`,
]}
>
{name}
</div>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
</div>
)
}
export default MarketSelect

View File

@ -3,7 +3,7 @@ import xw from 'xwind'
import { TrashIcon } from '@heroicons/react/solid'
const OpenOrdersTable = () => {
const [openOrders, setOpenOrders] = useState(['test'])
const [openOrders] = useState(['test'])
return (
<div css={xw`flex flex-col py-6`}>
@ -79,7 +79,7 @@ const OpenOrdersTable = () => {
0.00
</td>
<td
css={xw`px-6 py-4 whitespace-nowrap text-right text-sm font-medium`}
css={xw`px-6 py-4 opacity-75 whitespace-nowrap text-right text-sm font-medium`}
>
<button
css={xw`flex items-center ml-auto text-mango-red border border-mango-red hover:text-red-700 hover:border-red-700 py-1 px-4`}

View File

@ -7,7 +7,7 @@ import { isEqual, getDecimalCount } from '../utils/'
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/solid'
import useMarkPrice from '../hooks/useMarkPrice'
import useOrderbook from '../hooks/useOrderbook'
import useMarkets from '../hooks/useMarkets'
import useMarket from '../hooks/useMarket'
import { ElementTitle } from './styles'
const Line = styled.div<any>`
@ -22,7 +22,7 @@ const Line = styled.div<any>`
export default function Orderbook({ depth = 7 }) {
const markPrice = useMarkPrice()
const [orderbook] = useOrderbook()
const { baseCurrency, quoteCurrency } = useMarkets()
const { baseCurrency, quoteCurrency } = useMarket()
const currentOrderbookData = useRef(null)
const lastOrderbookData = useRef(null)
@ -117,7 +117,7 @@ export default function Orderbook({ depth = 7 }) {
const OrderbookRow = React.memo<any>(
({ side, price, size, sizePercent, onSizeClick, onPriceClick, invert }) => {
const element = useRef(null)
const { market } = useMarkets()
const { market } = useMarket()
useEffect(() => {
!element.current?.classList.contains('flash') &&
@ -193,7 +193,7 @@ const OrderbookRow = React.memo<any>(
const MarkPriceComponent = React.memo<{ markPrice: number }>(
({ markPrice }) => {
const { market } = useMarkets()
const { market } = useMarket()
const previousMarkPrice: number = usePrevious(markPrice)
const markPriceColor =

View File

@ -4,13 +4,13 @@ import xw from 'xwind'
import { getDecimalCount } from '../utils'
import { ChartTradeType } from '../@types/types'
import FloatingElement from './FloatingElement'
import useMarkets from '../hooks/useMarkets'
import useMarket from '../hooks/useMarket'
import useInterval from '../hooks/useInterval'
import ChartApi from '../utils/chartDataConnector'
import { ElementTitle } from './styles'
export default function PublicTrades() {
const { baseCurrency, quoteCurrency, market, marketAddress } = useMarkets()
const { baseCurrency, quoteCurrency, market, marketAddress } = useMarket()
const [trades, setTrades] = useState([])
useInterval(async () => {

View File

@ -43,7 +43,7 @@ const TopBar = () => {
onClick={handleConnectDisconnect}
css={xw`bg-mango-dark-light hover:bg-mango-dark-lighter text-mango-yellow rounded-md px-4 py-2 focus:outline-none`}
>
<div css={xw`text-base font-light`}>
<div css={xw`text-lg font-light`}>
{connected ? (
<div onClick={wallet.disconnect}>
<span>Disconnect: </span>

View File

@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'
import { Input, Radio, Switch, Select } from 'antd'
import xw from 'xwind'
import styled from '@emotion/styled'
import useMarkets from '../hooks/useMarkets'
import useMarket from '../hooks/useMarket'
import useWallet from '../hooks/useWallet'
import useIpAddress from '../hooks/useIpAddress'
import useConnection from '../hooks/useConnection'
@ -36,10 +36,10 @@ export default function TradeForm({
ref: ({ size, price }: { size?: number; price?: number }) => void
) => void
}) {
console.log('reloading trade form')
// console.log('reloading trade form')
const [side, setSide] = useState<'buy' | 'sell'>('buy')
const { baseCurrency, quoteCurrency, market } = useMarkets()
const { baseCurrency, quoteCurrency, market } = useMarket()
const address = market?.publicKey
const { wallet, connected } = useWallet()
@ -368,20 +368,24 @@ export default function TradeForm({
side === 'buy' ? (
<BuyButton
disabled={
(!price && tradeType === 'Limit') || !baseSize || !connected
(!price && tradeType === 'Limit') ||
!baseSize ||
!connected ||
submitting
}
onClick={onSubmit}
loading={submitting}
>
{connected ? `Buy ${baseCurrency}` : 'CONNECT WALLET TO TRADE'}
</BuyButton>
) : (
<SellButton
disabled={
(!price && tradeType === 'Limit') || !baseSize || !connected
(!price && tradeType === 'Limit') ||
!baseSize ||
!connected ||
submitting
}
onClick={onSubmit}
loading={submitting}
>
{connected ? `Sell ${baseCurrency}` : 'CONNECT WALLET TO TRADE'}
</SellButton>

View File

@ -1,4 +1,4 @@
import * as React from 'react'
import { useEffect, useRef } from 'react'
import {
widget,
ChartingLibraryWidgetOptions,
@ -7,6 +7,7 @@ import {
} from '../charting_library' // Make sure to follow step 1 of the README
// import { useMarket } from '../../utils/markets';
import { CHART_DATA_FEED } from '../../utils/chartDataConnector'
import useMangoStore from '../../stores/useMangoStore'
// This is a basic example of how to create a TV widget
// You can add more feature such as storing charts in localStorage
@ -30,9 +31,10 @@ export interface ChartContainerProps {
// export interface ChartContainerState {}
const TVChartContainer = () => {
const selectedMarketName = useMangoStore((s) => s.selectedMarket.name)
// @ts-ignore
const defaultProps: ChartContainerProps = {
symbol: 'BTC/USDT',
symbol: selectedMarketName,
interval: '60' as ResolutionString,
theme: 'Dark',
containerId: 'tv_chart_container',
@ -46,25 +48,13 @@ const TVChartContainer = () => {
},
}
const tvWidgetRef = React.useRef<IChartingLibraryWidget | null>(null)
const tvWidgetRef = useRef<IChartingLibraryWidget | null>(null)
// TODO: fetch market from store and wire up to chart
// const { market, marketName } = useMarket()
const parsedMarketName = 'BTC/USDT'
// switch (marketName) {
// case 'BTC/WUSDT':
// parsedMarketName = 'BTC/USDT'
// break
// case 'ETH/WUSDT':
// parsedMarketName = 'ETH/USDT'
// break
// default:
// parsedMarketName = marketName
// }
React.useEffect(() => {
useEffect(() => {
const widgetOptions: ChartingLibraryWidgetOptions = {
symbol: parsedMarketName,
symbol: selectedMarketName,
// BEWARE: no trailing slash is expected in feed URL
// tslint:disable-next-line:no-any
datafeed: new (window as any).Datafeeds.UDFCompatibleDatafeed(
@ -138,7 +128,7 @@ const TVChartContainer = () => {
})
})
//eslint-disable-next-line
}, [])
}, [selectedMarketName])
// TODO: add market back to dep array
// }, [market])

49
hooks/useHydrateStore.tsx Normal file
View File

@ -0,0 +1,49 @@
import { useEffect } from 'react'
import { Market } from '@project-serum/serum'
import useConnection from './useConnection'
import useMangoStore from '../stores/useMangoStore'
import { PublicKey } from '@solana/web3.js'
const useHydrateStore = () => {
const setMangoStore = useMangoStore((s) => s.set)
const selectedMarket = useMangoStore((s) => s.selectedMarket)
const { connection, dexProgramId } = useConnection()
useEffect(() => {
console.log(
'useEffect loading market',
selectedMarket.address,
dexProgramId
)
Market.load(
connection,
new PublicKey(selectedMarket.address),
{},
new PublicKey(dexProgramId)
)
.then((market) => {
setMangoStore((state) => {
state.market.current = market
// @ts-ignore
state.accountInfos[market._decoded.bids.toString()] = null
// @ts-ignore
state.accountInfos[market._decoded.asks.toString()] = null
})
})
.catch(
(e) => {
console.error('failed to load market', e)
}
// TODO
// notify({
// message: 'Error loading market',
// description: e.message,
// type: 'error',
// }),
)
}, [selectedMarket])
return null
}
export default useHydrateStore

60
hooks/useMarket.tsx Normal file
View File

@ -0,0 +1,60 @@
import { useMemo } from 'react'
import useConnection from './useConnection'
import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
import { IDS } from '@blockworks-foundation/mango-client'
const formatTokenMints = (symbols: { [name: string]: string }) => {
return Object.entries(symbols).map(([name, address]) => {
return {
address: new PublicKey(address),
name: name,
}
})
}
const useMarket = () => {
const market = useMangoStore((state) => state.market.current)
const selectedMarket = useMangoStore((state) => state.selectedMarket)
const { cluster, programId } = useConnection()
const marketAddress = useMemo(
() => (market ? market.publicKey.toString() : null),
[market]
)
const TOKEN_MINTS = useMemo(() => formatTokenMints(IDS[cluster].symbols), [
cluster,
])
const baseCurrency = useMemo(
() =>
(market?.baseMintAddress &&
TOKEN_MINTS.find((token) =>
token.address.equals(market.baseMintAddress)
)?.name) ||
'UNKNOWN',
[market, TOKEN_MINTS]
)
const quoteCurrency = useMemo(
() =>
(market?.quoteMintAddress &&
TOKEN_MINTS.find((token) =>
token.address.equals(market.quoteMintAddress)
)?.name) ||
'UNKNOWN',
[market, TOKEN_MINTS]
)
return {
market,
marketAddress,
programId,
marketName: selectedMarket.name,
baseCurrency,
quoteCurrency,
}
}
export default useMarket

36
hooks/useMarketList.tsx Normal file
View File

@ -0,0 +1,36 @@
import { useMemo } from 'react'
import useConnection from './useConnection'
import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
import { IDS } from '@blockworks-foundation/mango-client'
const useMarketList = () => {
const mangoGroupName = useMangoStore((state) => state.selectedMangoGroup.name)
const { cluster, programId, dexProgramId } = useConnection()
const spotMarkets = useMemo(
() => IDS[cluster]?.mango_groups[mangoGroupName]?.spot_market_symbols || {},
[cluster, mangoGroupName]
)
const marketList = useMemo(
() =>
Object.entries(spotMarkets).map(([name, address]) => {
return {
address: new PublicKey(address as string),
programId: new PublicKey(dexProgramId as string),
deprecated: false,
name,
}
}),
[spotMarkets, dexProgramId]
)
return {
programId,
marketList,
spotMarkets,
}
}
export default useMarketList

View File

@ -1,106 +0,0 @@
import { useEffect, useMemo } from 'react'
import useConnection from './useConnection'
import { PublicKey } from '@solana/web3.js'
import { Market } from '@project-serum/serum'
import useMangoStore from '../stores/useMangoStore'
import { IDS } from '@blockworks-foundation/mango-client'
const formatTokenMints = (symbols: { [name: string]: string }) => {
return Object.entries(symbols).map(([name, address]) => {
return {
address: new PublicKey(address),
name: name,
}
})
}
const useMarkets = () => {
const setMangoStore = useMangoStore((state) => state.set)
const mangoGroupName = useMangoStore((state) => state.selectedMangoGroup.name)
const market = useMangoStore((state) => state.market.current)
const { connection, cluster, programId, dexProgramId } = useConnection()
const marketAddress = useMemo(
() => (market ? market.publicKey.toString() : null),
[market]
)
const spotMarkets = useMemo(
() => IDS[cluster]?.mango_groups[mangoGroupName]?.spot_market_symbols || {},
[cluster, mangoGroupName]
)
const marketList = useMemo(
() =>
Object.entries(spotMarkets).map(([name, address]) => {
return {
address: new PublicKey(address as string),
programId: new PublicKey(dexProgramId as string),
deprecated: false,
name,
}
}),
[spotMarkets]
)
useEffect(() => {
if (market) return
console.log('loading market', connection)
Market.load(connection, marketList[0].address, {}, marketList[0].programId)
.then((market) => {
setMangoStore((state) => {
state.market.current = market
// @ts-ignore
state.accountInfos[market._decoded.bids.toString()] = null
// @ts-ignore
state.accountInfos[market._decoded.asks.toString()] = null
})
})
.catch(
(e) => {
console.error('failed to load market', e)
}
// TODO
// notify({
// message: 'Error loading market',
// description: e.message,
// type: 'error',
// }),
)
}, [])
const TOKEN_MINTS = useMemo(() => formatTokenMints(IDS[cluster].symbols), [
cluster,
])
const baseCurrency = useMemo(
() =>
(market?.baseMintAddress &&
TOKEN_MINTS.find((token) =>
token.address.equals(market.baseMintAddress)
)?.name) ||
'UNKNOWN',
[market, TOKEN_MINTS]
)
const quoteCurrency = useMemo(
() =>
(market?.quoteMintAddress &&
TOKEN_MINTS.find((token) =>
token.address.equals(market.quoteMintAddress)
)?.name) ||
'UNKNOWN',
[market, TOKEN_MINTS]
)
return {
market,
marketAddress,
programId,
marketList,
baseCurrency,
quoteCurrency,
}
}
export default useMarkets

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react'
import { PublicKey, AccountInfo } from '@solana/web3.js'
import { Orderbook } from '@project-serum/serum'
import useMarkets from './useMarkets'
import useMarket from './useMarket'
import useInterval from './useInterval'
import useMangoStore from '../stores/useMangoStore'
import useConnection from './useConnection'
@ -69,7 +69,7 @@ export default function useOrderbook(
depth = 20
): [{ bids: number[][]; asks: number[][] }, boolean] {
const { bidOrderbook, askOrderbook } = useOrderbookAccounts()
const { market } = useMarkets()
const { market } = useMarket()
const setMangoStore = useMangoStore((s) => s.set)

View File

@ -29,6 +29,7 @@
"@blockworks-foundation/mango-client": "^0.1.10",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
"@headlessui/react": "^0.3.2",
"@heroicons/react": "^1.0.0",
"@project-serum/serum": "^0.13.31",
"@project-serum/sol-wallet-adapter": "^0.1.8",

View File

@ -2,12 +2,17 @@ import xw from 'xwind'
import TopBar from '../components/TopBar'
import Notifications from '../components/Notification'
import TradePageGrid from '../components/TradePageGrid'
import MarketSelect from '../components/MarketSelect'
import useHydrateStore from '../hooks/useHydrateStore'
const Index = () => {
useHydrateStore()
return (
<div css={xw`bg-mango-dark text-white`}>
<TopBar />
<div css={xw`min-h-screen p-1 sm:p-2 md:p-6`}>
<div css={xw`min-h-screen p-1 sm:p-2 md:p-6 md:pt-4`}>
<MarketSelect />
<TradePageGrid />
</div>
<Notifications

View File

@ -3,6 +3,7 @@ import { devtools } from 'zustand/middleware'
import produce from 'immer'
import { Market } from '@project-serum/serum'
import {
IDS,
MangoClient,
MangoGroup,
MarginAccount,
@ -27,6 +28,7 @@ export const ENDPOINTS: EndpointInfo[] = [
const CLUSTER = 'mainnet-beta'
const ENDPOINT_URL = ENDPOINTS.find((e) => e.name === CLUSTER).endpoint
const DEFAULT_CONNECTION = new Connection(ENDPOINT_URL, 'recent')
const DEFAULT_MANGO_GROUP = 'BTC_ETH_USDT'
interface AccountInfoList {
[key: string]: AccountInfo<Buffer>
@ -40,11 +42,15 @@ interface MangoStore extends State {
endpoint: string
}
market: {
programId: number | null
current: Market | null
mangoProgramId: number | null
markPrice: number
orderBook: any[]
}
selectedMarket: {
name: string
address: string
}
mangoClient: MangoClient
mangoGroups: Array<MangoGroup>
selectedMangoGroup: {
@ -74,17 +80,24 @@ const useMangoStore = create<MangoStore>(
endpoint: ENDPOINT_URL,
},
selectedMangoGroup: {
name: 'BTC_ETH_USDT',
name: DEFAULT_MANGO_GROUP,
current: null,
},
selectedMarket: {
name: Object.entries(
IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP].spot_market_symbols
)[0][0],
address: Object.entries(
IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP].spot_market_symbols
)[0][1],
},
market: {
programId: null,
current: null,
mangoProgramId: null,
markPrice: 0,
orderBook: [],
},
mangoClient: new MangoClient(),
mangoGroup: null,
mangoGroups: [],
marginAccounts: [],
selectedMarginAccount: {

View File

@ -1153,6 +1153,11 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.1.tgz#9daf5745156fd84b8e9889a2dc721f0c58e894aa"
integrity sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==
"@headlessui/react@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-0.3.2.tgz#fa8600fa669fe704b84e9256855fb39092b6e233"
integrity sha512-N8XpYJNEP1jH5V8qq2cX8pnipOd93UJNKky1B7Ac3/yFFehB/0C4k5kOHt8ShHsmcohrZUB4sOl4+m0CjQpXng==
"@heroicons/react@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.0.tgz#16a6147f665434b68f401f050a6cb1d68cf819bc"