2021-04-02 11:26:21 -07:00
|
|
|
import { useState, useEffect, useRef } from 'react'
|
2021-04-05 07:32:11 -07:00
|
|
|
import { Input, Radio, Switch, Select } from 'antd'
|
|
|
|
import xw from 'xwind'
|
2021-04-02 11:26:21 -07:00
|
|
|
import styled from '@emotion/styled'
|
2021-04-06 15:11:42 -07:00
|
|
|
import useMarket from '../hooks/useMarket'
|
2021-04-02 11:26:21 -07:00
|
|
|
import useWallet from '../hooks/useWallet'
|
|
|
|
import useIpAddress from '../hooks/useIpAddress'
|
|
|
|
import useConnection from '../hooks/useConnection'
|
|
|
|
import { PublicKey } from '@solana/web3.js'
|
|
|
|
import { IDS } from '@blockworks-foundation/mango-client'
|
|
|
|
import { notify } from '../utils/notifications'
|
|
|
|
import { placeAndSettle } from '../utils/mango'
|
|
|
|
import { calculateMarketPrice, getDecimalCount } from '../utils'
|
|
|
|
import FloatingElement from './FloatingElement'
|
|
|
|
import { roundToDecimal } from '../utils/index'
|
|
|
|
import useMarginAccount from '../hooks/useMarginAcccount'
|
|
|
|
import useMangoStore from '../stores/useMangoStore'
|
|
|
|
|
2021-04-05 07:32:11 -07:00
|
|
|
const SellButton = styled.button`
|
2021-04-02 11:26:21 -07:00
|
|
|
margin: 20px 0px 0px 0px;
|
|
|
|
background: #e54033;
|
|
|
|
border-color: #e54033;
|
|
|
|
`
|
|
|
|
|
2021-04-05 07:32:11 -07:00
|
|
|
const BuyButton = styled.button`
|
2021-04-02 11:26:21 -07:00
|
|
|
margin: 20px 0px 0px 0px;
|
|
|
|
color: #141026;
|
|
|
|
background: #9bd104;
|
|
|
|
border-color: #9bd104;
|
|
|
|
`
|
|
|
|
|
|
|
|
export default function TradeForm({
|
|
|
|
setChangeOrderRef,
|
|
|
|
}: {
|
|
|
|
setChangeOrderRef?: (
|
|
|
|
ref: ({ size, price }: { size?: number; price?: number }) => void
|
|
|
|
) => void
|
|
|
|
}) {
|
2021-04-06 15:11:42 -07:00
|
|
|
// console.log('reloading trade form')
|
2021-04-05 07:32:11 -07:00
|
|
|
|
2021-04-02 11:26:21 -07:00
|
|
|
const [side, setSide] = useState<'buy' | 'sell'>('buy')
|
2021-04-06 15:11:42 -07:00
|
|
|
const { baseCurrency, quoteCurrency, market } = useMarket()
|
2021-04-02 11:26:21 -07:00
|
|
|
const address = market?.publicKey
|
|
|
|
const { wallet, connected } = useWallet()
|
|
|
|
|
|
|
|
const { connection, cluster } = useConnection()
|
2021-04-05 07:32:11 -07:00
|
|
|
const { marginAccount, mangoGroup } = useMarginAccount()
|
|
|
|
const tradeForm = useMangoStore((s) => s.tradeForm)
|
2021-04-02 11:26:21 -07:00
|
|
|
|
|
|
|
const orderBookRef = useRef(useMangoStore.getState().market.orderBook)
|
|
|
|
const orderbook = orderBookRef.current[0]
|
|
|
|
useEffect(
|
|
|
|
() =>
|
|
|
|
useMangoStore.subscribe(
|
|
|
|
(orderBook) => (orderBookRef.current = orderBook as any[]),
|
|
|
|
(state) => state.market.orderBook
|
|
|
|
),
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
|
|
|
|
const markPriceRef = useRef(useMangoStore.getState().market.markPrice)
|
|
|
|
const markPrice = markPriceRef.current
|
|
|
|
useEffect(
|
|
|
|
() =>
|
|
|
|
useMangoStore.subscribe(
|
|
|
|
(markPrice) => (markPriceRef.current = markPrice as number),
|
|
|
|
(state) => state.market.markPrice
|
|
|
|
),
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
|
|
|
|
const { ipAllowed } = useIpAddress()
|
|
|
|
|
|
|
|
const [postOnly, setPostOnly] = useState(false)
|
|
|
|
const [ioc, setIoc] = useState(false)
|
|
|
|
const [baseSize, setBaseSize] = useState<number | undefined>(undefined)
|
|
|
|
const [quoteSize, setQuoteSize] = useState<number | undefined>(undefined)
|
|
|
|
const [price, setPrice] = useState<number | undefined>(undefined)
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
const [tradeType, setTradeType] = useState('Limit')
|
|
|
|
|
|
|
|
const sizeDecimalCount =
|
|
|
|
market?.minOrderSize && getDecimalCount(market.minOrderSize)
|
|
|
|
const priceDecimalCount = market?.tickSize && getDecimalCount(market.tickSize)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setChangeOrderRef && setChangeOrderRef(doChangeOrder)
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [setChangeOrderRef])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!price && markPrice && tradeType !== 'Market') {
|
|
|
|
setPrice(markPrice)
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [price, baseSize, quoteSize])
|
|
|
|
|
|
|
|
// Set the price from the balance comp
|
|
|
|
useEffect(() => {
|
|
|
|
if (tradeForm.currency) {
|
|
|
|
if (tradeForm.currency === baseCurrency) {
|
|
|
|
// onSetBaseSize(size.size);
|
|
|
|
} else {
|
|
|
|
// onSetQuoteSize(size.size);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [tradeForm])
|
|
|
|
|
|
|
|
const onSetBaseSize = (baseSize: number | undefined) => {
|
|
|
|
setBaseSize(baseSize)
|
|
|
|
if (!baseSize) {
|
|
|
|
setQuoteSize(undefined)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const usePrice = price || markPrice
|
|
|
|
if (!usePrice) {
|
|
|
|
setQuoteSize(undefined)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const rawQuoteSize = baseSize * usePrice
|
|
|
|
const quoteSize = baseSize && roundToDecimal(rawQuoteSize, sizeDecimalCount)
|
|
|
|
setQuoteSize(quoteSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
const onSetQuoteSize = (quoteSize: number | undefined) => {
|
|
|
|
setQuoteSize(quoteSize)
|
|
|
|
if (!quoteSize) {
|
|
|
|
setBaseSize(undefined)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const usePrice = price || markPrice
|
|
|
|
if (!usePrice) {
|
|
|
|
setBaseSize(undefined)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const rawBaseSize = quoteSize / usePrice
|
|
|
|
const baseSize = quoteSize && roundToDecimal(rawBaseSize, sizeDecimalCount)
|
|
|
|
setBaseSize(baseSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
const doChangeOrder = ({
|
|
|
|
size,
|
|
|
|
price,
|
|
|
|
}: {
|
|
|
|
size?: number
|
|
|
|
price?: number
|
|
|
|
}) => {
|
|
|
|
const formattedSize = size && roundToDecimal(size, sizeDecimalCount)
|
|
|
|
const formattedPrice = price && roundToDecimal(price, priceDecimalCount)
|
|
|
|
formattedSize && onSetBaseSize(formattedSize)
|
|
|
|
formattedPrice && setPrice(formattedPrice)
|
|
|
|
}
|
|
|
|
|
|
|
|
const postOnChange = (checked) => {
|
|
|
|
if (checked) {
|
|
|
|
setIoc(false)
|
|
|
|
}
|
|
|
|
setPostOnly(checked)
|
|
|
|
}
|
|
|
|
const iocOnChange = (checked) => {
|
|
|
|
if (checked) {
|
|
|
|
setPostOnly(false)
|
|
|
|
}
|
|
|
|
setIoc(checked)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onSubmit() {
|
|
|
|
if (!price && tradeType === 'Limit') {
|
|
|
|
console.warn('Missing price')
|
|
|
|
notify({
|
|
|
|
message: 'Missing price',
|
|
|
|
type: 'error',
|
|
|
|
})
|
|
|
|
return
|
|
|
|
} else if (!baseSize) {
|
|
|
|
console.warn('Missing size')
|
|
|
|
notify({
|
|
|
|
message: 'Missing size',
|
|
|
|
type: 'error',
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
console.log('checking if we can call place order', {
|
|
|
|
mangoGroup,
|
|
|
|
address,
|
|
|
|
marginAccount,
|
|
|
|
market,
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!mangoGroup || !address || !marginAccount || !market) return
|
|
|
|
setSubmitting(true)
|
|
|
|
|
|
|
|
try {
|
|
|
|
let calculatedPrice
|
|
|
|
if (tradeType === 'Market') {
|
|
|
|
calculatedPrice =
|
|
|
|
side === 'buy'
|
|
|
|
? calculateMarketPrice(orderbook.asks, tradeForm.size, side)
|
|
|
|
: calculateMarketPrice(orderbook.bids, tradeForm.size, side)
|
|
|
|
}
|
|
|
|
|
|
|
|
await placeAndSettle(
|
|
|
|
connection,
|
|
|
|
new PublicKey(IDS[cluster].mango_program_id),
|
|
|
|
mangoGroup,
|
2021-04-05 07:32:11 -07:00
|
|
|
// TODO:
|
|
|
|
// @ts-ignore
|
2021-04-02 11:26:21 -07:00
|
|
|
marginAccount,
|
|
|
|
market,
|
|
|
|
wallet,
|
|
|
|
side,
|
|
|
|
calculatedPrice ?? price,
|
|
|
|
baseSize,
|
|
|
|
ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit'
|
|
|
|
)
|
|
|
|
|
|
|
|
// refreshCache(tuple('getTokenAccounts', wallet, connected))
|
|
|
|
setPrice(undefined)
|
|
|
|
onSetBaseSize(undefined)
|
|
|
|
} catch (e) {
|
|
|
|
console.warn(e)
|
|
|
|
notify({
|
|
|
|
message: 'Error placing order',
|
|
|
|
description: e.message,
|
|
|
|
type: 'error',
|
|
|
|
})
|
|
|
|
} finally {
|
|
|
|
setSubmitting(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleTradeTypeChange = (tradeType) => {
|
|
|
|
setTradeType(tradeType)
|
|
|
|
if (tradeType === 'Market') {
|
|
|
|
setIoc(true)
|
|
|
|
setPrice(undefined)
|
|
|
|
} else {
|
|
|
|
const limitPrice =
|
|
|
|
side === 'buy' ? orderbook.asks[0][0] : orderbook.bids[0][0]
|
|
|
|
setPrice(limitPrice)
|
|
|
|
setIoc(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<FloatingElement>
|
|
|
|
<div>
|
|
|
|
<Radio.Group
|
|
|
|
onChange={(e) => setSide(e.target.value)}
|
|
|
|
value={side}
|
|
|
|
buttonStyle="solid"
|
|
|
|
style={{
|
|
|
|
marginBottom: 8,
|
|
|
|
width: '100%',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Radio.Button
|
|
|
|
value="buy"
|
|
|
|
style={{
|
|
|
|
width: '50%',
|
|
|
|
textAlign: 'center',
|
|
|
|
color: side === 'buy' ? '#141026' : '',
|
|
|
|
background: side === 'buy' ? '#AFD803' : '',
|
|
|
|
borderColor: side === 'buy' ? '#AFD803' : '',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
BUY
|
|
|
|
</Radio.Button>
|
|
|
|
<Radio.Button
|
|
|
|
className="sell-button"
|
|
|
|
value="sell"
|
|
|
|
style={{
|
|
|
|
width: '50%',
|
|
|
|
textAlign: 'center',
|
|
|
|
background: side === 'sell' ? '#E54033' : '',
|
|
|
|
borderColor: side === 'sell' ? '#E54033' : '',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
SELL
|
|
|
|
</Radio.Button>
|
|
|
|
</Radio.Group>
|
|
|
|
<Input.Group compact style={{ paddingBottom: 8 }}>
|
|
|
|
<Input
|
|
|
|
style={{
|
|
|
|
width: 'calc(50% + 30px)',
|
|
|
|
textAlign: 'right',
|
|
|
|
paddingBottom: 8,
|
|
|
|
}}
|
|
|
|
addonBefore={<div style={{ width: '30px' }}>Price</div>}
|
|
|
|
suffix={
|
|
|
|
<span style={{ fontSize: 10, opacity: 0.5 }}>
|
|
|
|
{quoteCurrency}
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
value={price}
|
|
|
|
type="number"
|
|
|
|
step={market?.tickSize || 1}
|
|
|
|
onChange={(e) => setPrice(parseFloat(e.target.value))}
|
|
|
|
disabled={tradeType === 'Market'}
|
|
|
|
/>
|
|
|
|
<Select
|
|
|
|
style={{ width: 'calc(50% - 30px)' }}
|
|
|
|
onChange={handleTradeTypeChange}
|
|
|
|
value={tradeType}
|
|
|
|
>
|
|
|
|
<Select.Option value="Limit">Limit</Select.Option>
|
|
|
|
<Select.Option value="Market">Market</Select.Option>
|
|
|
|
</Select>
|
|
|
|
</Input.Group>
|
|
|
|
<Input.Group compact style={{ paddingBottom: 8 }}>
|
|
|
|
<Input
|
|
|
|
style={{ width: 'calc(50% + 30px)', textAlign: 'right' }}
|
|
|
|
addonBefore={<div style={{ width: '30px' }}>Size</div>}
|
|
|
|
suffix={
|
|
|
|
<span style={{ fontSize: 10, opacity: 0.5 }}>{baseCurrency}</span>
|
|
|
|
}
|
|
|
|
value={baseSize}
|
|
|
|
type="number"
|
|
|
|
step={market?.minOrderSize || 1}
|
|
|
|
onChange={(e) => onSetBaseSize(parseFloat(e.target.value))}
|
|
|
|
/>
|
|
|
|
<Input
|
|
|
|
style={{ width: 'calc(50% - 30px)', textAlign: 'right' }}
|
|
|
|
suffix={
|
|
|
|
<span style={{ fontSize: 10, opacity: 0.5 }}>
|
|
|
|
{quoteCurrency}
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
value={quoteSize}
|
|
|
|
type="number"
|
|
|
|
step={market?.minOrderSize || 1}
|
|
|
|
onChange={(e) => onSetQuoteSize(parseFloat(e.target.value))}
|
|
|
|
/>
|
|
|
|
</Input.Group>
|
|
|
|
{/* {connected && marginInfo.prices.length ? (
|
|
|
|
<StyledSlider
|
|
|
|
value={sizeFraction}
|
|
|
|
onChange={onSliderChange}
|
|
|
|
min={getSliderMin()}
|
|
|
|
max={getSliderMax()}
|
|
|
|
step={getSliderStep()}
|
|
|
|
tooltipVisible={false}
|
|
|
|
renderTrack={Track}
|
|
|
|
renderThumb={Thumb}
|
|
|
|
/>
|
|
|
|
) : null} */}
|
|
|
|
{tradeType !== 'Market' ? (
|
|
|
|
<div style={{ paddingTop: 18 }}>
|
|
|
|
{'POST '}
|
|
|
|
<Switch
|
|
|
|
checked={postOnly}
|
|
|
|
onChange={postOnChange}
|
|
|
|
style={{ marginRight: 40 }}
|
|
|
|
disabled={tradeType === 'Market'}
|
|
|
|
/>
|
|
|
|
{'IOC '}
|
|
|
|
<Switch
|
|
|
|
checked={ioc}
|
|
|
|
onChange={iocOnChange}
|
|
|
|
disabled={tradeType === 'Market'}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</div>
|
2021-04-05 07:32:11 -07:00
|
|
|
<div css={xw`flex`}>
|
|
|
|
{ipAllowed ? (
|
|
|
|
side === 'buy' ? (
|
|
|
|
<BuyButton
|
|
|
|
disabled={
|
2021-04-06 15:11:42 -07:00
|
|
|
(!price && tradeType === 'Limit') ||
|
|
|
|
!baseSize ||
|
|
|
|
!connected ||
|
|
|
|
submitting
|
2021-04-05 07:32:11 -07:00
|
|
|
}
|
|
|
|
onClick={onSubmit}
|
|
|
|
>
|
|
|
|
{connected ? `Buy ${baseCurrency}` : 'CONNECT WALLET TO TRADE'}
|
|
|
|
</BuyButton>
|
|
|
|
) : (
|
|
|
|
<SellButton
|
|
|
|
disabled={
|
2021-04-06 15:11:42 -07:00
|
|
|
(!price && tradeType === 'Limit') ||
|
|
|
|
!baseSize ||
|
|
|
|
!connected ||
|
|
|
|
submitting
|
2021-04-05 07:32:11 -07:00
|
|
|
}
|
|
|
|
onClick={onSubmit}
|
|
|
|
>
|
|
|
|
{connected ? `Sell ${baseCurrency}` : 'CONNECT WALLET TO TRADE'}
|
|
|
|
</SellButton>
|
|
|
|
)
|
2021-04-02 11:26:21 -07:00
|
|
|
) : (
|
2021-04-05 07:32:11 -07:00
|
|
|
<button css={xw`flex-grow border`} disabled>
|
|
|
|
<div css={xw`text-lg font-light p-2`}>Country Not Allowed</div>
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</div>
|
2021-04-02 11:26:21 -07:00
|
|
|
</FloatingElement>
|
|
|
|
)
|
|
|
|
}
|