merge main

This commit is contained in:
saml33 2023-08-08 10:09:05 +10:00
commit 751afaae79
59 changed files with 1449 additions and 315 deletions

View File

@ -14,7 +14,7 @@ socket.addEventListener('open', (_event) => {
// Listen for messages
socket.addEventListener('message', (msg) => {
const data = JSON.parse(msg.data)
if (data.type !== 'PRICE_DATA') return console.warn(data)
if (data.type !== 'BASE_QUOTE_PRICE_DATA') return console.warn(data)
const currTime = data.data.unixTime * 1000
const lastBar = subscriptionItem.lastBar
@ -60,11 +60,11 @@ export function subscribeOnStream(
}
const msg = {
type: 'SUBSCRIBE_PRICE',
type: 'SUBSCRIBE_BASE_QUOTE_PRICE',
data: {
chartType: parseResolution(resolution),
address: symbolInfo.address,
currency: symbolInfo.type || 'usd',
baseAddress: symbolInfo.base_token,
quoteAddress: symbolInfo.quote_token,
},
}
if (!isOpen(socket)) {
@ -80,7 +80,7 @@ export function subscribeOnStream(
export function unsubscribeFromStream() {
const msg = {
type: 'UNSUBSCRIBE_PRICE',
type: 'UNSUBSCRIBE_BASE_QUOTE_PRICE',
}
if (!isOpen(socket)) {

View File

@ -24,7 +24,11 @@ import {
SearchSymbolResultItem,
} from '@public/charting_library'
import { PublicKey } from '@solana/web3.js'
import { Group } from '@blockworks-foundation/mango-v4'
import {
Group,
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
export const SUPPORTED_RESOLUTIONS = [
'1',
@ -58,6 +62,8 @@ type Bar = KlineBar & TradingViewBar
export type SymbolInfo = LibrarySymbolInfo & {
address: string
quote_token: string
base_token: string
}
const lastBarsCache = new Map()
@ -70,17 +76,17 @@ const configurationData = {
exchanges: [],
}
const getTickerFromMktAddress = (
const getMktFromMktAddress = (
group: Group,
symbolAddress: string,
): string | null => {
): Serum3Market | PerpMarket | null => {
try {
const serumMkt = group.getSerum3MarketByExternalMarket(
new PublicKey(symbolAddress),
)
if (serumMkt) {
return serumMkt.name
return serumMkt
}
} catch {
console.log('Address is not a serum market')
@ -92,7 +98,7 @@ const getTickerFromMktAddress = (
)
if (perpMkt) {
return perpMkt.name
return perpMkt
}
return null
@ -156,14 +162,18 @@ export const queryBirdeyeBars = async (
from: number
to: number
},
quote_token: string,
): Promise<Bar[]> => {
const { from, to } = periodParams
const urlParameters = {
address: tokenAddress,
base_address: tokenAddress,
quote_address: quote_token,
type: parseResolution(resolution),
time_from: from,
time_to: to,
}
const query = Object.keys(urlParameters)
.map(
(name: string) =>
@ -171,7 +181,7 @@ export const queryBirdeyeBars = async (
)
.join('&')
const data = await makeApiRequest(`defi/ohlcv/pair?${query}`)
const data = await makeApiRequest(`defi/ohlcv/base_quote?${query}`)
if (!data.success || data.data.items.length === 0) {
return []
}
@ -237,15 +247,34 @@ export default {
const mangoStoreState = mangoStore.getState()
const group = mangoStoreState.group
let ticker = mangoStoreState.selectedMarket.name
let quote_token = ''
let base_token = ''
console.log(1)
if (group && symbolAddress) {
const newTicker = getTickerFromMktAddress(group, symbolAddress)
if (newTicker) {
ticker = newTicker
console.log(2)
const market = getMktFromMktAddress(group, symbolAddress)
if (market) {
console.log(3)
ticker = market.name
if (market instanceof Serum3Market) {
console.log(4)
base_token = group
.getFirstBankByTokenIndex(market.baseTokenIndex)
.mint.toString()
quote_token = group
.getFirstBankByTokenIndex(market.quoteTokenIndex)
.mint.toString()
}
}
}
const symbolInfo: SymbolInfo = {
quote_token,
base_token,
address: symbolItem.address,
ticker: symbolItem.address,
name: ticker || symbolItem.address,
@ -287,25 +316,24 @@ export default {
) => void,
onErrorCallback: (e: any) => void,
) => {
console.log('symboleInfo', symbolInfo)
try {
const { firstDataRequest } = periodParams
let bars
if (
symbolInfo.description?.includes('PERP') &&
symbolInfo.address !== '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
) {
if (symbolInfo.description?.includes('PERP') && symbolInfo.address) {
marketType = 'perp'
bars = await queryPerpBars(
symbolInfo.address,
resolution as any,
periodParams,
)
} else {
} else if (symbolInfo.address) {
marketType = 'spot'
bars = await queryBirdeyeBars(
symbolInfo.address,
symbolInfo.base_token,
resolution as any,
periodParams,
symbolInfo.quote_token,
)
}
if (!bars || bars.length === 0) {

View File

@ -46,6 +46,8 @@ type Bar = KlineBar & TradingViewBar
type SymbolInfo = LibrarySymbolInfo & {
address: string
base_token: string
quote_token: string
}
const lastBarsCache = new Map()
@ -150,6 +152,8 @@ export default {
const ticker = mangoStore.getState().selectedMarket.name
const symbolInfo: SymbolInfo = {
base_token: '',
quote_token: '',
address: symbolItem.address,
ticker: symbolItem.address,
name: symbolItem.symbol || symbolItem.address,

View File

@ -9,7 +9,8 @@ import {
} from 'react'
import { ArrowPathIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from '../utils/theme'
import { breakpoints, nftThemeMeta } from '../utils/theme'
import mangoStore from '@store/mangoStore'
import BottomBar from './mobile/BottomBar'
import TopBar from './TopBar'
import useLocalStorageState from '../hooks/useLocalStorageState'
@ -26,7 +27,7 @@ import useInterval from './shared/useInterval'
import { Transition } from '@headlessui/react'
import { useTranslation } from 'next-i18next'
import TermsOfUseModal from './modals/TermsOfUseModal'
import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts'
import { useTheme } from 'next-themes'
import PromoBanner from './rewards/PromoBanner'
import { useRouter } from 'next/router'
@ -34,6 +35,8 @@ export const sideBarAnimationDuration = 300
const termsLastUpdated = 1679441610978
const Layout = ({ children }: { children: ReactNode }) => {
const themeData = mangoStore((s) => s.themeData)
const { theme } = useTheme()
const [isCollapsed, setIsCollapsed] = useLocalStorageState(
SIDEBAR_COLLAPSE_KEY,
false,
@ -73,14 +76,33 @@ const Layout = ({ children }: { children: ReactNode }) => {
particlesInit()
}, [])
useEffect(() => {
const set = mangoStore.getState().set
if (theme && nftThemeMeta[theme]) {
set((s) => {
s.themeData = nftThemeMeta[theme]
})
} else {
set((s) => {
s.themeData = nftThemeMeta.default
})
}
}, [theme])
return (
<main
className={`${ttCommons.variable} ${ttCommonsExpanded.variable} ${ttCommonsMono.variable} font-sans`}
className={`${themeData.fonts.body.variable} ${themeData.fonts.display.variable} ${themeData.fonts.mono.variable} font-sans`}
>
<div className="fixed z-30">
<SuccessParticles />
</div>
<div className="flex-grow bg-th-bkg-1 text-th-fgd-2 transition-all">
<div
className={`min-h-screen flex-grow ${
!themeData.useGradientBg
? 'bg-th-bkg-1'
: 'bg-gradient-to-b from-th-bkg-1 to-th-bkg-2'
} text-th-fgd-2 transition-all`}
>
<div className="fixed bottom-0 left-0 z-20 w-full md:hidden">
<BottomBar />
</div>
@ -107,7 +129,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
{/* note: overflow-x-hidden below prevents position sticky from working in activity feed */}
<div
className={`w-full overflow-x-hidden transition-all duration-${sideBarAnimationDuration} ease-in-out ${
isCollapsed ? 'md:pl-[64px]' : 'md:pl-44 lg:pl-48 xl:pl-52'
isCollapsed ? 'md:pl-[64px]' : 'pl-[200px]'
}`}
>
<TopBar />

View File

@ -20,7 +20,7 @@ import {
} from '@heroicons/react/20/solid'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { Fragment, ReactNode } from 'react'
import { Fragment, ReactNode, useEffect, useMemo } from 'react'
import { Disclosure, Popover, Transition } from '@headlessui/react'
import MangoAccountSummary from './account/MangoAccountSummary'
import Tooltip from './shared/Tooltip'
@ -32,34 +32,94 @@ import useMangoAccount from 'hooks/useMangoAccount'
import { useTheme } from 'next-themes'
import LeaderboardIcon from './icons/LeaderboardIcon'
import { sideBarAnimationDuration } from './Layout'
import { CUSTOM_SKINS } from 'utils/theme'
import { NFT } from 'types'
const SideNav = ({ collapsed }: { collapsed: boolean }) => {
const { t } = useTranslation(['common', 'search'])
const { connected } = useWallet()
const { connected, publicKey } = useWallet()
const { theme } = useTheme()
const group = mangoStore.getState().group
const themeData = mangoStore((s) => s.themeData)
const nfts = mangoStore((s) => s.wallet.nfts.data)
const { mangoAccount } = useMangoAccount()
const router = useRouter()
const { pathname } = router
const playAnimation = () => {
const set = mangoStore.getState().set
set((s) => {
s.successAnimation.theme = true
})
}
// fetch nfts when pk changes
useEffect(() => {
if (publicKey) {
const actions = mangoStore.getState().actions
const connection = mangoStore.getState().connection
actions.fetchNfts(connection, publicKey)
}
}, [publicKey])
// find all mango skin nfts
const mangoNfts = useMemo(() => {
if (!nfts.length) return []
const mangoNfts: NFT[] = []
for (const nft of nfts) {
const collectionAddress = nft?.collectionAddress
for (const themeKey in CUSTOM_SKINS) {
if (CUSTOM_SKINS[themeKey] === collectionAddress) {
mangoNfts.push(nft)
}
}
}
return mangoNfts
}, [nfts])
// find sidebar image url from skin nft for theme
const sidebarImageUrl = useMemo(() => {
if (!theme) return themeData.sideImagePath
const collectionAddress = CUSTOM_SKINS[theme.toLowerCase()]
if (collectionAddress && mangoNfts.length) {
const sidebarImageUrl =
mangoNfts.find((nft) => nft.collectionAddress === collectionAddress)
?.image || themeData.sideImagePath
return sidebarImageUrl
}
return themeData.sideImagePath
}, [mangoNfts, theme, themeData])
return (
<div
className={`transition-all duration-${sideBarAnimationDuration} ${
collapsed ? 'w-[64px]' : 'w-44 lg:w-48 xl:w-52'
} box-border border-r border-th-bkg-3 bg-th-bkg-1`}
collapsed ? 'w-[64px]' : 'w-[200px]'
} border-r border-th-bkg-3 bg-th-bkg-1 bg-repeat`}
style={{ backgroundImage: `url(${themeData.sideTilePath})` }}
>
{sidebarImageUrl && !collapsed ? (
<img
className={`absolute bottom-16 h-auto w-full flex-shrink-0`}
onClick={() => playAnimation()}
src={sidebarImageUrl}
alt="next"
/>
) : null}
<div className="flex min-h-screen flex-col justify-between">
<div className="my-2">
<div className="mb-2">
<Link href={'/'} shallow={true} passHref legacyBehavior>
<div
className={`h-14 items-center transition-all duration-${sideBarAnimationDuration} ease-in-out ${
className={`items-center transition-all duration-${sideBarAnimationDuration} ease-in-out ${
collapsed ? '' : 'justify-start'
} pb-1 pt-2 pl-4`}
} pb-1 pl-3`}
>
<div className={`flex flex-shrink-0 cursor-pointer items-center`}>
<div
className={`flex h-16 flex-shrink-0 cursor-pointer items-center bg-th-bkg-1`}
>
<img
className={`h-8 w-8 flex-shrink-0`}
src="/logos/logo-mark.svg"
alt="next"
className={`h-9 w-9 flex-shrink-0`}
src={themeData.logoPath}
alt="logo"
/>
<Transition
show={!collapsed}
@ -71,8 +131,8 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<span className="ml-3 font-display text-lg text-th-fgd-1">
Mango
<span className={`ml-3 font-display text-lg text-th-fgd-1`}>
{themeData.platformName}
</span>
</Transition>
</div>
@ -208,7 +268,7 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
</ExpandableMenuItem>
</div>
</div>
<div className="border-t border-th-bkg-3">
<div className="z-10 border-t border-th-bkg-3 bg-th-bkg-1">
<ExpandableMenuItem
collapsed={collapsed}
icon={
@ -341,6 +401,7 @@ export const ExpandableMenuItem = ({
title: string | ReactNode
}) => {
const { theme } = useTheme()
const themeData = mangoStore((s) => s.themeData)
return collapsed ? (
<Popover className={`relative z-30 ${alignBottom ? '' : 'py-2 pl-4'}`}>
@ -438,7 +499,11 @@ export const ExpandableMenuItem = ({
leaveFrom="opacity-100 max-h-80"
leaveTo="opacity-0 max-h-0"
>
<Disclosure.Panel className="w-full overflow-hidden">
<Disclosure.Panel
className={`w-full overflow-hidden ${
themeData.sideImagePath ? 'z-10 bg-th-bkg-1 py-2' : ''
}`}
>
<div className={`${!alignBottom ? 'ml-1.5' : ''}`}>
{children}
</div>

View File

@ -27,10 +27,10 @@ import useUnownedAccount from 'hooks/useUnownedAccount'
import NotificationsButton from './notifications/NotificationsButton'
import Tooltip from './shared/Tooltip'
import { copyToClipboard } from 'utils'
import mangoStore from '@store/mangoStore'
import UserSetupModal from './modals/UserSetupModal'
import { IS_ONBOARDED_KEY } from 'utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import mangoStore from '@store/mangoStore'
const set = mangoStore.getState().set
@ -38,6 +38,7 @@ const TopBar = () => {
const { t } = useTranslation('common')
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { connected } = useWallet()
const themeData = mangoStore((s) => s.themeData)
const [action, setAction] = useState<'deposit' | 'withdraw'>('deposit')
const [copied, setCopied] = useState('')
@ -89,9 +90,8 @@ const TopBar = () => {
return (
<div
className={`flex h-16 items-center justify-between border-b border-th-bkg-3 bg-th-bkg-1 ${
query.token || query.market ? '' : 'pl-4 md:pl-6'
}`}
className={`flex h-16 items-center justify-between border-b border-th-bkg-3 bg-th-bkg-1`}
style={{ backgroundImage: `url(${themeData.topTilePath})` }}
>
<div className="flex w-full items-center justify-between md:space-x-4">
<span className="mb-0 flex items-center">
@ -104,18 +104,18 @@ const TopBar = () => {
</button>
) : null}
{connected ? (
<div className="hidden md:block">
<div className="hidden h-[63px] bg-th-bkg-1 md:flex md:items-center md:pl-6 md:pr-8">
<SolanaTps />
</div>
) : null}
<img
className="mr-4 h-8 w-8 flex-shrink-0 md:hidden"
src="/logos/logo-mark.svg"
alt="next"
className="mr-4 h-9 w-9 flex-shrink-0 md:hidden"
src={themeData.logoPath}
alt="logo"
/>
{!connected ? (
mangoAccount ? (
<span className="hidden items-center md:flex">
<span className="hidden items-center md:flex md:pl-6">
<EyeIcon className="h-5 w-5 text-th-fgd-3" />
<span className="ml-2">
{t('unowned-helper', {
@ -169,7 +169,7 @@ const TopBar = () => {
</Tooltip>
</span>
) : (
<span className="hidden items-center md:flex">
<span className="hidden items-center md:flex md:pl-6">
<WalletIcon className="h-5 w-5 text-th-fgd-3" />
<span className="ml-2">{t('connect-helper')}</span>
<ArrowRightIcon className="sideways-bounce ml-2 h-5 w-5 text-th-fgd-1" />
@ -199,7 +199,7 @@ const TopBar = () => {
>{`${t('deposit')} / ${t('withdraw')}`}</Button>
)}
{connected ? (
<div className="flex items-center">
<div className="flex h-[63px] items-center bg-th-bkg-1">
{mangoAccountAddress && <NotificationsButton />}
<AccountsButton />
<ConnectedMenu />

View File

@ -1,7 +1,7 @@
import { HealthType } from '@blockworks-foundation/mango-v4'
import {
ArrowUpTrayIcon,
ExclamationCircleIcon,
// ExclamationCircleIcon,
LinkIcon,
} from '@heroicons/react/20/solid'
import Decimal from 'decimal.js'
@ -153,6 +153,7 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
const showInsufficientBalance = Number(inputAmount)
? tokenMax.lt(inputAmount)
: false
console.log(showInsufficientBalance)
return (
<>
@ -262,7 +263,9 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
size="large"
disabled={
connected &&
(!inputAmount || showInsufficientBalance || initHealth <= 0)
(!inputAmount ||
// showInsufficientBalance ||
initHealth <= 0)
}
>
{!connected ? (
@ -272,14 +275,15 @@ function WithdrawForm({ onSuccess, token }: WithdrawFormProps) {
</div>
) : submitting ? (
<Loading className="mr-2 h-5 w-5" />
) : showInsufficientBalance ? (
<div className="flex items-center">
<ExclamationCircleIcon className="mr-2 h-5 w-5 flex-shrink-0" />
{t('swap:insufficient-balance', {
symbol: selectedToken,
})}
</div>
) : (
// showInsufficientBalance ? (
// <div className="flex items-center">
// <ExclamationCircleIcon className="mr-2 h-5 w-5 flex-shrink-0" />
// {t('swap:insufficient-balance', {
// symbol: selectedToken,
// })}
// </div>
// ) :
<div className="flex items-center">
<ArrowUpTrayIcon className="mr-2 h-5 w-5" />
{t('withdraw')}

View File

@ -5,6 +5,7 @@ import {
ArrowUpLeftIcon,
DocumentDuplicateIcon,
PencilIcon,
SquaresPlusIcon,
TrashIcon,
UserPlusIcon,
WrenchIcon,
@ -26,6 +27,7 @@ import ActionsLinkButton from './ActionsLinkButton'
import useUnownedAccount from 'hooks/useUnownedAccount'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import MangoAccountSizeModal from '@components/modals/MangoAccountSizeModal'
export const handleCopyAddress = (
mangoAccount: MangoAccount,
@ -39,7 +41,7 @@ export const handleCopyAddress = (
}
const AccountActions = () => {
const { t } = useTranslation(['common', 'close-account'])
const { t } = useTranslation(['common', 'close-account', 'settings'])
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const [showCloseAccountModal, setShowCloseAccountModal] = useState(false)
const [showEditAccountModal, setShowEditAccountModal] = useState(false)
@ -47,6 +49,7 @@ const AccountActions = () => {
const [showRepayModal, setShowRepayModal] = useState(false)
const [showDelegateModal, setShowDelegateModal] = useState(false)
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const [showAccountSizeModal, setShowAccountSizeModal] = useState(false)
const { connected } = useWallet()
const { isDelegatedAccount, isUnownedAccount } = useUnownedAccount()
const { width } = useViewport()
@ -144,6 +147,16 @@ const AccountActions = () => {
<UserPlusIcon className="h-4 w-4" />
<span className="ml-2">{t('delegate-account')}</span>
</ActionsLinkButton>
<ActionsLinkButton
disabled={isDelegatedAccount}
mangoAccount={mangoAccount!}
onClick={() => setShowAccountSizeModal(true)}
>
<SquaresPlusIcon className="h-4 w-4" />
<span className="ml-2">
{t('settings:increase-account-size')}
</span>
</ActionsLinkButton>
<ActionsLinkButton
disabled={isDelegatedAccount}
mangoAccount={mangoAccount!}
@ -197,6 +210,12 @@ const AccountActions = () => {
onClose={() => setShowCreateAccountModal(false)}
/>
) : null}
{showAccountSizeModal ? (
<MangoAccountSizeModal
isOpen={showAccountSizeModal}
onClose={() => setShowAccountSizeModal(false)}
/>
) : null}
</>
)
}

View File

@ -135,7 +135,9 @@ const VolumeChart = ({ hideChart }: { hideChart: () => void }) => {
return dataTime >= limit
})
if (daysToShow === '30') {
return chunkDataByDay(filtered)
return chunkDataByDay(filtered).sort((a, b) =>
a.time.localeCompare(b.time),
)
}
return filtered
}, [chartData, daysToShow])

View File

@ -0,0 +1,397 @@
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import { isMangoError } from 'types'
import { notify } from 'utils/notifications'
import Tooltip from '../shared/Tooltip'
import Button, { LinkButton } from '../shared/Button'
import Loading from '../shared/Loading'
import InlineNotification from '../shared/InlineNotification'
import Modal from '@components/shared/Modal'
import { ModalProps } from 'types/modal'
import Label from '@components/forms/Label'
import {
getAvaialableAccountsColor,
getTotalMangoAccountAccounts,
getUsedMangoAccountAccounts,
} from '@components/settings/AccountSettings'
const MIN_ACCOUNTS = 8
export const MAX_ACCOUNTS: AccountSizeForm = {
tokenAccounts: '16',
spotOpenOrders: '8',
perpAccounts: '8',
perpOpenOrders: '64',
}
const INPUT_CLASSES =
'h-10 rounded-md rounded-r-none border w-full border-th-input-border bg-th-input-bkg px-3 font-mono text-base text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover disabled:text-th-fgd-4 disabled:bg-th-bkg-2 disabled:hover:border-th-input-border'
type FormErrors = Partial<Record<keyof AccountSizeForm, string>>
type AccountSizeForm = {
tokenAccounts: string | undefined
spotOpenOrders: string | undefined
perpAccounts: string | undefined
perpOpenOrders: string | undefined
[key: string]: string | undefined
}
const DEFAULT_FORM = {
tokenAccounts: '',
spotOpenOrders: '',
perpAccounts: '',
perpOpenOrders: '',
}
const MangoAccountSizeModal = ({ isOpen, onClose }: ModalProps) => {
const { t } = useTranslation(['common', 'settings'])
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const [accountSizeForm, setAccountSizeForm] =
useState<AccountSizeForm>(DEFAULT_FORM)
const [formErrors, setFormErrors] = useState<FormErrors>()
const [submitting, setSubmitting] = useState(false)
const [availableTokens, availableSerum3, availablePerps, availablePerpOo] =
useMemo(() => {
const [usedTokens, usedSerum3, usedPerps, usedPerpOo] =
getUsedMangoAccountAccounts(mangoAccountAddress)
const [totalTokens, totalSerum3, totalPerps, totalPerpOpenOrders] =
getTotalMangoAccountAccounts(mangoAccountAddress)
return [
<span
className={getAvaialableAccountsColor(usedTokens, totalTokens)}
key="tokenAccounts"
>{`${usedTokens}/${totalTokens}`}</span>,
<span
className={getAvaialableAccountsColor(usedSerum3, totalSerum3)}
key="spotOpenOrders"
>{`${usedSerum3}/${totalSerum3}`}</span>,
<span
className={getAvaialableAccountsColor(usedPerps, totalPerps)}
key="perpAccounts"
>{`${usedPerps}/${totalPerps}`}</span>,
<span
className={getAvaialableAccountsColor(
usedPerpOo,
totalPerpOpenOrders,
)}
key="perpOpenOrders"
>{`${usedPerpOo}/${totalPerpOpenOrders}`}</span>,
]
}, [mangoAccountAddress])
useEffect(() => {
if (mangoAccountAddress) {
setAccountSizeForm({
tokenAccounts: mangoAccount?.tokens.length.toString(),
spotOpenOrders: mangoAccount?.serum3.length.toString(),
perpAccounts: mangoAccount?.perps.length.toString(),
perpOpenOrders: mangoAccount?.perpOpenOrders.length.toString(),
})
}
}, [mangoAccountAddress])
const isFormValid = (form: AccountSizeForm) => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const invalidFields: FormErrors = {}
setFormErrors({})
const { tokenAccounts, spotOpenOrders, perpAccounts, perpOpenOrders } = form
if (tokenAccounts) {
const minTokenAccountsLength = mangoAccount?.tokens.length || MIN_ACCOUNTS
if (parseInt(tokenAccounts) < minTokenAccountsLength) {
invalidFields.tokenAccounts = t('settings:error-amount', {
type: t('settings:token-accounts'),
greaterThan: mangoAccount?.tokens.length,
lessThan: '17',
})
}
}
if (spotOpenOrders) {
const minSpotOpenOrdersLength =
mangoAccount?.serum3.length || MIN_ACCOUNTS
if (parseInt(spotOpenOrders) < minSpotOpenOrdersLength) {
invalidFields.spotOpenOrders = t('settings:error-amount', {
type: t('settings:spot-open-orders'),
greaterThan: mangoAccount?.serum3.length,
lessThan: '17',
})
}
}
if (perpAccounts) {
const minPerpAccountsLength = mangoAccount?.perps.length || MIN_ACCOUNTS
if (parseInt(perpAccounts) < minPerpAccountsLength) {
invalidFields.perpAccounts = t('settings:error-amount', {
type: t('settings:perp-accounts'),
greaterThan: mangoAccount?.perps.length,
lessThan: '17',
})
}
}
if (perpOpenOrders) {
const minPerpOpenOrdersLength =
mangoAccount?.perpOpenOrders.length || MIN_ACCOUNTS
if (parseInt(perpOpenOrders) < minPerpOpenOrdersLength) {
invalidFields.perpOpenOrders = t('settings:error-amount', {
type: t('settings:perp-open-orders'),
greaterThan: mangoAccount?.perpOpenOrders.length,
lessThan: '17',
})
}
}
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
}
const handleMax = (propertyName: keyof AccountSizeForm) => {
setFormErrors({})
setAccountSizeForm((prevState) => ({
...prevState,
[propertyName]: MAX_ACCOUNTS[propertyName],
}))
}
// const handleMaxAll = () => {
// setFormErrors({})
// const newValues = { ...accountSizeForm }
// for (const key in newValues) {
// newValues[key] = MAX_ACCOUNTS
// }
// setAccountSizeForm(newValues)
// }
const handleSetForm = (
propertyName: keyof AccountSizeForm,
e: NumberFormatValues,
info: SourceInfo,
) => {
if (info.source !== 'event') return
setFormErrors({})
setAccountSizeForm((prevState) => ({
...prevState,
[propertyName]: e.value,
}))
}
const handleUpdateAccountSize = useCallback(async () => {
const invalidFields = isFormValid(accountSizeForm)
if (Object.keys(invalidFields).length) {
return
}
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
const { tokenAccounts, spotOpenOrders, perpAccounts, perpOpenOrders } =
accountSizeForm
if (
!mangoAccount ||
!group ||
!tokenAccounts ||
!spotOpenOrders ||
!perpAccounts ||
!perpOpenOrders
)
return
setSubmitting(true)
try {
const tx = await client.accountExpandV2(
group,
mangoAccount,
parseInt(tokenAccounts),
parseInt(spotOpenOrders),
parseInt(perpAccounts),
parseInt(perpOpenOrders),
mangoAccount.tokenConditionalSwaps.length,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
})
await actions.reloadMangoAccount()
setSubmitting(false)
} catch (e) {
console.error(e)
if (!isMangoError(e)) return
notify({
title: 'Transaction failed',
description: e.message,
txid: e.txid,
type: 'error',
})
} finally {
setSubmitting(false)
}
}, [accountSizeForm])
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
<h2 className="mb-2 text-center">{t('settings:account-size')}</h2>
{/* <LinkButton className="font-normal mb-0.5" onClick={handleMaxAll}>
{t('settings:max-all')}
</LinkButton> */}
<p className="mb-4 text-center text-xs">
{t('settings:account-size-desc')}
</p>
<div className="mb-4">
<AccountSizeFormInput
availableAccounts={availableTokens}
error={formErrors?.tokenAccounts}
label={t('tokens')}
handleMax={() => handleMax('tokenAccounts')}
handleSetForm={handleSetForm}
tooltipContent={t('settings:tooltip-token-accounts', {
max: MAX_ACCOUNTS.tokenAccounts,
})}
type="tokenAccounts"
value={accountSizeForm.tokenAccounts}
/>
</div>
<div className="mb-4">
<AccountSizeFormInput
availableAccounts={availableSerum3}
disabled
error={formErrors?.spotOpenOrders}
label={t('settings:spot-open-orders')}
handleMax={() => handleMax('spotOpenOrders')}
handleSetForm={handleSetForm}
tooltipContent={t('settings:tooltip-spot-open-orders', {
max: MAX_ACCOUNTS.spotOpenOrders,
})}
type="spotOpenOrders"
value={accountSizeForm.spotOpenOrders}
/>
</div>
<div className="mb-4">
<AccountSizeFormInput
availableAccounts={availablePerps}
disabled
error={formErrors?.perpAccounts}
label={t('settings:perp-positions')}
handleMax={() => handleMax('perpAccounts')}
handleSetForm={handleSetForm}
tooltipContent={t('settings:tooltip-perp-positions', {
max: MAX_ACCOUNTS.perpAccounts,
})}
type="perpAccounts"
value={accountSizeForm.perpAccounts}
/>
</div>
<div>
<AccountSizeFormInput
availableAccounts={availablePerpOo}
error={formErrors?.perpOpenOrders}
label={t('settings:perp-open-orders')}
handleMax={() => handleMax('perpOpenOrders')}
handleSetForm={handleSetForm}
tooltipContent={t('settings:tooltip-perp-open-orders', {
max: MAX_ACCOUNTS.perpOpenOrders,
})}
type="perpOpenOrders"
value={accountSizeForm.perpOpenOrders}
/>
</div>
<Button
className="w-full mb-4 mt-6 flex items-center justify-center"
onClick={handleUpdateAccountSize}
size="large"
>
{submitting ? <Loading /> : t('settings:increase-account-size')}
</Button>
<LinkButton className="mx-auto" onClick={onClose}>
{t('cancel')}
</LinkButton>
</>
</Modal>
)
}
export default MangoAccountSizeModal
const AccountSizeFormInput = ({
availableAccounts,
disabled,
error,
label,
handleMax,
handleSetForm,
tooltipContent,
type,
value,
}: {
availableAccounts: ReactNode
disabled?: boolean
error: string | undefined
label: string
handleMax: (type: keyof AccountSizeForm) => void
handleSetForm: (
type: keyof AccountSizeForm,
values: NumberFormatValues,
info: SourceInfo,
) => void
tooltipContent: string
type: keyof AccountSizeForm
value: string | undefined
}) => {
const { t } = useTranslation(['common', 'settings'])
return (
<>
<div className="flex items-center justify-between">
<div className="flex items-center">
<Tooltip content={tooltipContent}>
<Label className="mr-1 tooltip-underline" text={label} />
</Tooltip>
</div>
{!disabled ? (
<LinkButton
className="mb-2 font-normal"
onClick={() => handleMax('tokenAccounts')}
>
{t('max')}
</LinkButton>
) : null}
</div>
<div className="flex items-center">
<NumberFormat
name={type as string}
id={type as string}
inputMode="numeric"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
className={INPUT_CLASSES}
value={value}
onValueChange={(e, sourceInfo) => handleSetForm(type, e, sourceInfo)}
disabled={disabled}
/>
<div
className={`flex items-center border border-l-0 border-th-input-border rounded-r-md h-10 px-2 ${
disabled ? 'bg-th-bkg-2' : 'bg-th-input-bkg'
}`}
>
<p className="font-mono text-xs">{availableAccounts}</p>
</div>
</div>
{error ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={error}
hideBorder
hidePadding
/>
</div>
) : null}
</>
)
}

View File

@ -14,17 +14,10 @@ const EditNftProfilePic = ({ onClose }: { onClose: () => void }) => {
const { publicKey, signMessage } = useWallet()
const nfts = mangoStore((s) => s.wallet.nfts.data)
const nftsLoading = mangoStore((s) => s.wallet.nfts.loading)
const connection = mangoStore((s) => s.connection)
const [selectedProfile, setSelectedProfile] = useState<string>('')
const actions = mangoStore.getState().actions
const profile = mangoStore((s) => s.profile.details)
useEffect(() => {
if (publicKey) {
actions.fetchNfts(connection, publicKey)
}
}, [publicKey])
useEffect(() => {
if (profile?.profile_image_url) {
setSelectedProfile(profile.profile_image_url)

View File

@ -21,6 +21,7 @@ const SEARCH_TYPES = [
'mango-account-name',
'profile-name',
'wallet-pk',
'open-orders-pk',
]
const SearchPage = () => {
@ -33,7 +34,11 @@ const SearchPage = () => {
const [isAccountSearch, setIsAccountSearch] = useState(true)
const handleSearch = async () => {
if (searchType === 'mango-account' || searchType === 'mango-account-name') {
if (
searchType === 'mango-account' ||
searchType === 'mango-account-name' ||
searchType === 'open-orders-pk'
) {
setIsAccountSearch(true)
} else {
setIsAccountSearch(false)

View File

@ -0,0 +1,152 @@
import MangoAccountSizeModal, {
MAX_ACCOUNTS,
} from '@components/modals/MangoAccountSizeModal'
import { LinkButton } from '@components/shared/Button'
import Tooltip from '@components/shared/Tooltip'
import { SquaresPlusIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import { useTranslation } from 'next-i18next'
import { useMemo, useState } from 'react'
// todo: use these functions to auto show model when an account is full
export const getUsedMangoAccountAccounts = (
mangoAccountAddress: string | undefined,
) => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccountAddress || !mangoAccount) return [0, 0, 0, 0]
const { tokens, serum3, perps, perpOpenOrders } = mangoAccount
const usedTokens = tokens.filter((t) => t.inUseCount).length
const usedSerum3 = serum3.filter((s) => s.marketIndex !== 65535).length
const usedPerps = perps.filter((p) => p.marketIndex !== 65535).length
const usedPerpOo = perpOpenOrders.filter(
(p) => p.orderMarket !== 65535,
).length
return [usedTokens, usedSerum3, usedPerps, usedPerpOo]
}
export const getTotalMangoAccountAccounts = (
mangoAccountAddress: string | undefined,
) => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccountAddress || !mangoAccount) return [0, 0, 0, 0]
const { tokens, serum3, perps, perpOpenOrders } = mangoAccount
const totalTokens = tokens.length
const totalSerum3 = serum3.length
const totalPerps = perps.length
const totalPerpOpenOrders = perpOpenOrders.length
return [totalTokens, totalSerum3, totalPerps, totalPerpOpenOrders]
}
export const getAvaialableAccountsColor = (used: number, total: number) => {
const remaining = total - used
return remaining >= 4
? 'text-th-up'
: remaining >= 2
? 'text-th-warning'
: 'text-th-down'
}
const AccountSettings = () => {
const { t } = useTranslation(['common', 'settings'])
const { mangoAccountAddress } = useMangoAccount()
const [showAccountSizeModal, setShowAccountSizeModal] = useState(false)
const [availableTokens, availableSerum3, availablePerps, availablePerpOo] =
useMemo(() => {
const [usedTokens, usedSerum3, usedPerps, usedPerpOo] =
getUsedMangoAccountAccounts(mangoAccountAddress)
const [totalTokens, totalSerum3, totalPerps, totalPerpOpenOrders] =
getTotalMangoAccountAccounts(mangoAccountAddress)
return [
<span
className={getAvaialableAccountsColor(usedTokens, totalTokens)}
key="tokenAccounts"
>{`${usedTokens}/${totalTokens}`}</span>,
<span
className={getAvaialableAccountsColor(usedSerum3, totalSerum3)}
key="spotOpenOrders"
>{`${usedSerum3}/${totalSerum3}`}</span>,
<span
className={getAvaialableAccountsColor(usedPerps, totalPerps)}
key="perpAccounts"
>{`${usedPerps}/${totalPerps}`}</span>,
<span
className={getAvaialableAccountsColor(
usedPerpOo,
totalPerpOpenOrders,
)}
key="perpOpenOrders"
>{`${usedPerpOo}/${totalPerpOpenOrders}`}</span>,
]
}, [mangoAccountAddress])
return (
<>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-base">{t('account')}</h2>
<LinkButton
className="flex items-center"
onClick={() => setShowAccountSizeModal(true)}
>
<SquaresPlusIcon className="h-4 w-4 mr-1.5" />
{t('settings:increase-account-size')}
</LinkButton>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<Tooltip
content={t('settings:tooltip-token-accounts', {
max: MAX_ACCOUNTS.tokenAccounts,
})}
>
<p className="tooltip-underline mb-2 md:mb-0">{t('tokens')}</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">{availableTokens}</p>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<Tooltip
content={t('settings:tooltip-spot-open-orders', {
max: MAX_ACCOUNTS.spotOpenOrders,
})}
>
<p className="tooltip-underline mb-2 md:mb-0">
{t('settings:spot-open-orders')}
</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">{availableSerum3}</p>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<Tooltip
content={t('settings:tooltip-perp-positions', {
max: MAX_ACCOUNTS.perpAccounts,
})}
>
<p className="tooltip-underline mb-2 md:mb-0">
{t('settings:perp-positions')}
</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">{availablePerps}</p>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<Tooltip
content={t('settings:tooltip-perp-open-orders', {
max: MAX_ACCOUNTS.perpOpenOrders,
})}
>
<p className="tooltip-underline mb-2 md:mb-0">
{t('settings:perp-open-orders')}
</p>
</Tooltip>
<p className="font-mono text-th-fgd-2">{availablePerpOo}</p>
</div>
{showAccountSizeModal ? (
<MangoAccountSizeModal
isOpen={showAccountSizeModal}
onClose={() => setShowAccountSizeModal(false)}
/>
) : null}
</>
)
}
export default AccountSettings

View File

@ -7,7 +7,7 @@ import ChartOnRight from '@components/icons/ChartOnRight'
import Tooltip from '@components/shared/Tooltip'
import { TradeLayout } from '@components/trade/TradeAdvancedPage'
// import dayjs from 'dayjs'
import { ReactNode } from 'react'
import { ReactNode, useEffect, useState } from 'react'
// import { useRouter } from 'next/router'
// import { useCallback } from 'react'
import dayjs from 'dayjs'
@ -23,7 +23,9 @@ import {
TRADE_CHART_UI_KEY,
TRADE_LAYOUT_KEY,
} from 'utils/constants'
import mangoStore from '@store/mangoStore'
import Switch from '@components/forms/Switch'
import { CUSTOM_SKINS } from 'utils/theme'
const NOTIFICATION_POSITIONS = [
'bottom-left',
@ -47,7 +49,7 @@ const LANGS = [
// { locale: 'zh', name: 'chinese', description: 'simplified chinese' },
]
export const THEMES = [
const DEFAULT_THEMES = [
'light',
'medium',
'dark',
@ -63,6 +65,9 @@ export const THEMES = [
const DisplaySettings = () => {
const { t } = useTranslation(['common', 'settings'])
const { theme, setTheme } = useTheme()
const [themes, setThemes] = useState(DEFAULT_THEMES)
const nfts = mangoStore((s) => s.wallet.nfts.data)
const [savedLanguage, setSavedLanguage] = useLocalStorageState(
'language',
'en',
@ -87,6 +92,24 @@ const DisplaySettings = () => {
true,
)
// add nft skins to theme selection list
useEffect(() => {
if (nfts.length) {
const customThemes = []
for (const nft of nfts) {
const collectionAddress = nft?.collectionAddress
for (const themeKey in CUSTOM_SKINS) {
if (CUSTOM_SKINS[themeKey] === collectionAddress) {
customThemes.push(themeKey)
}
}
}
if (customThemes.length) {
setThemes([...customThemes, ...DEFAULT_THEMES])
}
}
}, [nfts])
const handleLangChange = useCallback(
(l: string) => {
setSavedLanguage(l)
@ -99,24 +122,22 @@ const DisplaySettings = () => {
return (
<>
<h2 className="mb-4 text-base">{t('settings:display')}</h2>
{theme ? (
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<p className="mb-2 md:mb-0">{t('settings:theme')}</p>
<div className="w-full min-w-[140px] md:w-auto">
<Select
value={theme}
onChange={(t: string) => setTheme(t)}
className="w-full"
>
{THEMES.map((theme) => (
<Select.Option key={theme} value={t(`settings:${theme}`)}>
{t(`settings:${theme}`)}
</Select.Option>
))}
</Select>
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<p className="mb-2 md:mb-0">{t('settings:theme')}</p>
<div className="w-full min-w-[140px] md:w-auto">
<Select
value={theme || DEFAULT_THEMES[0]}
onChange={(t: string) => setTheme(t)}
className="w-full"
>
{themes.map((theme) => (
<Select.Option key={theme} value={t(`settings:${theme}`)}>
{t(`settings:${theme}`)}
</Select.Option>
))}
</Select>
</div>
) : null}
</div>
<div className="flex flex-col border-t border-th-bkg-3 py-4 md:flex-row md:items-center md:justify-between md:px-4">
<p className="mb-2 md:mb-0">{t('settings:language')}</p>
<div className="w-full min-w-[220px] md:w-auto md:pl-4">

View File

@ -23,7 +23,7 @@ const PreferredExplorerSettings = () => {
<div className="space-y-2">
{EXPLORERS.map((ex) => (
<button
className="flex w-full items-center justify-between rounded-md bg-th-bkg-2 p-4 hover:bg-th-bkg-3"
className="flex w-full items-center justify-between rounded-md border border-th-bkg-4 p-4 hover:border-th-fgd-4"
onClick={() => setPreferredExplorer(ex)}
key={ex.name}
>

View File

@ -7,6 +7,7 @@ import PreferredExplorerSettings from './PreferredExplorerSettings'
import RpcSettings from './RpcSettings'
import SoundSettings from './SoundSettings'
import { breakpoints } from 'utils/theme'
import AccountSettings from './AccountSettings'
const SettingsPage = () => {
const { width } = useViewport()
@ -16,6 +17,9 @@ const SettingsPage = () => {
<div className="col-span-12 border-b border-th-bkg-3 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<RpcSettings />
</div>
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<AccountSettings />
</div>
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<DisplaySettings />
</div>

View File

@ -1,3 +1,4 @@
import mangoStore from '@store/mangoStore'
import { useTheme } from 'next-themes'
import { forwardRef, FunctionComponent, ReactNode, Ref } from 'react'
@ -27,12 +28,15 @@ const Button: FunctionComponent<ButtonCombinedProps> = ({
...props
}) => {
const { theme } = useTheme()
const themeData = mangoStore((s) => s.themeData)
return (
<button
onClick={onClick}
disabled={disabled}
className={`rounded-md ${
secondary
themeData.buttonStyle === 'raised'
? 'raised-button'
: secondary
? 'border border-th-button focus-visible:border-th-fgd-4 md:hover:border-th-button-hover'
: 'bg-th-button focus-visible:border focus-visible:border-th-fgd-4 md:hover:bg-th-button-hover'
} ${

View File

@ -1,6 +1,6 @@
import { Dialog } from '@headlessui/react'
import { XMarkIcon } from '@heroicons/react/20/solid'
import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts'
import mangoStore from '@store/mangoStore'
type ModalProps = {
children: React.ReactNode
@ -21,6 +21,8 @@ function Modal({
panelClassNames,
hideClose,
}: ModalProps) {
const themeData = mangoStore((s) => s.themeData)
const handleClose = () => {
if (disableOutsideClose) return
onClose()
@ -44,8 +46,10 @@ function Modal({
}`}
>
<Dialog.Panel
className={`${ttCommons.variable} ${ttCommonsExpanded.variable} ${
ttCommonsMono.variable
className={`${themeData.fonts.body.variable} ${
themeData.fonts.display.variable
} ${
themeData.fonts.mono.variable
} font-sans h-full w-full bg-th-bkg-1 font-body ${
fullScreen
? ''

View File

@ -3,6 +3,7 @@ import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettin
import mangoStore from '@store/mangoStore'
import useJupiterMints from 'hooks/useJupiterMints'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useTheme } from 'next-themes'
import { useEffect, useMemo } from 'react'
import Particles from 'react-tsparticles'
import { ANIMATION_SETTINGS_KEY, CUSTOM_TOKEN_ICONS } from 'utils/constants'
@ -10,13 +11,16 @@ import { ANIMATION_SETTINGS_KEY, CUSTOM_TOKEN_ICONS } from 'utils/constants'
const SuccessParticles = () => {
const { mangoTokens } = useJupiterMints()
const showForSwap = mangoStore((s) => s.successAnimation.swap)
const showForTheme = mangoStore((s) => s.successAnimation.theme)
const showForTrade = mangoStore((s) => s.successAnimation.trade)
const tradeType = mangoStore((s) => s.tradeForm.tradeType)
const themeData = mangoStore((s) => s.themeData)
const set = mangoStore((s) => s.set)
const [animationSettings] = useLocalStorageState(
ANIMATION_SETTINGS_KEY,
INITIAL_ANIMATION_SETTINGS,
)
const { theme } = useTheme()
const tokenLogo = useMemo(() => {
if (!mangoTokens.length) return ''
@ -61,7 +65,10 @@ const SuccessParticles = () => {
}
}
}
}, [mangoTokens, showForSwap, showForTrade])
if (showForTheme) {
return themeData.rainAnimationImagePath
}
}, [mangoTokens, showForSwap, showForTheme, showForTrade, theme])
useEffect(() => {
if (showForSwap) {
@ -73,6 +80,15 @@ const SuccessParticles = () => {
8000,
)
}
if (showForTheme) {
setTimeout(
() =>
set((s) => {
s.successAnimation.theme = false
}),
6000,
)
}
if (showForTrade) {
setTimeout(
() =>
@ -82,11 +98,11 @@ const SuccessParticles = () => {
8000,
)
}
}, [showForSwap, showForTrade])
}, [showForSwap, showForTheme, showForTrade])
return animationSettings['swap-success'] &&
return (animationSettings['swap-success'] || showForTheme) &&
tokenLogo &&
(showForSwap || showForTrade) ? (
(showForSwap || showForTrade || showForTheme) ? (
<Particles
id="tsparticles"
options={{

View File

@ -1,7 +1,7 @@
import React, { HTMLAttributes, ReactNode } from 'react'
import Tippy, { TippyProps } from '@tippyjs/react'
import 'tippy.js/animations/scale.css'
import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts'
import mangoStore from '@store/mangoStore'
type TooltipProps = {
content: ReactNode
@ -22,6 +22,7 @@ const Tooltip = ({
show = true,
maxWidth = '20rem',
}: TooltipProps) => {
const themeData = mangoStore((s) => s.themeData)
if (show) {
return (
<Tippy
@ -34,7 +35,7 @@ const Tooltip = ({
content={
content ? (
<div
className={`${ttCommons.variable} ${ttCommonsExpanded.variable} ${ttCommonsMono.variable} font-sans rounded-md bg-th-bkg-2 p-3 font-body text-xs leading-4 text-th-fgd-3 outline-none focus:outline-none ${className}`}
className={`${themeData.fonts.body.variable} ${themeData.fonts.display.variable} ${themeData.fonts.mono.variable} font-sans font-sans rounded-md bg-th-bkg-2 p-3 font-body text-xs leading-4 text-th-fgd-3 outline-none focus:outline-none ${className}`}
style={{ boxShadow: '0px 0px 8px 0px rgba(0,0,0,0.25)' }}
>
{content}

View File

@ -399,7 +399,7 @@ const TokenOverviewTable = () => {
<div className="col-span-1">
<p className="text-xs">{t('utilization')}</p>
<p className="font-mono text-th-fgd-1">
{utilization}%
{utilization.toFixed(1)}%
</p>
</div>
<div className="col-span-1">

View File

@ -26,9 +26,9 @@ import { floorToDecimal } from 'utils/numbers'
import { withValueLimit } from './MarketSwapForm'
import SellTokenInput from './SellTokenInput'
import BuyTokenInput from './BuyTokenInput'
import { notify } from 'utils/notifications'
import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
// import { notify } from 'utils/notifications'
// import * as sentry from '@sentry/nextjs'
// import { isMangoError } from 'types'
import Button, { LinkButton } from '@components/shared/Button'
import Loading from '@components/shared/Loading'
import TokenLogo from '@components/shared/TokenLogo'
@ -36,7 +36,7 @@ import InlineNotification from '@components/shared/InlineNotification'
import { getChartPairSettings, handleFlipPrices } from './SwapTokenChart'
import Select from '@components/forms/Select'
import useIpAddress from 'hooks/useIpAddress'
import { Bank } from '@blockworks-foundation/mango-v4'
// import { Bank } from '@blockworks-foundation/mango-v4'
type LimitSwapFormProps = {
showTokenSelect: 'input' | 'output' | undefined
@ -67,12 +67,12 @@ const ORDER_TYPES = [
const set = mangoStore.getState().set
const getSellTokenBalance = (inputBank: Bank | undefined) => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!inputBank || !mangoAccount) return 0
const balance = mangoAccount.getTokenBalanceUi(inputBank)
return balance
}
// const getSellTokenBalance = (inputBank: Bank | undefined) => {
// const mangoAccount = mangoStore.getState().mangoAccount.current
// if (!inputBank || !mangoAccount) return 0
// const balance = mangoAccount.getTokenBalanceUi(inputBank)
// return balance
// }
const getOrderTypeMultiplier = (orderType: OrderTypes, flipPrices: boolean) => {
if (orderType === OrderTypes.STOP_LOSS) {
@ -95,7 +95,10 @@ const LimitSwapForm = ({
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [orderTypeMultiplier, setOrderTypeMultiplier] =
useState<OrderTypeMultiplier | null>(null)
const [submitting, setSubmitting] = useState(false)
const [
submitting,
// setSubmitting
] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [formErrors, setFormErrors] = useState<FormErrors>({})
const [swapChartSettings, setSwapChartSettings] = useLocalStorageState(
@ -108,7 +111,7 @@ const LimitSwapForm = ({
outputBank,
amountIn: amountInFormValue,
amountOut: amountOutFormValue,
limitPrice,
// limitPrice,
} = mangoStore((s) => s.swap)
const [inputBankName, outputBankName, inputBankDecimals, outputBankDecimals] =
@ -222,64 +225,64 @@ const LimitSwapForm = ({
return borrow
}, [orderType, outputBank])
const isFormValid = useCallback(
(form: LimitSwapForm) => {
const invalidFields: FormErrors = {}
setFormErrors({})
const requiredFields: (keyof LimitSwapForm)[] = [
'amountIn',
'triggerPrice',
]
const triggerPriceNumber = parseFloat(form.triggerPrice)
const sellTokenBalance = getSellTokenBalance(inputBank)
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
invalidFields[key] = t('settings:error-required-field')
}
}
if (orderType === OrderTypes.STOP_LOSS) {
if (!flipPrices && triggerPriceNumber <= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be above oracle price'
}
if (flipPrices && triggerPriceNumber >= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be below oracle price'
}
}
if (orderType === OrderTypes.TAKE_PROFIT) {
if (!flipPrices && triggerPriceNumber >= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be below oracle price'
}
if (flipPrices && triggerPriceNumber <= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be above oracle price'
}
}
if (orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay) {
invalidFields.hasBorrows = t('swap:no-borrow')
}
if (form.amountIn > sellTokenBalance) {
invalidFields.amountIn = t('swap:insufficient-balance', {
symbol: inputBank?.name,
})
}
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
},
[
flipPrices,
hasBorrowToRepay,
inputBank,
orderType,
quotePrice,
setFormErrors,
],
)
// const isFormValid = useCallback(
// (form: LimitSwapForm) => {
// const invalidFields: FormErrors = {}
// setFormErrors({})
// const requiredFields: (keyof LimitSwapForm)[] = [
// 'amountIn',
// 'triggerPrice',
// ]
// const triggerPriceNumber = parseFloat(form.triggerPrice)
// const sellTokenBalance = getSellTokenBalance(inputBank)
// for (const key of requiredFields) {
// const value = form[key] as string
// if (!value) {
// invalidFields[key] = t('settings:error-required-field')
// }
// }
// if (orderType === OrderTypes.STOP_LOSS) {
// if (!flipPrices && triggerPriceNumber <= quotePrice) {
// invalidFields.triggerPrice =
// 'Trigger price must be above oracle price'
// }
// if (flipPrices && triggerPriceNumber >= quotePrice) {
// invalidFields.triggerPrice =
// 'Trigger price must be below oracle price'
// }
// }
// if (orderType === OrderTypes.TAKE_PROFIT) {
// if (!flipPrices && triggerPriceNumber >= quotePrice) {
// invalidFields.triggerPrice =
// 'Trigger price must be below oracle price'
// }
// if (flipPrices && triggerPriceNumber <= quotePrice) {
// invalidFields.triggerPrice =
// 'Trigger price must be above oracle price'
// }
// }
// if (orderType === OrderTypes.REPAY_BORROW && !hasBorrowToRepay) {
// invalidFields.hasBorrows = t('swap:no-borrow')
// }
// if (form.amountIn > sellTokenBalance) {
// invalidFields.amountIn = t('swap:insufficient-balance', {
// symbol: inputBank?.name,
// })
// }
// if (Object.keys(invalidFields).length) {
// setFormErrors(invalidFields)
// }
// return invalidFields
// },
// [
// flipPrices,
// hasBorrowToRepay,
// inputBank,
// orderType,
// quotePrice,
// setFormErrors,
// ],
// )
// set order type multiplier on page load
useEffect(() => {
@ -451,76 +454,76 @@ const LimitSwapForm = ({
triggerPrice,
])
const handlePlaceStopLoss = useCallback(async () => {
const invalidFields = isFormValid({
amountIn: amountInAsDecimal.toNumber(),
hasBorrows: hasBorrowToRepay,
triggerPrice,
})
if (Object.keys(invalidFields).length) {
return
}
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank
// const handlePlaceStopLoss = useCallback(async () => {
// const invalidFields = isFormValid({
// amountIn: amountInAsDecimal.toNumber(),
// hasBorrows: hasBorrowToRepay,
// triggerPrice,
// })
// if (Object.keys(invalidFields).length) {
// return
// }
// try {
// const client = mangoStore.getState().client
// const group = mangoStore.getState().group
// const actions = mangoStore.getState().actions
// const mangoAccount = mangoStore.getState().mangoAccount.current
// const inputBank = mangoStore.getState().swap.inputBank
// const outputBank = mangoStore.getState().swap.outputBank
if (!mangoAccount || !group || !inputBank || !outputBank || !triggerPrice)
return
setSubmitting(true)
// if (!mangoAccount || !group || !inputBank || !outputBank || !triggerPrice)
// return
// setSubmitting(true)
const inputMint = inputBank.mint
const outputMint = outputBank.mint
const amountIn = amountInAsDecimal.toNumber()
// const inputMint = inputBank.mint
// const outputMint = outputBank.mint
// const amountIn = amountInAsDecimal.toNumber()
try {
const tx = await client.tokenConditionalSwapStopLoss(
group,
mangoAccount,
inputMint,
parseFloat(triggerPrice),
outputMint,
null,
amountIn,
null,
null,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount()
} catch (e) {
console.error('onSwap error: ', e)
sentry.captureException(e)
if (isMangoError(e)) {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}
} catch (e) {
console.error('Swap error:', e)
} finally {
setSubmitting(false)
}
}, [
hasBorrowToRepay,
flipPrices,
limitPrice,
triggerPrice,
amountInAsDecimal,
amountOutFormValue,
])
// try {
// const tx = await client.tokenConditionalSwapStopLoss(
// group,
// mangoAccount,
// inputMint,
// parseFloat(triggerPrice),
// outputMint,
// null,
// amountIn,
// null,
// null,
// )
// notify({
// title: 'Transaction confirmed',
// type: 'success',
// txid: tx,
// noSound: true,
// })
// actions.fetchGroup()
// await actions.reloadMangoAccount()
// } catch (e) {
// console.error('onSwap error: ', e)
// sentry.captureException(e)
// if (isMangoError(e)) {
// notify({
// title: 'Transaction failed',
// description: e.message,
// txid: e?.txid,
// type: 'error',
// })
// }
// }
// } catch (e) {
// console.error('Swap error:', e)
// } finally {
// setSubmitting(false)
// }
// }, [
// hasBorrowToRepay,
// flipPrices,
// limitPrice,
// triggerPrice,
// amountInAsDecimal,
// amountOutFormValue,
// ])
const orderDescription = useMemo(() => {
if (
@ -782,7 +785,7 @@ const LimitSwapForm = ({
) : null}
{ipAllowed ? (
<Button
onClick={handlePlaceStopLoss}
// onClick={handlePlaceStopLoss}
className="mt-6 mb-4 flex w-full items-center justify-center text-base"
size="large"
>

View File

@ -56,7 +56,7 @@ const ChartTabs = ({ bank }: { bank: Bank }) => {
['token:deposit-rates', 0],
]}
/>
<div className="h-96 border-t border-th-bkg-3 px-6 py-6">
<div className="h-[412px] sm:h-96 border-t border-th-bkg-3 px-6 py-6">
{activeDepositsTab === 'token:deposits' ? (
<DetailedAreaOrBarChart
data={statsHistory}
@ -96,7 +96,7 @@ const ChartTabs = ({ bank }: { bank: Bank }) => {
['token:borrow-rates', 0],
]}
/>
<div className="h-96 border-t border-th-bkg-3 px-6 py-6">
<div className="h-[412px] sm:h-96 border-t border-th-bkg-3 px-6 py-6">
{activeBorrowsTab === 'token:borrows' ? (
<DetailedAreaOrBarChart
data={statsHistory}

View File

@ -166,7 +166,7 @@ const TokenPage = () => {
</div>
) : null}
</div>
{coingeckoTokenInfo?.market_data ? (
{high_24h.usd && low_24h.usd ? (
<DailyRange
high={high_24h.usd}
low={low_24h.usd}

View File

@ -135,7 +135,7 @@ const TokenParams = ({ bank }: { bank: Bank }) => {
</div>
</div>
<div className="col-span-1 px-6 pb-4 md:pt-4">
<div className="flex justify-between pb-4">
<div className="flex justify-between py-4 md:pt-0">
<Tooltip content={t('token:tooltip-net-borrow-period')}>
<p className="tooltip-underline">{t('token:net-borrow-period')}</p>
</Tooltip>

View File

@ -55,7 +55,9 @@ const AdvancedMarketHeader = ({
return (
<>
<div className="flex flex-col bg-th-bkg-1 md:h-12 md:flex-row md:items-center">
<div
className={`flex flex-col bg-th-bkg-1 md:h-12 md:flex-row md:items-center`}
>
<div className="w-full pl-4 md:w-auto md:py-0 md:pl-6 lg:pb-0">
<MarketSelectDropdown />
</div>

View File

@ -82,6 +82,7 @@ const AdvancedTradeForm = () => {
const { t } = useTranslation(['common', 'trade'])
const { mangoAccount } = useMangoAccount()
const tradeForm = mangoStore((s) => s.tradeForm)
const themeData = mangoStore((s) => s.themeData)
const [placingOrder, setPlacingOrder] = useState(false)
const [tradeFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [savedCheckboxSettings, setSavedCheckboxSettings] =
@ -747,8 +748,16 @@ const AdvancedTradeForm = () => {
!connected
? ''
: tradeForm.side === 'buy'
? 'bg-th-up-dark text-white md:hover:bg-th-up-dark md:hover:brightness-90'
: 'bg-th-down-dark text-white md:hover:bg-th-down-dark md:hover:brightness-90'
? `bg-th-up-dark md:hover:bg-th-up-dark ${
themeData.buttonStyle === 'raised'
? 'raised-buy-button'
: 'text-white md:hover:brightness-90'
}`
: `bg-th-down-dark text-white ${
themeData.buttonStyle === 'raised'
? ''
: 'md:hover:bg-th-down-dark md:hover:brightness-90'
}`
}`}
disabled={disabled}
size="large"

View File

@ -16,12 +16,12 @@ import { useTranslation } from 'next-i18next'
import Decimal from 'decimal.js'
import Tooltip from '@components/shared/Tooltip'
import GroupSize from './GroupSize'
import { useViewport } from 'hooks/useViewport'
// import { useViewport } from 'hooks/useViewport'
import { BookSide, Serum3Market } from '@blockworks-foundation/mango-v4'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
import { breakpoints } from 'utils/theme'
// import { breakpoints } from 'utils/theme'
import {
decodeBook,
decodeBookL2,
@ -48,23 +48,61 @@ const Orderbook = () => {
const [tickSize, setTickSize] = useState(0)
const [grouping, setGrouping] = useState(0.01)
const [useOrderbookFeed, setUseOrderbookFeed] = useState(false)
const orderbookElRef = useRef<HTMLDivElement>(null)
const [isScrolled, setIsScrolled] = useState(false)
// const [useOrderbookFeed, setUseOrderbookFeed] = useState(
// localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null
// ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
// : true
// )
const { width } = useViewport()
// const { width } = useViewport()
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
const currentOrderbookData = useRef<OrderbookL2>()
const depth = useMemo(() => {
return width > breakpoints['3xl'] ? 12 : 10
}, [width])
return 30
}, [])
const depthArray: number[] = useMemo(() => {
return Array(depth).fill(0)
}, [depth])
const verticallyCenterOrderbook = useCallback(() => {
const element = orderbookElRef.current
if (element) {
console.log('vertically centering')
if (
element.parentElement &&
element.scrollHeight > element.parentElement.offsetHeight
) {
element.scrollTop =
(element.scrollHeight - element.scrollHeight) / 2 +
(element.scrollHeight - element.parentElement.offsetHeight) / 2 +
60
} else {
element.scrollTop = (element.scrollHeight - element.offsetHeight) / 2
}
}
}, [])
useEffect(() => {
console.log('hi')
window.addEventListener('resize', verticallyCenterOrderbook)
}, [verticallyCenterOrderbook])
// const resetOrderbook = useCallback(async () => {
// // setShowBids(true)
// // setShowAsks(true)
// await sleep(300)
// verticallyCenterOrderbook()
// }, [verticallyCenterOrderbook])
const handleScroll = useCallback(() => {
setIsScrolled(true)
}, [])
const orderbookFeed = useRef<OrderbookFeed | null>(null)
useEffect(() => {
@ -182,6 +220,9 @@ const Orderbook = () => {
spread,
spreadPercentage,
})
if (!isScrolled) {
verticallyCenterOrderbook()
}
} else {
setOrderbookData(null)
}
@ -422,7 +463,7 @@ const Orderbook = () => {
}, [])
return (
<div>
<div className="flex h-full flex-col">
<div className="h-10 flex items-center justify-between border-b border-th-bkg-3 px-4">
{market ? (
<>
@ -448,7 +489,11 @@ const Orderbook = () => {
<div className="col-span-1">{t('price')}</div>
<div className="col-span-1 text-right">{t('trade:size')}</div>
</div>
<div className="relative h-full">
<div
className="hide-scroll relative h-full overflow-y-scroll"
ref={orderbookElRef}
onScroll={handleScroll}
>
{depthArray.map((_x, idx) => {
let index = idx
if (orderbookData?.asks) {

View File

@ -59,7 +59,7 @@ const TradeAdvancedPage = () => {
const totalCols = 24
const gridBreakpoints = useMemo(() => {
const sidebarWidth = isCollapsed ? 64 : 207
const sidebarWidth = isCollapsed ? 64 : 200
return {
md: breakpoints.md - sidebarWidth,
lg: breakpoints.lg - sidebarWidth,

View File

@ -161,7 +161,7 @@ const TradeHistory = () => {
</Td>
<Td className="text-right font-mono">{size}</Td>
<Td className="text-right font-mono">
<FormatNumericValue value={price} />
<p>{price}</p>
</Td>
<Td className="text-right font-mono">
<FormatNumericValue value={value} decimals={2} isUsd />

View File

@ -283,7 +283,6 @@ const TradeSummary = ({
<div className="flex justify-between text-xs">
<p>{t('trade:avg-entry-price')}</p>
<p className="text-th-fgd-2">
{tradeForm.tradeType === 'Market' ? '~' : null}
<FormatNumericValue
value={avgEntryPrice}
decimals={getDecimalCount(selectedMarket.tickSize)}
@ -292,6 +291,10 @@ const TradeSummary = ({
</p>
</div>
) : null}
<div className="flex justify-between text-xs">
<p>{t('common:route')}</p>
<p className="text-th-fgd-2">Openbook</p>
</div>
</div>
)
}

View File

@ -1,4 +1,11 @@
import { useEffect, useRef, useMemo, useState, useCallback } from 'react'
import {
useEffect,
useRef,
useMemo,
useState,
useCallback,
Fragment,
} from 'react'
import {
widget,
ChartingLibraryWidgetOptions,
@ -35,6 +42,7 @@ import useTradeHistory from 'hooks/useTradeHistory'
import dayjs from 'dayjs'
import ModifyTvOrderModal from '@components/modals/ModifyTvOrderModal'
import { findSerum3MarketPkInOpenOrders } from './OpenOrders'
import { Transition } from '@headlessui/react'
import useThemeWrapper from 'hooks/useThemeWrapper'
export interface ChartContainerProps {
@ -79,9 +87,11 @@ const TradingViewChart = () => {
showOrderLinesLocalStorage,
)
const tradeExecutions = mangoStore((s) => s.tradingView.tradeExecutions)
const themeData = mangoStore((s) => s.themeData)
const { data: combinedTradeHistory, isLoading: loadingTradeHistory } =
useTradeHistory()
const [showTradeExecutions, toggleShowTradeExecutions] = useState(false)
const [showThemeEasterEgg, toggleShowThemeEasterEgg] = useState(false)
const [cachedTradeHistory, setCachedTradeHistory] =
useState(combinedTradeHistory)
const [userId] = useLocalStorageState(TV_USER_ID_KEY, '')
@ -112,26 +122,29 @@ const TradingViewChart = () => {
const tvWidgetRef = useRef<IChartingLibraryWidget>()
const orderLinesButtonRef = useRef<HTMLElement>()
const selectedMarketPk = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarketName)
return '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
if (!selectedMarketName?.toLowerCase().includes('perp')) {
return group
.getSerum3MarketByName(selectedMarketName)
.serumMarketExternal.toString()
} else {
return group.getPerpMarketByName(selectedMarketName).publicKey.toString()
}
}, [selectedMarketName])
// Sets the "symbol" in trading view which is used to fetch chart data via the datafeed
useEffect(() => {
const group = mangoStore.getState().group
if (tvWidgetRef.current && chartReady && selectedMarketPk && group) {
let mktAddress = 'So11111111111111111111111111111111111111112'
if (
group &&
selectedMarketName &&
!selectedMarketName?.toLowerCase().includes('perp')
) {
mktAddress = group
.getSerum3MarketByName(selectedMarketName)
.serumMarketExternal.toString()
} else if (group && selectedMarketName) {
mktAddress = group
.getPerpMarketByName(selectedMarketName)
.publicKey.toString()
}
if (tvWidgetRef.current && chartReady && mktAddress && group) {
try {
tvWidgetRef.current.setSymbol(
selectedMarketPk,
mktAddress,
tvWidgetRef.current.activeChart().resolution(),
() => {
if (showOrderLinesLocalStorage) {
@ -146,7 +159,7 @@ const TradingViewChart = () => {
console.warn('Trading View change symbol error: ', e)
}
}
}, [chartReady, selectedMarketPk, showOrderLinesLocalStorage])
}, [chartReady, selectedMarketName, showOrderLinesLocalStorage])
useEffect(() => {
if (showOrderLines !== showOrderLinesLocalStorage) {
@ -444,6 +457,18 @@ const TradingViewChart = () => {
[theme],
)
const toggleThemeEasterEgg = useCallback(
(el: HTMLElement) => {
toggleShowThemeEasterEgg((prevState) => !prevState)
if (el.style.color === hexToRgb(COLORS.ACTIVE[theme])) {
el.style.color = COLORS.FGD4[theme]
} else {
el.style.color = COLORS.ACTIVE[theme]
}
},
[theme],
)
const createOLButton = useCallback(() => {
const button = tvWidgetRef?.current?.createButton()
if (!button) {
@ -475,6 +500,20 @@ const TradingViewChart = () => {
}
}, [t, toggleTradeExecutions, showTradeExecutions, theme])
const createEasterEggButton = useCallback(() => {
const button = tvWidgetRef?.current?.createButton()
if (!button) {
return
}
button.textContent = theme.toUpperCase()
button.addEventListener('click', () => toggleThemeEasterEgg(button))
if (showThemeEasterEgg) {
button.style.color = COLORS.ACTIVE[theme]
} else {
button.style.color = COLORS.FGD4[theme]
}
}, [toggleThemeEasterEgg, showTradeExecutions, theme])
useEffect(() => {
if (window) {
let chartStyleOverrides = {
@ -486,6 +525,7 @@ const TradingViewChart = () => {
'paneProperties.legendProperties.showStudyTitles': false,
'scalesProperties.showStudyLastValue': false,
'scalesProperties.fontSize': 11,
'scalesProperties.lineColor': COLORS.BKG4[theme],
}
const mainSeriesProperties = [
@ -514,8 +554,7 @@ const TradingViewChart = () => {
const marketAddress =
(mkt instanceof Serum3Market
? mkt?.serumMarketExternal.toString()
: mkt?.publicKey.toString()) ||
'8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
: mkt?.publicKey.toString()) || 'Loading'
const widgetOptions: ChartingLibraryWidgetOptions = {
// debug: true,
@ -573,7 +612,7 @@ const TradingViewChart = () => {
theme:
theme === 'Light' || theme === 'Banana' || theme === 'Lychee'
? 'Light'
: 'Dark',
: themeData.tvChartTheme,
custom_css_url: '/styles/tradingview.css',
loading_screen: {
backgroundColor: COLORS.BKG1[theme],
@ -586,15 +625,15 @@ const TradingViewChart = () => {
console.log('creating new chart')
const tvWidget = new widget(widgetOptions)
tvWidgetRef.current = tvWidget
tvWidgetRef.current.onChartReady(() => {
tvWidget.onChartReady(() => {
tvWidgetRef.current = tvWidget
setChartReady(true)
})
tvWidgetRef.current.headerReady().then(() => {
tvWidget.headerReady().then(() => {
setHeaderReady(true)
})
}
}, [theme, defaultProps, isMobile, userId])
}, [theme, themeData, defaultProps, isMobile, userId])
// set a limit price from right click context menu
useEffect(() => {
@ -628,8 +667,11 @@ const TradingViewChart = () => {
if (chartReady && headerReady && !orderLinesButtonRef.current) {
createOLButton()
createTEButton()
if (themeData.tvImagePath) {
createEasterEggButton()
}
}
}, [createOLButton, createTEButton, chartReady, headerReady])
}, [createOLButton, createTEButton, chartReady, headerReady, themeData])
// update order lines if a user's open orders change
useEffect(() => {
@ -780,6 +822,21 @@ const TradingViewChart = () => {
return (
<>
<Transition
show={showThemeEasterEgg}
as={Fragment}
enter="transition ease-in duration-500"
enterFrom="scale-0 opacity-0"
enterTo="scale-100 rotate-[-370deg] opacity-100"
leave="transition ease-out duration-500"
leaveFrom="scale-100 opacity-100"
leaveTo="scale-0 opacity-0"
>
<img
className="absolute top-8 right-20 h-auto w-36"
src="/images/themes/bonk/tv-chart-image.png"
/>
</Transition>
<div
id={defaultProps.container as string}
className="tradingview-chart"

View File

@ -186,7 +186,7 @@ const TradingViewChartKline = ({ setIsFullView, isFullView }: Props) => {
useEffect(() => {
let dataFeed = spotDataFeed
const group = mangoStore.getState().group
let address = '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'
let address = ''
if (!selectedMarketName?.toLowerCase().includes('perp') && group) {
address = group!

View File

@ -22,7 +22,7 @@
},
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-v4": "^0.18.5",
"@blockworks-foundation/mango-v4": "^0.18.11",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
"@metaplex-foundation/js": "0.19.4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -10,5 +10,6 @@
"search-by": "Search By",
"search-for": "Search For",
"search-failed": "Something went wrong. Try again later",
"wallet-pk": "Wallet Address"
"wallet-pk": "Wallet Address",
"open-orders-pk": "Open Orders Address"
}

View File

@ -1,5 +1,7 @@
{
"above": "Above",
"account-size": "Account Size",
"account-size-desc": "It costs SOL to increase your account size. This can be recovered if you close your account",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
@ -7,6 +9,7 @@
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bonk": "Bonk",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
"buttons": "Buttons",
@ -21,6 +24,7 @@
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-amount": "{{type}} must be greater than {{greaterThan}} and less than {{lessThan}}",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
@ -33,26 +37,33 @@
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"increase-account-size": "Increase Account Size",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"limit-order-filled": "Limit Order Fills",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"max-all": "Max All",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notifications": "Notifications",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"perp-open-orders": "Perp Open Orders",
"perp-positions": "Perp Positions",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
@ -69,12 +80,18 @@
"solscan": "Solscan",
"sounds": "Sounds",
"spanish": "Español",
"spot-open-orders": "Spot Open Orders",
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"tooltip-perp-positions": "The number of perp markets you can have positions in. The maximum is {{max}}",
"tooltip-perp-open-orders": "The number of perp markets you can have open orders in. The maximum is {{max}}",
"tooltip-spot-open-orders": "The number of spot markets you can have open orders in. The maximum is {{max}}",
"tooltip-token-accounts": "The number of tokens you can hold in your account. The maximum is {{max}}",
"tooltip-orderbook-bandwidth-saving": "Use an off-chain service for Orderbook updates to decrease data usage by ~1000x. Disable this if open orders are not highlighted in the book correctly.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -82,9 +99,5 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",
"tooltip-orderbook-bandwidth-saving": "Use an off-chain service for Orderbook updates to decrease data usage by ~1000x. Disable this if open orders are not highlighted in the book correctly."
"trigger-key": "Trigger Key"
}

View File

@ -10,5 +10,6 @@
"search-by": "Search By",
"search-for": "Search For",
"search-failed": "Something went wrong. Try again later",
"wallet-pk": "Wallet Address"
"wallet-pk": "Wallet Address",
"open-orders-pk": "Open Orders Address"
}

View File

@ -1,5 +1,7 @@
{
"above": "Above",
"account-size": "Account Size",
"account-size-desc": "It costs SOL to increase your account size. This can be recovered if you close your account",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
@ -7,6 +9,7 @@
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bonk": "Bonk",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
"buttons": "Buttons",
@ -21,6 +24,7 @@
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-amount": "{{type}} must be greater than {{greaterThan}} and less than {{lessThan}}",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
@ -33,26 +37,33 @@
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"increase-account-size": "Increase Account Size",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"limit-order-filled": "Limit Order Fills",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"max-all": "Max All",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notifications": "Notifications",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"perp-open-orders": "Perp Open Orders",
"perp-positions": "Perp Positions",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
@ -69,12 +80,18 @@
"solscan": "Solscan",
"sounds": "Sounds",
"spanish": "Español",
"spot-open-orders": "Spot Open Orders",
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"tooltip-perp-positions": "The number of perp markets you can have positions in. The maximum is {{max}}",
"tooltip-perp-open-orders": "The number of perp markets you can have open orders in. The maximum is {{max}}",
"tooltip-spot-open-orders": "The number of spot markets you can have open orders in. The maximum is {{max}}",
"tooltip-token-accounts": "The number of tokens you can hold in your account. The maximum is {{max}}",
"tooltip-orderbook-bandwidth-saving": "Use an off-chain service for Orderbook updates to decrease data usage by ~1000x. Disable this if open orders are not highlighted in the book correctly.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -82,9 +99,5 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",
"tooltip-orderbook-bandwidth-saving": "Use an off-chain service for Orderbook updates to decrease data usage by ~1000x. Disable this if open orders are not highlighted in the book correctly."
"trigger-key": "Trigger Key"
}

View File

@ -10,5 +10,6 @@
"search-by": "Search By",
"search-for": "Search For",
"search-failed": "Something went wrong. Try again later",
"wallet-pk": "Wallet Address"
"wallet-pk": "Wallet Address",
"open-orders-pk": "Open Orders Address"
}

View File

@ -1,5 +1,7 @@
{
"above": "Above",
"account-size": "Account Size",
"account-size-desc": "It costs SOL to increase your account size. This can be recovered if you close your account",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
@ -7,6 +9,7 @@
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bonk": "Bonk",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
"buttons": "Buttons",
@ -21,6 +24,7 @@
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-amount": "{{type}} must be greater than {{greaterThan}} and less than {{lessThan}}",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
@ -33,26 +37,33 @@
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"increase-account-size": "Increase Account Size",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"limit-order-filled": "Limit Order Fills",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"max-all": "Max All",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notifications": "Notifications",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"perp-open-orders": "Perp Open Orders",
"perp-positions": "Perp Positions",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
@ -69,12 +80,18 @@
"solscan": "Solscan",
"sounds": "Sounds",
"spanish": "Español",
"spot-open-orders": "Spot Open Orders",
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"tooltip-perp-positions": "The number of perp markets you can have positions in. The maximum is {{max}}",
"tooltip-perp-open-orders": "The number of perp markets you can have open orders in. The maximum is {{max}}",
"tooltip-spot-open-orders": "The number of spot markets you can have open orders in. The maximum is {{max}}",
"tooltip-token-accounts": "The number of tokens you can hold in your account. The maximum is {{max}}",
"tooltip-orderbook-bandwidth-saving": "Use an off-chain service for Orderbook updates to decrease data usage by ~1000x. Disable this if open orders are not highlighted in the book correctly.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -82,9 +99,5 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",
"tooltip-orderbook-bandwidth-saving": "Use an off-chain service for Orderbook updates to decrease data usage by ~1000x. Disable this if open orders are not highlighted in the book correctly."
"trigger-key": "Trigger Key"
}

View File

@ -10,5 +10,6 @@
"search-by": "搜寻方式",
"search-for": "搜寻",
"search-failed": "出错了。稍后再试。",
"wallet-pk": "钱包地址"
"wallet-pk": "钱包地址",
"open-orders-pk": "Open Orders Address"
}

View File

@ -1,5 +1,7 @@
{
"above": "Above",
"account-size": "Account Size",
"account-size-desc": "It costs SOL to increase your account size. This can be recovered if you close your account",
"animations": "动画",
"at": "at",
"avocado": "酪梨",
@ -7,6 +9,7 @@
"base-key": "Base Key",
"below": "Below",
"blueberry": "蓝莓",
"bonk": "Bonk",
"bottom-left": "左下",
"bottom-right": "右下",
"buttons": "按钮",
@ -21,6 +24,7 @@
"dark": "暗",
"display": "显示",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-amount": "{{type}} must be greater than {{greaterThan}} and less than {{lessThan}}",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
@ -33,6 +37,7 @@
"high-contrast": "高对比度",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"increase-account-size": "Increase Account Size",
"key-sequence": "Key Sequence",
"language": "语言",
"light": "光",
@ -40,6 +45,7 @@
"lychee": "荔枝",
"mango": "芒果",
"mango-classic": "芒果经典",
"max-all": "Max All",
"medium": "中",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
@ -55,6 +61,8 @@
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"perp-open-orders": "Perp Open Orders",
"perp-positions": "Perp Positions",
"placing-order": "Placing Order...",
"preferred-explorer": "首选探索器",
"recent-trades": "最近交易",
@ -72,12 +80,17 @@
"solscan": "Solscan",
"sounds": "声响",
"spanish": "Español",
"spot-open-orders": "Spot Open Orders",
"swap-success": "换币/交易成功",
"swap-trade-size-selector": "换币/交易大小选择器",
"theme": "模式",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"tooltip-perp-positions": "The number of perp markets you can have positions in. The maximum is {{max}}",
"tooltip-perp-open-orders": "The number of perp markets you can have open orders in. The maximum is {{max}}",
"tooltip-spot-open-orders": "The number of spot markets you can have open orders in. The maximum is {{max}}",
"tooltip-token-accounts": "The number of tokens you can hold in your account. The maximum is {{max}}",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易图表",

View File

@ -10,5 +10,6 @@
"search-by": "搜尋方式",
"search-for": "搜尋",
"search-failed": "出錯了。稍後再試。",
"wallet-pk": "錢包地址"
"wallet-pk": "錢包地址",
"open-orders-pk": "Open Orders Address"
}

View File

@ -1,5 +1,7 @@
{
"above": "Above",
"account-size": "Account Size",
"account-size-desc": "It costs SOL to increase your account size. This can be recovered if you close your account",
"animations": "動畫",
"at": "at",
"avocado": "酪梨",
@ -7,6 +9,7 @@
"base-key": "Base Key",
"below": "Below",
"blueberry": "藍莓",
"bonk": "Bonk",
"bottom-left": "左下",
"bottom-right": "右下",
"buttons": "按鈕",
@ -21,6 +24,7 @@
"dark": "暗",
"display": "顯示",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-amount": "{{type}} must be greater than {{greaterThan}} and less than {{lessThan}}",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
@ -33,6 +37,7 @@
"high-contrast": "高對比度",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"increase-account-size": "Increase Account Size",
"key-sequence": "Key Sequence",
"language": "語言",
"light": "光",
@ -40,6 +45,7 @@
"lychee": "荔枝",
"mango": "芒果",
"mango-classic": "芒果經典",
"max-all": "Max All",
"medium": "中",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
@ -55,6 +61,8 @@
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"perp-open-orders": "Perp Open Orders",
"perp-positions": "Perp Positions",
"placing-order": "Placing Order...",
"preferred-explorer": "首選探索器",
"recent-trades": "最近交易",
@ -72,12 +80,17 @@
"solscan": "Solscan",
"sounds": "聲響",
"spanish": "Español",
"spot-open-orders": "Spot Open Orders",
"swap-success": "換幣/交易成功",
"swap-trade-size-selector": "換幣/交易大小選擇器",
"theme": "模式",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"tooltip-perp-positions": "The number of perp markets you can have positions in. The maximum is {{max}}",
"tooltip-perp-open-orders": "The number of perp markets you can have open orders in. The maximum is {{max}}",
"tooltip-spot-open-orders": "The number of spot markets you can have open orders in. The maximum is {{max}}",
"tooltip-token-accounts": "The number of tokens you can hold in your account. The maximum is {{max}}",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易圖表",

View File

@ -66,6 +66,10 @@
color: var(--text);
}
.marketStatusOpen-OPHVUfI4, html.theme-dark .marketStatusOpen-OPHVUfI4 {
color: #60BF4F;
}
/* light theme */
html .chart-page .chart-container-border {

View File

@ -59,6 +59,7 @@ import {
TourSettings,
ProfileDetails,
MangoTokenStatsItem,
ThemeData,
PositionStat,
OrderbookTooltip,
} from 'types'
@ -73,6 +74,7 @@ import {
IExecutionLineAdapter,
IOrderLineAdapter,
} from '@public/charting_library/charting_library'
import { nftThemeMeta } from 'utils/theme'
const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX')
@ -204,6 +206,7 @@ export type MangoStore = {
}
successAnimation: {
swap: boolean
theme: boolean
trade: boolean
}
swap: {
@ -219,6 +222,7 @@ export type MangoStore = {
limitPrice?: string
}
set: (x: (x: MangoStore) => void) => void
themeData: ThemeData
tokenStats: {
initialLoad: boolean
loading: boolean
@ -369,6 +373,7 @@ const mangoStore = create<MangoStore>()(
},
successAnimation: {
swap: false,
theme: false,
trade: false,
},
swap: {
@ -383,6 +388,7 @@ const mangoStore = create<MangoStore>()(
amountOut: '',
limitPrice: '',
},
themeData: nftThemeMeta.default,
tokenStats: {
initialLoad: false,
loading: true,
@ -678,15 +684,17 @@ const mangoStore = create<MangoStore>()(
const nfts = await getNFTsByOwner(ownerPk, connection)
set((state) => {
state.wallet.nfts.data = nfts
state.wallet.nfts.loading = false
})
} catch (error) {
notify({
type: 'error',
title: 'Unable to fetch nfts',
})
} finally {
set((state) => {
state.wallet.nfts.loading = false
})
}
return []
},
fetchOpenOrders: async (refetchMangoAccount = false) => {
const set = get().set

View File

@ -10,6 +10,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#fcf9cf',
Lychee: '#faebec',
Olive: '#383629',
Bonk: '#EE7C2F',
},
BKG2: {
'Mango Classic': '#282433',
@ -22,6 +23,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#f7f2ba',
Lychee: '#f4d7d9',
Olive: '#474433',
Bonk: '#DD7813',
},
BKG3: {
'Mango Classic': '#332e42',
@ -34,6 +36,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#f0eaa8',
Lychee: '#efc3c6',
Olive: '#56523e',
Bonk: '#E5B55D',
},
BKG4: {
'Mango Classic': '#3f3851',
@ -46,6 +49,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#e6df99',
Lychee: '#eaaeb2',
Olive: '#656049',
Bonk: '#DDA131',
},
FGD4: {
'Mango Classic': '#9189ae',
@ -58,6 +62,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#7b7b65',
Lychee: '#b7343a',
Olive: '#acaa8b',
Bonk: '#F3E9AA',
},
UP: {
'Mango Classic': '#89B92A',
@ -70,6 +75,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#86AE7E',
Lychee: '#2d805e',
Olive: '#4eaa27',
Bonk: '#FAE34C',
},
ACTIVE: {
'Mango Classic': '#f1c84b',
@ -82,6 +88,7 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#606afb',
Lychee: '#040e9f',
Olive: '#e7dc83',
Bonk: '#332910',
},
DOWN: {
'Mango Classic': '#F84638',
@ -94,5 +101,6 @@ export const COLORS: Record<string, Record<string, string>> = {
Banana: '#BE6A6A',
Lychee: '#c5303a',
Olive: '#ee392f',
Bonk: '#C22E30',
},
}

View File

@ -364,10 +364,39 @@ th {
--warning: theme('colors.avocado-theme.warning');
}
[data-theme='Bonk'] {
--active: theme('colors.bonk-theme.active.DEFAULT');
--active-dark: theme('colors.bonk-theme.active.dark');
--down: theme('colors.bonk-theme.down.DEFAULT');
--down-dark: theme('colors.bonk-theme.down.dark');
--down-muted: theme('colors.bonk-theme.down.muted');
--up: theme('colors.bonk-theme.up.DEFAULT');
--up-dark: theme('colors.bonk-theme.up.dark');
--up-muted: theme('colors.bonk-theme.up.muted');
--link: theme('colors.bonk-theme.link.DEFAULT');
--link-hover: theme('colors.bonk-theme.link.hover');
--bkg-1: theme('colors.bonk-theme.bkg-1');
--bkg-2: theme('colors.bonk-theme.bkg-2');
--bkg-3: theme('colors.bonk-theme.bkg-3');
--bkg-4: theme('colors.bonk-theme.bkg-4');
--fgd-1: theme('colors.bonk-theme.fgd-1');
--fgd-2: theme('colors.bonk-theme.fgd-2');
--fgd-3: theme('colors.bonk-theme.fgd-3');
--fgd-4: theme('colors.bonk-theme.fgd-4');
--button: theme('colors.bonk-theme.button.DEFAULT');
--button-hover: theme('colors.bonk-theme.button.hover');
--input-bkg: theme('colors.bonk-theme.input.bkg');
--input-border: theme('colors.bonk-theme.input.border');
--input-border-hover: theme('colors.bonk-theme.input.borderDark');
--error: theme('colors.bonk-theme.error');
--success: theme('colors.bonk-theme.success');
--warning: theme('colors.bonk-theme.warning');
}
/* Base */
body {
@apply font-body text-sm tracking-wider;
@apply font-body text-sm font-medium tracking-wider;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@ -626,6 +655,42 @@ input[type='range']::-webkit-slider-runnable-track {
@apply absolute h-screen w-full;
}
/* raised button */
.raised-button {
@apply relative flex items-center justify-center bg-th-button text-th-fgd-1 transition-none;
box-shadow: 0 6px var(--button-hover);
}
.raised-button:hover {
background-color: var(--button);
box-shadow: 0 4px var(--button-hover);
top: 2px;
}
.raised-button:active {
box-shadow: 0 0 var(--button-hover);
top: 6px;
}
/* raised buy button */
.raised-buy-button {
@apply relative flex items-center justify-center bg-th-up text-th-active transition-none;
box-shadow: 0 6px var(--up-dark);
}
.raised-buy-button:hover {
background-color: var(--up) !important;
box-shadow: 0 4px var(--up-dark);
top: 2px;
}
.raised-buy-button:active {
box-shadow: 0 0 var(--up-dark);
top: 6px;
}
.pagination {
margin-top: 15px;
margin-bottom: 15px;

View File

@ -392,6 +392,43 @@ module.exports = {
'fgd-3': 'hsl(357, 46%, 41%)',
'fgd-4': 'hsl(357, 41%, 51%)',
},
'bonk-theme': {
active: {
DEFAULT: '#000',
dark: '#000',
},
button: {
DEFAULT: 'hsl(12, 50%, 45%)',
hover: 'hsl(12, 50%, 40%)',
},
input: {
bkg: 'hsl(30, 84%, 47%)',
border: 'hsl(30, 84%, 33%)',
borderDark: 'hsl(30, 84%, 28%)',
},
link: { DEFAULT: 'hsl(45, 86%, 62%)', hover: 'hsl(45, 86%, 57%)' },
down: {
DEFAULT: 'hsl(359, 62%, 47%)',
dark: 'hsl(359, 62%, 42%)',
muted: 'hsl(359, 22%, 42%)',
},
up: {
DEFAULT: 'hsl(52, 95%, 64%)',
dark: 'hsl(52, 95%, 44%)',
muted: 'hsl(52, 55%, 54%)',
},
error: 'hsl(359, 62%, 47%)',
success: 'hsl(52, 95%, 64%)',
warning: 'hsl(24, 100%, 43%)',
'bkg-1': 'hsl(24, 85%, 56%)',
'bkg-2': 'hsl(30, 84%, 47%)',
'bkg-3': 'hsl(39, 72%, 63%)',
'bkg-4': 'hsl(39, 72%, 68%)',
'fgd-1': 'hsl(52, 93%, 99%)',
'fgd-2': 'hsl(52, 85%, 93%)',
'fgd-3': 'hsl(52, 80%, 87%)',
'fgd-4': 'hsl(52, 75%, 81%)',
},
'th-bkg-1': 'var(--bkg-1)',
'th-bkg-2': 'var(--bkg-2)',
'th-bkg-3': 'var(--bkg-3)',

View File

@ -300,6 +300,7 @@ export interface SwapHistoryItem {
export interface NFT {
address: string
collectionAddress?: string
image: string
name: string
mint: string
@ -392,6 +393,24 @@ export interface TradeForm {
reduceOnly: boolean
}
export interface ThemeData {
buttonStyle: 'flat' | 'raised'
fonts: {
body: any
display: any
mono: any
}
logoPath: string
platformName: string
rainAnimationImagePath: string
sideImagePath: string
sideTilePath: string
topTilePath: string
tvChartTheme: 'Light' | 'Dark'
tvImagePath: string
useGradientBg: boolean
}
export interface MangoError extends Error {
txid: string
}

View File

@ -1,4 +1,12 @@
import localFont from 'next/font/local'
import { Nunito } from 'next/font/google'
// this font should be used as the mono variant for all themes
export const ttCommonsMono = localFont({
src: '../fonts/TT_Commons_Pro_Mono_Medium.woff2',
variable: '--font-mono',
})
export const ttCommons = localFont({
src: [
@ -26,7 +34,15 @@ export const ttCommonsExpanded = localFont({
variable: '--font-display',
})
export const ttCommonsMono = localFont({
src: '../fonts/TT_Commons_Pro_Mono_Medium.woff2',
variable: '--font-mono',
// bonk skin
export const nunitoDisplay = Nunito({
weight: '900',
subsets: ['latin'],
variable: '--font-display',
})
export const nunitoBody = Nunito({
subsets: ['latin'],
variable: '--font-body',
})

View File

@ -1,3 +1,12 @@
import { ThemeData } from 'types'
import {
nunitoBody,
nunitoDisplay,
ttCommons,
ttCommonsExpanded,
ttCommonsMono,
} from './fonts'
export const breakpoints = {
sm: 640,
// => @media (min-width: 640px) { ... }
@ -17,3 +26,40 @@ export const breakpoints = {
'3xl': 1792,
// => @media (min-width: 1792px) { ... }
}
type NftThemeMeta = {
[key: string]: ThemeData
}
export const nftThemeMeta: NftThemeMeta = {
default: {
buttonStyle: 'flat',
fonts: { body: ttCommons, display: ttCommonsExpanded, mono: ttCommonsMono },
logoPath: '/logos/logo-mark.svg',
platformName: 'Mango',
rainAnimationImagePath: '',
sideImagePath: '',
sideTilePath: '',
topTilePath: '',
tvChartTheme: 'Dark',
tvImagePath: '',
useGradientBg: false,
},
Bonk: {
buttonStyle: 'raised',
fonts: { body: nunitoBody, display: nunitoDisplay, mono: ttCommonsMono },
logoPath: '/images/themes/bonk/bonk-logo.png',
platformName: 'Bongo',
rainAnimationImagePath: '/images/themes/bonk/bonk-animation-logo.png',
sideImagePath: '/images/themes/bonk/sidenav-image.png',
sideTilePath: '/images/themes/bonk/bonk-tile.png',
topTilePath: '/images/themes/bonk/bonk-tile.png',
tvChartTheme: 'Light',
tvImagePath: '/images/themes/bonk/tv-chart-image.png',
useGradientBg: true,
},
}
export const CUSTOM_SKINS: { [key: string]: string } = {
bonk: '6FUYsgvSPiLsMpKZqLWswkw7j4juudZyVopU6RYKLkQ3',
}

View File

@ -90,6 +90,7 @@ const enhanceNFT = (nft: NftWithATA) => {
image: nft.json?.image || '',
name: nft.json?.name || '',
address: nft.metadataAddress.toBase58(),
collectionAddress: nft.collection?.address.toBase58(),
mint: nft.mint.address.toBase58(),
tokenAccount: nft.tokenAccountAddress?.toBase58() || '',
}

View File

@ -26,10 +26,10 @@
dependencies:
ws "^8.13.0"
"@blockworks-foundation/mango-v4@^0.18.5":
version "0.18.5"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.5.tgz#dec488ea957b78dabe61b05049e6f65ebe45ae8f"
integrity sha512-SF4qboOFAQ+pWmzDnNdboDQN6DTLkVR82Qm9SSkTiQRUfHi9gqEaF6QlJkjzBcECiqk6FB9Xg9rOZb4OKAssNw==
"@blockworks-foundation/mango-v4@^0.18.11":
version "0.18.11"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.18.11.tgz#3c78a6e073be8eb551d6aa5d32406ba7570bcd6d"
integrity sha512-khWsbsZxBeVQ6Fa3NDoFSaLe/tMcPLyAVl1l9alZyrOmzCb14rCnZz2Bsrhi3amuw0ZYN0aYMUtSpLavpoatQg==
dependencies:
"@coral-xyz/anchor" "^0.27.0"
"@project-serum/serum" "0.13.65"