Merge branch 'align-advanced-trade-form' into max/stoploss

This commit is contained in:
Maximilian Schneider 2021-09-30 11:51:49 +02:00
commit 82a3dcebb6
28 changed files with 1089 additions and 1179 deletions

View File

@ -1,758 +0,0 @@
import { useState, useEffect, useRef } from 'react'
import styled from '@emotion/styled'
import useIpAddress from '../hooks/useIpAddress'
import {
getTokenBySymbol,
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 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 OrderSideTabs from './OrderSideTabs'
import { ElementTitle } from './styles'
const StyledRightInput = styled(Input)`
border-left: 1px solid transparent;
`
export default function AdvancedTradeForm({ initLeverage }) {
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 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,
triggerCondition,
} = useMangoStore((s) => s.tradeForm)
const isLimitOrder = ['Limit', 'Stop Limit', 'Take Profit Limit'].includes(
tradeType
)
const isMarketOrder = ['Market', 'Stop Loss', 'Take Profit'].includes(
tradeType
)
const isTriggerOrder = [
'Stop Loss',
'Stop Limit',
'Take Profit',
'Take Profit Limit',
].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])
useEffect(() => {
let condition
switch (tradeType) {
case 'Stop Loss':
case 'Stop Limit':
condition = side == 'buy' ? 'above' : 'below'
break
case 'Take Profit':
case 'Take Profit Limit':
condition = side == 'buy' ? 'below' : 'above'
break
}
if (condition) {
set((s) => {
s.tradeForm.triggerCondition = condition
})
}
}, [set, tradeType, side])
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 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', 'Stop Loss', 'Take Profit'].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,
isMarketOrder ? 'market' : orderType,
side,
orderPrice,
baseSize,
triggerCondition,
Number(triggerPrice)
)
} else {
txid = await mangoClient.placePerpOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
isMarketOrder ? 'market' : orderType,
Date.now(),
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',
})
console.error(e)
} finally {
// TODO: should be removed, main issue are newly created OO accounts
// await sleep(600)
actions.reloadMangoAccount()
actions.loadMarketFills()
setSubmitting(false)
}
}
const disabledTradeButton =
(!price && isLimitOrder) ||
!baseSize ||
!connected ||
submitting ||
!mangoAccount
return !isMobile ? (
<div className={!connected ? 'fliter blur-sm' : 'flex flex-col h-full'}>
<ElementTitle>
{marketConfig.name}
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
{initLeverage}x
</span>
</ElementTitle>
<OrderSideTabs onChange={setSide} side={side} />
<Input.Group className="mt-4">
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={isMarketOrder}
prefix={'Price'}
suffix={groupConfig.quoteSymbol}
className="rounded-r-none"
wrapperClassName="w-3/5"
/>
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
offerTriggers={isPerpMarket}
className="hover:border-th-primary flex-grow"
/>
</Input.Group>
{isTriggerOrder && (
<Input.Group className="mt-4">
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => setTriggerPrice(e.target.value)}
value={triggerPrice}
prefix={'Trigger Price'}
suffix={groupConfig.quoteSymbol}
className="rounded-r-none"
// wrapperClassName="w-3/5"
/>
</Input.Group>
)}
<Input.Group className="mt-4">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
className="rounded-r-none"
wrapperClassName="w-3/5"
prefixClassName="w-12"
prefix={'Size'}
suffix={marketConfig.baseSymbol}
/>
<StyledRightInput
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
className="rounded-l-none"
wrapperClassName="w-2/5"
suffix={groupConfig.quoteSymbol}
/>
</Input.Group>
<LeverageSlider
onChange={(e) => 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
)}
/>
<div className="flex mt-2">
{isLimitOrder ? (
<>
<div className="mr-4">
<Tooltip
delay={250}
placement="left"
content="Post only orders are guaranteed to be the maker order or else it will be canceled."
>
<Switch checked={postOnly} onChange={postOnChange}>
POST
</Switch>
</Tooltip>
</div>
<div className="mr-4">
<Tooltip
delay={250}
placement="left"
content="Immediate or cancel orders are guaranteed to be the taker or it will be canceled."
>
<Switch checked={ioc} onChange={iocOnChange}>
IOC
</Switch>
</Tooltip>
</div>
</>
) : null}
{marketConfig.kind === 'perp' && !isTriggerOrder ? (
<div>
<Tooltip
delay={250}
placement="left"
content="Reduce only orders will only reduce your overall position."
>
<Switch checked={reduceOnly} onChange={reduceOnChange}>
Reduce Only
</Switch>
</Tooltip>
</div>
) : null}
</div>
<div className={`flex py-4`}>
{ipAllowed ? (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`${
!disabledTradeButton
? 'bg-th-bkg-2 border border-th-green hover:border-th-green-dark'
: 'border border-th-bkg-4'
} text-th-green hover:text-th-fgd-1 hover:bg-th-green-dark flex-grow`}
>
{submitting ? (
<div className="w-full">
<Loading className="mx-auto" />
</div>
) : side.toLowerCase() === 'buy' ? (
market instanceof PerpMarket ? (
`${baseSize > 0 ? 'Long ' + baseSize : 'Long '} ${
marketConfig.name
}`
) : (
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
marketConfig.baseSymbol
}`
)
) : market instanceof PerpMarket ? (
`${baseSize > 0 ? 'Short ' + baseSize : 'Short '} ${
marketConfig.name
}`
) : (
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
marketConfig.baseSymbol
}`
)}
</Button>
) : (
<Button disabled className="flex-grow">
<span>Country Not Allowed</span>
</Button>
)}
</div>
<div className="flex text-xs text-th-fgd-4 px-6 mt-2.5">
<MarketFee />
</div>
</div>
) : (
<div className="flex flex-col h-full">
<div className={`flex pb-3 text-base text-th-fgd-4`}>
<button
onClick={() => setSide('buy')}
className={`flex-1 outline-none focus:outline-none`}
>
<div
className={`hover:text-th-green pb-1 transition-colors duration-500
${
side === 'buy'
? `text-th-green hover:text-th-green border-b-2 border-th-green`
: undefined
}`}
>
Buy
</div>
</button>
<button
onClick={() => setSide('sell')}
className={`flex-1 outline-none focus:outline-none`}
>
<div
className={`hover:text-th-red pb-1 transition-colors duration-500
${
side === 'sell'
? `text-th-red hover:text-th-red border-b-2 border-th-red`
: undefined
}
`}
>
Sell
</div>
</button>
</div>
<div className="pb-3">
<label className="block mb-1 text-th-fgd-3 text-xs">Price</label>
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={tradeType === 'Market'}
suffix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="flex items-center justify-between pb-3">
<label className="text-th-fgd-3 text-xs">Type</label>
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
className=""
/>
</div>
<label className="block mb-1 text-th-fgd-3 text-xs">Size</label>
<div className="grid grid-cols-2 grid-rows-1 gap-2">
<div className="col-span-1">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
suffix={
<img
src={`/assets/icons/${marketConfig.baseSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-1">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
suffix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
</div>
<LeverageSlider
onChange={(e) => 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' ? (
<div className="flex mt-2">
<Switch checked={postOnly} onChange={postOnChange}>
POST
</Switch>
<div className="ml-4">
<Switch checked={ioc} onChange={iocOnChange}>
IOC
</Switch>
</div>
</div>
) : null}
<div className={`flex py-4`}>
{ipAllowed ? (
side === 'buy' ? (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`${
!disabledTradeButton
? 'bg-th-bkg-2 border border-th-green hover:border-th-green-dark'
: 'border border-th-bkg-4'
} text-th-green hover:text-th-fgd-1 hover:bg-th-green-dark flex-grow`}
>
{submitting ? (
<div className="w-full">
<Loading className="mx-auto" />
</div>
) : (
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
}`
)}
</Button>
) : (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`${
!disabledTradeButton
? 'bg-th-bkg-2 border border-th-red hover:border-th-red-dark'
: 'border border-th-bkg-4'
} text-th-red hover:text-th-fgd-1 hover:bg-th-red-dark flex-grow`}
>
{submitting ? (
<div className="w-full">
<Loading className="mx-auto" />
</div>
) : (
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
}`
)}
</Button>
)
) : (
<Button disabled className="flex-grow">
<span>Country Not Allowed</span>
</Button>
)}
</div>
<div className="flex text-xs text-th-fgd-4 px-6 mt-2.5">
<MarketFee />
</div>
</div>
)
}

View File

@ -1,5 +1,3 @@
// import { useViewport } from '../hooks/useViewport'
// import { breakpoints } from './TradePageGrid'
import { FunctionComponent } from 'react' import { FunctionComponent } from 'react'
interface ButtonGroupProps { interface ButtonGroupProps {
@ -15,8 +13,6 @@ const ButtonGroup: FunctionComponent<ButtonGroupProps> = ({
values, values,
onChange, onChange,
}) => { }) => {
// const { width } = useViewport()
// const isMobile = width ? width < breakpoints.sm : false
return ( return (
<div className="bg-th-bkg-3 rounded-md"> <div className="bg-th-bkg-3 rounded-md">
<div className="flex relative"> <div className="flex relative">
@ -37,7 +33,7 @@ const ButtonGroup: FunctionComponent<ButtonGroupProps> = ({
${ ${
v === activeValue v === activeValue
? `text-th-primary` ? `text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100` : `text-th-fgd-1 opacity-70 hover:opacity-100`
} }
`} `}
key={`${v}${i}`} key={`${v}${i}`}

View File

@ -15,19 +15,20 @@ const HiddenCheckbox = styled.input`
width: 1px; width: 1px;
` `
const Checkbox = ({ checked, ...props }) => ( const Checkbox = ({ checked, children, ...props }) => (
<> <label className="cursor-pointer flex items-center">
<HiddenCheckbox checked={checked} {...props} type="checkbox" /> <HiddenCheckbox checked={checked} {...props} type="checkbox" />
<div <div
className={`${ className={`${
checked ? 'bg-th-fgd-4' : 'bg-th-bkg-4' checked ? 'border-th-primary' : 'border-th-fgd-4'
} cursor-pointer default-transition inline-block rounded h-4 w-4`} } border cursor-pointer default-transition flex items-center justify-center rounded h-4 w-4`}
> >
<CheckIcon <CheckIcon
className={`${checked ? 'block' : 'hidden'} h-4 w-4 text-th-primary`} className={`${checked ? 'block' : 'hidden'} h-4 w-4 text-th-primary`}
/> />
</div> </div>
</> <span className="ml-2 text-xs text-th-fgd-3">{children}</span>
</label>
) )
export default Checkbox export default Checkbox

