import { useMemo, useState, useEffect, useRef } from 'react' import styled from '@emotion/styled' import useIpAddress from '../hooks/useIpAddress' import { getTokenBySymbol, getWeights, PerpMarket, } from '@blockworks-foundation/mango-client' import { notify } from '../utils/notifications' import { calculateTradePrice, getDecimalCount } from '../utils' import { floorToDecimal } from '../utils/index' import useMangoStore from '../stores/useMangoStore' import Button from './Button' import TradeType from './TradeType' import TriggerType from './TriggerType' import Input from './Input' import Switch from './Switch' import { Market } from '@project-serum/serum' import Big from 'big.js' import MarketFee from './MarketFee' import LeverageSlider from './LeverageSlider' import Loading from './Loading' import Tooltip from './Tooltip' import { useViewport } from '../hooks/useViewport' import { breakpoints } from './TradePageGrid' import { ElementTitle } from './styles' const StyledRightInput = styled(Input)` border-left: 1px solid transparent; ` export default function AdvancedTradeForm() { const set = useMangoStore((s) => s.set) const { ipAllowed } = useIpAddress() const connected = useMangoStore((s) => s.wallet.connected) const actions = useMangoStore((s) => s.actions) const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const marketConfig = useMangoStore((s) => s.selectedMarket.config) const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current) const mangoClient = useMangoStore((s) => s.connection.client) const market = useMangoStore((s) => s.selectedMarket.current) const isPerpMarket = market instanceof PerpMarket const [reduceOnly, setReduceOnly] = useState(false) const { side, baseSize, quoteSize, price, tradeType, triggerPrice, triggerType, } = useMangoStore((s) => s.tradeForm) const isLimitOrder = ['Limit', 'Trigger Limit'].includes(tradeType) const isMarketOrder = ['Market', 'Trigger Market'].includes(tradeType) const isTriggerOrder = ['Trigger Limit', 'Trigger Market'].includes(tradeType) const { width } = useViewport() const isMobile = width ? width < breakpoints.sm : false const [postOnly, setPostOnly] = useState(false) const [ioc, setIoc] = useState(false) const [submitting, setSubmitting] = useState(false) const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook) const orderbook = orderBookRef.current useEffect( () => useMangoStore.subscribe( // @ts-ignore (orderBook) => (orderBookRef.current = orderBook), (state) => state.selectedMarket.orderBook ), [] ) useEffect(() => { if (tradeType === 'Market') { set((s) => { s.tradeForm.price = '' }) } }, [tradeType, set]) const setSide = (side) => set((s) => { s.tradeForm.side = side }) const setBaseSize = (baseSize) => set((s) => { if (!Number.isNaN(parseFloat(baseSize))) { s.tradeForm.baseSize = parseFloat(baseSize) } else { s.tradeForm.baseSize = baseSize } }) const setQuoteSize = (quoteSize) => set((s) => { if (!Number.isNaN(parseFloat(quoteSize))) { s.tradeForm.quoteSize = parseFloat(quoteSize) } else { s.tradeForm.quoteSize = quoteSize } }) const setPrice = (price) => set((s) => { if (!Number.isNaN(parseFloat(price))) { s.tradeForm.price = parseFloat(price) } else { s.tradeForm.price = price } }) const setTradeType = (type) => set((s) => { s.tradeForm.tradeType = type }) const setTriggerPrice = (price) => { set((s) => { if (!Number.isNaN(parseFloat(price))) { s.tradeForm.triggerPrice = parseFloat(price) } else { s.tradeForm.triggerPrice = price } }) if (isMarketOrder) { onSetPrice(price) } } const setTriggerType = (type) => set((s) => { s.tradeForm.triggerType = type }) const markPriceRef = useRef(useMangoStore.getState().selectedMarket.markPrice) const markPrice = markPriceRef.current useEffect( () => useMangoStore.subscribe( (markPrice) => (markPriceRef.current = markPrice as number), (state) => state.selectedMarket.markPrice ), [] ) let minOrderSize = '0' if (market instanceof Market && market.minOrderSize) { minOrderSize = market.minOrderSize.toString() } else if (market instanceof PerpMarket) { const baseDecimals = getTokenBySymbol( groupConfig, marketConfig.baseSymbol ).decimals minOrderSize = new Big(market.baseLotSize) .div(new Big(10).pow(baseDecimals)) .toString() } const sizeDecimalCount = getDecimalCount(minOrderSize) let tickSize = 1 if (market instanceof Market) { tickSize = market.tickSize } else if (isPerpMarket) { const baseDecimals = getTokenBySymbol( groupConfig, marketConfig.baseSymbol ).decimals const quoteDecimals = getTokenBySymbol( groupConfig, groupConfig.quoteSymbol ).decimals const nativeToUi = new Big(10).pow(baseDecimals - quoteDecimals) const lotsToNative = new Big(market.quoteLotSize).div( new Big(market.baseLotSize) ) tickSize = lotsToNative.mul(nativeToUi).toNumber() } const onSetPrice = (price: number | '') => { setPrice(price) if (!price) return if (baseSize) { onSetBaseSize(baseSize) } } const onSetBaseSize = (baseSize: number | '') => { const { price } = useMangoStore.getState().tradeForm setBaseSize(baseSize) if (!baseSize) { setQuoteSize('') return } const usePrice = Number(price) || markPrice if (!usePrice) { setQuoteSize('') return } const rawQuoteSize = baseSize * usePrice setQuoteSize(rawQuoteSize.toFixed(6)) } const onSetQuoteSize = (quoteSize: number | '') => { setQuoteSize(quoteSize) if (!quoteSize) { setBaseSize('') return } if (!Number(price) && isLimitOrder) { setBaseSize('') return } const usePrice = Number(price) || markPrice const rawBaseSize = quoteSize / usePrice const baseSize = quoteSize && floorToDecimal(rawBaseSize, sizeDecimalCount) setBaseSize(baseSize) } const onTradeTypeChange = (tradeType) => { setTradeType(tradeType) if (['Market', 'Trigger Market'].includes(tradeType)) { setIoc(true) if (isTriggerOrder) { setPrice(triggerPrice) } } else { const priceOnBook = side === 'buy' ? orderbook?.asks : orderbook?.bids if (priceOnBook && priceOnBook.length > 0 && priceOnBook[0].length > 0) { setPrice(priceOnBook[0][0]) } setIoc(false) } } const postOnChange = (checked) => { if (checked) { setIoc(false) } setPostOnly(checked) } const iocOnChange = (checked) => { if (checked) { setPostOnly(false) } setIoc(checked) } const reduceOnChange = (checked) => { if (checked) { setReduceOnly(false) } setReduceOnly(checked) } async function onSubmit() { if (!price && isLimitOrder) { notify({ title: 'Missing price', type: 'error', }) return } else if (!baseSize) { notify({ title: 'Missing size', type: 'error', }) return } else if (!triggerPrice && isTriggerOrder) { notify({ title: 'Missing trigger price', type: 'error', }) return } const mangoAccount = useMangoStore.getState().selectedMangoAccount.current const mangoGroup = useMangoStore.getState().selectedMangoGroup.current const { askInfo, bidInfo } = useMangoStore.getState().selectedMarket const wallet = useMangoStore.getState().wallet.current if (!wallet || !mangoGroup || !mangoAccount || !market) return setSubmitting(true) try { const orderPrice = calculateTradePrice( tradeType, orderbook, baseSize, side, price, triggerPrice ) if (!orderPrice) { notify({ title: 'Price not available', description: 'Please try again', type: 'error', }) } const orderType = ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit' let txid if (market instanceof Market) { txid = await mangoClient.placeSpotOrder2( mangoGroup, mangoAccount, mangoGroup.mangoCache, market, wallet, side, orderPrice, baseSize, orderType ) } else { if (isTriggerOrder) { txid = await mangoClient.addPerpTriggerOrder( mangoGroup, mangoAccount, market, wallet, orderType, side, orderPrice, baseSize, side === 'buy' ? 'below' : 'above', // triggerCondition Number(triggerPrice), reduceOnly ) } else { txid = await mangoClient.placePerpOrder( mangoGroup, mangoAccount, mangoGroup.mangoCache, market, wallet, side, orderPrice, baseSize, tradeType === 'Market' ? 'market' : orderType, 0, side === 'buy' ? askInfo : bidInfo, // book side used for ConsumeEvents reduceOnly ) } } notify({ title: 'Successfully placed trade', txid }) setPrice('') onSetBaseSize('') } catch (e) { notify({ title: 'Error placing order', description: e.message, txid: e.txid, type: 'error', }) } finally { // TODO: should be removed, main issue are newly created OO accounts // await sleep(600) actions.reloadMangoAccount() actions.loadMarketFills() setSubmitting(false) } } const initLeverage = useMemo(() => { if (!mangoGroup || !marketConfig) return 1 const ws = getWeights(mangoGroup, marketConfig.marketIndex, 'Init') const w = marketConfig.kind === 'perp' ? ws.perpAssetWeight : ws.spotAssetWeight return Math.round((100 * -1) / (w.toNumber() - 1)) / 100 }, [mangoGroup, marketConfig]) const disabledTradeButton = (!price && isLimitOrder) || !baseSize || !connected || submitting || !mangoAccount return !isMobile ? (
Trade {marketConfig.name} {initLeverage}x
onSetPrice(e.target.value)} value={price} disabled={isMarketOrder} prefix={'Price'} suffix={groupConfig.quoteSymbol} className="rounded-r-none" wrapperClassName="w-3/5" /> onSetBaseSize(e.target.value)} value={baseSize} className="rounded-r-none" wrapperClassName="w-3/5" prefixClassName="w-12" prefix={'Size'} suffix={marketConfig.baseSymbol} /> onSetQuoteSize(e.target.value)} value={quoteSize} className="rounded-l-none" wrapperClassName="w-2/5" suffix={groupConfig.quoteSymbol} /> onSetBaseSize(e)} value={baseSize ? baseSize : 0} step={parseFloat(minOrderSize)} disabled={false} side={side} decimalCount={sizeDecimalCount} price={calculateTradePrice( tradeType, orderbook, baseSize ? baseSize : 0, side, price, triggerPrice )} />
{tradeType !== 'Market' ? ( <>
POST
IOC
) : null} {marketConfig.kind === 'perp' ? (
Reduce Only
) : null}
{isTriggerOrder && ( setTriggerPrice(e.target.value)} value={triggerPrice} prefix={'Price'} suffix={groupConfig.quoteSymbol} className="rounded-l-none" wrapperClassName="rounded-l-none w-3/5" /> )}
{ipAllowed ? ( side === 'buy' ? ( ) : ( ) ) : ( )}
) : (
onSetPrice(e.target.value)} value={price} disabled={tradeType === 'Market'} suffix={ } />
onSetBaseSize(e.target.value)} value={baseSize} suffix={ } />
onSetQuoteSize(e.target.value)} value={quoteSize} suffix={ } />
onSetBaseSize(e)} value={baseSize ? baseSize : 0} step={parseFloat(minOrderSize)} disabled={false} side={side} decimalCount={sizeDecimalCount} price={calculateTradePrice( tradeType, orderbook, baseSize ? baseSize : 0, side, price, triggerPrice )} /> {tradeType !== 'Market' ? (
POST
IOC
) : null}
{ipAllowed ? ( side === 'buy' ? ( ) : ( ) ) : ( )}
) }