Merges main

This commit is contained in:
Luc Succes 2022-04-06 07:24:51 -06:00
commit f7801941a5
111 changed files with 3268 additions and 2650 deletions

1
.env
View File

@ -1 +0,0 @@
NEXT_PUBLIC_GROUP=devnet.2

6
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,6 @@
PR Requirements:
- [ ] Summarize changes
- [ ] Included a screenshot, if applicable
- [ ] Test on mobile
- [ ] Request at least one reviewer

View File

@ -23,8 +23,7 @@ jobs:
with:
node-version: '14.x'
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: yarn install --frozen-lockfile --network-concurrency 1
- name: Restore next build
uses: actions/cache@v2

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: yarn install --frozen-lockfile
- run: yarn install --frozen-lockfile --network-concurrency 1
- run: yarn type-check
- run: yarn lint
- run: yarn prettier --check .

View File

@ -66,7 +66,8 @@ export default function AccountInfo() {
setShowAlertsModal(false)
}, [])
const equity = mangoAccount
const equity =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.computeValue(mangoGroup, mangoCache)
: ZERO_I80F48
@ -75,28 +76,40 @@ export default function AccountInfo() {
return perpAcct.mngoAccrued.add(acc)
}, ZERO_BN)
: ZERO_BN
// console.log('rerendering account info', mangoAccount, mngoAccrued.toNumber())
const handleRedeemMngo = async () => {
const mangoClient = useMangoStore.getState().connection.client
const mngoNodeBank =
mangoGroup.rootBankAccounts[MNGO_INDEX].nodeBankAccounts[0]
mangoGroup?.rootBankAccounts?.[MNGO_INDEX]?.nodeBankAccounts[0]
if (!mngoNodeBank || !mangoAccount || !wallet) {
return
}
try {
setRedeeming(true)
const txid = await mangoClient.redeemAllMngo(
const txids = await mangoClient.redeemAllMngo(
mangoGroup,
mangoAccount,
wallet?.adapter,
wallet.adapter,
mangoGroup.tokens[MNGO_INDEX].rootBank,
mngoNodeBank.publicKey,
mngoNodeBank.vault
)
if (txids) {
for (const txid of txids) {
notify({
title: t('redeem-success'),
description: '',
txid,
})
}
} else {
notify({
title: t('redeem-failure'),
description: t('transaction-failed'),
})
}
} catch (e) {
notify({
title: t('redeem-failure'),
@ -110,23 +123,27 @@ export default function AccountInfo() {
}
}
const maintHealthRatio = mangoAccount
const maintHealthRatio =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
: I80F48_100
const initHealthRatio = mangoAccount
const initHealthRatio =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Init')
: I80F48_100
const maintHealth = mangoAccount
const maintHealth =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealth(mangoGroup, mangoCache, 'Maint')
: I80F48_100
const initHealth = mangoAccount
const initHealth =
mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealth(mangoGroup, mangoCache, 'Init')
: I80F48_100
const liquidationPrice =
mangoGroup && mangoAccount && marketConfig
mangoGroup && mangoAccount && marketConfig && mangoGroup && mangoCache
? mangoAccount.getLiquidationPrice(
mangoGroup,
mangoCache,
@ -199,7 +216,7 @@ export default function AccountInfo() {
<div className="text-th-fgd-1">
{initialLoad ? (
<DataLoader />
) : mangoAccount ? (
) : mangoAccount && mangoGroup && mangoCache ? (
`${mangoAccount
.getLeverage(mangoGroup, mangoCache)
.toFixed(2)}x`
@ -215,7 +232,7 @@ export default function AccountInfo() {
<div className={`text-th-fgd-1`}>
{initialLoad ? (
<DataLoader />
) : mangoAccount ? (
) : mangoAccount && mangoGroup ? (
usdFormatter(
nativeI80F48ToUi(
initHealth,
@ -232,7 +249,7 @@ export default function AccountInfo() {
{marketConfig.name} {t('margin-available')}
</div>
<div className={`text-th-fgd-1`}>
{mangoAccount
{mangoAccount && mangoGroup && mangoCache
? usdFormatter(
nativeI80F48ToUi(
mangoAccount.getMarketMarginAvailable(

View File

@ -34,7 +34,7 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
const submitName = async () => {
const mangoClient = useMangoStore.getState().connection.client
if (!wallet || !mangoAccount || !mangoGroup) return
try {
const txid = await mangoClient.addMangoAccountInfo(
mangoGroup,
@ -44,7 +44,7 @@ const AccountNameModal: FunctionComponent<AccountNameModalProps> = ({
)
actions.fetchAllMangoAccounts(wallet)
actions.reloadMangoAccount()
onClose()
onClose?.()
notify({
title: t('name-updated'),
txid,

View File

@ -4,7 +4,6 @@ import { ChevronDownIcon } from '@heroicons/react/solid'
import { abbreviateAddress } from '../utils'
import useMangoStore, { WalletToken } from '../stores/useMangoStore'
import { RefreshClockwiseIcon } from './icons'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import { useTranslation } from 'next-i18next'
import { LinkButton } from './Button'
import { Label } from './Input'
@ -23,14 +22,14 @@ const AccountSelect = ({
hideAddress = false,
}: AccountSelectProps) => {
const { t } = useTranslation('common')
const groupConfig = useMangoGroupConfig()
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const tokenSymbols = useMemo(
() => groupConfig.tokens.map((t) => t.symbol),
() => groupConfig?.tokens.map((t) => t.symbol),
[groupConfig]
)
const missingTokenSymbols = useMemo(() => {
const symbolsForAccounts = accounts.map((a) => a.config.symbol)
return tokenSymbols.filter((sym) => !symbolsForAccounts.includes(sym))
return tokenSymbols?.filter((sym) => !symbolsForAccounts.includes(sym))
}, [accounts, tokenSymbols])
const actions = useMangoStore((s) => s.actions)
@ -53,7 +52,7 @@ const AccountSelect = ({
<div className={`relative inline-block w-full`}>
<div className="flex justify-between">
<Label>{t('asset')}</Label>
{missingTokenSymbols.length > 0 ? (
{missingTokenSymbols && missingTokenSymbols.length > 0 ? (
<LinkButton className="mb-1.5 ml-2" onClick={handleRefreshBalances}>
<div className="flex items-center">
<RefreshClockwiseIcon
@ -160,7 +159,7 @@ const AccountSelect = ({
</Listbox.Option>
)
})}
{missingTokenSymbols.map((token) => (
{missingTokenSymbols?.map((token) => (
<Listbox.Option disabled key={token} value={token}>
<div
className={`px-2 py-1 opacity-50 hover:cursor-not-allowed`}

View File

@ -51,9 +51,10 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
useEffect(() => {
if (newAccPublicKey) {
setMangoStore((state) => {
state.selectedMangoAccount.current = mangoAccounts.find(
state.selectedMangoAccount.current =
mangoAccounts.find(
(ma) => ma.publicKey.toString() === newAccPublicKey
)
) ?? null
})
}
}, [mangoAccounts, newAccPublicKey])
@ -96,7 +97,11 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
</div>
<RadioGroup
value={selectedMangoAccount}
onChange={(acc) => handleMangoAccountChange(acc)}
onChange={(acc) => {
if (acc) {
handleMangoAccountChange(acc)
}
}}
>
<RadioGroup.Label className="sr-only">
{t('select-account')}
@ -125,7 +130,8 @@ const AccountsModal: FunctionComponent<AccountsModalProps> = ({
<div className="flex items-center pb-0.5">
{account?.name ||
abbreviateAddress(account.publicKey)}
{!account?.owner.equals(publicKey) ? (
{publicKey &&
!account?.owner.equals(publicKey) ? (
<Tooltip
content={t(
'delegate:delegated-account'
@ -188,6 +194,9 @@ const AccountInfo = ({
mangoAccount: MangoAccount
}) => {
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
if (!mangoCache) {
return null
}
const accountEquity = mangoAccount.computeValue(mangoGroup, mangoCache)
const leverage = mangoAccount.getLeverage(mangoGroup, mangoCache).toFixed(2)

View File

@ -32,16 +32,23 @@ const BalancesTable = ({
const [actionSymbol, setActionSymbol] = useState('')
const balances = useBalances()
const { items, requestSort, sortConfig } = useSortableData(
balances
.filter(
(bal) =>
balances?.length > 0
? balances
.filter((bal) => {
return (
showZeroBalances ||
+bal.deposits > 0 ||
+bal.borrows > 0 ||
bal.orders > 0 ||
bal.unsettled > 0
(bal.deposits && +bal.deposits > 0) ||
(bal.borrows && +bal.borrows > 0) ||
(bal.orders && bal.orders > 0) ||
(bal.unsettled && bal.unsettled > 0)
)
.sort((a, b) => Math.abs(+b.value) - Math.abs(+a.value))
})
.sort((a, b) => {
const aV = a.value ? Math.abs(+a.value) : 0
const bV = b.value ? Math.abs(+b.value) : 0
return bV - aV
})
: []
)
const actions = useMangoStore((s) => s.actions)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
@ -60,13 +67,21 @@ const BalancesTable = ({
const { asPath } = useRouter()
const handleSizeClick = (size, symbol) => {
const minOrderSize = selectedMarket.minOrderSize
const sizePrecisionDigits = getPrecisionDigits(minOrderSize)
const minOrderSize = selectedMarket?.minOrderSize
const sizePrecisionDigits = minOrderSize
? getPrecisionDigits(minOrderSize)
: null
const marketIndex = marketConfig.marketIndex
const priceOrDefault = price
? price
: mangoGroup.getPriceUi(marketIndex, mangoGroupCache)
: mangoGroup && mangoGroupCache
? mangoGroup.getPriceUi(marketIndex, mangoGroupCache)
: null
if (!priceOrDefault || !sizePrecisionDigits || !minOrderSize) {
return
}
let roundedSize, side
if (symbol === 'USDC') {
@ -113,16 +128,27 @@ const BalancesTable = ({
(mkt) => mkt instanceof Market
) as Market[]
const txids: TransactionSignature[] = await mangoClient.settleAll(
if (!mangoGroup || !mangoAccount || !wallet) {
return
}
const txids: TransactionSignature[] | undefined =
await mangoClient.settleAll(
mangoGroup,
mangoAccount,
spotMarkets,
wallet?.adapter
)
if (txids) {
for (const txid of txids) {
notify({ title: t('settle-success'), txid })
}
} else {
notify({
title: t('settle-error'),
type: 'error',
})
}
} catch (e) {
console.warn('Error settling all:', e)
if (e.message === 'No unsettled funds') {
@ -145,7 +171,9 @@ const BalancesTable = ({
}
}
const unsettledBalances = balances.filter((bal) => bal.unsettled > 0)
const unsettledBalances = balances.filter(
(bal) => bal.unsettled && bal.unsettled > 0
)
const trimDecimals = useCallback((num: string) => {
if (parseFloat(num) === 0) {
@ -202,9 +230,14 @@ const BalancesTable = ({
<p className="mb-0 text-xs text-th-fgd-1">
{bal.symbol}
</p>
{bal.unsettled ? (
<div className="font-bold text-th-green">
{floorToDecimal(bal.unsettled, tokenConfig.decimals)}
{floorToDecimal(
bal.unsettled,
tokenConfig.decimals
)}
</div>
) : null}
</div>
</div>
</div>
@ -378,7 +411,16 @@ const BalancesTable = ({
</thead>
<tbody>
{items.map((balance, index) => {
if (!balance) {
if (
!balance ||
typeof balance.decimals !== 'number' ||
!balance.deposits ||
!balance.borrows ||
!balance.net ||
!balance.value ||
!balance.borrowRate ||
!balance.depositRate
) {
return null
}
return (
@ -514,7 +556,22 @@ const BalancesTable = ({
colOneHeader={t('asset')}
colTwoHeader={t('net-balance')}
/>
{items.map((balance, index) => (
{items.map((balance, index) => {
if (
!balance ||
typeof balance.decimals !== 'number' ||
typeof balance.orders !== 'number' ||
typeof balance.unsettled !== 'number' ||
!balance.deposits ||
!balance.borrows ||
!balance.net ||
!balance.value ||
!balance.borrowRate ||
!balance.depositRate
) {
return null
}
return (
<ExpandableRow
buttonTemplate={
<div className="flex w-full items-center justify-between text-th-fgd-1">
@ -628,7 +685,8 @@ const BalancesTable = ({
</>
}
/>
))}
)
})}
</div>
)
) : (

View File

@ -98,13 +98,17 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
const closeAccount = async () => {
const mangoClient = useMangoStore.getState().connection.client
if (!mangoGroup || !mangoAccount || !mangoCache || !wallet) {
return
}
try {
const txids = await mangoClient.emptyAndCloseMangoAccount(
mangoGroup,
mangoAccount,
mangoCache,
MNGO_INDEX,
wallet?.adapter
wallet.adapter
)
await actions.fetchAllMangoAccounts(wallet)
@ -116,13 +120,21 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
: null
})
onClose()
onClose?.()
if (txids) {
for (const txid of txids) {
notify({
title: t('close-account:transaction-confirmed'),
txid,
})
}
} else {
notify({
title: t('close-account:error-deleting-account'),
description: t('transaction-failed'),
type: 'error',
})
}
} catch (err) {
console.warn('Error deleting account:', err)
notify({
@ -153,6 +165,8 @@ const CloseAccountModal: FunctionComponent<CloseAccountModalProps> = ({
{t('close-account:delete-your-account')}
</div>
{mangoAccount &&
mangoGroup &&
mangoCache &&
mangoAccount.getAssetsVal(mangoGroup, mangoCache).gt(ZERO_I80F48) ? (
<div className="flex items-center text-th-fgd-2">
<CheckCircleIcon className="mr-1.5 h-4 w-4 text-th-green" />

View File

@ -20,7 +20,7 @@ import { ProfileIcon, WalletIcon } from './icons'
import { useTranslation } from 'next-i18next'
import { WalletSelect } from 'components/WalletSelect'
import AccountsModal from './AccountsModal'
import { uniqBy } from 'lodash'
import uniqBy from 'lodash/uniqBy'
export const handleWalletConnect = (wallet: Wallet) => {
if (!wallet) {
@ -68,7 +68,9 @@ export const ConnectWalletButton: React.FC = () => {
}, [wallets, installedWallets])
const handleConnect = useCallback(() => {
if (wallet) {
handleWalletConnect(wallet)
}
}, [wallet])
const handleCloseAccounts = useCallback(() => {

View File

@ -63,8 +63,8 @@ const CreateAlertModal: FunctionComponent<CreateAlertModalProps> = ({
return
}
const body = {
mangoGroupPk: mangoGroup.publicKey.toString(),
mangoAccountPk: mangoAccount.publicKey.toString(),
mangoGroupPk: mangoGroup?.publicKey.toString(),
mangoAccountPk: mangoAccount?.publicKey.toString(),
health,
alertProvider: 'mail',
email,
@ -84,7 +84,7 @@ const CreateAlertModal: FunctionComponent<CreateAlertModalProps> = ({
}
useEffect(() => {
actions.loadAlerts(mangoAccount.publicKey)
actions.loadAlerts(mangoAccount?.publicKey)
}, [])
return (

View File

@ -1,97 +0,0 @@
import DatePicker from 'react-datepicker/dist/react-datepicker'
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import Select from './Select'
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
const MangoDatePicker = ({ date, setDate, ...props }) => {
const generateArrayOfYears = () => {
const max = new Date().getFullYear()
const min = max - (max - 2020)
const years = []
for (let i = max; i >= min; i--) {
years.push(i)
}
return years
}
const years = generateArrayOfYears()
return (
<DatePicker
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<div className="flex items-center justify-between px-1">
<button
className="default-transition mr-1 text-th-fgd-3 hover:text-th-fgd-1"
onClick={decreaseMonth}
disabled={prevMonthButtonDisabled}
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
<div className="flex space-x-2">
<Select
className="w-28"
dropdownPanelClassName="text-left"
value={months[date.getMonth()]}
onChange={(value) => changeMonth(months.indexOf(value))}
>
{months.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
<Select
dropdownPanelClassName="text-left"
value={date.getFullYear()}
onChange={(value) => changeYear(value)}
>
{years.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
</div>
<button
className="default-transition ml-1 text-th-fgd-3 hover:text-th-fgd-1"
onClick={increaseMonth}
disabled={nextMonthButtonDisabled}
>
<ChevronRightIcon className="h-6 w-6" />
</button>
</div>
)}
placeholderText="dd/mm/yyyy"
dateFormat="dd/MM/yyyy"
selected={date}
onChange={(date: Date) => setDate(date)}
className="default-transition h-10 w-full cursor-pointer rounded-md border border-th-bkg-4 bg-th-bkg-1 px-2 text-th-fgd-1 hover:border-th-fgd-4 focus:border-th-fgd-4 focus:outline-none"
{...props}
/>
)
}
export default MangoDatePicker

View File

@ -0,0 +1,54 @@
import { ChevronRightIcon } from '@heroicons/react/solid'
import { useTranslation } from 'next-i18next'
import { enAU } from 'date-fns/locale'
import { DateRangePicker } from 'react-nice-dates'
import { Label } from './Input'
const MangoDateRangePicker = ({
startDate,
setStartDate,
endDate,
setEndDate,
}) => {
const { t } = useTranslation('common')
return (
<DateRangePicker
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
minimumDate={new Date('January 01, 2020 00:00:00')}
maximumDate={new Date()}
minimumLength={1}
format="dd MMM yyyy"
locale={enAU}
>
{({ startDateInputProps, endDateInputProps }) => (
<div className="date-range flex items-end">
<div className="w-full">
<Label>{t('from')}</Label>
<input
className="default-transition h-10 w-full rounded-md border border-th-bkg-4 bg-th-bkg-1 px-2 text-th-fgd-1 hover:border-th-fgd-4 focus:border-th-fgd-4 focus:outline-none"
{...startDateInputProps}
placeholder="Start Date"
/>
</div>
<div className="flex h-10 items-center justify-center">
<ChevronRightIcon className="mx-1 h-5 w-5 flex-shrink-0 text-th-fgd-3" />
</div>
<div className="w-full">
<Label>{t('to')}</Label>
<input
className="default-transition h-10 w-full rounded-md border border-th-bkg-4 bg-th-bkg-1 px-2 text-th-fgd-1 hover:border-th-fgd-4 focus:border-th-fgd-4 focus:outline-none"
{...endDateInputProps}
placeholder="End Date"
/>
</div>
</div>
)}
</DateRangePicker>
)
}
export default MangoDateRangePicker

View File

@ -30,7 +30,11 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
const { wallet } = useWallet()
const [keyBase58, setKeyBase58] = useState(
delegate.equals(PublicKey.default) ? '' : delegate.toBase58()
delegate && delegate.equals(PublicKey.default)
? ''
: delegate
? delegate.toBase58()
: ''
)
const [invalidKeyMessage, setInvalidKeyMessage] = useState('')
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
@ -40,6 +44,8 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
const setDelegate = async () => {
const mangoClient = useMangoStore.getState().connection.client
if (!mangoGroup || !mangoAccount || !wallet) return
try {
const key = keyBase58.length
? new PublicKey(keyBase58)
@ -47,11 +53,11 @@ const DelegateModal: FunctionComponent<DelegateModalProps> = ({
const txid = await mangoClient.setDelegate(
mangoGroup,
mangoAccount,
wallet?.adapter,
wallet.adapter,
key
)
actions.reloadMangoAccount()
onClose()
onClose?.()
notify({
title: t('delegate:delegate-updated'),
txid,

View File

@ -61,6 +61,7 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
const handleDeposit = () => {
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
if (!wallet || !mangoAccount) return
setSubmitting(true)
deposit({
@ -135,19 +136,25 @@ const DepositModal: FunctionComponent<DepositModalProps> = ({
validateAmountInput(amount)
}
const percentage = (parseFloat(inputAmount) / parseFloat(repayAmount)) * 100
const net = parseFloat(inputAmount) - parseFloat(repayAmount)
const percentage = repayAmount
? (parseFloat(inputAmount) / parseFloat(repayAmount)) * 100
: null
const net = repayAmount
? parseFloat(inputAmount) - parseFloat(repayAmount)
: null
const repayMessage =
percentage === 100
? t('repay-full')
: percentage > 100
: typeof percentage === 'number' && percentage > 100
? t('repay-and-deposit', {
amount: trimDecimals(net, 6).toString(),
symbol: selectedAccount.config.symbol,
})
: t('repay-partial', {
: typeof percentage === 'number'
? t('repay-partial', {
percentage: percentage.toFixed(2),
})
: ''
const inputDisabled =
selectedAccount &&

View File

@ -19,6 +19,9 @@ const DepositMsrmModal = ({ onClose, isOpen }) => {
const cluster = useMangoStore.getState().connection.cluster
const handleMsrmDeposit = async () => {
if (!mangoGroup || !mangoAccount || !wallet) {
return
}
setSubmitting(true)
const mangoClient = useMangoStore.getState().connection.client
const ownerMsrmAccount = walletTokens.find((t) =>

View File

@ -1,34 +1,16 @@
import React from 'react'
import { FiveOhFive } from './FiveOhFive'
import * as Sentry from '@sentry/react'
class ErrorBoundary extends React.Component<
any,
{ hasError: boolean; error: any }
> {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true, error: error }
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
// logErrorToMyService(error, errorInfo)
const ErrorBoundary: React.FC<any> = (props) => {
const postError = (error, componentStack) => {
if (process.env.NEXT_PUBLIC_ERROR_WEBHOOK_URL) {
try {
fetch(process.env.NEXT_PUBLIC_ERROR_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `UI ERROR: ${error} : ${errorInfo?.componentStack}`.slice(
0,
1999
),
content: `UI ERROR: ${error} : ${componentStack}`.slice(0, 1999),
}),
})
} catch (err) {
@ -37,14 +19,17 @@ class ErrorBoundary extends React.Component<
}
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <FiveOhFive error={this.state.error} />
}
return (
<Sentry.ErrorBoundary
fallback={({ error, componentStack }) => {
postError(error, componentStack)
return this.props.children
}
return <FiveOhFive error={error} />
}}
>
{props.children}
</Sentry.ErrorBoundary>
)
}
export default ErrorBoundary

View File

@ -9,14 +9,17 @@ import { useRouter } from 'next/router'
import { initialMarket } from './SettingsModal'
import * as MonoIcons from './icons'
import { Transition } from '@headlessui/react'
import useMangoStore from '../stores/useMangoStore'
const FavoritesShortcutBar = () => {
const [favoriteMarkets] = useLocalStorageState(FAVORITE_MARKETS_KEY, [])
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { asPath } = useRouter()
const marketsInfo = useMangoStore((s) => s.marketsInfo)
const renderIcon = (symbol) => {
const renderIcon = (mktName) => {
const symbol = mktName.slice(0, -5)
const iconName = `${symbol.slice(0, 1)}${symbol
.slice(1, 4)
.toLowerCase()}MonoIcon`
@ -39,18 +42,32 @@ const FavoritesShortcutBar = () => {
>
<StarIcon className="h-5 w-5 text-th-fgd-4" />
{favoriteMarkets.map((mkt) => {
const change24h = marketsInfo?.find((m) => m.name === mkt)?.change24h
return (
<Link href={`/?name=${mkt.name}`} key={mkt.name} shallow={true}>
<Link href={`/?name=${mkt}`} key={mkt} shallow={true}>
<a
className={`default-transition flex items-center whitespace-nowrap py-1 text-xs hover:text-th-primary ${
asPath.includes(mkt.name) ||
(asPath === '/' && initialMarket.name === mkt.name)
asPath.includes(mkt) ||
(asPath === '/' && initialMarket.name === mkt)
? 'text-th-primary'
: 'text-th-fgd-3'
}`}
>
{renderIcon(mkt.baseSymbol)}
<span className="mb-0 mr-1.5 text-xs">{mkt.name}</span>
{renderIcon(mkt)}
<span className="mb-0 mr-1.5 text-xs">{mkt}</span>
{change24h ? (
<div
className={`text-xs ${
change24h
? change24h >= 0
? 'text-th-green'
: 'text-th-red'
: 'text-th-fgd-4'
}`}
>
{`${(change24h * 100).toFixed(1)}%`}
</div>
) : null}
</a>
</Link>
)

View File

@ -48,7 +48,7 @@ export const FiveOhFive = ({ error }) => {
<div className="mx-auto max-w-xl py-16 sm:py-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide">
<GradientText>505 error</GradientText>
<GradientText>500 error</GradientText>
</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
Something went wrong

View File

@ -23,7 +23,9 @@ const FloatingElement: FunctionComponent<FloatingElementProps> = ({
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const handleConnect = useCallback(() => {
if (wallet) {
handleWalletConnect(wallet)
}
}, [wallet])
return (

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { XIcon } from '@heroicons/react/solid'
import { Connection } from '@solana/web3.js'
import { sumBy } from 'lodash'
import sumBy from 'lodash/sumBy'
import useInterval from '../hooks/useInterval'
import { SECONDS } from '../stores/useMangoStore'
import { useTranslation } from 'next-i18next'
@ -16,7 +16,7 @@ const getRecentPerformance = async (setShow, setTps) => {
const totalTransactions = sumBy(response, 'numTransactions')
const tps = totalTransactions / totalSecs
if (tps < 1900) {
if (tps < 1800) {
setShow(true)
setTps(tps)
} else {

View File

@ -11,8 +11,9 @@ interface InputProps {
[x: string]: any
}
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const {
const Input = forwardRef<HTMLInputElement, InputProps>(
({
ref,
type,
value,
onChange,
@ -23,7 +24,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
prefix,
prefixClassName,
suffix,
} = props
...props
}) => {
return (
<div className={`relative flex ${wrapperClassName}`}>
{prefix ? (
@ -34,9 +36,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
</div>
) : null}
<input
type={type}
value={value}
onChange={onChange}
className={`${className} h-10 w-full flex-1 rounded-md border bg-th-bkg-1 px-2 pb-px
text-th-fgd-1 ${
error ? 'border-th-red' : 'border-th-bkg-4'
@ -52,6 +51,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
disabled={disabled}
ref={ref}
{...props}
type={type}
value={value}
onChange={onChange}
/>
{suffix ? (
<span className="absolute right-0 flex h-full items-center bg-transparent pr-2 text-xs text-th-fgd-4">
@ -60,7 +62,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
) : null}
</div>
)
})
}
)
export default Input

View File

@ -178,10 +178,12 @@ class IntroTips extends Component<Props, State> {
if (nextStepIndex === 1) {
this.steps.updateStepElement(nextStepIndex)
const el = document.querySelector<HTMLElement>('.introjs-nextbutton')
if (el) {
el.style.pointerEvents = 'auto'
el.style.opacity = '100%'
}
}
}
componentDidUpdate(prevProps) {
if (this.props.connected !== prevProps.connected) {

View File

@ -10,7 +10,8 @@ import { TOKEN_LIST_URL } from '@jup-ag/core'
import { PublicKey } from '@solana/web3.js'
import useMangoStore from '../stores/useMangoStore'
import { connectionSelector } from '../stores/selectors'
import { sortBy, sum } from 'lodash'
import sortBy from 'lodash/sortBy'
import sum from 'lodash/sum'
import {
CogIcon,
ExclamationCircleIcon,
@ -48,6 +49,9 @@ import { handleWalletConnect } from 'components/ConnectWalletButton'
const TABS = ['Market Data', 'Performance Insights']
type UseJupiterProps = Parameters<typeof useJupiter>[0]
type UseFormValue = Omit<UseJupiterProps, 'amount'> & {
amount: null | number
}
const JupiterForm: FunctionComponent = () => {
const { t } = useTranslation(['common', 'swap'])
@ -55,17 +59,17 @@ const JupiterForm: FunctionComponent = () => {
useWallet()
const connection = useMangoStore(connectionSelector)
const [showSettings, setShowSettings] = useState(false)
const [depositAndFee, setDepositAndFee] = useState(null)
const [selectedRoute, setSelectedRoute] = useState<RouteInfo>(null)
const [depositAndFee, setDepositAndFee] = useState<any | null>(null)
const [selectedRoute, setSelectedRoute] = useState<RouteInfo | null>(null)
const [showInputTokenSelect, setShowInputTokenSelect] = useState(false)
const [showOutputTokenSelect, setShowOutputTokenSelect] = useState(false)
const [swapping, setSwapping] = useState(false)
const [tokens, setTokens] = useState<Token[]>([])
const [tokenPrices, setTokenPrices] = useState(null)
const [coinGeckoList, setCoinGeckoList] = useState(null)
const [walletTokens, setWalletTokens] = useState([])
const [tokenPrices, setTokenPrices] = useState<any | null>(null)
const [coinGeckoList, setCoinGeckoList] = useState<any[] | null>(null)
const [walletTokens, setWalletTokens] = useState<any[]>([])
const [slippage, setSlippage] = useState(0.5)
const [formValue, setFormValue] = useState<UseJupiterProps>({
const [formValue, setFormValue] = useState<UseFormValue>({
amount: null,
inputMint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
outputMint: new PublicKey('MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'),
@ -73,10 +77,10 @@ const JupiterForm: FunctionComponent = () => {
})
const [hasSwapped, setHasSwapped] = useLocalStorageState('hasSwapped', false)
const [showWalletDraw, setShowWalletDraw] = useState(false)
const [walletTokenPrices, setWalletTokenPrices] = useState(null)
const [walletTokenPrices, setWalletTokenPrices] = useState<any[] | null>(null)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const [feeValue, setFeeValue] = useState(null)
const [feeValue, setFeeValue] = useState<number | null>(null)
const [showRoutesModal, setShowRoutesModal] = useState(false)
const [loadWalletTokens, setLoadWalletTokens] = useState(false)
const [swapRate, setSwapRate] = useState(false)
@ -90,7 +94,7 @@ const JupiterForm: FunctionComponent = () => {
if (!publicKey) {
return
}
const ownedTokens = []
const ownedTokens: any[] = []
const ownedTokenAccounts = await getTokenAccountsByOwnerWithWrappedSol(
connection,
publicKey
@ -182,13 +186,15 @@ const JupiterForm: FunctionComponent = () => {
}, [inputTokenInfo, outputTokenInfo, coinGeckoList])
const amountInDecimal = useMemo(() => {
if (typeof formValue?.amount === 'number') {
return formValue.amount * 10 ** (inputTokenInfo?.decimals || 1)
}
}, [inputTokenInfo, formValue.amount])
const { routeMap, allTokenMints, routes, loading, exchange, error, refresh } =
useJupiter({
...formValue,
amount: amountInDecimal,
amount: amountInDecimal ? amountInDecimal : 0,
slippage,
})
@ -212,23 +218,26 @@ const JupiterForm: FunctionComponent = () => {
useEffect(() => {
const getDepositAndFee = async () => {
const fees = await selectedRoute.getDepositAndFee()
const fees = await selectedRoute?.getDepositAndFee()
if (fees) {
setDepositAndFee(fees)
}
}
if (selectedRoute && connected) {
getDepositAndFee()
}
}, [selectedRoute])
const outputTokenMints = useMemo(() => {
const outputTokenMints: any[] = useMemo(() => {
if (routeMap.size && formValue.inputMint) {
const routeOptions = routeMap.get(formValue.inputMint.toString())
const routeOptionTokens = routeOptions.map((address) => {
const routeOptionTokens =
routeOptions?.map((address) => {
return tokens.find((t) => {
return t?.address === address
})
})
}) ?? []
return routeOptionTokens
} else {
@ -237,7 +246,9 @@ const JupiterForm: FunctionComponent = () => {
}, [routeMap, tokens, formValue.inputMint])
const handleConnect = useCallback(() => {
if (wallet) {
handleWalletConnect(wallet)
}
}, [wallet])
const inputWalletBalance = () => {
@ -264,7 +275,7 @@ const JupiterForm: FunctionComponent = () => {
}
const [walletTokensWithInfos] = useMemo(() => {
const userTokens = []
const userTokens: any[] = []
tokens.map((item) => {
const found = walletTokens.find(
(token) => token.account.mint.toBase58() === item?.address
@ -295,6 +306,7 @@ const JupiterForm: FunctionComponent = () => {
}
const getSwapFeeTokenValue = async () => {
if (!selectedRoute) return
const mints = selectedRoute.marketInfos.map((info) => info.lpFee.mint)
const response = await fetch(
`https://api.coingecko.com/api/v3/simple/token_price/solana?contract_addresses=${mints.toString()}&vs_currencies=usd`
@ -303,7 +315,9 @@ const JupiterForm: FunctionComponent = () => {
const feeValue = selectedRoute.marketInfos.reduce((a, c) => {
const feeToken = tokens.find((item) => item?.address === c.lpFee?.mint)
const amount = c.lpFee?.amount / Math.pow(10, feeToken?.decimals)
// FIXME: Remove ts-ignore possibly move the logic out of a reduce
// @ts-ignore
const amount = c.lpFee?.amount / Math.pow(10, feeToken.decimals)
if (data[c.lpFee?.mint]) {
return a + data[c.lpFee?.mint].usd * amount
}
@ -311,8 +325,10 @@ const JupiterForm: FunctionComponent = () => {
return a + 1 * amount
}
}, 0)
if (feeValue) {
setFeeValue(feeValue)
}
}
useEffect(() => {
if (selectedRoute) {
@ -338,7 +354,7 @@ const JupiterForm: FunctionComponent = () => {
}
const sortedTokenMints = sortBy(tokens, (token) => {
return token?.symbol?.toLowerCase()
return token?.symbol.toLowerCase()
})
const outAmountUi = selectedRoute
@ -374,6 +390,7 @@ const JupiterForm: FunctionComponent = () => {
<div className="text-base font-bold text-th-fgd-1">
{t('wallet')}
</div>
{publicKey ? (
<a
className="flex items-center text-xs text-th-fgd-3 hover:text-th-fgd-2"
href={`https://explorer.solana.com/address/${publicKey}`}
@ -383,6 +400,7 @@ const JupiterForm: FunctionComponent = () => {
{abbreviateAddress(publicKey)}
<ExternalLinkIcon className="ml-0.5 -mt-0.5 h-3.5 w-3.5" />
</a>
) : null}
</div>
<IconButton onClick={() => refreshWallet()}>
<RefreshClockwiseIcon
@ -689,6 +707,7 @@ const JupiterForm: FunctionComponent = () => {
</IconButton>
</div>
</div>
{outAmountUi && formValue?.amount ? (
<div className="flex justify-between">
<span>{t('swap:rate')}</span>
<div>
@ -753,6 +772,7 @@ const JupiterForm: FunctionComponent = () => {
) : null}
</div>
</div>
) : null}
<div className="flex justify-between">
<span>{t('swap:price-impact')}</span>
<div className="text-right text-th-fgd-1">
@ -765,15 +785,17 @@ const JupiterForm: FunctionComponent = () => {
</div>
<div className="flex justify-between">
<span>{t('swap:minimum-received')}</span>
{outputTokenInfo?.decimals ? (
<div className="text-right text-th-fgd-1">
{numberFormatter.format(
selectedRoute?.outAmountWithSlippage /
10 ** outputTokenInfo?.decimals || 1
10 ** outputTokenInfo.decimals || 1
)}{' '}
{outputTokenInfo?.symbol}
</div>
) : null}
</div>
{!isNaN(feeValue) ? (
{typeof feeValue === 'number' ? (
<div className="flex justify-between">
<span>{t('swap:swap-fee')}</span>
<div className="flex items-center">
@ -797,6 +819,7 @@ const JupiterForm: FunctionComponent = () => {
info.marketMeta?.amm?.label,
})}
</span>
{feeToken?.decimals && (
<div className="text-th-fgd-1">
{(
info.lpFee?.amount /
@ -806,6 +829,7 @@ const JupiterForm: FunctionComponent = () => {
{info.lpFee?.pct * 100}
%)
</div>
)}
</div>
)
}
@ -830,13 +854,15 @@ const JupiterForm: FunctionComponent = () => {
feeRecipient: info.marketMeta?.amm?.label,
})}
</span>
{feeToken?.decimals && (
<div className="text-right text-th-fgd-1">
{(
info.lpFee?.amount /
Math.pow(10, feeToken?.decimals)
Math.pow(10, feeToken.decimals)
).toFixed(6)}{' '}
{feeToken?.symbol} ({info.lpFee?.pct * 100}%)
</div>
)}
</div>
)
})
@ -950,7 +976,14 @@ const JupiterForm: FunctionComponent = () => {
onClick={async () => {
if (!connected && zeroKey !== publicKey) {
handleConnect()
} else if (!loading && selectedRoute && connected) {
} else if (
!loading &&
selectedRoute &&
connected &&
wallet &&
signAllTransactions &&
signTransaction
) {
setSwapping(true)
let txCount = 1
let errorTxid
@ -988,21 +1021,27 @@ const JupiterForm: FunctionComponent = () => {
console.log('Error:', swapResult.error)
notify({
type: 'error',
title: swapResult.error.name,
description: swapResult.error.message,
title: swapResult?.error?.name
? swapResult.error.name
: '',
description: swapResult?.error?.message,
txid: errorTxid,
})
} else if ('txid' in swapResult) {
notify({
type: 'success',
title: 'Swap Successful',
description: `Swapped ${
const description =
swapResult?.inputAmount && swapResult.outputAmount
? `Swapped ${
swapResult.inputAmount /
10 ** (inputTokenInfo?.decimals || 1)
} ${inputTokenInfo?.symbol} to ${
swapResult.outputAmount /
10 ** (outputTokenInfo?.decimals || 1)
} ${outputTokenInfo?.symbol}`,
} ${outputTokenInfo?.symbol}`
: ''
notify({
type: 'success',
title: 'Swap Successful',
description,
txid: swapResult.txid,
})
setFormValue((val) => ({
@ -1033,7 +1072,7 @@ const JupiterForm: FunctionComponent = () => {
})}
</div>
<div className="thin-scroll max-h-96 overflow-y-auto overflow-x-hidden pr-1">
{routes.map((route, index) => {
{routes?.map((route, index) => {
const selected = selectedRoute === route
return (
<div

View File

@ -34,6 +34,9 @@ const MangoAccountSelect = ({
const mangoAccount = mangoAccounts.find(
(ma) => ma.publicKey.toString() === value
)
if (!mangoAccount) {
return
}
setSelectedMangoAccount(mangoAccount)
if (onChange) {
onChange(mangoAccount)

View File

@ -25,6 +25,7 @@ export default function MarketBalances() {
const isMobile = width ? width < breakpoints.sm : false
const handleSizeClick = (size, symbol) => {
if (!selectedMarket || !mangoGroup || !mangoGroupCache) return
const minOrderSize = selectedMarket.minOrderSize
const sizePrecisionDigits = getPrecisionDigits(minOrderSize)
const marketIndex = marketConfig.marketIndex
@ -59,10 +60,10 @@ export default function MarketBalances() {
})
}
if (!mangoGroup || !selectedMarket) return null
if (!mangoGroup || !selectedMarket || !mangoGroupCache) return null
return (
<div className={!connected ? 'blur filter' : null}>
<div className={!connected ? 'blur filter' : ''}>
{!isMobile ? (
<ElementTitle className="hidden 2xl:flex">{t('balances')}</ElementTitle>
) : null}

View File

@ -15,19 +15,23 @@ import { useTranslation } from 'next-i18next'
import SwitchMarketDropdown from './SwitchMarketDropdown'
import Tooltip from './Tooltip'
import { useWallet } from '@solana/wallet-adapter-react'
import { InformationCircleIcon } from '@heroicons/react/outline'
const OraclePrice = () => {
const oraclePrice = useOraclePrice()
const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
const decimals = useMemo(
() => getPrecisionDigits(selectedMarket?.tickSize),
() =>
selectedMarket?.tickSize !== undefined
? getPrecisionDigits(selectedMarket?.tickSize)
: null,
[selectedMarket]
)
return (
<div className="text-th-fgd-1 md:text-xs">
{oraclePrice && selectedMarket
{decimals && oraclePrice && selectedMarket
? oraclePrice.toNumber().toLocaleString(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
@ -100,10 +104,15 @@ const MarketDetails = () => {
{usdFormatter(market?.volumeUsd24h, 0)}
</div>
</div>
<Tooltip content={t('tooltip-funding')} placement={'bottom'}>
<div className="flex items-center justify-between hover:cursor-help md:block">
<div className="flex items-center justify-between md:block">
<div className="flex items-center text-th-fgd-3 md:pb-0.5 md:text-[0.65rem]">
{t('average-funding')}
<Tooltip
content={t('tooltip-funding')}
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3 hover:cursor-help" />
</Tooltip>
</div>
<div className="text-th-fgd-1 md:text-xs">
{`${market?.funding1h.toFixed(4)}% (${(
@ -113,16 +122,24 @@ const MarketDetails = () => {
).toFixed(2)}% APR)`}
</div>
</div>
</Tooltip>
<div className="flex items-center justify-between md:block">
<div className="text-th-fgd-3 md:pb-0.5 md:text-[0.65rem]">
{t('open-interest')}
</div>
<div className="text-th-fgd-1 md:text-xs">
{`${market?.openInterest.toLocaleString(undefined, {
<div className="flex items-center text-th-fgd-1 md:text-xs">
{usdFormatter(market?.openInterestUsd, 0)}
<Tooltip
content={`${market?.openInterest.toLocaleString(
undefined,
{
maximumFractionDigits:
perpContractPrecision[baseSymbol],
})} ${baseSymbol}`}
}
)} ${baseSymbol}`}
placement={'bottom'}
>
<InformationCircleIcon className="ml-1.5 h-4 w-4 text-th-fgd-3 hover:cursor-help" />
</Tooltip>
</div>
</div>
</>

View File

@ -5,9 +5,10 @@ import Link from 'next/link'
import * as MonoIcons from './icons'
import { initialMarket } from './SettingsModal'
// const isMarketSelected = ()
export default function MarketMenuItem({ menuTitle = '', linksArray = [] }) {
const MarketMenuItem: React.FC<{ menuTitle: string; linksArray: any[] }> = ({
menuTitle = '',
linksArray = [],
}) => {
const { asPath } = useRouter()
const [openState, setOpenState] = useState(false)
@ -76,3 +77,5 @@ export default function MarketMenuItem({ menuTitle = '', linksArray = [] }) {
</div>
)
}
export default MarketMenuItem

View File

@ -35,25 +35,39 @@ export const settlePosPnl = async (
const actions = useMangoStore.getState().actions
const mangoClient = useMangoStore.getState().connection.client
const rootBankAccount = mangoGroup?.rootBankAccounts[QUOTE_INDEX]
if (!mangoGroup || !mangoCache || !mangoAccount || !rootBankAccount) return
try {
const txids = await mangoClient.settlePosPnl(
mangoGroup,
mangoCache,
mangoAccount,
perpMarkets,
mangoGroup.rootBankAccounts[QUOTE_INDEX],
rootBankAccount,
wallet?.adapter,
mangoAccounts
)
actions.reloadMangoAccount()
for (const txid of txids) {
if (txid) {
// FIXME: Remove filter when settlePosPnl return type is undefined or string[]
const filteredTxids = txids?.filter(
(x) => typeof x === 'string'
) as string[]
if (filteredTxids) {
for (const txid of filteredTxids) {
notify({
title: t('pnl-success'),
description: '',
txid,
})
}
} else {
notify({
title: t('pnl-error'),
description: t('transaction-failed'),
type: 'error',
})
}
} catch (e) {
console.log('Error settling PNL: ', `${e}`, `${perpAccount}`)
@ -77,8 +91,18 @@ export const settlePnl = async (
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const mangoCache = useMangoStore.getState().selectedMangoGroup.cache
const actions = useMangoStore.getState().actions
const marketIndex = mangoGroup.getPerpMarketIndex(perpMarket.publicKey)
const marketIndex = mangoGroup?.getPerpMarketIndex(perpMarket.publicKey)
const mangoClient = useMangoStore.getState().connection.client
const rootBank = mangoGroup?.rootBankAccounts[QUOTE_INDEX]
if (
!rootBank ||
!mangoGroup ||
!mangoCache ||
!mangoAccount ||
typeof marketIndex !== 'number'
)
return
try {
const txid = await mangoClient.settlePnl(
@ -86,17 +110,25 @@ export const settlePnl = async (
mangoCache,
mangoAccount,
perpMarket,
mangoGroup.rootBankAccounts[QUOTE_INDEX],
rootBank,
mangoCache.priceCache[marketIndex].price,
wallet?.adapter,
mangoAccounts
)
actions.reloadMangoAccount()
if (txid) {
notify({
title: t('pnl-success'),
description: '',
txid,
})
} else {
notify({
title: t('pnl-error'),
description: t('transaction-failed'),
type: 'error',
})
}
} catch (e) {
console.log('Error settling PNL: ', `${e}`, `${perpAccount}`)
notify({
@ -138,6 +170,7 @@ export default function MarketPosition() {
}
const handleSizeClick = (size) => {
if (!mangoGroup || !mangoCache || !selectedMarket) return
const sizePrecisionDigits = getPrecisionDigits(selectedMarket.minOrderSize)
const priceOrDefault = price
? price
@ -156,11 +189,13 @@ export default function MarketPosition() {
}, [])
const handleSettlePnl = (perpMarket, perpAccount) => {
if (wallet) {
setSettling(true)
settlePnl(perpMarket, perpAccount, t, undefined, wallet).then(() => {
setSettling(false)
})
}
}
if (!mangoGroup || !selectedMarket || !(selectedMarket instanceof PerpMarket))
return null
@ -195,7 +230,7 @@ export default function MarketPosition() {
return (
<>
<div
className={!connected && !isMobile ? 'blur-sm filter' : null}
className={!connected && !isMobile ? 'blur-sm filter' : ''}
id="perp-positions-tip"
>
{!isMobile ? (
@ -293,23 +328,23 @@ export default function MarketPosition() {
)}
</div>
</div>
{basePosition && (
{basePosition ? (
<Button
onClick={() => setShowMarketCloseModal(true)}
className="mt-2.5 w-full"
>
<span>{t('market-close')}</span>
</Button>
)}
) : null}
</div>
{showMarketCloseModal && (
{showMarketCloseModal ? (
<MarketCloseModal
isOpen={showMarketCloseModal}
onClose={handleCloseWarning}
market={selectedMarket}
marketIndex={marketIndex}
/>
)}
) : null}
</>
)
}

View File

@ -1,6 +1,5 @@
import { useEffect, useState } from 'react'
import { MenuIcon, PlusCircleIcon } from '@heroicons/react/outline'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import MarketMenuItem from './MarketMenuItem'
import { LinkButton } from './Button'
import MarketsModal from './MarketsModal'
@ -8,19 +7,24 @@ import useLocalStorageState from '../hooks/useLocalStorageState'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { useTranslation } from 'next-i18next'
import useMangoStore from 'stores/useMangoStore'
const MarketSelect = () => {
const { t } = useTranslation('common')
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const [showMarketsModal, setShowMarketsModal] = useState(false)
const [hiddenMarkets] = useLocalStorageState('hiddenMarkets', [])
const [sortedMarkets, setSortedMarkets] = useState([])
const groupConfig = useMangoGroupConfig()
const [sortedMarkets, setSortedMarkets] = useState<any[]>([])
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
useEffect(() => {
const markets = []
const allMarkets = [...groupConfig.spotMarkets, ...groupConfig.perpMarkets]
if (groupConfig) {
const markets: any[] = []
const allMarkets = [
...groupConfig.spotMarkets,
...groupConfig.perpMarkets,
]
allMarkets.forEach((market) => {
const base = market.name.slice(0, -5)
const found = markets.find((b) => b.baseAsset === base)
@ -31,6 +35,7 @@ const MarketSelect = () => {
}
})
setSortedMarkets(markets)
}
}, [groupConfig])
return (

View File

@ -111,11 +111,13 @@ const MarketsModal = ({
{m.name}
<div className="flex items-center">
<span className="w-20 text-right">
{formatUsdValue(
{mangoGroup && mangoCache
? formatUsdValue(
mangoGroup
.getPrice(m.marketIndex, mangoCache)
.toNumber()
)}
)
: null}
</span>
{/* <span className="text-th-green text-right w-20">
+2.44%

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react'
import Link from 'next/link'
import { formatUsdValue, usdFormatter } from '../utils'
import { formatUsdValue, perpContractPrecision, usdFormatter } from '../utils'
import { Table, Td, Th, TrBody, TrHead } from './TableElements'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
@ -97,44 +97,6 @@ const MarketsTable = ({ isPerpMarket }) => {
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('low24h')}
>
<span className="font-normal text-th-fgd-3">
{t('daily-low')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'low24h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
onClick={() => requestSort('high24h')}
>
<span className="font-normal text-th-fgd-3">
{t('daily-high')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'high24h'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
: null
}`}
/>
</LinkButton>
</Th>
<Th>
<LinkButton
className="flex items-center font-normal no-underline"
@ -178,14 +140,14 @@ const MarketsTable = ({ isPerpMarket }) => {
<Th>
<LinkButton
className="flex items-center no-underline"
onClick={() => requestSort('openInterest')}
onClick={() => requestSort('openInterestUsd')}
>
<span className="font-normal text-th-fgd-3">
{t('open-interest')}
</span>
<ArrowSmDownIcon
className={`default-transition ml-1 h-4 w-4 flex-shrink-0 ${
sortConfig?.key === 'openInterest'
sortConfig?.key === 'openInterestUsd'
? sortConfig.direction === 'ascending'
? 'rotate-180 transform'
: 'rotate-360 transform'
@ -207,11 +169,10 @@ const MarketsTable = ({ isPerpMarket }) => {
baseSymbol,
change24h,
funding1h,
high24h,
last,
low24h,
name,
openInterest,
openInterestUsd,
volumeUsd24h,
} = market
const fundingApr = funding1h
@ -219,9 +180,11 @@ const MarketsTable = ({ isPerpMarket }) => {
: '-'
return (
<TrBody key={name}>
<TrBody key={name} className="hover:bg-th-bkg-3">
<Td>
<div className="flex items-center">
<Link href={`/?name=${name}`} shallow={true}>
<a className="hover:cursor-pointer">
<div className="flex h-full items-center text-th-fgd-2 hover:text-th-primary">
<img
alt=""
width="20"
@ -229,10 +192,10 @@ const MarketsTable = ({ isPerpMarket }) => {
src={`/assets/icons/${baseSymbol.toLowerCase()}.svg`}
className={`mr-2.5`}
/>
<Link href={`/?name=${name}`} shallow={true}>
<a className="default-transition text-th-fgd-2">{name}</a>
</Link>
<span className="default-transition">{name}</span>
</div>
</a>
</Link>
</Td>
<Td>
{last ? (
@ -252,20 +215,6 @@ const MarketsTable = ({ isPerpMarket }) => {
)}
</span>
</Td>
<Td>
{low24h ? (
formatUsdValue(low24h)
) : (
<span className="text-th-fgd-4">Unavailable</span>
)}
</Td>
<Td>
{high24h ? (
formatUsdValue(high24h)
) : (
<span className="text-th-fgd-4">Unavailable</span>
)}
</Td>
<Td>
{volumeUsd24h ? (
usdFormatter(volumeUsd24h, 0)
@ -286,12 +235,18 @@ const MarketsTable = ({ isPerpMarket }) => {
)}
</Td>
<Td>
{openInterest ? (
{openInterestUsd ? (
<>
<span>{openInterest.toLocaleString()}</span>{' '}
<span className="text-xs text-th-fgd-3">
<span>{usdFormatter(openInterestUsd, 0)}</span>{' '}
{openInterest ? (
<div className="text-xs text-th-fgd-4">
{openInterest.toLocaleString(undefined, {
maximumFractionDigits:
perpContractPrecision[baseSymbol],
})}{' '}
{baseSymbol}
</span>
</div>
) : null}
</>
) : (
<span className="text-th-fgd-4">Unavailable</span>

View File

@ -12,7 +12,7 @@ export default function NavDropMenu({
menuTitle = '',
linksArray = [],
}: NavDropMenuProps) {
const buttonRef = useRef(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const toggleMenu = () => {
buttonRef?.current?.click()

View File

@ -24,7 +24,7 @@ import Modal from './Modal'
import { useWallet } from '@solana/wallet-adapter-react'
interface NewAccountProps {
onAccountCreation?: (x?) => void
onAccountCreation: (x?) => void
}
const NewAccount: FunctionComponent<NewAccountProps> = ({
@ -55,6 +55,7 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
}
const handleNewAccountDeposit = () => {
if (!wallet) return
setSubmitting(true)
deposit({
amount: parseFloat(inputAmount),
@ -66,12 +67,14 @@ const NewAccount: FunctionComponent<NewAccountProps> = ({
await sleep(1000)
actions.fetchWalletTokens(wallet)
actions.fetchAllMangoAccounts(wallet)
setSubmitting(false)
if (response && response.length > 0) {
onAccountCreation(response[0])
notify({
title: 'Mango Account Created',
txid: response[1],
})
}
setSubmitting(false)
})
.catch((e) => {
setSubmitting(false)

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react'
import { Fragment, useEffect } from 'react'
import {
CheckCircleIcon,
ExternalLinkIcon,
@ -9,6 +9,7 @@ import useMangoStore, { CLUSTER } from '../stores/useMangoStore'
import { Notification, notify } from '../utils/notifications'
import { useTranslation } from 'next-i18next'
import Loading from './Loading'
import { Transition } from '@headlessui/react'
const NotificationList = () => {
const { t } = useTranslation('common')
@ -95,7 +96,7 @@ const Notification = ({ notification }: { notification: Notification }) => {
})
}
// auto hide a notification after 10 seconds unless it is a confirming or time out notification
// auto hide a notification after 8 seconds unless it is a confirming or time out notification
useEffect(() => {
const id = setTimeout(
() => {
@ -103,7 +104,7 @@ const Notification = ({ notification }: { notification: Notification }) => {
hideNotification()
}
},
parsedTitle || type === 'confirm' || type === 'error' ? 90000 : 8000
parsedTitle || type === 'confirm' || type === 'error' ? 40000 : 8000
)
return () => {
@ -111,9 +112,18 @@ const Notification = ({ notification }: { notification: Notification }) => {
}
})
if (!show) return null
return (
<Transition
show={show}
as={Fragment}
appear={true}
enter="transform ease-out duration-500 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:-translate-x-8"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transform ease-in duration-300 transition"
leaveFrom="translate-y-0 opacity-100 sm:translate-x-0"
leaveTo="-translate-y-2 opacity-0 sm:translate-y-0 sm:-translate-x-8"
>
<div
className={`pointer-events-auto mt-2 w-full max-w-sm overflow-hidden rounded-md border border-th-bkg-4 bg-th-bkg-3 shadow-lg ring-1 ring-black ring-opacity-5`}
>
@ -123,7 +133,9 @@ const Notification = ({ notification }: { notification: Notification }) => {
<CheckCircleIcon className={`mr-1 h-7 w-7 text-th-green`} />
) : null}
{type === 'info' && (
<InformationCircleIcon className={`mr-1 h-7 w-7 text-th-primary`} />
<InformationCircleIcon
className={`mr-1 h-7 w-7 text-th-primary`}
/>
)}
{type === 'error' && (
<XCircleIcon className={`mr-1 h-7 w-7 text-th-red`} />
@ -144,7 +156,10 @@ const Notification = ({ notification }: { notification: Notification }) => {
{txid ? (
<a
href={
'https://explorer.solana.com/tx/' + txid + '?cluster=' + CLUSTER
'https://explorer.solana.com/tx/' +
txid +
'?cluster=' +
CLUSTER
}
className="mt-1 flex items-center text-sm"
target="_blank"
@ -182,6 +197,7 @@ const Notification = ({ notification }: { notification: Notification }) => {
</div>
</div>
</div>
</Transition>
)
}

View File

@ -251,10 +251,14 @@ const MobileTable = ({
{order.side.toUpperCase()}
</span>
{order.perpTrigger
? `${order.size} ${
order.triggerCondition
} ${formatUsdValue(order.triggerPrice)}`
: `${order.size} at ${formatUsdValue(order.price)}`}
? `${order.size.toLocaleString(undefined, {
maximumFractionDigits: 4,
})} ${order.triggerCondition} ${formatUsdValue(
order.triggerPrice
)}`
: `${order.size.toLocaleString(undefined, {
maximumFractionDigits: 4,
})} at ${formatUsdValue(order.price)}`}
</div>
</div>
</div>
@ -342,8 +346,8 @@ const OpenOrdersTable = () => {
const { asPath } = useRouter()
const { wallet } = useWallet()
const openOrders = useMangoStore((s) => s.selectedMangoAccount.openOrders)
const [cancelId, setCancelId] = useState(null)
const [modifyId, setModifyId] = useState(null)
const [cancelId, setCancelId] = useState<any>(null)
const [modifyId, setModifyId] = useState<any>(null)
const [editOrderIndex, setEditOrderIndex] = useState(null)
const actions = useMangoStore((s) => s.actions)
const { width } = useViewport()
@ -361,12 +365,12 @@ const OpenOrdersTable = () => {
setCancelId(order.orderId)
let txid
try {
if (!selectedMangoGroup || !selectedMangoAccount) return
if (!selectedMangoGroup || !selectedMangoAccount || !wallet) return
if (market instanceof Market) {
txid = await mangoClient.cancelSpotOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet?.adapter,
wallet.adapter,
market,
order as Order
)
@ -377,7 +381,7 @@ const OpenOrdersTable = () => {
txid = await mangoClient.removeAdvancedOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet?.adapter,
wallet.adapter,
(order as PerpTriggerOrder).orderId
)
actions.reloadOrders()
@ -385,7 +389,7 @@ const OpenOrdersTable = () => {
txid = await mangoClient.cancelPerpOrder(
selectedMangoGroup,
selectedMangoAccount,
wallet?.adapter,
wallet.adapter,
market,
order as PerpOrder,
false

View File

@ -1,6 +1,6 @@
import React, { useRef, useEffect, useState, useMemo } from 'react'
import Big from 'big.js'
import { isEqual as isEqualLodash } from 'lodash'
import isEqualLodash from 'lodash/isEqual'
import useInterval from '../hooks/useInterval'
import usePrevious from '../hooks/usePrevious'
import {
@ -135,12 +135,12 @@ export default function Orderbook({ depth = 8 }) {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const currentOrderbookData = useRef(null)
const nextOrderbookData = useRef(null)
const currentOrderbookData = useRef<any>(null)
const nextOrderbookData = useRef<any>(null)
const previousDepth = usePrevious(depth)
const [openOrderPrices, setOpenOrderPrices] = useState([])
const [orderbookData, setOrderbookData] = useState(null)
const [openOrderPrices, setOpenOrderPrices] = useState<any[]>([])
const [orderbookData, setOrderbookData] = useState<any | null>(null)
const [defaultLayout, setDefaultLayout] = useState(true)
const [displayCumulativeSize, setDisplayCumulativeSize] = useState(false)
const [grouping, setGrouping] = useState(0.01)
@ -156,7 +156,8 @@ export default function Orderbook({ depth = 8 }) {
useInterval(() => {
if (
!currentOrderbookData.current ||
nextOrderbookData?.current?.bids &&
(!currentOrderbookData.current ||
!isEqualLodash(
currentOrderbookData.current.bids,
nextOrderbookData.current.bids
@ -166,7 +167,7 @@ export default function Orderbook({ depth = 8 }) {
nextOrderbookData.current.asks
) ||
previousDepth !== depth ||
previousGrouping !== grouping
previousGrouping !== grouping)
) {
// check if user has open orders so we can highlight them on orderbook
const openOrders =
@ -603,10 +604,11 @@ export default function Orderbook({ depth = 8 }) {
const OrderbookSpread = ({ orderbookData }) => {
const { t } = useTranslation('common')
const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
const decimals = useMemo(
() => getPrecisionDigits(selectedMarket?.tickSize),
[selectedMarket]
)
const decimals = useMemo(() => {
if (selectedMarket) {
getPrecisionDigits(selectedMarket?.tickSize)
}
}, [selectedMarket])
return (
<div className="mb-0 mt-3 flex justify-between rounded-md bg-th-bkg-1 p-2 text-xs">
@ -641,7 +643,7 @@ const OrderbookRow = React.memo<any>(
grouping: number
market: Market | PerpMarket
}) => {
const element = useRef(null)
const element = useRef<HTMLDivElement>(null)
const setMangoStore = useMangoStore(setStoreSelector)
const [showOrderbookFlash] = useLocalStorageState(ORDERBOOK_FLASH_KEY, true)
const flashClassName = side === 'sell' ? 'red-flash' : 'green-flash'
@ -671,13 +673,13 @@ const OrderbookRow = React.memo<any>(
const handlePriceClick = () => {
setMangoStore((state) => {
state.tradeForm.price = price
state.tradeForm.price = Number(formattedPrice)
})
}
const handleSizeClick = () => {
setMangoStore((state) => {
state.tradeForm.baseSize = size
state.tradeForm.baseSize = Number(formattedSize)
})
}
@ -700,7 +702,7 @@ const OrderbookRow = React.memo<any>(
side === 'buy' ? `bg-th-green-muted` : `bg-th-red-muted`
}`}
/>
<div className="flex w-full justify-between hover:font-semibold">
<div className="flex w-full items-center justify-between hover:font-semibold">
<div
onClick={handlePriceClick}
className={`z-10 text-xs leading-5 md:pl-5 md:leading-6 ${
@ -724,7 +726,7 @@ const OrderbookRow = React.memo<any>(
</>
) : (
<>
<div className="flex w-full justify-between hover:font-semibold">
<div className="flex w-full items-center justify-between hover:font-semibold">
<div
className={`z-10 text-xs leading-5 md:leading-6 ${
hasOpenOrder ? 'text-th-primary' : 'text-th-fgd-2'

View File

@ -18,7 +18,7 @@ const MenuButton: React.FC<{
className={`default-transition flex items-center justify-end whitespace-nowrap pb-2.5 text-xs tracking-wider hover:cursor-pointer hover:text-th-primary ${
disabled ? 'pointer-events-none text-th-fgd-4' : 'text-th-fgd-1'
}`}
onClick={disabled ? null : onClick}
onClick={disabled ? () => null : onClick}
>
{text}
</div>
@ -43,6 +43,7 @@ export const RedeemDropdown: React.FC = () => {
const loading = settling || settlingPosPnl
const handleSettleAll = async () => {
if (!wallet) return
setOpen(false)
setSettling(true)
for (const p of unsettledPositions) {
@ -54,10 +55,11 @@ export const RedeemDropdown: React.FC = () => {
}
const handleSettlePosPnl = async () => {
if (!wallet) return
setOpen(false)
setSettlingPosPnl(true)
for (const p of unsettledPositivePositions) {
await settlePosPnl([p.perpMarket], p.perpAccount, t, null, wallet)
await settlePosPnl([p.perpMarket], p.perpAccount, t, undefined, wallet)
}
setSettlingPosPnl(false)
}

View File

@ -47,7 +47,7 @@ const PositionsTable: React.FC = () => {
}, [])
const handleSizeClick = (size, indexPrice) => {
const sizePrecisionDigits = getPrecisionDigits(market.minOrderSize)
const sizePrecisionDigits = getPrecisionDigits(market!.minOrderSize)
const priceOrDefault = price ? price : indexPrice
const roundedSize = parseFloat(Math.abs(size).toFixed(sizePrecisionDigits))
const quoteSize = parseFloat((roundedSize * priceOrDefault).toFixed(2))
@ -68,10 +68,12 @@ const PositionsTable: React.FC = () => {
}
const handleSettlePnl = async (perpMarket, perpAccount, index) => {
if (wallet) {
setSettleSinglePos(index)
await settlePnl(perpMarket, perpAccount, t, undefined, wallet)
setSettleSinglePos(null)
}
}
return (
<div className="flex flex-col md:pb-2">
@ -311,7 +313,11 @@ const PositionsTable: React.FC = () => {
</div>
</div>
</div>
{breakEvenPrice ? (
<PnlText pnl={unrealizedPnl} />
) : (
'--'
)}
</div>
</>
}
@ -361,7 +367,7 @@ const PositionsTable: React.FC = () => {
<ShareModal
isOpen={showShareModal}
onClose={handleCloseShare}
position={positionToShare}
position={positionToShare!}
/>
) : null}
</div>

View File

@ -17,7 +17,7 @@ export default function RecentMarketTrades() {
const market = useMangoStore((s) => s.selectedMarket.current)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const [trades, setTrades] = useState([])
const [trades, setTrades] = useState<any[]>([])
const fetchTradesForChart = useCallback(async () => {
if (!marketConfig) return

View File

@ -1,6 +1,5 @@
import React, { useMemo, useState } from 'react'
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import Modal from './Modal'
import { ElementTitle } from './styles'
import Button, { LinkButton } from './Button'
@ -87,10 +86,12 @@ const SettingsModal = ({ isOpen, onClose }) => {
const rpcEndpoint =
NODE_URLS.find((node) => node.value === rpcEndpointUrl) || CUSTOM_NODE
const savedLanguageName = useMemo(
() => LANGS.find((l) => l.locale === savedLanguage).name,
[savedLanguage]
)
const savedLanguageName = useMemo(() => {
const matchingLang = LANGS.find((l) => l.locale === savedLanguage)
if (matchingLang) {
return matchingLang.name
}
}, [savedLanguage])
return (
<Modal isOpen={isOpen} onClose={onClose}>
@ -133,10 +134,12 @@ const SettingsModal = ({ isOpen, onClose }) => {
onClick={() => setSettingsView('Language')}
>
<span>{t('language')}</span>
{savedLanguageName ? (
<div className="flex items-center text-xs text-th-fgd-3">
{t(savedLanguageName)}
<ChevronRightIcon className="ml-1 h-5 w-5 text-th-fgd-1" />
</div>
) : null}
</button>
<button
className="default-transition flex w-full items-center justify-between rounded-none border-t border-th-bkg-4 py-3 font-normal text-th-fgd-1 hover:text-th-primary focus:outline-none"
@ -144,7 +147,7 @@ const SettingsModal = ({ isOpen, onClose }) => {
>
<span>{t('rpc-endpoint')}</span>
<div className="flex items-center text-xs text-th-fgd-3">
{rpcEndpoint.label}
{rpcEndpoint?.label}
<ChevronRightIcon className="ml-1 h-5 w-5 text-th-fgd-1" />
</div>
</button>
@ -190,18 +193,19 @@ const SettingsContent = ({ settingsView, setSettingsView }) => {
return <ThemeSettings setSettingsView={setSettingsView} />
case 'Language':
return <LanguageSettings />
case '':
default:
return null
}
}
const DefaultMarketSettings = ({ setSettingsView }) => {
const { t } = useTranslation('common')
const groupConfig = useMangoGroupConfig()
const allMarkets = [
...groupConfig.spotMarkets,
...groupConfig.perpMarkets,
].sort((a, b) => a.name.localeCompare(b.name))
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const allMarkets = groupConfig
? [...groupConfig.spotMarkets, ...groupConfig.perpMarkets].sort((a, b) =>
a.name.localeCompare(b.name)
)
: []
const [defaultMarket, setDefaultMarket] = useLocalStorageState(
DEFAULT_MARKET_KEY,
{
@ -269,7 +273,7 @@ const RpcEndpointSettings = ({ setSettingsView }) => {
<div className="flex flex-col text-th-fgd-1">
<Label>{t('rpc-endpoint')}</Label>
<Select
value={rpcEndpoint.label}
value={rpcEndpoint?.label}
onChange={(url) => handleSelectEndpointUrl(url)}
className="w-full"
>
@ -279,7 +283,7 @@ const RpcEndpointSettings = ({ setSettingsView }) => {
</Select.Option>
))}
</Select>
{rpcEndpoint.label === 'Custom' ? (
{rpcEndpoint?.label === 'Custom' ? (
<div className="pt-4">
<Label>{t('node-url')}</Label>
<Input

View File

@ -90,6 +90,7 @@ const ShareModal: FunctionComponent<ShareModalProps> = ({
}
useEffect(() => {
if (mangoCache) {
const mngoIndex = getMarketIndexBySymbol(groupConfig, 'MNGO')
const hasRequiredMngo =
@ -106,7 +107,8 @@ const ShareModal: FunctionComponent<ShareModalProps> = ({
if (hasRequiredMngo) {
setHasRequiredMngo(true)
}
}, [mangoAccount, mangoGroup])
}
}, [mangoAccount, mangoGroup, mangoCache])
useEffect(() => {
if (image) {
@ -119,7 +121,7 @@ const ShareModal: FunctionComponent<ShareModalProps> = ({
useEffect(() => {
// if the button is hidden we are taking a screenshot
if (!showButton) {
takeScreenshot(ref.current)
takeScreenshot(ref.current as HTMLElement)
}
}, [showButton])
@ -129,7 +131,9 @@ const ShareModal: FunctionComponent<ShareModalProps> = ({
const fetchCustomReferralLinks = useCallback(async () => {
// setLoading(true)
const referrerIds = await client.getReferrerIdsForMangoAccount(mangoAccount)
const referrerIds = await client.getReferrerIdsForMangoAccount(
mangoAccount!
)
setCustomRefLinks(referrerIds)
// setLoading(false)
@ -178,7 +182,7 @@ const ShareModal: FunctionComponent<ShareModalProps> = ({
value={
customRefLinks.length > 0
? `https://trade.mango.markets?ref=${customRefLinks[0].referrerId}`
: `https://trade.mango.markets?ref=${mangoAccount.publicKey.toString()}`
: `https://trade.mango.markets?ref=${mangoAccount?.publicKey.toString()}`
}
/>
</div>

View File

@ -33,7 +33,7 @@ const SwapSettingsModal = ({
const handleSave = () => {
setSlippage(inputValue ? parseFloat(inputValue) : tempSlippage)
onClose()
onClose?.()
}
useEffect(() => {

View File

@ -34,11 +34,11 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
const [hideChart, setHideChart] = useState(false)
const [baseTokenId, setBaseTokenId] = useState('')
const [quoteTokenId, setQuoteTokenId] = useState('')
const [inputTokenInfo, setInputTokenInfo] = useState(null)
const [outputTokenInfo, setOutputTokenInfo] = useState(null)
const [inputTokenInfo, setInputTokenInfo] = useState<any>(null)
const [outputTokenInfo, setOutputTokenInfo] = useState<any>(null)
const [mouseData, setMouseData] = useState<string | null>(null)
const [daysToShow, setDaysToShow] = useState(1)
const [topHolders, setTopHolders] = useState(null)
const [topHolders, setTopHolders] = useState<any>(null)
const { observe, width, height } = useDimensions()
const { t } = useTranslation(['common', 'swap'])
@ -78,6 +78,9 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
}
useEffect(() => {
if (!inputTokenId || !outputTokenId) {
return
}
if (['usd-coin', 'tether'].includes(inputTokenId)) {
setBaseTokenId(outputTokenId)
setQuoteTokenId(inputTokenId)
@ -99,7 +102,7 @@ const SwapTokenInfo: FunctionComponent<SwapTokenInfoProps> = ({
const inputData = await inputResponse.json()
const outputData = await outputResponse.json()
let data = []
let data: any[] = []
if (Array.isArray(inputData)) {
data = data.concat(inputData)
}

View File

@ -17,8 +17,8 @@ const insightTypeVals = ['best', 'worst']
dayjs.extend(relativeTime)
const SwapTokenInsights = ({ formState, jupiterTokens, setOutputToken }) => {
const [tokenInsights, setTokenInsights] = useState([])
const [filteredTokenInsights, setFilteredTokenInsights] = useState([])
const [tokenInsights, setTokenInsights] = useState<any>([])
const [filteredTokenInsights, setFilteredTokenInsights] = useState<any>([])
const [insightType, setInsightType] = useState(insightTypeVals[0])
const [filterBy, setFilterBy] = useState(filterByVals[0])
const [timeframe, setTimeframe] = useState(timeFrameVals[0])
@ -156,6 +156,9 @@ const SwapTokenInsights = ({ formState, jupiterTokens, setOutputToken }) => {
) : filteredTokenInsights.length > 0 ? (
<div className="border-b border-th-bkg-4">
{filteredTokenInsights.map((insight) => {
if (!insight) {
return null
}
const jupToken = jupiterTokens.find(
(t) => t?.extensions?.coingeckoId === insight.id
)

View File

@ -86,7 +86,7 @@ const SwapTokenSelect = ({
useEffect(() => {
function onEscape(e) {
if (e.keyCode === 27) {
onClose()
onClose?.()
}
}
window.addEventListener('keydown', onEscape)

View File

@ -1,5 +1,4 @@
import { Fragment, useCallback, useMemo, useRef, useState } from 'react'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import { Popover, Transition } from '@headlessui/react'
import { SearchIcon } from '@heroicons/react/outline'
import { ChevronDownIcon } from '@heroicons/react/solid'
@ -9,7 +8,7 @@ import MarketNavItem from './MarketNavItem'
import useMangoStore from '../stores/useMangoStore'
const SwitchMarketDropdown = () => {
const groupConfig = useMangoGroupConfig()
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const baseSymbol = marketConfig.baseSymbol
const isPerpMarket = marketConfig.kind === 'perp'
@ -32,7 +31,7 @@ const SwitchMarketDropdown = () => {
[marketsInfo]
)
const [suggestions, setSuggestions] = useState([])
const [suggestions, setSuggestions] = useState<any[]>([])
const [searchString, setSearchString] = useState('')
const buttonRef = useRef(null)
const { t } = useTranslation('common')
@ -43,7 +42,7 @@ const SwitchMarketDropdown = () => {
const onSearch = (searchString) => {
if (searchString.length > 0) {
const newSuggestions = suggestions.filter((v) =>
v.name.toLowerCase().includes(searchString.toLowerCase())
v.name?.toLowerCase().includes(searchString.toLowerCase())
)
setSuggestions(newSuggestions)
}
@ -81,7 +80,7 @@ const SwitchMarketDropdown = () => {
{isPerpMarket ? '-' : '/'}
</span>
<div className="pl-0.5 text-xl font-semibold">
{isPerpMarket ? 'PERP' : groupConfig.quoteSymbol}
{isPerpMarket ? 'PERP' : groupConfig?.quoteSymbol}
</div>
<div
className={`flex h-10 w-8 items-center justify-center rounded-none`}

View File

@ -17,8 +17,8 @@ export const Th = ({ children }) => (
</th>
)
export const TrBody = ({ children }) => (
<tr className="border-b border-th-bkg-4">{children}</tr>
export const TrBody = ({ children, className = '' }) => (
<tr className={`border-b border-th-bkg-4 ${className}`}>{children}</tr>
)
export const Td = ({

View File

@ -14,27 +14,30 @@ import TradeNavMenu from './TradeNavMenu'
import {
CalculatorIcon,
CurrencyDollarIcon,
LibraryIcon,
LightBulbIcon,
UserAddIcon,
} from '@heroicons/react/outline'
import { MangoIcon } from './icons'
import { useWallet } from '@solana/wallet-adapter-react'
const StyledNewLabel = ({ children, ...props }) => (
<div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
{children}
</div>
)
// const StyledNewLabel = ({ children, ...props }) => (
// <div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
// {children}
// </div>
// )
const TopBar = () => {
const { t } = useTranslation('common')
const { publicKey } = useWallet()
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const cluster = useMangoStore((s) => s.connection.cluster)
const [showAccountsModal, setShowAccountsModal] = useState(false)
const [defaultMarket] = useLocalStorageState(
DEFAULT_MARKET_KEY,
initialMarket
)
const isDevnet = cluster === 'devnet'
const handleCloseAccounts = useCallback(() => {
setShowAccountsModal(false)
@ -62,16 +65,7 @@ const TopBar = () => {
>
<TradeNavMenu />
<MenuItem href="/account">{t('account')}</MenuItem>
<div className="relative">
<MenuItem href="/markets">
{t('markets')}
<div className="absolute -right-3 -top-3 flex h-4 items-center justify-center rounded-full bg-gradient-to-br from-red-500 to-yellow-500 px-1.5">
<StyledNewLabel className="uppercase text-white">
new
</StyledNewLabel>
</div>
</MenuItem>
</div>
<MenuItem href="/markets">{t('markets')}</MenuItem>
<MenuItem href="/borrow">{t('borrow')}</MenuItem>
<MenuItem href="/swap">{t('swap')}</MenuItem>
<MenuItem href="/stats">{t('stats')}</MenuItem>
@ -103,6 +97,12 @@ const TopBar = () => {
true,
<LightBulbIcon className="h-4 w-4" key="learn" />,
],
[
t('governance'),
'https://dao.mango.markets/',
true,
<LibraryIcon className="h-4 w-4" key="governance" />,
],
[
'Mango v2',
'https://v2.mango.markets',
@ -126,6 +126,7 @@ const TopBar = () => {
</div>
</div>
<div className="flex items-center space-x-2.5">
{isDevnet ? <div className="pl-2 text-xxs">Devnet</div> : null}
<div className="pl-2">
<Settings />
</div>

View File

@ -12,13 +12,11 @@ import Button, { LinkButton } from './Button'
import Modal from './Modal'
import { ElementTitle } from './styles'
import { useTranslation } from 'next-i18next'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import { Popover, Transition } from '@headlessui/react'
import Checkbox from './Checkbox'
import dayjs from 'dayjs'
import DatePicker from './DatePicker'
import 'react-datepicker/dist/react-datepicker.css'
import DateRangePicker from './DateRangePicker'
import useMangoStore from 'stores/useMangoStore'
interface TradeHistoryFilterModalProps {
filters: any
@ -32,18 +30,20 @@ const TradeHistoryFilterModal: FunctionComponent<
> = ({ filters, setFilters, isOpen, onClose }) => {
const { t } = useTranslation('common')
const [newFilters, setNewFilters] = useState({ ...filters })
const [dateFrom, setDateFrom] = useState(null)
const [dateTo, setDateTo] = useState(null)
const [dateFrom, setDateFrom] = useState<Date | null>(null)
const [dateTo, setDateTo] = useState<Date | null>(null)
const [sizeFrom, setSizeFrom] = useState(filters?.size?.values?.from || '')
const [sizeTo, setSizeTo] = useState(filters?.size?.values?.to || '')
const [valueFrom, setValueFrom] = useState(filters?.value?.values?.from || '')
const [valueTo, setValueTo] = useState(filters?.value?.values?.to || '')
const groupConfig = useMangoGroupConfig()
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const markets = useMemo(
() =>
[...groupConfig.perpMarkets, ...groupConfig.spotMarkets].sort((a, b) =>
a.name.localeCompare(b.name)
),
groupConfig
? [...groupConfig.perpMarkets, ...groupConfig.spotMarkets].sort(
(a, b) => a.name.localeCompare(b.name)
)
: [],
[groupConfig]
)
@ -124,7 +124,7 @@ const TradeHistoryFilterModal: FunctionComponent<
useEffect(() => {
if (dateFrom && dateTo) {
const dateFromTimestamp = dayjs(dateFrom).unix() * 1000
const dateToTimestamp = dayjs(dateTo).unix() * 1000
const dateToTimestamp = (dayjs(dateTo).unix() + 86399) * 1000
// filter should still work if users get from/to backwards
const from =
dateFromTimestamp < dateToTimestamp
@ -152,8 +152,8 @@ const TradeHistoryFilterModal: FunctionComponent<
const handleResetFilters = () => {
setFilters({})
setNewFilters({})
setDateFrom('')
setDateTo('')
setDateFrom(null)
setDateTo(null)
setSizeFrom('')
setSizeTo('')
setValueFrom('')
@ -184,13 +184,13 @@ const TradeHistoryFilterModal: FunctionComponent<
<div className="pb-4">
<p className="font-bold text-th-fgd-1">{t('date')}</p>
<div className="flex items-center space-x-2">
<div className="w-1/2">
<Label>{t('from')}</Label>
<DatePicker date={dateFrom} setDate={setDateFrom} />
</div>
<div className="w-1/2">
<Label>{t('to')}</Label>
<DatePicker date={dateTo} setDate={setDateTo} />
<div className="w-full">
<DateRangePicker
startDate={dateFrom}
setStartDate={setDateFrom}
endDate={dateTo}
setEndDate={setDateTo}
/>
</div>
</div>
</div>

View File

@ -48,10 +48,10 @@ const formatTradeDateTime = (timestamp: BN | string) => {
const TradeHistoryTable = ({
numTrades,
showExportPnl,
showActions,
}: {
numTrades?: number
showExportPnl?: boolean
showActions?: boolean
}) => {
const { t } = useTranslation('common')
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
@ -114,6 +114,7 @@ const TradeHistoryTable = ({
}
const exportPerformanceDataToCSV = async () => {
if (!mangoAccount) return
setLoadExportData(true)
const exportData = await fetchHourlyPerformanceStats(
mangoAccount.publicKey.toString(),
@ -142,9 +143,9 @@ const TradeHistoryTable = ({
}, [data, filteredData])
const mangoAccountPk = useMemo(() => {
console.log('new mango account')
if (mangoAccount) {
return mangoAccount.publicKey.toString()
}
}, [mangoAccount])
const canWithdraw =
@ -152,12 +153,28 @@ const TradeHistoryTable = ({
return (
<>
{showActions ? (
<div className="flex flex-col pb-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center">
<h4 className="mb-0 flex items-center text-th-fgd-1">
{!initialLoad ? <Loading className="mr-2" /> : data.length}{' '}
{data.length === 1 ? 'Trade' : 'Trades'}
{data.length === 1
? t('number-trade', {
number: !initialLoad ? (
<Loading className="mr-2" />
) : (
data.length
),
})
: t('number-trades', {
number: !initialLoad ? (
<Loading className="mr-2" />
) : (
data.length
),
})}
</h4>
{mangoAccount ? (
<Tooltip
content={
<div className="mr-4 text-xs text-th-fgd-3">
@ -175,7 +192,9 @@ const TradeHistoryTable = ({
>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-pointer text-th-fgd-3" />
</Tooltip>
) : null}
</div>
<div className="flex flex-col space-y-3 pl-2 sm:flex-row sm:items-center sm:space-y-0 sm:space-x-3">
{hasActiveFilter ? (
<LinkButton
@ -197,7 +216,7 @@ const TradeHistoryTable = ({
{t('filter')}
</Button>
) : null}
{canWithdraw && showExportPnl ? (
{canWithdraw && !isMobile ? (
<Button
className={`flex h-8 items-center justify-center whitespace-nowrap pt-0 pb-0 pl-3 pr-3 text-xs`}
onClick={exportPerformanceDataToCSV}
@ -212,7 +231,7 @@ const TradeHistoryTable = ({
)}
</Button>
) : null}
{canWithdraw ? (
{canWithdraw && mangoAccount && !isMobile ? (
<div className={`flex items-center`}>
<a
className={`default-transition flex h-8 w-full items-center justify-center whitespace-nowrap rounded-full bg-th-bkg-button pt-0 pb-0 pl-3 pr-3 text-xs font-bold text-th-fgd-1 hover:text-th-fgd-1 hover:brightness-[1.1]`}
@ -235,6 +254,7 @@ const TradeHistoryTable = ({
) : null}
</div>
</div>
) : null}
<div className={`flex flex-col sm:pb-4`}>
<div className={`overflow-x-auto sm:-mx-6 lg:-mx-8`}>
<div

View File

@ -22,13 +22,13 @@ const initialMenuCategories = [
{ name: 'Spot', desc: 'spot-desc' },
]
export const FAVORITE_MARKETS_KEY = 'favoriteMarkets'
export const FAVORITE_MARKETS_KEY = 'favoriteMarkets-0.1'
const TradeNavMenu = () => {
const [favoriteMarkets] = useLocalStorageState(FAVORITE_MARKETS_KEY, [])
const [activeMenuCategory, setActiveMenuCategory] = useState('Futures')
const [menuCategories, setMenuCategories] = useState(initialMenuCategories)
const buttonRef = useRef(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const { t } = useTranslation('common')
const marketsInfo = useMangoStore((s) => s.marketsInfo)
@ -55,7 +55,7 @@ const TradeNavMenu = () => {
? perpMarketsInfo
: activeMenuCategory === 'Spot'
? spotMarketsInfo
: favoriteMarkets,
: marketsInfo.filter((mkt) => favoriteMarkets.includes(mkt.name)),
[activeMenuCategory, marketsInfo]
)
@ -230,25 +230,25 @@ export const FavoriteMarketButton = ({ market }) => {
)
const addToFavorites = (mkt) => {
const newFavorites = [...favoriteMarkets, mkt]
const newFavorites: any = [...favoriteMarkets, mkt]
setFavoriteMarkets(newFavorites)
}
const removeFromFavorites = (mkt) => {
setFavoriteMarkets(favoriteMarkets.filter((m) => m.name !== mkt.name))
setFavoriteMarkets(favoriteMarkets.filter((m) => m.name !== mkt))
}
return favoriteMarkets.find((mkt) => mkt.name === market.name) ? (
return favoriteMarkets.find((mkt) => mkt === market.name) ? (
<button
className="default-transition flex items-center justify-center text-th-primary hover:text-th-fgd-3"
onClick={() => removeFromFavorites(market)}
onClick={() => removeFromFavorites(market.name)}
>
<FilledStarIcon className="h-5 w-5" />
</button>
) : (
<button
className="default-transition flex items-center justify-center text-th-fgd-4 hover:text-th-primary"
onClick={() => addToFavorites(market)}
onClick={() => addToFavorites(market.name)}
>
<StarIcon className="h-5 w-5" />
</button>

View File

@ -1,6 +1,7 @@
import dynamic from 'next/dynamic'
import { Responsive, WidthProvider } from 'react-grid-layout'
import { round, max } from 'lodash'
import round from 'lodash/round'
import max from 'lodash/max'
import MobileTradePage from './mobile/MobileTradePage'
const TVChartContainer = dynamic(
@ -71,7 +72,8 @@ const getCurrentBreakpoint = () => {
)
}
const TradePageGrid = () => {
const TradePageGrid: React.FC = () => {
const [mounted, setMounted] = useState(false)
const { uiLocked } = useMangoStore((s) => s.settings)
const [savedLayouts, setSavedLayouts] = useLocalStorageState(
GRID_LAYOUT_KEY,
@ -86,15 +88,15 @@ const TradePageGrid = () => {
}
}
const [orderbookDepth, setOrderbookDepth] = useState(10)
const [currentBreakpoint, setCurrentBreakpoint] = useState<string | null>(
null
)
const onBreakpointChange = (newBreakpoint: string) => {
console.log('new breakpoint', newBreakpoint)
setCurrentBreakpoint(newBreakpoint)
}
const [orderbookDepth, setOrderbookDepth] = useState(10)
const [currentBreakpoint, setCurrentBreakpoint] = useState(null)
const [mounted, setMounted] = useState(false)
useEffect(() => {
const adjustOrderBook = (layouts, breakpoint?: string | null) => {
const bp = breakpoint ? breakpoint : getCurrentBreakpoint()
@ -102,7 +104,10 @@ const TradePageGrid = () => {
return obj.i === 'orderbook'
})
let depth = orderbookLayout.h * 0.891 - 5
depth = round(max([1, depth]))
const maxNum = max([1, depth])
if (typeof maxNum === 'number') {
depth = round(maxNum)
}
setOrderbookDepth(depth)
}

View File

@ -86,11 +86,12 @@ const TVChartContainer = () => {
useEffect(() => {
if (
chartReady &&
selectedMarketConfig.name !== tvWidgetRef.current.activeChart().symbol()
tvWidgetRef.current &&
selectedMarketConfig.name !== tvWidgetRef.current?.activeChart()?.symbol()
) {
tvWidgetRef.current.setSymbol(
selectedMarketConfig.name,
defaultProps.interval,
tvWidgetRef.current.activeChart().resolution(),
() => {
if (showOrderLines) {
deleteLines()
@ -121,7 +122,7 @@ const TVChartContainer = () => {
'use_localstorage_for_settings',
'timeframes_toolbar',
// 'volume_force_overlay',
isMobile && 'left_toolbar',
isMobile ? 'left_toolbar' : '',
'show_logo_on_all_charts',
'caption_buttons_text_if_possible',
'header_settings',
@ -177,7 +178,10 @@ const TVChartContainer = () => {
tvWidgetRef.current = tvWidget
tvWidgetRef.current.onChartReady(function () {
const button = tvWidgetRef.current.createButton()
const button = tvWidgetRef?.current?.createButton()
if (!button) {
return
}
setChartReady(true)
button.textContent = 'OL'
if (showOrderLinesLocalStorage) {
@ -351,8 +355,7 @@ const TVChartContainer = () => {
function drawLine(order, market) {
const orderSizeUi = roundPerpSize(order.size, market.config.baseSymbol)
if (!tvWidgetRef.current.chart()) return
if (!tvWidgetRef?.current?.chart() || !wallet) return
return tvWidgetRef.current
.chart()
.createOrderLine({ disableUndo: false })
@ -368,7 +371,7 @@ const TVChartContainer = () => {
(order.side === 'sell' &&
updatedOrderPrice < 0.95 * selectedMarketPrice)
) {
tvWidgetRef.current.showNoticeDialog({
tvWidgetRef.current?.showNoticeDialog({
title: t('tv-chart:outside-range'),
body:
t('tv-chart:slippage-warning', {
@ -383,7 +386,7 @@ const TVChartContainer = () => {
},
})
} else {
tvWidgetRef.current.showConfirmDialog({
tvWidgetRef.current?.showConfirmDialog({
title: t('tv-chart:modify-order'),
body: t('tv-chart:modify-order-details', {
orderSize: orderSizeUi,
@ -407,7 +410,7 @@ const TVChartContainer = () => {
})
}
} else {
tvWidgetRef.current.showNoticeDialog({
tvWidgetRef.current?.showNoticeDialog({
title: t('tv-chart:advanced-order'),
body: t('tv-chart:advanced-order-details'),
callback: () => {
@ -417,7 +420,7 @@ const TVChartContainer = () => {
}
})
.onCancel(function () {
tvWidgetRef.current.showConfirmDialog({
tvWidgetRef.current?.showConfirmDialog({
title: t('tv-chart:cancel-order'),
body: t('tv-chart:cancel-order-details', {
orderSize: orderSizeUi,
@ -569,7 +572,7 @@ const TVChartContainer = () => {
if (orderLines.size > 0) {
orderLines?.forEach((value, key) => {
orderLines.get(key).remove()
orderLines.get(key)?.remove()
})
setMangoStore((state) => {
@ -587,7 +590,7 @@ const TVChartContainer = () => {
// updated order lines if a user's open orders change
useEffect(() => {
if (chartReady) {
if (chartReady && tvWidgetRef?.current) {
const orderLines = useMangoStore.getState().tradingView.orderLines
tvWidgetRef.current.onChartReady(() => {
let matchingOrderLines = 0
@ -607,7 +610,7 @@ const TVChartContainer = () => {
}
})
tvWidgetRef.current.activeChart().dataReady(() => {
tvWidgetRef.current?.activeChart().dataReady(() => {
if (
(showOrderLines && matchingOrderLines !== openOrdersForMarket) ||
orderLines?.size != matchingOrderLines

View File

@ -18,6 +18,7 @@ export const WalletListener: React.FC = () => {
useEffect(() => {
const onConnect = async () => {
if (!wallet) return
set((state) => {
state.selectedMangoAccount.initialLoad = true
})

View File

@ -170,10 +170,10 @@ export const WalletProvider: FC<WalletProviderProps> = ({
const handleDisconnect = useCallback(() => {
setState((state) => ({
...state,
connected: adapter.connected,
connected: false,
publicKey: null,
}))
}, [adapter])
}, [])
// Handle the adapter's error event, and local errors
const handleError = useCallback(

View File

@ -49,7 +49,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
const [maxAmount, setMaxAmount] = useState(0)
const [submitting, setSubmitting] = useState(false)
const [includeBorrow, setIncludeBorrow] = useState(borrow)
const [simulation, setSimulation] = useState(null)
const [simulation, setSimulation] = useState<any | null>(null)
const [showSimulation, setShowSimulation] = useState(false)
const { wallet } = useWallet()
const actions = useMangoStore((s) => s.actions)
@ -63,10 +63,12 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
() => tokens.find((t) => t.symbol === withdrawTokenSymbol),
[withdrawTokenSymbol, tokens]
)
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
const tokenIndex =
mangoGroup && token ? mangoGroup.getTokenIndex(token.mintKey) : 0
useEffect(() => {
if (!mangoGroup || !mangoAccount || !withdrawTokenSymbol) return
if (!mangoGroup || !mangoAccount || !withdrawTokenSymbol || !mangoCache)
return
const mintDecimals = mangoGroup.tokens[tokenIndex].decimals
const tokenDeposits = mangoAccount.getUiDeposit(
@ -99,7 +101,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
: maxWithBorrows
}
if (maxWithdraw.gt(I80F48.fromNumber(0))) {
if (maxWithdraw.gt(I80F48.fromNumber(0)) && token) {
setMaxAmount(
floorToDecimal(parseFloat(maxWithdraw.toFixed()), token.decimals)
)
@ -119,6 +121,8 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
newBorrow = newBorrow.add(tokenBorrows)
// clone MangoAccount and arrays to not modify selectedMangoAccount
// FIXME: MangoAccount needs type updated to accept null for pubKey
// @ts-ignore
const simulation = new MangoAccount(null, mangoAccount)
simulation.deposits = [...mangoAccount.deposits]
simulation.borrows = [...mangoAccount.borrows]
@ -156,6 +160,9 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
])
const handleWithdraw = () => {
if (!mangoGroup || !wallet) {
return
}
setSubmitting(true)
withdraw({
@ -194,7 +201,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
}
const getDepositsForSelectedAsset = (): I80F48 => {
return mangoAccount
return mangoAccount && mangoCache && mangoGroup
? mangoAccount.getUiDeposit(
mangoCache.rootBankCache[tokenIndex],
mangoGroup,
@ -273,6 +280,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
const mangoCache = useMangoStore.getState().selectedMangoGroup.cache
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
if (mangoGroup && mangoCache) {
return tokens.map((token) => {
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
return {
@ -287,13 +295,14 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
}
})
}
}
if (!withdrawTokenSymbol) return null
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
{!showSimulation ? (
{!showSimulation && mangoCache && mangoGroup ? (
<>
<Modal.Header>
<ElementTitle noMarginBottom>
@ -329,7 +338,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
}
onChange={(asset) => handleSetSelectedAsset(asset)}
>
{getTokenBalances().map(({ symbol, balance }) => (
{getTokenBalances()?.map(({ symbol, balance }) => (
<Select.Option key={symbol} value={symbol}>
<div className="flex items-center justify-between">
<div className="flex items-center">
@ -447,7 +456,7 @@ const WithdrawModal: FunctionComponent<WithdrawModalProps> = ({
<div className="pt-2 text-th-fgd-4">{`${t(
'includes-borrow'
)} ~${getBorrowAmount().toFixed(
mangoGroup.tokens[tokenIndex].decimals
mangoGroup?.tokens[tokenIndex].decimals
)} ${withdrawTokenSymbol}`}</div>
) : null}
</div>

View File

@ -19,6 +19,9 @@ const WithdrawMsrmModal = ({ onClose, isOpen }) => {
const cluster = useMangoStore.getState().connection.cluster
const handleMsrmWithdraw = async () => {
if (!mangoGroup || !mangoAccount || !wallet) {
return
}
setSubmitting(true)
const mangoClient = useMangoStore.getState().connection.client
const ownerMsrmAccount = walletTokens.find((t) =>

View File

@ -36,7 +36,7 @@ export default function AccountBorrows() {
)
const [borrowSymbol, setBorrowSymbol] = useState('')
const [depositToSettle, setDepositToSettle] = useState(null)
const [depositToSettle, setDepositToSettle] = useState<any | null>(null)
const [showBorrowModal, setShowBorrowModal] = useState(false)
const [showDepositModal, setShowDepositModal] = useState(false)
const { width } = useViewport()
@ -77,7 +77,7 @@ export default function AccountBorrows() {
<div className="flex flex-col pb-8 pt-4">
<div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full align-middle sm:px-6 lg:px-8">
{balances.find((b) => b.borrows.gt(ZERO_I80F48)) ? (
{balances.find((b) => b?.borrows?.gt(ZERO_I80F48)) ? (
!isMobile ? (
<Table>
<thead>
@ -90,7 +90,7 @@ export default function AccountBorrows() {
</thead>
<tbody>
{balances
.filter((assets) => assets.borrows.gt(ZERO_I80F48))
.filter((assets) => assets?.borrows?.gt(ZERO_I80F48))
.map((asset) => {
const token = getTokenBySymbol(
mangoConfig,
@ -113,18 +113,20 @@ export default function AccountBorrows() {
<div>{asset.symbol}</div>
</div>
</Td>
<Td>{asset.borrows.toFixed()}</Td>
<Td>{asset?.borrows?.toFixed()}</Td>
<Td>
{formatUsdValue(
asset.borrows
.mul(
{asset?.borrows && mangoCache && mangoGroup
? formatUsdValue(
asset?.borrows
?.mul(
mangoGroup.getPrice(
tokenIndex,
mangoCache
)
)
.toNumber()
)}
?.toNumber()
)
: null}
</Td>
<Td>
<span className={`text-th-red`}>
@ -142,7 +144,7 @@ export default function AccountBorrows() {
onClick={() =>
handleShowDeposit(
asset.symbol,
asset.borrows.toFixed()
asset?.borrows?.toFixed()
)
}
className="ml-3 h-8 pt-0 pb-0 pl-3 pr-3 text-xs"
@ -179,7 +181,7 @@ export default function AccountBorrows() {
colTwoHeader={t('balance')}
/>
{balances
.filter((assets) => assets.borrows.gt(ZERO_I80F48))
.filter((assets) => assets?.borrows?.gt(ZERO_I80F48))
.map((asset, i) => {
const token = getTokenBySymbol(
mangoConfig,
@ -204,7 +206,7 @@ export default function AccountBorrows() {
{asset.symbol}
</div>
<div className="text-fgd-1 text-right">
{asset.borrows.toFixed(
{asset?.borrows?.toFixed(
tokenPrecision[asset.symbol]
)}
</div>
@ -214,6 +216,7 @@ export default function AccountBorrows() {
panelTemplate={
<>
<div className="grid grid-flow-row grid-cols-2 gap-4 pb-4">
{asset?.borrows && mangoCache ? (
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('value')}
@ -229,6 +232,7 @@ export default function AccountBorrows() {
.toNumber()
)}
</div>
) : null}
<div className="text-left">
<div className="pb-0.5 text-xs text-th-fgd-3">
{t('borrow-rate')} (APR)
@ -249,7 +253,7 @@ export default function AccountBorrows() {
onClick={() =>
handleShowDeposit(
asset.symbol,
asset.borrows.toFixed()
asset?.borrows?.toFixed()
)
}
className="h-8 w-full pt-0 pb-0 text-xs"
@ -309,7 +313,8 @@ export default function AccountBorrows() {
</TrHead>
</thead>
<tbody>
{mangoConfig.tokens.map((token, i) => {
{mangoGroup &&
mangoConfig.tokens.map((token, i) => {
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
return (
<TrBody key={`${token.symbol}${i}`}>
@ -326,29 +331,35 @@ export default function AccountBorrows() {
</div>
</Td>
<Td>
{formatUsdValue(
{mangoGroup && mangoCache
? formatUsdValue(
mangoGroup
.getPrice(tokenIndex, mangoCache)
.toNumber()
)}
)
: null}
</Td>
<Td>
{mangoGroup ? (
<span className={`text-th-green`}>
{i80f48ToPercent(
mangoGroup.getDepositRate(tokenIndex)
).toFixed(2)}
%
</span>
) : null}
</Td>
<Td>
{mangoGroup ? (
<span className={`text-th-red`}>
{i80f48ToPercent(
mangoGroup.getBorrowRate(tokenIndex)
).toFixed(2)}
%
</span>
) : null}
</Td>
{mangoAccount ? (
{mangoAccount && mangoGroup && mangoCache ? (
<Td>
{mangoAccount
.getMaxWithBorrowForToken(
@ -377,6 +388,7 @@ export default function AccountBorrows() {
) : null}
<Td>
{mangoGroup
? mangoGroup
.getUiTotalDeposit(tokenIndex)
.sub(mangoGroup.getUiTotalBorrow(tokenIndex))
.toNumber()
@ -385,7 +397,8 @@ export default function AccountBorrows() {
tokenPrecision[token.symbol],
maximumFractionDigits:
tokenPrecision[token.symbol],
})}
})
: null}
</Td>
<Td>
<div className={`flex justify-end`}>
@ -417,7 +430,9 @@ export default function AccountBorrows() {
colOneHeader={t('asset')}
colTwoHeader={`${t('deposit')}/${t('borrow-rate')}`}
/>
{mangoConfig.tokens.map((token, i) => {
{mangoGroup &&
mangoCache &&
mangoConfig.tokens.map((token, i) => {
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
return (
<ExpandableRow
@ -506,7 +521,9 @@ export default function AccountBorrows() {
onClick={() => handleShowBorrow(token.symbol)}
className="col-span-2 h-8 pt-0 pb-0 text-xs"
disabled={
!connected || loadingMangoAccount || !canWithdraw
!connected ||
loadingMangoAccount ||
!canWithdraw
}
>
{t('borrow')}
@ -521,7 +538,7 @@ export default function AccountBorrows() {
</div>
</div>
</div>
{showBorrowModal && (
{showBorrowModal ? (
<WithdrawModal
isOpen={showBorrowModal}
onClose={handleCloseWithdraw}
@ -529,15 +546,15 @@ export default function AccountBorrows() {
title={t('borrow-withdraw')}
borrow
/>
)}
{showDepositModal && (
) : null}
{showDepositModal ? (
<DepositModal
isOpen={showDepositModal}
onClose={handleCloseDeposit}
repayAmount={depositToSettle.amount}
tokenSymbol={depositToSettle.symbol}
/>
)}
) : null}
</>
)
}

View File

@ -9,7 +9,7 @@ import {
TrBody,
TrHead,
} from '../TableElements'
import { isEmpty } from 'lodash'
import isEmpty from 'lodash/isEmpty'
import { useTranslation } from 'next-i18next'
import Select from '../Select'
import Pagination from '../Pagination'
@ -50,15 +50,17 @@ const AccountFunding = () => {
'hideFundingDust',
false
)
const [chartData, setChartData] = useState([])
const [chartData, setChartData] = useState<any[]>([])
const mangoAccountPk = useMemo(() => {
if (mangoAccount) {
return mangoAccount.publicKey.toString()
}
}, [mangoAccount])
const exportFundingDataToCSV = () => {
const assets = Object.keys(hourlyFunding)
let dataToExport = []
let dataToExport: any[] = []
for (const asset of assets) {
dataToExport = [
@ -75,7 +77,7 @@ const AccountFunding = () => {
}
const title = `${
mangoAccount.name || mangoAccount.publicKey
mangoAccount?.name || mangoAccount?.publicKey
}-Funding-${new Date().toLocaleDateString()}`
const columns = ['Timestamp', 'Asset', 'Amount']
@ -89,7 +91,7 @@ const AccountFunding = () => {
}, [selectedAsset, hourlyFunding])
useEffect(() => {
const hideDust = []
const hideDust: any[] = []
const fetchFundingStats = async () => {
setLoadTotalStats(true)
const response = await fetch(
@ -172,7 +174,7 @@ const AccountFunding = () => {
(d) => new Date(d.time).getTime() > start
)
const dailyFunding = []
const dailyFunding: any[] = []
for (let i = 0; i < 30; i++) {
dailyFunding.push({

View File

@ -43,9 +43,9 @@ export default function AccountHistory() {
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoAccountPk = useMemo(() => {
console.log('new mango account')
if (mangoAccount) {
return mangoAccount.publicKey.toString()
}
}, [mangoAccount])
useEffect(() => {
@ -91,7 +91,7 @@ export default function AccountHistory() {
const ViewContent = ({ view, history }) => {
switch (view) {
case 'Trades':
return <TradeHistoryTable showExportPnl />
return <TradeHistoryTable showActions />
case 'Deposit':
return <HistoryTable history={history} view={view} />
case 'Withdraw':
@ -99,7 +99,7 @@ const ViewContent = ({ view, history }) => {
case 'Liquidation':
return <LiquidationHistoryTable history={history} view={view} />
default:
return <TradeHistoryTable showExportPnl />
return <TradeHistoryTable showActions />
}
}
@ -200,7 +200,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
const tab = historyViews.filter((v) => v.key == view)[0].label
const title = `${
mangoAccount.name || mangoAccount.publicKey
mangoAccount?.name || mangoAccount?.publicKey
}-${tab}-${new Date().toLocaleDateString()}`
exportDataToCSV(dataToExport, title, headers, t)
@ -211,15 +211,16 @@ const LiquidationHistoryTable = ({ history, view }) => {
<div className="flex items-center justify-between pb-3">
<div className="flex items-center">
<h4 className="mb-0 text-th-fgd-1">
{filteredHistory.length}{' '}
{filteredHistory.length === 1 ? view : `${view}s`}
{filteredHistory.length === 1
? t('number-liquidation', { number: filteredHistory.length })
: t('number-liquidations', { number: filteredHistory.length })}
</h4>
<Tooltip
content={
<div className="mr-4 text-xs text-th-fgd-3">
{t('delay-displaying-recent')} {t('use-explorer-one')}
<a
href={`https://explorer.solana.com/address/${mangoAccount.publicKey.toString()}`}
href={`https://explorer.solana.com/address/${mangoAccount?.publicKey?.toString()}`}
target="_blank"
rel="noopener noreferrer"
>
@ -341,7 +342,7 @@ const LiquidationHistoryTable = ({ history, view }) => {
</thead>
<tbody>
{items.map(({ activity_details, activity_type }) => {
let perpMarket: PerpMarket
let perpMarket: PerpMarket | null = null
if (activity_type.includes('perp')) {
const symbol = activity_details.perp_market.split('-')[0]
const marketConfig = getMarketByBaseSymbolAndKind(
@ -464,7 +465,7 @@ const HistoryTable = ({ history, view }) => {
const tab = historyViews.filter((v) => v.key == view)[0].label
const title = `${
mangoAccount.name || mangoAccount.publicKey
mangoAccount?.name || mangoAccount?.publicKey
}-${tab}-${new Date().toLocaleDateString()}`
exportDataToCSV(dataToExport, title, headers, t)
@ -475,21 +476,20 @@ const HistoryTable = ({ history, view }) => {
<div className="flex items-center justify-between pb-3">
<div className="flex items-center">
<h4 className="mb-0 text-th-fgd-1">
{filteredHistory.length}{' '}
{filteredHistory.length === 1
? view === 'Withdraw'
? 'Withdrawal'
: view
? t('number-withdrawal', { number: filteredHistory.length })
: t('number-deposit', { number: filteredHistory.length })
: view === 'Withdraw'
? 'Withdrawals'
: `${view}s`}
? t('number-withdrawals', { number: filteredHistory.length })
: t('number-deposits', { number: filteredHistory.length })}
</h4>
<Tooltip
content={
<div className="mr-4 text-xs text-th-fgd-3">
{t('delay-displaying-recent')} {t('use-explorer-one')}
<a
href={`https://explorer.solana.com/address/${mangoAccount.publicKey.toString()}`}
href={`https://explorer.solana.com/address/${mangoAccount?.publicKey?.toString()}`}
target="_blank"
rel="noopener noreferrer"
>

View File

@ -12,7 +12,7 @@ import {
TrHead,
} from '../TableElements'
import { useTranslation } from 'next-i18next'
import { isEmpty } from 'lodash'
import isEmpty from 'lodash/isEmpty'
import usePagination from '../../hooks/usePagination'
import { numberCompactFormatter, roundToDecimal } from '../../utils/'
import Pagination from '../Pagination'
@ -62,7 +62,7 @@ const AccountInterest = () => {
const [loadHourlyStats, setLoadHourlyStats] = useState(false)
const [loadTotalStats, setLoadTotalStats] = useState(false)
const [selectedAsset, setSelectedAsset] = useState<string>('')
const [chartData, setChartData] = useState([])
const [chartData, setChartData] = useState<any[]>([])
const {
paginatedData,
setData,
@ -81,7 +81,9 @@ const AccountInterest = () => {
)
const mangoAccountPk = useMemo(() => {
if (mangoAccount) {
return mangoAccount.publicKey.toString()
}
}, [mangoAccount])
const token = useMemo(() => {
@ -92,7 +94,7 @@ const AccountInterest = () => {
const exportInterestDataToCSV = () => {
const assets = Object.keys(hourlyInterestStats)
let dataToExport = []
let dataToExport: any[] = []
for (const asset of assets) {
dataToExport = [
@ -110,7 +112,7 @@ const AccountInterest = () => {
}
const title = `${
mangoAccount.name || mangoAccount.publicKey
mangoAccount?.name || mangoAccount?.publicKey
}-Interest-${new Date().toLocaleDateString()}`
const headers = [
'Timestamp',
@ -135,7 +137,7 @@ const AccountInterest = () => {
}, [hourlyInterestStats])
useEffect(() => {
const hideDust = []
const hideDust: any[] = []
const fetchInterestStats = async () => {
setLoadTotalStats(true)
const response = await fetch(
@ -147,6 +149,9 @@ const AccountInterest = () => {
Object.entries(parsedResponse).forEach((r) => {
const tokens = groupConfig.tokens
const token = tokens.find((t) => t.symbol === r[0])
if (!token || !mangoGroup || !mangoCache) {
return
}
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
const price = mangoGroup.getPrice(tokenIndex, mangoCache).toNumber()
const interest =
@ -240,7 +245,7 @@ const AccountInterest = () => {
(d) => new Date(d.time).getTime() > start
)
const dailyInterest = []
const dailyInterest: any[] = []
for (let i = 0; i < 30; i++) {
dailyInterest.push({
@ -486,9 +491,13 @@ const AccountInterest = () => {
xAxis="time"
yAxis="interest"
data={chartData}
labelFormat={(x) =>
x === 0 ? 0 : x.toFixed(token.decimals + 1)
}
labelFormat={(x) => {
return x === 0
? 0
: token
? x.toFixed(token.decimals + 1)
: null
}}
tickFormat={handleDustTicks}
titleValue={chartData.reduce(
(a, c) => a + c.interest,
@ -506,6 +515,7 @@ const AccountInterest = () => {
className="relative mb-6 w-full rounded-md border border-th-bkg-4 p-4 sm:w-1/2"
style={{ height: '330px' }}
>
{token ? (
<Chart
hideRangeFilters
title={t('interest-chart-value-title', {
@ -518,7 +528,9 @@ const AccountInterest = () => {
x === 0
? 0
: x < 0
? `-$${Math.abs(x)?.toFixed(token.decimals + 1)}`
? `-$${Math.abs(x)?.toFixed(
token.decimals + 1
)}`
: `$${x?.toFixed(token.decimals + 1)}`
}
tickFormat={handleUsdDustTicks}
@ -531,6 +543,7 @@ const AccountInterest = () => {
yAxisWidth={increaseYAxisWidth ? 70 : 50}
zeroLine
/>
) : null}
</div>
) : null}
</div>
@ -555,6 +568,7 @@ const AccountInterest = () => {
<Td className="w-1/3">
<TableDateDisplay date={utc} />
</Td>
{token ? (
<Td className="w-1/3">
{stat.borrow_interest > 0
? `-${stat.borrow_interest.toFixed(
@ -565,6 +579,8 @@ const AccountInterest = () => {
)}{' '}
{selectedAsset}
</Td>
) : null}
{token ? (
<Td className="w-1/3">
{stat.borrow_interest > 0
? `-$${(
@ -574,6 +590,7 @@ const AccountInterest = () => {
stat.deposit_interest * stat.price
).toFixed(token.decimals + 1)}`}
</Td>
) : null}
</TrBody>
)
})}

View File

@ -63,34 +63,40 @@ export default function AccountOverview() {
const [hourlyPerformanceStats, setHourlyPerformanceStats] = useState([])
useEffect(() => {
const pubKey = mangoAccount?.publicKey?.toString()
const fetchData = async () => {
if (!pubKey) {
return
}
const stats = await fetchHourlyPerformanceStats(
mangoAccount.publicKey.toString(),
pubKey,
performanceRangePresets[performanceRangePresets.length - 1].value
)
setPnl(stats?.length ? stats?.[0]?.['pnl'] : 0)
setHourlyPerformanceStats(stats)
}
if (mangoAccount) {
if (pubKey) {
fetchData()
}
}, [mangoAccount?.publicKey])
const maintHealthRatio = useMemo(() => {
return mangoAccount
return mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Maint')
: 100
}, [mangoAccount, mangoGroup, mangoCache])
const initHealthRatio = useMemo(() => {
return mangoAccount
return mangoAccount && mangoGroup && mangoCache
? mangoAccount.getHealthRatio(mangoGroup, mangoCache, 'Init')
: 100
}, [mangoAccount, mangoGroup, mangoCache])
const mangoAccountValue = useMemo(() => {
return mangoAccount ? +mangoAccount.computeValue(mangoGroup, mangoCache) : 0
return mangoAccount && mangoGroup && mangoCache
? +mangoAccount.computeValue(mangoGroup, mangoCache)
: 0
}, [mangoAccount])
return mangoAccount ? (
@ -127,9 +133,11 @@ export default function AccountOverview() {
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
{t('leverage')}
</div>
{mangoGroup && mangoCache ? (
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
{mangoAccount.getLeverage(mangoGroup, mangoCache).toFixed(2)}x
</div>
) : null}
</div>
<div className="p-3 sm:p-4">
<div className="pb-0.5 text-xs text-th-fgd-3 sm:text-sm">
@ -187,21 +195,25 @@ export default function AccountOverview() {
<div className="border-t border-th-bkg-4 p-3 sm:p-4 md:border-b">
<div className="pb-0.5 text-th-fgd-3">{t('total-assets')}</div>
<div className="flex items-center">
{mangoGroup && mangoCache ? (
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{formatUsdValue(
+mangoAccount.getAssetsVal(mangoGroup, mangoCache)
)}
</div>
) : null}
</div>
</div>
<div className="border-b border-t border-th-bkg-4 p-3 sm:p-4">
<div className="pb-0.5 text-th-fgd-3">{t('total-liabilities')}</div>
<div className="flex items-center">
{mangoGroup && mangoCache ? (
<div className="text-xl font-bold text-th-fgd-1 md:text-2xl">
{formatUsdValue(
+mangoAccount.getLiabsVal(mangoGroup, mangoCache)
)}
</div>
) : null}
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@ import dayjs from 'dayjs'
import useMangoStore from '../../stores/useMangoStore'
import { Table, Td, Th, TrBody, TrHead } from '../TableElements'
import { useTranslation } from 'next-i18next'
import { isEmpty } from 'lodash'
import isEmpty from 'lodash/isEmpty'
import usePagination from '../../hooks/usePagination'
import { numberCompactFormatter } from '../../utils/'
import Pagination from '../Pagination'
@ -40,7 +40,9 @@ const AccountPerformance = () => {
} = usePagination(hourlyPerformanceStats)
const mangoAccountPk = useMemo(() => {
if (mangoAccount) {
return mangoAccount.publicKey.toString()
}
}, [mangoAccount])
const exportPerformanceDataToCSV = () => {
@ -54,7 +56,7 @@ const AccountPerformance = () => {
})
const title = `${
mangoAccount.name || mangoAccount.publicKey
mangoAccount?.name || mangoAccount?.publicKey
}-Performance-${new Date().toLocaleDateString()}`
const headers = ['Timestamp', 'Account Equity', 'PNL']
@ -97,7 +99,7 @@ const AccountPerformance = () => {
}
}, [hourlyPerformanceStats])
const increaseYAxisWidth = !!chartData.find((data) => data.value < 0.001)
const increaseYAxisWidth = !!chartData.find((data: any) => data.value < 0.001)
return (
<>

View File

@ -15,7 +15,7 @@ export const MangoAccountLookup = () => {
const validatePubKey = (key: string) => {
try {
const pubkey = new PublicKey(key)
return PublicKey.isOnCurve(pubkey.toBuffer())
return !!pubkey
} catch (e) {
return false
}

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { cloneDeep } from 'lodash'
import cloneDeep from 'lodash/cloneDeep'
import dayjs from 'dayjs'
import {
AreaChart,
@ -38,7 +38,7 @@ const PerformanceChart = ({
const { t } = useTranslation('common')
const { observe, width, height } = useDimensions()
const [chartData, setChartData] = useState([])
const [chartData, setChartData] = useState<any[]>([])
const [mouseData, setMouseData] = useState<string | null>(null)
const [chartToShow, setChartToShow] = useState('Value')
const [showSpotPnl, setShowSpotPnl] = useState(true)

View File

@ -1,7 +1,7 @@
type MobileTableHeaderProps = {
colOneHeader: string
colTwoHeader: string
colThreeHeader?: string
colThreeHeader?: string | null
}
const MobileTableHeader = ({

View File

@ -1,6 +1,5 @@
import { PerpMarket } from '@blockworks-foundation/mango-client'
import { useState, useMemo } from 'react'
import useMangoGroupConfig from '../../hooks/useMangoGroupConfig'
import useMangoStore from '../../stores/useMangoStore'
import Chart from '../Chart'
import BN from 'bn.js'
@ -40,8 +39,10 @@ function calculateFundingRate(
export default function StatsPerps({ perpStats }) {
const { t } = useTranslation('common')
const [selectedAsset, setSelectedAsset] = useState<string>('BTC-PERP')
const marketConfigs = useMangoGroupConfig().perpMarkets
const selectedMarketConfig = marketConfigs.find(
const marketConfigs = useMangoStore(
(s) => s.selectedMangoGroup.config
).perpMarkets
const selectedMarketConfig = marketConfigs?.find(
(m) => m.name === selectedAsset
)
const marketDirectory = useMangoStore(marketsSelector)
@ -52,9 +53,11 @@ export default function StatsPerps({ perpStats }) {
}, [markets])
const selectedMarket = useMemo(() => {
if (selectedMarketConfig) {
return perpMarkets.find((m) =>
m.publicKey.equals(selectedMarketConfig.publicKey)
)
}
}, [selectedMarketConfig, perpMarkets])
const perpsData = useMemo(() => {
@ -121,14 +124,14 @@ export default function StatsPerps({ perpStats }) {
onChange={(a) => setSelectedAsset(a)}
className="ml-4 w-36 flex-shrink-0 md:hidden"
>
{marketConfigs.map((market) => (
{marketConfigs?.map((market) => (
<Select.Option key={market.name} value={market.name}>
{market.name}
</Select.Option>
))}
</Select>
<div className="mb-4 hidden rounded-md bg-th-bkg-3 px-3 py-2 md:mb-6 md:flex md:px-4">
{marketConfigs.map((market, index) => (
{marketConfigs?.map((market, index) => (
<div
className={`py-1 text-xs font-bold md:px-2 md:text-sm ${
index > 0 ? 'ml-4 md:ml-2' : null
@ -182,6 +185,7 @@ export default function StatsPerps({ perpStats }) {
className="relative rounded-md border border-th-bkg-3 p-4"
style={{ height: '330px' }}
>
{selectedMarketConfig?.baseSymbol ? (
<Chart
title={t('open-interest')}
xAxis="time"
@ -198,6 +202,7 @@ export default function StatsPerps({ perpStats }) {
}
type="area"
/>
) : null}
</div>
</div>
<div className="mb-4">
@ -207,9 +212,11 @@ export default function StatsPerps({ perpStats }) {
<p className="mb-0">{t('depth-rewarded')}</p>
<div className="text-lg font-bold">
{maxDepthUi.toLocaleString() + ' '}
{selectedMarketConfig?.baseSymbol ? (
<span className="text-xs font-normal text-th-fgd-3">
{selectedMarketConfig.baseSymbol}
</span>
) : null}
</div>
</div>
<div className="col-span-1 border-y border-th-bkg-4 py-3">

View File

@ -7,6 +7,17 @@ import { Table, Td, Th, TrBody, TrHead } from '../TableElements'
import { ExpandableRow, Row } from '../TableElements'
import { useTranslation } from 'next-i18next'
interface Values {
name: string
value: number
time: string
}
interface Points {
value: number
time: string
}
function formatNumberString(x: number, decimals): string {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
@ -49,10 +60,12 @@ export default function StatsTotals({ latestStats, stats }) {
const isMobile = width ? width < breakpoints.sm : false
// get deposit and borrow values from stats
const depositValues = []
const borrowValues = []
const depositValues: Values[] = []
const borrowValues: Values[] = []
for (let i = 0; i < stats.length; i++) {
const time = stats[i].hourly
const name = stats[i].name
const depositValue =
stats[i].name === 'USDC'
? stats[i].totalDeposits
@ -63,19 +76,19 @@ export default function StatsTotals({ latestStats, stats }) {
? stats[i].totalBorrows
: stats[i].totalBorrows * stats[i].baseOraclePrice
if (depositValue) {
if (typeof depositValue === 'number' && name && time) {
depositValues.push({
name: stats[i].name,
name,
value: depositValue,
time: stats[i].hourly,
time,
})
}
if (borrowValue) {
if (typeof borrowValue === 'number' && name && time) {
borrowValues.push({
name: stats[i].name,
name,
value: borrowValue,
time: stats[i].hourly,
time,
})
}
}
@ -108,7 +121,7 @@ export default function StatsTotals({ latestStats, stats }) {
}
})
const points = []
const points: Points[] = []
for (const prop in holder) {
points.push({ time: prop, value: holder[prop] })

View File

@ -8,6 +8,7 @@ import {
nativeI80F48ToUi,
PerpMarket,
PerpOrderType,
ZERO_I80F48,
} from '@blockworks-foundation/mango-client'
import {
ExclamationIcon,
@ -124,9 +125,9 @@ export default function AdvancedTradeForm({
MAX_SLIPPAGE_KEY,
'0.025'
)
const [maxSlippagePercentage, setMaxSlippagePercentage] = useState(
clamp(parseFloat(maxSlippage), 0, 1) * 100
)
const [maxSlippagePercentage, setMaxSlippagePercentage] = useState<
number | null
>(null)
const [editMaxSlippage, setEditMaxSlippage] = useState(false)
const [showCustomSlippageForm, setShowCustomSlippageForm] = useState(false)
const slippagePresets = ['1', '1.5', '2', '2.5', '3']
@ -136,6 +137,12 @@ export default function AdvancedTradeForm({
setEditMaxSlippage(false)
}
useEffect(() => {
if (maxSlippage && !maxSlippagePercentage) {
setMaxSlippagePercentage(clamp(parseFloat(maxSlippage), 0, 1) * 100)
}
}, [setMaxSlippagePercentage, maxSlippage, maxSlippagePercentage])
useEffect(
() =>
useMangoStore.subscribe(
@ -180,7 +187,16 @@ export default function AdvancedTradeForm({
}, [set, tradeType, side])
const { max, deposits, borrows, spotMax, reduceMax } = useMemo(() => {
if (!mangoAccount) return { max: 0 }
const defaultValues = {
max: ZERO_I80F48,
deposits: ZERO_I80F48,
borrows: ZERO_I80F48,
spotMax: 0,
reduceMax: 0,
}
if (!mangoAccount || !mangoGroup || !mangoCache || !market) {
return defaultValues
}
const priceOrDefault = price
? I80F48.fromNumber(price)
: mangoGroup.getPrice(marketIndex, mangoCache)
@ -228,7 +244,7 @@ export default function AdvancedTradeForm({
reduceMax = 0
}
if (maxQuote.toNumber() <= 0) return { max: 0 }
if (maxQuote.toNumber() <= 0) return defaultValues
// 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
@ -463,8 +479,8 @@ export default function AdvancedTradeForm({
return (size / total) * 100
}
const roundedDeposits = parseFloat(deposits?.toFixed(sizeDecimalCount))
const roundedBorrows = parseFloat(borrows?.toFixed(sizeDecimalCount))
const roundedDeposits = parseFloat(deposits.toFixed(sizeDecimalCount))
const roundedBorrows = parseFloat(borrows.toFixed(sizeDecimalCount))
const closeDepositString =
percentToClose(baseSize, roundedDeposits) > 100
@ -543,7 +559,7 @@ export default function AdvancedTradeForm({
}
async function onSubmit() {
if (!price && isLimitOrder) {
if (!price && isLimitOrder && !postOnlySlide) {
notify({
title: t('missing-price'),
type: 'error',
@ -591,6 +607,7 @@ export default function AdvancedTradeForm({
description: t('try-again'),
type: 'error',
})
return
}
// TODO: this has a race condition when switching between markets or buy & sell
@ -612,12 +629,12 @@ export default function AdvancedTradeForm({
mangoGroup,
mangoAccount,
market,
wallet?.adapter,
wallet.adapter,
side,
orderPrice,
baseSize,
orderType,
null,
undefined,
totalMsrm > 0
)
actions.reloadOrders()
@ -628,7 +645,7 @@ export default function AdvancedTradeForm({
if (isMarketOrder) {
if (postOnlySlide) {
perpOrderType = 'postOnlySlide'
} else if (tradeType === 'Market' && maxSlippage !== undefined) {
} else if (tradeType === 'Market' && maxSlippage) {
perpOrderType = 'ioc'
if (side === 'buy') {
perpOrderPrice = markPrice * (1 + parseFloat(maxSlippage))
@ -648,7 +665,7 @@ export default function AdvancedTradeForm({
mangoGroup,
mangoAccount,
market,
wallet?.adapter,
wallet.adapter,
perpOrderType,
side,
perpOrderPrice,
@ -663,7 +680,7 @@ export default function AdvancedTradeForm({
mangoGroup,
mangoAccount,
market,
wallet?.adapter,
wallet.adapter,
side,
perpOrderPrice,
baseSize,
@ -724,7 +741,7 @@ export default function AdvancedTradeForm({
: baseSize > spotMax*/
const disabledTradeButton =
(!price && isLimitOrder) ||
(!price && isLimitOrder && !postOnlySlide) ||
!baseSize ||
!connected ||
!mangoAccount ||
@ -789,12 +806,14 @@ export default function AdvancedTradeForm({
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={postOnlySlide ? '' : price}
disabled={isMarketOrder || postOnlySlide}
placeholder={tradeType === 'Market' ? markPrice : null}
value={postOnlySlide && tradeType === 'Limit' ? '' : price}
disabled={
isMarketOrder || (postOnlySlide && tradeType === 'Limit')
}
placeholder={tradeType === 'Market' ? markPrice : ''}
prefix={
<>
{!postOnlySlide && (
{postOnlySlide && tradeType === 'Limit' ? null : (
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
@ -968,7 +987,7 @@ export default function AdvancedTradeForm({
</Tooltip>
</div>
) : null}
{marketConfig.kind === 'perp' ? (
{marketConfig.kind === 'perp' && tradeType === 'Limit' ? (
<div className="mt-3">
<Tooltip
className="hidden md:block"
@ -981,7 +1000,7 @@ export default function AdvancedTradeForm({
onChange={(e) => postOnlySlideOnChange(e.target.checked)}
disabled={isTriggerOrder}
>
Post & Slide
Slide
</Checkbox>
</Tooltip>
</div>
@ -1085,7 +1104,11 @@ export default function AdvancedTradeForm({
/>
) : (
<ButtonGroup
activeValue={maxSlippagePercentage.toString()}
activeValue={
maxSlippagePercentage
? maxSlippagePercentage.toString()
: ''
}
className="h-10"
onChange={(p) => setMaxSlippagePercentage(p)}
unit="%"
@ -1113,9 +1136,11 @@ export default function AdvancedTradeForm({
</Tooltip>
</div>
<div className="flex">
{maxSlippage ? (
<span className="text-th-fgd-1">
{(parseFloat(maxSlippage) * 100).toFixed(2)}%
</span>
) : null}
<LinkButton
className="ml-2 text-xs"
onClick={() => setEditMaxSlippage(true)}

View File

@ -7,8 +7,12 @@ const EstPriceImpact = ({
priceImpact?: { slippage: number[]; takerFee: number[] }
}) => {
const { t } = useTranslation('common')
const priceImpactAbs = priceImpact.slippage[0]
const priceImpactRel = priceImpact.slippage[1]
const priceImpactAbs = priceImpact?.slippage[0]
const priceImpactRel = priceImpact?.slippage[1]
if (!priceImpactAbs || !priceImpactRel) {
return null
}
return (
<div className={`text-xs text-th-fgd-3`}>
@ -31,9 +35,11 @@ const EstPriceImpact = ({
<div className="flex justify-between">
{t('taker-fee')}
<span className="text-th-fgd-1">
${priceImpact.takerFee[0].toFixed(2)}
${priceImpact?.takerFee[0].toFixed(2)}
<span className="px-1 text-th-fgd-4">|</span>
{percentFormat.format(priceImpact.takerFee[1])}
{priceImpact?.takerFee[1]
? percentFormat.format(priceImpact.takerFee[1])
: null}
</span>
</div>
</div>

View File

@ -298,6 +298,7 @@ export default function SimpleTradeForm({ initLeverage }) {
description: t('try-again'),
type: 'error',
})
return
}
const orderType = ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit'
@ -348,7 +349,8 @@ export default function SimpleTradeForm({ initLeverage }) {
}
const { max, deposits, borrows } = useMemo(() => {
if (!mangoAccount) return { max: 0 }
if (!mangoAccount || !mangoGroup || !mangoCache || !market)
return { max: 0 }
const priceOrDefault = price
? I80F48.fromNumber(price)
: mangoGroup.getPrice(marketIndex, mangoCache)
@ -398,8 +400,10 @@ export default function SimpleTradeForm({ initLeverage }) {
return (size / total) * 100
}
const roundedDeposits = parseFloat(deposits?.toFixed(sizeDecimalCount))
const roundedBorrows = parseFloat(borrows?.toFixed(sizeDecimalCount))
if (!deposits || !borrows) return null
const roundedDeposits = parseFloat(deposits.toFixed(sizeDecimalCount))
const roundedBorrows = parseFloat(borrows.toFixed(sizeDecimalCount))
const closeDepositString =
percentToClose(baseSize, roundedDeposits) > 100

View File

@ -7,12 +7,12 @@ import {
} from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import { i80f48ToPercent } from '../utils/index'
import { sumBy } from 'lodash'
import sumBy from 'lodash/sumBy'
import { I80F48 } from '@blockworks-foundation/mango-client'
import useMangoAccount from './useMangoAccount'
export function useBalances(): Balances[] {
const balances = []
const balances: any[] = []
const { mangoAccount } = useMangoAccount()
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
@ -23,7 +23,7 @@ export function useBalances(): Balances[] {
baseSymbol,
name,
} of mangoGroupConfig.spotMarkets) {
if (!mangoAccount || !mangoGroup) {
if (!mangoAccount || !mangoGroup || !mangoCache) {
return []
}
@ -147,6 +147,10 @@ export function useBalances(): Balances[] {
const quoteInOrders = sumBy(quoteBalances, 'orders')
const unsettled = sumBy(quoteBalances, 'unsettled')
if (!mangoGroup || !mangoCache) {
return []
}
const net: I80F48 = quoteMeta.deposits
.add(I80F48.fromNumber(unsettled))
.sub(quoteMeta.borrows)

View File

@ -42,11 +42,12 @@ export default function useFees(): { makerFee: number; takerFee: number } {
const refShare = mangoGroup.refShareCentibps / CENTIBPS_PER_UNIT
const mngoConfig = getSpotMarketByBaseSymbol(groupConfig, 'MNGO')
const mngoRequired =
mangoGroup.refMngoRequired.toNumber() /
const mngoRequired = mngoConfig
? mangoGroup.refMngoRequired.toNumber() /
Math.pow(10, mngoConfig.baseDecimals)
: null
if (mangoAccount) {
if (mangoAccount && mangoCache && mngoConfig) {
const mngoBalance = mangoAccount
.getUiDeposit(
mangoCache.rootBankCache[mngoConfig.marketIndex],
@ -57,7 +58,7 @@ export default function useFees(): { makerFee: number; takerFee: number } {
const hasReferrer = useMangoStore.getState().referrerPk
if (mngoBalance >= mngoRequired) {
if (typeof mngoRequired === 'number' && mngoBalance >= mngoRequired) {
discount = refSurcharge
} else {
discount = hasReferrer ? refSurcharge - refShare : 0

View File

@ -27,26 +27,29 @@ function decodeBookL2(market, accInfo: AccountInfo<Buffer>): number[][] {
const book = SpotOrderBook.decode(market, accInfo.data)
return book.getL2(depth).map(([price, size]) => [price, size])
} else if (market instanceof PerpMarket) {
// FIXME: Review the null being passed here
const book = new BookSide(
// @ts-ignore
null,
market,
BookSideLayout.decode(accInfo.data)
)
return book.getL2Ui(depth)
}
} else {
return []
}
return []
}
export function decodeBook(
market,
accInfo: AccountInfo<Buffer>
): BookSide | SpotOrderBook {
): BookSide | SpotOrderBook | undefined {
if (market && accInfo?.data) {
if (market instanceof Market) {
return SpotOrderBook.decode(market, accInfo.data)
} else if (market instanceof PerpMarket) {
// FIXME: Review the null being passed here
// @ts-ignore
return new BookSide(null, market, BookSideLayout.decode(accInfo.data))
}
}
@ -246,19 +249,6 @@ const useHydrateStore = () => {
actions.loadMarketFills()
}, 20 * SECONDS)
useInterval(() => {
const blockhashTimes = useMangoStore.getState().connection.blockhashTimes
const blockhashTimesCopy = [...blockhashTimes]
const mangoClient = useMangoStore.getState().connection.client
mangoClient.updateRecentBlockhash(blockhashTimesCopy).then(() => {
setMangoStore((state) => {
state.connection.client = mangoClient
state.connection.blockhashTimes = blockhashTimesCopy
})
})
}, 10 * SECONDS)
useEffect(() => {
actions.loadMarketFills()
}, [selectedMarket])

View File

@ -53,10 +53,12 @@ export function useLocalStorageStringState(
return [state, setState]
}
export default function useLocalStorageState<T = any>(
type LocalStoreState = any[] | any
export default function useLocalLocalStoreState(
key: string,
defaultState: T | null = null
): [T, (newState: T) => void] {
defaultState: LocalStoreState | null = null
): [LocalStoreState, (newState: LocalStoreState) => void] {
const [stringState, setStringState] = useLocalStorageStringState(
key,
JSON.stringify(defaultState)

View File

@ -3,7 +3,7 @@ import { MangoAccount } from '@blockworks-foundation/mango-client'
import shallow from 'zustand/shallow'
export default function useMangoAccount(): {
mangoAccount: MangoAccount
mangoAccount: MangoAccount | null
initialLoad: boolean
} {
const { mangoAccount, initialLoad } = useMangoStore(

View File

@ -1,16 +0,0 @@
import { useMemo } from 'react'
import useMangoStore from '../stores/useMangoStore'
import { Config } from '@blockworks-foundation/mango-client'
import { GroupConfig } from '@blockworks-foundation/mango-client/lib/src/config'
export default function useMangoGroupConfig(): GroupConfig {
const mangoGroupName = useMangoStore((state) => state.selectedMangoGroup.name)
const cluster = useMangoStore((s) => s.connection.cluster)
const mangoGroupConfig = useMemo(
() => Config.ids().getGroup(cluster, mangoGroupName),
[cluster, mangoGroupName]
)
return mangoGroupConfig
}

View File

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'
import { I80F48 } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import useMangoGroupConfig from './useMangoGroupConfig'
import { tokenPrecision } from '../utils'
const useMangoStats = () => {
@ -33,12 +32,12 @@ const useMangoStats = () => {
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoGroupName = useMangoStore((s) => s.selectedMangoGroup.name)
const connection = useMangoStore((s) => s.connection.current)
const config = useMangoGroupConfig()
const config = useMangoStore((s) => s.selectedMangoGroup.config)
useEffect(() => {
const fetchHistoricalStats = async () => {
const response = await fetch(
`https://mango-stats-v3.herokuapp.com/spot?mangoGroup=${mangoGroupName}`
`https://mango-transaction-log.herokuapp.com/v3/stats/spot_stats_hourly?mango-group=${mangoGroupName}`
)
const stats = await response.json()
setStats(stats)
@ -49,7 +48,7 @@ const useMangoStats = () => {
useEffect(() => {
const fetchHistoricalPerpStats = async () => {
const response = await fetch(
`https://mango-stats-v3.herokuapp.com/perp?mangoGroup=${mangoGroupName}`
`https://mango-transaction-log.herokuapp.com/v3/stats/perp_stats_hourly?mango-group=${mangoGroupName}`
)
const stats = await response.json()
setPerpStats(stats)
@ -61,6 +60,7 @@ const useMangoStats = () => {
const getLatestStats = async () => {
if (mangoGroup) {
const rootBanks = await mangoGroup.loadRootBanks(connection)
if (!config) return
const latestStats = config.tokens.map((token) => {
const rootBank = rootBanks.find((bank) => {
if (!bank) {
@ -68,6 +68,9 @@ const useMangoStats = () => {
}
return bank.publicKey.toBase58() == token.rootKey.toBase58()
})
if (!rootBank) {
return
}
const totalDeposits = rootBank.getUiTotalDeposit(mangoGroup)
const totalBorrows = rootBank.getUiTotalBorrow(mangoGroup)

View File

@ -90,7 +90,7 @@ function parsePerpOpenOrders(
const advancedOrdersForMarket = mangoAccount.advancedOrders
.map((o, i) => {
const pto = o.perpTrigger
if (pto.isActive && pto.marketIndex == config.marketIndex) {
if (pto && pto.isActive && pto.marketIndex == config.marketIndex) {
return {
...o,
orderId: i,
@ -133,6 +133,9 @@ export function useOpenOrders() {
const openOrders = Object.entries(markets).map(([address, market]) => {
const marketConfig = getMarketByPublicKey(groupConfig, address)
if (!marketConfig) {
return
}
if (market instanceof Market) {
return parseSpotOrders(market, marketConfig, mangoAccount, accountInfos)
} else if (market instanceof PerpMarket) {

View File

@ -2,11 +2,11 @@ import { I80F48 } from '@blockworks-foundation/mango-client'
import { useCallback, useEffect, useState } from 'react'
import useMangoStore from '../stores/useMangoStore'
export default function useOraclePrice(): I80F48 {
export default function useOraclePrice(): I80F48 | null {
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const selectedMarket = useMangoStore((s) => s.selectedMarket.config)
const [oraclePrice, setOraclePrice] = useState(null)
const [oraclePrice, setOraclePrice] = useState<any>(null)
const fetchOraclePrice = useCallback(() => {
if (mangoGroup && mangoCache) {

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
export default function usePrevious(value) {
export default function usePrevious(value): any {
// The ref object is a generic container whose current property is mutable ...
// ... and can hold any value, similar to an instance property on a class
const ref = useRef()

View File

@ -14,14 +14,18 @@ import html2canvas from 'html2canvas'
* hook for creating screenshot from html node
* @returns {HookReturn}
*/
const useScreenshot = () => {
const [image, setImage] = useState(null)
const useScreenshot: () => [
HTMLCanvasElement | null,
(node: HTMLElement) => Promise<void | HTMLCanvasElement>,
{ error: null }
] = () => {
const [image, setImage] = useState<HTMLCanvasElement | null>(null)
const [error, setError] = useState(null)
/**
* convert html node to image
* @param {HTMLElement} node
*/
const takeScreenShot = (node) => {
const takeScreenshot = (node: HTMLElement) => {
if (!node) {
throw new Error('You should provide correct html node.')
}
@ -38,7 +42,7 @@ const useScreenshot = () => {
croppedCanvas.width = cropWidth
croppedCanvas.height = cropHeight
croppedCanvasContext.drawImage(
croppedCanvasContext?.drawImage(
canvas,
cropPositionLeft,
cropPositionTop
@ -52,7 +56,7 @@ const useScreenshot = () => {
return [
image,
takeScreenShot,
takeScreenshot,
{
error,
},

View File

@ -1,10 +1,17 @@
import { useMemo, useState } from 'react'
type Direction = 'ascending' | 'descending'
interface Config {
key: string
direction: Direction
}
export function useSortableData<T>(
items: T[],
config = null
): { items: T[]; requestSort: any; sortConfig: any } {
const [sortConfig, setSortConfig] = useState(config)
const [sortConfig, setSortConfig] = useState<Config | null>(config)
const sortedItems = useMemo(() => {
const sortableItems = items ? [...items] : []
@ -28,7 +35,7 @@ export function useSortableData<T>(
}, [items, sortConfig])
const requestSort = (key) => {
let direction = 'ascending'
let direction: Direction = 'ascending'
if (
sortConfig &&
sortConfig.key === key &&

View File

@ -51,8 +51,8 @@ export function getFeeTier(msrmBalance: number, srmBalance: number): number {
const useSrmAccount = () => {
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const connection = useMangoStore((s) => s.connection.current)
const [srmAccount, setSrmAccount] = useState(null)
const [msrmAccount, setMsrmAccount] = useState(null)
const [srmAccount, setSrmAccount] = useState<any>(null)
const [msrmAccount, setMsrmAccount] = useState<any>(null)
useEffect(() => {
if (mangoGroup) {

View File

@ -26,8 +26,10 @@ function getMarketName(event) {
let marketName
if (!event.marketName && event.address) {
const marketInfo = getMarketByPublicKey(mangoGroupConfig, event.address)
if (marketInfo) {
marketName = marketInfo.name
}
}
return event.marketName || marketName
}
@ -96,7 +98,7 @@ export const useTradeHistory = () => {
const setMangoStore = useMangoStore.getState().set
useEffect(() => {
if (!mangoAccount || !selectedMangoGroup) return null
if (!mangoAccount || !selectedMangoGroup) return
const previousTradeHistory = useMangoStore.getState().tradeHistory.parsed
// combine the trade histories loaded from the DB

View File

@ -8,11 +8,13 @@ const ViewportContext = createContext({} as ViewportContextProps)
export const ViewportProvider = ({ children }) => {
const [mounted, setMounted] = useState(false)
const [width, setWidth] = useState(null)
const [width, setWidth] = useState<number>(0)
const handleWindowResize = () => {
if (typeof window !== 'undefined') {
setWidth(window.innerWidth)
}
}
useEffect(() => {
handleWindowResize()

View File

@ -17,13 +17,14 @@
"analyze": "ANALYZE=true yarn build"
},
"dependencies": {
"@blockworks-foundation/mango-client": "^3.3.27",
"@blockworks-foundation/mango-client": "^3.4.0",
"@headlessui/react": "^0.0.0-insiders.2dbc38c",
"@heroicons/react": "^1.0.0",
"@jup-ag/react-hook": "^1.0.0-beta.16",
"@next/bundle-analyzer": "^12.1.0",
"@jup-ag/react-hook": "^1.0.0-beta.19",
"@project-serum/serum": "0.13.55",
"@project-serum/sol-wallet-adapter": "0.2.0",
"@sentry/react": "^6.19.2",
"@sentry/tracing": "^6.19.2",
"@solana/wallet-adapter-base": "^0.9.5",
"@solana/wallet-adapter-bitpie": "^0.5.3",
"@solana/wallet-adapter-glow": "^0.1.1",
@ -41,6 +42,7 @@
"bignumber.js": "^9.0.2",
"bn.js": "^5.1.0",
"bs58": "^4.0.1",
"date-fns": "^2.28.0",
"dayjs": "^1.10.4",
"export-to-csv": "^0.2.1",
"html2canvas": "^1.4.1",
@ -54,9 +56,9 @@
"rc-slider": "^9.7.5",
"react": "^17.0.2",
"react-cool-dimensions": "^2.0.7",
"react-datepicker": "^4.7.0",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.3",
"react-nice-dates": "^3.1.0",
"react-portal": "^4.2.1",
"react-qr-code": "^2.0.3",
"react-super-responsive-table": "^5.2.0",
@ -67,6 +69,7 @@
"zustand": "^3.7.0"
},
"devDependencies": {
"@next/bundle-analyzer": "^12.1.0",
"@svgr/webpack": "^6.1.2",
"@testing-library/react": "^11.2.5",
"@types/node": "^14.14.25",
@ -74,6 +77,7 @@
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "^10.4.2",
"encoding": "^0.1.13",
"eslint": "^7.32.0",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-react": "^7.26.0",
@ -88,7 +92,7 @@
"tailwindcss": "^3.0.23",
"tsc-files": "^1.1.3",
"twin.macro": "^2.4.1",
"typescript": "^4.1.3"
"typescript": "^4.6.3"
},
"resolutions": {
"bn.js": "5.1.3",

View File

@ -2,9 +2,9 @@ import Head from 'next/head'
import { ThemeProvider } from 'next-themes'
import '../node_modules/react-grid-layout/css/styles.css'
import '../node_modules/react-resizable/css/styles.css'
import '../node_modules/react-datepicker/dist/react-datepicker.css'
import 'intro.js/introjs.css'
import '../styles/index.css'
import 'react-nice-dates/build/style.css'
import '../styles/datepicker.css'
import useHydrateStore from '../hooks/useHydrateStore'
import Notifications from '../components/Notification'
@ -27,6 +27,9 @@ import {
ReferrerIdRecord,
} from '@blockworks-foundation/mango-client'
import useTradeHistory from '../hooks/useTradeHistory'
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'
import { WalletProvider, WalletListener } from 'components/WalletAdapter'
import { PhantomWalletAdapter } from '@solana/wallet-adapter-phantom'
import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare'
@ -36,6 +39,14 @@ import { BitpieWalletAdapter } from '@solana/wallet-adapter-bitpie'
import { HuobiWalletAdapter } from '@solana/wallet-adapter-huobi'
import { GlowWalletAdapter } from '@solana/wallet-adapter-glow'
const SENTRY_URL = process.env.NEXT_PUBLIC_SENTRY_URL
if (SENTRY_URL) {
Sentry.init({
dsn: SENTRY_URL,
integrations: [new BrowserTracing()],
})
}
const MangoStoreUpdater = () => {
useHydrateStore()
return null
@ -70,14 +81,15 @@ const FetchReferrer = () => {
if (query.ref.length === 44) {
referrerPk = new PublicKey(query.ref)
} else {
let decodedRefLink: string
let decodedRefLink: string | null = null
try {
decodedRefLink = decodeURIComponent(query.ref as string)
} catch (e) {
console.log('Failed to decode referrer link', e)
}
const mangoClient = useMangoStore.getState().connection.client
if (!decodedRefLink) return
const { referrerPda } = await mangoClient.getReferrerPda(
mangoGroup,
decodedRefLink
@ -172,7 +184,6 @@ function App({ Component, pageProps }) {
<meta name="google" content="notranslate" />
<link rel="manifest" href="/manifest.json"></link>
</Head>
<ErrorBoundary>
<ErrorBoundary>
<PageTitle />
<MangoStoreUpdater />
@ -180,22 +191,17 @@ function App({ Component, pageProps }) {
<PerpPositionsStoreUpdater />
<TradeHistoryStoreUpdater />
<FetchReferrer />
</ErrorBoundary>
<ThemeProvider defaultTheme="Mango">
<WalletProvider wallets={wallets}>
<WalletListener />
<ViewportProvider>
<div className="min-h-screen bg-th-bkg-1">
<ErrorBoundary>
<GlobalNotification />
<Component {...pageProps} />
</ErrorBoundary>
</div>
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<ErrorBoundary>
<BottomBar />
</ErrorBoundary>
</div>
<Notifications />

View File

@ -120,12 +120,17 @@ export default function Account() {
}, [])
const handleConnect = useCallback(() => {
if (wallet) {
handleWalletConnect(wallet)
}
}, [wallet])
useEffect(() => {
async function loadUnownedMangoAccount() {
try {
if (!pubkey) {
return
}
const unownedMangoAccountPubkey = new PublicKey(pubkey)
const mangoClient = useMangoStore.getState().connection.client
if (mangoGroup) {
@ -158,7 +163,7 @@ export default function Account() {
const handleRouteChange = () => {
if (resetOnLeave) {
setMangoStore((state) => {
state.selectedMangoAccount.current = undefined
state.selectedMangoAccount.current = null
})
}
}
@ -210,24 +215,38 @@ export default function Account() {
const handleRedeemMngo = async () => {
const mangoClient = useMangoStore.getState().connection.client
const mngoNodeBank =
mangoGroup.rootBankAccounts[MNGO_INDEX].nodeBankAccounts[0]
mangoGroup?.rootBankAccounts?.[MNGO_INDEX]?.nodeBankAccounts?.[0]
if (!mangoAccount || !mngoNodeBank || !mangoGroup || !wallet) {
return
}
try {
const txid = await mangoClient.redeemAllMngo(
const txids = await mangoClient.redeemAllMngo(
mangoGroup,
mangoAccount,
wallet?.adapter,
wallet.adapter,
mangoGroup.tokens[MNGO_INDEX].rootBank,
mngoNodeBank.publicKey,
mngoNodeBank.vault
)
actions.reloadMangoAccount()
setMngoAccrued(ZERO_BN)
if (txids) {
for (const txid of txids) {
notify({
title: t('redeem-success'),
description: '',
txid,
})
}
} else {
notify({
title: t('redeem-failure'),
description: t('transaction-failed'),
type: 'error',
})
}
} catch (e) {
notify({
title: t('redeem-failure'),
@ -291,7 +310,7 @@ export default function Account() {
>
<div className="flex items-center whitespace-nowrap">
<GiftIcon className="mr-1.5 h-4 w-4 flex-shrink-0" />
{!mngoAccrued.eq(ZERO_BN)
{!mngoAccrued.eq(ZERO_BN) && mangoGroup
? t('claim-x-mngo', {
amount: nativeToUi(
mngoAccrued.toNumber(),

View File

@ -1,4 +1,4 @@
import { useCallback, useState, useEffect } from 'react'
import { useCallback, useState } from 'react'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import AccountsModal from '../components/AccountsModal'
@ -26,14 +26,6 @@ export default function Borrow() {
setShowAccountsModal(false)
}, [])
useEffect(() => {
// @ts-ignore
if (window.solana) {
// @ts-ignore
window.solana.connect({ onlyIfTrusted: true })
}
}, [])
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />

View File

@ -1,6 +1,5 @@
import React, { useEffect } from 'react'
import { useRouter } from 'next/router'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import useMangoStore, { serumProgramId } from '../stores/useMangoStore'
import {
getMarketByBaseSymbolAndKind,
@ -41,7 +40,7 @@ const PerpMarket: React.FC = () => {
const [alphaAccepted] = useLocalStorageState(ALPHA_MODAL_KEY, false)
const [showTour] = useLocalStorageState(SHOW_TOUR_KEY, false)
const { connected } = useWallet()
const groupConfig = useMangoGroupConfig()
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const setMangoStore = useMangoStore((s) => s.set)
const mangoAccount = useMangoStore(mangoAccountSelector)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
@ -54,6 +53,7 @@ const PerpMarket: React.FC = () => {
useEffect(() => {
async function loadUnownedMangoAccount() {
if (!pubkey) return
try {
const unownedMangoAccountPubkey = new PublicKey(pubkey)
const mangoClient = useMangoStore.getState().connection.client
@ -86,7 +86,7 @@ const PerpMarket: React.FC = () => {
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
let marketQueryParam, marketBaseSymbol, marketType, newMarket, marketIndex
if (name) {
if (name && groupConfig) {
marketQueryParam = name.toString().split(/-|\//)
marketBaseSymbol = marketQueryParam[0]
marketType = marketQueryParam[1].includes('PERP') ? 'perp' : 'spot'
@ -117,7 +117,7 @@ const PerpMarket: React.FC = () => {
// state.selectedMarket.current = null
state.selectedMarket.config = newMarket
state.tradeForm.price =
state.tradeForm.tradeType === 'Limit'
state.tradeForm.tradeType === 'Limit' && mangoCache
? parseFloat(
mangoGroup.getPrice(marketIndex, mangoCache).toFixed(2)
)

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -25,14 +25,6 @@ export default function Markets() {
setActiveTab(tabName)
}
useEffect(() => {
// @ts-ignore
if (window.solana) {
// @ts-ignore
window.solana.connect({ onlyIfTrusted: true })
}
}, [])
const isPerp = activeTab === 'perp'
return (

View File

@ -1,4 +1,4 @@
import { useEffect, useState, useCallback } from 'react'
import { useEffect, useMemo, useState, useCallback } from 'react'
import PageBodyContainer from '../components/PageBodyContainer'
import TopBar from '../components/TopBar'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
@ -90,7 +90,7 @@ export default function Referral() {
const [existingCustomRefLinks, setexistingCustomRefLinks] = useState<
ReferrerIdRecord[]
>([])
const [hasCopied, setHasCopied] = useState(null)
const [hasCopied, setHasCopied] = useState<number | null>(null)
const [showAccountsModal, setShowAccountsModal] = useState(false)
// const [hasReferrals] = useState(false) // Placeholder to show/hide users referral stats
@ -100,6 +100,7 @@ export default function Referral() {
const isMobile = width ? width < breakpoints.sm : false
const fetchCustomReferralLinks = useCallback(async () => {
if (!mangoAccount) return
setLoading(true)
const mangoClient = useMangoStore.getState().connection.client
const referrerIds = await mangoClient.getReferrerIdsForMangoAccount(
@ -110,6 +111,17 @@ export default function Referral() {
setLoading(false)
}, [mangoAccount])
const uniqueReferrals = useMemo(
() =>
hasReferrals
? referralHistory.reduce(
(resultSet, item) => resultSet.add(item['referree_mango_account']),
new Set()
).size
: 0,
[hasReferrals]
)
useEffect(() => {
if (mangoAccount) {
fetchCustomReferralLinks()
@ -152,7 +164,9 @@ export default function Referral() {
}
const handleConnect = useCallback(() => {
if (wallet) {
handleWalletConnect(wallet)
}
}, [wallet])
const submitRefLink = async () => {
@ -164,15 +178,16 @@ export default function Referral() {
type: 'error',
title: 'Invalid custom referral link',
})
return
}
if (!inputError) {
if (!inputError && mangoGroup && mangoAccount && wallet) {
try {
const mangoClient = useMangoStore.getState().connection.client
const txid = await mangoClient.registerReferrerId(
mangoGroup,
mangoAccount,
wallet?.adapter,
wallet.adapter,
encodedRefLink
)
notify({
@ -199,7 +214,7 @@ export default function Referral() {
const mngoIndex = getMarketIndexBySymbol(groupConfig, 'MNGO')
const hasRequiredMngo =
mangoGroup && mangoAccount
mangoGroup && mangoAccount && mangoCache
? mangoAccount
.getUiDeposit(
mangoCache.rootBankCache[mngoIndex],
@ -242,7 +257,7 @@ export default function Referral() {
{t('referrals:total-referrals')}
</div>
<div className="text-xl font-bold text-th-fgd-1 sm:text-2xl">
{referralHistory.length}
{uniqueReferrals}
</div>
</div>
</div>
@ -287,7 +302,7 @@ export default function Referral() {
className={`flex-shrink-0 ${
hasCopied === 1 && 'bg-th-green'
}`}
disabled={hasCopied}
disabled={typeof hasCopied === 'number'}
onClick={() =>
handleCopyLink(
`https://trade.mango.markets?ref=${mangoAccount.publicKey.toString()}`,
@ -339,7 +354,9 @@ export default function Referral() {
hasCopied === index + 1 &&
'bg-th-green'
}`}
disabled={hasCopied}
disabled={
typeof hasCopied === 'number'
}
onClick={() =>
handleCopyLink(
`https://trade.mango.markets?ref=${customRefs.referrerId}`,
@ -439,7 +456,7 @@ export default function Referral() {
</thead>
<tbody>
{referralHistory.map((ref) => {
const pk = ref.referrer_mango_account
const pk = ref.referree_mango_account
const acct = pk.slice(0, 5) + '…' + pk.slice(-5)
return (

View File

@ -109,7 +109,9 @@ export default function RiskCalculator() {
const [scenarioInitialized, setScenarioInitialized] = useState(false)
const [blankScenarioInitialized, setBlankScenarioInitialized] =
useState(false)
const [scenarioBars, setScenarioBars] = useState<ScenarioCalculator>()
const [scenarioBars, setScenarioBars] = useState<ScenarioCalculator>({
rowData: [],
})
const [accountConnected, setAccountConnected] = useState(false)
const [showZeroBalances, setShowZeroBalances] = useState(true)
const [interimValue, setInterimValue] = useState(new Map())
@ -127,6 +129,9 @@ export default function RiskCalculator() {
useEffect(() => {
async function loadUnownedMangoAccount() {
try {
if (!pubkey) {
return
}
const unownedMangoAccountPubkey = new PublicKey(pubkey)
const mangoClient = useMangoStore.getState().connection.client
if (mangoGroup) {
@ -154,7 +159,7 @@ export default function RiskCalculator() {
const handleRouteChange = () => {
if (resetOnLeave) {
setMangoStore((state) => {
state.selectedMangoAccount.current = undefined
state.selectedMangoAccount.current = null
})
}
}
@ -195,9 +200,21 @@ export default function RiskCalculator() {
// Retrieve the data to create the scenario table
const createScenario = (type) => {
const rowData = []
if (!mangoGroup || !mangoCache || !mangoAccount) {
return null
}
const rowData: any[] = []
let calculatorRowData
for (let i = -1; i < mangoGroup.numOracles; i++) {
if (!mangoAccount?.spotOpenOrdersAccounts[i]) {
return
}
const iBaseTokenTotal =
mangoAccount?.spotOpenOrdersAccounts?.[i]?.baseTokenTotal
const iBaseTokenFree =
mangoAccount?.spotOpenOrdersAccounts?.[i]?.baseTokenFree
const iQuoteTokenFree =
mangoAccount?.spotOpenOrdersAccounts?.[i]?.quoteTokenFree
// Get market configuration data
const spotMarketConfig =
i < 0
@ -254,18 +271,15 @@ export default function RiskCalculator() {
: 0
) || 0
const spotBaseTokenLocked =
mangoAccount && spotMarketConfig
? Number(
mangoAccount.spotOpenOrdersAccounts[i]?.baseTokenTotal.sub(
mangoAccount.spotOpenOrdersAccounts[i]?.baseTokenFree
)
) / Math.pow(10, spotMarketConfig.baseDecimals) || 0
mangoAccount && spotMarketConfig && iBaseTokenTotal && iBaseTokenFree
? Number(iBaseTokenTotal.sub(iBaseTokenFree)) /
Math.pow(10, spotMarketConfig.baseDecimals) || 0
: 0
const spotQuoteTokenLocked =
mangoAccount && spotMarketConfig
mangoAccount && spotMarketConfig && iQuoteTokenFree
? Number(
mangoAccount.spotOpenOrdersAccounts[i]?.quoteTokenTotal.sub(
mangoAccount.spotOpenOrdersAccounts[i]?.quoteTokenFree
iQuoteTokenFree
)
) / Math.pow(10, 6) || 0
: 0
@ -282,19 +296,20 @@ export default function RiskCalculator() {
let inOrders = 0
if (symbol === 'USDC' && ordersAsBalance) {
for (let j = 0; j < mangoGroup.tokens.length; j++) {
const jQuoteTokenTotal =
mangoAccount?.spotOpenOrdersAccounts[j]?.quoteTokenTotal
const inOrder =
j !== QUOTE_INDEX &&
mangoConfig.spotMarkets[j]?.publicKey &&
mangoAccount?.spotOpenOrdersAccounts[j]?.quoteTokenTotal
? mangoAccount.spotOpenOrdersAccounts[j].quoteTokenTotal
jQuoteTokenTotal
? jQuoteTokenTotal
: 0
inOrders += Number(inOrder) / Math.pow(10, 6)
}
} else {
inOrders =
spotMarketConfig &&
mangoAccount?.spotOpenOrdersAccounts[i]?.baseTokenTotal
? Number(mangoAccount.spotOpenOrdersAccounts[i].baseTokenTotal) /
spotMarketConfig && iBaseTokenTotal
? Number(iBaseTokenTotal) /
Math.pow(10, spotMarketConfig.baseDecimals)
: 0
}
@ -305,9 +320,12 @@ export default function RiskCalculator() {
? mangoAccount?.perpAccounts[i]
: null
const perpMarketIndex =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey && mangoAccount && symbol
? getMarketIndexBySymbol(mangoConfig, symbol)
: null
if (typeof perpMarketIndex !== 'number') {
return
}
const perpAccount =
perpMarketConfig?.publicKey && mangoAccount
? mangoAccount?.perpAccounts[perpMarketIndex]
@ -321,18 +339,21 @@ export default function RiskCalculator() {
? mangoGroup?.perpMarkets[perpMarketIndex]
: null
const basePosition =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey && mangoAccount && symbol
? Number(perpAccount?.basePosition) /
Math.pow(10, perpContractPrecision[symbol]) || 0
: 0
const unsettledFunding =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey && mangoAccount && perpMarketCache
? (Number(perpAccount?.getUnsettledFunding(perpMarketCache)) *
basePosition) /
Math.pow(10, 6) || 0
: 0
const positionPnL =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey &&
mangoAccount &&
perpMarketInfo &&
perpMarketCache
? Number(
perpAccount?.getPnl(
perpMarketInfo,
@ -342,12 +363,12 @@ export default function RiskCalculator() {
) / Math.pow(10, 6) || 0
: 0
const perpBids =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey && mangoAccount && symbol
? Number(perpPosition?.bidsQuantity) /
Math.pow(10, perpContractPrecision[symbol]) || 0
: Number(0)
const perpAsks =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey && mangoAccount && symbol
? Number(perpPosition?.asksQuantity) /
Math.pow(10, perpContractPrecision[symbol]) || 0
: Number(0)
@ -562,9 +583,12 @@ export default function RiskCalculator() {
precision:
symbol === 'USDC'
? 4
: mangoGroup.spotMarkets[i]?.spotMarket
? tokenPrecision[spotMarketConfig?.baseSymbol]
: tokenPrecision[perpMarketConfig?.baseSymbol] || 6,
: mangoGroup.spotMarkets[i]?.spotMarket &&
spotMarketConfig?.baseSymbol
? tokenPrecision[spotMarketConfig.baseSymbol]
: (perpMarketConfig?.baseSymbol &&
tokenPrecision[perpMarketConfig?.baseSymbol]) ||
6,
}
rowData.push(calculatorRowData)
@ -584,17 +608,20 @@ export default function RiskCalculator() {
// Reset column details
const resetScenarioColumn = (column) => {
if (!mangoCache || !mangoAccount || !scenarioBars) {
return
}
let resetRowData
mangoGroup
? (resetRowData = scenarioBars.rowData.map((asset) => {
let resetValue: number
let resetDeposit: number
let resetBorrow: number
let resetInOrders: number
let resetPositionSide: string
let resetPerpPositionPnL: number
let resetPerpUnsettledFunding: number
let resetPerpInOrders: number
let resetValue: number | null = null
let resetDeposit: number | null = null
let resetBorrow: number | null = null
let resetInOrders: number | null = null
let resetPositionSide: string | null = null
let resetPerpPositionPnL: number | null = null
let resetPerpUnsettledFunding: number | null = null
let resetPerpInOrders: number | null = null
switch (column) {
case 'price':
@ -683,23 +710,26 @@ export default function RiskCalculator() {
if (asset.symbolName === 'USDC' && ordersAsBalance) {
for (let j = 0; j < mangoGroup.tokens.length; j++) {
const jQuoteTokenTotal =
mangoAccount?.spotOpenOrdersAccounts?.[j]?.quoteTokenTotal
const inOrder =
j !== QUOTE_INDEX &&
mangoConfig.spotMarkets[j]?.publicKey &&
mangoAccount?.spotOpenOrdersAccounts[j]?.quoteTokenTotal
? mangoAccount.spotOpenOrdersAccounts[j].quoteTokenTotal
jQuoteTokenTotal
? jQuoteTokenTotal
: 0
resetInOrders += Number(inOrder) / Math.pow(10, 6)
}
} else {
resetInOrders =
spotMarketConfig &&
mangoAccount?.spotOpenOrdersAccounts[asset.oracleIndex]
const baseTokenTotal =
mangoAccount?.spotOpenOrdersAccounts?.[asset.oracleIndex]
?.baseTokenTotal
? Number(
mangoAccount.spotOpenOrdersAccounts[asset.oracleIndex]
.baseTokenTotal
) / Math.pow(10, spotMarketConfig.baseDecimals)
spotMarketConfig &&
typeof asset?.oracleIndex === 'number' &&
mangoAccount.spotOpenOrdersAccounts[asset.oracleIndex] &&
baseTokenTotal
? Number(baseTokenTotal) /
Math.pow(10, spotMarketConfig.baseDecimals)
: 0
}
resetValue = floorToDecimal(
@ -731,16 +761,23 @@ export default function RiskCalculator() {
perpMarketConfig?.publicKey && mangoAccount
? getMarketIndexBySymbol(mangoConfig, symbol)
: null
const hasPerpMarketIndex = typeof perpMarketIndex === 'number'
const perpAccount =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey &&
mangoAccount &&
hasPerpMarketIndex
? mangoAccount?.perpAccounts[perpMarketIndex]
: null
const perpMarketCache =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey &&
mangoAccount &&
hasPerpMarketIndex
? mangoCache?.perpMarketCache[perpMarketIndex]
: null
const perpMarketInfo =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey &&
mangoAccount &&
hasPerpMarketIndex
? mangoGroup?.perpMarkets[perpMarketIndex]
: null
const basePosition =
@ -749,7 +786,7 @@ export default function RiskCalculator() {
Math.pow(10, perpContractPrecision[symbol])
: 0
const unsettledFunding =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey && mangoAccount && perpMarketCache
? (Number(
perpAccount?.getUnsettledFunding(perpMarketCache)
) *
@ -757,7 +794,11 @@ export default function RiskCalculator() {
Math.pow(10, 6)
: 0
const positionPnL =
perpMarketConfig?.publicKey && mangoAccount
perpMarketConfig?.publicKey &&
mangoAccount &&
perpMarketInfo &&
perpMarketCache &&
hasPerpMarketIndex
? Number(
perpAccount?.getPnl(
perpMarketInfo,
@ -819,7 +860,7 @@ export default function RiskCalculator() {
const updateValue = (symbol, field, val) => {
const updateValue = Number(val)
if (!Number.isNaN(val)) {
const updatedRowData = scenarioBars.rowData.map((asset) => {
const updatedRowData: any[] = scenarioBars?.rowData?.map((asset) => {
if (asset.symbolName.toLowerCase() === symbol.toLowerCase()) {
switch (field) {
case 'spotNet':
@ -855,14 +896,16 @@ export default function RiskCalculator() {
}
})
if (updatedRowData) {
const calcData = updateCalculator(updatedRowData)
setScenarioBars(calcData)
}
}
}
// Anchor current displayed prices to zero
const anchorPricing = () => {
const updatedRowData = scenarioBars.rowData.map((asset) => {
const updatedRowData = scenarioBars?.rowData.map((asset) => {
return {
...asset,
['price']:
@ -870,9 +913,11 @@ export default function RiskCalculator() {
}
})
if (updatedRowData) {
const calcData = updateCalculator(updatedRowData)
setScenarioBars(calcData)
}
}
// Handle slider usage
const onChangeSlider = async (percentage) => {
@ -1013,7 +1058,7 @@ export default function RiskCalculator() {
let perpsAssets = 0
let perpsLiabilities = 0
scenarioBars.rowData.map((asset) => {
scenarioBars?.rowData.map((asset) => {
// SPOT
// Calculate spot quote
if (asset.symbolName === 'USDC' && Number(asset.spotNet) > 0) {
@ -1299,7 +1344,7 @@ export default function RiskCalculator() {
<h1 className={`mb-2`}>{t('calculator:risk-calculator')}</h1>
<p className="mb-0">{t('calculator:in-testing-warning')}</p>
</div>
{scenarioBars?.rowData.length > 0 ? (
{scenarioBars?.rowData?.length && scenarioBars.rowData.length > 0 ? (
<div className="rounded-lg bg-th-bkg-2">
<div className="grid grid-cols-12">
<div className="col-span-12 p-4 md:col-span-8">

View File

@ -1,7 +1,6 @@
import { useTranslation } from 'next-i18next'
import { useEffect, useState } from 'react'
import { ChevronRightIcon } from '@heroicons/react/solid'
import useMangoGroupConfig from '../hooks/useMangoGroupConfig'
import useMangoStore from '../stores/useMangoStore'
import Link from 'next/link'
import { formatUsdValue } from '../utils'
@ -20,14 +19,17 @@ export async function getStaticProps({ locale }) {
const SelectMarket = () => {
const { t } = useTranslation('common')
const groupConfig = useMangoGroupConfig()
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const [markets, setMarkets] = useState([])
const [markets, setMarkets] = useState<any[]>([])
useEffect(() => {
const markets = []
const allMarkets = [...groupConfig.spotMarkets, ...groupConfig.perpMarkets]
const markets: any[] = []
const allMarkets =
groupConfig?.spotMarkets && groupConfig?.perpMarkets
? [...groupConfig.spotMarkets, ...groupConfig.perpMarkets]
: []
allMarkets.forEach((market) => {
const base = market.name.slice(0, -5)
const found = markets.find((b) => b.baseAsset === base)
@ -40,6 +42,10 @@ const SelectMarket = () => {
setMarkets(markets)
}, [])
if (!mangoCache) {
return null
}
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />
@ -47,7 +53,8 @@ const SelectMarket = () => {
<div className="py-4 text-2xl font-bold text-th-fgd-1">
{t('markets')}
</div>
{markets.map((mkt) => (
{markets.map((mkt) => {
return (
<div key={mkt.baseAsset}>
<div className="flex items-center justify-between bg-th-bkg-3 px-2.5 py-2">
<div className="flex items-center">
@ -86,7 +93,8 @@ const SelectMarket = () => {
: null}
</div>
</div>
))}
)
})}
{/* spacer so last market can be selected albeit bottom bar overlay */}
<p className="flex h-12 md:hidden"></p>
</PageBodyContainer>

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState } from 'react'
import TopBar from '../components/TopBar'
import PageBodyContainer from '../components/PageBodyContainer'
import StatsTotals from '../components/stats_page/StatsTotals'
@ -45,14 +45,6 @@ export default function StatsPage() {
setActiveTab(tabName)
}
useEffect(() => {
// @ts-ignore
if (window.solana) {
// @ts-ignore
window.solana.connect({ onlyIfTrusted: true })
}
}, [])
return (
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />

View File

@ -29,18 +29,18 @@ export default function Swap() {
if (connected) {
actions.fetchWalletTokens()
}
}, [connected])
}, [connected, actions])
if (!connection) return null
const userPublicKey =
publicKey && !zeroKey.equals(publicKey) ? publicKey : null
publicKey && !zeroKey.equals(publicKey) ? publicKey : undefined
return (
<JupiterProvider
connection={connection}
cluster="mainnet-beta"
userPublicKey={connected ? userPublicKey : null}
userPublicKey={connected ? userPublicKey : undefined}
>
<div className={`bg-th-bkg-1 text-th-fgd-1 transition-all`}>
<TopBar />

View File

@ -1,5 +1,3 @@
{
"about-to-withdraw": "You're about to withdraw",
"above": "Above",
@ -101,7 +99,7 @@
"data-refresh-tip-title": "Manual Data Refresh",
"date": "Date",
"default-market": "Default Market",
"default-spot-margin": "Trade with margin by default",
"default-spot-margin": "Spot margin enabled by default",
"degraded-performance": "Solana network is experiencing degraded performance. Transactions may fail to send or confirm. (TPS: {{tps}})",
"delay-displaying-recent": "There may be a delay in displaying the latest activity.",
"delayed-info": "Data updates hourly",
@ -142,17 +140,18 @@
"favorite": "Favorite",
"favorites": "Favorites",
"fee": "Fee",
"fees": "Fees",
"fee-discount": "Fee Discount",
"fees": "Fees",
"filter": "Filter",
"filters-selected": "{{selectedFilters}} selected",
"filter-trade-history": "Filter Trade History",
"filters-selected": "{{selectedFilters}} selected",
"first-deposit-desc": "There is a one-time cost of 0.035 SOL when you make your first deposit. This covers the rent on the Solana Blockchain for your account.",
"from": "From",
"funding": "Funding",
"funding-chart-title": "Funding Last 30 days (current bar is delayed)",
"futures": "Futures",
"get-started": "Get Started",
"governance": "Governance",
"health": "Health",
"health-check": "Account Health Check",
"health-ratio": "Health Ratio",
@ -185,7 +184,9 @@
"intro-feature-2": "All assets count as collateral to trade or borrow",
"intro-feature-3": "Deposit any asset and earn interest automatically",
"intro-feature-4": "Borrow against your assets for other DeFi activities",
"invalid-address": "The address is invalid",
"ioc": "IOC",
"language": "Language",
"languages-tip-desc": "Choose another language here. More coming soon...",
"languages-tip-title": "Multilingual?",
"layout-tip-desc": "Unlock to re-arrange and re-size the trading panels to your liking.",
@ -212,6 +213,8 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"mango": "Mango",
"mango-account-lookup-desc": "Enter a Mango account address to show account details",
"mango-account-lookup-title": "View a Mango Account",
"mango-accounts": "Mango Accounts",
"margin": "Margin",
"margin-available": "Margin Available",
@ -267,6 +270,14 @@
"not-enough-balance": "Insufficient wallet balance",
"not-enough-sol": "You may not have enough SOL for this transaction",
"notional-size": "Notional Size",
"number-deposit": "{{number}} Deposit",
"number-deposits": "{{number}} Deposits",
"number-liquidation": "{{number}} Liquidation",
"number-liquidations": "{{number}} Liquidations",
"number-trade": "{{number}} Trade",
"number-trades": "{{number}} Trades",
"number-withdrawal": "{{number}} Withdrawal",
"number-withdrawals": "{{number}} Withdrawals",
"open-interest": "Open Interest",
"open-orders": "Open Orders",
"optional": "(Optional)",
@ -367,6 +378,7 @@
"taker": "Taker",
"taker-fee": "Taker Fee",
"target-period-length": "Target Period Length",
"theme": "Theme",
"themes-tip-desc": "Mango, Dark or Light (if you're that way inclined).",
"themes-tip-title": "Color Themes",
"time": "Time",
@ -388,7 +400,8 @@
"tooltip-ioc": "Immediate or cancel orders are guaranteed to be the taker or it will be canceled.",
"tooltip-lock-layout": "Lock Layout",
"tooltip-name-onchain": "Account names are stored on-chain",
"tooltip-post": "Post only orders are guaranteed to be the maker order or else it will be canceled.",
"tooltip-post": "Post orders are guaranteed to be the maker order or else it will be canceled.",
"tooltip-post-and-slide": "Post and slide is a limit order that will set your price one tick less than the opposite side of the book.",
"tooltip-projected-leverage": "Projected Leverage",
"tooltip-reduce": "Reduce only orders will only reduce your overall position.",
"tooltip-reset-layout": "Reset Layout",
@ -413,6 +426,7 @@
"trade-history": "Trade History",
"trades": "Trades",
"trades-history": "Trade History",
"transaction-failed": "Transaction failed",
"transaction-sent": "Transaction sent",
"trigger-price": "Trigger Price",
"try-again": "Try again",
@ -431,6 +445,7 @@
"v3-unaudited": "The V3 protocol is in public beta. This is unaudited software, use it at your own risk.",
"v3-welcome": "Welcome to Mango",
"value": "Value",
"view": "View",
"view-all-trades": "View all trades in the Account page",
"view-counterparty": "View Counterparty",
"view-transaction": "View Transaction",

Some files were not shown because too many files have changed in this diff Show More