Merge branch 'main' into stop-orders

This commit is contained in:
saml33 2023-08-02 09:39:55 +10:00
commit 02301bedcc
17 changed files with 162 additions and 37 deletions

View File

@ -7,6 +7,7 @@ export type NotificationSettings = {
export const fetchNotificationSettings = async (
wallet: string,
token: string,
mangoAccount: string,
) => {
const data = await fetch(
`${NOTIFICATION_API}notifications/user/getSettings`,
@ -14,6 +15,7 @@ export const fetchNotificationSettings = async (
headers: {
authorization: token,
publickey: wallet,
'mango-account': mangoAccount,
},
},
)

View File

@ -8,11 +8,16 @@ export type Notification = {
id: number
}
export const fetchNotifications = async (wallet: string, token: string) => {
export const fetchNotifications = async (
wallet: string,
token: string,
mangoAccount: string,
) => {
const data = await fetch(`${NOTIFICATION_API}notifications`, {
headers: {
authorization: token,
publickey: wallet,
'mango-account': mangoAccount,
},
})
const body = await data.json()

View File

@ -3,14 +3,16 @@ import { NOTIFICATION_API_WEBSOCKET } from 'utils/constants'
export class NotificationsWebSocket {
ws: WebSocket | null = null
token: string
mangoAccount: string
publicKey: string
pingInterval: NodeJS.Timer | null
retryCount = 0
maxRetries = 2
constructor(token: string, publicKey: string) {
constructor(token: string, publicKey: string, mangoAccount: string) {
this.token = token
this.publicKey = publicKey
this.mangoAccount = mangoAccount
this.pingInterval = null
}
@ -18,6 +20,7 @@ export class NotificationsWebSocket {
const wsUrl = new URL(NOTIFICATION_API_WEBSOCKET)
wsUrl.searchParams.append('authorization', this.token)
wsUrl.searchParams.append('publickey', this.publicKey)
wsUrl.searchParams.append('mangoAccount', this.mangoAccount)
this.ws = new WebSocket(wsUrl)
this.ws.addEventListener('open', () => {

View File

@ -79,6 +79,7 @@ const TokenList = () => {
for (const b of banks) {
const bank = b.bank
const balance = b.balance
const balanceValue = balance * bank.uiPrice
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const hasInterestEarned = totalInterestData.find(
@ -113,6 +114,7 @@ const TokenList = () => {
const data = {
balance,
balanceValue,
bank,
symbol,
interestAmount,
@ -178,8 +180,8 @@ const TokenList = () => {
<div className="flex justify-end">
<Tooltip content="A negative balance represents a borrow">
<SortableColumnHeader
sortKey="balance"
sort={() => requestSort('balance')}
sortKey="balanceValue"
sort={() => requestSort('balanceValue')}
sortConfig={sortConfig}
title={t('balance')}
titleClass="tooltip-underline"

View File

@ -36,7 +36,7 @@ const set = mangoStore.getState().set
const TopBar = () => {
const { t } = useTranslation('common')
const { mangoAccount } = useMangoAccount()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { connected } = useWallet()
const [action, setAction] = useState<'deposit' | 'withdraw'>('deposit')
@ -200,7 +200,7 @@ const TopBar = () => {
)}
{connected ? (
<div className="flex items-center">
<NotificationsButton />
{mangoAccountAddress && <NotificationsButton />}
<AccountsButton />
<ConnectedMenu />
</div>

View File

@ -292,6 +292,8 @@ const CreateSwitchboardOracleModal = ({
onClose,
orcaPoolAddress,
raydiumPoolAddress,
tier,
tierToSwapValue,
wallet,
])

View File

@ -65,6 +65,7 @@ const BalancesTable = () => {
for (const b of filteredBanks) {
const bank = b.bank
const balance = b.balance
const balanceValue = balance * bank.uiPrice
const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name
const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0
@ -80,6 +81,7 @@ const BalancesTable = () => {
const data = {
assetWeight,
balance,
balanceValue,
bankWithBalance: b,
collateralValue,
inOrders,
@ -114,8 +116,8 @@ const BalancesTable = () => {
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="balance"
sort={() => requestSort('balance')}
sortKey="balanceValue"
sort={() => requestSort('balanceValue')}
sortConfig={sortConfig}
title={t('balance')}
/>

View File

@ -43,7 +43,9 @@ const TokenOverviewTable = () => {
for (const b of banks) {
const bank: Bank = b.bank
const deposits = bank.uiDeposits()
const depositsValue = deposits * bank.uiPrice
const borrows = bank.uiBorrows()
const borrowsValue = borrows * bank.uiPrice
const availableVaultBalance = group
? group.getTokenVaultBalanceByMintUi(bank.mint) -
deposits * bank.minVaultToDepositsRatio
@ -52,10 +54,12 @@ const TokenOverviewTable = () => {
0,
availableVaultBalance.toFixed(bank.mintDecimals),
)
const availableValue = available.toNumber() * bank.uiPrice
const feesEarned = toUiDecimals(
bank.collectedFeesNative,
bank.mintDecimals,
)
const feeValue = feesEarned * bank.uiPrice
const utilization =
bank.uiDeposits() > 0 ? (bank.uiBorrows() / bank.uiDeposits()) * 100 : 0
@ -65,12 +69,16 @@ const TokenOverviewTable = () => {
const data = {
available,
availableValue,
bank,
borrows,
borrowRate,
deposits,
borrows,
borrowsValue,
depositRate,
deposits,
depositsValue,
feesEarned,
feeValue,
symbol,
utilization,
}
@ -103,8 +111,8 @@ const TokenOverviewTable = () => {
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="deposits"
sort={() => requestSort('deposits')}
sortKey="depositsValue"
sort={() => requestSort('depositsValue')}
sortConfig={sortConfig}
title={t('total-deposits')}
/>
@ -113,8 +121,8 @@ const TokenOverviewTable = () => {
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="borrows"
sort={() => requestSort('borrows')}
sortKey="borrowsValue"
sort={() => requestSort('borrowsValue')}
sortConfig={sortConfig}
title={t('total-borrows')}
/>
@ -124,8 +132,8 @@ const TokenOverviewTable = () => {
<div className="flex justify-end">
<Tooltip content="The amount available to borrow">
<SortableColumnHeader
sortKey="available"
sort={() => requestSort('available')}
sortKey="availableValue"
sort={() => requestSort('availableValue')}
sortConfig={sortConfig}
title={t('available')}
titleClass="tooltip-underline"
@ -137,8 +145,8 @@ const TokenOverviewTable = () => {
<div className="flex justify-end">
<Tooltip content={t('token:fees-tooltip')}>
<SortableColumnHeader
sortKey="feesEarned"
sort={() => requestSort('feesEarned')}
sortKey="feeValue"
sort={() => requestSort('feeValue')}
sortConfig={sortConfig}
title={t('fees')}
titleClass="tooltip-underline"

View File

@ -1,11 +1,11 @@
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
import { Popover } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { ChevronDownIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import useMangoGroup from 'hooks/useMangoGroup'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { useMemo, useState } from 'react'
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'
import {
floorToDecimal,
formatCurrencyValue,
@ -21,8 +21,11 @@ import Loading from '@components/shared/Loading'
import MarketChange from '@components/shared/MarketChange'
import SheenLoader from '@components/shared/SheenLoader'
// import Select from '@components/forms/Select'
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
import useListedMarketsWithMarketData, {
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
import { AllowedKeys, sortPerpMarkets, sortSpotMarkets } from 'utils/markets'
import Input from '@components/forms/Input'
const MARKET_LINK_CLASSES =
'grid grid-cols-3 md:grid-cols-4 flex items-center w-full py-2 px-4 rounded-r-md focus:outline-none focus-visible:text-th-active md:hover:cursor-pointer md:hover:bg-th-bkg-3 md:hover:text-th-fgd-1'
@ -37,6 +40,38 @@ const MARKET_LINK_DISABLED_CLASSES =
// 'change_1h',
// ]
const generateSearchTerm = (
item: SerumMarketWithMarketData,
searchValue: string,
) => {
const normalizedSearchValue = searchValue.toLowerCase()
const value = item.name.toLowerCase()
const isMatchingWithName =
item.name.toLowerCase().indexOf(normalizedSearchValue) >= 0
const matchingSymbolPercent = isMatchingWithName
? normalizedSearchValue.length / item.name.length
: 0
return {
token: item,
matchingIdx: value.indexOf(normalizedSearchValue),
matchingSymbolPercent,
}
}
const startSearch = (
items: SerumMarketWithMarketData[],
searchValue: string,
) => {
return items
.map((item) => generateSearchTerm(item, searchValue))
.filter((item) => item.matchingIdx >= 0)
.sort((i1, i2) => i1.matchingIdx - i2.matchingIdx)
.sort((i1, i2) => i2.matchingSymbolPercent - i1.matchingSymbolPercent)
.map((item) => item.token)
}
const MarketSelectDropdown = () => {
const { t } = useTranslation('common')
const { selectedMarket } = useSelectedMarket()
@ -44,10 +79,13 @@ const MarketSelectDropdown = () => {
selectedMarket instanceof PerpMarket ? 'perp' : 'spot',
)
const [sortByKey] = useState<AllowedKeys>('quote_volume_24h')
const [search, setSearch] = useState('')
const [isOpen, setIsOpen] = useState(false)
const { group } = useMangoGroup()
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const focusRef = useRef<HTMLInputElement>(null)
const perpMarketsToShow = useMemo(() => {
if (!perpMarketsWithData.length) return []
@ -70,17 +108,30 @@ const MarketSelectDropdown = () => {
const serumMarketsToShow = useMemo(() => {
if (!serumMarketsWithData.length) return []
if (spotBaseFilter !== 'All') {
const filteredMarkets = serumMarketsWithData.filter((m) => {
const base = m.name.split('/')[1]
return base === spotBaseFilter
})
return sortSpotMarkets(filteredMarkets, sortByKey)
return search
? startSearch(filteredMarkets, search)
: sortSpotMarkets(filteredMarkets, sortByKey)
} else {
return sortSpotMarkets(serumMarketsWithData, sortByKey)
return search
? startSearch(serumMarketsWithData, search)
: sortSpotMarkets(serumMarketsWithData, sortByKey)
}
}, [serumMarketsWithData, sortByKey, spotBaseFilter])
}, [search, serumMarketsWithData, sortByKey, spotBaseFilter])
const handleUpdateSearch = (e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
useEffect(() => {
if (focusRef?.current && spotOrPerp === 'spot') {
focusRef.current.focus()
}
}, [focusRef, isOpen, spotOrPerp])
const loadingMarketData = isLoading || isFetching
@ -94,6 +145,7 @@ const MarketSelectDropdown = () => {
<Popover.Button
className="-ml-4 flex h-12 items-center justify-between px-4 focus-visible:bg-th-bkg-3 disabled:cursor-not-allowed disabled:opacity-60 md:hover:bg-th-bkg-2 disabled:md:hover:bg-th-bkg-1"
disabled={!group}
onClick={() => setIsOpen(!isOpen)}
>
<div className="flex items-center">
{selectedMarket ? (
@ -157,6 +209,7 @@ const MarketSelectDropdown = () => {
}}
onClick={() => {
close()
setSearch('')
}}
shallow={true}
>
@ -218,6 +271,16 @@ const MarketSelectDropdown = () => {
{spotOrPerp === 'spot' && serumMarketsToShow.length ? (
<>
<div className="flex items-center justify-between mb-3 px-4">
<div className="relative w-1/2">
<Input
className="pl-8 h-8"
type="text"
value={search}
onChange={handleUpdateSearch}
ref={focusRef}
/>
<MagnifyingGlassIcon className="absolute left-2 top-2 h-4 w-4" />
</div>
<div>
{spotBaseTokens.map((tab) => (
<button
@ -294,6 +357,7 @@ const MarketSelectDropdown = () => {
}}
onClick={() => {
close()
setSearch('')
}}
shallow={true}
>

View File

@ -1,13 +1,16 @@
import { useWallet } from '@solana/wallet-adapter-react'
import NotificationCookieStore from '@store/notificationCookieStore'
import useMangoAccount from 'hooks/useMangoAccount'
export function useHeaders() {
const { publicKey } = useWallet()
const { mangoAccountAddress } = useMangoAccount()
const token = NotificationCookieStore((s) => s.currentToken)
return {
headers: {
authorization: token,
'mango-account': mangoAccountAddress,
publickey: publicKey?.toBase58() || '',
'Content-Type': 'application/json',
},

View File

@ -1,14 +1,17 @@
import { useWallet } from '@solana/wallet-adapter-react'
import { useNotifications } from './useNotifications'
import NotificationCookieStore from '@store/notificationCookieStore'
import useMangoAccount from 'hooks/useMangoAccount'
export function useIsAuthorized() {
const { publicKey, connected } = useWallet()
const { mangoAccountAddress } = useMangoAccount()
const { error, isFetched, isLoading } = useNotifications()
const token = NotificationCookieStore((s) => s.currentToken)
const isAuthorized =
publicKey?.toBase58() &&
mangoAccountAddress &&
token &&
!error &&
isFetched &&

View File

@ -4,20 +4,22 @@ import { useWallet } from '@solana/wallet-adapter-react'
import { fetchNotificationSettings } from 'apis/notifications/notificationSettings'
import { useIsAuthorized } from './useIsAuthorized'
import { DAILY_MILLISECONDS } from 'utils/constants'
import useMangoAccount from 'hooks/useMangoAccount'
export function useNotificationSettings() {
const { publicKey } = useWallet()
const { mangoAccountAddress } = useMangoAccount()
const walletPubKey = publicKey?.toBase58()
const token = NotificationCookieStore((s) => s.currentToken)
const isAuth = useIsAuthorized()
const criteria = walletPubKey && token && isAuth
const criteria = [token, isAuth, mangoAccountAddress]
return useQuery(
['notificationSettings', criteria],
() => fetchNotificationSettings(walletPubKey!, token!),
['notificationSettings', ...criteria],
() => fetchNotificationSettings(walletPubKey!, token!, mangoAccountAddress),
{
enabled: !!isAuth,
enabled: !!isAuth && !!mangoAccountAddress,
retry: 1,
staleTime: DAILY_MILLISECONDS,
},

View File

@ -7,14 +7,16 @@ import { useQueryClient } from '@tanstack/react-query'
import { Notification } from 'apis/notifications/notifications'
import { tryParse } from 'utils/formatting'
import { NotificationsWebSocket } from 'apis/notifications/websocket'
import useMangoAccount from 'hooks/useMangoAccount'
export function useNotificationSocket() {
const isAuth = useIsAuthorized()
const { publicKey } = useWallet()
const { mangoAccountAddress } = useMangoAccount()
const token = NotificationCookieStore((s) => s.currentToken)
const queryClient = useQueryClient()
const criteria = publicKey?.toBase58() && token
const criteria = [token, mangoAccountAddress]
const [socket, setSocket] = useState<WebSocket | null>(null)
@ -24,10 +26,11 @@ export function useNotificationSocket() {
}
let ws: WebSocket | null = null
if (isAuth && publicKey && token) {
if (isAuth && publicKey && token && mangoAccountAddress) {
const notificationWs = new NotificationsWebSocket(
token,
publicKey.toBase58(),
mangoAccountAddress,
).connect()
ws = notificationWs.ws!
@ -43,7 +46,7 @@ export function useNotificationSocket() {
})
//we push new data to our notifications data
queryClient.setQueryData<Notification[]>(
['notifications', criteria],
['notifications', ...criteria],
(prevData) => {
if (!prevData) {
return []
@ -68,5 +71,5 @@ export function useNotificationSocket() {
socket?.close(1000, 'hook')
}
}
}, [isAuth, token])
}, [isAuth, token, mangoAccountAddress])
}

View File

@ -2,21 +2,24 @@ import { useQuery } from '@tanstack/react-query'
import { fetchNotifications } from 'apis/notifications/notifications'
import NotificationCookieStore from '@store/notificationCookieStore'
import { useWallet } from '@solana/wallet-adapter-react'
import useMangoAccount from 'hooks/useMangoAccount'
//10min
const refetchMs = 600000
export function useNotifications() {
const { publicKey } = useWallet()
const { mangoAccountAddress } = useMangoAccount()
const walletPubKey = publicKey?.toBase58()
const token = NotificationCookieStore((s) => s.currentToken)
const criteria = walletPubKey && token
const criteria = [token, mangoAccountAddress]
return useQuery(
['notifications', criteria],
() => fetchNotifications(walletPubKey!, token!),
['notifications', ...criteria],
() => fetchNotifications(walletPubKey!, token!, mangoAccountAddress),
{
enabled: !!(walletPubKey && token),
enabled: !!(walletPubKey && token && mangoAccountAddress),
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,

View File

@ -10,6 +10,7 @@ export default function useMangoAccount(): {
mangoAccountAddress: string
} {
const mangoAccount = mangoStore((s) => s.mangoAccount.current)
const initialLoad = mangoStore((s) => s.mangoAccount.initialLoad)
const mangoAccountPk = useMemo(() => {

21
public/icons/kin.svg Normal file
View File

@ -0,0 +1,21 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_17_15)">
<circle cx="16" cy="16" r="16" fill="#7546F6"/>
<path d="M7 11.1652L16 6L25 11.1652V21.4957L16 26.6609L7 21.4957V11.1652Z" fill="white"/>
<path d="M25 11.1652L16 6V16.3304L25 11.1652Z" fill="url(#paint0_linear_17_15)"/>
<path d="M25 21.4957L16 16.3304V26.6609L25 21.4957Z" fill="url(#paint1_linear_17_15)"/>
</g>
<defs>
<linearGradient id="paint0_linear_17_15" x1="20.5391" y1="8.73913" x2="16" y2="16.3304" gradientUnits="userSpaceOnUse">
<stop stop-color="#9467FF"/>
<stop offset="1" stop-color="#F5F5F5"/>
</linearGradient>
<linearGradient id="paint1_linear_17_15" x1="16" y1="17.5826" x2="22.4957" y2="23.2174" gradientUnits="userSpaceOnUse">
<stop stop-color="#8D5DED"/>
<stop offset="1" stop-color="#DBCAF7"/>
</linearGradient>
<clipPath id="clip0_17_15">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -127,6 +127,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
'eth (portal)': true,
hnt: true,
jitosol: true,
kin: true,
ldo: true,
mngo: true,
msol: true,