Merge branch 'max/stoploss' into main

This commit is contained in:
Tyler Shipe 2021-10-11 14:04:16 -04:00
commit eb8210b0c7
49 changed files with 2398 additions and 990 deletions

View File

@ -122,3 +122,14 @@ export interface WalletAdapter {
disconnect: () => any
on(event: string, fn: () => void): this
}
export interface PerpTriggerOrder {
orderId: number
marketIndex: number
orderType: 'limit' | 'ioc' | 'postOnly' | 'market'
side: 'buy' | 'sell'
price: number
size: number
triggerCondition: 'above' | 'below'
triggerPrice: number
}

View File

@ -70,7 +70,7 @@ export default function AccountInfo() {
mngoNodeBank.publicKey,
mngoNodeBank.vault
)
actions.fetchMangoAccounts()
actions.reloadMangoAccount()
notify({
title: 'Successfully redeemed MNGO',
description: '',

View File

@ -39,7 +39,8 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
wallet,
name
)
actions.fetchMangoAccounts()
actions.fetchAllMangoAccounts()
actions.reloadMangoAccount()
onClose()
notify({
title: 'Account name updated',

View File

@ -0,0 +1,56 @@
import { FunctionComponent } from 'react'
interface ButtonGroupProps {
activeValue: string
className?: string
onChange: (x) => void
unit?: string
values: Array<string>
}
const ButtonGroup: FunctionComponent<ButtonGroupProps> = ({
activeValue,
className,
unit,
values,
onChange,
}) => {
return (
<div className="bg-th-bkg-3 rounded-md">
<div className="flex relative">
{activeValue ? (
<div
className={`absolute bg-th-bkg-4 default-transition h-full left-0 top-0 rounded-md transform`}
style={{
transform: `translateX(${
values.findIndex((v) => v === activeValue) * 100
}%)`,
width: `${100 / values.length}%`,
}}
/>
) : null}
{values.map((v, i) => (
<button
className={`${className} cursor-pointer default-transition font-normal px-2 py-1.5 relative rounded-md text-center text-xs w-1/2
${
v === activeValue
? `text-th-primary`
: `text-th-fgd-1 opacity-70 hover:opacity-100`
}
`}
key={`${v}${i}`}
onClick={() => onChange(v)}
style={{
width: `${100 / values.length}%`,
}}
>
{v}
{unit}
</button>
))}
</div>
</div>
)
}
export default ButtonGroup

41
components/Checkbox.tsx Normal file
View File

@ -0,0 +1,41 @@
import React from 'react'
import styled from '@emotion/styled'
import { CheckIcon } from '@heroicons/react/solid'
const HiddenCheckbox = styled.input`
border: 0;
clip: rect(0 0 0 0);
clippath: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
`
const Checkbox = ({ checked, children, disabled = false, ...props }) => (
<label className="cursor-pointer flex items-center">
<HiddenCheckbox
checked={checked}
{...props}
disabled={disabled}
type="checkbox"
/>
<div
className={`${
checked && !disabled ? 'border-th-primary' : 'border-th-fgd-4'
} border cursor-pointer default-transition flex items-center justify-center rounded h-4 w-4`}
>
<CheckIcon
className={`${checked ? 'block' : 'hidden'} h-4 w-4 ${
disabled ? 'text-th-fgd-4' : 'text-th-primary'
}`}
/>
</div>
<span className="ml-2 text-xs text-th-fgd-3">{children}</span>
</label>
)
export default Checkbox

View File

@ -85,7 +85,7 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
sleep(500).then(() => {
mangoAccount
? actions.reloadMangoAccount()
: actions.fetchMangoAccounts()
: actions.fetchAllMangoAccounts()
actions.fetchWalletTokens()
})
})

50
components/FlipCard.tsx Normal file
View File

@ -0,0 +1,50 @@
import styled from '@emotion/styled'
import { css, keyframes } from '@emotion/react'
import FloatingElement from './FloatingElement'
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`
export const FlipCard = styled.div`
background-color: transparent;
height: 100%;
perspective: 1000px;
`
export const FlipCardInner = styled.div<any>`
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.8s ease-out;
transform-style: preserve-3d;
transform: ${({ flip }) => (flip ? 'rotateY(0deg)' : 'rotateY(180deg)')};
`
export const FlipCardFront = styled.div`
width: 100%;
@media screen and (min-width: 768px) {
height: 100%;
position: absolute;
}
`
export const FlipCardBack = styled.div`
position: absolute;
width: 100%;
height: 100%;
transform: rotateY(180deg);
`
export const StyledFloatingElement = styled(FloatingElement)`
animation: ${css`
${fadeIn} 1s linear
`};
`

View File

@ -41,7 +41,7 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
const wallet = useMangoStore((s) => s.wallet.current)
return (
<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 ? (
<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}`}>
{prefix ? (
<div
className={`flex items-center justify-end p-2 border border-r-0
border-th-fgd-4 bg-th-bkg-2 h-full text-xs rounded rounded-r-none text-right ${prefixClassName}`}
className={`absolute left-2 top-1/2 transform -translate-y-1/2 ${prefixClassName}`}
>
{prefix}
</div>
@ -40,7 +39,7 @@ const Input = ({
type={type}
value={value}
onChange={onChange}
className={`${className} pb-px px-2 flex-1 bg-th-bkg-1 rounded 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 ${
error ? 'border-th-red' : 'border-th-fgd-4'
} 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'
: ''
}
${prefix ? 'rounded-l-none' : ''}`}
${prefix ? 'pl-7' : ''}`}
disabled={disabled}
{...props}
/>

View File

@ -13,7 +13,6 @@ const ManualRefresh = ({ className = '' }) => {
await actions.fetchMangoGroup()
await actions.reloadMangoAccount()
actions.fetchTradeHistory()
actions.updateOpenOrders()
}
useEffect(() => {

View File

@ -4,7 +4,7 @@ import { PerpMarket, ZERO_BN } from '@blockworks-foundation/mango-client'
import Button, { LinkButton } from './Button'
import { notify } from '../utils/notifications'
import Loading from './Loading'
import { calculateTradePrice, sleep } from '../utils'
import { sleep } from '../utils'
import Modal from './Modal'
interface MarketCloseModalProps {
@ -26,7 +26,6 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook)
const config = useMangoStore.getState().selectedMarket.config
const orderbook = orderBookRef.current
useEffect(
() =>
useMangoStore.subscribe(
@ -42,32 +41,17 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const { askInfo, bidInfo } = useMangoStore.getState().selectedMarket
const wallet = useMangoStore.getState().wallet.current
const connection = useMangoStore.getState().connection.current
if (!wallet || !mangoGroup || !mangoAccount) return
setSubmitting(true)
try {
const reloadedMangoAccount = await mangoAccount.reload(connection)
const perpAccount = reloadedMangoAccount.perpAccounts[marketIndex]
const perpAccount = mangoAccount.perpAccounts[marketIndex]
const side = perpAccount.basePosition.gt(ZERO_BN) ? 'sell' : 'buy'
const size = Math.abs(market.baseLotsToNumber(perpAccount.basePosition))
const orderPrice = calculateTradePrice(
'Market',
orderbook,
size,
side,
''
)
if (!orderPrice) {
notify({
title: 'Price not available',
description: 'Please try again',
type: 'error',
})
}
const price = 1
// send a large size to ensure we are reducing the entire position
const size =
Math.abs(market.baseLotsToNumber(perpAccount.basePosition)) * 2
const txid = await mangoClient.placePerpOrder(
mangoGroup,
@ -76,11 +60,12 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
market,
wallet,
side,
orderPrice,
price,
size,
'ioc',
0,
side === 'buy' ? askInfo : bidInfo
'market',
0, // client order id
side === 'buy' ? askInfo : bidInfo,
true // reduce only
)
await sleep(500)
actions.reloadMangoAccount()

View File

@ -1,42 +1,16 @@
import {
getMarketIndexBySymbol,
PerpMarket,
} from '@blockworks-foundation/mango-client'
import useSrmAccount from '../hooks/useSrmAccount'
import useMangoStore from '../stores/useMangoStore'
import useFees from '../hooks/useFees'
import { percentFormat } from '../utils'
export default function MarketFee() {
const { rates } = useSrmAccount()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const market = useMangoStore((s) => s.selectedMarket.current)
const marketIndex = getMarketIndexBySymbol(
mangoGroupConfig,
marketConfig.baseSymbol
)
let takerFee, makerFee
if (market instanceof PerpMarket) {
takerFee = parseFloat(
mangoGroup.perpMarkets[marketIndex].takerFee.toFixed()
)
makerFee = parseFloat(
mangoGroup.perpMarkets[marketIndex].makerFee.toFixed()
)
} else {
takerFee = rates.taker
makerFee = rates.maker
}
const { takerFee, makerFee } = useFees()
return (
<div className="block sm:flex mx-auto text-center">
<>
<div className="flex text-xs text-th-fgd-4 px-6 mt-2.5">
<div className="block sm:flex mx-auto text-center">
<div>Maker Fee: {percentFormat.format(makerFee)}</div>
<div className="hidden sm:block px-2">|</div>
<div>Taker Fee: {percentFormat.format(takerFee)}</div>
</>
</div>
</div>
)
}

View File

@ -67,7 +67,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
.then(async (response) => {
await sleep(1000)
actions.fetchWalletTokens()
actions.fetchMangoAccounts()
actions.fetchAllMangoAccounts()
setSubmitting(false)
console.log('response', response)

View File

@ -84,7 +84,7 @@ const Notification = ({ type, title, description, txid }) => {
return (
<div
className={`max-w-sm w-full bg-th-bkg-3 shadow-lg rounded-md mt-2 pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden`}
className={`max-w-sm w-full bg-th-bkg-3 border border-th-bkg-4 shadow-lg rounded-md mt-2 pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden`}
>
<div className={`flex items-center p-4 relative`}>
<div className={`flex-shrink-0`}>

View File

@ -9,11 +9,129 @@ import { notify } from '../utils/notifications'
import SideBadge from './SideBadge'
import { Order, Market } from '@project-serum/serum/lib/market'
import { PerpOrder, PerpMarket } from '@blockworks-foundation/mango-client'
import { formatUsdValue, sleep } from '../utils'
import { formatUsdValue } from '../utils'
import { Table, Td, Th, TrBody, TrHead } from './TableElements'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { Row } from './TableElements'
import { PerpTriggerOrder } from '../@types/types'
const DesktopTable = ({ openOrders, cancelledOrderId, handleCancelOrder }) => {
return (
<Table>
<thead>
<TrHead>
<Th>Market</Th>
<Th>Side</Th>
<Th>Size</Th>
<Th>Price</Th>
<Th>Value</Th>
<Th>Condition</Th>
<Th>
<span className={`sr-only`}>Edit</span>
</Th>
</TrHead>
</thead>
<tbody>
{openOrders.map(({ order, market }, index) => {
return (
<TrBody index={index} key={`${order.orderId}${order.side}`}>
<Td>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${market.config.baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{market.config.name}</div>
</div>
</Td>
<Td>
<SideBadge side={order.side} />
</Td>
<Td>{order.size}</Td>
<Td>{formatUsdValue(order.price)}</Td>
<Td>{formatUsdValue(order.price * order.size)}</Td>
<Td>
{order.perpTrigger &&
`${order.orderType} ${
order.triggerCondition
} ${order.triggerPrice.toString()}`}
</Td>
<Td>
<div className={`flex justify-end`}>
<Button
onClick={() => handleCancelOrder(order, market.account)}
className="ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3"
>
{cancelledOrderId + '' === order.orderId + '' ? (
<Loading />
) : (
<span>Cancel</span>
)}
</Button>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
)
}
const MobileTable = ({ openOrders, cancelledOrderId, handleCancelOrder }) => {
return (
<>
{openOrders.map(({ market, order }, index) => (
<Row key={`${order.orderId}${order.side}`} index={index}>
<div className="col-span-12 flex items-center justify-between text-fgd-1 text-left">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${market.config.baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>
<div className="mb-0.5">{market.config.name}</div>
<div className="text-th-fgd-3 text-xs">
<span
className={`mr-1
${
order.side === 'buy'
? 'text-th-green'
: 'text-th-red'
}
`}
>
{order.side.toUpperCase()}
</span>
{order.perpTrigger
? `${order.size} ${order.triggerCondition} ${order.triggerPrice}`
: `${order.size} at ${formatUsdValue(order.price)}`}
</div>
</div>
</div>
<Button
onClick={() => handleCancelOrder(order, market.account)}
className="ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3"
>
{cancelledOrderId + '' === order.orderId + '' ? (
<Loading />
) : (
<span>Cancel</span>
)}
</Button>
</div>
</Row>
))}
</>
)
}
const OpenOrdersTable = () => {
const { asPath } = useRouter()
@ -24,7 +142,7 @@ const OpenOrdersTable = () => {
const isMobile = width ? width < breakpoints.md : false
const handleCancelOrder = async (
order: Order | PerpOrder,
order: Order | PerpOrder | PerpTriggerOrder,
market: Market | PerpMarket
) => {
const wallet = useMangoStore.getState().wallet.current
@ -46,14 +164,24 @@ const OpenOrdersTable = () => {
order as Order
)
} else if (market instanceof PerpMarket) {
txid = await mangoClient.cancelPerpOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
market,
order as PerpOrder,
false
)
// TODO: this is not ideal
if (order['triggerCondition']) {
txid = await mangoClient.removeAdvancedOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
(order as PerpTriggerOrder).orderId
)
} else {
txid = await mangoClient.cancelPerpOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet,
market,
order as PerpOrder,
false
)
}
}
notify({ title: 'Successfully cancelled order', txid })
} catch (e) {
@ -65,123 +193,27 @@ const OpenOrdersTable = () => {
})
console.log('error', `${e}`)
} finally {
await sleep(600)
// await sleep(600)
actions.reloadMangoAccount()
actions.updateOpenOrders()
setCancelId(null)
}
}
const tableProps = {
openOrders,
cancelledOrderId: cancelId,
handleCancelOrder,
}
return (
<div className={`flex flex-col py-2 sm:pb-4 sm:pt-4`}>
<div className={`-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div className={`align-middle inline-block min-w-full sm:px-6 lg:px-8`}>
{openOrders && openOrders.length > 0 ? (
!isMobile ? (
<Table>
<thead>
<TrHead>
<Th>Market</Th>
<Th>Side</Th>
<Th>Size</Th>
<Th>Price</Th>
<Th>Value</Th>
<Th>
<span className={`sr-only`}>Edit</span>
</Th>
</TrHead>
</thead>
<tbody>
{openOrders.map(({ order, market }, index) => {
return (
<TrBody
index={index}
key={`${order.orderId}${order.side}`}
>
<Td>
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${market.config.baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>{market.config.name}</div>
</div>
</Td>
<Td>
<SideBadge side={order.side} />
</Td>
<Td>{order.size}</Td>
<Td>{formatUsdValue(order.price)}</Td>
<Td>{formatUsdValue(order.price * order.size)}</Td>
<Td>
<div className={`flex justify-end`}>
<Button
onClick={() =>
handleCancelOrder(order, market.account)
}
className="ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3"
>
{cancelId + '' === order.orderId + '' ? (
<Loading />
) : (
<span>Cancel</span>
)}
</Button>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
<DesktopTable {...tableProps} />
) : (
<>
{openOrders.map(({ market, order }, index) => (
<Row key={`${order.orderId}${order.side}`} index={index}>
<div className="col-span-12 flex items-center justify-between text-fgd-1 text-left">
<div className="flex items-center">
<img
alt=""
width="20"
height="20"
src={`/assets/icons/${market.config.baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<div>
<div className="mb-0.5">{market.config.name}</div>
<div className="text-th-fgd-3 text-xs">
<span
className={`mr-1
${
order.side === 'buy'
? 'text-th-green'
: 'text-th-red'
}
`}
>
{order.side.toUpperCase()}
</span>
{`${order.size} at ${formatUsdValue(order.price)}`}
</div>
</div>
</div>
<Button
onClick={() => handleCancelOrder(order, market.account)}
className="ml-3 text-xs pt-0 pb-0 h-8 pl-3 pr-3"
>
{cancelId + '' === order.orderId + '' ? (
<Loading />
) : (
<span>Cancel</span>
)}
</Button>
</div>
</Row>
))}
</>
<MobileTable {...tableProps} />
)
) : (
<div

View File

@ -1,7 +1,6 @@
import React, { useRef, useEffect, useState } from 'react'
import styled from '@emotion/styled'
import Big from 'big.js'
import { css, keyframes } from '@emotion/react'
import useInterval from '../hooks/useInterval'
import usePrevious from '../hooks/usePrevious'
import { isEqual, getDecimalCount, usdFormatter } from '../utils/'
@ -16,10 +15,16 @@ import { ElementTitle } from './styles'
import useMangoStore from '../stores/useMangoStore'
import Tooltip from './Tooltip'
import GroupSize from './GroupSize'
import FloatingElement from './FloatingElement'
import { useOpenOrders } from '../hooks/useOpenOrders'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import {
FlipCard,
FlipCardBack,
FlipCardFront,
FlipCardInner,
StyledFloatingElement,
} from './FlipCard'
const Line = styled.div<any>`
text-align: ${(props) => (props.invert ? 'left' : 'right')};
@ -27,51 +32,6 @@ const Line = styled.div<any>`
filter: opacity(40%);
${(props) => props['data-width'] && `width: ${props['data-width']};`}
`
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`
const FlipCard = styled.div`
background-color: transparent;
height: 100%;
perspective: 1000px;
`
const FlipCardInner = styled.div<any>`
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.8s ease-out;
transform-style: preserve-3d;
transform: ${({ flip }) => (flip ? 'rotateY(0deg)' : 'rotateY(180deg)')};
`
const FlipCardFront = styled.div`
position: absolute;
width: 100%;
height: 100%;
`
const FlipCardBack = styled.div`
position: absolute;
width: 100%;
height: 100%;
transform: rotateY(180deg);
`
const StyledFloatingElement = styled(FloatingElement)`
animation: ${css`
${fadeIn} 1s linear
`};
overflow: hidden;
`
const groupBy = (ordersArray, market, grouping: number, isBids: boolean) => {
if (!ordersArray || !market || !grouping || grouping == market?.tickSize) {
@ -221,7 +181,8 @@ export default function Orderbook({ depth = 8 }) {
const ask = defaultLayout
? asksToDisplay[0]?.price
: asksToDisplay[asksToDisplay.length - 1]?.price
let spread, spreadPercentage
let spread = 0,
spreadPercentage = 0
if (bid && ask) {
spread = ask - bid
spreadPercentage = (spread / ask) * 100

View File

@ -59,7 +59,7 @@ const PositionsTable = () => {
<div className="flex flex-col pb-2 pt-4">
{unsettledPositions.length > 0 ? (
<div className="border border-th-bkg-4 rounded-lg mb-6 p-4 sm:p-6">
<div className="flex items-center justify-between pb-4">
<div className="flex items-center justify-between pb-2">
<div className="flex items-center sm:text-lg">
<ExclamationIcon className="flex-shrink-0 h-5 mr-1.5 mt-0.5 text-th-primary w-5" />
Unsettled Positions

View File

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

View File

@ -7,6 +7,7 @@ type TooltipProps = {
placement?: any
className?: string
children?: ReactNode
delay?: number
}
const Tooltip = ({
@ -14,6 +15,7 @@ const Tooltip = ({
content,
className,
placement = 'top',
delay = 0,
}: TooltipProps) => {
return (
<Tippy
@ -22,9 +24,10 @@ const Tooltip = ({
appendTo={() => document.body}
maxWidth="20rem"
interactive
delay={delay}
content={
<div
className={`rounded p-3 text-xs bg-th-bkg-3 leading-5 shadow-md text-th-fgd-3 outline-none focus:outline-none ${className}`}
className={`rounded p-2 text-xs bg-th-bkg-3 leading-4 shadow-md text-th-fgd-3 outline-none focus:outline-none border border-th-bkg-4 ${className}`}
>
{content}
</div>

View File

@ -1,658 +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, sleep } 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 { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
const StyledRightInput = styled(Input)`
border-left: 1px solid transparent;
`
export default function TradeForm() {
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 { side, baseSize, quoteSize, price, tradeType } = useMangoStore(
(s) => s.tradeForm
)
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 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 (market instanceof PerpMarket) {
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) && tradeType === 'Limit') {
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 (tradeType === 'Market') {
setIoc(true)
setPrice('')
} 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)
}
async function onSubmit() {
if (!price && tradeType === 'Limit') {
notify({
title: 'Missing price',
type: 'error',
})
return
} else if (!baseSize) {
notify({
title: 'Missing size',
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
)
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.placeSpotOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
orderType
)
} else {
txid = await mangoClient.placePerpOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
orderType,
0,
side === 'buy' ? askInfo : bidInfo
)
}
notify({ title: 'Successfully placed trade', txid })
setPrice('')
onSetBaseSize('')
} catch (e) {
notify({
title: 'Error placing order',
description: e.message,
txid: e.txid,
type: 'error',
})
} finally {
await sleep(600)
actions.reloadMangoAccount()
actions.updateOpenOrders()
actions.loadMarketFills()
setSubmitting(false)
}
}
const disabledTradeButton =
(!price && tradeType === 'Limit') ||
!baseSize ||
!connected ||
submitting ||
!mangoAccount
return !isMobile ? (
<div className={!connected ? 'fliter blur-sm' : 'flex flex-col h-full'}>
<div className={`flex 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>
<Input.Group className="mt-4">
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={tradeType === 'Market'}
prefix={'Price'}
suffix={groupConfig.quoteSymbol}
className="rounded-r-none"
wrapperClassName="w-3/5"
/>
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
className="hover:border-th-primary flex-grow"
/>
</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
)}
/>
{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>
) : (
<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
)}
/>
{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

@ -21,10 +21,12 @@ const TradeHistoryTable = ({ numTrades }: { numTrades?: number }) => {
const renderTradeDateTime = (timestamp: BN | string) => {
let date
if (timestamp instanceof BN) {
date = new Date(timestamp.toNumber() * 1000)
} else {
// don't compare to BN because of npm maddness
// prototypes can be different due to multiple versions being imported
if (typeof timestamp === 'string') {
date = new Date(timestamp)
} else {
date = new Date(timestamp.toNumber() * 1000)
}
return (

View File

@ -12,7 +12,7 @@ import FloatingElement from '../components/FloatingElement'
import Orderbook from '../components/Orderbook'
import AccountInfo from './AccountInfo'
import UserMarketInfo from './UserMarketInfo'
import TradeForm from './TradeForm'
import TradeForm from './trade_form/TradeForm'
import UserInfo from './UserInfo'
import RecentMarketTrades from './RecentMarketTrades'
import useMangoStore from '../stores/useMangoStore'
@ -26,7 +26,7 @@ export const defaultLayouts = {
xl: [
{ i: 'tvChart', x: 0, y: 0, w: 6, h: 30 },
{ i: 'orderbook', x: 6, y: 0, w: 3, h: 17 },
{ i: 'tradeForm', x: 9, y: 1, w: 3, h: 14 },
{ i: 'tradeForm', x: 9, y: 1, w: 3, h: 19 },
{ i: 'marketTrades', x: 6, y: 1, w: 3, h: 13 },
{ i: 'accountInfo', x: 9, y: 3, w: 3, h: 15 },
{ i: 'userInfo', x: 0, y: 2, w: 9, h: 19 },
@ -54,7 +54,7 @@ export const defaultLayouts = {
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 25, minW: 6 },
{ i: 'marketPosition', x: 0, y: 1, w: 6, h: 15, minW: 2 },
{ i: 'accountInfo', x: 6, y: 1, w: 6, h: 15, minW: 2 },
{ i: 'tradeForm', x: 0, y: 2, w: 12, h: 15, minW: 3 },
{ i: 'tradeForm', x: 0, y: 2, w: 12, h: 18, minW: 3 },
{ i: 'orderbook', x: 0, y: 3, w: 6, h: 17, minW: 3 },
{ i: 'marketTrades', x: 6, y: 3, w: 6, h: 17, minW: 2 },
{ i: 'userInfo', x: 0, y: 4, w: 12, h: 19, minW: 6 },
@ -63,14 +63,14 @@ export const defaultLayouts = {
{ i: 'tvChart', x: 0, y: 0, w: 12, h: 12, minW: 6 },
{ i: 'marketPosition', x: 0, y: 1, w: 6, h: 13, minW: 2 },
{ i: 'accountInfo', x: 0, y: 2, w: 6, h: 15, minW: 2 },
{ i: 'tradeForm', x: 0, y: 3, w: 12, h: 13, minW: 3 },
{ i: 'tradeForm', x: 0, y: 3, w: 12, h: 17, minW: 3 },
{ i: 'orderbook', x: 0, y: 4, w: 6, h: 17, minW: 3 },
{ i: 'marketTrades', x: 0, y: 5, w: 6, h: 17, minW: 2 },
{ i: 'userInfo', x: 0, y: 6, w: 12, h: 19, minW: 6 },
],
}
export const GRID_LAYOUT_KEY = 'mangoSavedLayouts-3.0.8'
export const GRID_LAYOUT_KEY = 'mangoSavedLayouts-3.0.9'
export const breakpoints = { xl: 1600, lg: 1200, md: 1110, sm: 768, xs: 0 }
const TradePageGrid = () => {
@ -146,9 +146,7 @@ const TradePageGrid = () => {
<Orderbook depth={orderbookDepth} />
</div>
<div key="tradeForm">
<FloatingElement className="h-full" showConnect>
<TradeForm />
</FloatingElement>
<TradeForm />
</div>
<div key="accountInfo">
<FloatingElement className="h-full" showConnect>

View File

@ -65,7 +65,7 @@ export default function AccountAssets() {
try {
await mangoClient.settleAll(mangoGroup, mangoAccount, spotMarkets, wallet)
actions.fetchMangoAccounts()
actions.reloadMangoAccount()
} catch (e) {
if (e.message === 'No unsettled funds') {
notify({

View File

@ -69,7 +69,7 @@ export default function AccountOverview() {
mngoNodeBank.publicKey,
mngoNodeBank.vault
)
actions.fetchMangoAccounts()
actions.reloadMangoAccount()
notify({
title: 'Successfully redeemed MNGO',
description: '',

View File

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

View File

@ -0,0 +1,872 @@
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, { Orderbook } 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 Checkbox from '../Checkbox'
import { useViewport } from '../../hooks/useViewport'
import { breakpoints } from '../TradePageGrid'
import EstPriceImpact from './EstPriceImpact'
import useFees from '../../hooks/useFees'
export const TRIGGER_ORDER_TYPES = [
'Stop Loss',
'Take Profit',
'Stop Limit',
'Take Profit Limit',
]
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(true)
const [positionSizePercent, setPositionSizePercent] = useState('')
const { takerFee } = useFees()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const marketIndex = getMarketIndexBySymbol(
groupConfig,
marketConfig.baseSymbol
)
let perpAccount
if (isPerpMarket && mangoAccount) {
perpAccount = mangoAccount.perpAccounts[marketIndex]
}
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 isTriggerLimit = ['Stop Limit', 'Take Profit Limit'].includes(tradeType)
const isTriggerOrder = TRIGGER_ORDER_TYPES.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])
useEffect(() => {
handleSetPositionSize(positionSizePercent, spotMargin)
if (!isPerpMarket && isTriggerOrder) {
onTradeTypeChange('Limit')
}
}, [market])
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))
setPositionSizePercent('')
}
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)
setPositionSizePercent('')
}
const onTradeTypeChange = (tradeType) => {
setTradeType(tradeType)
if (TRIGGER_ORDER_TYPES.includes(tradeType)) {
setReduceOnly(true)
}
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) => {
setSpotMargin(checked)
if (positionSizePercent) {
handleSetPositionSize(positionSizePercent, checked)
}
}
const handleSetPositionSize = (percent, spotMargin) => {
setPositionSizePercent(percent)
const baseSizeMax =
spotMargin || marketConfig.kind === 'perp' ? max : spotMax
const baseSize = baseSizeMax * (parseInt(percent) / 100)
const step = parseFloat(minOrderSize)
const roundedSize = (Math.floor(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) => {
if (!size || !total) return 0
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`
let priceImpact
let estimatedPrice = price
if (tradeType === 'Market') {
const estimateMarketPrice = (
orderBook: Orderbook,
size: number,
side: 'buy' | 'sell'
): number => {
const orders = side === 'buy' ? orderBook.asks : orderBook.bids
let accSize = 0
let accPrice = 0
for (const [orderPrice, orderSize] of orders) {
const remainingSize = size - accSize
if (remainingSize <= orderSize) {
accSize += remainingSize
accPrice += remainingSize * orderPrice
break
}
accSize += orderSize
accPrice += orderSize * orderPrice
}
if (!accSize) {
console.error('Orderbook empty no market price available')
return markPrice
}
return accPrice / accSize
}
const estimatedSize =
perpAccount && reduceOnly
? Math.abs(
(market as PerpMarket).baseLotsToNumber(perpAccount.basePosition)
)
: baseSize
estimatedPrice = estimateMarketPrice(orderbook, estimatedSize || 0, side)
const slippageAbs =
estimatedSize > 0 ? Math.abs(estimatedPrice - markPrice) : 0
const slippageRel = slippageAbs / markPrice
const takerFeeRel = takerFee
const takerFeeAbs = estimatedSize
? takerFeeRel * estimatedPrice * estimatedSize
: 0
priceImpact = {
slippage: [slippageAbs, slippageRel],
takerFee: [takerFeeAbs, takerFeeRel],
}
console.log('estimated', estimatedSize, estimatedPrice, priceImpact)
}
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',
})
}
// TODO: this has a race condition when switching between markets or buy & sell
// spot market orders will sometimes not be ioc but limit
const orderType = ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit'
console.log(
'submit',
side,
baseSize.toString(),
orderPrice.toString(),
orderType,
market instanceof Market && 'spot',
isTriggerOrder && 'trigger'
)
let txid
if (market instanceof Market) {
txid = await mangoClient.placeSpotOrder2(
mangoGroup,
mangoAccount,
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),
true // reduceOnly
)
} 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 gap-2 text-left">
<div className="col-span-12 md:col-span-6">
<label className="text-xxs text-th-fgd-3">Type</label>
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
offerTriggers={isPerpMarket}
/>
</div>
<div className="col-span-12 md:col-span-6">
{!isTriggerOrder ? (
<>
<label className="text-xxs text-th-fgd-3">Price</label>
<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"
/>
}
/>
</>
) : (
<>
<label className="text-xxs text-th-fgd-3">Trigger Price</label>
<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>
{isTriggerLimit && (
<>
<div className="col-span-12">
<label className="text-xxs text-th-fgd-3">Price</label>
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
</>
)}
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">Size</label>
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
prefix={
<img
src={`/assets/icons/${marketConfig.baseSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">Quantity</label>
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-12 -mt-1">
<ButtonGroup
activeValue={positionSizePercent}
onChange={(p) => handleSetPositionSize(p, spotMargin)}
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' ? (
<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)}
disabled={isTriggerOrder}
>
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="col-span-12 md:col-span-10 md:col-start-3 pt-1">
{tradeType === 'Market' && priceImpact ? (
<EstPriceImpact priceImpact={priceImpact} />
) : (
<MarketFee />
)}
</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' ? (
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
}`
) : (
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
}`
)}
</Button>
) : (
<Button disabled className="flex-grow">
<span>Country Not Allowed</span>
</Button>
)}
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,77 @@
import { InformationCircleIcon } from '@heroicons/react/outline'
import Tippy from '@tippyjs/react'
import 'tippy.js/animations/scale.css'
import { percentFormat } from '../../utils'
const EstPriceImpact = ({
priceImpact,
}: {
priceImpact?: { slippage: number[]; takerFee: number[] }
}) => {
const priceImpactAbs = priceImpact.slippage[0] + priceImpact.takerFee[0]
const priceImpactRel = priceImpact.slippage[1] + priceImpact.takerFee[1]
return (
<div
className={`border-t border-th-bkg-4 flex items-center justify-center mt-2 pt-2 text-th-fgd-3 text-xs`}
>
Est. Price Impact:
<span
className={`font-bold ml-2 ${
priceImpactRel <= 0.005
? 'text-th-green'
: priceImpactRel > 0.005 && priceImpactRel <= 0.01
? 'text-th-orange'
: 'text-th-red'
}`}
>
${priceImpactAbs.toFixed(2)}
<span className="mx-2 text-th-fgd-4">|</span>
{percentFormat.format(priceImpactRel)}
</span>
<Tippy
animation="scale"
placement="top"
appendTo={() => document.body}
maxWidth="20rem"
interactive
delay={0}
content={
<div
className={`rounded p-4 text-xs bg-th-bkg-3 leading-4 shadow-md text-th-fgd-3 outline-none space-y-1.5 w-56 focus:outline-none`}
>
<div className="flex justify-between">
Est. Slippage:
<span className="text-th-fgd-1">
${priceImpact.slippage[0].toFixed(2)}
<span className="px-1 text-th-fgd-4">|</span>
{percentFormat.format(priceImpact.slippage[1])}
</span>
</div>
{/* <div className="flex justify-between">
Maker Fee:
<span className="text-th-fgd-1">
${priceImpact.makerFee[0]}
<span className="px-1 text-th-fgd-4">|</span>
{priceImpact.makerFee[1].toFixed(2)}%
</span>
</div> */}
<div className="flex justify-between">
Taker Fee:
<span className="text-th-fgd-1">
${priceImpact.takerFee[0].toFixed(2)}
<span className="px-1 text-th-fgd-4">|</span>
{percentFormat.format(priceImpact.takerFee[1])}
</span>
</div>
</div>
}
>
<div className="outline-none focus:outline-none">
<InformationCircleIcon className="h-5 w-5 ml-2 text-th-primary" />
</div>
</Tippy>
</div>
)
}
export default EstPriceImpact

View File

@ -0,0 +1,56 @@
import { FunctionComponent } from 'react'
import { PerpMarket } from '@blockworks-foundation/mango-client'
import useMangoStore from '../../stores/useMangoStore'
interface OrderSideTabsProps {
isSimpleForm?: boolean
onChange: (x) => void
side: string
}
const OrderSideTabs: FunctionComponent<OrderSideTabsProps> = ({
isSimpleForm,
onChange,
side,
}) => {
const market = useMangoStore((s) => s.selectedMarket.current)
return (
<div className={`border-b border-th-fgd-4 mb-3 relative -mt-2.5`}>
<div
className={`absolute ${
side === 'buy'
? 'bg-th-green translate-x-0'
: 'bg-th-red translate-x-full'
} bottom-[-1px] default-transition left-0 h-0.5 transform w-1/2`}
/>
<nav className="-mb-px flex" aria-label="Tabs">
<button
onClick={() => onChange('buy')}
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'
? `text-th-green`
: `text-th-fgd-4 hover:text-th-green`
}
`}
>
{market instanceof PerpMarket && isSimpleForm ? 'Long' : 'Buy'}
</button>
<button
onClick={() => onChange('sell')}
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 === 'sell'
? `text-th-red`
: `text-th-fgd-4 hover:text-th-red`
}
`}
>
{market instanceof PerpMarket && isSimpleForm ? 'Short' : 'Sell'}
</button>
</nav>
</div>
)
}
export default OrderSideTabs

View File

@ -0,0 +1,713 @@
import { useState, useEffect, useRef, useMemo } from 'react'
import useIpAddress from '../../hooks/useIpAddress'
import {
getTokenBySymbol,
getMarketIndexBySymbol,
I80F48,
PerpMarket,
} from '@blockworks-foundation/mango-client'
import { notify } from '../../utils/notifications'
import { calculateTradePrice, getDecimalCount, sleep } from '../../utils'
import { floorToDecimal } from '../../utils/index'
import useMangoStore from '../../stores/useMangoStore'
import Button from '../Button'
import Input from '../Input'
import { Market } from '@project-serum/serum'
import Big from 'big.js'
import MarketFee from '../MarketFee'
import Loading from '../Loading'
import { ElementTitle } from '../styles'
import ButtonGroup from '../ButtonGroup'
import Checkbox from '../Checkbox'
import OrderSideTabs from './OrderSideTabs'
import Tooltip from '../Tooltip'
import EstPriceImpact from './EstPriceImpact'
export default function SimpleTradeForm({ 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 mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoClient = useMangoStore((s) => s.connection.client)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const marketIndex = getMarketIndexBySymbol(
groupConfig,
marketConfig.baseSymbol
)
const market = useMangoStore((s) => s.selectedMarket.current)
const { side, baseSize, quoteSize, price, triggerPrice, tradeType } =
useMangoStore((s) => s.tradeForm)
const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [positionSizePercent, setPositionSizePercent] = useState('')
const [showStopForm, setShowStopForm] = useState(false)
const [showTakeProfitForm, setShowTakeProfitForm] = useState(false)
const [stopSizePercent, setStopSizePercent] = useState('5%')
const [reduceOnly, setReduceOnly] = useState(false)
const [spotMargin, setSpotMargin] = 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' && tradeType !== 'Limit') {
setTradeType('Limit')
}
}, [])
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 setTriggerPrice = (price) =>
set((s) => {
if (!Number.isNaN(parseFloat(price))) {
s.tradeForm.tripperPrice = parseFloat(price)
} else {
s.tradeForm.tripperPrice = price
}
})
const setTradeType = (type) =>
set((s) => {
s.tradeForm.tradeType = 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 (market instanceof PerpMarket) {
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
setPositionSizePercent('')
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 | '') => {
setPositionSizePercent('')
setQuoteSize(quoteSize)
if (!quoteSize) {
setBaseSize('')
return
}
if (!Number(price) && tradeType === 'Limit') {
setBaseSize('')
return
}
const usePrice = Number(price) || markPrice
const rawBaseSize = quoteSize / usePrice
const baseSize = quoteSize && floorToDecimal(rawBaseSize, sizeDecimalCount)
setBaseSize(baseSize)
}
const onTradeTypeChange = (tradeType) => {
setTradeType(tradeType)
setPositionSizePercent('')
if (tradeType === 'Market') {
setIoc(true)
setPrice('')
} 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 && tradeType === 'Limit') {
notify({
title: 'Missing price',
type: 'error',
})
return
} else if (!baseSize) {
notify({
title: 'Missing size',
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
)
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.placeSpotOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
orderType
)
} else {
txid = await mangoClient.placePerpOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
orderPrice,
baseSize,
orderType,
0,
side === 'buy' ? askInfo : bidInfo
)
}
notify({ title: 'Successfully placed trade', txid })
setPrice('')
onSetBaseSize('')
} catch (e) {
notify({
title: 'Error placing order',
description: e.message,
txid: e.txid,
type: 'error',
})
} finally {
await sleep(600)
actions.reloadMangoAccount()
actions.loadMarketFills()
setSubmitting(false)
}
}
const { max, deposits, borrows } = useMemo(() => {
if (!mangoAccount) return { max: 0 }
const priceOrDefault = price
? I80F48.fromNumber(price)
: mangoGroup.getPrice(marketIndex, mangoCache)
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 }
}, [mangoAccount, mangoGroup, mangoCache, marketIndex, market, side, price])
const handleSetPositionSize = (percent) => {
setPositionSizePercent(percent)
const baseSize = max * (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`
const disabledTradeButton =
(!price && tradeType === 'Limit') ||
!baseSize ||
!connected ||
submitting ||
!mangoAccount
const hideProfitStop =
(side === 'sell' && baseSize === roundedDeposits) ||
(side === 'buy' && baseSize === roundedBorrows)
return (
<div className="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 isSimpleForm onChange={setSide} side={side} />
<div className="grid grid-cols-12 gap-2 text-left">
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">Type</label>
<ButtonGroup
activeValue={tradeType}
className="h-10"
onChange={(p) => onTradeTypeChange(p)}
values={['Limit', 'Market']}
/>
</div>
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">Price</label>
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={tradeType === 'Market'}
placeholder={tradeType === 'Market' ? markPrice : null}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">Size</label>
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
prefix={
<img
src={`/assets/icons/${marketConfig.baseSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">Quantity</label>
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
</div>
<div className="col-span-12">
<div className="-mt-1">
<ButtonGroup
activeValue={positionSizePercent}
onChange={(p) => handleSetPositionSize(p)}
unit="%"
values={['10', '25', '50', '75', '100']}
/>
</div>
{side === 'sell' ? (
<div className="text-th-fgd-3 text-xs tracking-normal mt-2">
<span>{roundedDeposits > 0 ? closeDepositString : null}</span>
</div>
) : (
<div className="text-th-fgd-3 text-xs tracking-normal mt-2">
<span>{roundedBorrows > 0 ? closeBorrowString : null}</span>
</div>
)}
<div className="flex items-center space-x-1">
{!hideProfitStop ? (
<div
className={`${
showStopForm ? 'bg-th-bkg-4' : 'bg-th-bkg-3'
} mt-1 p-2 rounded-md w-1/2`}
>
<Checkbox
checked={showStopForm}
onChange={(e) => setShowStopForm(e.target.checked)}
>
Set Stop Loss
</Checkbox>
</div>
) : null}
{!hideProfitStop ? (
<div
className={`${
showTakeProfitForm ? 'bg-th-bkg-4' : 'bg-th-bkg-3'
} mt-1 p-2 rounded-md w-1/2`}
>
<Checkbox
checked={showTakeProfitForm}
onChange={(e) => setShowTakeProfitForm(e.target.checked)}
>
Set Take Profit
</Checkbox>
</div>
) : null}
</div>
</div>
{showStopForm && !hideProfitStop ? (
<>
<div className="col-span-12">
<label className="text-xxs text-th-fgd-3">Stop Price</label>
<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 -mt-1">
<ButtonGroup
activeValue={stopSizePercent}
onChange={(p) => setStopSizePercent(p)}
values={['5%', '10%', '15%', '20%', '25%']}
/>
</div>
</>
) : null}
{showTakeProfitForm && !hideProfitStop ? (
<>
<div className="col-span-12">
<label className="text-left text-xs text-th-fgd-3">
Profit Price
</label>
<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 -mt-1">
<ButtonGroup
activeValue={stopSizePercent}
onChange={(p) => setStopSizePercent(p)}
values={['5%', '10%', '15%', '20%', '25%']}
/>
</div>
</>
) : null}
<div className="col-span-12 flex pt-2">
{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>
{tradeType === 'Market' ? (
<div className="col-span-12">
<EstPriceImpact />
</div>
) : null}
<div className={`col-span-12 flex pt-2`}>
{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="col-span-12 flex pt-2 text-xs text-th-fgd-4">
<MarketFee />
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,72 @@
import { useMemo, useState } from 'react'
import { SwitchHorizontalIcon } from '@heroicons/react/outline'
import { getWeights } from '@blockworks-foundation/mango-client'
import useMangoStore from '../../stores/useMangoStore'
import AdvancedTradeForm from './AdvancedTradeForm'
import SimpleTradeForm from './SimpleTradeForm'
import {
FlipCard,
FlipCardBack,
FlipCardFront,
FlipCardInner,
StyledFloatingElement,
} from '../FlipCard'
export default function TradeForm() {
const [showAdvancedFrom, setShowAdvancedForm] = useState(true)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const connected = useMangoStore((s) => s.wallet.connected)
const handleFormChange = () => {
setShowAdvancedForm(!showAdvancedFrom)
}
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])
return (
<FlipCard>
<FlipCardInner flip={showAdvancedFrom}>
{showAdvancedFrom ? (
<FlipCardFront>
<StyledFloatingElement
className="h-full px-1 py-0 md:px-4 md:py-4"
showConnect
>
<div className={`${!connected ? 'filter blur-sm' : ''}`}>
{/* <button
onClick={handleFormChange}
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" />
</button> */}
<AdvancedTradeForm initLeverage={initLeverage} />
</div>
</StyledFloatingElement>
</FlipCardFront>
) : (
<FlipCardBack>
<StyledFloatingElement className="h-full px-1 md:px-4" showConnect>
<div className={`${!connected ? 'filter blur-sm' : ''}`}>
<button
onClick={handleFormChange}
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" />
</button>
<SimpleTradeForm initLeverage={initLeverage} />
</div>
</StyledFloatingElement>
</FlipCardBack>
)}
</FlipCardInner>
</FlipCard>
)
}

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 w-full bg-th-bkg-1 border border-th-fgd-4 px-2 h-10 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,18 +1,19 @@
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'
import { useViewport } from '../../hooks/useViewport'
import { breakpoints } from '../TradePageGrid'
const StyledListbox = styled(Listbox.Button)`
border-left: 1px solid transparent;
border-right: 1px solid transparent;
`
const TRADE_TYPES = ['Limit', 'Market']
const TradeType = ({ value, onChange, className = '' }) => {
const TriggerType = ({ value, onChange, className = '' }) => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const TRIGGER_TYPES = ['Above', 'Below']
return (
<div className={`relative ${className}`}>
{!isMobile ? (
@ -20,7 +21,7 @@ const TradeType = ({ value, onChange, className = '' }) => {
{({ 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`}
className={`font-normal h-full w-full bg-th-bkg-1 border border-th-fgd-4 hover:border-th-primary rounded rounded-r-none focus:outline-none focus:border-th-primary`}
>
<div
className={`flex items-center justify-between space-x-4 pl-2 pr-1`}
@ -40,7 +41,7 @@ const TradeType = ({ value, onChange, className = '' }) => {
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) => (
{TRIGGER_TYPES.map((type) => (
<Listbox.Option key={type} value={type}>
{({ selected }) => (
<div
@ -60,34 +61,25 @@ const TradeType = ({ value, onChange, className = '' }) => {
</Listbox>
) : (
<div className="flex">
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
{TRIGGER_TYPES.map((triggerType, i) => (
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${
value === 'Limit'
value === triggerType
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => onChange('Limit')}
>
Limit
</div>
<div
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
${
value === 'Market'
? `ring-1 ring-inset ring-th-primary text-th-primary`
: `text-th-fgd-1 opacity-50 hover:opacity-100`
}
`}
onClick={() => onChange('Market')}
>
Market
</div>
key={`${triggerType}${i}`}
onClick={() => onChange(triggerType)}
>
{triggerType}
</div>
))}
</div>
)}
</div>
)
}
export default TradeType
export default TriggerType

33
hooks/useFees.tsx Normal file
View File

@ -0,0 +1,33 @@
import {
getMarketIndexBySymbol,
PerpMarket,
} from '@blockworks-foundation/mango-client'
import useSrmAccount from '../hooks/useSrmAccount'
import useMangoStore from '../stores/useMangoStore'
export default function useFees() {
const { rates } = useSrmAccount()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const market = useMangoStore((s) => s.selectedMarket.current)
const marketIndex = getMarketIndexBySymbol(
mangoGroupConfig,
marketConfig.baseSymbol
)
let takerFee, makerFee
if (market instanceof PerpMarket) {
takerFee = parseFloat(
mangoGroup.perpMarkets[marketIndex].takerFee.toFixed()
)
makerFee = parseFloat(
mangoGroup.perpMarkets[marketIndex].makerFee.toFixed()
)
} else {
takerFee = rates.taker
makerFee = rates.maker
}
return { makerFee, takerFee }
}

View File

@ -9,10 +9,11 @@ import {
} from '@blockworks-foundation/mango-client'
import { Market, Orderbook } from '@project-serum/serum'
import { Order } from '@project-serum/serum/lib/market'
import { PerpTriggerOrder } from '../@types/types'
import useMangoStore from '../stores/useMangoStore'
type OrderInfo = {
order: Order | PerpOrder
order: Order | PerpOrder | PerpTriggerOrder
market: { account: Market | PerpMarket; config: MarketConfig }
}
@ -65,10 +66,33 @@ function parsePerpOpenOrders(
o.owner.equals(mangoAccount.publicKey)
)
return openOrdersForMarket.map<OrderInfo>((order) => ({
order,
market: { account: market, config: config },
}))
const advancedOrdersForMarket = mangoAccount.advancedOrders
.map((o, i) => {
const pto = o.perpTrigger
if (pto.isActive && pto.marketIndex == config.marketIndex) {
return {
...o,
orderId: i,
marketIndex: pto.marketIndex,
orderType: pto.orderType,
side: pto.side,
price: market.priceLotsToNumber(pto.price),
size: market.baseLotsToNumber(pto.quantity),
triggerCondition: pto.triggerCondition,
triggerPrice: pto.triggerPrice.toNumber(),
}
} else {
return undefined
}
})
.filter((o) => !!o)
return openOrdersForMarket
.concat(advancedOrdersForMarket)
.map<OrderInfo>((order) => ({
order,
market: { account: market, config: config },
}))
}
export function useOpenOrders() {

View File

@ -103,7 +103,8 @@ export default function useWallet() {
state.wallet.connected = true
})
// set connected before fetching data
await actions.fetchMangoAccounts()
await actions.fetchAllMangoAccounts()
actions.reloadOrders()
actions.fetchTradeHistory()
actions.fetchWalletTokens()
notify({
@ -147,12 +148,6 @@ export default function useWallet() {
}
}, 90 * SECONDS)
useInterval(() => {
if (connected && mangoAccount) {
actions.fetchMangoAccounts()
}
}, 180 * SECONDS)
useInterval(() => {
if (connected && mangoAccount) {
actions.reloadMangoAccount()

View File

@ -31,7 +31,7 @@
}
},
"dependencies": {
"@blockworks-foundation/mango-client": "git+https://github.com/blockworks-foundation/mango-client-v3.git",
"@blockworks-foundation/mango-client": "git+https://github.com/blockworks-foundation/mango-client-v3.git#v3.1",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
"@headlessui/react": "^1.2.0",

View File

@ -10,10 +10,10 @@ import useMangoStore from '../stores/useMangoStore'
import { copyToClipboard } from '../utils'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import AccountOrders from '../components/account-page/AccountOrders'
import AccountHistory from '../components/account-page/AccountHistory'
import AccountOrders from '../components/account_page/AccountOrders'
import AccountHistory from '../components/account_page/AccountHistory'
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 Button from '../components/Button'
import EmptyState from '../components/EmptyState'
@ -23,7 +23,7 @@ import Swipeable from '../components/mobile/Swipeable'
import Tabs from '../components/Tabs'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from '../components/TradePageGrid'
import AccountInterest from '../components/account-page/AccountInterest'
import AccountInterest from '../components/account_page/AccountInterest'
const TABS = [
'Portfolio',

View File

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

View File

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

View File

@ -51,7 +51,7 @@ export const ENDPOINTS: EndpointInfo[] = [
type ClusterType = 'mainnet' | 'devnet'
const CLUSTER = (process.env.NEXT_PUBLIC_CLUSTER as ClusterType) || 'mainnet'
const CLUSTER = (process.env.NEXT_PUBLIC_CLUSTER as ClusterType) || 'devnet'
const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER)
export const WEBSOCKET_CONNECTION = new Connection(
@ -59,7 +59,7 @@ export const WEBSOCKET_CONNECTION = new Connection(
'processed' as Commitment
)
const DEFAULT_MANGO_GROUP_NAME = process.env.NEXT_PUBLIC_GROUP || 'mainnet.1'
const DEFAULT_MANGO_GROUP_NAME = process.env.NEXT_PUBLIC_GROUP || 'devnet.2'
const DEFAULT_MANGO_GROUP_CONFIG = Config.ids().getGroup(
CLUSTER,
DEFAULT_MANGO_GROUP_NAME
@ -148,7 +148,15 @@ interface MangoStore extends State {
price: number | ''
baseSize: number | ''
quoteSize: number | ''
tradeType: 'Market' | 'Limit'
tradeType:
| 'Market'
| 'Limit'
| 'Stop Loss'
| 'Take Profit'
| 'Stop Limit'
| 'Take Profit Limit'
triggerPrice: number | ''
triggerCondition: 'above' | 'below'
}
wallet: {
providerUrl: string
@ -223,6 +231,8 @@ const useMangoStore = create<MangoStore>((set, get) => {
quoteSize: '',
tradeType: 'Limit',
price: '',
triggerPrice: '',
triggerCondition: 'above',
},
wallet: INITIAL_STATE.WALLET,
settings: {
@ -262,7 +272,7 @@ const useMangoStore = create<MangoStore>((set, get) => {
})
}
},
async fetchMangoAccounts() {
async fetchAllMangoAccounts() {
const set = get().set
const mangoGroup = get().selectedMangoGroup.current
const mangoClient = get().connection.client
@ -283,14 +293,7 @@ const useMangoStore = create<MangoStore>((set, get) => {
set((state) => {
state.selectedMangoAccount.initialLoad = false
state.mangoAccounts = sortedAccounts
if (state.selectedMangoAccount.current) {
state.selectedMangoAccount.current = mangoAccounts.find(
(ma) =>
ma.publicKey.equals(
state.selectedMangoAccount.current.publicKey
)
)
} else {
if (!state.selectedMangoAccount.current) {
const lastAccount = localStorage.getItem(LAST_ACCOUNT_KEY)
state.selectedMangoAccount.current =
mangoAccounts.find(
@ -443,20 +446,36 @@ const useMangoStore = create<MangoStore>((set, get) => {
const set = get().set
const mangoAccount = get().selectedMangoAccount.current
const connection = get().connection.current
const [reloadedMangoAccount, reloadedOpenOrders] = await Promise.all([
mangoAccount.reload(connection),
mangoAccount.loadOpenOrders(
const reloadedMangoAccount = await mangoAccount.reload(connection)
await Promise.all([
reloadedMangoAccount.loadOpenOrders(
connection,
new PublicKey(serumProgramId)
),
reloadedMangoAccount.loadAdvancedOrders(connection),
])
reloadedMangoAccount.spotOpenOrdersAccounts = reloadedOpenOrders
set((state) => {
state.selectedMangoAccount.current = reloadedMangoAccount
})
},
async updateOpenOrders() {
async reloadOrders() {
const mangoAccount = get().selectedMangoAccount.current
const connection = get().connection.current
if (mangoAccount) {
await Promise.all([
mangoAccount.loadOpenOrders(
connection,
new PublicKey(serumProgramId)
),
mangoAccount.loadAdvancedOrders(connection),
])
}
},
// DEPRECATED
async _updateOpenOrders() {
const set = get().set
const connection = get().connection.current
const bidAskAccounts = Object.keys(get().accountInfos).map(

View File

@ -2,6 +2,7 @@ import { I80F48 } from '@blockworks-foundation/mango-client/lib/src/fixednum'
import { TOKEN_MINTS } from '@project-serum/serum'
import { PublicKey } from '@solana/web3.js'
import BN from 'bn.js'
import { TRIGGER_ORDER_TYPES } from '../components/trade_form/AdvancedTradeForm'
import { Orderbook } from '../stores/useMangoStore'
export async function sleep(ms) {
@ -101,10 +102,13 @@ export function calculateTradePrice(
orderBook: Orderbook,
baseSize: number,
side: 'buy' | 'sell',
price: string | number
price: string | number,
triggerPrice?: string | number
): number {
if (tradeType === 'Market') {
return calculateMarketPrice(orderBook, baseSize, side)
} else if (TRIGGER_ORDER_TYPES.includes(tradeType)) {
return Number(triggerPrice)
}
return Number(price)
}

View File

@ -995,10 +995,11 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@blockworks-foundation/mango-client@git+https://github.com/blockworks-foundation/mango-client-v3.git":
"@blockworks-foundation/mango-client@git+https://github.com/blockworks-foundation/mango-client-v3.git#v3.1":
version "3.0.24"
resolved "git+https://github.com/blockworks-foundation/mango-client-v3.git#4cc24f3aec295ee93490eff2352bf98412927fc4"
resolved "git+https://github.com/blockworks-foundation/mango-client-v3.git#75ca2eb8abf68a74a920a2f51dd6db1fb52bc781"
dependencies:
"@project-serum/anchor" "^0.16.2"
"@project-serum/serum" "0.13.55"
"@project-serum/sol-wallet-adapter" "^0.2.0"
"@solana/spl-token" "^0.1.6"
@ -1488,6 +1489,26 @@
snake-case "^3.0.4"
toml "^3.0.0"
"@project-serum/anchor@^0.16.2":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.16.2.tgz#b8b4ec4c749d59a224108f8d82ab68217ef752ae"
integrity sha512-wOJwObd4wOZ5tRRMCKYjeMNsEmf7vuC71KQRnw6wthhErL8c/818n4gYIZCf/1ZPl/8WPruIlmtQHDSEyy2+0Q==
dependencies:
"@project-serum/borsh" "^0.2.2"
"@solana/web3.js" "^1.17.0"
base64-js "^1.5.1"
bn.js "^5.1.2"
bs58 "^4.0.1"
buffer-layout "^1.2.0"
camelcase "^5.3.1"
crypto-hash "^1.3.0"
eventemitter3 "^4.0.7"
find "^0.3.0"
js-sha256 "^0.9.0"
pako "^2.0.3"
snake-case "^3.0.4"
toml "^3.0.0"
"@project-serum/borsh@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@project-serum/borsh/-/borsh-0.2.2.tgz#63e558f2d6eb6ab79086bf499dea94da3182498f"
@ -1863,14 +1884,14 @@
integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==
"@types/node@*":
version "16.10.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e"
integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==
version "16.10.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5"
integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ==
"@types/node@^12.12.54":
version "12.20.27"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.27.tgz#4141fcad57c332a120591de883e26fe4bb14aaea"
integrity sha512-qZdePUDSLAZRXXV234bLBEUM0nAQjoxbcSwp1rqSMUe1rZ47mwU6OjciR/JvF1Oo8mc0ys6GE0ks0HGgqAZoGg==
version "12.20.28"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.28.tgz#4b20048c6052b5f51a8d5e0d2acbf63d5a17e1e2"
integrity sha512-cBw8gzxUPYX+/5lugXIPksioBSbE42k0fZ39p+4yRzfYjN6++eq9kAPdlY9qm+MXyfbk9EmvCYAYRn380sF46w==
"@types/node@^14.14.25":
version "14.17.3"
@ -7726,9 +7747,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
inherits "^2.0.1"
rpc-websockets@^7.4.2:
version "7.4.14"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.4.14.tgz#d1774ce4d4c231dea6ed448d2bc224587b9561a5"
integrity sha512-x/2Rwzla6bXAyE8A21yx3sHjn49JUlgBUYfnKurNeqrZQgFxfD43Udo5NkTWQp+TASrssTlks8ipcJfvswgv5g==
version "7.4.15"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.4.15.tgz#1f6b4640231c1d6479b9fda347e418d0b54ddab5"
integrity sha512-ICuqy1MeGl7z0/kesAShN1Lz0AYvOPLpmv2hzfIV5YNGhmJl+B/osmFoWrvxZcQnn/wyPl4Q29ItTJkXFNc9oA==
dependencies:
"@babel/runtime" "^7.11.2"
circular-json "^0.5.9"