add trade form component

This commit is contained in:
Tyler Shipe 2021-04-02 14:26:21 -04:00
parent 37cf5bae14
commit f03f1919a4
36 changed files with 3753 additions and 558 deletions

View File

@ -10,5 +10,15 @@
}
]
],
"plugins": ["xwind/babel", "@emotion/babel-plugin"]
}
"plugins": [
"xwind/babel",
"@emotion/babel-plugin",
[
"import",
{
"libraryName": "antd",
"style": true
}
]
]
}

View File

@ -1,13 +1,11 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"plugins": ["@typescript-eslint", "react-hooks"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
// Uncomment the following lines to enable eslint-config-prettier
// Is not enabled right now to avoid issues with the Next.js repo
// "prettier",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"env": {
"es6": true,
@ -24,6 +22,8 @@
"react/react-in-jsx-scope": 0,
"react/display-name": 0,
"react/prop-types": 0,
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/explicit-function-return-type": 0,

View File

@ -1,186 +1,201 @@
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import Wallet from '@project-serum/sol-wallet-adapter';
import { Market, OpenOrders } from '@project-serum/serum';
import { Event } from '@project-serum/serum/lib/queue';
import { Order } from '@project-serum/serum/lib/market';
import { MangoGroup, MarginAccount, MangoClient } from '@blockworks-foundation/mango-client';
import { MangoSrmAccount } from '@blockworks-foundation/mango-client/lib/client';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js'
import Wallet from '@project-serum/sol-wallet-adapter'
import { Market, OpenOrders } from '@project-serum/serum'
import { Event } from '@project-serum/serum/lib/queue'
import { Order } from '@project-serum/serum/lib/market'
import {
MangoGroup,
MarginAccount,
MangoClient,
} from '@blockworks-foundation/mango-client'
import { MangoSrmAccount } from '@blockworks-foundation/mango-client/lib/client'
export interface ConnectionContextValues {
endpoint: string;
setEndpoint: (newEndpoint: string) => void;
connection: Connection;
sendConnection: Connection;
availableEndpoints: EndpointInfo[];
setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void;
endpoint: string
setEndpoint: (newEndpoint: string) => void
connection: Connection
sendConnection: Connection
availableEndpoints: EndpointInfo[]
setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void
}
export interface WalletContextValues {
wallet: Wallet;
connected: boolean;
providerUrl: string;
setProviderUrl: (newProviderUrl: string) => void;
providerName: string;
wallet: Wallet
connected: boolean
providerUrl: string
setProviderUrl: (newProviderUrl: string) => void
providerName: string
}
export interface MarketInfo {
address: PublicKey;
name: string;
programId: PublicKey;
deprecated: boolean;
quoteLabel?: string;
baseLabel?: string;
address: PublicKey
name: string
programId: PublicKey
deprecated: boolean
quoteLabel?: string
baseLabel?: string
}
export interface CustomMarketInfo {
address: string;
name: string;
programId: string;
quoteLabel?: string;
baseLabel?: string;
address: string
name: string
programId: string
quoteLabel?: string
baseLabel?: string
}
export interface FullMarketInfo {
address?: PublicKey;
name?: string;
programId?: PublicKey;
deprecated?: boolean;
quoteLabel?: string;
baseLabel?: string;
marketName?: string;
baseCurrency?: string;
quoteCurrency?: string;
marketInfo?: MarketInfo;
address?: PublicKey
name?: string
programId?: PublicKey
deprecated?: boolean
quoteLabel?: string
baseLabel?: string
marketName?: string
baseCurrency?: string
quoteCurrency?: string
marketInfo?: MarketInfo
}
export interface MarketContextValues extends FullMarketInfo {
market: Market | undefined | null;
setMarketAddress: (newMarketAddress: string) => void;
market: Market | undefined | null
setMarketAddress: (newMarketAddress: string) => void
}
export interface TokenAccount {
pubkey: PublicKey;
account: AccountInfo<Buffer> | null;
effectiveMint: PublicKey;
pubkey: PublicKey
account: AccountInfo<Buffer> | null
effectiveMint: PublicKey
}
export interface Trade extends Event {
side: string;
price: number;
feeCost: number;
size: number;
side: string
price: number
feeCost: number
size: number
}
export interface OrderWithMarket extends Order {
marketName: string;
marketName: string
}
export interface OrderWithMarketAndMarketName extends Order {
market: Market;
marketName: string | undefined;
market: Market
marketName: string | undefined
}
interface BalancesBase {
key: string;
coin: string;
wallet?: number | null | undefined;
orders?: number | null | undefined;
openOrders?: OpenOrders | null | undefined;
unsettled?: number | null | undefined;
key: string
coin: string
wallet?: number | null | undefined
orders?: number | null | undefined
openOrders?: OpenOrders | null | undefined
unsettled?: number | null | undefined
}
export interface Balances extends BalancesBase {
market?: Market | null | undefined;
marginDeposits?: number | null | undefined;
borrows?: number | null | undefined;
net?: number | null | undefined;
market?: Market | null | undefined
marginDeposits?: number | null | undefined
borrows?: number | null | undefined
net?: number | null | undefined
}
export interface OpenOrdersBalances extends BalancesBase {
market?: string | null | undefined;
baseCurrencyAccount: { pubkey: PublicKey; account: AccountInfo<Buffer> } | null | undefined;
quoteCurrencyAccount: { pubkey: PublicKey; account: AccountInfo<Buffer> } | null | undefined;
market?: string | null | undefined
baseCurrencyAccount:
| { pubkey: PublicKey; account: AccountInfo<Buffer> }
| null
| undefined
quoteCurrencyAccount:
| { pubkey: PublicKey; account: AccountInfo<Buffer> }
| null
| undefined
}
export interface DeprecatedOpenOrdersBalances extends BalancesBase {
market: Market | null | undefined;
marketName: string | null | undefined;
market: Market | null | undefined
marketName: string | null | undefined
}
export interface PreferencesContextValues {
autoSettleEnabled: boolean;
setAutoSettleEnabled: (newAutoSettleEnabled: boolean) => void;
autoSettleEnabled: boolean
setAutoSettleEnabled: (newAutoSettleEnabled: boolean) => void
}
export interface EndpointInfo {
name: string;
endpoint: string;
custom: boolean;
name: string
endpoint: string
custom: boolean
}
/**
* {tokenMint: preferred token account's base58 encoded public key}
*/
export interface SelectedTokenAccounts {
[tokenMint: string]: string;
[tokenMint: string]: string
}
export interface BonfidaTrade {
market: string;
size: number;
price: number;
orderId: string;
time: number;
side: string;
feeCost: number;
marketAddress: string;
export interface ChartType {
market: string
size: number
price: number
orderId: string
time: number
side: string
feeCost: number
marketAddress: string
}
export interface FeeRates {
taker: number;
maker: number;
taker: number
maker: number
}
export interface SwapContextValues {
slippage: number;
setSlippage: (newSlippage: number) => void;
tokenProgramId: PublicKey;
swapProgramId: PublicKey;
legacySwapProgramIds: PublicKey[];
programIds: () => { token: PublicKey; swap: PublicKey };
slippage: number
setSlippage: (newSlippage: number) => void
tokenProgramId: PublicKey
swapProgramId: PublicKey
legacySwapProgramIds: PublicKey[]
programIds: () => { token: PublicKey; swap: PublicKey }
}
// Margin Account Type declaration
export interface MarginAccountContextValues {
marginAccount: MarginAccount | null; // The current margin account trading with
marginAccounts: MarginAccount[] | []; // List of all margin account pk in a mango group
mango_groups: Array<string>; // Identifier for the mango group
mangoOptions: any; //The different parameters for our mango program
mangoClient: MangoClient; // Instance of mango clinet
mangoGroup: MangoGroup | null; // The current mango group
setMarginAccount: (marginAccount: null | MarginAccount) => void;
setMarginAccounts: (marginAccounts: MarginAccount[]) => void;
createMarginAccount: () => Promise<MarginAccount | null>; // For creating a margin account
maPending: any; // Is the context updating
setMAPending: (any) => void; // Set the pending states on margin account transactions
getMarginAccount: (pubKey: PublicKey | undefined) => Promise<MarginAccount | null>;
size: { currency: string; size: number }; // The size of buy or sell on tradeform
setSize: (size: { currency: string; size: number }) => void; // Set the size on trade form
srmFeeRates: FeeRates | null;
totalSrm: number;
contributedSrm: number;
mangoSrmAccounts: MangoSrmAccount[] | null;
getUserSrmInfo: () => void;
marginAccount: MarginAccount | null // The current margin account trading with
marginAccounts: MarginAccount[] | [] // List of all margin account pk in a mango group
mango_groups: Array<string> // Identifier for the mango group
mangoOptions: any //The different parameters for our mango program
mangoClient: MangoClient // Instance of mango clinet
mangoGroup: MangoGroup | null // The current mango group
setMarginAccount: (marginAccount: null | MarginAccount) => void
setMarginAccounts: (marginAccounts: MarginAccount[]) => void
createMarginAccount: () => Promise<MarginAccount | null> // For creating a margin account
maPending: any // Is the context updating
setMAPending: (any) => void // Set the pending states on margin account transactions
getMarginAccount: (
pubKey: PublicKey | undefined
) => Promise<MarginAccount | null>
size: { currency: string; size: number } // The size of buy or sell on tradeform
setSize: (size: { currency: string; size: number }) => void // Set the size on trade form
srmFeeRates: FeeRates | null
totalSrm: number
contributedSrm: number
mangoSrmAccounts: MangoSrmAccount[] | null
getUserSrmInfo: () => void
}
// Type declaration for the margin accounts for the mango group
export type mangoTokenAccounts = { mango_group: string; accounts: TokenAccount[] };
export type mangoTokenAccounts = {
mango_group: string
accounts: TokenAccount[]
}
// Token infos
export interface KnownToken {
tokenSymbol: string;
tokenName: string;
icon?: string;
mintAddress: string;
tokenSymbol: string
tokenName: string
icon?: string
mintAddress: string
}

View File

@ -4,8 +4,10 @@ import xw from 'xwind'
import useInterval from '../hooks/useInterval'
import usePrevious from '../hooks/usePrevious'
import { isEqual, getDecimalCount } from '../utils/'
// import { useMarket, useOrderbook, useMarkPrice } from '../utils/markets'
// import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
import { ArrowUpIcon, ArrowDownIcon } from '@heroicons/react/solid'
import useMarkPrice from '../hooks/useMarkPrice'
import useOrderbook from '../hooks/useOrderbook'
import useMarkets from '../hooks/useMarkets'
const Title = styled.div`
color: rgba(255, 255, 255, 1);
@ -16,11 +18,6 @@ const SizeTitle = styled.div`
color: #434a59;
`
const MarkPriceTitle = styled.div`
padding: 4px 0 4px;
font-weight: 700;
`
const Line = styled.div<any>`
text-align: ${(props) => (props.invert ? 'left' : 'right')};
float: ${(props) => (props.invert ? 'left' : 'right')};
@ -41,7 +38,7 @@ export default function Orderbook({ depth = 7 }) {
const smallScreen = false
const markPrice = useMarkPrice()
const [orderbook] = useOrderbook()
const { baseCurrency, quoteCurrency } = useMarket()
const { baseCurrency, quoteCurrency } = useMarkets()
const currentOrderbookData = useRef(null)
const lastOrderbookData = useRef(null)
@ -99,116 +96,108 @@ export default function Orderbook({ depth = 7 }) {
return cumulative
}
return null
// return (
// <>
// <div>
// <Title>Orderbook</Title>
// </div>
// {smallScreen ? (
// <>
// <MarkPriceComponent markPrice={markPrice} />
// <div css={xw`flex`}>
// <div css={xw`flex`} flex={1}>
// <SizeTitle>
// <div css={xw`text-left`}>
// Size
// <div style={{ marginTop: -5 }}>({baseCurrency})</div>
// </div>
// <div css={xw`text-right pr-4`}>
// Price
// <div style={{ marginTop: -5 }}>({quoteCurrency})</div>
// </div>
// </SizeTitle>
// {orderbookData?.bids.map(({ price, size, sizePercent }) => (
// <OrderbookRow
// key={price + ''}
// price={price}
// size={size}
// side={'buy'}
// sizePercent={sizePercent}
// // onPriceClick={() => onPrice(price)}
// // onSizeClick={() => onSize(size)}
// />
// ))}
// </div>
// <Col flex={1} style={{ paddingLeft: 2 }}>
// <SizeTitle>
// <Col span={12} style={{ textAlign: 'left' }}>
// Price
// <div style={{ marginTop: -5 }}>({quoteCurrency})</div>
// </Col>
// <Col span={12} style={{ textAlign: 'right ' }}>
// Size
// <div style={{ marginTop: -5 }}>({baseCurrency})</div>
// </Col>
// </SizeTitle>
// {orderbookData?.asks
// .slice(0)
// .reverse()
// .map(({ price, size, sizePercent }) => (
// <OrderbookRow
// invert={true}
// key={price + ''}
// price={price}
// size={size}
// side={'sell'}
// sizePercent={sizePercent}
// onPriceClick={() => onPrice(price)}
// onSizeClick={() => onSize(size)}
// />
// ))}
// </Col>
// </div>
// </>
// ) : (
// <>
// <SizeTitle>
// <Col span={12} style={{ textAlign: 'left' }}>
// Size ({baseCurrency})
// </Col>
// <Col span={12} style={{ textAlign: 'right' }}>
// Price ({quoteCurrency})
// </Col>
// </SizeTitle>
// {orderbookData?.asks.map(({ price, size, sizePercent }) => (
// <OrderbookRow
// key={price + ''}
// price={price}
// size={size}
// side={'sell'}
// sizePercent={sizePercent}
// onPriceClick={() => onPrice(price)}
// onSizeClick={() => onSize(size)}
// />
// ))}
// <MarkPriceComponent markPrice={markPrice} />
// {orderbookData?.bids.map(({ price, size, sizePercent }) => (
// <OrderbookRow
// key={price + ''}
// price={price}
// size={size}
// side={'buy'}
// sizePercent={sizePercent}
// onPriceClick={() => onPrice(price)}
// onSizeClick={() => onSize(size)}
// />
// ))}
// </>
// )}
// </>
// )
return (
<>
<div>
<Title>Orderbook</Title>
</div>
{smallScreen ? (
<>
<MarkPriceComponent markPrice={markPrice} />
<div css={xw`flex`}>
<div css={xw`flex`}>
<SizeTitle>
<div css={xw`text-left`}>
Size
<div style={{ marginTop: -5 }}>({baseCurrency})</div>
</div>
<div css={xw`text-right pr-4`}>
Price
<div style={{ marginTop: -5 }}>({quoteCurrency})</div>
</div>
</SizeTitle>
{orderbookData?.bids.map(({ price, size, sizePercent }) => (
<OrderbookRow
key={price + ''}
price={price}
size={size}
side={'buy'}
sizePercent={sizePercent}
// onPriceClick={() => onPrice(price)}
// onSizeClick={() => onSize(size)}
/>
))}
</div>
<div css={xw`flex pl-1`}>
<SizeTitle>
<div css={xw`text-left`}>
Price
<div style={{ marginTop: -5 }}>({quoteCurrency})</div>
</div>
<div css={xw`text-right`}>
Size
<div style={{ marginTop: -5 }}>({baseCurrency})</div>
</div>
</SizeTitle>
{orderbookData?.asks
.slice(0)
.reverse()
.map(({ price, size, sizePercent }) => (
<OrderbookRow
invert
key={price + ''}
price={price}
size={size}
side={'sell'}
sizePercent={sizePercent}
onPriceClick={() => alert(`price ${price}`)}
onSizeClick={() => alert(`size ${size}`)}
/>
))}
</div>
</div>
</>
) : (
<>
<SizeTitle>
<div style={{ textAlign: 'left' }}>Size ({baseCurrency})</div>
<div style={{ textAlign: 'right' }}>Price ({quoteCurrency})</div>
</SizeTitle>
{orderbookData?.asks.map(({ price, size, sizePercent }) => (
<OrderbookRow
key={price + ''}
price={price}
size={size}
side={'sell'}
sizePercent={sizePercent}
onPriceClick={() => alert(`price ${price}`)}
onSizeClick={() => alert(`size ${size}`)}
/>
))}
<MarkPriceComponent markPrice={markPrice} />
{orderbookData?.bids.map(({ price, size, sizePercent }) => (
<OrderbookRow
key={price + ''}
price={price}
size={size}
side={'buy'}
sizePercent={sizePercent}
onPriceClick={() => alert(`price ${price}`)}
onSizeClick={() => alert(`size ${size}`)}
/>
))}
</>
)}
</>
)
}
const OrderbookRow = React.memo(
const OrderbookRow = React.memo<any>(
({ side, price, size, sizePercent, onSizeClick, onPriceClick, invert }) => {
const element = useRef()
const { market } = useMarket()
const element = useRef(null)
const { market } = useMarkets()
useEffect(() => {
// eslint-disable-next-line
!element.current?.classList.contains('flash') &&
element.current?.classList.add('flash')
const id = setTimeout(
@ -257,10 +246,8 @@ const OrderbookRow = React.memo(
</>
) : (
<>
<Col span={12} style={{ textAlign: 'left' }}>
{formattedSize}
</Col>
<Col span={12} style={{ textAlign: 'right' }}>
<div css={xw`text-left`}>{formattedSize}</div>
<div css={xw`text-right`}>
<Line
data-width={sizePercent + '%'}
data-bgcolor={side === 'buy' ? '#5b6b16' : '#E54033'}
@ -271,7 +258,7 @@ const OrderbookRow = React.memo(
>
{formattedPrice}
</Price>
</Col>
</div>
</>
)}
</div>
@ -281,10 +268,10 @@ const OrderbookRow = React.memo(
isEqual(prevProps, nextProps, ['price', 'size', 'sizePercent'])
)
const MarkPriceComponent = React.memo(
const MarkPriceComponent = React.memo<{ markPrice: number }>(
({ markPrice }) => {
const { market } = useMarket()
const previousMarkPrice = usePrevious(markPrice)
const { market } = useMarkets()
const previousMarkPrice: number = usePrevious(markPrice)
const markPriceColor =
markPrice > previousMarkPrice
@ -299,17 +286,18 @@ const MarkPriceComponent = React.memo(
markPrice.toFixed(getDecimalCount(market.tickSize))
return (
<MarkPriceTitle justify="center">
<div style={{ color: markPriceColor }}>
{markPrice > previousMarkPrice && (
<ArrowUpOutlined style={{ marginRight: 5 }} />
)}
{markPrice < previousMarkPrice && (
<ArrowDownOutlined style={{ marginRight: 5 }} />
)}
{formattedMarkPrice || '----'}
</div>
</MarkPriceTitle>
<div
css={xw`flex justify-center items-center font-bold p-1`}
style={{ color: markPriceColor }}
>
{markPrice > previousMarkPrice && (
<ArrowUpIcon css={xw`h-5 w-5 mr-1`} />
)}
{markPrice < previousMarkPrice && (
<ArrowDownIcon css={xw`h-5 w-5 mr-1`} />
)}
{formattedMarkPrice || '----'}
</div>
)
},
(prevProps, nextProps) => isEqual(prevProps, nextProps, ['markPrice'])

View File

@ -1,16 +1,17 @@
import { useState } from 'react'
import xw from 'xwind'
import MenuItem from './MenuItem'
import useStore from '../hooks/useStore'
import { useWallet } from '../hooks/useWallet'
import useWallet from '../hooks/useWallet'
const TopBar = () => {
const connected = useStore((state) => state.wallet.connected)
const wallet = useWallet()
console.log('load topbar')
const { connected, wallet } = useWallet()
const [showMenu, setShowMenu] = useState(false)
const handleConnectDisconnect = () =>
const handleConnectDisconnect = () => {
connected ? wallet.disconnect() : wallet.connect()
}
return (
<nav css={xw`bg-mango-dark`}>

404
components/TradeForm.tsx Normal file
View File

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

View File

@ -0,0 +1,56 @@
// import xw from 'xwind'
import dynamic from 'next/dynamic'
import { Responsive, WidthProvider } from 'react-grid-layout'
const TVChartContainer = dynamic(
() => import('../components/TradingView/index'),
{ ssr: false }
)
import FloatingElement from '../components/FloatingElement'
import Orderbook from '../components/Orderbook'
import TradeForm from './TradeForm'
const ResponsiveGridLayout = WidthProvider(Responsive)
const TradePageGrid = () => {
const layouts = {
lg: [
{ i: '1', x: 0, y: 0, w: 2, h: 2 },
{ i: '2', x: 2, y: 0, w: 1, h: 2 },
{ i: '3', x: 3, y: 0, w: 1, h: 1 },
{ i: '4', x: 3, y: 1, w: 1, h: 1 },
{ i: '5', x: 0, y: 2, w: 2, h: 1 },
{ i: '6', x: 2, y: 2, w: 1, h: 1 },
{ i: '7', x: 3, y: 2, w: 1, h: 1 },
],
}
return (
<ResponsiveGridLayout
className="layout"
layouts={layouts}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 0 }}
cols={{ lg: 4, md: 3, sm: 2, xs: 1 }}
rowHeight={300}
>
<div key="1">
<FloatingElement>
<TVChartContainer />
</FloatingElement>
</div>
<div key="2">
<FloatingElement>
<Orderbook />
</FloatingElement>
</div>
<div key="3">
<TradeForm />
</div>
<div key="4">4</div>
<div key="5">5</div>
<div key="6">6</div>
<div key="7">7</div>
</ResponsiveGridLayout>
)
}
export default TradePageGrid

View File

@ -6,7 +6,7 @@ import {
ResolutionString,
} from '../charting_library' // Make sure to follow step 1 of the README
// import { useMarket } from '../../utils/markets';
import { BONFIDA_DATA_FEED } from '../../utils/bonfidaConnector'
import { CHART_DATA_FEED } from '../../utils/chartDataConnector'
// This is a basic example of how to create a TV widget
// You can add more feature such as storing charts in localStorage
@ -36,7 +36,7 @@ const TVChartContainer = () => {
interval: '60' as ResolutionString,
theme: 'Dark',
containerId: 'tv_chart_container',
datafeedUrl: BONFIDA_DATA_FEED,
datafeedUrl: CHART_DATA_FEED,
libraryPath: '/charting_library/',
fullscreen: false,
autosize: true,

View File

@ -1,33 +1,27 @@
import { useEffect, useMemo } from 'react'
import { Account, Connection } from '@solana/web3.js'
import { IDS } from '@blockworks-foundation/mango-client'
import { EndpointInfo } from '../@types/types'
import useStore from './useStore'
export const ENDPOINTS: EndpointInfo[] = [
{
name: 'mainnet-beta',
endpoint: 'https://solana-api.projectserum.com',
custom: false,
},
{
name: 'devnet',
endpoint: 'https://devnet.solana.com',
custom: false,
},
]
const cluster = 'mainnet-beta'
import useSolanaStore from '../stores/useSolanaStore'
const useConnection = () => {
const setConnection = useStore((state) => state.connection.setConnection)
const { endpoint } = ENDPOINTS.find((e) => e.name === cluster)
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
const { cluster, current: connection, endpoint } = useSolanaStore(
(s) => s.connection
)
const setSolanaStore = useSolanaStore((s) => s.set)
const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [
endpoint,
])
useEffect(() => {
setConnection(connection)
// @ts-ignore
if (connection && endpoint === connection._rpcEndpoint) return
console.log('setting new connection')
const newConnection = new Connection(endpoint, 'recent')
setSolanaStore((state) => {
state.connection.current = newConnection
})
}, [endpoint])
useEffect(() => {
@ -47,7 +41,7 @@ const useConnection = () => {
const programId = IDS[cluster].mango_program_id
const dexProgramId = IDS[cluster]?.dex_program_id
return { connection, dexProgramId, cluster, programId }
return { connection, dexProgramId, cluster, programId, sendConnection }
}
export default useConnection

22
hooks/useIpAddress.tsx Normal file
View File

@ -0,0 +1,22 @@
import { useEffect, useState } from 'react'
export default function useIpAddress() {
const [ipAllowed, setIpAllowed] = useState(true)
useEffect(() => {
const checkIpLocation = async () => {
const response = await fetch(`https://www.cloudflare.com/cdn-cgi/trace`)
const parsedResponse = await response.text()
const ipLocation = parsedResponse.match(/loc=(.+)/)
const ipCountryCode = ipLocation ? ipLocation[1] : ''
if (ipCountryCode) {
setIpAllowed(ipCountryCode !== 'US')
}
}
checkIpLocation()
}, [])
return { ipAllowed }
}

View File

@ -0,0 +1,86 @@
import { useCallback, useEffect, useMemo } from 'react'
import { PublicKey } from '@solana/web3.js'
import { IDS } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import useConnection from './useConnection'
import useInterval from './useInterval'
import useSolanaStore from '../stores/useSolanaStore'
const useMarginAccount = () => {
const mangoClient = useMangoStore((s) => s.mangoClient)
const tradeForm = useMangoStore((s) => s.tradeForm)
const selectedMangoGroup = useMangoStore((s) => s.selectedMangoGroup)
const selectedMarginAccount = useMangoStore((s) => s.selectedMarginAccount)
const mangoGroup = useMangoStore((s) => s.mangoGroup)
const setMangoStore = useMangoStore((s) => s.set)
const { current: wallet } = useSolanaStore((s) => s.wallet)
const { cluster, connection } = useConnection()
const clusterIds = useMemo(() => IDS[cluster], [cluster])
const mangoGroupIds = useMemo(
() => clusterIds.mango_groups[selectedMangoGroup],
[clusterIds, selectedMangoGroup]
)
const fetchMangoGroup = useCallback(() => {
if (!mangoClient) return
const mangoGroupPk = new PublicKey(mangoGroupIds.mango_group_pk)
const srmVaultPk = new PublicKey(mangoGroupIds.srm_vault_pk)
mangoClient
.getMangoGroup(connection, mangoGroupPk, srmVaultPk)
.then((mangoGroup) => {
// Set the mango group
setMangoStore((state) => {
state.mangoGroup = mangoGroup
})
})
.catch((err) => {
console.error('Could not get mango group: ', err)
})
}, [connection, mangoClient, mangoGroupIds, setMangoStore])
const fetchMarginAccounts = useCallback(() => {
if (!mangoClient || !wallet || !mangoGroup) return
mangoClient
.getMarginAccountsForOwner(
connection,
new PublicKey(clusterIds.mango_program_id),
mangoGroup,
wallet
)
.then((marginAccounts) => {
if (marginAccounts.length > 0) {
setMangoStore((state) => {
state.marginAcccounts = marginAccounts
state.selectedMarginAccount = marginAccounts[0]
})
}
})
.catch((err) => {
console.error('Could not get margin accounts for user in effect ', err)
})
}, [mangoClient, connection, mangoGroup, wallet])
// useEffect(() => {
// fetchMangoGroup()
// }, [fetchMangoGroup])
// useInterval(() => {
// fetchMarginAccounts()
// fetchMangoGroup()
// }, 20000)
return {
mangoClient,
setMangoStore,
tradeForm,
mangoGroup,
marginAccount: selectedMarginAccount,
}
}
export default useMarginAccount

78
hooks/useMarkPrice.tsx Normal file
View File

@ -0,0 +1,78 @@
import { useEffect, useState } from 'react'
import useMangoStore from '../stores/useMangoStore'
import useInterval from './useInterval'
import useSerumStore from '../stores/useSerumStore'
import useOrderbook from './useOrderbook'
import useConnection from './useConnection'
// const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
// For things that don't really change
const _SLOW_REFRESH_INTERVAL = 5 * 1000
// For things that change frequently
// const _FAST_REFRESH_INTERVAL = 1000;
export function _useUnfilteredTrades(limit = 10000) {
console.log('fetching unfiltered trades')
const market = useMangoStore((state) => state.market.current)
const { connection } = useConnection()
const setSerumStore = useSerumStore((state) => state.set)
useInterval(() => {
async function fetchFills() {
if (!market || !connection) {
return null
}
const loadedFills = await market.loadFills(connection, limit)
setSerumStore((state) => {
state.fills = loadedFills
})
}
fetchFills()
}, _SLOW_REFRESH_INTERVAL)
}
export function useTrades() {
const trades = useSerumStore((state) => state.fills)
if (!trades) {
return null
}
// Until partial fills are each given their own fill, use maker fills
return trades
.filter(({ eventFlags }) => eventFlags.maker)
.map((trade) => ({
...trade,
side: trade.side === 'buy' ? 'sell' : 'buy',
}))
}
export default function useMarkPrice() {
const setMangoStore = useMangoStore((s) => s.set)
const markPrice = useMangoStore((s) => s.market.markPrice)
const [orderbook] = useOrderbook()
const trades = useTrades()
useEffect(() => {
const bb = orderbook?.bids?.length > 0 && Number(orderbook.bids[0][0])
const ba = orderbook?.asks?.length > 0 && Number(orderbook.asks[0][0])
const last = trades && trades.length > 0 && trades[0].price
const newMarkPrice =
bb && ba
? last
? [bb, ba, last].sort((a, b) => a - b)[1]
: (bb + ba) / 2
: null
setMangoStore((state) => {
state.market.markPrice = newMarkPrice
})
}, [orderbook, trades])
return markPrice
}

View File

@ -1,52 +0,0 @@
import { useEffect } from 'react'
import useConnection from './useConnection'
import { PublicKey } from '@solana/web3.js'
import { Market } from '@project-serum/serum'
import useStore from './useStore'
import { IDS } from '@blockworks-foundation/mango-client'
const useMarkets = () => {
const defaultMangoGroup = useStore((state) => state.defaultMangoGroup)
const marketStore = useStore((state) => state.market)
const { connection, cluster, programId, dexProgramId } = useConnection()
const spotMarkets =
IDS[cluster]?.mango_groups[defaultMangoGroup]?.spot_market_symbols || {}
const marketList = Object.entries(spotMarkets).map(([name, address]) => {
return {
address: new PublicKey(address as string),
programId: new PublicKey(dexProgramId as string),
deprecated: false,
name,
}
})
// TODO
const defaultMarket = marketList[0]
useEffect(() => {
Market.load(connection, defaultMarket.address, {}, defaultMarket.programId)
.then((market) => {
console.log('market loaded', market)
marketStore.setMarket(market)
})
.catch(
(e) => {
console.log('failed to load market', e)
}
// TODO
// notify({
// message: 'Error loading market',
// description: e.message,
// type: 'error',
// }),
)
}, [connection])
console.log('rerendering useMarkets hook', marketStore.market)
return { market: marketStore.market, programId, marketList }
}
export default useMarkets

88
hooks/useMarkets.tsx Normal file
View File

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

View File

@ -1,75 +1,61 @@
import { useEffect, useRef } from 'react'
import tuple from 'immutable-tuple'
import { useEffect } from 'react'
import { PublicKey, AccountInfo } from '@solana/web3.js'
import { Orderbook } from '@project-serum/serum'
import useMarkets from './useMarket'
import useMarkets from './useMarkets'
import useInterval from './useInterval'
import useMangoStore from '../stores/useMangoStore'
import useSolanaStore from '../stores/useSolanaStore'
import useConnection from './useConnection'
import { setCache, useAsyncData } from '../utils/fetch-loop'
const accountListenerCount = new Map()
export function useAccountInfo(
publicKey: PublicKey | undefined | null
): [AccountInfo<Buffer> | null | undefined, boolean] {
function useAccountInfo(account: PublicKey) {
const setSolanaStore = useSolanaStore((s) => s.set)
const { connection } = useConnection()
const cacheKey = tuple(connection, publicKey?.toBase58())
const [accountInfo, loaded] = useAsyncData<AccountInfo<Buffer> | null>(
async () => (publicKey ? connection.getAccountInfo(publicKey) : null),
cacheKey,
{ refreshInterval: 60_000 }
)
const accountPkAsString = account ? account.toString() : null
useInterval(async () => {
if (!account) return
const info = await connection.getAccountInfo(account)
console.log('fetching account info on interval', accountPkAsString)
setSolanaStore((state) => {
state.accountInfos[accountPkAsString] = info
})
}, 60000)
useEffect(() => {
if (!publicKey) {
return
}
if (accountListenerCount.has(cacheKey)) {
const currentItem = accountListenerCount.get(cacheKey)
++currentItem.count
} else {
let previousInfo: AccountInfo<Buffer> | null = null
const subscriptionId = connection.onAccountChange(publicKey, (info) => {
if (
!previousInfo ||
!previousInfo.data.equals(info.data) ||
previousInfo.lamports !== info.lamports
) {
previousInfo = info
setCache(cacheKey, info)
}
})
accountListenerCount.set(cacheKey, { count: 1, subscriptionId })
}
return () => {
const currentItem = accountListenerCount.get(cacheKey)
const nextCount = currentItem.count - 1
if (nextCount <= 0) {
connection.removeAccountChangeListener(currentItem.subscriptionId)
accountListenerCount.delete(cacheKey)
} else {
--currentItem.count
if (!account) return
let previousInfo: AccountInfo<Buffer> | null = null
const subscriptionId = connection.onAccountChange(account, (info) => {
if (
!previousInfo ||
!previousInfo.data.equals(info.data) ||
previousInfo.lamports !== info.lamports
) {
previousInfo = info
setSolanaStore((state) => {
state.accountInfos[accountPkAsString] = previousInfo
})
}
})
return () => {
connection.removeAccountChangeListener(subscriptionId)
}
// eslint-disable-next-line
}, [cacheKey])
const previousInfoRef = useRef<AccountInfo<Buffer> | null | undefined>(null)
if (
!accountInfo ||
!previousInfoRef.current ||
!previousInfoRef.current.data.equals(accountInfo.data) ||
previousInfoRef.current.lamports !== accountInfo.lamports
) {
previousInfoRef.current = accountInfo
}
return [previousInfoRef.current, loaded]
}, [account, connection])
}
export function useAccountData(publicKey) {
const [accountInfo] = useAccountInfo(publicKey)
return accountInfo && accountInfo.data
useAccountInfo(publicKey)
const account = publicKey ? publicKey.toString() : null
const accountInfo = useSolanaStore((s) => s.accountInfos[account])
return accountInfo && Buffer.from(accountInfo.data)
}
export function useOrderbookAccounts() {
const { market } = useMarkets()
const market = useMangoStore((s) => s.market.current)
// @ts-ignore
const bidData = useAccountData(market && market._decoded.bids)
// @ts-ignore
@ -85,6 +71,9 @@ export default function useOrderbook(
): [{ bids: number[][]; asks: number[][] }, boolean] {
const { bidOrderbook, askOrderbook } = useOrderbookAccounts()
const { market } = useMarkets()
const setMangoStore = useMangoStore((s) => s.set)
const bids =
!bidOrderbook || !market
? []
@ -93,5 +82,15 @@ export default function useOrderbook(
!askOrderbook || !market
? []
: askOrderbook.getL2(depth).map(([price, size]) => [price, size])
return [{ bids, asks }, !!bids || !!asks]
const orderBook: [{ bids: number[][]; asks: number[][] }, boolean] = [
{ bids, asks },
!!bids || !!asks,
]
setMangoStore((state) => {
state.market.orderBook = orderBook
})
return orderBook
}

View File

@ -1,54 +0,0 @@
import create, { State } from 'zustand'
import { devtools } from 'zustand/middleware'
import { Connection } from '@solana/web3.js'
import { Market } from '@project-serum/serum'
interface MangoStore extends State {
wallet: {
connected: boolean
setConnected: (x: boolean) => void
}
connection: {
endpoint: string
connection: Connection
setConnection: (x: Connection) => void
}
defaultMangoGroup: string
market: {
market: Market | null
setMarket: (x: Market) => void
}
}
const useStore = create<MangoStore>(
devtools((set) => ({
wallet: {
connected: false,
setConnected: (connected) => {
set((state) => ({ ...state, wallet: { ...state.wallet, connected } }))
},
},
connection: {
endpoint: 'devnet',
connection: null,
setConnection: (connection) => {
set((state) => ({
...state,
connection: { ...state.connection, connection },
}))
},
},
defaultMangoGroup: 'BTC_ETH_USDT',
market: {
market: null,
setMarket: (market) => {
set((state) => ({
...state,
market: { ...state.market, market },
}))
},
},
}))
)
export default useStore

View File

@ -1,8 +1,8 @@
import { useEffect, useMemo } from 'react'
import { useEffect } from 'react'
import Wallet from '@project-serum/sol-wallet-adapter'
// import { notify } from './notifications'
import useLocalStorageState from './useLocalStorageState'
import useStore from './useStore'
import useSolanaStore from '../stores/useSolanaStore'
export const WALLET_PROVIDERS = [
{ name: 'sollet.io', url: 'https://www.sollet.io' },
@ -10,32 +10,34 @@ export const WALLET_PROVIDERS = [
const ENDPOINT = process.env.CLUSTER ? process.env.CLUSTER : 'mainnet-beta'
export function useWallet() {
const walletStore = useStore((state) => state.wallet)
export default function useWallet() {
const setSolanaStore = useSolanaStore((state) => state.set)
const { current: wallet, connected } = useSolanaStore((state) => state.wallet)
const endpoint = useSolanaStore((state) => state.connection.endpoint)
const [savedProviderUrl] = useLocalStorageState(
'walletProvider',
'https://www.sollet.io'
)
let providerUrl
if (!savedProviderUrl) {
providerUrl = 'https://www.sollet.io'
} else {
providerUrl = savedProviderUrl
}
const wallet =
typeof window !== 'undefined'
? useMemo(() => new Wallet(providerUrl, ENDPOINT), [
providerUrl,
ENDPOINT,
])
: {}
const providerUrl = savedProviderUrl
? savedProviderUrl
: 'https://www.sollet.io'
useEffect(() => {
console.log('creating wallet', endpoint)
const newWallet = new Wallet(providerUrl, ENDPOINT)
setSolanaStore((state) => {
state.wallet.current = newWallet
})
}, [endpoint])
useEffect(() => {
if (!wallet) return
wallet.on('connect', () => {
walletStore.setConnected(true)
console.log('connected!!!!')
setSolanaStore((state) => {
state.wallet.connected = true
})
console.log('connected!')
// const walletPublicKey = wallet.publicKey.toBase58()
// const keyToDisplay =
@ -51,7 +53,9 @@ export function useWallet() {
// })
})
wallet.on('disconnect', () => {
walletStore.setConnected(false)
setSolanaStore((state) => {
state.wallet.connected = false
})
// notify({
// message: 'Wallet update',
// description: 'Disconnected from wallet',
@ -60,9 +64,11 @@ export function useWallet() {
})
return () => {
wallet.disconnect()
walletStore.setConnected(false)
setSolanaStore((state) => {
state.wallet.connected = false
})
}
}, [wallet])
return wallet
return { wallet, connected }
}

View File

@ -1,4 +1,7 @@
module.exports = {
const withAntdLess = require('next-plugin-antd-less')
module.exports = withAntdLess({
lessVarsFilePath: './styles/theme.less',
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
@ -7,4 +10,4 @@ module.exports = {
return config
},
}
})

View File

@ -29,11 +29,17 @@
"@blockworks-foundation/mango-client": "^0.1.10",
"@emotion/react": "^11.1.5",
"@emotion/styled": "^11.1.5",
"@heroicons/react": "^1.0.0",
"@project-serum/serum": "^0.13.31",
"@project-serum/sol-wallet-adapter": "^0.1.8",
"@solana/web3.js": "^1.2.3",
"antd": "^4.15.0",
"babel-plugin-import": "^1.13.3",
"bn.js": "^5.2.0",
"immer": "^9.0.1",
"immutable-tuple": "^0.4.10",
"next": "latest",
"next-plugin-antd-less": "^0.3.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-grid-layout": "^1.2.4",
@ -54,6 +60,7 @@
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^4.2.3",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",

View File

@ -1,7 +1,9 @@
import Head from 'next/head'
import { Global } from '@emotion/react'
import xw from 'xwind'
import './index.css'
import '../node_modules/react-grid-layout/css/styles.css'
import '../node_modules/react-resizable/css/styles.css'
import '../styles/index.css'
function App({ Component, pageProps }) {
return (

View File

@ -1,8 +0,0 @@
.TVChartContainer {
height: 100%;
width: 100%;
}
.tradingview-chart {
display: contents;
}

View File

@ -1,67 +1,21 @@
import xw from 'xwind'
import dynamic from 'next/dynamic'
import { Responsive, WidthProvider } from 'react-grid-layout'
const TVChartContainer = dynamic(
() => import('../components/TradingView/index'),
{ ssr: false }
)
import TopBar from '../components/TopBar'
import Notifications from '../components/Notification'
import FloatingElement from '../components/FloatingElement'
// import Orderbook from '../components/Orderbook'
import useConnection from '../hooks/useConnection'
import useMarkets from '../hooks/useMarket'
const ResponsiveGridLayout = WidthProvider(Responsive)
const LAYOUTS = {
lg: [
{ i: '1', x: 0, y: 0, w: 2, h: 2 },
{ i: '2', x: 2, y: 0, w: 1, h: 2 },
{ i: '3', x: 3, y: 0, w: 1, h: 1 },
{ i: '4', x: 3, y: 1, w: 1, h: 1 },
{ i: '5', x: 0, y: 2, w: 2, h: 1 },
{ i: '6', x: 2, y: 2, w: 1, h: 1 },
{ i: '7', x: 3, y: 2, w: 1, h: 1 },
],
}
import TradePageGrid from '../components/TradePageGrid'
const Index = () => {
useConnection()
useMarkets()
return (
<div css={xw`bg-mango-dark text-white`}>
<TopBar />
<div css={xw`min-h-screen p-1 sm:p-2 md:p-6`}>
<TradePageGrid />
</div>
<Notifications
notifications={[
{ title: 'test', message: 'ok' },
{ title: 'test2', message: 'ok2' },
]}
/>
<TopBar />
<div css={xw`min-h-screen p-6`}>
<ResponsiveGridLayout
className="layout"
layouts={LAYOUTS}
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 0 }}
cols={{ lg: 4, md: 3, sm: 2, xs: 1 }}
>
<div key="1">
<FloatingElement>
<TVChartContainer />
</FloatingElement>
</div>
<div key="2">
<FloatingElement>{/* <Orderbook /> */}</FloatingElement>
</div>
<div key="3">3</div>
<div key="4">4</div>
<div key="5">5</div>
<div key="6">6</div>
<div key="7">7</div>
</ResponsiveGridLayout>
</div>
</div>
)
}

48
stores/useMangoStore.tsx Normal file
View File

@ -0,0 +1,48 @@
import create, { State } from 'zustand'
import { devtools } from 'zustand/middleware'
import produce from 'immer'
import { Market } from '@project-serum/serum'
import {
MangoClient,
MangoGroup,
MarginAccount,
} from '@blockworks-foundation/mango-client'
interface MangoStore extends State {
selectedMangoGroup: string
market: {
current: Market | null
markPrice: number
orderBook: any[]
}
mangoClient: MangoClient
mangoGroup: MangoGroup | null
selectedMarginAcccount: MarginAccount | null
tradeForm: {
currency: string
size: number
}
set: (x: any) => void
}
const useMangoStore = create<MangoStore>(
devtools((set) => ({
selectedMangoGroup: 'BTC_ETH_USDT',
market: {
current: null,
markPrice: 0,
orderBook: [],
},
mangoClient: new MangoClient(),
mangoGroup: null,
marginAccounts: [],
selectedMarginAcccount: null,
tradeForm: {
size: 0,
currency: 'BTC',
},
set: (fn) => set(produce(fn)),
}))
)
export default useMangoStore

27
stores/useSerumStore.tsx Normal file
View File

@ -0,0 +1,27 @@
import create, { State } from 'zustand'
import { devtools } from 'zustand/middleware'
import produce from 'immer'
// import { Connection } from '@solana/web3.js'
// import { Market } from '@project-serum/serum'
interface SerumStore extends State {
orderbook: {
bids: any[]
asks: any[]
}
fills: any[]
set: (x: any) => void
}
const useSerumStore = create<SerumStore>(
devtools((set) => ({
orderbook: {
bids: [],
asks: [],
},
fills: [],
set: (fn) => set(produce(fn)),
}))
)
export default useSerumStore

59
stores/useSolanaStore.tsx Normal file
View File

@ -0,0 +1,59 @@
import create, { State } from 'zustand'
import { devtools } from 'zustand/middleware'
import produce from 'immer'
import { AccountInfo, Connection } from '@solana/web3.js'
import { Wallet } from '@project-serum/sol-wallet-adapter'
import { EndpointInfo } from '../@types/types'
export const ENDPOINTS: EndpointInfo[] = [
{
name: 'mainnet-beta',
endpoint: 'https://solana-api.projectserum.com',
custom: false,
},
{
name: 'devnet',
endpoint: 'https://devnet.solana.com',
custom: false,
},
]
const CLUSTER = 'mainnet-beta'
const ENDPOINT_URL = ENDPOINTS.find((e) => e.name === CLUSTER).endpoint
const DEFAULT_CONNECTION = new Connection(ENDPOINT_URL, 'recent')
interface AccountInfoList {
[key: string]: AccountInfo<Buffer>
}
interface SolanaStore extends State {
accountInfos: AccountInfoList
connection: {
cluster: string
current: Connection
endpoint: string
}
wallet: {
connected: boolean
current: Wallet
}
set: (x: any) => void
}
const useSolanaStore = create<SolanaStore>(
devtools((set) => ({
accountInfos: {},
connection: {
cluster: CLUSTER,
current: DEFAULT_CONNECTION,
endpoint: ENDPOINT_URL,
},
wallet: {
connected: false,
current: null,
},
set: (fn) => set(produce(fn)),
}))
)
export default useSolanaStore

19
styles/index.css Normal file
View File

@ -0,0 +1,19 @@
.TVChartContainer {
height: 100%;
width: 100%;
}
.tradingview-chart {
display: contents;
}
.react-grid-item.react-grid-placeholder {
background: #332f46;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}

20
styles/theme.less Normal file
View File

@ -0,0 +1,20 @@
// colors
@primary-color: #f2c94c;
@success-color: #afd803;
@error-color: #e54033;
@text-color: #eeeeee;
@text-color-secondary: #9490a6;
@border-color-base: #584f81;
@divider-color: #584f81;
@btn-primary-color: #141026;
@body-background: #141026;
@component-background: #141026;
@layout-body-background: #141026;
// font
@font-family: 'Lato', sans-serif;
/*@font-size-base: 21px;*/
// layout
@border-radius-base: 9px;
@layout-header-height: 40px;

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
@ -23,6 +23,6 @@
}
]
},
"exclude": ["node_modules", ".next", "out"],
"exclude": ["node_modules", ".next", "out", "public/datafeeds/udf/src"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"]
}

View File

@ -1,10 +1,7 @@
import { BonfidaTrade } from '../@types/types'
import { ChartType } from '../@types/types'
const baseUrl = 'https://serum-history.herokuapp.com'
//const baseUrl = "http://85.214.116.56:5000";
//const baseUrl = "http://localhost:5000";
export default class BonfidaApi {
export default class ChartApi {
static URL = `${baseUrl}/`
static async get(path: string) {
@ -15,16 +12,16 @@ export default class BonfidaApi {
return responseJson.success ? responseJson.data : null
}
} catch (err) {
console.log(`Error fetching from Bonfida API ${path}: ${err}`)
console.log(`Error fetching from Chart API ${path}: ${err}`)
}
return null
}
static async getRecentTrades(
marketAddress: string
): Promise<BonfidaTrade[] | null> {
return BonfidaApi.get(`trades/address/${marketAddress}`)
): Promise<ChartType[] | null> {
return ChartApi.get(`trades/address/${marketAddress}`)
}
}
export const BONFIDA_DATA_FEED = `${baseUrl}/tv`
export const CHART_DATA_FEED = `${baseUrl}/tv`

View File

@ -203,7 +203,6 @@ export function useAsyncData<T = any>(
useEffect(() => {
if (!cacheKey) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {}
}
const listener = new FetchLoopListener<T>(
@ -216,7 +215,7 @@ export function useAsyncData<T = any>(
)
globalLoops.addListener(listener)
return () => globalLoops.removeListener(listener)
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line
}, [cacheKey, refreshInterval])
if (!cacheKey) {

View File

@ -112,3 +112,25 @@ export function groupBy(list, keyGetter) {
export function isDefined<T>(argument: T | undefined): argument is T {
return argument !== undefined
}
export const calculateMarketPrice = (
orderBook: Array<any>,
size: number,
side: string
) => {
let acc = 0
let selectedOrder
for (const order of orderBook) {
acc += order[1]
if (acc >= size) {
selectedOrder = order
break
}
}
if (side === 'buy') {
return selectedOrder[0] * 1.05
} else {
return selectedOrder[0] * 0.95
}
}

1321
utils/mango.tsx Normal file

File diff suppressed because it is too large Load Diff

3
utils/notifications.tsx Normal file
View File

@ -0,0 +1,3 @@
export function notify(args) {
alert(`notify: ${args}`)
}

378
utils/send.tsx Normal file
View File

@ -0,0 +1,378 @@
import { notify } from './notifications'
import { sleep } from './index'
import {
Account,
AccountInfo,
Commitment,
Connection,
PublicKey,
RpcResponseAndContext,
SimulatedTransactionResponse,
Transaction,
TransactionSignature,
} from '@solana/web3.js'
import Wallet from '@project-serum/sol-wallet-adapter'
import { Buffer } from 'buffer'
import assert from 'assert'
import { struct } from 'superstruct'
export const getUnixTs = () => {
return new Date().getTime() / 1000
}
const DEFAULT_TIMEOUT = 30000
export async function sendTransaction({
transaction,
wallet,
signers = [],
connection,
sendingMessage = 'Sending transaction...',
sentMessage = 'Transaction sent',
successMessage = 'Transaction confirmed',
timeout = DEFAULT_TIMEOUT,
}: {
transaction: Transaction
wallet: Wallet
signers?: Array<Account>
connection: Connection
sendingMessage?: string
sentMessage?: string
successMessage?: string
timeout?: number
}) {
const signedTransaction = await signTransaction({
transaction,
wallet,
signers,
connection,
})
return await sendSignedTransaction({
signedTransaction,
connection,
sendingMessage,
sentMessage,
successMessage,
timeout,
})
}
export async function signTransaction({
transaction,
wallet,
signers = [],
connection,
}: {
transaction: Transaction
wallet: Wallet
signers?: Array<Account>
connection: Connection
}) {
transaction.recentBlockhash = (
await connection.getRecentBlockhash('max')
).blockhash
transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
if (signers.length > 0) {
transaction.partialSign(...signers)
}
return await wallet.signTransaction(transaction)
}
export async function signTransactions({
transactionsAndSigners,
wallet,
connection,
}: {
transactionsAndSigners: {
transaction: Transaction
signers?: Array<Account>
}[]
wallet: Wallet
connection: Connection
}) {
const blockhash = (await connection.getRecentBlockhash('max')).blockhash
transactionsAndSigners.forEach(({ transaction, signers = [] }) => {
transaction.recentBlockhash = blockhash
transaction.setSigners(wallet.publicKey, ...signers.map((s) => s.publicKey))
if (signers?.length > 0) {
transaction.partialSign(...signers)
}
})
return await wallet.signAllTransactions(
transactionsAndSigners.map(({ transaction }) => transaction)
)
}
export async function sendSignedTransaction({
signedTransaction,
connection,
sendingMessage = 'Sending transaction...',
sentMessage = 'Transaction sent',
successMessage = 'Transaction confirmed',
timeout = DEFAULT_TIMEOUT,
}: {
signedTransaction: Transaction
connection: Connection
sendingMessage?: string
sentMessage?: string
successMessage?: string
timeout?: number
}): Promise<string> {
const rawTransaction = signedTransaction.serialize()
const startTime = getUnixTs()
notify({ message: sendingMessage })
const txid: TransactionSignature = await connection.sendRawTransaction(
rawTransaction,
{
skipPreflight: true,
}
)
notify({ message: sentMessage, type: 'success', txid })
console.log('Started awaiting confirmation for', txid)
let done = false
;(async () => {
while (!done && getUnixTs() - startTime < timeout) {
connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
})
await sleep(300)
}
})()
try {
await awaitTransactionSignatureConfirmation(txid, timeout, connection)
} catch (err) {
if (err.timeout) {
throw new Error('Timed out awaiting confirmation on transaction')
}
let simulateResult: SimulatedTransactionResponse | null = null
try {
simulateResult = (
await simulateTransaction(connection, signedTransaction, 'single')
).value
} catch (e) {
console.log('Error: ', e)
}
if (simulateResult && simulateResult.err) {
if (simulateResult.logs) {
for (let i = simulateResult.logs.length - 1; i >= 0; --i) {
const line = simulateResult.logs[i]
if (line.startsWith('Program log: ')) {
throw new Error(
'Transaction failed: ' + line.slice('Program log: '.length)
)
}
}
}
throw new Error(JSON.stringify(simulateResult.err))
}
throw new Error('Transaction failed')
} finally {
done = true
}
notify({ message: successMessage, type: 'success', txid })
console.log('Latency', txid, getUnixTs() - startTime)
return txid
}
async function awaitTransactionSignatureConfirmation(
txid: TransactionSignature,
timeout: number,
connection: Connection
) {
let done = false
const result = await new Promise((resolve, reject) => {
// eslint-disable-next-line
;(async () => {
setTimeout(() => {
if (done) {
return
}
done = true
console.log('Timed out for txid', txid)
reject({ timeout: true })
}, timeout)
try {
connection.onSignature(
txid,
(result) => {
console.log('WS confirmed', txid, result)
done = true
if (result.err) {
reject(result.err)
} else {
resolve(result)
}
},
connection.commitment
)
console.log('Set up WS connection', txid)
} catch (e) {
done = true
console.log('WS error in setup', txid, e)
}
while (!done) {
// eslint-disable-next-line
;(async () => {
try {
const signatureStatuses = await connection.getSignatureStatuses([
txid,
])
const result = signatureStatuses && signatureStatuses.value[0]
if (!done) {
if (!result) {
// console.log('REST null result for', txid, result);
} else if (result.err) {
console.log('REST error for', txid, result)
done = true
reject(result.err)
}
// @ts-ignore
else if (
!(
result.confirmations ||
result.confirmationStatus === 'confirmed' ||
result.confirmationStatus === 'finalized'
)
) {
console.log('REST not confirmed', txid, result)
} else {
console.log('REST confirmed', txid, result)
done = true
resolve(result)
}
}
} catch (e) {
if (!done) {
console.log('REST connection error: txid', txid, e)
}
}
})()
await sleep(300)
}
})()
})
done = true
return result
}
function jsonRpcResult(resultDescription: any) {
const jsonRpcVersion = struct.literal('2.0')
return struct.union([
struct({
jsonrpc: jsonRpcVersion,
id: 'string',
error: 'any',
}),
struct({
jsonrpc: jsonRpcVersion,
id: 'string',
error: 'null?',
result: resultDescription,
}),
])
}
function jsonRpcResultAndContext(resultDescription: any) {
return jsonRpcResult({
context: struct({
slot: 'number',
}),
value: resultDescription,
})
}
const AccountInfoResult = struct({
executable: 'boolean',
owner: 'string',
lamports: 'number',
data: 'any',
rentEpoch: 'number?',
})
export const GetMultipleAccountsAndContextRpcResult = jsonRpcResultAndContext(
struct.array([struct.union(['null', AccountInfoResult])])
)
export async function getMultipleSolanaAccounts(
connection: Connection,
publicKeys: PublicKey[]
): Promise<
RpcResponseAndContext<{ [key: string]: AccountInfo<Buffer> | null }>
> {
const args = [publicKeys.map((k) => k.toBase58()), { commitment: 'recent' }]
// @ts-ignore
const unsafeRes = await connection._rpcRequest('getMultipleAccounts', args)
const res = GetMultipleAccountsAndContextRpcResult(unsafeRes)
if (res.error) {
throw new Error(
'failed to get info about accounts ' +
publicKeys.map((k) => k.toBase58()).join(', ') +
': ' +
res.error.message
)
}
assert(typeof res.result !== 'undefined')
const accounts: Array<{
executable: any
owner: PublicKey
lamports: any
data: Buffer
} | null> = []
for (const account of res.result.value) {
let value: {
executable: any
owner: PublicKey
lamports: any
data: Buffer
} | null = null
if (res.result.value) {
const { executable, owner, lamports, data } = account
assert(data[1] === 'base64')
value = {
executable,
owner: new PublicKey(owner),
lamports,
data: Buffer.from(data[0], 'base64'),
}
}
accounts.push(value)
}
return {
context: {
slot: res.result.context.slot,
},
value: Object.fromEntries(
accounts.map((account, i) => [publicKeys[i].toBase58(), account])
),
}
}
/** Copy of Connection.simulateTransaction that takes a commitment parameter. */
async function simulateTransaction(
connection: Connection,
transaction: Transaction,
commitment: Commitment
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
// @ts-ignore
transaction.recentBlockhash = await connection._recentBlockhash(
// @ts-ignore
connection._disableBlockhashCaching
)
const signData = transaction.serializeMessage()
// @ts-ignore
const wireTransaction = transaction._serialize(signData)
const encodedTransaction = wireTransaction.toString('base64')
const config: any = { encoding: 'base64', commitment }
const args = [encodedTransaction, config]
// @ts-ignore
const res = await connection._rpcRequest('simulateTransaction', args)
if (res.error) {
throw new Error('failed to simulate transaction: ' + res.error.message)
}
return res.result
}

5
utils/tokens.tsx Normal file
View File

@ -0,0 +1,5 @@
import { PublicKey } from '@solana/web3.js'
export const TOKEN_PROGRAM_ID = new PublicKey(
'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
)

724
yarn.lock

File diff suppressed because it is too large Load Diff