View File

@ -29,9 +29,11 @@ export const FlipCardInner = styled.div<any>`
` `
export const FlipCardFront = styled.div` export const FlipCardFront = styled.div`
position: absolute;
width: 100%; width: 100%;
@media screen and (min-width: 768px) {
height: 100%; height: 100%;
position: absolute;
}
` `
export const FlipCardBack = styled.div` export const FlipCardBack = styled.div`
@ -45,5 +47,4 @@ export const StyledFloatingElement = styled(FloatingElement)`
animation: ${css` animation: ${css`
${fadeIn} 1s linear ${fadeIn} 1s linear
`}; `};
overflow: hidden;
` `

View File

@ -41,7 +41,7 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
const wallet = useMangoStore((s) => s.wallet.current) const wallet = useMangoStore((s) => s.wallet.current)
return ( return (
<div <div
className={`thin-scroll bg-th-bkg-2 rounded-lg p-2.5 sm:p-4 overflow-auto overflow-x-hidden relative ${className}`} className={`thin-scroll bg-th-bkg-2 rounded-lg p-2.5 md:p-4 overflow-auto overflow-x-hidden relative ${className}`}
> >
{!connected && showConnect ? ( {!connected && showConnect ? (
<div className="absolute top-0 left-0 w-full h-full z-10"> <div className="absolute top-0 left-0 w-full h-full z-10">

View File

@ -30,8 +30,7 @@ const Input = ({
<div className={`flex relative ${wrapperClassName}`}> <div className={`flex relative ${wrapperClassName}`}>
{prefix ? ( {prefix ? (
<div <div
className={`flex items-center justify-end p-2 border border-r-0 className={`absolute left-2 top-1/2 transform -translate-y-1/2 ${prefixClassName}`}
border-th-fgd-4 bg-th-bkg-2 text-xs rounded-md rounded-r-none text-right ${prefixClassName}`}
> >
{prefix} {prefix}
</div> </div>
@ -40,7 +39,7 @@ const Input = ({
type={type} type={type}
value={value} value={value}
onChange={onChange} onChange={onChange}
className={`${className} pb-px px-2 flex-1 bg-th-bkg-1 rounded-md h-10 text-th-fgd-1 w-full className={`${className} bg-th-bkg-1 pb-px px-2 flex-1 rounded-md h-10 text-th-fgd-1 w-full
border ${ border ${
error ? 'border-th-red' : 'border-th-fgd-4' error ? 'border-th-red' : 'border-th-fgd-4'
} default-transition hover:border-th-primary } default-transition hover:border-th-primary
@ -50,7 +49,7 @@ const Input = ({
? 'bg-th-bkg-3 cursor-not-allowed hover:border-th-fgd-4 text-th-fgd-3' ? 'bg-th-bkg-3 cursor-not-allowed hover:border-th-fgd-4 text-th-fgd-3'
: '' : ''
} }
${prefix ? 'rounded-l-none' : ''}`} ${prefix ? 'pl-7' : ''}`}
disabled={disabled} disabled={disabled}
{...props} {...props}
/> />

View File

@ -18,9 +18,6 @@ const Switch: FunctionComponent<SwitchProps> = ({
return ( return (
<div className={`flex items-center ${className}`}> <div className={`flex items-center ${className}`}>
<span className="mr-1">
<span className="">{children}</span>
</span>
<button <button
type="button" type="button"
className={`${ className={`${
@ -41,6 +38,9 @@ const Switch: FunctionComponent<SwitchProps> = ({
shadow transform ring-0 transition ease-in-out duration-200`} shadow transform ring-0 transition ease-in-out duration-200`}
></span> ></span>
</button> </button>
<span className="ml-2">
<span className="">{children}</span>
</span>
</div> </div>
) )
} }

View File

@ -12,7 +12,7 @@ import FloatingElement from '../components/FloatingElement'
import Orderbook from '../components/Orderbook' import Orderbook from '../components/Orderbook'
import AccountInfo from './AccountInfo' import AccountInfo from './AccountInfo'
import UserMarketInfo from './UserMarketInfo' import UserMarketInfo from './UserMarketInfo'
import TradeForm from './TradeForm' import TradeForm from './trade_form/TradeForm'
import UserInfo from './UserInfo' import UserInfo from './UserInfo'
import RecentMarketTrades from './RecentMarketTrades' import RecentMarketTrades from './RecentMarketTrades'
import useMangoStore from '../stores/useMangoStore' import useMangoStore from '../stores/useMangoStore'

View File

@ -1,97 +0,0 @@
import { Listbox } from '@headlessui/react'
import styled from '@emotion/styled'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
const StyledListbox = styled(Listbox.Button)`
border-left: 1px solid transparent;
`
const TradeType = ({
value,
onChange,
offerTriggers = false,
className = '',
}) => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const TRADE_TYPES = ['Limit', 'Market']
if (offerTriggers)
TRADE_TYPES.push(
'Stop Loss',
'Stop Limit',
'Take Profit',
'Take Profit Limit'
)
return (
<div className={`relative ${className}`}>
{!isMobile ? (
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<>
<StyledListbox
className={`font-normal h-full w-full bg-th-bkg-1 border border-th-fgd-4 hover:border-th-primary rounded rounded-l-none focus:outline-none focus:border-th-primary`}
>
<div
className={`flex items-center justify-between space-x-4 pl-2 pr-1`}
>
<span>{value}</span>
{open ? (
<ChevronUpIcon className={`h-5 w-5 mr-1 text-th-primary`} />
) : (
<ChevronDownIcon
className={`h-5 w-5 mr-1 text-th-primary`}
/>
)}
</div>
</StyledListbox>
{open ? (
<Listbox.Options
static
className={`z-20 w-full p-1 absolute left-0 mt-1 bg-th-bkg-1 origin-top-left divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md`}
>
{TRADE_TYPES.map((type) => (
<Listbox.Option key={type} value={type}>
{({ selected }) => (
<div
className={`p-2 hover:bg-th-bkg-2 hover:cursor-pointer tracking-wider ${
selected && `text-th-primary`
}`}
>
{type}
</div>
)}
</Listbox.Option>
))}
</Listbox.Options>
) : null}
</>
)}
</Listbox>
) : (
<div className="flex">
{TRADE_TYPES.slice(0, 2).map((tradeType) => (
<div
key={tradeType}
className={`px-2 py-2 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${
value === tradeType
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => onChange(tradeType)}
>
{tradeType}
</div>
))}
</div>
)}
</div>
)
}
export default TradeType

View File

@ -1,12 +1,12 @@
import { useState } from 'react' import { useMemo, useState } from 'react'
import { Disclosure } from '@headlessui/react' import { Disclosure } from '@headlessui/react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { XIcon } from '@heroicons/react/outline' import { XIcon } from '@heroicons/react/outline'
import useMangoStore from '../../stores/useMangoStore' import useMangoStore from '../../stores/useMangoStore'
import { PerpMarket } from '@blockworks-foundation/mango-client' import { getWeights, PerpMarket } from '@blockworks-foundation/mango-client'
import { CandlesIcon } from '../icons' import { CandlesIcon } from '../icons'
import SwipeableTabs from './SwipeableTabs' import SwipeableTabs from './SwipeableTabs'
import TradeForm from '../TradeForm' import AdvancedTradeForm from '../trade_form/AdvancedTradeForm'
import Orderbook from '../Orderbook' import Orderbook from '../Orderbook'
import MarketBalances from '../MarketBalances' import MarketBalances from '../MarketBalances'
import MarketDetails from '../MarketDetails' import MarketDetails from '../MarketDetails'
@ -25,6 +25,7 @@ const MobileTradePage = () => {
const [viewIndex, setViewIndex] = useState(0) const [viewIndex, setViewIndex] = useState(0)
const selectedMarket = useMangoStore((s) => s.selectedMarket.current) const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
const marketConfig = useMangoStore((s) => s.selectedMarket.config) const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const connected = useMangoStore((s) => s.wallet.connected) const connected = useMangoStore((s) => s.wallet.connected)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config) const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const baseSymbol = marketConfig.baseSymbol const baseSymbol = marketConfig.baseSymbol
@ -34,6 +35,15 @@ const MobileTradePage = () => {
setViewIndex(index) setViewIndex(index)
} }
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 TABS = const TABS =
selectedMarket instanceof PerpMarket selectedMarket instanceof PerpMarket
? ['Trade', 'Details', 'Position', 'Orders'] ? ['Trade', 'Details', 'Position', 'Orders']
@ -59,6 +69,9 @@ const MobileTradePage = () => {
{isPerpMarket ? 'PERP' : groupConfig.quoteSymbol} {isPerpMarket ? 'PERP' : groupConfig.quoteSymbol}
</div> </div>
</div> </div>
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
{initLeverage}x
</span>
</div> </div>
<Disclosure> <Disclosure>
{({ open }) => ( {({ open }) => (
@ -72,7 +85,7 @@ const MobileTradePage = () => {
)} )}
</div> </div>
</Disclosure.Button> </Disclosure.Button>
<Disclosure.Panel className="pt-3"> <Disclosure.Panel>
<div className="bg-th-bkg-2 h-96 mb-2 p-2 rounded-lg"> <div className="bg-th-bkg-2 h-96 mb-2 p-2 rounded-lg">
<TVChartContainer /> <TVChartContainer />
</div> </div>
@ -90,7 +103,7 @@ const MobileTradePage = () => {
<div> <div>
<div className="bg-th-bkg-2 grid grid-cols-12 grid-rows-1 gap-4 mb-2 px-2 py-3 rounded-lg"> <div className="bg-th-bkg-2 grid grid-cols-12 grid-rows-1 gap-4 mb-2 px-2 py-3 rounded-lg">
<div className="col-span-7"> <div className="col-span-7">
<TradeForm /> <AdvancedTradeForm />
</div> </div>
<div className="col-span-5"> <div className="col-span-5">
<Orderbook depth={8} /> <Orderbook depth={8} />

View File

@ -0,0 +1,789 @@
import { useMemo, useState, useEffect, useRef } from 'react'
import useIpAddress from '../../hooks/useIpAddress'
import {
getMarketIndexBySymbol,
getTokenBySymbol,
I80F48,
nativeI80F48ToUi,
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 Input from '../Input'
import { Market } from '@project-serum/serum'
import Big from 'big.js'
import MarketFee from '../MarketFee'
import Loading from '../Loading'
import Tooltip from '../Tooltip'
import OrderSideTabs from './OrderSideTabs'
import { ElementTitle } from '../styles'
import ButtonGroup from '../ButtonGroup'
import SlippageWarning from './SlippageWarning'
import Checkbox from '../Checkbox'
import { useViewport } from '../../hooks/useViewport'
import { breakpoints } from '../TradePageGrid'
interface AdvancedTradeFormProps {
initLeverage?: number
}
export default function AdvancedTradeForm({
initLeverage,
}: AdvancedTradeFormProps) {
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 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 [spotMargin, setSpotMargin] = useState(false)
const [positionSizePercent, setPositionSizePercent] = useState('')
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const marketIndex = getMarketIndexBySymbol(
groupConfig,
marketConfig.baseSymbol
)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const {
side,
baseSize,
quoteSize,
price,
tradeType,
triggerPrice,
triggerCondition,
} = useMangoStore((s) => s.tradeForm)
const isLimitOrder = ['Limit', 'Stop Limit', 'Take Profit Limit'].includes(
tradeType
)
const isMarketOrder = ['Market', 'Stop Loss', 'Take Profit'].includes(
tradeType
)
const isTriggerOrder = [
'Stop Loss',
'Stop Limit',
'Take Profit',
'Take Profit Limit',
].includes(tradeType)
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])
useEffect(() => {
let condition
switch (tradeType) {
case 'Stop Loss':
case 'Stop Limit':
condition = side == 'buy' ? 'above' : 'below'
break
case 'Take Profit':
case 'Take Profit Limit':
condition = side == 'buy' ? 'below' : 'above'
break
}
if (condition) {
set((s) => {
s.tradeForm.triggerCondition = condition
})
}
}, [set, tradeType, side])
const { max, deposits, borrows, spotMax } = useMemo(() => {
if (!mangoAccount) return { max: 0 }
const priceOrDefault = price
? I80F48.fromNumber(price)
: mangoGroup.getPrice(marketIndex, mangoCache)
const token =
side === 'buy'
? getTokenBySymbol(groupConfig, 'USDC')
: getTokenBySymbol(groupConfig, marketConfig.baseSymbol)
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
const availableBalance = floorToDecimal(
nativeI80F48ToUi(
mangoAccount.getAvailableBalance(mangoGroup, mangoCache, tokenIndex),
token.decimals
).toNumber(),
token.decimals
)
const spotMax =
side === 'buy'
? availableBalance / priceOrDefault.toNumber()
: availableBalance
const {
max: maxQuote,
deposits,
borrows,
} = mangoAccount.getMaxLeverageForMarket(
mangoGroup,
mangoCache,
marketIndex,
market,
side,
priceOrDefault
)
if (maxQuote.toNumber() <= 0) return { max: 0 }
// multiply the maxQuote by a scaler value to account for
// srm fees or rounding issues in getMaxLeverageForMarket
const maxScaler = market instanceof PerpMarket ? 0.99 : 0.95
const scaledMax = price
? (maxQuote.toNumber() * maxScaler) / price
: (maxQuote.toNumber() * maxScaler) /
mangoGroup.getPrice(marketIndex, mangoCache).toNumber()
return { max: scaledMax, deposits, borrows, spotMax }
}, [mangoAccount, mangoGroup, mangoCache, marketIndex, market, side, price])
const onChangeSide = (side) => {
setPositionSizePercent('')
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 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', 'Stop Loss', 'Take Profit'].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)
}
const marginOnChange = (checked) => {
setPositionSizePercent('')
setSpotMargin(checked)
}
const handleSetPositionSize = (percent) => {
setPositionSizePercent(percent)
const baseSizeMax =
spotMargin || marketConfig.kind === 'perp' ? max : spotMax
const baseSize = baseSizeMax * (parseInt(percent) / 100)
const step = parseFloat(minOrderSize)
const roundedSize = (Math.round(baseSize / step) * step).toFixed(
sizeDecimalCount
)
setBaseSize(parseFloat(roundedSize))
const usePrice = Number(price) || markPrice
if (!usePrice) {
setQuoteSize('')
}
const rawQuoteSize = parseFloat(roundedSize) * usePrice
setQuoteSize(rawQuoteSize.toFixed(6))
}
const percentToClose = (size, total) => {
return (size / total) * 100
}
const roundedDeposits = parseFloat(deposits?.toFixed(sizeDecimalCount))
const roundedBorrows = parseFloat(borrows?.toFixed(sizeDecimalCount))
const closeDepositString =
percentToClose(baseSize, roundedDeposits) > 100
? `100% close position and open a ${(+baseSize - roundedDeposits).toFixed(
sizeDecimalCount
)} ${marketConfig.baseSymbol} short`
: `${percentToClose(baseSize, roundedDeposits).toFixed(
0
)}% close position`
const closeBorrowString =
percentToClose(baseSize, roundedBorrows) > 100
? `100% close position and open a ${(+baseSize - roundedBorrows).toFixed(
sizeDecimalCount
)} ${marketConfig.baseSymbol} short`
: `${percentToClose(baseSize, roundedBorrows).toFixed(0)}% close position`
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,
isMarketOrder ? 'market' : orderType,
side,
orderPrice,
baseSize,
triggerCondition,
Number(triggerPrice)
)
} else {
txid = await mangoClient.placePerpOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
isMarketOrder ? 'market' : orderType,
Date.now(),
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',
})
console.error(e)
} finally {
// TODO: should be removed, main issue are newly created OO accounts
// await sleep(600)
actions.reloadMangoAccount()
actions.loadMarketFills()
setSubmitting(false)
}
}
const roundedMax = (
Math.round(max / parseFloat(minOrderSize)) * parseFloat(minOrderSize)
).toFixed(sizeDecimalCount)
const sizeTooLarge =
spotMargin || marketConfig.kind === 'perp'
? baseSize > roundedMax
: baseSize > spotMax
const disabledTradeButton =
(!price && isLimitOrder) ||
!baseSize ||
!connected ||
submitting ||
!mangoAccount ||
sizeTooLarge
return (
<div className="flex flex-col h-full">
<ElementTitle className="hidden md:flex">
{marketConfig.name}
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
{initLeverage}x
</span>
</ElementTitle>
<OrderSideTabs onChange={onChangeSide} side={side} />
<div className="grid grid-cols-12 md:gap-2 text-left">
<div className="col-span-12 md:col-span-2 flex items-center">
<label className="text-xxs md:text-xs text-th-fgd-3">Type</label>
</div>
<div className="col-span-12 md:col-span-10 pb-2 md:pb-0">
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
offerTriggers={isPerpMarket}
/>
</div>
<div className="col-span-12 md:col-span-2 flex items-center">
<label className="text-xxs md:text-xs text-th-fgd-3">Price</label>
</div>
<div className="col-span-12 md:col-span-10 pb-2 md:pb-0">
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={isMarketOrder}
placeholder={tradeType === 'Market' ? markPrice : null}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
{isTriggerOrder && (
<>
<div className="col-span-12 md:col-span-2 flex items-center">
<label className="text-xxs md:text-xs text-th-fgd-3">
Trigger Price
</label>
</div>
<div className="col-span-12 md:col-span-10 pb-2 md:pb-0">
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => setTriggerPrice(e.target.value)}
value={triggerPrice}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
</>
)}
<div className="col-span-12 md:col-span-2 flex items-center">
<label className="text-xxs md:text-xs text-th-fgd-3">Size</label>
</div>
<div className="col-span-12 md:col-span-10 pb-2 md:pb-0">
<Input.Group className="-mb-1">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
wrapperClassName="mr-0.5 w-1/2"
prefix={
<img
src={`/assets/icons/${marketConfig.baseSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
wrapperClassName="ml-0.5 w-1/2"
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</Input.Group>
</div>
<div className="col-span-12 md:col-span-10 md:col-start-3">
<ButtonGroup
activeValue={positionSizePercent}
onChange={(p) => handleSetPositionSize(p)}
unit="%"
values={
isMobile
? ['10', '25', '50', '75']
: ['10', '25', '50', '75', '100']
}
/>
{marketConfig.kind === 'perp' ? (
side === 'sell' ? (
roundedDeposits > 0 ? (
<div className="text-th-fgd-3 text-xs tracking-normal mt-2">
<span>{closeDepositString}</span>
</div>
) : null
) : roundedBorrows > 0 ? (
<div className="text-th-fgd-3 text-xs tracking-normal mt-2">
<span>{closeBorrowString}</span>
</div>
) : null
) : null}
<div className="sm:flex">
{isLimitOrder ? (
<div className="flex">
<div className="mr-4 mt-4">
<Tooltip
className="hidden md:block"
delay={250}
placement="left"
content="Post only orders are guaranteed to be the maker order or else it will be canceled."
>
<Checkbox
checked={postOnly}
onChange={(e) => postOnChange(e.target.checked)}
>
POST
</Checkbox>
</Tooltip>
</div>
<div className="mr-4 mt-4">
<Tooltip
className="hidden md:block"
delay={250}
placement="left"
content="Immediate or cancel orders are guaranteed to be the taker or it will be canceled."
>
<div className="flex items-center text-th-fgd-3 text-xs">
<Checkbox
checked={ioc}
onChange={(e) => iocOnChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
</Tooltip>
</div>
</div>
) : null}
{marketConfig.kind === 'perp' && !isTriggerOrder ? (
<div className="mt-4">
<Tooltip
className="hidden md:block"
delay={250}
placement="left"
content="Reduce only orders will only reduce your overall position."
>
<Checkbox
checked={reduceOnly}
onChange={(e) => reduceOnChange(e.target.checked)}
>
Reduce Only
</Checkbox>
</Tooltip>
</div>
) : null}
{marketConfig.kind === 'spot' ? (
<div className="mt-4">
<Tooltip
delay={250}
placement="left"
content="Enable spot margin for this trade"
>
<Checkbox
checked={spotMargin}
onChange={(e) => marginOnChange(e.target.checked)}
>
Margin
</Checkbox>
</Tooltip>
</div>
) : null}
</div>
<div className={`flex pt-4`}>
{ipAllowed ? (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`bg-th-bkg-2 border ${
!disabledTradeButton
? side === 'buy'
? 'border-th-green hover:border-th-green-dark text-th-green hover:bg-th-green-dark'
: 'border-th-red hover:border-th-red-dark text-th-red hover:bg-th-red-dark'
: 'border border-th-bkg-4'
} hover:text-th-fgd-1 flex-grow`}
>
{submitting ? (
<div className="w-full">
<Loading className="mx-auto" />
</div>
) : sizeTooLarge ? (
'Size Too Large'
) : side === 'buy' ? (
market instanceof PerpMarket ? (
<>
{baseSize > 0 ? 'Long ' + baseSize : 'Long '}{' '}
<span className="whitespace-nowrap">
{marketConfig.name}
</span>
</>
) : (
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
marketConfig.baseSymbol
}`
)
) : market instanceof PerpMarket ? (
sizeTooLarge ? (
'Size Too Large'
) : (
<>
{baseSize > 0 ? 'Short ' + baseSize : 'Short '}{' '}
<span className="whitespace-nowrap">
{marketConfig.name}
</span>
</>
)
) : (
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
marketConfig.baseSymbol
}`
)}
</Button>
) : (
<Button disabled className="flex-grow">
<span>Country Not Allowed</span>
</Button>
)}
</div>
{tradeType === 'Market' ? (
<div className="col-span-12 md:col-span-10 md:col-start-3 pt-2">
<SlippageWarning slippage={0.2} />
</div>
) : null}
<div className="flex text-xs text-th-fgd-4 pt-4">
<MarketFee />
</div>
</div>
</div>
</div>
)
}

View File

@ -1,19 +1,21 @@
import { FunctionComponent } from 'react' import { FunctionComponent } from 'react'
import { PerpMarket } from '@blockworks-foundation/mango-client' import { PerpMarket } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore' import useMangoStore from '../../stores/useMangoStore'
interface OrderSideTabsProps { interface OrderSideTabsProps {
isSimpleForm?: boolean
onChange: (x) => void onChange: (x) => void
side: string side: string
} }
const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
side, isSimpleForm,
onChange, onChange,
side,
}) => { }) => {
const market = useMangoStore((s) => s.selectedMarket.current) const market = useMangoStore((s) => s.selectedMarket.current)
return ( return (
<div className={`border-b border-th-fgd-4 mb-4 relative`}> <div className={`border-b border-th-fgd-4 mb-2 md:mb-4 relative`}>
<div <div
className={`absolute ${ className={`absolute ${
side === 'buy' side === 'buy'
@ -24,7 +26,7 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
<nav className="-mb-px flex" aria-label="Tabs"> <nav className="-mb-px flex" aria-label="Tabs">
<button <button
onClick={() => onChange('buy')} onClick={() => onChange('buy')}
className={`cursor-pointer default-transition flex font-semibold items-center justify-center p-2 relative text-base w-1/2 whitespace-nowrap hover:opacity-100 className={`cursor-pointer default-transition flex font-semibold items-center justify-center pb-2 md:py-2 relative text-base w-1/2 whitespace-nowrap hover:opacity-100
${ ${
side === 'buy' side === 'buy'
? `text-th-green` ? `text-th-green`
@ -32,11 +34,11 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
} }
`} `}
> >
{market instanceof PerpMarket ? 'Long' : 'Buy'} {market instanceof PerpMarket && isSimpleForm ? 'Long' : 'Buy'}
</button> </button>
<button <button
onClick={() => onChange('sell')} onClick={() => onChange('sell')}
className={`cursor-pointer default-transition flex font-semibold items-center justify-center p-2 relative text-base w-1/2 whitespace-nowrap hover:opacity-100 className={`cursor-pointer default-transition flex font-semibold items-center justify-center pb-2 relative text-base w-1/2 whitespace-nowrap hover:opacity-100
${ ${
side === 'sell' side === 'sell'
? `text-th-red` ? `text-th-red`
@ -44,7 +46,7 @@ const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
} }
`} `}
> >
{market instanceof PerpMarket ? 'Short' : 'Sell'} {market instanceof PerpMarket && isSimpleForm ? 'Short' : 'Sell'}
</button> </button>
</nav> </nav>
</div> </div>

View File

@ -1,30 +1,27 @@
import { useState, useEffect, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useMemo } from 'react'
import useIpAddress from '../hooks/useIpAddress' import useIpAddress from '../../hooks/useIpAddress'
import { import {
getTokenBySymbol, getTokenBySymbol,
getMarketIndexBySymbol, getMarketIndexBySymbol,
I80F48, I80F48,
PerpMarket, PerpMarket,
} from '@blockworks-foundation/mango-client' } from '@blockworks-foundation/mango-client'
import { notify } from '../utils/notifications' import { notify } from '../../utils/notifications'
import { calculateTradePrice, getDecimalCount, sleep } from '../utils' import { calculateTradePrice, getDecimalCount, sleep } from '../../utils'
import { floorToDecimal } from '../utils/index' import { floorToDecimal } from '../../utils/index'
import useMangoStore from '../stores/useMangoStore' import useMangoStore from '../../stores/useMangoStore'
import Button from './Button' import Button from '../Button'
import TradeType from './TradeType' import Input from '../Input'
import Input from './Input'
import Switch from './Switch'
import { Market } from '@project-serum/serum' import { Market } from '@project-serum/serum'
import Big from 'big.js' import Big from 'big.js'
import MarketFee from './MarketFee' import MarketFee from '../MarketFee'
import LeverageSlider from './LeverageSlider' import Loading from '../Loading'
import Loading from './Loading' import { ElementTitle } from '../styles'
import { useViewport } from '../hooks/useViewport' import ButtonGroup from '../ButtonGroup'
import { breakpoints } from './TradePageGrid' import Checkbox from '../Checkbox'
import { ElementTitle } from './styles'
import ButtonGroup from './ButtonGroup'
import Checkbox from './Checkbox'
import OrderSideTabs from './OrderSideTabs' import OrderSideTabs from './OrderSideTabs'
import Tooltip from '../Tooltip'
import SlippageWarning from './SlippageWarning'
export default function SimpleTradeForm({ initLeverage }) { export default function SimpleTradeForm({ initLeverage }) {
const set = useMangoStore((s) => s.set) const set = useMangoStore((s) => s.set)
@ -42,10 +39,8 @@ export default function SimpleTradeForm({ initLeverage }) {
marketConfig.baseSymbol marketConfig.baseSymbol
) )
const market = useMangoStore((s) => s.selectedMarket.current) const market = useMangoStore((s) => s.selectedMarket.current)
const { side, baseSize, quoteSize, price, stopPrice, tradeType } = const { side, baseSize, quoteSize, price, triggerPrice, tradeType } =
useMangoStore((s) => s.tradeForm) useMangoStore((s) => s.tradeForm)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const [postOnly, setPostOnly] = useState(false) const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false) const [ioc, setIoc] = useState(false)
@ -54,6 +49,8 @@ export default function SimpleTradeForm({ initLeverage }) {
const [showStopForm, setShowStopForm] = useState(false) const [showStopForm, setShowStopForm] = useState(false)
const [showTakeProfitForm, setShowTakeProfitForm] = useState(false) const [showTakeProfitForm, setShowTakeProfitForm] = useState(false)
const [stopSizePercent, setStopSizePercent] = useState('5%') const [stopSizePercent, setStopSizePercent] = useState('5%')
const [reduceOnly, setReduceOnly] = useState(false)
const [spotMargin, setSpotMargin] = useState(false)
const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook) const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook)
const orderbook = orderBookRef.current const orderbook = orderBookRef.current
@ -67,6 +64,12 @@ export default function SimpleTradeForm({ initLeverage }) {
[] []
) )
useEffect(() => {
if (tradeType !== 'Market' && tradeType !== 'Limit') {
setTradeType('Limit')
}
}, [])
useEffect(() => { useEffect(() => {
if (tradeType === 'Market') { if (tradeType === 'Market') {
set((s) => { set((s) => {
@ -110,9 +113,9 @@ export default function SimpleTradeForm({ initLeverage }) {
const setStopPrice = (price) => const setStopPrice = (price) =>
set((s) => { set((s) => {
if (!Number.isNaN(parseFloat(price))) { if (!Number.isNaN(parseFloat(price))) {
s.tradeForm.stopPrice = parseFloat(price) s.tradeForm.triggerPrice = parseFloat(price)
} else { } else {
s.tradeForm.stopPrice = price s.tradeForm.triggerPrice = price
} }
}) })
@ -237,6 +240,12 @@ export default function SimpleTradeForm({ initLeverage }) {
} }
setIoc(checked) setIoc(checked)
} }
const reduceOnChange = (checked) => {
if (checked) {
setReduceOnly(false)
}
setReduceOnly(checked)
}
async function onSubmit() { async function onSubmit() {
if (!price && tradeType === 'Limit') { if (!price && tradeType === 'Limit') {
@ -406,16 +415,16 @@ export default function SimpleTradeForm({ initLeverage }) {
(side === 'sell' && baseSize === roundedDeposits) || (side === 'sell' && baseSize === roundedDeposits) ||
(side === 'buy' && baseSize === roundedBorrows) (side === 'buy' && baseSize === roundedBorrows)
return !isMobile ? ( return (
<div className={!connected ? 'fliter blur-sm' : 'flex flex-col h-full'}> <div className="flex flex-col h-full">
<ElementTitle> <ElementTitle>
{marketConfig.name} {marketConfig.name}
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary"> <span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
{initLeverage}x {initLeverage}x
</span> </span>
</ElementTitle> </ElementTitle>
<OrderSideTabs onChange={setSide} side={side} /> <OrderSideTabs isSimpleForm onChange={setSide} side={side} />
<div className="grid grid-cols-12 gap-2"> <div className="grid grid-cols-12 gap-2 text-left">
<div className="col-span-2 flex items-center"> <div className="col-span-2 flex items-center">
<label className="text-xs text-th-fgd-3">Type</label> <label className="text-xs text-th-fgd-3">Type</label>
</div> </div>
@ -437,7 +446,7 @@ export default function SimpleTradeForm({ initLeverage }) {
onChange={(e) => onSetPrice(e.target.value)} onChange={(e) => onSetPrice(e.target.value)}
value={price} value={price}
disabled={tradeType === 'Market'} disabled={tradeType === 'Market'}
placeholder={tradeType === 'Market' ? 'Market Price' : null} placeholder={tradeType === 'Market' ? markPrice : null}
prefix={ prefix={
<img <img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`} src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
@ -484,7 +493,7 @@ export default function SimpleTradeForm({ initLeverage }) {
/> />
</Input.Group> </Input.Group>
</div> </div>
<div className="col-span-10 col-start-3"> <div className="col-span-10 col-start-3 pb-2">
<ButtonGroup <ButtonGroup
activeValue={positionSizePercent} activeValue={positionSizePercent}
onChange={(p) => handleSetPositionSize(p)} onChange={(p) => handleSetPositionSize(p)}
@ -500,31 +509,33 @@ export default function SimpleTradeForm({ initLeverage }) {
<span>{roundedBorrows > 0 ? closeBorrowString : null}</span> <span>{roundedBorrows > 0 ? closeBorrowString : null}</span>
</div> </div>
)} )}
<div className="flex items-center"> <div className="flex items-center space-x-1">
{!hideProfitStop ? ( {!hideProfitStop ? (
<div className="pr-4 pt-3"> <div
<label className="cursor-pointer flex items-center"> className={`${
showStopForm ? 'bg-th-bkg-4' : 'bg-th-bkg-3'
} mt-2 p-2 rounded-md w-1/2`}
>
<Checkbox <Checkbox
checked={showStopForm} checked={showStopForm}
onChange={(e) => setShowStopForm(e.target.checked)} onChange={(e) => setShowStopForm(e.target.checked)}
/> >
<span className="ml-2 text-xs text-th-fgd-3">
Set Stop Loss Set Stop Loss
</span> </Checkbox>
</label>
</div> </div>
) : null} ) : null}
{!hideProfitStop ? ( {!hideProfitStop ? (
<div className="pt-3"> <div
<label className="cursor-pointer flex items-center"> className={`${
showTakeProfitForm ? 'bg-th-bkg-4' : 'bg-th-bkg-3'
} mt-2 p-2 rounded-md w-1/2`}
>
<Checkbox <Checkbox
checked={showTakeProfitForm} checked={showTakeProfitForm}
onChange={(e) => setShowTakeProfitForm(e.target.checked)} onChange={(e) => setShowTakeProfitForm(e.target.checked)}
/> >
<span className="ml-2 text-xs text-th-fgd-3">
Set Take Profit Set Take Profit
</span> </Checkbox>
</label>
</div> </div>
) : null} ) : null}
</div> </div>
@ -540,7 +551,7 @@ export default function SimpleTradeForm({ initLeverage }) {
min="0" min="0"
step={tickSize} step={tickSize}
onChange={(e) => setStopPrice(e.target.value)} onChange={(e) => setStopPrice(e.target.value)}
value={stopPrice} value={triggerPrice}
prefix={ prefix={
<img <img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`} src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
@ -550,7 +561,7 @@ export default function SimpleTradeForm({ initLeverage }) {
} }
/> />
</div> </div>
<div className="col-span-10 col-start-3"> <div className="col-span-10 col-start-3 pb-2">
<ButtonGroup <ButtonGroup
activeValue={stopSizePercent} activeValue={stopSizePercent}
onChange={(p) => setStopSizePercent(p)} onChange={(p) => setStopSizePercent(p)}
@ -563,7 +574,7 @@ export default function SimpleTradeForm({ initLeverage }) {
<> <>
<div className="col-span-2 flex items-center"> <div className="col-span-2 flex items-center">
<label className="text-left text-xs text-th-fgd-3"> <label className="text-left text-xs text-th-fgd-3">
Take Profit Price Profit Price
</label> </label>
</div> </div>
<div className="col-span-10 -mb-1"> <div className="col-span-10 -mb-1">
@ -572,7 +583,7 @@ export default function SimpleTradeForm({ initLeverage }) {
min="0" min="0"
step={tickSize} step={tickSize}
onChange={(e) => setStopPrice(e.target.value)} onChange={(e) => setStopPrice(e.target.value)}
value={stopPrice} value={triggerPrice}
prefix={ prefix={
<img <img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`} src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
@ -582,7 +593,7 @@ export default function SimpleTradeForm({ initLeverage }) {
} }
/> />
</div> </div>
<div className="col-span-10 col-start-3"> <div className="col-span-10 col-start-3 pb-2">
<ButtonGroup <ButtonGroup
activeValue={stopSizePercent} activeValue={stopSizePercent}
onChange={(p) => setStopSizePercent(p)} onChange={(p) => setStopSizePercent(p)}
@ -591,7 +602,71 @@ export default function SimpleTradeForm({ initLeverage }) {
</div> </div>
</> </>
) : null} ) : null}
<div className={`col-span-10 col-start-3 flex py-3`}> <div className="col-span-10 col-start-3 flex">
{tradeType === 'Limit' ? (
<>
<div className="mr-4">
<Tooltip
delay={250}
placement="left"
content="Post only orders are guaranteed to be the maker order or else it will be canceled."
>
<Checkbox
checked={postOnly}
onChange={(e) => postOnChange(e.target.checked)}
>
POST
</Checkbox>
</Tooltip>
</div>
<div className="mr-4">
<Tooltip
delay={250}
placement="left"
content="Immediate or cancel orders are guaranteed to be the taker or it will be canceled."
>
<div className="flex items-center text-th-fgd-3 text-xs">
<Checkbox
checked={ioc}
onChange={(e) => iocOnChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
</Tooltip>
</div>
</>
) : null}
{marketConfig.kind === 'perp' ? (
<Tooltip
delay={250}
placement="left"
content="Reduce only orders will only reduce your overall position."
>
<Checkbox
checked={reduceOnly}
onChange={(e) => reduceOnChange(e.target.checked)}
>
Reduce Only
</Checkbox>
</Tooltip>
) : null}
{marketConfig.kind === 'spot' ? (
<Tooltip
delay={250}
placement="left"
content="Enable spot margin for this trade"
>
<Checkbox
checked={spotMargin}
onChange={(e) => setSpotMargin(e.target.checked)}
>
Margin
</Checkbox>
</Tooltip>
) : null}
</div>
<div className={`col-span-10 col-start-3 flex pt-2`}>
{ipAllowed ? ( {ipAllowed ? (
<Button <Button
disabled={disabledTradeButton} disabled={disabledTradeButton}
@ -632,215 +707,15 @@ export default function SimpleTradeForm({ initLeverage }) {
</Button> </Button>
)} )}
</div> </div>
{!showStopForm ? ( {tradeType === 'Market' ? (
<div className="col-span-10 col-start-3 flex text-xs text-th-fgd-4"> <div className="col-span-10 col-start-3">
<MarketFee /> <SlippageWarning slippage={0.2} />
</div> </div>
) : null} ) : null}
</div> <div className="col-span-10 col-start-3 flex pt-2 text-xs text-th-fgd-4">
{/* <LeverageSlider
onChange={(e) => onSetBaseSize(e)}
value={baseSize ? baseSize : 0}
step={parseFloat(minOrderSize)}
disabled={false}
side={side}
decimalCount={sizeDecimalCount}
price={calculateTradePrice(
tradeType,
orderbook,
baseSize ? baseSize : 0,
side,
price
)}
/> */}
{/* {tradeType !== 'Market' ? (
<div className="flex mt-2">
<Switch checked={postOnly} onChange={postOnChange}>
POST
</Switch>
<div className="ml-4">
<Switch checked={ioc} onChange={iocOnChange}>
IOC
</Switch>
</div>
</div>
) : null} */}
</div>
) : (
<div className="flex flex-col h-full">
<div className={`flex pb-3 text-base text-th-fgd-4`}>
<button
onClick={() => setSide('buy')}
className={`flex-1 outline-none focus:outline-none`}
>
<div
className={`hover:text-th-green pb-1 transition-colors duration-500
${
side === 'buy'
? `text-th-green hover:text-th-green border-b-2 border-th-green`
: undefined
}`}
>
{market instanceof PerpMarket ? 'Long' : 'Buy'}
</div>
</button>
<button
onClick={() => setSide('sell')}
className={`flex-1 outline-none focus:outline-none`}
>
<div
className={`hover:text-th-red pb-1 transition-colors duration-500
${
side === 'sell'
? `text-th-red hover:text-th-red border-b-2 border-th-red`
: undefined
}
`}
>
{market instanceof PerpMarket ? 'Short' : 'Sell'}
</div>
</button>
</div>
<div className="pb-3">
<label className="block mb-1 text-th-fgd-3 text-xs">Price</label>
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={tradeType === 'Market'}
suffix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="flex items-center justify-between pb-3">
<label className="text-th-fgd-3 text-xs">Type</label>
<TradeType onChange={onTradeTypeChange} value={tradeType} />
</div>
<label className="block mb-1 text-th-fgd-3 text-xs">Size</label>
<div className="grid grid-cols-2 grid-rows-1 gap-2">
<div className="col-span-1">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
suffix={
<img
src={`/assets/icons/${marketConfig.baseSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-1">
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
suffix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
</div>
<LeverageSlider
onChange={(e) => onSetBaseSize(e)}
value={baseSize ? baseSize : 0}
step={parseFloat(minOrderSize)}
disabled={false}
side={side}
decimalCount={sizeDecimalCount}
price={calculateTradePrice(
tradeType,
orderbook,
baseSize ? baseSize : 0,
side,
price
)}
/>
{tradeType !== 'Market' ? (
<div className="flex mt-2">
<Switch checked={postOnly} onChange={postOnChange}>
POST
</Switch>
<div className="ml-4">
<Switch checked={ioc} onChange={iocOnChange}>
IOC
</Switch>
</div>
</div>
) : null}
<div className={`flex py-4`}>
{ipAllowed ? (
side === 'buy' ? (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`${
!disabledTradeButton
? 'bg-th-bkg-2 border border-th-green hover:border-th-green-dark'
: 'border border-th-bkg-4'
} text-th-green hover:text-th-fgd-1 hover:bg-th-green-dark flex-grow`}
>
{submitting ? (
<div className="w-full">
<Loading className="mx-auto" />
</div>
) : (
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
}`
)}
</Button>
) : (
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`${
!disabledTradeButton
? 'bg-th-bkg-2 border border-th-red hover:border-th-red-dark'
: 'border border-th-bkg-4'
} text-th-red hover:text-th-fgd-1 hover:bg-th-red-dark flex-grow`}
>
{submitting ? (
<div className="w-full">
<Loading className="mx-auto" />
</div>
) : (
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
marketConfig.name.includes('PERP')
? marketConfig.name
: marketConfig.baseSymbol
}`
)}
</Button>
)
) : (
<Button disabled className="flex-grow">
<span>Country Not Allowed</span>
</Button>
)}
</div>
<div className="flex text-xs text-th-fgd-4 px-6 mt-2.5">
<MarketFee /> <MarketFee />
</div> </div>
</div> </div>
</div>
) )
} }

View File

@ -0,0 +1,17 @@
const SlippageWarning = ({ slippage }) => {
return (
<div
className={`font-bold ${
slippage <= 0.5
? 'text-th-green'
: slippage > 0.5 && slippage <= 2
? 'text-th-orange'
: 'text-th-red'
} pt-2 rounded-md text-center text-th-fgd-3 text-xs`}
>
{slippage}% expected slippage
</div>
)
}
export default SlippageWarning

View File

@ -1,7 +1,7 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { SwitchHorizontalIcon } from '@heroicons/react/outline' import { SwitchHorizontalIcon } from '@heroicons/react/outline'
import { getWeights } from '@blockworks-foundation/mango-client' import { getWeights } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore' import useMangoStore from '../../stores/useMangoStore'
import AdvancedTradeForm from './AdvancedTradeForm' import AdvancedTradeForm from './AdvancedTradeForm'
import SimpleTradeForm from './SimpleTradeForm' import SimpleTradeForm from './SimpleTradeForm'
import { import {
@ -10,12 +10,13 @@ import {
FlipCardFront, FlipCardFront,
FlipCardInner, FlipCardInner,
StyledFloatingElement, StyledFloatingElement,
} from './FlipCard' } from '../FlipCard'
export default function TradeForm() { export default function TradeForm() {
const [showAdvancedFrom, setShowAdvancedForm] = useState(true) const [showAdvancedFrom, setShowAdvancedForm] = useState(true)
const marketConfig = useMangoStore((s) => s.selectedMarket.config) const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current) const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const connected = useMangoStore((s) => s.wallet.connected)
const handleFormChange = () => { const handleFormChange = () => {
setShowAdvancedForm(!showAdvancedFrom) setShowAdvancedForm(!showAdvancedFrom)
@ -35,26 +36,33 @@ export default function TradeForm() {
<FlipCardInner flip={showAdvancedFrom}> <FlipCardInner flip={showAdvancedFrom}>
{showAdvancedFrom ? ( {showAdvancedFrom ? (
<FlipCardFront> <FlipCardFront>
<StyledFloatingElement className="h-full"> <StyledFloatingElement
className="h-full px-1 py-0 md:px-4 md:py-4"
showConnect
>
<div className={`${!connected ? 'filter blur-sm' : ''}`}>
<button <button
onClick={handleFormChange} onClick={handleFormChange}
className="absolute flex items-center justify-center right-4 top-4 rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none" className="absolute hidden md:flex items-center justify-center right-4 rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
> >
<SwitchHorizontalIcon className="w-5 h-5" /> <SwitchHorizontalIcon className="w-5 h-5" />
</button> </button>
<AdvancedTradeForm initLeverage={initLeverage} /> <AdvancedTradeForm initLeverage={initLeverage} />
</div>
</StyledFloatingElement> </StyledFloatingElement>
</FlipCardFront> </FlipCardFront>
) : ( ) : (
<FlipCardBack> <FlipCardBack>
<StyledFloatingElement className="h-full"> <StyledFloatingElement className="h-full px-1 md:px-4" showConnect>
<div className={`${!connected ? 'filter blur-sm' : ''}`}>
<button <button
onClick={handleFormChange} onClick={handleFormChange}
className="absolute flex items-center justify-center right-4 top-4 rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none" className="absolute flex items-center justify-center right-4 rounded-full bg-th-bkg-3 w-8 h-8 hover:text-th-primary focus:outline-none"
> >
<SwitchHorizontalIcon className="w-5 h-5" /> <SwitchHorizontalIcon className="w-5 h-5" />
</button> </button>
<SimpleTradeForm initLeverage={initLeverage} /> <SimpleTradeForm initLeverage={initLeverage} />
</div>
</StyledFloatingElement> </StyledFloatingElement>
</FlipCardBack> </FlipCardBack>
)} )}

View File

@ -0,0 +1,63 @@
import { Listbox } from '@headlessui/react'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
const TradeType = ({
value,
onChange,
offerTriggers = false,
className = '',
}) => {
const TRADE_TYPES = ['Limit', 'Market']
if (offerTriggers)
TRADE_TYPES.push(
'Stop Loss',
'Stop Limit',
'Take Profit',
'Take Profit Limit'
)
return (
<div className={`relative ${className}`}>
<Listbox value={value} onChange={onChange}>
{({ open }) => (
<>
<Listbox.Button
className={`font-normal h-full w-full bg-th-bkg-1 border border-th-fgd-4 p-2 hover:border-th-primary rounded-md focus:outline-none focus:border-th-primary`}
>
<div className={`flex items-center justify-between space-x-4`}>
<span>{value}</span>
{open ? (
<ChevronUpIcon className={`h-5 w-5 mr-1 text-th-primary`} />
) : (
<ChevronDownIcon className={`h-5 w-5 mr-1 text-th-primary`} />
)}
</div>
</Listbox.Button>
{open ? (
<Listbox.Options
static
className={`z-20 w-full p-1 absolute left-0 mt-1 bg-th-bkg-1 origin-top-left divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md text-left`}
>
{TRADE_TYPES.map((type) => (
<Listbox.Option key={type} value={type}>
{({ selected }) => (
<div
className={`p-2 hover:bg-th-bkg-2 hover:cursor-pointer tracking-wider ${
selected && `text-th-primary`
}`}
>
{type}
</div>
)}
</Listbox.Option>
))}
</Listbox.Options>
) : null}
</>
)}
</Listbox>
</div>
)
}
export default TradeType

View File

@ -1,8 +1,8 @@
import { Listbox } from '@headlessui/react' import { Listbox } from '@headlessui/react'
import styled from '@emotion/styled' import styled from '@emotion/styled'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid' import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
import { useViewport } from '../hooks/useViewport' import { useViewport } from '../../hooks/useViewport'
import { breakpoints } from './TradePageGrid' import { breakpoints } from '../TradePageGrid'
const StyledListbox = styled(Listbox.Button)` const StyledListbox = styled(Listbox.Button)`
border-right: 1px solid transparent; border-right: 1px solid transparent;
@ -61,8 +61,8 @@ const TriggerType = ({ value, onChange, className = '' }) => {
</Listbox> </Listbox>
) : ( ) : (
<div className="flex"> <div className="flex">
{TRIGGER_TYPES.map((triggerType) => { {TRIGGER_TYPES.map((triggerType, i) => (
;<div <div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4 className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${ ${
value === triggerType value === triggerType
@ -70,11 +70,12 @@ const TriggerType = ({ value, onChange, className = '' }) => {
: `text-th-fgd-1 opacity-50 hover:opacity-100` : `text-th-fgd-1 opacity-50 hover:opacity-100`
} }
`} `}
key={`${triggerType}${i}`}
onClick={() => onChange(triggerType)} onClick={() => onChange(triggerType)}
> >
{triggerType} {triggerType}
</div> </div>
})} ))}
</div> </div>
)} )}
</div> </div>

View File

@ -10,10 +10,10 @@ import useMangoStore from '../stores/useMangoStore'
import { copyToClipboard } from '../utils' import { copyToClipboard } from '../utils'
import PageBodyContainer from '../components/PageBodyContainer' import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar' import TopBar from '../components/TopBar'
import AccountOrders from '../components/account-page/AccountOrders' import AccountOrders from '../components/account_page/AccountOrders'
import AccountHistory from '../components/account-page/AccountHistory' import AccountHistory from '../components/account_page/AccountHistory'
import AccountsModal from '../components/AccountsModal' import AccountsModal from '../components/AccountsModal'
import AccountOverview from '../components/account-page/AccountOverview' import AccountOverview from '../components/account_page/AccountOverview'
import AccountNameModal from '../components/AccountNameModal' import AccountNameModal from '../components/AccountNameModal'
import Button from '../components/Button' import Button from '../components/Button'
import EmptyState from '../components/EmptyState' import EmptyState from '../components/EmptyState'

View File

@ -5,7 +5,7 @@ import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar' import TopBar from '../components/TopBar'
import EmptyState from '../components/EmptyState' import EmptyState from '../components/EmptyState'
import AccountsModal from '../components/AccountsModal' import AccountsModal from '../components/AccountsModal'
import AccountBorrows from '../components/account-page/AccountBorrows' import AccountBorrows from '../components/account_page/AccountBorrows'
import Loading from '../components/Loading' import Loading from '../components/Loading'
export default function Borrow() { export default function Borrow() {

View File

@ -1,9 +1,9 @@
import { useState } from 'react' import { useState } from 'react'
import TopBar from '../components/TopBar' import TopBar from '../components/TopBar'
import PageBodyContainer from '../components/PageBodyContainer' import PageBodyContainer from '../components/PageBodyContainer'
import StatsTotals from '../components/stats-page/StatsTotals' import StatsTotals from '../components/stats_page/StatsTotals'
import StatsAssets from '../components/stats-page/StatsAssets' import StatsAssets from '../components/stats_page/StatsAssets'
import StatsPerps from '../components/stats-page/StatsPerps' import StatsPerps from '../components/stats_page/StatsPerps'
import useMangoStats from '../hooks/useMangoStats' import useMangoStats from '../hooks/useMangoStats'
import Swipeable from '../components/mobile/Swipeable' import Swipeable from '../components/mobile/Swipeable'
import SwipeableTabs from '../components/mobile/SwipeableTabs' import SwipeableTabs from '../components/mobile/SwipeableTabs'