Merge branch 'main' into chinese-localization

This commit is contained in:
rjpeterson 2023-09-09 21:47:16 -07:00
commit f266bfbec4
134 changed files with 3277 additions and 1506 deletions

View File

@ -1,57 +1,78 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
This repo contains the Next.js app for the Mango v4 user interface.
## Dependency Management
## ⚡️ Quickstart
When updating dependencies, there are various files that must be kept up-to-date. Newly added, or updated dependencies can introduce unwanted/malicious scripts that can introduce risks for users and/or developers. The `lavamoat allow-scripts` feature allows us to deny by default, but adds some additional steps to the usual workflow.
To get started, follow these steps:
`yarn.lock`:
- Instead of running `yarn` or `yarn install`, run `yarn setup` to ensure the `yarn.lock` file is in sync and that dependency scripts are run according to the `allowScripts` policy (set in `packages.json`)
- If `lavamoat` detects new scripts that are not explicitely allowed/denied, it'll throw and error with details (see below)
- Running `yarn setup` will also dedupe the `yarn.lock` file to reduce the dependency tree. Note CI will fail if there are dupes in `yarn.lock`!
The `allowScripts` configuration in `package.json`:
- There are two ways to configure script policies:
1. Update the allow-scripts section manually by adding the missing package in the `allowScripts` section in `package.json`
2. Run `yarn allow-scripts auto` to update the `allowScripts` configuration automatically
- Review each new package to determine whether the install script needs to run or not, testing if necessary.
- Use `npx can-i-ignore-scripts` to help assessing whether scripts are needed
## Getting Started
First, run the development server:
1. **Clone the repo:** Begin by cloning the repository using the command:
```bash
git clone git@github.com:blockworks-foundation/mango-v4-ui.git
```
2. **Install Dependencies:** Move into the directory and install the dependencies:
```bash
cd mango-v4-ui
yarn setup
```
3. **Run the app:**
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
4. Browse to http://localhost:3000
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
## ⌨️ Contributor's Guide
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
### Code quality
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
- Avoid duplication
- Consider performance (use useMemo and useCallback where appropriate)
- Create logical components and give them descriptive names
- Destructure objects and arrays
- Define constants for event functions unless they are very simple e.g. a single state update
- Create hooks for shared logic
- Add translation keys in alphabetical order
## Learn More
### Branching
To learn more about Next.js, take a look at the following resources:
Prefix your branches with your Git username and give them concise and descriptive names
e.g. username/branch-name
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
### Commits
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
Add commits for each self-contained change and give your commits clear messages that describe the change. Smaller commits that encompass a specific change are preferred over large commits with many changes
## Deploy on Vercel
### PRs
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
All PRs should have a meaningful name and include a description of what the changes are.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
If there are visual changes, include screenshots in the description.
## Creating Color Themes
If the PR is unfinished include a "TODO" section with work not yet completed. If there are known issues/bugs include a section outlining what they are.
#### Drafts
Opening draft PRs is a good way for other contributors to know a feature is being worked on. This is most useful for larger/complex features and is not a requirement. When your feature is at a point where you'd like to gather feedback or it's close to completion open a draft PR and share the preview link in the relevant Discord channel
Prefix "WIP:" to your draft PR name
### Reviews
When your changes are finished, who you request review from depends on the type of changes.
For complex changes e.g. new transactions, large features, lots of client or backend interactions you should at a minimum include @tlrsssss in your review
For changes that affect visual elements of the app (including text changes), request a review from @saml33 at a minimum
If you're unsure, request a review from @tlrssss and @saml33
If your work involves other parts of the stack (backend, client, etc.) request a review from the relevant person in that area
## 🎨 Creating Color Themes
1. Copy one of the other color themes in [tailwind.config.js](https://github.com/blockworks-foundation/mango-v4-ui/blob/main/tailwind.config.js) (starting line 25)
2. Modify the colors. For the variables bkg-\* and fgd-\* pick a base color for bkg-1 and fgd-1 then adjust the lightness for 2-4. Use this same process to create dark/hover variations for the colors that have these properties. The base color can be anything that works for your theme.

View File

@ -8,7 +8,6 @@ import {
closeSocket,
// isOpen,
subscribeOnStream as subscribeOnSpotStream,
unsubscribeFromStream,
} from './birdeye/streaming'
import {
closeSocket as closePerpSocket,
@ -116,11 +115,11 @@ export const queryPerpBars = async (
},
): Promise<Bar[]> => {
const { from, to } = periodParams
if (tokenAddress === 'Loading') return []
const urlParameters = {
'perp-market': tokenAddress,
resolution: parsePerpResolution(resolution),
start_datetime: new Date(from * 1000).toISOString(),
start_datetime: new Date((from - 3_000_000) * 1000).toISOString(),
end_datetime: new Date(to * 1000).toISOString(),
}
@ -135,21 +134,19 @@ export const queryPerpBars = async (
let previousBar: Bar | undefined = undefined
for (const bar of data) {
const timestamp = new Date(bar.candle_start).getTime()
if (timestamp >= from * 1000 && timestamp < to * 1000) {
bars = [
...bars,
{
time: timestamp,
low: bar.low,
high: bar.high,
open: previousBar ? previousBar.close : bar.open,
close: bar.close,
volume: bar.volume,
timestamp,
},
]
previousBar = bar
}
bars = [
...bars,
{
time: timestamp,
low: bar.low,
high: bar.high,
open: previousBar ? previousBar.close : bar.open,
close: bar.close,
volume: bar.volume,
timestamp,
},
]
previousBar = bar
}
return bars
}

View File

@ -114,7 +114,7 @@ export const queryBars = async (
return bars
}
export default {
const datafeed = {
onReady: (callback: (configuration: DatafeedConfiguration) => void) => {
setTimeout(() => callback(configurationData as any))
},
@ -251,3 +251,5 @@ export default {
name: 'mngo',
isSocketOpen: isOpen,
}
export default datafeed

View File

@ -1,13 +1,15 @@
import { useEffect } from 'react'
import { useCallback, useEffect } from 'react'
import mangoStore from '@store/mangoStore'
import { Keypair, PublicKey } from '@solana/web3.js'
import { useRouter } from 'next/router'
import useMangoAccount from 'hooks/useMangoAccount'
import useInterval from './shared/useInterval'
import { LAST_WALLET_NAME, SECONDS } from 'utils/constants'
import { LAST_WALLET_NAME, PRIORITY_FEE_KEY, SECONDS } from 'utils/constants'
import useNetworkSpeed from 'hooks/useNetworkSpeed'
import { useWallet } from '@solana/wallet-adapter-react'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { DEFAULT_PRIORITY_FEE_LEVEL } from './settings/RpcSettings'
import { useHiddenMangoAccounts } from 'hooks/useHiddenMangoAccounts'
const set = mangoStore.getState().set
const actions = mangoStore.getState().actions
@ -22,6 +24,21 @@ const HydrateStore = () => {
const [, setLastWalletName] = useLocalStorageState(LAST_WALLET_NAME, '')
const handleWindowResize = useCallback(() => {
if (typeof window !== 'undefined') {
set((s) => {
s.window.width = window.innerWidth
s.window.height = window.innerHeight
})
}
}, [])
// store the window width and height on resize
useEffect(() => {
handleWindowResize()
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
}, [handleWindowResize])
useEffect(() => {
if (wallet?.adapter) {
setLastWalletName(wallet?.adapter.name)
@ -41,7 +58,7 @@ const HydrateStore = () => {
() => {
actions.fetchGroup()
},
(slowNetwork ? 40 : 20) * SECONDS,
(slowNetwork ? 60 : 30) * SECONDS,
)
// refetches open orders every 30 seconds
@ -72,6 +89,20 @@ const HydrateStore = () => {
(slowNetwork ? 60 : 20) * SECONDS,
)
// estimate the priority fee every 30 seconds
useInterval(
async () => {
if (mangoAccountAddress) {
const priorityFeeMultiplier = Number(
localStorage.getItem(PRIORITY_FEE_KEY) ??
DEFAULT_PRIORITY_FEE_LEVEL.value,
)
actions.estimatePriorityFee(priorityFeeMultiplier)
}
},
(slowNetwork ? 60 : 10) * SECONDS,
)
// The websocket library solana/web3.js uses closes its websocket connection when the subscription list
// is empty after opening its first time, preventing subsequent subscriptions from receiving responses.
// This is a hack to prevent the list from every getting empty
@ -128,6 +159,7 @@ const ReadOnlyMangoAccount = () => {
const router = useRouter()
const groupLoaded = mangoStore((s) => s.groupLoaded)
const ma = router.query?.address
const { hiddenAccounts } = useHiddenMangoAccounts()
useEffect(() => {
if (!groupLoaded) return
@ -136,7 +168,7 @@ const ReadOnlyMangoAccount = () => {
async function loadUnownedMangoAccount() {
try {
if (!ma || !group) return
if (!ma || !group || hiddenAccounts?.includes(ma as string)) return
const client = mangoStore.getState().client
const pk = new PublicKey(ma)

View File

@ -1,7 +1,4 @@
import {
ArrowDownRightIcon,
ExclamationCircleIcon,
} from '@heroicons/react/20/solid'
import { ArrowDownRightIcon } from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import Decimal from 'decimal.js'
import { useTranslation } from 'next-i18next'
@ -19,7 +16,6 @@ import { EnterBottomExitBottom, FadeInFadeOut } from './shared/Transitions'
import { withValueLimit } from './swap/MarketSwapForm'
import MaxAmountButton from '@components/shared/MaxAmountButton'
import HealthImpactTokenChange from '@components/HealthImpactTokenChange'
import { walletBalanceForToken } from './DepositForm'
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
import useMangoAccount from 'hooks/useMangoAccount'
import {
@ -58,13 +54,6 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
}, [selectedToken])
const { connected, publicKey } = useWallet()
const walletTokens = mangoStore((s) => s.wallet.tokens)
const walletBalance = useMemo(() => {
return selectedToken
? walletBalanceForToken(walletTokens, selectedToken)
: { maxAmount: 0, maxDecimals: 6 }
}, [walletTokens, selectedToken])
const borrowAmount = useMemo(() => {
if (!mangoAccount || !bank) return new Decimal(0)
@ -163,8 +152,6 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
}
}, [token, banks, selectedToken])
const showInsufficientBalance = walletBalance.maxAmount < Number(inputAmount)
const outstandingAmount = borrowAmount.toNumber() - parseFloat(inputAmount)
const isDeposit = parseFloat(inputAmount) > borrowAmount.toNumber()
@ -291,18 +278,11 @@ function RepayForm({ onSuccess, token }: RepayFormProps) {
<Button
onClick={() => handleDeposit(inputAmount)}
className="flex w-full items-center justify-center"
disabled={!inputAmount || showInsufficientBalance}
disabled={!inputAmount}
size="large"
>
{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>
) : (
<div className="flex items-center">
<ArrowDownRightIcon className="mr-2 h-5 w-5" />

View File

@ -37,6 +37,8 @@ import { NFT } from 'types'
import { useViewport } from 'hooks/useViewport'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { SIDEBAR_COLLAPSE_KEY } from 'utils/constants'
import { createTransferInstruction } from '@solana/spl-token'
import { PublicKey, TransactionInstruction } from '@solana/web3.js'
const SideNav = ({ collapsed }: { collapsed: boolean }) => {
const { t } = useTranslation(['common', 'search'])
@ -46,6 +48,10 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
const themeData = mangoStore((s) => s.themeData)
const nfts = mangoStore((s) => s.wallet.nfts.data)
const { mangoAccount } = useMangoAccount()
const setPrependedGlobalAdditionalInstructions = mangoStore(
(s) => s.actions.setPrependedGlobalAdditionalInstructions,
)
const router = useRouter()
const { pathname } = router
@ -88,14 +94,39 @@ const SideNav = ({ collapsed }: { collapsed: boolean }) => {
return mangoNfts
}, [nfts])
//mark transactions with used nfts
useEffect(() => {
let newInstruction: TransactionInstruction[] = []
if (mangoNfts.length && theme) {
const collectionAddress = CUSTOM_SKINS[theme.toLowerCase()]
const usedNft = mangoNfts.find(
(nft) => nft.collectionAddress === collectionAddress,
)
if (usedNft && publicKey && collectionAddress) {
newInstruction = [
createTransferInstruction(
new PublicKey(usedNft.tokenAccount),
new PublicKey(usedNft.tokenAccount),
publicKey,
1,
),
]
}
}
setPrependedGlobalAdditionalInstructions(newInstruction)
}, [mangoNfts, theme, themeData])
// 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
const attributes = mangoNfts.find(
(nft) => nft.collectionAddress === collectionAddress,
)?.json?.attributes
const sidebarImageUrl = attributes
? attributes[0].value || themeData.sideImagePath
: ''
return sidebarImageUrl
}
return themeData.sideImagePath

View File

@ -21,7 +21,6 @@ import useOnlineStatus from 'hooks/useOnlineStatus'
import { abbreviateAddress } from 'utils/formatting'
import DepositWithdrawModal from './modals/DepositWithdrawModal'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import AccountsButton from './AccountsButton'
import useUnownedAccount from 'hooks/useUnownedAccount'
import NotificationsButton from './notifications/NotificationsButton'
@ -50,8 +49,7 @@ const TopBar = () => {
const router = useRouter()
const { query } = router
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { isMobile } = useViewport()
const { isUnownedAccount } = useUnownedAccount()
const showUserSetup = mangoStore((s) => s.showUserSetup)

View File

@ -4,6 +4,8 @@ import {
ArrowDownRightIcon,
ArrowUpLeftIcon,
DocumentDuplicateIcon,
EyeIcon,
EyeSlashIcon,
PencilIcon,
SquaresPlusIcon,
TrashIcon,
@ -26,9 +28,10 @@ import { Popover, Transition } from '@headlessui/react'
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'
import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { PRIVACY_MODE } from 'utils/constants'
export const handleCopyAddress = (
mangoAccount: MangoAccount,
@ -51,10 +54,10 @@ const AccountActions = () => {
const [showDelegateModal, setShowDelegateModal] = useState(false)
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
const [showAccountSizeModal, setShowAccountSizeModal] = useState(false)
const [privacyMode, setPrivacyMode] = useLocalStorageState(PRIVACY_MODE)
const { connected } = useWallet()
const { isDelegatedAccount, isUnownedAccount } = useUnownedAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { isMobile } = useViewport()
const { isAccountFull } = useMangoAccountAccounts()
const handleBorrowModal = () => {
@ -169,6 +172,26 @@ const AccountActions = () => {
<TrashIcon className="h-4 w-4" />
<span className="ml-2">{t('close-account')}</span>
</ActionsLinkButton>
<ActionsLinkButton
mangoAccount={mangoAccount!}
onClick={() => setPrivacyMode(!privacyMode)}
>
{privacyMode ? (
<>
<EyeIcon className="h-4 w-4" />
<span className="ml-2">
{t('settings:privacy-disable')}
</span>
</>
) : (
<>
<EyeSlashIcon className="h-4 w-4" />
<span className="ml-2">
{t('settings:privacy-enable')}
</span>
</>
)}
</ActionsLinkButton>
</Popover.Panel>
</Transition>
</>

View File

@ -8,7 +8,6 @@ import useMangoAccount from '../../hooks/useMangoAccount'
import useLocalStorageState from 'hooks/useLocalStorageState'
import dayjs from 'dayjs'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import useMangoGroup from 'hooks/useMangoGroup'
import PnlHistoryModal from '@components/modals/PnlHistoryModal'
import AssetsLiabilities from './AssetsLiabilities'
@ -38,8 +37,7 @@ const AccountPage = () => {
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const [showPnlHistory, setShowPnlHistory] = useState<boolean>(false)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { isTablet } = useViewport()
const [activeTab, setActiveTab] = useLocalStorageState(
'accountHeroKey-0.1',
'account-value',
@ -127,7 +125,7 @@ const AccountPage = () => {
/>
) : null}
{activeTab === 'account:assets-liabilities' ? (
<AssetsLiabilities isMobile={isMobile} />
<AssetsLiabilities isMobile={isTablet} />
) : null}
</div>
</div>

View File

@ -4,7 +4,6 @@ import TokenList from '../TokenList'
import UnsettledTrades from '@components/trade/UnsettledTrades'
import { useUnsettledSpotBalances } from 'hooks/useUnsettledSpotBalances'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import useUnsettledPerpPositions from 'hooks/useUnsettledPerpPositions'
import mangoStore from '@store/mangoStore'
import PerpPositions from '@components/trade/PerpPositions'
@ -14,17 +13,16 @@ import HistoryTabs from './HistoryTabs'
import ManualRefresh from '@components/shared/ManualRefresh'
import useMangoAccount from 'hooks/useMangoAccount'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
import SwapOrders from '@components/swap/SwapOrders'
import SwapTriggerOrders from '@components/swap/SwapTriggerOrders'
const AccountTabs = () => {
const [activeTab, setActiveTab] = useState('balances')
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const { isMobile, isTablet } = useViewport()
const unsettledSpotBalances = useUnsettledSpotBalances()
const unsettledPerpPositions = useUnsettledPerpPositions()
const openPerpPositions = useOpenPerpPositions()
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
const isMobile = width ? width < breakpoints.lg : false
const { data: isWhiteListed } = useIsWhiteListed()
const tabsWithCount: [string, number][] = useMemo(() => {
@ -63,12 +61,12 @@ const AccountTabs = () => {
onChange={(v) => setActiveTab(v)}
values={tabsWithCount}
showBorders
fillWidth={isMobile}
fillWidth={isMobile || isTablet}
/>
<ManualRefresh
classNames="fixed bottom-16 right-4 lg:relative lg:bottom-0 md:bottom-6 md:right-6 z-10 shadow-lg lg:shadow-none bg-th-bkg-3 lg:bg-transparent"
hideBg={isMobile}
size={isMobile ? 'large' : 'small'}
classNames="fixed bottom-16 right-4 md:relative md:px-2 lg:px-0 lg:pr-6 md:bottom-0 md:right-0 z-10 shadow-lg md:shadow-none bg-th-bkg-3 md:bg-transparent"
hideBg={isMobile || isTablet}
size={isTablet ? 'large' : 'small'}
/>
</div>
<TabContent activeTab={activeTab} />
@ -87,7 +85,7 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
case 'trade:orders':
return <OpenOrders />
case 'trade:trigger-orders':
return <SwapOrders />
return <SwapTriggerOrders />
case 'trade:unsettled':
return (
<UnsettledTrades

View File

@ -17,7 +17,6 @@ import { PerformanceDataItem } from 'types'
import { useMemo, useState } from 'react'
import { useTranslation } from 'next-i18next'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { ViewToShow } from './AccountPage'
import useAccountPerformanceData from 'hooks/useAccountPerformanceData'
import useThemeWrapper from 'hooks/useThemeWrapper'
@ -42,9 +41,8 @@ const AccountValue = ({
ANIMATION_SETTINGS_KEY,
INITIAL_ANIMATION_SETTINGS,
)
const { width } = useViewport()
const { isTablet } = useViewport()
const { performanceLoading: loading } = useAccountPerformanceData()
const isMobile = width ? width < breakpoints.md : false
const accountValueChange = useMemo(() => {
if (!accountValue || !rollingDailyData.length) return 0
@ -118,7 +116,7 @@ const AccountValue = ({
<Transition
appear={true}
className="absolute bottom-2 right-2"
show={showExpandChart || isMobile}
show={showExpandChart || isTablet}
enter="transition ease-in duration-300"
enterFrom="opacity-0 scale-75"
enterTo="opacity-100 scale-100"

View File

@ -1,7 +1,7 @@
import { ChangeEvent, useState } from 'react'
import { useTranslation } from 'next-i18next'
import mangoStore from '@store/mangoStore'
import { notify } from '../../utils/notifications'
import { createSolanaMessage, notify } from '../../utils/notifications'
import Button, { IconButton } from '../shared/Button'
import BounceLoader from '../shared/BounceLoader'
import Input from '../forms/Input'
@ -12,6 +12,9 @@ import { MangoAccount } from '@blockworks-foundation/mango-v4'
import { ArrowLeftIcon } from '@heroicons/react/20/solid'
import useSolBalance from 'hooks/useSolBalance'
import { isMangoError } from 'types'
import { MAX_ACCOUNTS } from 'utils/constants'
import Switch from '@components/forms/Switch'
import NotificationCookieStore from '@store/notificationCookieStore'
const getNextAccountNumber = (accounts: MangoAccount[]): number => {
if (accounts.length > 1) {
@ -27,26 +30,27 @@ const getNextAccountNumber = (accounts: MangoAccount[]): number => {
}
const CreateAccountForm = ({
isFirstAccount,
customClose,
handleBack,
}: {
isFirstAccount?: boolean
customClose?: () => void
handleBack?: () => void
}) => {
const { t } = useTranslation('common')
const [loading, setLoading] = useState(false)
const [name, setName] = useState('')
const { wallet } = useWallet()
const [signToNotifications, setSignToNotifications] = useState(true)
//whole context needed to sign msgs
const walletContext = useWallet()
const { maxSolDeposit } = useSolBalance()
const setCookie = NotificationCookieStore((s) => s.setCookie)
const handleNewAccount = async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccounts = mangoStore.getState().mangoAccounts
const set = mangoStore.getState().set
if (!group || !wallet) return
if (!group || !walletContext.wallet) return
setLoading(true)
try {
const newAccountNum = getNextAccountNumber(mangoAccounts)
@ -54,10 +58,16 @@ const CreateAccountForm = ({
group,
newAccountNum,
name || `Account ${newAccountNum + 1}`,
10, // tokenCount
parseInt(MAX_ACCOUNTS.tokenAccounts), // tokens
parseInt(MAX_ACCOUNTS.spotOpenOrders), // serum3
parseInt(MAX_ACCOUNTS.perpAccounts), // perps
parseInt(MAX_ACCOUNTS.perpOpenOrders), // perp Oo
)
if (tx) {
const pk = wallet.adapter.publicKey
if (signToNotifications) {
createSolanaMessage(walletContext, setCookie)
}
const pk = walletContext.wallet.adapter.publicKey
const mangoAccounts = await client.getMangoAccountsForOwner(group, pk!)
const reloadedMangoAccounts = await Promise.all(
mangoAccounts.map((ma) => ma.reloadSerum3OpenOrders(client)),
@ -99,7 +109,7 @@ const CreateAccountForm = ({
</div>
) : (
<div className="flex h-full flex-col justify-between">
<div className="pb-4">
<div className="pb-3">
<div className="flex items-center">
{handleBack ? (
<IconButton className="mr-3" onClick={handleBack} size="small">
@ -109,11 +119,7 @@ const CreateAccountForm = ({
<h2 className="w-full text-center">{t('create-account')}</h2>
{handleBack ? <div className="h-5 w-5" /> : null}
</div>
{isFirstAccount ? (
<p className="mt-1 text-center">
You need a Mango Account to get started.
</p>
) : null}
<p className="mt-1 text-center">{t('insufficient-sol')}</p>
<div className="pt-4">
<Label optional text={t('account-name')} />
</div>
@ -128,21 +134,29 @@ const CreateAccountForm = ({
}
maxLength={30}
/>
</div>
<div className="space-y-4">
<InlineNotification type="info" desc={t('insufficient-sol')} />
<Button
className="w-full"
disabled={maxSolDeposit <= 0}
onClick={handleNewAccount}
size="large"
>
{t('create-account')}
</Button>
<div className="my-3 flex items-center justify-between rounded-md border border-th-bkg-3 px-3 py-2">
<div>
<p className="text-th-fgd-2">{t('enable-notifications')}</p>
<p className="text-xs">{t('asked-sign-transaction')}</p>
</div>
<Switch
className="text-th-fgd-3"
checked={signToNotifications}
onChange={(checked) => setSignToNotifications(checked)}
/>
</div>
{maxSolDeposit <= 0 ? (
<InlineNotification type="error" desc={t('deposit-more-sol')} />
) : null}
</div>
<Button
className="mt-6 w-full"
disabled={maxSolDeposit <= 0}
onClick={handleNewAccount}
size="large"
>
{t('create-account')}
</Button>
</div>
)
}

View File

@ -11,7 +11,6 @@ import { COLORS } from 'styles/colors'
import { useMemo } from 'react'
import { formatCurrencyValue } from 'utils/numbers'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { HealthContribution } from 'types'
import useThemeWrapper from 'hooks/useThemeWrapper'
@ -26,8 +25,7 @@ const HealthContributionsChart = ({
}) => {
const { t } = useTranslation(['common', 'account'])
const { theme } = useThemeWrapper()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { isMobile } = useViewport()
const handleClick = (index: number) => {
setActiveIndex(index)

View File

@ -0,0 +1,57 @@
import {
toggleMangoAccountHidden,
useMangoAccountHidden,
} from 'hooks/useMangoAccountHidden'
import { useWallet } from '@solana/wallet-adapter-react'
import useMangoAccount from 'hooks/useMangoAccount'
import Switch from '@components/forms/Switch'
import { useState } from 'react'
import Loading from '@components/shared/Loading'
import Tooltip from '@components/shared/Tooltip'
import { useTranslation } from 'react-i18next'
const HideMangoAccount = () => {
const { t } = useTranslation('settings')
const { publicKey, signMessage } = useWallet()
const { mangoAccountPk } = useMangoAccount()
const { accountHidden, refetch } = useMangoAccountHidden()
const [signingForHide, setSigningForHide] = useState(false)
const handleHideMangoAccount = async () => {
if (!publicKey || !mangoAccountPk || !signMessage) return
setSigningForHide(true)
try {
await toggleMangoAccountHidden(
mangoAccountPk,
publicKey,
!(accountHidden ?? false),
signMessage,
)
refetch()
setSigningForHide(false)
} catch (e) {
console.error('Error toggling account visibility', e)
setSigningForHide(false)
}
}
return (
<>
<div className="flex items-center justify-between border-y border-th-bkg-3 p-4">
<Tooltip content={t('settings:tooltip-private-account')}>
<p className="tooltip-underline">{t('settings:private-account')}</p>
</Tooltip>
{signingForHide ? (
<Loading />
) : (
<Switch
checked={accountHidden ?? false}
onChange={handleHideMangoAccount}
/>
)}
</div>
</>
)
}
export default HideMangoAccount

View File

@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next'
import useMangoGroup from 'hooks/useMangoGroup'
import useMangoAccount from 'hooks/useMangoAccount'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { MouseEventHandler } from 'react'
import MarketLogos from '@components/trade/MarketLogos'
import { HealthContribution } from 'types'
@ -28,8 +27,7 @@ const MarketsHealthTable = ({
const { t } = useTranslation(['common', 'account', 'trade'])
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { isMobile } = useViewport()
return group && mangoAccount ? (
!isMobile ? (
<Table>

View File

@ -8,7 +8,6 @@ import { useTranslation } from 'next-i18next'
import useMangoGroup from 'hooks/useMangoGroup'
import useMangoAccount from 'hooks/useMangoAccount'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import { MouseEventHandler } from 'react'
import { ContributionDetails, HealthContribution } from 'types'
@ -28,8 +27,7 @@ const TokensHealthTable = ({
const { t } = useTranslation(['common', 'account', 'trade'])
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
const { isMobile } = useViewport()
return group && mangoAccount ? (
!isMobile ? (

View File

@ -22,11 +22,11 @@ const ButtonGroup = <T extends Values>({
large,
}: ButtonGroupProps<T>) => {
return (
<div className={`rounded-md bg-th-bkg-2 ${disabled ? 'opacity-50' : ''}`}>
<div className={`rounded-md bg-th-bkg-3 ${disabled ? 'opacity-50' : ''}`}>
<div className="relative flex">
{activeValue && values.includes(activeValue) ? (
<div
className={`absolute left-0 top-0 h-full transform rounded-md bg-th-bkg-3`}
className={`absolute left-0 top-0 h-full transform rounded-md bg-th-bkg-4`}
style={{
transform: `translateX(${
values.findIndex((v) => v === activeValue) * 100
@ -37,7 +37,7 @@ const ButtonGroup = <T extends Values>({
) : null}
{values.map((v, i) => (
<button
className={`${className} relative w-1/2 cursor-pointer rounded-md px-3 text-center focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-2 disabled:cursor-not-allowed ${
className={`${className} relative w-1/2 cursor-pointer rounded-md px-3 text-center focus-visible:bg-th-bkg-4 focus-visible:text-th-fgd-2 disabled:cursor-not-allowed ${
large ? 'h-12 text-sm' : 'h-10 text-xs'
} font-normal
${

View File

@ -468,9 +468,18 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
Number(tierPreset.maintLiabWeight),
Number(tierPreset.initLiabWeight),
Number(tierPreset.liquidationFee),
Number(tierPreset.stablePriceDelayIntervalSeconds),
Number(tierPreset.stablePriceDelayGrowthLimit),
Number(tierPreset.stablePriceGrowthLimit),
Number(tierPreset.minVaultToDepositsRatio),
new BN(tierPreset.netBorrowLimitWindowSizeTs),
new BN(tierPreset.netBorrowLimitPerWindowQuote),
Number(tierPreset.borrowWeightScaleStartQuote),
Number(tierPreset.depositWeightScaleStartQuote),
Number(tierPreset.reduceOnly),
Number(tierPreset.tokenConditionalSwapTakerFeeRate),
Number(tierPreset.tokenConditionalSwapMakerFeeRate),
Number(tierPreset.flashLoanDepositFeeRate),
)
.accounts({
admin: MANGO_DAO_WALLET,
@ -502,13 +511,16 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
null,
null,
null,
tierPreset.borrowWeightScale,
tierPreset.depositWeightScale,
null,
null,
false,
false,
null,
null,
null,
null,
null,
null,
)
.accounts({
oracle: new PublicKey(advForm.oraclePk),
@ -524,7 +536,9 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
} as AccountMeta,
])
.instruction()
proposalTx.push(editIx)
if (!tierPreset.insuranceFound) {
proposalTx.push(editIx)
}
} else {
const trustlessIx = await client!.program.methods
.tokenRegisterTrustless(Number(advForm.tokenIndex), advForm.name)

View File

@ -0,0 +1,25 @@
const NukeIcon = ({ className }: { className?: string }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
fill="currentColor"
>
<g clipPath="url(#a)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 32c8.8366 0 16-7.1634 16-16 0-8.83656-7.1634-16-16-16C7.16344 0 0 7.16344 0 16c0 8.8366 7.16344 16 16 16Zm.026-20.9524c.7047 0 1.3722.1482 1.9656.4077.2967.1482.6675 0 .8159-.2595l3.3748-5.85549c.1854-.33355.0742-.74121-.2596-.88945-3.5973-1.85302-8.0105-2.00126-11.8675-.03706-.33375.1853-.445.59296-.25957.92651l3.44897 5.81849c.1484.2965.5192.4076.8159.2594.5934-.2224 1.2609-.3706 1.9655-.3706Zm-5.5628 4.9661H3.67652c-.37086 0-.66755.2965-.66755.6671.11126 1.9642.66755 3.9654 1.70595 5.7814 1.07549 1.853 2.52184 3.3354 4.22779 4.4472.29668.1853.74171.0741.89006-.2224l3.44893-5.8925c.1484-.2965.1113-.6301-.1483-.8524-1.0755-.7783-1.8172-2.0013-2.0026-3.3725-.0371-.2965-.3338-.5559-.6676-.5559Zm10.4953.5559c-.1855 1.3712-.9272 2.5942-2.0027 3.3725-.2596.1853-.3338.5559-.1483.8524l3.3748 5.8555c.1854.3335.5934.4447.9271.2224 3.5603-2.2978 5.6371-6.152 5.8967-10.1546.0371-.3706-.2967-.667-.6676-.667h-6.7496c-.3338-.0371-.5934.2223-.6304.5188ZM16.026 19.312c1.8434 0 3.3377-1.4933 3.3377-3.3354 0-1.8421-1.4943-3.3354-3.3377-3.3354s-3.3377 1.4933-3.3377 3.3354c0 1.8421 1.4943 3.3354 3.3377 3.3354Z"
/>
</g>
<defs>
<clipPath id="a">
<path d="M0 0h32v32H0z" />
</clipPath>
</defs>
</svg>
)
}
export default NukeIcon

View File

@ -3,6 +3,7 @@ import { LinkButton } from '@components/shared/Button'
import SheenLoader from '@components/shared/SheenLoader'
import { NoSymbolIcon } from '@heroicons/react/20/solid'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useHiddenMangoAccounts } from 'hooks/useHiddenMangoAccounts'
import { useTranslation } from 'next-i18next'
import { useMemo, useState } from 'react'
import { EmptyObject } from 'types'
@ -51,6 +52,7 @@ const fetchLeaderboard = async (
const LeaderboardPage = () => {
const { t } = useTranslation(['common', 'leaderboard'])
const [daysToShow, setDaysToShow] = useState<DaysToShow>('ALLTIME')
const { hiddenAccounts } = useHiddenMangoAccounts()
const { data, isLoading, isFetching, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery(
@ -68,7 +70,13 @@ const LeaderboardPage = () => {
const leaderboardData = useMemo(() => {
if (data?.pages.length) {
return data.pages.flat()
if (hiddenAccounts) {
return data.pages
.flat()
.filter((d) => !hiddenAccounts.includes(d.mango_account))
} else {
return data.pages.flat()
}
}
return []
}, [data, daysToShow])

View File

@ -4,7 +4,6 @@ import SheenLoader from '@components/shared/SheenLoader'
import { ChevronRightIcon } from '@heroicons/react/20/solid'
import { useViewport } from 'hooks/useViewport'
import { formatCurrencyValue } from 'utils/numbers'
import { breakpoints } from 'utils/theme'
import { LeaderboardRes } from './LeaderboardPage'
const LeaderboardTable = ({
@ -16,17 +15,6 @@ const LeaderboardTable = ({
}) => {
return (
<>
{/* <div className="grid grid-cols-12 px-4 pb-2">
<div className="col-span-2 md:col-span-1">
<p className="text-xs text-th-fgd-4">{t('rank')}</p>
</div>
<div className="col-span-4 md:col-span-5">
<p className="text-xs text-th-fgd-4">{t('trader')}</p>
</div>
<div className="col-span-5 flex justify-end">
<p className="text-xs text-th-fgd-4">{t('pnl')}</p>
</div>
</div> */}
<div className="space-y-2">
{data.map((d, i) => (
<LeaderboardRow
@ -54,8 +42,7 @@ const LeaderboardRow = ({
}) => {
const { profile_name, profile_image_url, mango_account, pnl, wallet_pk } =
item
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { isTablet } = useViewport()
return !loading ? (
<a
@ -80,9 +67,9 @@ const LeaderboardRow = ({
{rank < 4 ? <MedalIcon className="absolute" rank={rank} /> : null}
</div>
<ProfileImage
imageSize={isMobile ? '32' : '40'}
imageSize={isTablet ? '32' : '40'}
imageUrl={profile_image_url}
placeholderSize={isMobile ? '20' : '24'}
placeholderSize={isTablet ? '20' : '24'}
/>
<div className="text-left">
<p className="capitalize text-th-fgd-2 md:text-base">

View File

@ -2,10 +2,8 @@ import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import CreateAccountForm from '@components/account/CreateAccountForm'
import { useRouter } from 'next/router'
import useMangoAccount from 'hooks/useMangoAccount'
const CreateAccountModal = ({ isOpen, onClose }: ModalProps) => {
const { mangoAccount } = useMangoAccount()
const router = useRouter()
const handleClose = () => {
@ -17,11 +15,8 @@ const CreateAccountModal = ({ isOpen, onClose }: ModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex min-h-[264px] flex-col items-center justify-center">
<CreateAccountForm
customClose={handleClose}
isFirstAccount={!mangoAccount}
/>
<div className="flex min-h-[400px] flex-col items-center justify-center">
<CreateAccountForm customClose={handleClose} />
</div>
</Modal>
)

View File

@ -122,7 +122,7 @@ const CreateSwitchboardOracleModal = ({
minRequiredOracleResults: 3,
minRequiredJobResults: 2,
minUpdateDelaySeconds: 6,
forceReportPeriod: 24 * 60 * 60,
forceReportPeriod: 60 * 60,
withdrawAuthority: MANGO_DAO_WALLET,
authority: payer,
crankDataBuffer: crankAccount.dataBuffer?.publicKey,

View File

@ -1,4 +1,4 @@
import { ReactNode, useCallback, useEffect, useState } from 'react'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import mangoStore from '@store/mangoStore'
@ -54,28 +54,42 @@ const DashboardSuggestedValues = ({
const [suggestedTier, setSuggstedTier] =
useState<LISTING_PRESETS_KEYS>('SHIT')
const getApiTokenName = (bankName: string) => {
if (bankName === 'ETH (Portal)') {
return 'ETH'
}
return bankName
}
const priceImpactsFiltered = useMemo(
() =>
priceImpacts
.reduce((acc: PriceImpactRespWithoutSide[], val: PriceImpactResp) => {
if (val.side === 'ask') {
const bidSide = priceImpacts.find(
(x) =>
x.symbol === val.symbol &&
x.target_amount === val.target_amount &&
x.side === 'bid',
)
acc.push({
target_amount: val.target_amount,
avg_price_impact_percent: bidSide
? (bidSide.avg_price_impact_percent +
val.avg_price_impact_percent) /
2
: val.avg_price_impact_percent,
symbol: val.symbol,
})
}
return acc
}, [])
.filter((x) => x.symbol === getApiTokenName(bank.name)),
[priceImpacts, bank.name],
)
const getSuggestedTierForListedTokens = useCallback(async () => {
const filteredResp = priceImpacts
.reduce((acc: PriceImpactRespWithoutSide[], val: PriceImpactResp) => {
if (val.side === 'ask') {
const bidSide = priceImpacts.find(
(x) =>
x.symbol === val.symbol &&
x.target_amount === val.target_amount &&
x.side === 'bid',
)
acc.push({
target_amount: val.target_amount,
avg_price_impact_percent: bidSide
? (bidSide.avg_price_impact_percent +
val.avg_price_impact_percent) /
2
: val.avg_price_impact_percent,
symbol: val.symbol,
})
}
return acc
}, [])
const filteredResp = priceImpactsFiltered
.filter((x) => x.avg_price_impact_percent < 1)
.reduce(
(
@ -110,7 +124,7 @@ const DashboardSuggestedValues = ({
: liqudityTier
setSuggstedTier(listingTier as LISTING_PRESETS_KEYS)
}, [priceImpacts])
}, [priceImpactsFiltered])
const proposeNewSuggestedValues = useCallback(
async (
@ -175,13 +189,16 @@ const DashboardSuggestedValues = ({
getNullOrVal(fieldsToChange.netBorrowLimitWindowSizeTs)
? new BN(fieldsToChange.netBorrowLimitWindowSizeTs!)
: null,
getNullOrVal(fieldsToChange.borrowWeightScale),
getNullOrVal(fieldsToChange.depositWeightScale),
getNullOrVal(fieldsToChange.borrowWeightScaleStartQuote),
getNullOrVal(fieldsToChange.depositWeightScaleStartQuote),
false,
false,
bank.reduceOnly ? 0 : null,
null,
null,
getNullOrVal(fieldsToChange.tokenConditionalSwapTakerFeeRate),
getNullOrVal(fieldsToChange.tokenConditionalSwapMakerFeeRate),
getNullOrVal(fieldsToChange.loanFeeRate),
)
.accounts({
group: group!.publicKey,
@ -226,6 +243,7 @@ const DashboardSuggestedValues = ({
}
},
[
PRESETS,
client,
connection,
group,
@ -236,13 +254,6 @@ const DashboardSuggestedValues = ({
],
)
const getApiTokenName = (bankName: string) => {
if (bankName === 'ETH (Portal)') {
return 'ETH'
}
return bankName
}
useEffect(() => {
getSuggestedTierForListedTokens()
}, [getSuggestedTierForListedTokens])
@ -443,6 +454,21 @@ const DashboardSuggestedValues = ({
`${suggestedFields.liquidationFee}%`
}
/>
<div>
<h3 className="mb-4 pl-6">Price impacts</h3>
{priceImpactsFiltered.map((x) => (
<div className="flex pl-6" key={x.target_amount}>
<p className="mr-4 w-[150px] space-x-4">
<span>Amount:</span>
<span>${x.target_amount}</span>
</p>
<p className="space-x-4">
<span>Price impact:</span>{' '}
<span>{x.avg_price_impact_percent.toFixed(3)}%</span>
</p>
</div>
))}
</div>
</Disclosure.Panel>
{invalidKeys.length && (

View File

@ -18,14 +18,9 @@ import Label from '@components/forms/Label'
import useMangoAccountAccounts, {
getAvaialableAccountsColor,
} from 'hooks/useMangoAccountAccounts'
import { MAX_ACCOUNTS } from 'utils/constants'
const MIN_ACCOUNTS = 8
export const MAX_ACCOUNTS: AccountSizeForm = {
tokenAccounts: '10',
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'
@ -132,9 +127,10 @@ const MangoAccountSizeModal = ({ isOpen, onClose }: ModalProps) => {
const handleMax = (propertyName: keyof AccountSizeForm) => {
setFormErrors({})
const defaultSizes = MAX_ACCOUNTS as AccountSizeForm
setAccountSizeForm((prevState) => ({
...prevState,
[propertyName]: MAX_ACCOUNTS[propertyName],
[propertyName]: defaultSizes[propertyName],
}))
}

View File

@ -102,7 +102,7 @@ const MangoAccountsListModal = ({
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="inline-block w-full transform overflow-x-hidden">
<div className="flex min-h-[324px] flex-col justify-between">
<div className="flex min-h-[400px] flex-col justify-between">
<div>
<h2 className="text-center">{t('accounts')}</h2>
{loading ? (

View File

@ -22,7 +22,7 @@ import {
useMemo,
useState,
} from 'react'
import { notify } from 'utils/notifications'
import { createSolanaMessage, notify } from 'utils/notifications'
import ActionTokenList from '../account/ActionTokenList'
import ButtonGroup from '../forms/ButtonGroup'
import Input from '../forms/Input'
@ -31,7 +31,6 @@ import Label from '../forms/Label'
// import EditNftProfilePic from '../profile/EditNftProfilePic'
// import EditProfileForm from '../profile/EditProfileForm'
import Button, { LinkButton } from '../shared/Button'
import InlineNotification from '../shared/InlineNotification'
import Loading from '../shared/Loading'
import MaxAmountButton from '../shared/MaxAmountButton'
import SolBalanceWarnings from '../shared/SolBalanceWarnings'
@ -43,9 +42,11 @@ import BankAmountWithValue from '@components/shared/BankAmountWithValue'
import { isMangoError } from 'types'
import ColorBlur from '@components/ColorBlur'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { ACCEPT_TERMS_KEY } from 'utils/constants'
import { ACCEPT_TERMS_KEY, MAX_ACCOUNTS } from 'utils/constants'
import { ACCOUNT_ACTIONS_NUMBER_FORMAT_CLASSES } from '@components/BorrowForm'
import { WalletReadyState } from '@solana/wallet-adapter-base'
import Switch from '@components/forms/Switch'
import NotificationCookieStore from '@store/notificationCookieStore'
const UserSetupModal = ({
isOpen,
@ -65,6 +66,7 @@ const UserSetupModal = ({
const [depositAmount, setDepositAmount] = useState('')
const [submitDeposit, setSubmitDeposit] = useState(false)
const [sizePercentage, setSizePercentage] = useState('')
const [signToNotifications, setSignToNotifications] = useState(true)
// const [showEditProfilePic, setShowEditProfilePic] = useState(false)
const { maxSolDeposit } = useSolBalance()
const banks = useBanksWithBalances('walletBalance')
@ -72,6 +74,9 @@ const UserSetupModal = ({
const [walletsToDisplay, setWalletstoDisplay] = useState<'default' | 'all'>(
'default',
)
//used to sign txes
const walletContext = useWallet()
const setCookie = NotificationCookieStore((s) => s.setCookie)
const walletsDisplayed = useMemo(() => {
const firstFive = wallets.slice(0, 5)
@ -107,10 +112,16 @@ const UserSetupModal = ({
group,
0,
accountName || 'Account 1',
16, // tokenCount
parseInt(MAX_ACCOUNTS.tokenAccounts), // tokens
parseInt(MAX_ACCOUNTS.spotOpenOrders), // serum3
parseInt(MAX_ACCOUNTS.perpAccounts), // perps
parseInt(MAX_ACCOUNTS.perpOpenOrders), // perp Oo
)
actions.fetchMangoAccounts(publicKey)
if (tx) {
if (signToNotifications) {
createSolanaMessage(walletContext, setCookie)
}
actions.fetchWalletTokens(publicKey) // need to update sol balance after account rent
setShowSetupStep(3)
notify({
@ -354,9 +365,7 @@ const UserSetupModal = ({
<h2 className="mb-4 font-display text-3xl tracking-normal md:text-5xl lg:text-6xl">
{t('onboarding:create-account')}
</h2>
<p className="text-base">
{t('onboarding:create-account-desc')}
</p>
<p className="text-base">{t('insufficient-sol')}</p>
</div>
<div className="mb-4">
<Label text={t('account-name')} optional />
@ -373,11 +382,18 @@ const UserSetupModal = ({
/>
</div>
<SolBalanceWarnings className="mt-4" />
<div className="mt-2">
<InlineNotification
type="info"
desc={t('insufficient-sol')}
<div className="flex items-center justify-between rounded-md border border-th-bkg-3 px-3 py-2">
<div>
<p className="text-th-fgd-2">{t('enable-notifications')}</p>
<p className="text-xs">{t('asked-sign-transaction')}</p>
</div>
<Switch
className="text-th-fgd-3"
checked={signToNotifications}
onChange={(checked) => setSignToNotifications(checked)}
/>
</div>
<div className="space-y-3">
<div className="mt-10">
<Button
className="mb-6 flex items-center justify-center"

View File

@ -26,6 +26,9 @@ import NftMarketButton from './NftMarketButton'
import { abbreviateAddress } from 'utils/formatting'
import EmptyState from './EmptyState'
import { formatNumericValue } from 'utils/numbers'
import Loading from '@components/shared/Loading'
import { notify } from 'utils/notifications'
import SheenLoader from '@components/shared/SheenLoader'
const AllBidsView = () => {
const { publicKey } = useWallet()
@ -34,9 +37,16 @@ const AllBidsView = () => {
// const { t } = useTranslation(['nft-market'])
const [showBidModal, setShowBidModal] = useState(false)
const [bidListing, setBidListing] = useState<null | Listing>(null)
const [buying, setBuying] = useState('')
const [cancellingBid, setCancellingBid] = useState('')
const [accepting, setAccepting] = useState('')
const { data: bids, refetch } = useBids()
const bidsToLoad = bids ? bids : []
const { data: loadedBids } = useLoadBids(bidsToLoad)
const {
data: loadedBids,
isLoading: loadingBids,
isFetching: fetchingBids,
} = useLoadBids(bidsToLoad)
const connection = mangoStore((s) => s.connection)
const fetchNfts = mangoStore((s) => s.actions.fetchNfts)
const nfts = mangoStore((s) => s.wallet.nfts.data)
@ -49,33 +59,74 @@ const AllBidsView = () => {
}, [publicKey])
const cancelBid = async (bid: Bid) => {
await metaplex!.auctionHouse().cancelBid({
auctionHouse: auctionHouse!,
bid,
})
refetch()
setCancellingBid(bid.asset.mint.address.toString())
try {
const { response } = await metaplex!.auctionHouse().cancelBid({
auctionHouse: auctionHouse!,
bid,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error cancelling bid', e)
} finally {
setCancellingBid('')
}
}
const sellAsset = async (bid: Bid, tokenAccountPk: string) => {
console.log(tokenAccountPk)
const tokenAccount = await metaplex
?.tokens()
.findTokenByAddress({ address: new PublicKey(tokenAccountPk) })
setAccepting(bid.asset.mint.address.toString())
try {
const tokenAccount = await metaplex
?.tokens()
.findTokenByAddress({ address: new PublicKey(tokenAccountPk) })
await metaplex!.auctionHouse().sell({
auctionHouse: auctionHouse!,
bid: bid as PublicBid,
sellerToken: tokenAccount!,
})
refetch()
const { response } = await metaplex!.auctionHouse().sell({
auctionHouse: auctionHouse!,
bid: bid as PublicBid,
sellerToken: tokenAccount!,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error accepting offer', e)
} finally {
setAccepting('')
}
}
const buyAsset = async (listing: Listing) => {
await metaplex!.auctionHouse().buy({
auctionHouse: auctionHouse!,
listing,
})
refetch()
setBuying(listing.asset.mint.address.toString())
try {
const { response } = await metaplex!.auctionHouse().buy({
auctionHouse: auctionHouse!,
listing,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error buying nft', e)
} finally {
setBuying('')
}
}
const openBidModal = (listing: Listing) => {
@ -83,15 +134,17 @@ const AllBidsView = () => {
setShowBidModal(true)
}
const loading = loadingBids || fetchingBids
return (
<>
<div className="flex flex-col">
{loadedBids?.length ? (
{loadedBids && loadedBids?.length ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">Date</Th>
<Th className="text-right">NFT</Th>
<Th className="text-left">NFT</Th>
<Th className="text-right">Offer</Th>
<Th className="text-right">Buy Now Price</Th>
<Th className="text-right">Buyer</Th>
@ -104,23 +157,32 @@ const AllBidsView = () => {
.map((x, idx) => {
const listing = listings?.results?.find(
(nft: Listing) =>
nft.asset.mint.toString() === x.asset.mint.toString(),
nft.asset.mint.address.toString() ===
x.asset.mint.address.toString(),
)
return (
<TrBody key={idx}>
<Td>
<TableDateDisplay
date={x.createdAt.toNumber()}
date={x.createdAt.toNumber() * 1000}
showSeconds
/>
</Td>
<Td>
<div className="flex justify-end">
<div className="flex items-center justify-start">
<ImgWithLoader
className="w-12 rounded-md"
className="mr-2 w-12 rounded-md"
alt={x.asset.name}
src={x.asset.json!.image!}
/>
<div>
<p className="font-body">
{x.asset.json?.name || 'Unknown'}
</p>
<p className="font-body text-xs text-th-fgd-3">
{x.asset.json?.collection?.family || 'Unknown'}
</p>
</div>
</div>
</Td>
<Td>
@ -178,14 +240,28 @@ const AllBidsView = () => {
)
}
colorClass="fgd-3"
text="Accept Offer"
text={
accepting ===
x.asset.mint.address.toString() ? (
<Loading />
) : (
'Accept Offer'
)
}
/>
) : (
<>
{publicKey && x.buyerAddress.equals(publicKey) ? (
<NftMarketButton
colorClass="error"
text="Cancel Offer"
text={
cancellingBid ===
x.asset.mint.address.toString() ? (
<Loading />
) : (
'Cancel Offer'
)
}
onClick={() => cancelBid(x)}
/>
) : listing ? (
@ -198,7 +274,14 @@ const AllBidsView = () => {
{listing ? (
<NftMarketButton
colorClass="success"
text="Buy Now"
text={
buying ===
listing.asset.mint.address.toString() ? (
<Loading />
) : (
'Buy Now'
)
}
onClick={() => buyAsset(listing)}
/>
) : null}
@ -211,6 +294,14 @@ const AllBidsView = () => {
})}
</tbody>
</Table>
) : loading ? (
<div className="mt-4 space-y-1.5">
{[...Array(4)].map((x, i) => (
<SheenLoader className="mx-4 flex flex-1 md:mx-6" key={i}>
<div className="h-16 w-full bg-th-bkg-2" />
</SheenLoader>
))}
</div>
) : (
<EmptyState text="No offers to display..." />
)}

View File

@ -7,10 +7,15 @@ import {
} from 'hooks/market/useAuctionHouse'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
import Button from '@components/shared/Button'
import metaplexStore from '@store/metaplexStore'
import { LazyBid, Listing, PublicBid } from '@metaplex-foundation/js'
import { useTranslation } from 'next-i18next'
import EmptyState from './EmptyState'
import dayjs from 'dayjs'
import NftMarketButton from './NftMarketButton'
import Loading from '@components/shared/Loading'
import { useState } from 'react'
import { notify } from 'utils/notifications'
const AssetBidsModal = ({
isOpen,
@ -18,6 +23,7 @@ const AssetBidsModal = ({
listing,
}: ModalProps & { listing: Listing }) => {
const { t } = useTranslation(['nft-market'])
const [accepting, setAccepting] = useState('')
const metaplex = metaplexStore((s) => s.metaplex)
const { data: auctionHouse } = useAuctionHouse()
const { data: lazyBids, refetch: reftechBids } = useBids()
@ -27,32 +33,73 @@ const AssetBidsModal = ({
)
const acceptBid = async (lazyBid: LazyBid) => {
const bid = await metaplex!.auctionHouse().loadBid({
lazyBid,
loadJsonMetadata: true,
})
setAccepting(lazyBid.metadataAddress.toString())
try {
const bid = await metaplex!.auctionHouse().loadBid({
lazyBid,
loadJsonMetadata: true,
})
await metaplex!.auctionHouse().sell({
auctionHouse: auctionHouse!,
bid: bid as PublicBid,
sellerToken: listing.asset.token,
})
refetchLazyListings()
reftechBids()
const { response } = await metaplex!.auctionHouse().sell({
auctionHouse: auctionHouse!,
bid: bid as PublicBid,
sellerToken: listing.asset.token,
})
refetchLazyListings()
reftechBids()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error accepting offer', e)
} finally {
setAccepting('')
}
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex max-h-[500px] min-h-[264px] flex-col overflow-auto">
{assetBids?.map((x) => (
<p className="flex space-x-2" key={x.createdAt.toNumber()}>
<div>{x.createdAt.toNumber()}</div>
<div>{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}</div>
<div>
<Button onClick={() => acceptBid(x)}>{t('accept-bid')}</Button>
</div>
</p>
))}
<div className="thin-scroll flex max-h-[500px] flex-col overflow-auto">
<h2 className="mb-4 text-center text-lg">Offers</h2>
<div className="space-y-4">
{assetBids && assetBids.length ? (
assetBids.map((x) => (
<div
className="flex items-center justify-between"
key={x.createdAt.toNumber()}
>
<div>
<p className="text-xs">
{dayjs(x.createdAt.toNumber() * 1000).format(
'DD MMM YY h:mma',
)}
</p>
<span className="font-display text-th-fgd-2">
{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}{' '}
MNGO
</span>
</div>
<NftMarketButton
text={
accepting === x.metadataAddress.toString() ? (
<Loading />
) : (
t('accept')
)
}
colorClass="error"
onClick={() => acceptBid(x)}
/>
</div>
))
) : (
<EmptyState text="No offers to display..." />
)}
</div>
</div>
</Modal>
)

View File

@ -12,6 +12,7 @@ import { ImgWithLoader } from '@components/ImgWithLoader'
// import { useTranslation } from 'next-i18next'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import Loading from '@components/shared/Loading'
import { notify } from 'utils/notifications'
type ListingModalProps = {
listing?: Listing
@ -27,23 +28,31 @@ const BidNftModal = ({ isOpen, onClose, listing }: ListingModalProps) => {
const [bidPrice, setBidPrice] = useState('')
const [assetMint, setAssetMint] = useState('')
const [submittingOffer, setSubmittingOffer] = useState(false)
const [buying, setBuying] = useState(false)
const bid = useCallback(async () => {
setSubmittingOffer(true)
try {
await metaplex!.auctionHouse().bid({
const { response } = await metaplex!.auctionHouse().bid({
auctionHouse: auctionHouse!,
price: token(bidPrice, MANGO_MINT_DECIMALS),
mintAccount: noneListedAssetMode
? new PublicKey(assetMint)
: listing!.asset.mint.address,
})
onClose()
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error making offer', e)
} finally {
setSubmittingOffer(false)
onClose()
}
}, [
metaplex,
@ -57,29 +66,50 @@ const BidNftModal = ({ isOpen, onClose, listing }: ListingModalProps) => {
setSubmittingOffer,
])
const handleBuyNow = useCallback(
async (listing: Listing) => {
setBuying(true)
try {
const { response } = await metaplex!.auctionHouse().buy({
auctionHouse: auctionHouse!,
listing,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error buying nft', e)
} finally {
setBuying(false)
}
},
[metaplex, auctionHouse, refetch, setBuying],
)
return (
<Modal isOpen={isOpen} onClose={onClose}>
<h2 className="mb-4 text-center text-lg">Make an Offer</h2>
<div className="flex flex-col items-center">
{listing ? (
<div className="flex flex-col items-center">
<ImgWithLoader
alt={listing.asset.name}
className="mb-3 h-40 w-40 flex-shrink-0 rounded-md"
src={listing.asset.json!.image!}
/>
<LinkButton>
<span className="font-body font-normal">
Buy Now:{' '}
<span className="font-display">
{toUiDecimals(
listing.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)}{' '}
<span className="font-bold">MNGO</span>
</span>
</span>
</LinkButton>
<div className="flex flex-col items-center text-center">
{listing.asset?.json?.image ? (
<ImgWithLoader
alt={listing.asset?.name || 'Unknown'}
className="mb-3 h-40 w-40 flex-shrink-0 rounded-md"
src={listing.asset.json.image}
/>
) : null}
<p className="font-bold text-th-fgd-2">
{listing.asset?.json?.name || 'Unknown'}
</p>
<p className="text-xs">
{listing.asset?.json?.collection?.family || 'Unknown'}
</p>
</div>
) : (
<>
@ -94,7 +124,7 @@ const BidNftModal = ({ isOpen, onClose, listing }: ListingModalProps) => {
/>
</>
)}
<div className="mt-4 flex w-full items-end">
<div className="mt-4 flex w-full items-end border-t border-th-bkg-3 pt-4">
<div className="w-full">
<Label text="Offer Price"></Label>
<Input
@ -114,6 +144,26 @@ const BidNftModal = ({ isOpen, onClose, listing }: ListingModalProps) => {
{submittingOffer ? <Loading /> : 'Make Offer'}
</Button>
</div>
{listing ? (
buying ? (
<div className="mt-4 text-th-fgd-3">
<Loading />
</div>
) : (
<LinkButton className="mt-4" onClick={() => handleBuyNow(listing)}>
<span className="font-body font-normal">
Buy Now:{' '}
<span className="font-display">
{toUiDecimals(
listing.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)}{' '}
<span className="font-bold">MNGO</span>
</span>
</span>
</LinkButton>
)
) : null}
</div>
</Modal>
)

View File

@ -12,7 +12,7 @@ import {
useLazyListings,
useListings,
} from 'hooks/market/useAuctionHouse'
import { useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
// import { useTranslation } from 'next-i18next'
import { ImgWithLoader } from '@components/ImgWithLoader'
@ -21,8 +21,17 @@ import { formatNumericValue } from 'utils/numbers'
import Loading from '@components/shared/Loading'
import SheenLoader from '@components/shared/SheenLoader'
import EmptyState from './EmptyState'
import { notify } from 'utils/notifications'
const filter = [ALL_FILTER, 'My Listings']
const YOUR_LISTINGS = 'Your Listings'
const PRICE_LOW_HIGH = 'Price: Low to High'
const PRICE_HIGH_LOW = 'Price: High to Low'
const defaultFilters = [
ALL_FILTER,
YOUR_LISTINGS,
PRICE_LOW_HIGH,
PRICE_HIGH_LOW,
]
const ListingsView = () => {
const { publicKey } = useWallet()
@ -44,15 +53,37 @@ const ListingsView = () => {
isLoading: loadingListings,
isFetching: fetchingListings,
} = useListings()
const [listingsToShow, setListingsToShow] = useState<Listing[] | undefined>(
undefined,
)
useEffect(() => {
if (
(!listingsToShow || !listingsToShow.length) &&
listings?.results.length
) {
const sortedResults = listings.results.sort(
(a, b) => b.createdAt.toNumber() - a.createdAt.toNumber(),
)
setListingsToShow(sortedResults)
}
}, [listings])
const cancelListing = async (listing: Listing) => {
setCancellingListing(listing.asset.mint.address.toString())
try {
await metaplex!.auctionHouse().cancelListing({
const { response } = await metaplex!.auctionHouse().cancelListing({
auctionHouse: auctionHouse!,
listing: listing,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error cancelling listing', e)
} finally {
@ -63,11 +94,18 @@ const ListingsView = () => {
const buyAsset = async (listing: Listing) => {
setBuying(listing.asset.mint.address.toString())
try {
await metaplex!.auctionHouse().buy({
const { response } = await metaplex!.auctionHouse().buy({
auctionHouse: auctionHouse!,
listing,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error buying nft', e)
} finally {
@ -95,19 +133,67 @@ const ListingsView = () => {
// setPage(page)
// }
const filters = useMemo(() => {
if (!listings?.results || !listings?.results.length) return defaultFilters
const collections: string[] = []
for (const listing of listings.results) {
const collectionName = listing.asset.json?.collection?.family || 'Unknown'
if (!collections.includes(collectionName)) {
collections.push(collectionName)
}
}
return defaultFilters.concat(collections.sort((a, b) => a.localeCompare(b)))
}, [listings])
const handleFilter = useCallback(
(filter: string) => {
setCurrentFilter(filter)
if (filter === ALL_FILTER) {
const sortedResults = listings?.results.sort(
(a, b) => b.createdAt.toNumber() - a.createdAt.toNumber(),
)
setListingsToShow(sortedResults)
} else if (filter === YOUR_LISTINGS) {
const filteredListings = listings?.results.filter((listing) => {
return listing.sellerAddress.toString() === publicKey?.toString()
})
setListingsToShow(filteredListings)
} else if (filter.includes('Price')) {
return listings?.results.sort((a, b) => {
const aPrice = toUiDecimals(
a.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)
const bPrice = toUiDecimals(
b.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)
return filter === PRICE_LOW_HIGH ? aPrice - bPrice : bPrice - aPrice
})
} else {
const filteredListings = listings?.results.filter((listing) => {
const collectionName =
listing.asset.json?.collection?.family || 'Unknown'
return collectionName === filter
})
setListingsToShow(filteredListings)
}
},
[listings, publicKey],
)
const loading = loadingListings || fetchingListings
console.log(listings?.results)
return (
<div className="flex flex-col">
<div>
<div className="mb-4 mt-2 flex items-center justify-between rounded-md bg-th-bkg-2 p-2 pl-4">
<h3 className="text-sm font-normal text-th-fgd-3">{`Filter Results`}</h3>
<Select
value={currentFilter}
onChange={(filter) => setCurrentFilter(filter)}
className="w-[150px]"
onChange={(filter) => handleFilter(filter)}
className="w-[180px]"
>
{filter.map((filter) => (
{filters.map((filter) => (
<Select.Option key={filter} value={filter}>
<div className="flex w-full items-center justify-between">
{filter}
@ -115,17 +201,10 @@ const ListingsView = () => {
</Select.Option>
))}
</Select>
{asssetBidsModal && assetBidsListing ? (
<AssetBidsModal
listing={assetBidsListing}
isOpen={asssetBidsModal}
onClose={closeBidsModal}
></AssetBidsModal>
) : null}
</div>
<div className="grid grid-flow-row auto-rows-max grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5">
{listings?.results ? (
listings.results.map((x, idx) => {
{listingsToShow && listingsToShow.length ? (
listingsToShow.map((x, idx) => {
const imgSource = x.asset.json?.image
const nftBids = bids?.filter((bid) =>
bid.metadataAddress.equals(x.asset.metadataAddress),
@ -157,28 +236,25 @@ const ListingsView = () => {
</div>
) : null}
<div className="p-4">
<div className="flex justify-between">
<div>
<p className="text-xs">Buy Now</p>
<div className="flex items-center">
{/* <img
className="mr-1 h-3.5 w-auto"
src="/icons/mngo.svg"
/> */}
<span className="font-display text-base">
{formatNumericValue(
toUiDecimals(
x.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
),
)}{' '}
<span className="font-body font-bold">MNGO</span>
</span>
</div>
</div>
<h3 className="text-sm text-th-fgd-2">
{x.asset.json?.name || x.asset.name || 'Unknown'}
</h3>
<p className="mb-1.5 text-xs">
{x.asset.json?.collection?.family || 'Unknown'}
</p>
<div className="flex items-center">
<span className="font-display text-base">
{formatNumericValue(
toUiDecimals(
x.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
),
)}{' '}
<span className="font-body font-bold">MNGO</span>
</span>
</div>
<div>
<p className="mt-2 text-xs">
<p className="text-xs">
{bestBid ? `Best Offer: ${bestBid} MNGO` : 'No offers'}
</p>
</div>
@ -202,13 +278,6 @@ const ListingsView = () => {
colorClass="fgd-3"
onClick={() => openBidModal(x)}
/>
{bidNftModal && bidListing && (
<BidNftModal
listing={bidListing}
isOpen={bidNftModal}
onClose={closeBidModal}
></BidNftModal>
)}
</div>
)}
{publicKey && x.sellerAddress.equals(publicKey) && (
@ -257,6 +326,20 @@ const ListingsView = () => {
onPageChange={handlePageClick}
/>
</div> */}
{asssetBidsModal && assetBidsListing ? (
<AssetBidsModal
listing={assetBidsListing}
isOpen={asssetBidsModal}
onClose={closeBidsModal}
/>
) : null}
{bidNftModal && bidListing ? (
<BidNftModal
listing={bidListing}
isOpen={bidNftModal}
onClose={closeBidModal}
/>
) : null}
</div>
)
}

View File

@ -12,11 +12,15 @@ import { ImgWithLoader } from '@components/ImgWithLoader'
import metaplexStore from '@store/metaplexStore'
import { Bid } from '@metaplex-foundation/js'
import { useTranslation } from 'next-i18next'
import dayjs from 'dayjs'
import NftMarketButton from './NftMarketButton'
import { useState } from 'react'
import Loading from '@components/shared/Loading'
import EmptyState from './EmptyState'
import { notify } from 'utils/notifications'
const MyBidsModal = ({ isOpen, onClose }: ModalProps) => {
const { publicKey } = useWallet()
const [cancelling, setCancelling] = useState('')
const metaplex = metaplexStore((s) => s.metaplex)
const { t } = useTranslation(['nft-market'])
const { data: auctionHouse } = useAuctionHouse()
@ -29,44 +33,76 @@ const MyBidsModal = ({ isOpen, onClose }: ModalProps) => {
const { data: bids } = useLoadBids(myBids)
const cancelBid = async (bid: Bid) => {
await metaplex!.auctionHouse().cancelBid({
auctionHouse: auctionHouse!,
bid,
})
refetch()
setCancelling(bid.asset.mint.address.toString())
try {
const { response } = await metaplex!.auctionHouse().cancelBid({
auctionHouse: auctionHouse!,
bid,
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
} catch (e) {
console.log('error cancelling offer', e)
} finally {
setCancelling('')
}
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<h2 className="mb-4 text-center text-lg">Your Offers</h2>
<div className="space-y-4">
{bids?.map((x) => (
<div
className="flex items-center justify-between"
key={x.createdAt.toNumber()}
>
<div className="flex items-center">
<ImgWithLoader
className="mr-3 w-12 rounded-md"
alt={x.asset.name}
src={x.asset.json!.image!}
/>
<div>
<p className="text-xs">
{dayjs(x.createdAt.toNumber()).format('DD MMM YY h:mma')}
</p>
<span className="font-display text-th-fgd-2">
{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)} MNGO
</span>
{bids && bids.length ? (
bids
.sort((a, b) => b.createdAt.toNumber() - a.createdAt.toNumber())
.map((x) => (
<div
className="flex items-center justify-between"
key={x.createdAt.toNumber()}
>
<div className="flex items-center">
{x.asset?.json?.image ? (
<ImgWithLoader
className="mr-3 w-12 rounded-md"
alt={x.asset?.name || 'Unknown'}
src={x.asset.json.image}
/>
) : null}
<div>
<p className="font-bold text-th-fgd-2">
{x.asset?.json?.name || 'Unknown'}
</p>
<p className="text-xs">
{x.asset?.json?.collection?.family || 'Unknown'}
</p>
<span className="font-display text-th-fgd-2">
{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}{' '}
<span className="font-body">MNGO</span>
</span>
</div>
</div>
<NftMarketButton
text={
cancelling === x.asset.mint.address.toString() ? (
<Loading />
) : (
t('cancel')
)
}
colorClass="error"
onClick={() => cancelBid(x)}
/>
</div>
</div>
<NftMarketButton
text={t('cancel')}
colorClass="error"
onClick={() => cancelBid(x)}
/>
</div>
))}
))
) : (
<EmptyState text="No offers to display..." />
)}
</div>
</Modal>
)

View File

@ -15,6 +15,7 @@ import { token } from '@metaplex-foundation/js'
import metaplexStore from '@store/metaplexStore'
import { useAuctionHouse, useLazyListings } from 'hooks/market/useAuctionHouse'
import Loading from '@components/shared/Loading'
import { notify } from 'utils/notifications'
const SellNftModal = ({ isOpen, onClose }: ModalProps) => {
const { publicKey } = useWallet()
@ -49,12 +50,19 @@ const SellNftModal = ({ isOpen, onClose }: ModalProps) => {
if (isCurrentlyListed) {
throw 'Item is currently listed by you'
}
await metaplex!.auctionHouse().list({
const { response } = await metaplex!.auctionHouse().list({
auctionHouse: auctionHouse!, // A model of the Auction House related to this listing
mintAccount: new PublicKey(mint), // The mint account to create a listing for, used to find the metadata
price: token(price, MANGO_MINT_DECIMALS), // The listing price
})
refetch()
if (response) {
notify({
title: 'Transaction confirmed',
type: 'success',
txid: response.signature,
})
}
onClose()
} catch (e) {
console.log('error listing nft', e)

View File

@ -7,9 +7,7 @@ import {
TrashIcon,
XMarkIcon,
} from '@heroicons/react/20/solid'
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { WalletContextState, useWallet } from '@solana/wallet-adapter-react'
import { Payload, SIWS } from '@web3auth/sign-in-with-solana'
import { useWallet } from '@solana/wallet-adapter-react'
import { useHeaders } from 'hooks/notifications/useHeaders'
import { useIsAuthorized } from 'hooks/notifications/useIsAuthorized'
import { useNotifications } from 'hooks/notifications/useNotifications'
@ -18,58 +16,13 @@ import { NOTIFICATION_API } from 'utils/constants'
import NotificationCookieStore from '@store/notificationCookieStore'
import dayjs from 'dayjs'
import { useTranslation } from 'next-i18next'
import { notify } from 'utils/notifications'
export const createSolanaMessage = (
wallet: WalletContextState,
setCookie: (wallet: string, token: string) => void,
) => {
const payload = new Payload()
payload.domain = window.location.host
payload.address = wallet.publicKey!.toString()
payload.uri = window.location.origin
payload.statement = 'Login to Mango Notifications Admin App'
payload.version = '1'
payload.chainId = 1
const message = new SIWS({ payload })
const messageText = message.prepareMessage()
const messageEncoded = new TextEncoder().encode(messageText)
wallet.signMessage!(messageEncoded)
.then(async (resp) => {
const tokenResp = await fetch(`${NOTIFICATION_API}auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...payload,
signatureString: bs58.encode(resp),
}),
})
const body = await tokenResp.json()
const token = body.token
const error = body.error
if (error) {
notify({
type: 'error',
title: 'Error',
description: error,
})
return
}
setCookie(payload.address, token)
})
.catch((e) => {
notify({
type: 'error',
title: 'Error',
description: e.message ? e.message : `${e}`,
})
})
}
import {
createLedgerMessage,
createSolanaMessage,
notify,
} from 'utils/notifications'
import mangoStore from '@store/mangoStore'
import { ttCommons, ttCommonsExpanded, ttCommonsMono } from 'utils/fonts'
const NotificationsDrawer = ({
isOpen,
@ -80,6 +33,7 @@ const NotificationsDrawer = ({
}) => {
const { t } = useTranslation('notifications')
const { data, refetch } = useNotifications()
const connection = mangoStore((s) => s.connection)
const wallet = useWallet()
const isAuth = useIsAuthorized()
const headers = useHeaders()
@ -192,7 +146,7 @@ const NotificationsDrawer = ({
as={Fragment}
>
<Dialog.Panel
className={`thin-scroll absolute right-0 z-40 h-full w-full overflow-y-auto bg-th-bkg-1 text-left md:w-96`}
className={`thin-scroll absolute right-0 z-40 h-full w-full overflow-y-auto bg-th-bkg-1 text-left font-body md:w-96 ${ttCommons.variable} ${ttCommonsExpanded.variable} ${ttCommonsMono.variable}`}
>
<div className="flex h-16 items-center justify-between border-b border-th-bkg-3 pl-6">
<h2 className="text-lg">{t('notifications')}</h2>
@ -268,13 +222,22 @@ const NotificationsDrawer = ({
<div className="flex flex-col items-center justify-center text-center">
<InboxIcon className="mb-2 h-7 w-7 text-th-fgd-2" />
<h3 className="mb-1 text-base">{t('unauth-title')}</h3>
<p>{t('unauth-desc')}</p>
<p className="mb-3">{t('unauth-desc')}</p>
<Button
className="mt-6"
onClick={() => createSolanaMessage(wallet, setCookie)}
>
{t('sign-message')}
</Button>
<LinkButton
className="mt-6 text-th-fgd-2"
secondary
onClick={() =>
createLedgerMessage(wallet, setCookie, connection)
}
>
{t('sign-using-ledger')}
</LinkButton>
</div>
</div>
)}

View File

@ -2,7 +2,6 @@ import MedalIcon from '@components/icons/MedalIcon'
import ProfileImage from '@components/profile/ProfileImage'
import { ArrowLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import {
Badge,
RewardsLeaderboardItem,
@ -126,8 +125,7 @@ const LeaderboardCard = ({
rank: number
wallet: RewardsLeaderboardItem
}) => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { isTablet } = useViewport()
return (
<a
className="flex w-full items-center justify-between rounded-md border border-th-bkg-3 px-3 py-3 md:px-4 md:hover:bg-th-bkg-2"
@ -151,9 +149,9 @@ const LeaderboardCard = ({
{rank < 4 ? <MedalIcon className="absolute" rank={rank} /> : null}
</div>
<ProfileImage
imageSize={isMobile ? '32' : '40'}
imageSize={isTablet ? '32' : '40'}
imageUrl={''}
placeholderSize={isMobile ? '20' : '24'}
placeholderSize={isTablet ? '20' : '24'}
/>
<div className="text-left">
<p className="capitalize text-th-fgd-2 md:text-base">

View File

@ -10,6 +10,7 @@ import {
UserIcon,
} from '@heroicons/react/20/solid'
import { PublicKey } from '@solana/web3.js'
import { useHiddenMangoAccounts } from 'hooks/useHiddenMangoAccounts'
import { useTranslation } from 'next-i18next'
import { ChangeEvent, useState } from 'react'
import { MANGO_DATA_API_URL } from 'utils/constants'
@ -32,6 +33,7 @@ const SearchPage = () => {
const [searchType, setSearchType] = useState('mango-account')
const [showNoResults, setShowNoResults] = useState(false)
const [isAccountSearch, setIsAccountSearch] = useState(true)
const { hiddenAccounts } = useHiddenMangoAccounts()
const handleSearch = async () => {
if (
@ -48,7 +50,25 @@ const SearchPage = () => {
const response = await fetch(
`${MANGO_DATA_API_URL}/user-data/profile-search?search-string=${searchString}&search-method=${searchType}`,
)
const data = await response.json()
let data = await response.json()
if (isAccountSearch && hiddenAccounts) {
data = data.filter(
(d: MangoAccountItem) => !hiddenAccounts.includes(d.mango_account_pk),
)
} else if (hiddenAccounts) {
data = data
.map((d: WalletItem) => {
const visibleMangoAccounts = d.mango_accounts.filter(
(m) => !hiddenAccounts.includes(m.mango_account_pk),
)
return {
owner: d.owner,
profile_name: d.profile_name,
mango_accounts: visibleMangoAccounts,
}
})
.filter((d: WalletItem) => d.mango_accounts.length > 0)
}
setSearchResults(data)
if (!data.length) {
setShowNoResults(true)

View File

@ -1,16 +1,11 @@
import MangoAccountSizeModal, {
MAX_ACCOUNTS,
} from '@components/modals/MangoAccountSizeModal'
import HideMangoAccount from '@components/account/HideMangoAccount'
import MangoAccountSizeModal from '@components/modals/MangoAccountSizeModal'
import { LinkButton } from '@components/shared/Button'
import TokenLogo from '@components/shared/TokenLogo'
import Tooltip from '@components/shared/Tooltip'
import MarketLogos from '@components/trade/MarketLogos'
import { Disclosure } from '@headlessui/react'
import {
ChevronDownIcon,
ExclamationCircleIcon,
SquaresPlusIcon,
} from '@heroicons/react/20/solid'
import { ChevronDownIcon, SquaresPlusIcon } from '@heroicons/react/20/solid'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoAccountAccounts, {
getAvaialableAccountsColor,
@ -18,6 +13,7 @@ import useMangoAccountAccounts, {
import useMangoGroup from 'hooks/useMangoGroup'
import { useTranslation } from 'next-i18next'
import { useState } from 'react'
import { MAX_ACCOUNTS } from 'utils/constants'
const AccountSettings = () => {
const { t } = useTranslation(['common', 'settings'])
@ -38,8 +34,12 @@ const AccountSettings = () => {
return mangoAccountAddress && group ? (
<>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-base">{t('account')}</h2>
<h2 className="mb-4 text-base">{t('account')}</h2>
<div className="pb-6">
<HideMangoAccount />
</div>
<div className="mb-4 flex items-center justify-between md:px-4">
<h3 className="text-sm text-th-fgd-2">{t('settings:account-size')}</h3>
{!isAccountFull ? (
<LinkButton
className="flex items-center"
@ -48,14 +48,7 @@ const AccountSettings = () => {
<SquaresPlusIcon className="mr-1.5 h-4 w-4" />
{t('settings:increase-account-size')}
</LinkButton>
) : (
<div className="flex items-center">
<ExclamationCircleIcon className="mr-1.5 h-4 w-4 text-th-error" />
<p className="text-th-error">
{t('settings:error-account-size-full')}
</p>
</div>
)}
) : null}
</div>
<Disclosure>
{({ open }) => (

View File

@ -19,6 +19,7 @@ import { useRouter } from 'next/router'
import {
AUTO_CONNECT_WALLET,
NOTIFICATION_POSITION_KEY,
PRIVACY_MODE,
SIZE_INPUT_UI_KEY,
TRADE_CHART_UI_KEY,
TRADE_LAYOUT_KEY,
@ -92,6 +93,8 @@ const DisplaySettings = () => {
true,
)
const [privacyMode, setPrivacyMode] = useLocalStorageState(PRIVACY_MODE)
// add nft skins to theme selection list
useEffect(() => {
if (nfts.length) {
@ -227,6 +230,15 @@ const DisplaySettings = () => {
onChange={() => setAutoConnect(!autoConnect)}
/>
</div>
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
<Tooltip content={t('settings:tooltip-privacy-mode')}>
<p className="tooltip-underline">{t('settings:privacy-mode')}</p>
</Tooltip>
<Switch
checked={privacyMode}
onChange={() => setPrivacyMode(!privacyMode)}
/>
</div>
</>
)
}

View File

@ -1,6 +1,5 @@
import Switch from '@components/forms/Switch'
import { createSolanaMessage } from '@components/notifications/NotificationsDrawer'
import Button from '@components/shared/Button'
import Button, { LinkButton } from '@components/shared/Button'
import ConnectEmptyState from '@components/shared/ConnectEmptyState'
import { BellIcon } from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
@ -10,6 +9,8 @@ import { useNotificationSettings } from 'hooks/notifications/useNotificationSett
import { useTranslation } from 'next-i18next'
import { NOTIFICATION_API } from 'utils/constants'
import NotificationCookieStore from '@store/notificationCookieStore'
import mangoStore from '@store/mangoStore'
import { createLedgerMessage, createSolanaMessage } from 'utils/notifications'
const NotificationSettings = () => {
const { t } = useTranslation(['common', 'notifications', 'settings'])
@ -19,6 +20,7 @@ const NotificationSettings = () => {
const setCookie = NotificationCookieStore((s) => s.setCookie)
const headers = useHeaders()
const isAuth = useIsAuthorized()
const connection = mangoStore((s) => s.connection)
const handleSettingChange = async (key: string, val: boolean) => {
if (data) {
@ -60,11 +62,23 @@ const NotificationSettings = () => {
<div className="flex flex-col items-center">
<BellIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p className="mb-4">{t('notifications:unauth-desc')}</p>
<Button onClick={() => createSolanaMessage(wallet, setCookie)}>
<Button
className="mt-3"
onClick={() => createSolanaMessage(wallet, setCookie)}
>
<div className="flex items-center">
{t('notifications:sign-message')}
</div>
</Button>
<LinkButton
className="mt-4 text-th-fgd-2"
secondary
onClick={() =>
createLedgerMessage(wallet, setCookie, connection)
}
>
{t('notifications:sign-using-ledger')}
</LinkButton>
</div>
) : (
<ConnectEmptyState text={t('settings:connect-notifications')} />

View File

@ -34,13 +34,14 @@ const RPC_URLS = [
{ label: 'Custom', value: '' },
]
export const PRIORITY_FEES = [
export const PRIORITY_FEE_LEVELS = [
{ label: 'None', value: 0 },
{ label: 'Low', value: 50000 },
{ label: 'High', value: 100000 },
{ label: 'Low', value: 1.2 }, // +20%
{ label: 'High', value: 2 }, // +100%
]
export const DEFAULT_PRIORITY_FEE = PRIORITY_FEES[1]
export const DEFAULT_PRIORITY_FEE = 1
export const DEFAULT_PRIORITY_FEE_LEVEL = PRIORITY_FEE_LEVELS[1]
const RpcSettings = () => {
const { t } = useTranslation('settings')
@ -52,10 +53,8 @@ const RpcSettings = () => {
RPC_PROVIDER_KEY,
RPC_URLS[0].value,
)
const [storedPriorityFee, setStoredPriorityFee] = useLocalStorageState(
PRIORITY_FEE_KEY,
DEFAULT_PRIORITY_FEE.value,
)
const [storedPriorityFeeLevel, setStoredPriorityFeeLevel] =
useLocalStorageState(PRIORITY_FEE_KEY, DEFAULT_PRIORITY_FEE_LEVEL)
const [storedUseOrderbookFeed, setStoredUseOrderbookFeed] =
useLocalStorageState(USE_ORDERBOOK_FEED_KEY, true)
@ -70,10 +69,11 @@ const RpcSettings = () => {
const priorityFee = useMemo(() => {
return (
PRIORITY_FEES.find((node) => node.value === storedPriorityFee) ||
DEFAULT_PRIORITY_FEE
PRIORITY_FEE_LEVELS.find(
(node) => node.value === storedPriorityFeeLevel,
) || DEFAULT_PRIORITY_FEE_LEVEL
)
}, [storedPriorityFee])
}, [storedPriorityFeeLevel])
const handleSetEndpointProvider = (provider: string) => {
const endpointProvider = RPC_URLS.find(
@ -88,15 +88,15 @@ const RpcSettings = () => {
const handlePriorityFee = useCallback(
(label: string) => {
const fee = PRIORITY_FEES.find((fee) => fee.label === label)
const fee = PRIORITY_FEE_LEVELS.find((fee) => fee.label === label)
if (fee) {
setStoredPriorityFee(fee?.value)
setStoredPriorityFeeLevel(fee?.value)
if (wallet) {
actions.connectMangoClientWithWallet(wallet)
}
}
},
[setStoredPriorityFee, actions, wallet],
[setStoredPriorityFeeLevel, actions, wallet],
)
useEffect(() => {
@ -154,7 +154,7 @@ const RpcSettings = () => {
<ButtonGroup
activeValue={priorityFee.label}
onChange={(v) => handlePriorityFee(v)}
values={PRIORITY_FEES.map((val) => val.label)}
values={PRIORITY_FEE_LEVELS.map((val) => val.label)}
/>
{/* {showCustomForm ? (
<div className="mt-2">

View File

@ -6,16 +6,14 @@ import NotificationSettings from './NotificationSettings'
import PreferredExplorerSettings from './PreferredExplorerSettings'
import RpcSettings from './RpcSettings'
import SoundSettings from './SoundSettings'
import { breakpoints } from 'utils/theme'
import AccountSettings from './AccountSettings'
import useMangoAccount from 'hooks/useMangoAccount'
import useUnownedAccount from 'hooks/useUnownedAccount'
const SettingsPage = () => {
const { width } = useViewport()
const { isDesktop } = useViewport()
const { mangoAccountAddress } = useMangoAccount()
const { isUnownedAccount } = useUnownedAccount()
const isMobile = width ? width < breakpoints.lg : false
return (
<div className="grid grid-cols-12">
<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">
@ -32,7 +30,7 @@ const SettingsPage = () => {
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<NotificationSettings />
</div>
{!isMobile ? (
{isDesktop ? (
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<HotKeysSettings />
</div>

View File

@ -241,6 +241,7 @@ const BalancesTable = () => {
{({ open }) => (
<>
<Disclosure.Button
as="div"
className={`w-full border-t border-th-bkg-3 p-4 text-left focus:outline-none ${
i === 0 ? 'border-t-0' : ''
}`}
@ -344,8 +345,7 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
const { selectedMarket } = useSelectedMarket()
const { asPath } = useRouter()
const { isUnownedAccount } = useUnownedAccount()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { isDesktop } = useViewport()
const tokenBank = bank.bank
@ -422,16 +422,27 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
s.swap.outputBank =
swap.outputBank.name === 'USDC' ? solBank : usdcBank
}
s.swap.triggerPrice = ''
})
} else {
set((s) => {
s.swap.outputBank = tokenBank
s.swap.amountIn = ''
s.swap.amountOut = Math.abs(balance).toString()
s.swap.swapMode = 'ExactOut'
if (tokenBank.name === swap.inputBank?.name) {
s.swap.inputBank =
swap.inputBank.name === 'USDC' ? solBank : usdcBank
if (swap.swapOrTrigger === 'swap') {
s.swap.outputBank = tokenBank
s.swap.amountIn = ''
s.swap.amountOut = Math.abs(balance).toString()
s.swap.swapMode = 'ExactOut'
if (tokenBank.name === swap.inputBank?.name) {
s.swap.inputBank =
swap.inputBank.name === 'USDC' ? solBank : usdcBank
}
} else {
s.swap.inputBank = tokenBank
s.swap.amountIn = Math.abs(balance).toString()
s.swap.amountOut = ''
if (tokenBank.name === swap.outputBank?.name) {
s.swap.outputBank =
swap.outputBank.name === 'USDC' ? solBank : usdcBank
}
}
})
}
@ -455,7 +466,7 @@ const Balance = ({ bank }: { bank: BankWithBalance }) => {
return (
<p className="font-mono text-th-fgd-2 md:flex md:justify-end">
{!isUnownedAccount && !isMobile ? (
{!isUnownedAccount && isDesktop ? (
asPath.includes('/trade') && isBaseOrQuote ? (
<LinkButton
className="font-normal underline underline-offset-2 md:underline-offset-4 md:hover:no-underline"

View File

@ -1,4 +1,6 @@
import Decimal from 'decimal.js'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { PRIVACY_MODE } from 'utils/constants'
import { formatCurrencyValue, formatNumericValue } from 'utils/numbers'
const FormatNumericValue = ({
@ -14,10 +16,13 @@ const FormatNumericValue = ({
isUsd?: boolean
roundUp?: boolean
}) => {
const [privacyMode] = useLocalStorageState(PRIVACY_MODE)
return (
<span className={classNames}>
{isUsd
? formatCurrencyValue(value, decimals)
? privacyMode
? '****'
: formatCurrencyValue(value, decimals)
: formatNumericValue(value, decimals, roundUp)}
</span>
)

View File

@ -50,7 +50,7 @@ function Modal({
themeData.fonts.display.variable
} ${
themeData.fonts.mono.variable
} font-sans h-full w-full bg-th-bkg-1 font-body ${
} h-full w-full bg-th-bkg-1 font-body ${
fullScreen
? ''
: 'p-4 pt-6 sm:h-auto sm:max-w-md sm:rounded-lg sm:border sm:border-th-bkg-3 sm:p-6'

View File

@ -1,6 +1,5 @@
import { useTranslation } from 'next-i18next'
import { formatCurrencyValue } from 'utils/numbers'
import FormatNumericValue from './FormatNumericValue'
const getPnlColor = (pnl: number) => {
return pnl < 0 ? 'text-th-down' : pnl > 0 ? 'text-th-up' : 'text-th-fgd-3'
@ -11,13 +10,11 @@ const PnlTooltipContent = ({
realizedPnl,
totalPnl,
unsettledPnl,
roe,
}: {
unrealizedPnl: number
realizedPnl: number
totalPnl: number
unsettledPnl: number
roe: number
}) => {
const { t } = useTranslation(['common', 'trade'])
return (
@ -35,20 +32,13 @@ const PnlTooltipContent = ({
{formatCurrencyValue(realizedPnl, 2)}
</span>
</div>
<div className="flex justify-between pt-1.5">
<div className="flex justify-between py-1.5">
<p className="mr-3">{t('trade:total-pnl')}</p>
<span className={`font-mono ${getPnlColor(totalPnl)}`}>
{formatCurrencyValue(totalPnl, 2)}
</span>
</div>
<div className="flex justify-between border-b border-th-bkg-4 pb-3">
<p className="mr-3">{t('trade:return-on-equity')}</p>
<span className={`font-mono ${getPnlColor(roe)}`}>
<FormatNumericValue classNames="text-xs" value={roe} decimals={2} />
%
</span>
</div>
<div className="flex justify-between pt-1.5">
<div className="flex justify-between border-t border-th-bkg-4 pt-3">
<p className="mr-3">
{t('trade:unsettled')} {t('pnl')}
</p>

View File

@ -25,7 +25,7 @@ const TabButtons = <T extends Values>({
{values.map(([label, count], i) => (
<div className={fillWidth ? 'flex-1' : ''} key={`${label}` + i}>
<button
className={`flex h-12 w-full items-center justify-center px-4 font-normal focus-visible:bg-th-bkg-2 focus-visible:text-th-fgd-1 md:px-6 ${
className={`flex h-12 w-full items-center justify-center px-4 font-normal focus-visible:bg-th-bkg-2 focus-visible:text-th-fgd-1 md:px-6 ${
rounded ? 'rounded-md' : 'rounded-none'
} ${
showBorders
@ -39,7 +39,7 @@ const TabButtons = <T extends Values>({
? 'bg-th-up-dark font-display text-th-fgd-1'
: label === 'sell'
? 'bg-th-down-dark font-display text-th-fgd-1'
: 'bg-th-bkg-2 text-th-active'
: 'bg-th-bkg-3 text-th-active'
: 'hover:cursor-pointer hover:text-th-fgd-2'
}`}
key={`${label}${i}`}
@ -57,7 +57,7 @@ const TabButtons = <T extends Values>({
{count ? (
<div
className={`ml-1.5 rounded ${
label === activeValue ? 'bg-th-bkg-4' : 'bg-th-bkg-3'
label === activeValue ? 'bg-th-bkg-1' : 'bg-th-bkg-3'
} px-1.5 font-mono text-xxs text-th-fgd-2`}
>
{count}

View File

@ -45,14 +45,14 @@ const TabUnderline = <T extends Values>({
width: `${100 / values.length}%`,
}}
/>
<nav className="-mb-px flex space-x-2" aria-label="Tabs">
<nav className="-mb-px flex" aria-label="Tabs">
{values.map((value, i) => (
<button
onClick={() => onChange(value)}
className={`relative flex h-10 w-1/2 ${
fillWidth ? '' : 'max-w-[176px]'
}
cursor-pointer items-center justify-center whitespace-nowrap rounded py-1 focus-visible:text-th-fgd-2 md:h-auto md:rounded-none md:hover:opacity-100 ${
cursor-pointer items-center justify-center whitespace-nowrap rounded py-1 focus-visible:text-th-fgd-2 md:h-auto md:rounded-none md:hover:opacity-100 ${
small ? 'text-sm' : 'text-sm lg:text-base'
}
${

View File

@ -125,7 +125,6 @@ const PerpPositionsStatsTable = ({
realizedPnl={realizedPnl}
totalPnl={totalPnl}
unsettledPnl={unsettledPnl}
roe={roe}
/>
}
delay={100}
@ -316,7 +315,6 @@ const PerpPositionsStatsTable = ({
realizedPnl={realizedPnl}
totalPnl={totalPnl}
unsettledPnl={unsettledPnl}
roe={roe}
/>
}
delay={100}

View File

@ -14,6 +14,7 @@ import { OUTPUT_TOKEN_DEFAULT } from 'utils/constants'
import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm'
import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { SwapFormTokenListType } from './SwapFormTokenList'
const BuyTokenInput = ({
error,
@ -25,7 +26,7 @@ const BuyTokenInput = ({
error?: string
handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void
loading?: boolean
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
handleRepay?: (amountOut: string) => void
}) => {
const { t } = useTranslation('common')
@ -70,7 +71,7 @@ const BuyTokenInput = ({
</div>
<div className="relative col-span-1">
{loading ? (
<div className="flex h-[56px] w-full items-center justify-center rounded-l-none rounded-r-lg bg-th-input-bkg">
<div className="flex h-[56px] w-full items-center justify-center rounded-l-none rounded-r-lg border-l border-th-bkg-2 bg-th-input-bkg">
<Loading />
</div>
) : (

View File

@ -7,12 +7,7 @@ import {
SetStateAction,
useLayoutEffect,
} from 'react'
import {
ArrowDownIcon,
ArrowDownTrayIcon,
ArrowsRightLeftIcon,
LinkIcon,
} from '@heroicons/react/20/solid'
import { ArrowsRightLeftIcon, LinkIcon } from '@heroicons/react/20/solid'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
@ -24,10 +19,14 @@ import { SIZE_INPUT_UI_KEY } from '../../utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import SwapSlider from './SwapSlider'
import PercentageSelectButtons from './PercentageSelectButtons'
import { floorToDecimal } from 'utils/numbers'
import {
floorToDecimal,
floorToDecimalSignificance,
formatCurrencyValue,
} from 'utils/numbers'
import { withValueLimit } from './MarketSwapForm'
import SellTokenInput from './SellTokenInput'
import BuyTokenInput from './BuyTokenInput'
import ReduceInputTokenInput from './ReduceInputTokenInput'
import ReduceOutputTokenInput from './ReduceOutputTokenInput'
import { notify } from 'utils/notifications'
import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
@ -37,53 +36,68 @@ import TokenLogo from '@components/shared/TokenLogo'
import InlineNotification from '@components/shared/InlineNotification'
import Select from '@components/forms/Select'
import useIpAddress from 'hooks/useIpAddress'
import { Bank, toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { Bank } from '@blockworks-foundation/mango-v4'
import useMangoAccount from 'hooks/useMangoAccount'
import { useWallet } from '@solana/wallet-adapter-react'
import { useTokenMax } from './useTokenMax'
import DepositWithdrawModal from '@components/modals/DepositWithdrawModal'
import { useAbsInputPosition } from './useTokenMax'
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { formatTokenSymbol } from 'utils/tokens'
import Tooltip from '@components/shared/Tooltip'
dayjs.extend(relativeTime)
const priceToDisplayString = (price: number | Decimal | string): string => {
const val = floorToDecimalSignificance(price, 6)
return val.toFixed(val.dp())
}
type LimitSwapFormProps = {
showTokenSelect: 'input' | 'output' | undefined
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
showTokenSelect: SwapFormTokenListType
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
}
type LimitSwapForm = {
amountIn: number
hasBorrows: number | undefined
triggerPrice: string
}
type FormErrors = Partial<Record<keyof LimitSwapForm, string>>
type OrderTypeMultiplier = 0.9 | 1 | 1.1
enum OrderTypes {
STOP_LOSS = 'trade:stop-loss',
TAKE_PROFIT = 'trade:take-profit',
REPAY_BORROW = 'repay-borrow',
}
const ORDER_TYPES = [
OrderTypes.STOP_LOSS,
OrderTypes.TAKE_PROFIT,
OrderTypes.REPAY_BORROW,
]
const ORDER_TYPES = [OrderTypes.STOP_LOSS, OrderTypes.TAKE_PROFIT]
const set = mangoStore.getState().set
const getSellTokenBalance = (inputBank: Bank | undefined) => {
export const getInputTokenBalance = (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) => {
const getOutputTokenBalance = (outputBank: Bank | undefined) => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!outputBank || !mangoAccount) return 0
const balance = mangoAccount.getTokenBalanceUi(outputBank)
return balance
}
const getOrderTypeMultiplier = (
orderType: OrderTypes,
flipPrices: boolean,
reducingShort: boolean,
) => {
if (orderType === OrderTypes.STOP_LOSS) {
return flipPrices ? 1.1 : 0.9
return reducingShort ? (flipPrices ? 0.9 : 1.1) : flipPrices ? 1.1 : 0.9
} else if (orderType === OrderTypes.TAKE_PROFIT) {
return flipPrices ? 0.9 : 1.1
return reducingShort ? (flipPrices ? 1.1 : 0.9) : flipPrices ? 0.9 : 1.1
} else {
return 1
}
@ -94,16 +108,15 @@ const LimitSwapForm = ({
setShowTokenSelect,
}: LimitSwapFormProps) => {
const { t } = useTranslation(['common', 'swap', 'trade'])
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { mangoAccountAddress } = useMangoAccount()
const { ipAllowed, ipCountry } = useIpAddress()
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
const [triggerPrice, setTriggerPrice] = useState('')
// const [triggerPrice, setTriggerPrice] = useState('')
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [orderTypeMultiplier, setOrderTypeMultiplier] =
useState<OrderTypeMultiplier | null>(null)
const [submitting, setSubmitting] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [formErrors, setFormErrors] = useState<FormErrors>({})
const { remainingBorrowsInPeriod, timeToNextPeriod } =
useRemainingBorrowsInPeriod(true)
const {
inputBank,
@ -111,11 +124,10 @@ const LimitSwapForm = ({
amountIn: amountInFormValue,
amountOut: amountOutFormValue,
flipPrices,
triggerPrice,
} = mangoStore((s) => s.swap)
const { connected, connect } = useWallet()
const { amount: tokenMax } = useTokenMax()
const [showDepositModal, setShowDepositModal] = useState(false)
const [inputBankName, outputBankName, inputBankDecimals, outputBankDecimals] =
useMemo(() => {
@ -140,17 +152,6 @@ const LimitSwapForm = ({
: new Decimal(0)
}, [amountOutFormValue])
const freeCollateral = useMemo(() => {
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
return group && mangoAccount
? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group))
: 0
}, [mangoAccountAddress])
const showInsufficientBalance =
tokenMax.lt(amountInAsDecimal) || tokenMax.eq(0)
const setAmountInFormValue = useCallback((amountIn: string) => {
set((s) => {
s.swap.amountIn = amountIn
@ -171,28 +172,34 @@ const LimitSwapForm = ({
const quotePrice = useMemo(() => {
if (!inputBank || !outputBank) return 0
const quote = flipPrices
? floorToDecimal(
outputBank.uiPrice / inputBank.uiPrice,
inputBank.mintDecimals,
).toNumber()
: floorToDecimal(
inputBank.uiPrice / outputBank.uiPrice,
outputBank.mintDecimals,
).toNumber()
return quote
return flipPrices
? outputBank.uiPrice / inputBank.uiPrice
: inputBank.uiPrice / outputBank.uiPrice
}, [flipPrices, inputBank, outputBank])
const isReducingShort = useMemo(() => {
if (!mangoAccountAddress || !inputBank) return false
const inputBalance = getInputTokenBalance(inputBank)
return inputBalance < 0
}, [inputBank, mangoAccountAddress])
// set default trigger price
useEffect(() => {
if (!quotePrice || triggerPrice || showTokenSelect) return
const multiplier = getOrderTypeMultiplier(OrderTypes.STOP_LOSS, flipPrices)
const decimals = !flipPrices ? inputBankDecimals : outputBankDecimals
setTriggerPrice((quotePrice * multiplier).toFixed(decimals))
const multiplier = getOrderTypeMultiplier(
orderType,
flipPrices,
isReducingShort,
)
set((state) => {
state.swap.triggerPrice = priceToDisplayString(
new Decimal(quotePrice).mul(new Decimal(multiplier)),
)
})
}, [
flipPrices,
inputBankDecimals,
outputBankDecimals,
isReducingShort,
orderType,
quotePrice,
showTokenSelect,
triggerPrice,
@ -201,10 +208,17 @@ const LimitSwapForm = ({
// flip trigger price and set amount out when chart direction is flipped
useLayoutEffect(() => {
if (!quotePrice) return
const multiplier = getOrderTypeMultiplier(orderType, flipPrices)
const decimals = flipPrices ? inputBankDecimals : outputBankDecimals
const price = (quotePrice * multiplier).toFixed(decimals)
setTriggerPrice(price)
const multiplier = getOrderTypeMultiplier(
orderType,
flipPrices,
isReducingShort,
)
const price = priceToDisplayString(
new Decimal(quotePrice).mul(new Decimal(multiplier)),
)
set((state) => {
state.swap.triggerPrice = price
})
if (amountInAsDecimal?.gt(0)) {
const amountOut = getAmountOut(
amountInAsDecimal.toString(),
@ -213,7 +227,7 @@ const LimitSwapForm = ({
)
setAmountOutFormValue(amountOut.toString())
}
}, [flipPrices, inputBankDecimals, orderType, outputBankDecimals])
}, [flipPrices, orderType, isReducingShort])
const triggerPriceDifference = useMemo(() => {
if (!quotePrice) return 0
@ -223,22 +237,37 @@ const LimitSwapForm = ({
return triggerDifference
}, [quotePrice, triggerPrice])
const handleTokenSelect = (type: 'input' | 'output') => {
const handleTokenSelect = (type: SwapFormTokenListType) => {
setShowTokenSelect(type)
setFormErrors({})
setTriggerPrice('')
set((state) => {
state.swap.triggerPrice = ''
})
}
const hasBorrowToRepay = useMemo(() => {
if (orderType !== OrderTypes.REPAY_BORROW || !outputBank || !mangoAccount)
return
const balance = mangoAccount.getTokenBalanceUi(outputBank)
const roundedBalance = floorToDecimal(
balance,
outputBank.mintDecimals,
).toNumber()
return balance && balance < 0 ? Math.abs(roundedBalance) : 0
}, [mangoAccount, orderType, outputBank])
// check if the borrowed amount exceeds the net borrow limit in the current period. Only currently applies to reducing shorts
const borrowExceedsLimitInPeriod = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (
!mangoAccount ||
!outputBank ||
!isReducingShort ||
!remainingBorrowsInPeriod
)
return false
const balance = mangoAccount.getTokenDepositsUi(outputBank)
const remainingBalance = balance - amountOutAsDecimal.toNumber()
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
return borrowAmount > remainingBorrowsInPeriod
}, [
amountOutAsDecimal,
outputBank,
isReducingShort,
mangoAccountAddress,
remainingBorrowsInPeriod,
])
const isFormValid = useCallback(
(form: LimitSwapForm) => {
@ -249,7 +278,8 @@ const LimitSwapForm = ({
'triggerPrice',
]
const triggerPriceNumber = parseFloat(form.triggerPrice)
const sellTokenBalance = getSellTokenBalance(inputBank)
const inputTokenBalance = getInputTokenBalance(inputBank)
const shouldFlip = flipPrices !== isReducingShort
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
@ -257,29 +287,26 @@ const LimitSwapForm = ({
}
}
if (orderType === OrderTypes.STOP_LOSS) {
if (flipPrices && triggerPriceNumber <= quotePrice) {
if (shouldFlip && triggerPriceNumber <= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be above oracle price'
}
if (!flipPrices && triggerPriceNumber >= quotePrice) {
if (!shouldFlip && triggerPriceNumber >= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be below oracle price'
}
}
if (orderType === OrderTypes.TAKE_PROFIT) {
if (flipPrices && triggerPriceNumber >= quotePrice) {
if (shouldFlip && triggerPriceNumber >= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be below oracle price'
}
if (!flipPrices && triggerPriceNumber <= quotePrice) {
if (!shouldFlip && 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) {
if (form.amountIn > Math.abs(inputTokenBalance)) {
invalidFields.amountIn = t('swap:insufficient-balance', {
symbol: inputBank?.name,
})
@ -291,22 +318,14 @@ const LimitSwapForm = ({
},
[
flipPrices,
hasBorrowToRepay,
inputBank,
isReducingShort,
orderType,
quotePrice,
setFormErrors,
],
)
// set order type multiplier on page load
useEffect(() => {
if (!orderTypeMultiplier) {
const multiplier = getOrderTypeMultiplier(orderType, flipPrices)
setOrderTypeMultiplier(multiplier)
}
}, [flipPrices, orderType, orderTypeMultiplier])
// get the out amount from the in amount and trigger or limit price
const getAmountOut = useCallback(
(amountIn: string, flipPrices: boolean, price: string) => {
@ -358,23 +377,6 @@ const LimitSwapForm = ({
],
)
const handleRepay = useCallback(
(amountOut: string) => {
setAmountOutFormValue(amountOut)
if (parseFloat(amountOut) > 0 && triggerPrice) {
const amountIn = getAmountIn(amountOut, flipPrices, triggerPrice)
setAmountInFormValue(amountIn.toString())
}
},
[
flipPrices,
getAmountIn,
setAmountInFormValue,
setAmountOutFormValue,
triggerPrice,
],
)
const handleAmountInChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
@ -438,60 +440,20 @@ const LimitSwapForm = ({
(e: NumberFormatValues, info: SourceInfo) => {
if (info.source !== 'event') return
setFormErrors({})
setTriggerPrice(e.value)
set((state) => {
state.swap.triggerPrice = e.value
})
if (parseFloat(e.value) > 0 && parseFloat(amountInFormValue) > 0) {
const amountOut = getAmountOut(amountInFormValue, flipPrices, e.value)
setAmountOutFormValue(amountOut.toString())
}
},
[amountInFormValue, flipPrices, setFormErrors, setTriggerPrice],
[amountInFormValue, flipPrices, setFormErrors],
)
const handleSwitchTokens = useCallback(() => {
if (!inputBank || !outputBank) return
setFormErrors({})
set((s) => {
s.swap.inputBank = outputBank
s.swap.outputBank = inputBank
})
const multiplier = getOrderTypeMultiplier(orderType, flipPrices)
const price = flipPrices
? floorToDecimal(
(inputBank.uiPrice / outputBank.uiPrice) * multiplier,
outputBank.mintDecimals,
).toString()
: floorToDecimal(
(outputBank.uiPrice / inputBank.uiPrice) * multiplier,
inputBank.mintDecimals,
).toString()
setTriggerPrice(price)
if (amountInAsDecimal?.gt(0)) {
const amountOut = getAmountOut(
amountInAsDecimal.toString(),
flipPrices,
price,
)
setAmountOutFormValue(amountOut.toString())
}
setAnimateSwitchArrow(
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1,
)
}, [
amountInAsDecimal,
flipPrices,
inputBank,
orderType,
outputBank,
setAmountInFormValue,
setFormErrors,
triggerPrice,
])
const handlePlaceStopLoss = useCallback(async () => {
const invalidFields = isFormValid({
amountIn: amountInAsDecimal.toNumber(),
hasBorrows: hasBorrowToRepay,
triggerPrice,
})
if (Object.keys(invalidFields).length) {
@ -511,65 +473,66 @@ const LimitSwapForm = ({
const amountIn = amountInAsDecimal.toNumber()
const isReduceLong = !isReducingShort
try {
let tx
if (orderType === OrderTypes.REPAY_BORROW) {
const amountOut = amountOutAsDecimal.toNumber()
const orderPrice = parseFloat(triggerPrice)
if (quotePrice > orderPrice) {
tx = await client.tcsStopLossOnBorrow(
if (orderType === OrderTypes.STOP_LOSS) {
if (isReduceLong) {
tx = await client.tcsStopLossOnDeposit(
group,
mangoAccount,
inputBank,
outputBank,
orderPrice,
parseFloat(triggerPrice),
flipPrices,
amountOut,
amountIn,
null,
null,
)
} else {
tx = await client.tcsStopLossOnBorrow(
group,
mangoAccount,
outputBank,
inputBank,
parseFloat(triggerPrice),
!flipPrices,
amountIn,
null,
true,
null,
)
}
}
if (orderType === OrderTypes.TAKE_PROFIT) {
if (isReduceLong) {
tx = await client.tcsTakeProfitOnDeposit(
group,
mangoAccount,
inputBank,
outputBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
false,
null,
)
} else {
tx = await client.tcsTakeProfitOnBorrow(
group,
mangoAccount,
inputBank,
outputBank,
orderPrice,
flipPrices,
amountOut,
inputBank,
parseFloat(triggerPrice),
!flipPrices,
amountIn,
null,
false,
true,
null,
)
}
}
if (orderType === OrderTypes.STOP_LOSS) {
tx = await client.tcsStopLossOnDeposit(
group,
mangoAccount,
inputBank,
outputBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
null,
)
}
if (orderType === OrderTypes.TAKE_PROFIT) {
tx = await client.tcsTakeProfitOnDeposit(
group,
mangoAccount,
inputBank,
outputBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
null,
)
}
notify({
title: 'Transaction confirmed',
type: 'success',
@ -596,13 +559,13 @@ const LimitSwapForm = ({
setSubmitting(false)
}
}, [
hasBorrowToRepay,
flipPrices,
orderType,
quotePrice,
triggerPrice,
amountInAsDecimal,
amountOutFormValue,
isReducingShort,
])
const orderDescription = useMemo(() => {
@ -615,64 +578,72 @@ const LimitSwapForm = ({
)
return
const quoteString = flipPrices
? `${inputBankName} per ${outputBankName}`
: `${outputBankName} per ${inputBankName}`
const formattedInputTokenName = formatTokenSymbol(inputBankName)
const formattedOutputTokenName = formatTokenSymbol(outputBankName)
if (hasBorrowToRepay && orderType === OrderTypes.REPAY_BORROW) {
const amountOut = floorToDecimal(
amountOutFormValue,
outputBankDecimals,
).toNumber()
if (amountOut <= hasBorrowToRepay) {
return t('trade:repay-borrow-order-desc', {
amount: amountOut,
priceUnit: quoteString,
symbol: outputBankName,
triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals),
})
} else {
const depositAmount = floorToDecimal(
amountOut - hasBorrowToRepay,
outputBankDecimals,
).toNumber()
return t('trade:repay-borrow-deposit-order-desc', {
borrowAmount: hasBorrowToRepay,
depositAmount: depositAmount,
priceUnit: quoteString,
symbol: outputBankName,
triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals),
})
const quoteString = flipPrices
? `${formattedInputTokenName} per ${formattedOutputTokenName}`
: `${formattedOutputTokenName} per ${formattedInputTokenName}`
const action = isReducingShort ? t('buy') : t('sell')
// calc borrowed amount when reducing short
let borrowToReduceShort = 0
if (isReducingShort && mangoAccountAddress) {
const balance = getOutputTokenBalance(outputBank)
if (balance >= 0 && parseFloat(amountOutFormValue) > balance) {
const amount = new Decimal(balance)
.sub(new Decimal(amountOutFormValue))
.toNumber()
borrowToReduceShort = Math.abs(amount)
}
} else {
const orderTypeString =
orderType === OrderTypes.STOP_LOSS
? !flipPrices
? t('trade:falls-to')
: t('trade:rises-to')
: !flipPrices
if (balance < 0) {
borrowToReduceShort = parseFloat(amountOutFormValue)
}
}
// xor of two flip flags
const shouldFlip = flipPrices !== isReducingShort
const orderTypeString =
orderType === OrderTypes.STOP_LOSS
? shouldFlip
? t('trade:rises-to')
: t('trade:falls-to')
: shouldFlip
? t('trade:falls-to')
: t('trade:rises-to')
return t('trade:trigger-order-desc', {
amount: floorToDecimal(amountInFormValue, inputBankDecimals),
orderType: orderTypeString,
priceUnit: quoteString,
symbol: inputBankName,
triggerPrice: floorToDecimal(triggerPrice, inputBankDecimals),
})
}
return borrowToReduceShort
? t('trade:trigger-order-desc-with-borrow', {
action: action,
amount: floorToDecimal(amountInFormValue, inputBankDecimals),
borrowAmount: borrowToReduceShort,
orderType: orderTypeString,
priceUnit: quoteString,
quoteSymbol: formattedOutputTokenName,
symbol: formattedInputTokenName,
triggerPrice: priceToDisplayString(triggerPrice),
})
: t('trade:trigger-order-desc', {
action: action,
amount: floorToDecimal(amountInFormValue, inputBankDecimals),
orderType: orderTypeString,
priceUnit: quoteString,
symbol: formattedInputTokenName,
triggerPrice: priceToDisplayString(triggerPrice),
})
}, [
amountInFormValue,
amountOutFormValue,
flipPrices,
hasBorrowToRepay,
inputBankDecimals,
inputBankName,
mangoAccountAddress,
orderType,
outputBankDecimals,
outputBankName,
triggerPrice,
isReducingShort,
])
const triggerPriceSuffix = useMemo(() => {
@ -700,10 +671,17 @@ const LimitSwapForm = ({
setFormErrors({})
const newType = type as OrderTypes
setOrderType(newType)
const triggerMultiplier = getOrderTypeMultiplier(newType, flipPrices)
setOrderTypeMultiplier(triggerMultiplier)
const trigger = (quotePrice * triggerMultiplier).toString()
setTriggerPrice(trigger)
const triggerMultiplier = getOrderTypeMultiplier(
newType,
flipPrices,
isReducingShort,
)
const trigger = priceToDisplayString(
quotePrice * triggerMultiplier,
).toString()
set((state) => {
state.swap.triggerPrice = trigger
})
if (amountInAsDecimal.gt(0)) {
const amountOut = getAmountOut(
amountInAsDecimal.toString(),
@ -713,27 +691,78 @@ const LimitSwapForm = ({
setAmountOutFormValue(amountOut)
}
},
[flipPrices, quotePrice, setFormErrors, setOrderTypeMultiplier],
[flipPrices, quotePrice, setFormErrors, isReducingShort],
)
const onClick = !connected
? connect
: showInsufficientBalance || freeCollateral <= 0
? () => setShowDepositModal(true)
: handlePlaceStopLoss
const onClick = !connected ? connect : handlePlaceStopLoss
return (
<>
<SellTokenInput
<div className="mb-3">
<InlineNotification
desc={
<div className="flex">
<span className="mr-1">{t('swap:trigger-beta')}</span>
<Tooltip
content={
<ul className="ml-4 list-disc space-y-2">
<li>
Trigger orders on long-tail assets could be susceptible to
oracle manipulation.
</li>
<li>
Trigger orders rely on a sufficient amount of well
collateralized liquidators.
</li>
<li>
The slippage on existing orders could be higher/lower than
what&apos;s estimated.
</li>
<li>
The amount of tokens used to fill your order can vary and
depends on the final execution price.
</li>
</ul>
}
>
<span className="tooltip-underline">
{t('swap:important-info')}
</span>
</Tooltip>
</div>
}
type="info"
/>
</div>
<ReduceInputTokenInput
className="rounded-b-none"
error={formErrors.amountIn}
handleAmountInChange={handleAmountInChange}
setShowTokenSelect={() => handleTokenSelect('input')}
setShowTokenSelect={() => handleTokenSelect('reduce-input')}
handleMax={handleMax}
isTriggerOrder
/>
<div className="bg-th-bkg-2 p-3 pt-0">
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={false}
amount={amountInAsDecimal.toNumber()}
onChange={(v) => handleAmountInUi(v)}
step={1 / 10 ** (inputBankDecimals || 6)}
maxAmount={useAbsInputPosition}
/>
) : (
<div className="-mt-2">
<PercentageSelectButtons
amountIn={amountInAsDecimal.toString()}
setAmountIn={(v) => handleAmountInUi(v)}
useMargin={false}
/>
</div>
)}
</div>
<div
className={`grid grid-cols-2 gap-2 rounded-b-xl bg-th-bkg-2 p-3 pt-1`}
className={`grid grid-cols-2 gap-2 bg-th-bkg-2 p-3 pt-1`}
id="swap-step-two"
>
<div className="col-span-1">
@ -760,7 +789,8 @@ const LimitSwapForm = ({
}`}
>
{triggerPriceDifference
? triggerPriceDifference.toFixed(2)
? (triggerPriceDifference > 0 ? '+' : '') +
triggerPriceDifference.toFixed(2)
: '0.00'}
%
</p>
@ -772,9 +802,6 @@ const LimitSwapForm = ({
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={
flipPrices ? inputBankDecimals : outputBankDecimals || 6
}
name="triggerPrice"
id="triggerPrice"
className="h-10 w-full rounded-lg bg-th-input-bkg p-3 pl-8 font-mono text-sm text-th-fgd-1 focus:outline-none md:hover:bg-th-bkg-1"
@ -812,74 +839,24 @@ const LimitSwapForm = ({
</div>
) : null}
</div>
<div className="my-2 flex justify-center">
<button
className="rounded-full border border-th-fgd-4 p-1.5 text-th-fgd-3 focus-visible:border-th-active md:hover:border-th-active md:hover:text-th-active"
onClick={handleSwitchTokens}
>
<ArrowDownIcon
className="h-5 w-5"
style={
animateSwitchArrow % 2 == 0
? { transform: 'rotate(0deg)' }
: { transform: 'rotate(360deg)' }
}
/>
</button>
</div>
<BuyTokenInput
error={formErrors.hasBorrows}
<ReduceOutputTokenInput
handleAmountOutChange={handleAmountOutChange}
setShowTokenSelect={() => handleTokenSelect('output')}
handleRepay={
orderType === OrderTypes.REPAY_BORROW ? handleRepay : undefined
}
setShowTokenSelect={() => handleTokenSelect('reduce-output')}
/>
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={false}
amount={amountInAsDecimal.toNumber()}
onChange={(v) => handleAmountInUi(v)}
step={1 / 10 ** (inputBankDecimals || 6)}
/>
) : (
<PercentageSelectButtons
amountIn={amountInAsDecimal.toString()}
setAmountIn={(v) => handleAmountInUi(v)}
useMargin={false}
/>
)}
{orderType === OrderTypes.REPAY_BORROW &&
!hasBorrowToRepay ? null : orderDescription ? (
{orderDescription ? (
<div className="mt-4">
<InlineNotification
desc={
<>
{orderType !== OrderTypes.REPAY_BORROW ? (
<>
<span className="text-th-down">{t('sell')}</span>{' '}
</>
) : null}
{orderDescription}
</>
}
type="info"
/>
<InlineNotification desc={orderDescription} type="info" />
</div>
) : null}
{ipAllowed ? (
<Button
disabled={borrowExceedsLimitInPeriod}
onClick={onClick}
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
size="large"
>
{connected ? (
showInsufficientBalance || freeCollateral <= 0 ? (
<div className="flex items-center">
<ArrowDownTrayIcon className="mr-2 h-5 w-5 flex-shrink-0" />
{t('swap:deposit-funds')}
</div>
) : submitting ? (
submitting ? (
<Loading />
) : (
<span>{t('swap:place-limit-order')}</span>
@ -902,13 +879,18 @@ const LimitSwapForm = ({
})}
</Button>
)}
{showDepositModal ? (
<DepositWithdrawModal
action="deposit"
isOpen={showDepositModal}
onClose={() => setShowDepositModal(false)}
token={freeCollateral > 0 ? inputBankName : ''}
/>
{borrowExceedsLimitInPeriod &&
remainingBorrowsInPeriod &&
timeToNextPeriod ? (
<div className="mb-4">
<InlineNotification
type="error"
desc={t('error-borrow-exceeds-limit', {
remaining: formatCurrencyValue(remainingBorrowsInPeriod),
resetTime: dayjs().to(dayjs().add(timeToNextPeriod, 'second')),
})}
/>
</div>
) : null}
</>
)

View File

@ -31,12 +31,19 @@ import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import DepositWithdrawModal from '@components/modals/DepositWithdrawModal'
import useMangoAccountAccounts from 'hooks/useMangoAccountAccounts'
import Link from 'next/link'
import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { formatCurrencyValue } from 'utils/numbers'
import { SwapFormTokenListType } from './SwapFormTokenList'
import useTokenPositionsFull from 'hooks/useTokenPositionsFull'
dayjs.extend(relativeTime)
type MarketSwapFormProps = {
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
}
const MAX_DIGITS = 11
@ -243,6 +250,25 @@ const MarketSwapForm = ({ setShowTokenSelect }: MarketSwapFormProps) => {
setShowTokenSelect={setShowTokenSelect}
handleMax={handleMax}
/>
<div className="rounded-b-xl bg-th-bkg-2 p-3 pt-0">
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={useMargin}
amount={amountInAsDecimal.toNumber()}
onChange={(v) => setAmountInFormValue(v, true)}
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
maxAmount={useTokenMax}
/>
) : (
<div className="-mt-2">
<PercentageSelectButtons
amountIn={amountInAsDecimal.toString()}
setAmountIn={(v) => setAmountInFormValue(v, true)}
useMargin={useMargin}
/>
</div>
)}
</div>
<div className="my-2 flex justify-center">
<button
className="rounded-full border border-th-fgd-4 p-1.5 text-th-fgd-3 focus-visible:border-th-active md:hover:border-th-active md:hover:text-th-active"
@ -264,20 +290,6 @@ const MarketSwapForm = ({ setShowTokenSelect }: MarketSwapFormProps) => {
setShowTokenSelect={setShowTokenSelect}
handleRepay={handleRepay}
/>
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={useMargin}
amount={amountInAsDecimal.toNumber()}
onChange={(v) => setAmountInFormValue(v, true)}
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
/>
) : (
<PercentageSelectButtons
amountIn={amountInAsDecimal.toString()}
setAmountIn={(v) => setAmountInFormValue(v, true)}
useMargin={useMargin}
/>
)}
{ipAllowed ? (
<SwapFormSubmitButton
loadingSwapDetails={loadingSwapDetails}
@ -291,7 +303,7 @@ const MarketSwapForm = ({ setShowTokenSelect }: MarketSwapFormProps) => {
) : (
<Button
disabled
className="mb-4 mt-6 w-full leading-tight"
className="mb-4 mt-6 flex w-full items-center justify-center text-base"
size="large"
>
{t('country-not-allowed', {
@ -327,25 +339,10 @@ const SwapFormSubmitButton = ({
const { connected } = useWallet()
const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin)
const [showDepositModal, setShowDepositModal] = useState(false)
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const { inputBank, outputBank } = mangoStore((s) => s.swap)
const tokenPositionsFull = useMemo(() => {
if (!inputBank || !outputBank || !usedTokens.length || !totalTokens.length)
return false
const hasInputTokenPosition = usedTokens.find(
(token) => token.tokenIndex === inputBank.tokenIndex,
)
const hasOutputTokenPosition = usedTokens.find(
(token) => token.tokenIndex === outputBank.tokenIndex,
)
if (
(hasInputTokenPosition && hasOutputTokenPosition) ||
totalTokens.length - usedTokens.length >= 2
) {
return false
} else return true
}, [inputBank, outputBank, usedTokens, totalTokens])
const { remainingBorrowsInPeriod, timeToNextPeriod } =
useRemainingBorrowsInPeriod(true)
const tokenPositionsFull = useTokenPositionsFull(outputBank, inputBank)
const freeCollateral = useMemo(() => {
const group = mangoStore.getState().group
@ -359,11 +356,27 @@ const SwapFormSubmitButton = ({
? amountWithBorrow.lt(amountIn) || amountWithBorrow.eq(0)
: tokenMax.lt(amountIn) || tokenMax.eq(0)
// check if the borrowed amount exceeds the net borrow limit in the current period
const borrowExceedsLimitInPeriod = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount || !inputBank || !remainingBorrowsInPeriod) return false
const balance = mangoAccount.getTokenDepositsUi(inputBank)
const remainingBalance = balance - amountIn.toNumber()
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
return borrowAmount > remainingBorrowsInPeriod
}, [amountIn, inputBank, mangoAccountAddress, remainingBorrowsInPeriod])
const disabled =
connected &&
!showInsufficientBalance &&
freeCollateral > 0 &&
(!amountIn.toNumber() || !amountOut || !selectedRoute || tokenPositionsFull)
(!amountIn.toNumber() ||
!amountOut ||
!selectedRoute ||
tokenPositionsFull ||
borrowExceedsLimitInPeriod)
const onClick =
showInsufficientBalance || freeCollateral <= 0
@ -411,6 +424,19 @@ const SwapFormSubmitButton = ({
/>
</div>
) : null}
{borrowExceedsLimitInPeriod &&
remainingBorrowsInPeriod &&
timeToNextPeriod ? (
<div className="mb-4">
<InlineNotification
type="error"
desc={t('error-borrow-exceeds-limit', {
remaining: formatCurrencyValue(remainingBorrowsInPeriod),
resetTime: dayjs().to(dayjs().add(timeToNextPeriod, 'second')),
})}
/>
</div>
) : null}
{selectedRoute === null && amountIn.gt(0) ? (
<div className="mb-4">
<InlineNotification type="error" desc={t('swap:no-swap-found')} />

View File

@ -3,22 +3,20 @@ import mangoStore from '@store/mangoStore'
import Decimal from 'decimal.js'
import { useTranslation } from 'next-i18next'
import { floorToDecimal } from 'utils/numbers'
import { useTokenMax } from './useTokenMax'
import { TokenMaxResults } from './useTokenMax'
const MaxSwapAmount = ({
setAmountIn,
useMargin,
maxAmount,
}: {
setAmountIn: (x: string) => void
useMargin: boolean
maxAmount: (useMargin: boolean) => TokenMaxResults
}) => {
const { t } = useTranslation('common')
const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad)
const {
amount: tokenMax,
amountWithBorrow,
decimals,
} = useTokenMax(useMargin)
const { amount: tokenMax, amountWithBorrow, decimals } = maxAmount(useMargin)
if (mangoAccountLoading) return null

View File

@ -0,0 +1,126 @@
import TokenSelect from './TokenSelect'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import { formatCurrencyValue } from 'utils/numbers'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction, useMemo } from 'react'
import mangoStore from '@store/mangoStore'
import useMangoGroup from 'hooks/useMangoGroup'
import { INPUT_TOKEN_DEFAULT } from 'utils/constants'
import { NUMBER_FORMAT_CLASSNAMES, withValueLimit } from './MarketSwapForm'
import MaxSwapAmount from './MaxSwapAmount'
import useUnownedAccount from 'hooks/useUnownedAccount'
import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { useAbsInputPosition } from './useTokenMax'
const ReduceInputTokenInput = ({
handleAmountInChange,
setShowTokenSelect,
handleMax,
className,
error,
isTriggerOrder,
}: {
handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
handleMax: (amountIn: string) => void
className?: string
error?: string
isTriggerOrder?: boolean
}) => {
const { t } = useTranslation(['common', 'swap'])
const { mangoAccountAddress } = useMangoAccount()
const { group } = useMangoGroup()
const { isUnownedAccount } = useUnownedAccount()
const {
margin: useMargin,
inputBank,
amountIn: amountInFormValue,
} = mangoStore((s) => s.swap)
const freeCollateral = useMemo(() => {
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
return group && mangoAccount
? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group))
: 10
}, [mangoAccountAddress])
return (
<div
className={`grid grid-cols-2 rounded-t-xl bg-th-bkg-2 p-3 pb-2 ${className}`}
>
<div className="col-span-2 mb-2 flex items-center justify-between">
<p className="text-th-fgd-2">{t('swap:reduce-position')}</p>
{!isUnownedAccount ? (
<MaxSwapAmount
useMargin={isTriggerOrder ? false : useMargin}
setAmountIn={(v) => handleMax(v)}
maxAmount={useAbsInputPosition}
/>
) : null}
</div>
<div className="col-span-1">
<TokenSelect
bank={
inputBank || group?.banksMapByName.get(INPUT_TOKEN_DEFAULT)?.[0] // default to a user position
}
showTokenList={setShowTokenSelect}
type="reduce-input"
/>
</div>
<div className="relative col-span-1">
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={inputBank?.mintDecimals || 6}
name="amountIn"
id="amountIn"
className={NUMBER_FORMAT_CLASSNAMES}
placeholder="0.00"
value={amountInFormValue}
onValueChange={handleAmountInChange}
isAllowed={withValueLimit}
/>
{!isNaN(Number(amountInFormValue)) ? (
<span className="absolute bottom-1.5 right-3 text-xxs text-th-fgd-4">
{inputBank
? formatCurrencyValue(
inputBank.uiPrice * Number(amountInFormValue),
)
: ''}
</span>
) : null}
</div>
{mangoAccountAddress && freeCollateral <= 0 ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="warning"
desc={t('swap:warning-no-collateral')}
hideBorder
hidePadding
/>
</div>
) : null}
{error ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="error"
desc={error}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
)
}
export default ReduceInputTokenInput

View File

@ -0,0 +1,105 @@
import TokenSelect from './TokenSelect'
import Loading from '@components/shared/Loading'
import NumberFormat, {
NumberFormatValues,
SourceInfo,
} from 'react-number-format'
import { formatCurrencyValue } from 'utils/numbers'
import { useTranslation } from 'react-i18next'
import { Dispatch, SetStateAction, useMemo } from 'react'
import mangoStore from '@store/mangoStore'
import useMangoGroup from 'hooks/useMangoGroup'
import { OUTPUT_TOKEN_DEFAULT } from 'utils/constants'
import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm'
import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { getInputTokenBalance } from './LimitSwapForm'
const ReduceOutputTokenInput = ({
error,
handleAmountOutChange,
loading,
setShowTokenSelect,
}: {
error?: string
handleAmountOutChange: (e: NumberFormatValues, info: SourceInfo) => void
loading?: boolean
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
}) => {
const { t } = useTranslation('common')
const { mangoAccountAddress } = useMangoAccount()
const { group } = useMangoGroup()
const {
inputBank,
outputBank,
amountOut: amountOutFormValue,
} = mangoStore((s) => s.swap)
const reducingLong = useMemo(() => {
if (!inputBank || !mangoAccountAddress) return false
const inputBalance = getInputTokenBalance(inputBank)
return inputBalance > 0
}, [inputBank, mangoAccountAddress])
return (
<div className="mb-2 grid grid-cols-2 rounded-b-xl border-t border-th-bkg-4 bg-th-bkg-2 p-3">
<p className="col-span-2 mb-2 text-th-fgd-2">
{reducingLong ? t('buy') : t('sell')}
</p>
<div className="col-span-1">
<TokenSelect
bank={
outputBank || group?.banksMapByName.get(OUTPUT_TOKEN_DEFAULT)?.[0]
}
showTokenList={setShowTokenSelect}
type="reduce-output"
/>
</div>
<div className="relative col-span-1">
{loading ? (
<div className="flex h-[56px] w-full items-center justify-center rounded-l-none rounded-r-lg border-l border-th-bkg-2 bg-th-input-bkg">
<Loading />
</div>
) : (
<>
<NumberFormat
inputMode="decimal"
thousandSeparator=","
allowNegative={false}
isNumericString={true}
decimalScale={outputBank?.mintDecimals || 6}
name="amountOut"
id="amountOut"
className={NUMBER_FORMAT_CLASSNAMES}
placeholder="0.00"
value={amountOutFormValue}
onValueChange={handleAmountOutChange}
/>
{!isNaN(Number(amountOutFormValue)) ? (
<span className="absolute bottom-1.5 right-3 text-xxs text-th-fgd-4">
{outputBank
? formatCurrencyValue(
outputBank.uiPrice * Number(amountOutFormValue),
)
: ''}
</span>
) : null}
</>
)}
</div>
{error ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="error"
desc={error}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
)
}
export default ReduceOutputTokenInput

View File

@ -14,8 +14,9 @@ import MaxSwapAmount from './MaxSwapAmount'
import useUnownedAccount from 'hooks/useUnownedAccount'
import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { useWallet } from '@solana/wallet-adapter-react'
import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { useTokenMax } from './useTokenMax'
const SellTokenInput = ({
handleAmountInChange,
@ -26,7 +27,7 @@ const SellTokenInput = ({
isTriggerOrder,
}: {
handleAmountInChange: (e: NumberFormatValues, info: SourceInfo) => void
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
setShowTokenSelect: Dispatch<SetStateAction<SwapFormTokenListType>>
handleMax: (amountIn: string) => void
className?: string
error?: string
@ -34,7 +35,6 @@ const SellTokenInput = ({
}) => {
const { t } = useTranslation('common')
const { mangoAccountAddress } = useMangoAccount()
const { connected } = useWallet()
const { group } = useMangoGroup()
const { isUnownedAccount } = useUnownedAccount()
const {
@ -48,17 +48,20 @@ const SellTokenInput = ({
const mangoAccount = mangoStore.getState().mangoAccount.current
return group && mangoAccount
? toUiDecimalsForQuote(mangoAccount.getCollateralValue(group))
: 0
: 10
}, [mangoAccountAddress])
return (
<div className={`grid grid-cols-2 rounded-xl bg-th-bkg-2 p-3 ${className}`}>
<div
className={`grid grid-cols-2 rounded-t-xl bg-th-bkg-2 p-3 pb-2 ${className}`}
>
<div className="col-span-2 mb-2 flex items-center justify-between">
<p className="text-th-fgd-2">{t('sell')}</p>
{!isUnownedAccount ? (
<MaxSwapAmount
useMargin={isTriggerOrder ? false : useMargin}
setAmountIn={(v) => handleMax(v)}
maxAmount={useTokenMax}
/>
) : null}
</div>
@ -96,7 +99,7 @@ const SellTokenInput = ({
</span>
) : null}
</div>
{connected && freeCollateral <= 0 ? (
{mangoAccountAddress && freeCollateral <= 0 ? (
<div className="col-span-2 mt-1 flex justify-center">
<InlineNotification
type="warning"

View File

@ -8,7 +8,7 @@ import SwapFormTokenList from './SwapFormTokenList'
import { LinkButton } from '../shared/Button'
import { EnterBottomExitBottom } from '../shared/Transitions'
import { HealthType } from '@blockworks-foundation/mango-v4'
import { SWAP_MARGIN_KEY } from '../../utils/constants'
import { OUTPUT_TOKEN_DEFAULT, SWAP_MARGIN_KEY } from '../../utils/constants'
import HealthImpact from '@components/shared/HealthImpact'
import TokenVaultWarnings from '@components/shared/TokenVaultWarnings'
import SwapSettings from './SwapSettings'
@ -19,16 +19,16 @@ import MarketSwapForm from './MarketSwapForm'
import LimitSwapForm from './LimitSwapForm'
import Switch from '@components/forms/Switch'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { TriggerOrderTypes } from 'types'
const set = mangoStore.getState().set
const SwapForm = () => {
const { t } = useTranslation(['common', 'swap', 'trade'])
const { data: isWhiteListed } = useIsWhiteListed()
const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>()
const [showTokenSelect, setShowTokenSelect] =
useState<SwapFormTokenListType>()
const [showSettings, setShowSettings] = useState(false)
const [swapOrLimit, setSwapOrLimit] = useState('swap')
const [, setSavedSwapMargin] = useLocalStorageState<boolean>(
SWAP_MARGIN_KEY,
true,
@ -41,6 +41,7 @@ const SwapForm = () => {
outputBank,
amountIn: amountInFormValue,
amountOut: amountOutFormValue,
swapOrTrigger,
} = mangoStore((s) => s.swap)
const handleTokenInSelect = useCallback((mintAddress: string) => {
@ -100,11 +101,18 @@ const SwapForm = () => {
: Math.trunc(simulatedHealthRatio)
}, [inputBank, outputBank, amountInFormValue, amountOutFormValue])
const handleSwapOrLimit = useCallback(
(orderType: string) => {
setSwapOrLimit(orderType)
const handleSwapOrTrigger = useCallback(
(orderType: TriggerOrderTypes) => {
set((state) => {
state.swap.swapOrTrigger = orderType
if (orderType !== 'swap' && outputBank?.name === OUTPUT_TOKEN_DEFAULT) {
const { group } = mangoStore.getState()
const outputBankName = inputBank?.name === 'USDC' ? 'SOL' : 'USDC'
state.swap.outputBank = group?.banksMapByName.get(outputBankName)?.[0]
}
})
},
[outputBank, set, setSwapOrLimit],
[inputBank, outputBank, set],
)
const handleSetMargin = () => {
@ -142,12 +150,12 @@ const SwapForm = () => {
<SwapFormTokenList
onClose={() => setShowTokenSelect(undefined)}
onTokenSelect={
showTokenSelect === 'input'
showTokenSelect === 'input' || showTokenSelect === 'reduce-input'
? handleTokenInSelect
: handleTokenOutSelect
}
type={showTokenSelect}
useMargin={useMargin}
useMargin={swapOrTrigger === 'swap' ? useMargin : false}
/>
</EnterBottomExitBottom>
<EnterBottomExitBottom
@ -157,16 +165,14 @@ const SwapForm = () => {
<SwapSettings onClose={() => setShowSettings(false)} />
</EnterBottomExitBottom>
<div className="relative p-6">
{isWhiteListed ? (
<div className="relative mb-6">
<TabUnderline
activeValue={swapOrLimit}
values={['swap', 'trade:trigger-order']}
onChange={(v) => handleSwapOrLimit(v)}
/>
</div>
) : null}
{swapOrLimit === 'swap' ? (
<div className="relative mb-6">
<TabUnderline
activeValue={swapOrTrigger}
values={['swap', 'trade:trigger-order']}
onChange={(v) => handleSwapOrTrigger(v)}
/>
</div>
{swapOrTrigger === 'swap' ? (
<MarketSwapForm setShowTokenSelect={setShowTokenSelect} />
) : (
<LimitSwapForm
@ -205,7 +211,7 @@ const SwapForm = () => {
<div id="swap-step-four">
<HealthImpact maintProjectedHealth={maintProjectedHealth} />
</div>
{swapOrLimit === 'swap' ? (
{swapOrTrigger === 'swap' ? (
<>
<div className="flex items-center justify-between">
<Tooltip content={t('swap:tooltip-margin')}>

View File

@ -14,6 +14,14 @@ import FormatNumericValue from '@components/shared/FormatNumericValue'
import { formatTokenSymbol } from 'utils/tokens'
import TokenLogo from '@components/shared/TokenLogo'
import Input from '@components/forms/Input'
import { getInputTokenBalance } from './LimitSwapForm'
export type SwapFormTokenListType =
| 'input'
| 'output'
| 'reduce-input'
| 'reduce-output'
| undefined
const generateSearchTerm = (item: Token, searchValue: string) => {
const normalizedSearchValue = searchValue.toLowerCase()
@ -50,7 +58,7 @@ const TokenItem = ({
token: TokenInfoWithAmounts
onSubmit: (x: string) => void
useMargin: boolean
type: 'input' | 'output' | undefined
type: SwapFormTokenListType
}) => {
const { t } = useTranslation('trade')
const { address, symbol, name } = token
@ -80,8 +88,17 @@ const TokenItem = ({
<div className="ml-2.5">
<p className="text-left text-th-fgd-2">
{bank?.name ? formatTokenSymbol(bank.name) : symbol || 'unknown'}
{type === 'reduce-input' && token.amount ? (
<span
className={`ml-1 rounded px-1 text-xxs uppercase ${
token.amount.gt(0) ? 'text-th-up' : 'text-th-down'
}`}
>
{t(`trade:${token.amount.gt(0) ? 'long' : 'short'}`)}
</span>
) : null}
{isReduceOnly ? (
<span className="ml-1.5 text-xxs text-th-warning">
<span className="ml-1 text-xxs text-th-warning">
{t('reduce-only')}
</span>
) : null}
@ -92,7 +109,7 @@ const TokenItem = ({
</p>
</div>
</div>
{type === 'input' &&
{(type === 'input' || type === 'reduce-input') &&
token.amount &&
token.amountWithBorrow &&
token.decimals ? (
@ -128,7 +145,7 @@ const SwapFormTokenList = ({
}: {
onClose: () => void
onTokenSelect: (x: string) => void
type: 'input' | 'output' | undefined
type: SwapFormTokenListType
useMargin: boolean
}) => {
const { t } = useTranslation(['common', 'search', 'swap'])
@ -137,7 +154,7 @@ const SwapFormTokenList = ({
const inputBank = mangoStore((s) => s.swap.inputBank)
const outputBank = mangoStore((s) => s.swap.outputBank)
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const focusRef = useRef<HTMLInputElement>(null)
useEffect(() => {
@ -177,6 +194,36 @@ const SwapFormTokenList = ({
: Number(b.amount) - Number(a.amount),
)
return filteredSortedTokens
} else if (
mangoTokens?.length &&
group &&
mangoAccount &&
outputBank &&
inputBank &&
type === 'reduce-input'
) {
const filteredSortedTokens = mangoTokens
.map((token) => {
const tokenBank = group.getFirstBankByMint(
new PublicKey(token.address),
)
const uiAmount = mangoAccount.getTokenBalanceUi(tokenBank)
const uiDollarValue = uiAmount * tokenBank.uiPrice
return {
...token,
amount: new Decimal(uiAmount),
amountWithBorrow: new Decimal(uiAmount),
absDollarValue: Math.abs(uiDollarValue),
decimals: inputBank.mintDecimals,
}
})
.filter(
(token) =>
token.symbol !== outputBank?.name && token.absDollarValue > 0.0001,
)
.sort((a, b) => b.absDollarValue - a.absDollarValue)
return filteredSortedTokens
} else if (mangoTokens?.length) {
const filteredTokens = mangoTokens
@ -205,15 +252,28 @@ const SwapFormTokenList = ({
}
}, [focusRef])
const listTitle = useMemo(() => {
if (!type) return ''
if (type === 'input') {
return t('swap:you-sell')
} else if (type === 'output') {
return t('swap:you-buy')
} else if (type === 'reduce-input') {
return t('swap:reduce-position')
} else {
if (!mangoAccountAddress || !inputBank) return ''
const uiPos = getInputTokenBalance(inputBank)
if (uiPos > 0) {
return t('swap:reduce-position-buy')
} else if (uiPos < 0) {
return t('swap:reduce-position-sell')
}
}
}, [inputBank, mangoAccountAddress, type])
return (
<>
<p className="mb-3">
{type === 'input'
? t('swap:you-sell')
: type === 'output'
? t('swap:you-buy')
: ''}
</p>
<p className="mb-3">{listTitle}</p>
<IconButton
className="absolute right-2 top-2 text-th-fgd-3 hover:text-th-fgd-2"
onClick={onClose}
@ -235,7 +295,7 @@ const SwapFormTokenList = ({
</div>
<div className="flex justify-between rounded bg-th-bkg-2 p-2">
<p className="text-xs text-th-fgd-4">{t('token')}</p>
{type === 'input' ? (
{!type?.includes('output') ? (
<p className="text-xs text-th-fgd-4">{t('max')}</p>
) : null}
</div>

View File

@ -5,16 +5,14 @@ import SwapHistoryTable from './SwapHistoryTable'
import useMangoAccount from 'hooks/useMangoAccount'
import ManualRefresh from '@components/shared/ManualRefresh'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import SwapOrders from './SwapOrders'
import SwapTriggerOrders from './SwapTriggerOrders'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
const SwapInfoTabs = () => {
const [selectedTab, setSelectedTab] = useState('balances')
const { mangoAccount } = useMangoAccount()
const { width } = useViewport()
const { isMobile, isTablet } = useViewport()
const { data: isWhiteListed } = useIsWhiteListed()
const isMobile = width ? width < breakpoints.lg : false
const tabsWithCount: [string, number][] = useMemo(() => {
const tabs: [string, number][] = [
@ -33,20 +31,22 @@ const SwapInfoTabs = () => {
return (
<div className="hide-scroll h-full overflow-y-scroll">
<div className="flex items-center border-b border-th-bkg-3">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}
values={tabsWithCount}
showBorders
/>
<div className="w-full md:border-r md:border-th-bkg-3">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}
values={tabsWithCount}
showBorders
/>
</div>
<ManualRefresh
classNames="fixed bottom-16 right-4 lg:relative lg:bottom-0 md:bottom-6 md:right-6 z-10 shadow-lg lg:shadow-none bg-th-bkg-3 lg:bg-transparent"
hideBg={isMobile}
size={isMobile ? 'large' : 'small'}
classNames="fixed bottom-16 right-4 md:relative md:px-2 md:bottom-0 md:right-0 z-10 shadow-lg md:shadow-none bg-th-bkg-3 md:bg-transparent"
hideBg={isMobile || isTablet}
size={isTablet ? 'large' : 'small'}
/>
</div>
{selectedTab === 'balances' ? <SwapTradeBalances /> : null}
{selectedTab === 'trade:trigger-orders' ? <SwapOrders /> : null}
{selectedTab === 'trade:trigger-orders' ? <SwapTriggerOrders /> : null}
{selectedTab === 'swap:swap-history' ? <SwapHistoryTable /> : null}
</div>
)

View File

@ -1,20 +1,22 @@
import useMangoAccount from 'hooks/useMangoAccount'
import LeverageSlider from '../shared/LeverageSlider'
import { useTokenMax } from './useTokenMax'
import { TokenMaxResults } from './useTokenMax'
const SwapSlider = ({
amount,
onChange,
useMargin,
step,
maxAmount,
}: {
amount: number
onChange: (x: string) => void
useMargin: boolean
step: number
maxAmount: (useMargin: boolean) => TokenMaxResults
}) => {
const { mangoAccount } = useMangoAccount()
const { amount: tokenMax, amountWithBorrow } = useTokenMax(useMargin)
const { amount: tokenMax, amountWithBorrow } = maxAmount(useMargin)
return (
<>

View File

@ -31,6 +31,7 @@ import Loading from '@components/shared/Loading'
import SideBadge from '@components/shared/SideBadge'
import { Disclosure, Transition } from '@headlessui/react'
import SheenLoader from '@components/shared/SheenLoader'
import { formatTokenSymbol } from 'utils/tokens'
const SwapOrders = () => {
const { t } = useTranslation(['common', 'swap', 'trade'])
@ -52,7 +53,6 @@ const SwapOrders = () => {
for (const order of orders) {
const buyBank = group.getFirstBankByTokenIndex(order.buyTokenIndex)
const sellBank = group.getFirstBankByTokenIndex(order.sellTokenIndex)
const pair = `${sellBank.name}/${buyBank.name}`
const maxBuy = floorToDecimal(
order.getMaxBuyUi(group),
buyBank.mintDecimals,
@ -70,11 +70,22 @@ const SwapOrders = () => {
size = maxBuy
side = 'buy'
}
const buyTokenName = formatTokenSymbol(buyBank.name)
const sellTokenName = formatTokenSymbol(sellBank.name)
const pair =
side === 'sell'
? `${sellTokenName}/${buyTokenName}`
: `${buyTokenName}/${sellTokenName}`
const triggerPrice = order.getThresholdPriceUi(group)
const pricePremium = order.getPricePremium()
const filled = order.getSoldUi(group)
const currentPrice = order.getCurrentPairPriceUi(group)
const sellTokenPerBuyToken = !!Object.prototype.hasOwnProperty.call(
order.priceDisplayStyle,
'sellTokenPerBuyToken',
)
const triggerDirection = triggerPrice < currentPrice ? '<=' : '>='
const data = {
...order,
@ -87,6 +98,8 @@ const SwapOrders = () => {
filled,
triggerPrice,
fee: pricePremium,
sellTokenPerBuyToken,
triggerDirection,
}
formatted.push(data)
}
@ -276,9 +289,18 @@ const SwapOrders = () => {
size,
filled,
triggerPrice,
sellTokenPerBuyToken,
triggerDirection,
} = data
const bank = side === 'buy' ? buyBank : sellBank
const formattedBuyTokenName = formatTokenSymbol(buyBank.name)
const formattedSellTokenName = formatTokenSymbol(sellBank.name)
const formattedBaseName =
side === 'buy' ? formattedBuyTokenName : formattedSellTokenName
const formattedQuoteName = !sellTokenPerBuyToken
? formattedBuyTokenName
: formattedSellTokenName
return (
<TrBody key={i} className="text-sm">
<Td>{pair}</Td>
@ -292,7 +314,7 @@ const SwapOrders = () => {
{size}
<span className="font-body text-th-fgd-3">
{' '}
{bank.name}
{formattedBaseName}
</span>
</p>
</Td>
@ -301,7 +323,7 @@ const SwapOrders = () => {
{filled}/{size}
<span className="font-body text-th-fgd-3">
{' '}
{bank.name}
{formattedBaseName}
</span>
</p>
</Td>
@ -310,16 +332,19 @@ const SwapOrders = () => {
{currentPrice}
<span className="font-body text-th-fgd-3">
{' '}
{buyBank.name}
{formattedQuoteName}
</span>
</p>
</Td>
<Td>
<p className="text-right">
<span className="font-body text-th-fgd-4">
{triggerDirection}{' '}
</span>
{triggerPrice}
<span className="font-body text-th-fgd-3">
{' '}
{side === 'buy' ? sellBank.name : buyBank.name}
{formattedQuoteName}
</span>
</p>
</Td>
@ -359,9 +384,17 @@ const SwapOrders = () => {
size,
filled,
triggerPrice,
sellTokenPerBuyToken,
triggerDirection,
} = data
const bank = side === 'buy' ? buyBank : sellBank
const formattedBuyTokenName = formatTokenSymbol(buyBank.name)
const formattedSellTokenName = formatTokenSymbol(sellBank.name)
const formattedBaseName =
side === 'buy' ? formattedBuyTokenName : formattedSellTokenName
const formattedQuoteName = !sellTokenPerBuyToken
? formattedBuyTokenName
: formattedSellTokenName
return (
<Disclosure key={i}>
{({ open }) => (
@ -379,7 +412,7 @@ const SwapOrders = () => {
{size}
<span className="font-body text-th-fgd-3">
{' '}
{bank.name}
{formattedBaseName}
</span>
<span className="font-body text-th-fgd-3">
{' at '}
@ -387,7 +420,7 @@ const SwapOrders = () => {
{triggerPrice}
<span className="font-body text-th-fgd-3">
{' '}
{side === 'buy' ? sellBank.name : buyBank.name}
{formattedQuoteName}
</span>
</p>
</div>
@ -413,7 +446,7 @@ const SwapOrders = () => {
{size}
<span className="font-body text-th-fgd-3">
{' '}
{bank.name}
{formattedBaseName}
</span>
</p>
</div>
@ -425,7 +458,7 @@ const SwapOrders = () => {
{filled}/{size}
<span className="font-body text-th-fgd-3">
{' '}
{bank.name}
{formattedBaseName}
</span>
</p>
</div>
@ -437,7 +470,7 @@ const SwapOrders = () => {
{currentPrice}
<span className="font-body text-th-fgd-3">
{' '}
{buyBank.name}
{formattedQuoteName}
</span>
</p>
</div>
@ -446,10 +479,15 @@ const SwapOrders = () => {
{t('trade:trigger-price')}
</p>
<p className="font-mono text-th-fgd-1">
<span className="font-body text-th-fgd-4">
{triggerDirection}{' '}
</span>
{triggerPrice}
<span className="font-body text-th-fgd-3">
{' '}
{side === 'buy' ? sellBank.name : buyBank.name}
{side === 'buy'
? formattedSellTokenName
: formattedBuyTokenName}
</span>
</p>
</div>

View File

@ -1,22 +1,37 @@
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import useMangoGroup from 'hooks/useMangoGroup'
import { Bank } from '@blockworks-foundation/mango-v4'
import { Dispatch, SetStateAction } from 'react'
import { Dispatch, SetStateAction, useMemo } from 'react'
import { formatTokenSymbol } from 'utils/tokens'
import TokenLogo from '@components/shared/TokenLogo'
import { SwapFormTokenListType } from './SwapFormTokenList'
import useMangoAccount from 'hooks/useMangoAccount'
import { useTranslation } from 'react-i18next'
type TokenSelectProps = {
bank: Bank | undefined
showTokenList: Dispatch<SetStateAction<'input' | 'output' | undefined>>
type: 'input' | 'output'
showTokenList: Dispatch<SetStateAction<SwapFormTokenListType>>
type: SwapFormTokenListType
}
const TokenSelect = ({ bank, showTokenList, type }: TokenSelectProps) => {
const { t } = useTranslation('trade')
const { group } = useMangoGroup()
const { mangoAccount } = useMangoAccount()
const posType = useMemo(() => {
if (!bank || !mangoAccount || type !== 'reduce-input') return ''
const uiPos = mangoAccount.getTokenBalanceUi(bank)
if (uiPos > 0) {
return 'long'
} else if (uiPos < 0) {
return 'short'
}
}, [bank, mangoAccount, type])
if (!group) return null
return (
return bank ? (
<button
onClick={() => showTokenList(type)}
className="flex h-[56px] w-full items-center rounded-lg rounded-r-none bg-th-input-bkg px-3 py-2 text-th-fgd-2 focus-visible:bg-th-bkg-3 md:hover:cursor-pointer md:hover:bg-th-bkg-1 md:hover:text-th-fgd-1"
@ -25,13 +40,26 @@ const TokenSelect = ({ bank, showTokenList, type }: TokenSelectProps) => {
<TokenLogo bank={bank} />
</div>
<div className="flex w-full items-center justify-between">
<div className="text-xl font-bold text-th-fgd-1">
{formatTokenSymbol(bank!.name)}
<div className="flex items-center">
<div className="text-lg font-bold text-th-fgd-1">
{formatTokenSymbol(bank.name)}
</div>
{posType ? (
<span
className={`ml-2 inline-block rounded border px-1 text-xs uppercase ${
posType === 'long'
? 'border-th-up text-th-up'
: 'border-th-down text-th-down'
}`}
>
{t(`trade:${posType}`)}
</span>
) : null}
</div>
<ChevronDownIcon className="h-6 w-6" />
</div>
</button>
)
) : null
}
export default TokenSelect

View File

@ -105,7 +105,7 @@ export const getTokenInMax = (
}
}
interface TokenMaxResults {
export interface TokenMaxResults {
amount: Decimal
amountWithBorrow: Decimal
decimals: number
@ -140,3 +140,25 @@ export const useTokenMax = (useMargin = true): TokenMaxResults => {
return tokenInMax
}
export const useAbsInputPosition = (): TokenMaxResults => {
const { mangoAccount } = useMangoAccount()
const { inputBank } = mangoStore((s) => s.swap)
if (!mangoAccount || !inputBank) {
return {
amount: new Decimal(0),
amountWithBorrow: new Decimal(0),
decimals: 6,
}
}
const amount = new Decimal(
Math.abs(mangoAccount.getTokenBalanceUi(inputBank)),
)
return {
decimals: inputBank.mintDecimals,
amount: amount,
amountWithBorrow: amount,
}
}

View File

@ -1,7 +1,6 @@
import Change from '@components/shared/Change'
import DailyRange from '@components/shared/DailyRange'
import { useTranslation } from 'next-i18next'
import Image from 'next/legacy/image'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import FlipNumbers from 'react-flip-numbers'
@ -22,6 +21,7 @@ import FormatNumericValue from '@components/shared/FormatNumericValue'
import TopTokenAccounts from './TopTokenAccounts'
import TokenParams from './TokenParams'
import { formatTokenSymbol } from 'utils/tokens'
import TokenLogo from '@components/shared/TokenLogo'
const DEFAULT_COINGECKO_VALUES = {
ath: 0,
@ -94,13 +94,6 @@ const TokenPage = () => {
}
}, [group, bankName])
const logoURI = useMemo(() => {
if (bank && mangoTokens.length) {
return mangoTokens.find((t) => t.address === bank.mint.toString())
?.logoURI
}
}, [bank, mangoTokens])
const coingeckoId = useMemo(() => {
if (bank && mangoTokens.length) {
return mangoTokens.find((t) => t.address === bank.mint.toString())
@ -133,7 +126,7 @@ const TokenPage = () => {
<div className="flex flex-col border-b border-th-bkg-3 px-6 py-5 md:flex-row md:items-center md:justify-between">
<div className="mb-4 md:mb-1">
<div className="mb-1.5 flex items-center space-x-2">
<Image src={logoURI!} height="20" width="20" />
<TokenLogo bank={bank} />
{coingeckoTokenInfo ? (
<h1 className="text-base font-normal">
{coingeckoTokenInfo.name}{' '}

View File

@ -13,9 +13,6 @@ import PerpMarketDetailsModal from '@components/modals/PerpMarketDetailsModal'
import OraclePrice from './OraclePrice'
import SpotMarketDetailsModal from '@components/modals/SpotMarketDetailsModal'
import { MarketData } from 'types'
import ManualRefresh from '@components/shared/ManualRefresh'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import MarketChange from '@components/shared/MarketChange'
import useMarketsData from 'hooks/useMarketsData'
@ -30,8 +27,6 @@ const AdvancedMarketHeader = ({
const { serumOrPerpMarket, selectedMarket } = useSelectedMarket()
const selectedMarketName = mangoStore((s) => s.selectedMarket.name)
const [showMarketDetails, setShowMarketDetails] = useState(false)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const volume = useMemo(() => {
@ -142,10 +137,6 @@ const AdvancedMarketHeader = ({
)}
</div>
<div className="ml-6 flex items-center space-x-4">
<ManualRefresh
hideBg={isMobile}
size={isMobile ? undefined : 'small'}
/>
<LinkButton
className="flex items-center whitespace-nowrap text-th-fgd-3"
onClick={() => setShowMarketDetails(true)}

View File

@ -45,7 +45,11 @@ import SpotButtonGroup from './SpotButtonGroup'
import PerpButtonGroup from './PerpButtonGroup'
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import {
floorToDecimal,
formatCurrencyValue,
getDecimalCount,
} from 'utils/numbers'
import LogoWithFallback from '@components/shared/LogoWithFallback'
import useIpAddress from 'hooks/useIpAddress'
import ButtonGroup from '@components/forms/ButtonGroup'
@ -59,6 +63,11 @@ import { isMangoError } from 'types'
import InlineNotification from '@components/shared/InlineNotification'
import SpotMarketOrderSwapForm from './SpotMarketOrderSwapForm'
import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
const set = mangoStore.getState().set
@ -76,7 +85,7 @@ export const INPUT_PREFIX_CLASSNAMES =
export const DEFAULT_CHECKBOX_SETTINGS = {
ioc: false,
post: false,
margin: false,
margin: true,
}
const AdvancedTradeForm = () => {
@ -103,6 +112,8 @@ const AdvancedTradeForm = () => {
quoteSymbol,
serumOrPerpMarket,
} = useSelectedMarket()
const { remainingBorrowsInPeriod, timeToNextPeriod } =
useRemainingBorrowsInPeriod()
const setTradeType = useCallback((tradeType: 'Limit' | 'Market') => {
set((s) => {
@ -480,11 +491,40 @@ const AdvancedTradeForm = () => {
handlePlaceOrder()
}
const balanceBank = useMemo(() => {
const { group } = mangoStore.getState()
if (
!group ||
!selectedMarket ||
selectedMarket instanceof PerpMarket ||
!savedCheckboxSettings.margin
)
return
if (tradeForm.side === 'buy') {
return group.getFirstBankByTokenIndex(selectedMarket.quoteTokenIndex)
} else {
return group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
}
}, [savedCheckboxSettings, selectedMarket, tradeForm.side])
// check if the borrowed amount exceeds the net borrow limit in the current period
const borrowExceedsLimitInPeriod = useMemo(() => {
if (!mangoAccount || !balanceBank || !remainingBorrowsInPeriod) return false
const size =
tradeForm.side === 'buy' ? tradeForm.quoteSize : tradeForm.baseSize
const balance = mangoAccount.getTokenDepositsUi(balanceBank)
const remainingBalance = balance - parseFloat(size)
const borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
return borrowAmount > remainingBorrowsInPeriod
}, [balanceBank, mangoAccount, remainingBorrowsInPeriod, tradeForm])
const disabled =
(connected && (!tradeForm.baseSize || !tradeForm.price)) ||
!serumOrPerpMarket ||
parseFloat(tradeForm.baseSize) < serumOrPerpMarket.minOrderSize ||
!isMarketEnabled
!isMarketEnabled ||
borrowExceedsLimitInPeriod
return (
<div>
@ -795,10 +835,22 @@ const AdvancedTradeForm = () => {
)}
</div>
</form>
<TradeSummary
mangoAccount={mangoAccount}
useMargin={savedCheckboxSettings.margin}
/>
{borrowExceedsLimitInPeriod &&
remainingBorrowsInPeriod &&
timeToNextPeriod ? (
<div className="mb-4 px-4">
<InlineNotification
type="error"
desc={t('error-borrow-exceeds-limit', {
remaining: formatCurrencyValue(remainingBorrowsInPeriod),
resetTime: dayjs().to(
dayjs().add(timeToNextPeriod, 'second'),
),
})}
/>
</div>
) : null}
<TradeSummary balanceBank={balanceBank} mangoAccount={mangoAccount} />
</>
)}
</div>

View File

@ -0,0 +1,128 @@
import { FunctionComponent, useCallback, useState } from 'react'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import Modal from '@components/shared/Modal'
import Button, { LinkButton } from '@components/shared/Button'
import { notify } from 'utils/notifications'
import Loading from '@components/shared/Loading'
import { isMangoError } from 'types'
import { ModalProps } from 'types/modal'
import useMangoGroup from 'hooks/useMangoGroup'
import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import MarketLogos from './MarketLogos'
import PerpSideBadge from './PerpSideBadge'
import FormatNumericValue from '@components/shared/FormatNumericValue'
const CloseAllPositionsModal: FunctionComponent<ModalProps> = ({
onClose,
isOpen,
}) => {
const { t } = useTranslation(['common', 'trade'])
const [submitting, setSubmitting] = useState(false)
const openPerpPositions = useOpenPerpPositions()
const { group } = useMangoGroup()
const handleCloseAll = useCallback(async () => {
const client = mangoStore.getState().client
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
if (!group || !mangoAccount) {
notify({
title: 'Something went wrong. Try again later',
type: 'error',
})
return
}
setSubmitting(true)
try {
const maxSlippage = 0.025
const { signature: tx } = await client.perpCloseAll(
group,
mangoAccount,
maxSlippage,
)
actions.fetchOpenOrders()
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
} catch (e) {
if (isMangoError(e)) {
notify({
title: 'There was an issue.',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
console.error('Place trade error:', e)
} finally {
setSubmitting(false)
onClose()
}
}, [group, onClose])
if (!group) return null
return (
<Modal onClose={onClose} isOpen={isOpen}>
<h3 className="mb-2 text-center">{t('trade:close-all-positions')}</h3>
<div className="pb-6 text-th-fgd-3">{t('trade:price-expect')}</div>
<div className="border-b border-th-bkg-3">
{openPerpPositions.map((position, i) => {
const market = group.getPerpMarketByMarketIndex(position.marketIndex)
const basePosition = position.getBasePositionUi(market)
const floorBasePosition = floorToDecimal(
basePosition,
getDecimalCount(market.minOrderSize),
).toNumber()
if (!basePosition) return null
return (
<div
className="flex items-center justify-between border-t border-th-bkg-3 py-3"
key={market.name + i}
>
<div className="flex items-center">
<MarketLogos market={market} />
<p className="mr-2">{market.name}</p>
<PerpSideBadge basePosition={basePosition} />
</div>
<p className="font-mono text-th-fgd-2">
<FormatNumericValue value={Math.abs(floorBasePosition)} />
<span className="mx-1 text-th-bkg-4">|</span>
<FormatNumericValue
value={Math.abs(floorBasePosition * market.uiPrice)}
isUsd
/>
</p>
</div>
)
})}
</div>
<Button
className="mb-4 mt-6 flex w-full items-center justify-center"
onClick={handleCloseAll}
size="large"
>
{submitting ? (
<Loading />
) : (
<span>{t('trade:close-all-positions')}</span>
)}
</Button>
<LinkButton
className="inline-flex w-full items-center justify-center"
onClick={onClose}
>
{t('cancel')}
</LinkButton>
</Modal>
)
}
export default CloseAllPositionsModal

View File

@ -74,9 +74,8 @@ const DepthChart = () => {
const markPrice = useMarkPrice()
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
const [priceRangePercent, setPriceRangePercentPercent] = useState('10')
const { width } = useViewport()
const { isTablet, width } = useViewport()
const increaseHeight = width ? width > breakpoints['3xl'] : false
const isMobile = width ? width < breakpoints.md : false
const formatOrderbookData = (orderbook: RawOrderbook, markPrice: number) => {
const maxPrice = markPrice * 4
@ -318,7 +317,7 @@ const DepthChart = () => {
</div>
<div
className={
increaseHeight ? 'h-[570px]' : isMobile ? 'h-[538px]' : 'h-[482px]'
increaseHeight ? 'h-[570px]' : isTablet ? 'h-[538px]' : 'h-[482px]'
}
>
<ResponsiveContainer width="100%" height="100%">

View File

@ -124,15 +124,15 @@ const MarketCloseModal: FunctionComponent<MarketCloseModalProps> = ({
}, [connection, perpMarket, group])
const insufficientLiquidity = useMemo(() => {
if (!perpMarket) return true
if (!perpMarket) return false
const baseSize = position.getBasePositionUi(perpMarket)
const isBids = baseSize < 0
if (isBids) {
if (!bids || !bids.length) return true
if (!bids || !bids.length) return false
const liquidityMax = bids.reduce((a, c) => a + c[1], 0)
return liquidityMax < baseSize
} else {
if (!asks || !asks.length) return true
if (!asks || !asks.length) return false
const liquidityMax = asks.reduce((a, c) => a + c[1], 0)
return liquidityMax < baseSize
}

View File

@ -13,6 +13,7 @@ import { IconButton } from '@components/shared/Button'
import ConnectEmptyState from '@components/shared/ConnectEmptyState'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import Loading from '@components/shared/Loading'
import SheenLoader from '@components/shared/SheenLoader'
import SideBadge from '@components/shared/SideBadge'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
@ -30,6 +31,7 @@ import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import useSelectedMarket from 'hooks/useSelectedMarket'
import useUnownedAccount from 'hooks/useUnownedAccount'
import useFilledOrders from 'hooks/useFilledOrders'
import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
@ -77,6 +79,7 @@ const OpenOrders = () => {
const { connected } = useWallet()
const { isUnownedAccount } = useUnownedAccount()
const { selectedMarket } = useSelectedMarket()
const { filledOrders, fetchingFilledOrders } = useFilledOrders()
const handleCancelSerumOrder = useCallback(
async (o: Order) => {
@ -249,12 +252,13 @@ const OpenOrders = () => {
<Table>
<thead>
<TrHead>
<Th className="w-[16.67%] text-left">{t('market')}</Th>
<Th className="w-[16.67%] text-right">{t('trade:size')}</Th>
<Th className="w-[16.67%] text-right">{t('price')}</Th>
<Th className="w-[16.67%] text-right">{t('value')}</Th>
<Th className="w-[14.28%] text-left">{t('market')}</Th>
<Th className="w-[14.28%] text-right">{t('trade:size')}</Th>
<Th className="w-[14.28%] text-right">{t('price')}</Th>
<Th className="w-[14.28%] text-right">{t('trade:filled')}</Th>
<Th className="w-[14.28%] text-right">{t('value')}</Th>
{!isUnownedAccount ? (
<Th className="w-[16.67%] text-right" />
<Th className="w-[14.28%] text-right" />
) : null}
</TrHead>
</thead>
@ -269,6 +273,7 @@ const OpenOrders = () => {
let minOrderSize: number
let expiryTimestamp: number | undefined
let value: number
let filledQuantity = 0
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
tickSize = market.tickSize
@ -278,6 +283,20 @@ const OpenOrders = () => {
? 0
: o.expiryTimestamp.toNumber()
value = o.size * o.price
// Find the filled perp order,
// the api returns client order ids for perps, but PerpOrder[] only has orderId
const mangoAccount =
mangoStore.getState().mangoAccount.current
const perpClientId = mangoAccount?.perpOpenOrders?.find((p) =>
p.id.eq(o.orderId),
)?.clientId
if (perpClientId) {
const filledOrder = filledOrders?.fills?.find(
(f) => f.order_id == perpClientId.toString(),
)
filledQuantity = filledOrder ? filledOrder.quantity : 0
}
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk),
@ -291,6 +310,10 @@ const OpenOrders = () => {
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
value = o.size * o.price * quoteBank.uiPrice
const filledOrder = filledOrders?.fills?.find(
(f) => o.orderId.toString() === f.order_id,
)
filledQuantity = filledOrder ? filledOrder.quantity : 0
}
const side =
o instanceof PerpOrder
@ -303,18 +326,18 @@ const OpenOrders = () => {
key={`${o.side}${o.size}${o.price}${o.orderId.toString()}`}
className="my-1 p-2"
>
<Td className="w-[16.67%]">
<Td className="w-[14.28%]">
<TableMarketName market={market} side={side} />
</Td>
{modifyOrderId !== o.orderId.toString() ? (
<>
<Td className="w-[16.67%] text-right font-mono">
<Td className="w-[14.28%] text-right font-mono">
<FormatNumericValue
value={o.size}
decimals={getDecimalCount(minOrderSize)}
/>
</Td>
<Td className="w-[16.67%] whitespace-nowrap text-right font-mono">
<Td className="w-[14.28%] whitespace-nowrap text-right font-mono">
<FormatNumericValue
value={o.price}
decimals={getDecimalCount(tickSize)}
@ -323,7 +346,7 @@ const OpenOrders = () => {
</>
) : (
<>
<Td className="w-[16.67%]">
<Td className="w-[14.28%]">
<input
className="h-8 w-full rounded-l-none rounded-r-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono text-sm hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
type="text"
@ -333,7 +356,7 @@ const OpenOrders = () => {
}
/>
</Td>
<Td className="w-[16.67%]">
<Td className="w-[14.28%]">
<input
autoFocus
className="h-8 w-full rounded-l-none rounded-r-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono text-sm hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
@ -346,7 +369,21 @@ const OpenOrders = () => {
</Td>
</>
)}
<Td className="w-[16.67%] text-right font-mono">
<Td className="w-[14.28%] text-right font-mono">
{fetchingFilledOrders ? (
<div className="items flex justify-end">
<SheenLoader className="flex justify-end">
<div className="h-4 w-8 bg-th-bkg-2" />
</SheenLoader>
</div>
) : (
<FormatNumericValue
value={filledQuantity}
decimals={getDecimalCount(minOrderSize)}
/>
)}
</Td>
<Td className="w-[14.28%] text-right font-mono">
<FormatNumericValue value={value} isUsd />
{expiryTimestamp ? (
<div className="h-min text-xxs leading-tight text-th-fgd-4">{`Expires ${new Date(
@ -355,7 +392,7 @@ const OpenOrders = () => {
) : null}
</Td>
{!isUnownedAccount ? (
<Td className="w-[16.67%]">
<Td className="w-[14.28%]">
<div className="flex justify-end space-x-2">
{modifyOrderId !== o.orderId.toString() ? (
<>

View File

@ -33,7 +33,6 @@ import {
import { OrderbookData, OrderbookL2 } from 'types'
import isEqual from 'lodash/isEqual'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
const sizeCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 6,
@ -57,14 +56,13 @@ const Orderbook = () => {
// ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
// : true
// )
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { isDesktop } = useViewport()
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
const currentOrderbookData = useRef<OrderbookL2>()
const depth = useMemo(() => {
return isMobile ? 12 : 30
}, [isMobile])
return isDesktop ? 30 : 12
}, [isDesktop])
const depthArray: number[] = useMemo(() => {
return Array(depth).fill(0)
@ -519,24 +517,28 @@ const Orderbook = () => {
)
})}
<div
className="my-1 grid grid-cols-2 border-y border-th-bkg-3 px-4 py-1 text-xs text-th-fgd-4"
className="my-1 grid grid-cols-3 bg-th-bkg-2 px-4 py-1 text-xs text-th-fgd-4"
id="trade-step-nine"
>
<div className="col-span-1 flex justify-between">
<div className="text-xxs">{t('trade:spread')}</div>
<div className="font-mono">
<div className="col-span-1">
<p className="text-xxs">{t('trade:spread')}</p>
</div>
<div className="col-span-1 text-center font-mono">
<span className="text-th-fgd-3">
{orderbookData?.spreadPercentage.toFixed(2)}%
</div>
</span>
</div>
<div className="col-span-1 text-right font-mono">
{orderbookData?.spread
? orderbookData.spread < SHOW_EXPONENTIAL_THRESHOLD
? orderbookData.spread.toExponential()
: formatNumericValue(
orderbookData.spread,
market ? getDecimalCount(market.tickSize) : undefined,
)
: null}
<span className="text-th-fgd-3">
{orderbookData?.spread
? orderbookData.spread < SHOW_EXPONENTIAL_THRESHOLD
? orderbookData.spread.toExponential()
: formatNumericValue(
orderbookData.spread,
market ? getDecimalCount(market.tickSize) : undefined,
)
: null}
</span>
</div>
</div>
{depthArray.map((_x, index) => (

View File

@ -1,4 +1,4 @@
import { BookSide, PerpMarket } from '@blockworks-foundation/mango-v4'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { useQuery } from '@tanstack/react-query'
import useMangoGroup from 'hooks/useMangoGroup'
import useSelectedMarket from 'hooks/useSelectedMarket'
@ -6,8 +6,6 @@ import { useMemo } from 'react'
import { MANGO_DATA_API_URL } from 'utils/constants'
import Tooltip from '@components/shared/Tooltip'
import { useTranslation } from 'next-i18next'
import mangoStore from '@store/mangoStore'
import { OrderbookL2 } from 'types'
import Link from 'next/link'
const fetchFundingRate = async (groupPk: string | undefined) => {
@ -39,64 +37,11 @@ export const formatFunding = Intl.NumberFormat('en', {
style: 'percent',
})
function getImpactPriceL2(
bookside: number[][],
baseDepth: number,
): number | undefined {
let total = 0
for (const level of bookside) {
total += level[1]
if (total >= baseDepth) {
return level[0]
}
}
return undefined
}
function getInstantaneousFundingRateL2(
market: PerpMarket,
orderbook: OrderbookL2,
) {
const MIN_FUNDING = market.minFunding.toNumber()
const MAX_FUNDING = market.maxFunding.toNumber()
const bid = getImpactPriceL2(
orderbook.bids,
market.baseLotsToUi(market.impactQuantity),
)
const ask = getImpactPriceL2(
orderbook.asks,
market.baseLotsToUi(market.impactQuantity),
)
const indexPrice = market._uiPrice
let funding
if (bid !== undefined && ask !== undefined) {
const bookPrice = (bid + ask) / 2
funding = Math.min(
Math.max(bookPrice / indexPrice - 1, MIN_FUNDING),
MAX_FUNDING,
)
} else if (bid !== undefined) {
funding = MAX_FUNDING
} else if (ask !== undefined) {
funding = MIN_FUNDING
} else {
funding = 0
}
return funding
}
const PerpFundingRate = () => {
const { selectedMarket } = useSelectedMarket()
const rate = usePerpFundingRate()
const { t } = useTranslation(['common', 'trade'])
const bids = mangoStore((s) => s.selectedMarket.bidsAccount)
const asks = mangoStore((s) => s.selectedMarket.asksAccount)
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
const fundingRate = useMemo(() => {
if (rate.isSuccess && selectedMarket instanceof PerpMarket) {
const marketRate = rate?.data?.find(
@ -107,21 +52,6 @@ const PerpFundingRate = () => {
}
}, [rate, selectedMarket])
const instantaneousRate = useMemo(() => {
if (!(selectedMarket instanceof PerpMarket)) return undefined
if (bids instanceof BookSide && asks instanceof BookSide) {
return selectedMarket.getInstantaneousFundingRateUi(bids, asks).toFixed(4)
}
if (orderbook.asks.length && orderbook.bids.length) {
return (
getInstantaneousFundingRateL2(selectedMarket, orderbook) * 100
).toFixed(4)
}
return undefined
}, [orderbook, bids, asks, selectedMarket])
return (
<>
<div className="ml-6 flex-col whitespace-nowrap">
@ -138,20 +68,12 @@ const PerpFundingRate = () => {
</div>
{typeof fundingRate === 'number' ? (
<div className="mt-2">
The 1hr rate as an APR is{' '}
The annualized funding rate is{' '}
<span className="font-mono text-th-fgd-2">
{formatFunding.format(fundingRate * 8760)}
</span>
</div>
) : null}
{instantaneousRate ? (
<div className="mt-2">
The latest instantaneous rate is{' '}
<span className="font-mono text-th-fgd-2">
{instantaneousRate}%
</span>
</div>
) : null}
<Link
className="mt-2 block"
href={`/stats?market=${selectedMarket?.name}`}

View File

@ -26,11 +26,14 @@ import { Disclosure, Transition } from '@headlessui/react'
import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import PnlTooltipContent from '@components/shared/PnlTooltipContent'
import PerpSideBadge from './PerpSideBadge'
import CloseAllPositionsModal from './CloseAllPositionsModal'
import NukeIcon from '@components/icons/NukeIcon'
const PerpPositions = () => {
const { t } = useTranslation(['common', 'trade'])
const { group } = useMangoGroup()
const [showMarketCloseModal, setShowMarketCloseModal] = useState(false)
const [showCloseAllModal, setShowCloseAllModal] = useState(false)
const [positionToClose, setPositionToClose] = useState<PerpPosition | null>(
null,
)
@ -147,7 +150,23 @@ const PerpPositions = () => {
</div>
</Th>
<Th className="text-right">{t('trade:unrealized-pnl')}</Th>
{!isUnownedAccount ? <Th /> : null}
<Th className="text-right">ROE</Th>
{!isUnownedAccount ? (
<Th>
{openPerpPositions?.length > 1 ? (
<div className="flex justify-end">
<div className="flex items-center">
<NukeIcon className="mr-1.5 h-4 w-4 text-th-active" />
<LinkButton
onClick={() => setShowCloseAllModal(true)}
>
{t('trade:close-all')}
</LinkButton>
</div>
</div>
) : null}
</Th>
) : null}
</TrHead>
</thead>
<tbody>
@ -262,7 +281,7 @@ const PerpPositions = () => {
)}
</Td>
<Td className="text-right font-mono">
<div className="flex flex-col items-end ">
<div className="flex flex-col items-end">
<Tooltip
content={
<PnlTooltipContent
@ -270,7 +289,6 @@ const PerpPositions = () => {
realizedPnl={realizedPnl}
totalPnl={totalPnl}
unsettledPnl={unsettledPnl}
roe={roe}
/>
}
delay={100}
@ -291,6 +309,13 @@ const PerpPositions = () => {
</Tooltip>
</div>
</Td>
<Td className="text-right font-mono">
<span
className={roe >= 0 ? 'text-th-up' : 'text-th-down'}
>
<FormatNumericValue value={roe} decimals={2} />%
</span>
</Td>
{!isUnownedAccount ? (
<Td>
<div className="flex items-center justify-end space-x-4">
@ -300,7 +325,7 @@ const PerpPositions = () => {
size="small"
onClick={() => showClosePositionModal(position)}
>
Close
{t('close')}
</Button>
<IconButton
hideBg
@ -346,7 +371,6 @@ const PerpPositions = () => {
realizedPnl={totalPnlStats.realized}
totalPnl={totalPnlStats.total}
unsettledPnl={totalPnlStats.unsettled}
roe={totalPnlStats.roe}
/>
}
delay={100}
@ -368,6 +392,19 @@ const PerpPositions = () => {
</Tooltip>
</div>
</Td>
<Td className="text-right font-mono">
<span
className={
totalPnlStats.roe >= 0 ? 'text-th-up' : 'text-th-down'
}
>
<FormatNumericValue
value={totalPnlStats.roe}
decimals={2}
/>
%
</span>
</Td>
{!isUnownedAccount ? (
<Td className="text-right font-mono">
{' '}
@ -581,7 +618,6 @@ const PerpPositions = () => {
realizedPnl={realizedPnl}
totalPnl={totalPnl}
unsettledPnl={unsettledPnl}
roe={roe}
/>
}
delay={100}
@ -613,14 +649,26 @@ const PerpPositions = () => {
</div>
<div className="col-span-2 mt-3 flex space-x-3">
<Button
className="w-1/2"
className="w-full text-xs sm:text-sm"
secondary
onClick={() => showClosePositionModal(position)}
>
{t('trade:close-position')}
{t('close')}
</Button>
{openPerpPositions?.length > 1 ? (
<Button
className="w-full text-xs sm:text-sm"
secondary
onClick={() => setShowCloseAllModal(true)}
>
<div className="flex items-center justify-center">
<NukeIcon className="mr-2 h-4 w-4 flex-shrink-0 text-th-active" />
{t('trade:close-all')}
</div>
</Button>
) : null}
<Button
className="w-1/2"
className="w-full text-xs sm:text-sm"
secondary
onClick={() =>
handleShowShare(openPerpPositions[i])
@ -642,70 +690,42 @@ const PerpPositions = () => {
)
})}
{openPerpPositions.length > 0 ? (
<>
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button
className={`flex w-full justify-end border-t border-th-bkg-3 p-1 text-right focus:outline-none`}
>
<div className="ml-auto mt-1 flex flex-col justify-end">
<div className="flex flex-row">
<span className="text-md mr-3 font-body text-th-fgd-3">
Total Unrealized PnL:
</span>
<span
className={`mr-2 font-mono ${
totalPnlStats.unrealized > 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<FormatNumericValue
value={totalPnlStats.unrealized}
isUsd
decimals={2}
/>
</span>
</div>
<div className="flex flex-row justify-end">
<Transition
enter="transition ease-in duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
>
<Disclosure.Panel className="mt-1">
<span className="text-md mr-3 text-right font-body text-th-fgd-3">
Total ROE:
</span>
<span
className={`mr-1.5 font-mono ${
totalPnlStats.roe >= 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<FormatNumericValue
value={totalPnlStats.roe}
decimals={2}
/>
%{' '}
</span>
</Disclosure.Panel>
</Transition>
</div>
</div>
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
} mr-3 mt-1 h-6 w-6 flex-shrink-0 text-th-fgd-3`}
/>
</Disclosure.Button>
</>
)}
</Disclosure>
</>
<div className="flex items-center justify-end space-x-3 border-t border-th-bkg-3 px-4 py-2">
<span>
<span className="text-md font-body text-xs text-th-fgd-3">
Total Unrealized PnL:{' '}
</span>
<span
className={`font-mono ${
totalPnlStats.unrealized >= 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<FormatNumericValue
value={totalPnlStats.unrealized}
isUsd
decimals={2}
/>
</span>
</span>
<span>
<span className="text-md font-body text-xs text-th-fgd-3">
ROE:{' '}
</span>
<span
className={`font-mono ${
totalPnlStats.roe >= 0 ? 'text-th-up' : 'text-th-down'
}`}
>
<FormatNumericValue
value={totalPnlStats.roe}
decimals={2}
/>
%
</span>
</span>
</div>
) : null}
</div>
)
@ -734,6 +754,12 @@ const PerpPositions = () => {
position={positionToClose}
/>
) : null}
{showCloseAllModal ? (
<CloseAllPositionsModal
isOpen={showCloseAllModal}
onClose={() => setShowCloseAllModal(false)}
/>
) : null}
</>
)
}

View File

@ -14,7 +14,7 @@ import useSelectedMarket from 'hooks/useSelectedMarket'
import { useWallet } from '@solana/wallet-adapter-react'
import useIpAddress from 'hooks/useIpAddress'
import { useTranslation } from 'next-i18next'
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react'
import { FormEvent, useCallback, useMemo, useState } from 'react'
import Loading from '@components/shared/Loading'
import Button from '@components/shared/Button'
import Image from 'next/image'
@ -32,20 +32,16 @@ import useUnownedAccount from 'hooks/useUnownedAccount'
import HealthImpact from '@components/shared/HealthImpact'
import Tooltip from '@components/shared/Tooltip'
import Checkbox from '@components/forms/Checkbox'
import MaxMarketSwapAmount from './MaxMarketSwapAmount'
// import MaxMarketSwapAmount from './MaxMarketSwapAmount'
import { floorToDecimal, formatNumericValue } from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { useTokenMax } from '@components/swap/useTokenMax'
import SheenLoader from '@components/shared/SheenLoader'
import { fetchJupiterTransaction } from '@components/swap/SwapReviewRouteInfo'
import {
AddressLookupTableAccount,
TransactionInstruction,
} from '@solana/web3.js'
import MaxSwapAmount from '@components/swap/MaxSwapAmount'
const set = mangoStore.getState().set
const slippage = 100
function stringToNumberOrZero(s: string): number {
const n = parseFloat(s)
@ -55,11 +51,6 @@ function stringToNumberOrZero(s: string): number {
return n
}
type PreloadedTransaction = {
data: [TransactionInstruction[], AddressLookupTableAccount[]]
timestamp: number
}
export default function SpotMarketOrderSwapForm() {
const { t } = useTranslation()
const { baseSize, quoteSize, side } = mangoStore((s) => s.tradeForm)
@ -70,7 +61,6 @@ export default function SpotMarketOrderSwapForm() {
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [savedCheckboxSettings, setSavedCheckboxSettings] =
useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS)
const [swapTx, setSwapTx] = useState<PreloadedTransaction>()
const {
selectedMarket,
price: oraclePrice,
@ -167,7 +157,9 @@ export default function SpotMarketOrderSwapForm() {
}
}, [selectedMarket, side])
const { bestRoute: selectedRoute, isLoading } = useQuoteRoutes({
const slippage = mangoStore.getState().swap.slippage
const { bestRoute: selectedRoute, isLoading: loadingRoute } = useQuoteRoutes({
inputMint: inputBank?.mint.toString() || '',
outputMint: outputBank?.mint.toString() || '',
amount: side === 'buy' ? quoteSize : baseSize,
@ -177,13 +169,14 @@ export default function SpotMarketOrderSwapForm() {
mode: 'JUPITER',
})
const fetchTransaction = useCallback(async () => {
const handlePlaceOrder = useCallback(async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const { baseSize, quoteSize, side } = mangoStore.getState().tradeForm
const actions = mangoStore.getState().actions
const connection = mangoStore.getState().connection
if (!group || !mangoAccount) return
if (
!mangoAccount ||
!group ||
@ -194,6 +187,8 @@ export default function SpotMarketOrderSwapForm() {
)
return
setPlacingOrder(true)
const [ixs, alts] = await fetchJupiterTransaction(
connection,
selectedRoute,
@ -203,37 +198,7 @@ export default function SpotMarketOrderSwapForm() {
outputBank.mint,
)
setSwapTx({ data: [ixs, alts], timestamp: Date.now() })
return [ixs, alts]
}, [selectedRoute, inputBank, outputBank, publicKey])
useEffect(() => {
if (selectedRoute) fetchTransaction()
}, [selectedRoute, fetchTransaction])
const handlePlaceOrder = useCallback(async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const { baseSize, quoteSize, side } = mangoStore.getState().tradeForm
const actions = mangoStore.getState().actions
if (
!mangoAccount ||
!group ||
!inputBank ||
!outputBank ||
!publicKey ||
!selectedRoute ||
!swapTx
)
return
setPlacingOrder(true)
try {
const [ixs, alts] = swapTx.data
const { signature: tx, slot } = await client.marginTrade({
group,
mangoAccount,
@ -280,7 +245,7 @@ export default function SpotMarketOrderSwapForm() {
} finally {
setPlacingOrder(false)
}
}, [inputBank, outputBank, publicKey, selectedRoute, swapTx])
}, [inputBank, outputBank, publicKey, selectedRoute])
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
@ -376,7 +341,7 @@ export default function SpotMarketOrderSwapForm() {
const disabled =
(connected && (!baseSize || !oraclePrice)) ||
!serumOrPerpMarket ||
isLoading ||
loadingRoute ||
tooMuchSize
return (
@ -384,9 +349,10 @@ export default function SpotMarketOrderSwapForm() {
<form onSubmit={(e) => handleSubmit(e)}>
<div className="mt-3 px-3 md:px-4">
{!isUnownedAccount ? (
<MaxMarketSwapAmount
<MaxSwapAmount
useMargin={savedCheckboxSettings.margin}
setAmountIn={setAmountFromSlider}
maxAmount={useTokenMax}
/>
) : null}
<div className="flex flex-col">
@ -448,7 +414,7 @@ export default function SpotMarketOrderSwapForm() {
}
name="quote"
id="quote"
className="-mt-[1px] flex w-full items-center rounded-md rounded-t-none border border-th-input-border bg-th-input-bkg p-2 pl-9 font-mono text-sm font-bold text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus:border-th-fgd-4 lg:text-base"
className="mt-[-1px] flex w-full items-center rounded-md rounded-t-none border border-th-input-border bg-th-input-bkg p-2 pl-9 font-mono text-sm font-bold text-th-fgd-1 focus:border-th-fgd-4 focus:outline-none md:hover:border-th-input-border-hover md:hover:focus:border-th-fgd-4 lg:text-base"
placeholder="0.00"
value={quoteSize}
onValueChange={handleQuoteSizeChange}
@ -467,6 +433,7 @@ export default function SpotMarketOrderSwapForm() {
}
onChange={setAmountFromSlider}
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
maxAmount={useTokenMax}
/>
</div>
) : (
@ -497,7 +464,7 @@ export default function SpotMarketOrderSwapForm() {
</Checkbox>
</Tooltip>
</div>
<div className="mb-4 mt-6 flex" onMouseEnter={fetchTransaction}>
<div className="mb-4 mt-6 flex">
{ipAllowed ? (
<Button
className={`flex w-full items-center justify-center ${
@ -511,7 +478,7 @@ export default function SpotMarketOrderSwapForm() {
size="large"
type="submit"
>
{isLoading ? (
{loadingRoute ? (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
@ -582,7 +549,7 @@ export default function SpotMarketOrderSwapForm() {
>
<p className="tooltip-underline">{t('swap:price-impact')}</p>
</Tooltip>
{isLoading ? (
{loadingRoute ? (
<SheenLoader>
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
@ -664,7 +631,7 @@ export default function SpotMarketOrderSwapForm() {
) : null}
<div className="flex items-center justify-between text-xs">
<p className="pr-2 text-th-fgd-3">{t('common:route')}</p>
{isLoading ? (
{loadingRoute ? (
<SheenLoader>
<div className="h-3.5 w-20 bg-th-bkg-2" />
</SheenLoader>

View File

@ -47,7 +47,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive)
const TradeAdvancedPage = () => {
const { height, width } = useViewport()
const { uiLocked } = mangoStore((s) => s.settings)
const showMobileView = width <= breakpoints.md
const showMobileView = width < breakpoints.md
// const tourSettings = mangoStore((s) => s.settings.tours)
// const { connected } = useWallet()
// const [isOnboarded] = useLocalStorageState(IS_ONBOARDED_KEY)
@ -59,7 +59,14 @@ const TradeAdvancedPage = () => {
const minPageHeight = 1000
const topnavbarHeight = 64
const statusBarHeight = 27
const marketHeaderHeight = 48
const totalCols = 24
const innerHeight = useMemo(() => {
return Math.max(height - topnavbarHeight - statusBarHeight, minPageHeight)
}, [height])
const gridBreakpoints = useMemo(() => {
const sidebarWidth = isCollapsed ? 64 : 200
return {
@ -72,9 +79,6 @@ const TradeAdvancedPage = () => {
}, [isCollapsed])
const defaultLayouts: ReactGridLayout.Layouts = useMemo(() => {
const innerHeight = Math.max(height - topnavbarHeight, minPageHeight)
const marketHeaderHeight = 48
const balancesXPos = {
chartLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
chartMiddleOBRight: { xxxl: 4, xxl: 5, xl: 5, lg: 6 },
@ -145,14 +149,14 @@ const TradeAdvancedPage = () => {
x: formXPos[tradeLayout].xxxl,
y: 1,
w: 4,
h: getHeight(innerHeight, 0, 0),
h: getHeight(innerHeight, 0, marketHeaderHeight),
},
{
i: 'balances',
x: balancesXPos[tradeLayout].xxxl,
y: 2,
w: 20,
h: getHeight(innerHeight, 0, 640),
h: getHeight(innerHeight, 0, 640 + marketHeaderHeight),
},
],
xxl: [
@ -176,14 +180,14 @@ const TradeAdvancedPage = () => {
x: formXPos[tradeLayout].xxl,
y: 1,
w: 5,
h: getHeight(innerHeight, 0, 0),
h: getHeight(innerHeight, 0, marketHeaderHeight),
},
{
i: 'balances',
x: balancesXPos[tradeLayout].xxl,
y: 2,
w: 19,
h: getHeight(innerHeight, 0, 552),
h: getHeight(innerHeight, 0, 552 + marketHeaderHeight),
},
],
xl: [
@ -262,7 +266,7 @@ const TradeAdvancedPage = () => {
},
],
}
}, [height, tradeLayout])
}, [innerHeight, tradeLayout])
const [layouts, setLayouts] = useState<Layouts>(defaultLayouts)
const [breakpoint, setBreakpoint] = useState('')
@ -303,7 +307,7 @@ const TradeAdvancedPage = () => {
margin={[0, 0]}
useCSSTransforms
onLayoutChange={handleLayoutChange}
measureBeforeMount
// measureBeforeMount
>
<div key="market-header" className="z-10">
<AdvancedMarketHeader />

View File

@ -14,7 +14,11 @@ import {
TrHead,
} from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import { NoSymbolIcon, UsersIcon } from '@heroicons/react/20/solid'
import {
EyeSlashIcon,
NoSymbolIcon,
UsersIcon,
} from '@heroicons/react/20/solid'
import { useWallet } from '@solana/wallet-adapter-react'
import { PublicKey } from '@solana/web3.js'
import mangoStore from '@store/mangoStore'
@ -32,6 +36,7 @@ import PerpSideBadge from './PerpSideBadge'
import TableMarketName from './TableMarketName'
import { useSortableData } from 'hooks/useSortableData'
import { useCallback } from 'react'
import { useHiddenMangoAccounts } from 'hooks/useHiddenMangoAccounts'
const TradeHistory = () => {
const { t } = useTranslation(['common', 'trade'])
@ -45,6 +50,7 @@ const TradeHistory = () => {
} = useTradeHistory()
const { width } = useViewport()
const { connected } = useWallet()
const { hiddenAccounts } = useHiddenMangoAccounts()
const showTableView = width ? width > breakpoints.md : false
const formattedTableData = useCallback(() => {
@ -142,6 +148,14 @@ const TradeHistory = () => {
{tableData.map((trade, index: number) => {
const { side, price, market, size, feeCost, liquidity, value } =
trade
let counterpartyAddress = ''
if ('taker' in trade) {
counterpartyAddress =
trade.liquidity === 'Taker'
? trade.maker.toString()
: trade.taker.toString()
}
return (
<TrBody
key={`${side}${size}${price}${index}`}
@ -184,29 +198,41 @@ const TradeHistory = () => {
<Td className="xl:!pl-0">
{'taker' in trade ? (
<div className="flex justify-end">
<Tooltip
content={`View Counterparty ${abbreviateAddress(
trade.liquidity === 'Taker'
? new PublicKey(trade.maker)
: new PublicKey(trade.taker),
)}`}
delay={0}
>
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${
trade.liquidity === 'Taker'
? trade.maker
: trade.taker
}`}
{!hiddenAccounts?.includes(counterpartyAddress) ? (
<Tooltip
content={t('trade:tooltip-view-counterparty', {
pk: abbreviateAddress(
trade.liquidity === 'Taker'
? new PublicKey(trade.maker)
: new PublicKey(trade.taker),
),
})}
delay={0}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${counterpartyAddress}`}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
</Tooltip>
) : (
<Tooltip
content={t('trade:tooltip-private-counterparty')}
>
<IconButton
className="bg-th-bkg-1"
disabled
size="small"
>
<EyeSlashIcon className="h-4 w-4" />
</IconButton>
</a>
</Tooltip>
</Tooltip>
)}
</div>
) : null}
</Td>
@ -220,6 +246,13 @@ const TradeHistory = () => {
<div>
{combinedTradeHistory.map((trade, index: number) => {
const { side, price, market, size, liquidity } = trade
let counterpartyAddress = ''
if ('taker' in trade) {
counterpartyAddress =
trade.liquidity === 'Taker'
? trade.maker.toString()
: trade.taker.toString()
}
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
@ -259,18 +292,41 @@ const TradeHistory = () => {
</div>
</div>
{'taker' in trade ? (
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${
liquidity === 'Taker' ? trade.maker : trade.taker
}`}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
!hiddenAccounts?.includes(counterpartyAddress) ? (
<Tooltip
content={t('trade:tooltip-view-counterparty', {
pk: abbreviateAddress(
liquidity === 'Taker'
? new PublicKey(trade.maker)
: new PublicKey(trade.taker),
),
})}
delay={0}
>
<a
className=""
target="_blank"
rel="noopener noreferrer"
href={`/?address=${counterpartyAddress}`}
>
<IconButton size="small">
<UsersIcon className="h-4 w-4" />
</IconButton>
</a>
</Tooltip>
) : (
<Tooltip
content={t('trade:tooltip-private-counterparty')}
>
<IconButton
className="bg-th-bkg-1"
disabled
size="small"
>
<EyeSlashIcon className="h-4 w-4" />
</IconButton>
</Tooltip>
)
) : null}
</div>
</div>

View File

@ -12,7 +12,6 @@ import {
import { HotKey } from '@components/settings/HotKeysSettings'
import mangoStore from '@store/mangoStore'
import { ReactNode, useCallback } from 'react'
import Hotkeys from 'react-hot-keys'
import { GenericMarket, isMangoError } from 'types'
import { HOT_KEYS_KEY, SOUND_SETTINGS_KEY } from 'utils/constants'
import { notify } from 'utils/notifications'
@ -22,11 +21,9 @@ import useLocalStorageState from 'hooks/useLocalStorageState'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import useMangoAccount from 'hooks/useMangoAccount'
import { Market } from '@project-serum/serum'
import { useRouter } from 'next/router'
import useUnownedAccount from 'hooks/useUnownedAccount'
import { useTranslation } from 'next-i18next'
import { useCustomHotkeys } from 'hooks/useCustomHotKeys'
const set = mangoStore.getState().set
@ -169,11 +166,8 @@ const calcPerpMax = (
}
const TradeHotKeys = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation(['common', 'settings'])
const { t } = useTranslation(['common', 'settings', 'trade'])
const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket()
const { mangoAccountAddress } = useMangoAccount()
const { isUnownedAccount } = useUnownedAccount()
const { asPath } = useRouter()
const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [soundSettings] = useLocalStorageState(
SOUND_SETTINGS_KEY,
@ -270,7 +264,7 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => {
notify({
type: 'info',
title: t('settings:placing-order'),
title: t('trade:placing-order'),
description: `${t(orderSide)} ${baseSize} ${selectedMarket.name} ${
orderType === 'limit'
? `${t('settings:at')} ${price}`
@ -360,34 +354,9 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => {
[serumOrPerpMarket],
)
const onKeyDown = useCallback(
(keyName: string) => {
const orderDetails = hotKeys.find(
(hk: HotKey) => hk.keySequence === keyName,
)
if (orderDetails) {
handlePlaceOrder(orderDetails)
}
},
[handlePlaceOrder, hotKeys],
)
useCustomHotkeys(hotKeys, handlePlaceOrder)
const showHotKeys =
hotKeys.length &&
asPath.includes('/trade') &&
mangoAccountAddress &&
!isUnownedAccount
return showHotKeys ? (
<Hotkeys
keyName={hotKeys.map((k: HotKey) => k.keySequence).toString()}
onKeyDown={onKeyDown}
>
{children}
</Hotkeys>
) : (
<>{children}</>
)
return <>{children}</>
}
export default TradeHotKeys

View File

@ -11,6 +11,7 @@ import { breakpoints } from 'utils/theme'
import useUnsettledPerpPositions from 'hooks/useUnsettledPerpPositions'
import TradeHistory from './TradeHistory'
import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import ManualRefresh from '@components/shared/ManualRefresh'
const TradeInfoTabs = () => {
const [selectedTab, setSelectedTab] = useState('balances')
@ -19,8 +20,8 @@ const TradeInfoTabs = () => {
const unsettledSpotBalances = useUnsettledSpotBalances()
const unsettledPerpPositions = useUnsettledPerpPositions()
const openPerpPositions = useOpenPerpPositions()
const { width } = useViewport()
const isMobile = width ? width < breakpoints['2xl'] : false
const { isMobile, isTablet, width } = useViewport()
const fillTabWidth = width ? width < breakpoints['2xl'] : false
useEffect(() => {
if (selectedMarketName && selectedMarketName.includes('PERP')) {
@ -48,27 +49,49 @@ const TradeInfoTabs = () => {
return (
<div className="hide-scroll h-full overflow-y-scroll">
<div className="hide-scroll overflow-x-auto border-b border-th-bkg-3">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}
values={tabsWithCount}
showBorders
fillWidth={isMobile}
<div className="hide-scroll flex items-center overflow-x-auto border-b border-th-bkg-3">
<div className="w-full md:w-auto md:border-r md:border-th-bkg-3 lg:w-full">
<TabButtons
activeValue={selectedTab}
onChange={(tab: string) => setSelectedTab(tab)}
values={tabsWithCount}
showBorders
fillWidth={fillTabWidth}
/>
</div>
<ManualRefresh
classNames="fixed bottom-16 right-4 md:relative md:px-2 md:bottom-0 md:right-0 z-10 shadow-lg md:shadow-none bg-th-bkg-3 md:bg-transparent"
hideBg={isMobile || isTablet}
size={isTablet ? 'large' : 'small'}
/>
</div>
{selectedTab === 'balances' ? <SwapTradeBalances /> : null}
{selectedTab === 'trade:orders' ? <OpenOrders /> : null}
{selectedTab === 'trade:unsettled' ? (
<UnsettledTrades
unsettledSpotBalances={unsettledSpotBalances}
unsettledPerpPositions={unsettledPerpPositions}
/>
) : null}
{selectedTab === 'trade:positions' ? <PerpPositions /> : null}
{selectedTab === 'trade-history' ? <TradeHistory /> : null}
<TabContent selectedTab={selectedTab} />
</div>
)
}
export default TradeInfoTabs
const TabContent = ({ selectedTab }: { selectedTab: string }) => {
const unsettledSpotBalances = useUnsettledSpotBalances()
const unsettledPerpPositions = useUnsettledPerpPositions()
switch (selectedTab) {
case 'balances':
return <SwapTradeBalances />
case 'trade:orders':
return <OpenOrders />
case 'trade:unsettled':
return (
<UnsettledTrades
unsettledSpotBalances={unsettledSpotBalances}
unsettledPerpPositions={unsettledPerpPositions}
/>
)
case 'trade:positions':
return <PerpPositions />
case 'trade-history':
return <TradeHistory />
default:
return <SwapTradeBalances />
}
}

View File

@ -1,4 +1,5 @@
import {
Bank,
HealthType,
MangoAccount,
PerpMarket,
@ -24,11 +25,11 @@ import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import { calculateEstPriceForBaseSize } from 'utils/tradeForm'
const TradeSummary = ({
balanceBank,
mangoAccount,
useMargin,
}: {
balanceBank: Bank | undefined
mangoAccount: MangoAccount | undefined
useMargin: boolean
}) => {
const { t } = useTranslation(['common', 'trade'])
const { group } = useMangoGroup()
@ -140,21 +141,6 @@ const TradeSummary = ({
: Math.trunc(simulatedHealthRatio)
}, [group, mangoAccount, selectedMarket, tradeForm])
const balanceBank = useMemo(() => {
if (
!group ||
!selectedMarket ||
selectedMarket instanceof PerpMarket ||
!useMargin
)
return
if (tradeForm.side === 'buy') {
return group.getFirstBankByTokenIndex(selectedMarket.quoteTokenIndex)
} else {
return group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
}
}, [group, selectedMarket, tradeForm.side])
const [balance, borrowAmount] = useMemo(() => {
if (!balanceBank || !mangoAccount) return [0, 0]
let borrowAmount

View File

@ -19,7 +19,6 @@ import {
import mangoStore from '@store/mangoStore'
import { useViewport } from 'hooks/useViewport'
import { SHOW_ORDER_LINES_KEY, TV_USER_ID_KEY } from 'utils/constants'
import { breakpoints } from 'utils/theme'
import { COLORS } from 'styles/colors'
import { useTranslation } from 'next-i18next'
import { notify } from 'utils/notifications'
@ -72,9 +71,9 @@ function hexToRgb(hex: string) {
}
const TradingViewChart = () => {
const { t } = useTranslation(['tv-chart', 'trade'])
const { t } = useTranslation(['common', 'tv-chart', 'trade'])
const { theme } = useThemeWrapper()
const { width } = useViewport()
const { isMobile } = useViewport()
const [chartReady, setChartReady] = useState(false)
const [headerReady, setHeaderReady] = useState(false)
const [orderToModify, setOrderToModify] = useState<Order | PerpOrder | null>(
@ -96,7 +95,6 @@ const TradingViewChart = () => {
useState(combinedTradeHistory)
const [userId] = useLocalStorageState(TV_USER_ID_KEY, '')
const selectedMarketName = mangoStore((s) => s.selectedMarket.current?.name)
const isMobile = width ? width < breakpoints.sm : false
const defaultProps = useMemo(() => {
const initialMktName = mangoStore.getState().selectedMarket.current?.name
@ -741,17 +739,31 @@ const TradingViewChart = () => {
.slice()
for (let i = 0; i < filteredTrades.length; i++) {
const trade = filteredTrades[i]
const { side, size, price, market, liquidity, time } = trade
let baseSymbol
let quoteSymbol
if (market instanceof Serum3Market) {
baseSymbol = market.name.split('/')[0]
quoteSymbol = market.name.split('/')[1]
} else {
baseSymbol = market.name.split('-')[0]
}
const orderType = liquidity === 'Taker' ? t('market') : t('trade:limit')
try {
const arrowID = tvWidgetRef
.current!.chart()
.createExecutionShape()
.setTime(dayjs(trade.time).unix())
.setDirection(trade.side as Direction)
.setTime(dayjs(time).unix())
.setDirection(side as Direction)
.setArrowHeight(6)
.setArrowColor(
trade.side === 'buy' ? COLORS.UP[theme] : COLORS.DOWN[theme],
side === 'buy' ? COLORS.UP[theme] : COLORS.DOWN[theme],
)
.setTooltip(
`${t(side)} ${orderType} ${size} ${baseSymbol} @ ${price}${
quoteSymbol ? ` ${quoteSymbol}` : ''
}`,
)
.setTooltip(`${trade.size} at ${trade.price}`)
if (arrowID) {
try {
newTradeExecutions.set(`${trade.time}${i}`, arrowID)
@ -769,7 +781,7 @@ const TradingViewChart = () => {
}
return newTradeExecutions
},
[selectedMarketName, theme],
[selectedMarketName, t, theme],
)
const removeTradeExecutions = useCallback(

View File

@ -12,7 +12,6 @@ import { notify } from '../../utils/notifications'
import ProfileImage from '../profile/ProfileImage'
import { abbreviateAddress } from '../../utils/formatting'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from '../../utils/theme'
import EditProfileModal from '@components/modals/EditProfileModal'
import MangoAccountsListModal from '@components/modals/MangoAccountsListModal'
import { TV_USER_ID_KEY } from 'utils/constants'
@ -25,7 +24,7 @@ const actions = mangoStore.getState().actions
const ConnectedMenu = () => {
const { t } = useTranslation('common')
const { publicKey, disconnect, wallet } = useWallet()
const { width } = useViewport()
const { isTablet, isDesktop } = useViewport()
const [tvUserId, setTvUserId] = useLocalStorageState(TV_USER_ID_KEY, '')
const [showEditProfileModal, setShowEditProfileModal] = useState(false)
const [showMangoAccountsModal, setShowMangoAccountsModal] = useState(false)
@ -34,7 +33,6 @@ const ConnectedMenu = () => {
const loadProfileDetails = mangoStore((s) => s.profile.loadDetails)
const groupLoaded = mangoStore((s) => s.groupLoaded)
const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad)
const isMobile = width ? width < breakpoints.md : false
const handleDisconnect = useCallback(() => {
set((state) => {
@ -71,7 +69,7 @@ const ConnectedMenu = () => {
<div className="relative">
<Popover.Button
className={`default-transition h-16 ${
!isMobile ? 'w-48 border-l border-th-bkg-3 px-4' : 'w-16'
isDesktop ? 'w-48 border-l border-th-bkg-3 px-4' : 'w-16'
} hover:bg-th-bkg-2 focus:outline-none focus-visible:bg-th-bkg-3`}
>
<div
@ -87,7 +85,7 @@ const ConnectedMenu = () => {
) : (
<Loading className="h-6 w-6" />
)}
{!loadProfileDetails && !isMobile ? (
{!loadProfileDetails && isDesktop ? (
<div className="ml-2.5 overflow-hidden text-left">
<p className="text-xs text-th-fgd-3">
{wallet?.adapter.name}
@ -123,7 +121,7 @@ const ConnectedMenu = () => {
{t('profile:edit-profile-pic')}
</div>
</button>
{isMobile ? (
{isTablet ? (
<button
className="flex w-full flex-row items-center rounded-none py-0.5 font-normal focus:outline-none focus-visible:text-th-active"
onClick={() => setShowMangoAccountsModal(true)}

22
hooks/useCustomHotKeys.ts Normal file
View File

@ -0,0 +1,22 @@
import { HotKey } from '@components/settings/HotKeysSettings'
import { useHotkeys } from 'react-hotkeys-hook'
export const useCustomHotkeys = (
hotkeys: HotKey[],
handleHotkeyAction: (hotkey: HotKey) => void,
) => {
hotkeys.forEach((hotkey: HotKey) => {
const { keySequence } = hotkey
useHotkeys(
keySequence,
(event) => {
event.preventDefault()
handleHotkeyAction(hotkey)
},
{
keydown: true,
},
)
})
}

46
hooks/useFilledOrders.ts Normal file
View File

@ -0,0 +1,46 @@
import { PerpOrder } from '@blockworks-foundation/mango-v4'
import mangoStore from '@store/mangoStore'
import { useQuery } from '@tanstack/react-query'
import { fetchFilledOrders } from 'utils/account'
import useMangoAccount from './useMangoAccount'
import { useMemo } from 'react'
export default function useFilledOrders() {
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
const orderIds = useMemo(() => {
if (!mangoAccount || !Object.values(openOrders).flat().length) return []
const perpIds = mangoAccount.perpOpenOrders
.filter((o) => o.orderMarket !== 65535)
.map((p) => p.clientId.toString())
const spotIds = Object.values(openOrders)
.flat()
.filter((o) => !(o instanceof PerpOrder))
.map((s) => s.orderId.toString())
const ids = spotIds.concat(perpIds)
return ids
}, [mangoAccount, openOrders])
const {
data: filledOrders,
isFetching: fetchingFilledOrders,
refetch,
} = useQuery(
['filled-orders', mangoAccountAddress, orderIds],
() => fetchFilledOrders(mangoAccountAddress, orderIds),
{
cacheTime: 1000 * 60 * 10,
staleTime: 30,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress && !!orderIds.length,
},
)
return {
filledOrders,
fetchingFilledOrders,
refetch,
}
}

View File

@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query'
import { MANGO_DATA_API_URL } from 'utils/constants'
const fetchAllHiddenMangoAccounts = async (): Promise<string[]> => {
try {
const hideResponse = await fetch(
`${MANGO_DATA_API_URL}/user-data/private-accounts`,
)
const res = await hideResponse.json()
return res?.private_accounts ?? []
} catch (e) {
console.error('Failed to fetch private mango accounts', e)
return []
}
}
export function useHiddenMangoAccounts() {
const { data: hiddenAccounts, isLoading: loadingHiddenAccounts } = useQuery(
['all-hidden-accounts'],
() => fetchAllHiddenMangoAccounts(),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
},
)
return {
hiddenAccounts,
loadingHiddenAccounts,
}
}

View File

@ -1,13 +1,13 @@
import mangoStore from '@store/mangoStore'
import { useMemo } from 'react'
import useMangoAccount from './useMangoAccount'
import { MAX_ACCOUNTS } from '@components/modals/MangoAccountSizeModal'
import {
PerpOo,
PerpPosition,
Serum3Orders,
TokenPosition,
} from '@blockworks-foundation/mango-v4'
import { MAX_ACCOUNTS } from 'utils/constants'
export const getAvaialableAccountsColor = (used: number, total: number) => {
const remaining = total - used
@ -50,7 +50,9 @@ export default function useMangoAccountAccounts() {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccountAddress || !mangoAccount) return [[], [], [], []]
const { tokens, serum3, perps, perpOpenOrders } = mangoAccount
const usedTokens: TokenPosition[] = tokens.filter((t) => t.inUseCount)
const usedTokens: TokenPosition[] = tokens.filter(
(t) => t.tokenIndex !== 65535,
)
const usedSerum3: Serum3Orders[] = serum3.filter(
(s) => s.marketIndex !== 65535,
)

View File

@ -0,0 +1,76 @@
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { PublicKey } from '@solana/web3.js'
import { useQuery } from '@tanstack/react-query'
import { MANGO_DATA_API_URL } from 'utils/constants'
import useMangoAccount from './useMangoAccount'
const fetchMangoAccountHidden = async (mangoAccountAddress: string) => {
try {
const hideResponse = await fetch(
`${MANGO_DATA_API_URL}/user-data/account-hidden?mango-account=${mangoAccountAddress}`,
)
const res = await hideResponse.json()
return res?.hidden ?? false
} catch (e) {
console.error('Failed to fetch mango account privacy', e)
}
}
export function useMangoAccountHidden() {
const { mangoAccountAddress } = useMangoAccount()
const {
data: accountHidden,
isLoading: loadingAccountHidden,
refetch,
} = useQuery(
['account-hidden', mangoAccountAddress],
() => fetchMangoAccountHidden(mangoAccountAddress),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: !!mangoAccountAddress,
},
)
return {
accountHidden,
loadingAccountHidden,
refetch,
}
}
export const toggleMangoAccountHidden = async (
mangoAccountPk: PublicKey,
walletPk: PublicKey,
hidden: boolean,
signMessage: (message: Uint8Array) => Promise<Uint8Array>,
) => {
try {
let messageObject = {
mango_account_pk: mangoAccountPk.toString(),
wallet_pk: walletPk.toString(),
hidden: hidden,
}
const messageString = JSON.stringify(messageObject)
const message = new TextEncoder().encode(messageString)
const signature = await signMessage(message)
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet_pk: walletPk.toString(),
message: messageString,
signature: bs58.encode(signature),
}),
}
return fetch(
`${MANGO_DATA_API_URL}/user-data/account-hidden`,
requestOptions,
)
} catch (e) {
console.error('Failed to toggle mango account privacy', e)
}
}

View File

@ -39,7 +39,7 @@ export const getOracleProvider = (
case OracleProvider.Switchboard:
return [
'Switchboard',
`https://switchboard.xyz/explorer/3/${marketOrBase.oracle.toString()}`,
`https://app.switchboard.xyz/solana/mainnet-beta/feed/${marketOrBase.oracle.toString()}`,
]
case OracleProvider.Stub:
return ['Stub', '']

View File

@ -0,0 +1,50 @@
import mangoStore from '@store/mangoStore'
import { useMemo } from 'react'
import useSelectedMarket from './useSelectedMarket'
import {
I80F48,
Serum3Market,
toUiDecimals,
toUiDecimalsForQuote,
} from '@blockworks-foundation/mango-v4'
export default function useRemainingBorrowsInPeriod(isSwap?: boolean) {
const { selectedMarket } = useSelectedMarket()
const { inputBank } = mangoStore((s) => s.swap)
const { side } = mangoStore((s) => s.tradeForm)
const bank = useMemo(() => {
if (isSwap && inputBank) {
return inputBank
} else {
if (selectedMarket instanceof Serum3Market) {
const group = mangoStore.getState().group
let balanceBank
if (side === 'buy') {
balanceBank = group?.getFirstBankByTokenIndex(
selectedMarket.quoteTokenIndex,
)
} else {
balanceBank = group?.getFirstBankByTokenIndex(
selectedMarket.baseTokenIndex,
)
}
return balanceBank
}
}
return
}, [inputBank, isSwap, selectedMarket, side])
const [remainingBorrowsInPeriod, timeToNextPeriod] = useMemo(() => {
if (!bank) return [undefined, undefined]
const borrowsInPeriod = toUiDecimalsForQuote(
I80F48.fromI64(bank.netBorrowsInWindow).mul(bank.price),
)
const borrowLimit = toUiDecimals(bank.netBorrowLimitPerWindowQuote, 6)
const remainingBorrows = borrowLimit - borrowsInPeriod
const timeToNextPeriod = bank.getTimeToNextBorrowLimitWindowStartsTs()
return [remainingBorrows, timeToNextPeriod]
}, [bank])
return { remainingBorrowsInPeriod, timeToNextPeriod }
}

View File

@ -0,0 +1,34 @@
import { Bank } from '@blockworks-foundation/mango-v4'
import useMangoAccountAccounts from './useMangoAccountAccounts'
import { useMemo } from 'react'
export default function useTokenPositionsFull(
buyBank: Bank | undefined,
sellBank: Bank | undefined,
) {
const { usedTokens, totalTokens } = useMangoAccountAccounts()
const tokenPositionsFull = useMemo(() => {
if (!buyBank || !sellBank || !usedTokens.length || !totalTokens.length)
return false
const hasInputTokenPosition = usedTokens.find(
(token) => token.tokenIndex === buyBank.tokenIndex,
)
const hasOutputTokenPosition = usedTokens.find(
(token) => token.tokenIndex === sellBank.tokenIndex,
)
const availableTokenPositions = totalTokens.length - usedTokens.length
if (
(hasInputTokenPosition && hasOutputTokenPosition) ||
availableTokenPositions >= 2
) {
return false
} else if (
(hasInputTokenPosition && !hasOutputTokenPosition) ||
(!hasInputTokenPosition && hasOutputTokenPosition)
) {
return availableTokenPositions >= 1 ? false : true
} else return true
}, [buyBank, sellBank, usedTokens, totalTokens])
return tokenPositionsFull
}

View File

@ -1,21 +1,18 @@
import { useCallback, useEffect, useState } from 'react'
import mangoStore from '@store/mangoStore'
import { useMemo } from 'react'
import { breakpoints } from 'utils/theme'
export const useViewport = () => {
const [width, setWidth] = useState<number>(0)
const [height, setHeight] = useState<number>(0)
const width = mangoStore((s) => s.window.width)
const height = mangoStore((s) => s.window.height)
const handleWindowResize = useCallback(() => {
if (typeof window !== 'undefined') {
setWidth(window.innerWidth)
setHeight(window.innerHeight)
}
}, [])
const [isMobile, isTablet, isDesktop] = useMemo(() => {
if (!width) return [false, false, false]
const mobile = width < breakpoints.sm
const tablet = width >= breakpoints.sm && width < breakpoints.md
const desktop = width >= breakpoints.md
return [mobile, tablet, desktop]
}, [width])
useEffect(() => {
handleWindowResize()
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
}, [handleWindowResize])
return { width, height }
return { width, height, isMobile, isTablet, isDesktop }
}

View File

@ -22,8 +22,8 @@
},
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-v4": "^0.19.3",
"@blockworks-foundation/mango-v4-settings": "0.2.6",
"@blockworks-foundation/mango-v4": "^0.19.27",
"@blockworks-foundation/mango-v4-settings": "0.2.10",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
"@metaplex-foundation/js": "0.19.4",
@ -38,7 +38,6 @@
"@switchboard-xyz/solana.js": "2.5.4",
"@tanstack/react-query": "4.10.1",
"@tippyjs/react": "4.2.6",
"@types/howler": "2.2.7",
"@web3auth/sign-in-with-solana": "1.0.0",
"big.js": "6.2.1",
"clsx": "1.2.1",
@ -63,12 +62,11 @@
"react-dom": "18.2.0",
"react-flip-numbers": "3.0.5",
"react-grid-layout": "1.3.4",
"react-hot-keys": "2.7.2",
"react-hotkeys-hook": "4.4.1",
"react-i18next": "13.0.2",
"react-nice-dates": "3.1.0",
"react-number-format": "4.9.2",
"react-tsparticles": "2.2.4",
"react-window": "1.8.7",
"recharts": "2.5.0",
"tsparticles": "2.2.4",
"walktour": "5.1.1",
@ -77,19 +75,19 @@
"peerDependencies": {
"@project-serum/anchor": "0.25.0",
"@project-serum/serum": "0.13.65",
"@solana/web3.js": ">=1.70.1"
"@solana/web3.js": ">=1.78.2"
},
"devDependencies": {
"@lavamoat/allow-scripts": "2.3.0",
"@lavamoat/preinstall-always-fail": "^1.0.0",
"@types/big.js": "6.1.6",
"@types/howler": "2.2.7",
"@types/js-cookie": "3.0.3",
"@types/lodash": "4.14.185",
"@types/node": "17.0.23",
"@types/react": "18.0.3",
"@types/react-dom": "18.0.0",
"@types/react-grid-layout": "1.3.2",
"@types/react-window": "1.8.5",
"@types/recharts": "1.8.24",
"@typescript-eslint/eslint-plugin": "5.43.0",
"autoprefixer": "10.4.13",

View File

@ -176,12 +176,12 @@ const PageTitle = () => {
market && selectedMarket && router.pathname == '/trade'
? `${price?.toFixed(getDecimalCount(market.tickSize))} ${
selectedMarket.name
} - `
: ''
} - Mango`
: 'Mango Markets'
return (
<Head>
<title>{marketTitleString}Mango Markets</title>
<title>{marketTitleString}</title>
</Head>
)
}

View File

@ -22,6 +22,10 @@ import GovernancePageWrapper from '@components/governance/GovernancePageWrapper'
import TokenLogo from '@components/shared/TokenLogo'
import DashboardSuggestedValues from '@components/modals/DashboardSuggestedValuesModal'
import { USDC_MINT } from 'utils/constants'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
export async function getStaticProps({ locale }: { locale: string }) {
return {
@ -307,7 +311,12 @@ const Dashboard: NextPage = () => {
value={`${formattedBankValues.minVaultToDepositsRatio}%`}
/>
<KeyValuePair
label="Net borrows in window / Net borrow limit per window quote"
label={`Net borrows in window (next window starts ${dayjs().to(
dayjs().add(
bank.getTimeToNextBorrowLimitWindowStartsTs(),
'second',
),
)})`}
value={`$${formattedBankValues.minVaultToDepositsRatio} / $${formattedBankValues.netBorrowLimitPerWindowQuote}`}
/>
<KeyValuePair

View File

@ -124,14 +124,17 @@ const RiskDashboard: NextPage = () => {
<tbody>
{table.data.map((rowData, index: number) => {
return (
<TrBody key={index}>
<TrBody
className={index % 2 === 0 ? 'bg-th-bkg-2' : ''}
key={index}
>
{Object.values(rowData).map(
(val, idx: number) => {
return (
<Td
xBorder
className={`${
val?.highlight ? 'bg-th-bkg-2' : ''
val?.highlight ? 'bg-th-bkg-4' : ''
}`}
key={idx}
>

View File

@ -36,7 +36,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
const Market: NextPage = () => {
// const { t } = useTranslation('nft-market')
useMetaplex()
const [activeTab, setActiveTab] = useState('Listings')
const [activeTab, setActiveTab] = useState(LISTINGS)
const [sellNftModal, setSellNftModal] = useState(false)
const [myBidsModal, setMyBidsModal] = useState(false)
const { data: isWhiteListed } = useIsWhiteListed()

View File

@ -20,6 +20,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
'notifications',
'onboarding',
'onboarding-tours',
'profile',
'trade',
'close-account',
'tv-chart',
@ -88,7 +89,7 @@ const Trade: NextPage = () => {
}, [marketName])
return (
<div className="pb-16 md:pb-0">
<div className="pb-32 md:pb-0">
<TradeAdvancedPage />
</div>
)

16
public/icons/usdh.svg Normal file
View File

@ -0,0 +1,16 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_106_38)">
<circle cx="16" cy="16" r="16" fill="url(#paint0_linear_106_38)"/>
<path d="M12.6673 9.3335H10.0006C9.63246 9.3335 9.33398 9.63197 9.33398 10.0002V22.0002C9.33398 22.3684 9.63246 22.6668 10.0006 22.6668H12.6673C13.0355 22.6668 13.334 22.3684 13.334 22.0002V17.3332H18.666V22.0002C18.666 22.3684 18.9645 22.6668 19.3327 22.6668H21.9994C22.3675 22.6668 22.666 22.3684 22.666 22.0002V10.0002C22.666 9.63197 22.3675 9.3335 21.9994 9.3335H19.3327C18.9645 9.3335 18.666 9.63197 18.666 10.0002V14.6665H13.334V10.0002C13.334 9.63197 13.0355 9.3335 12.6673 9.3335Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_106_38" x1="-27.3846" y1="-20" x2="53.2007" y2="53.8789" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF00FF"/>
<stop offset="0.531673" stop-color="#7A84FF"/>
<stop offset="1" stop-color="#05FAFF"/>
</linearGradient>
<clipPath id="clip0_106_38">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -2,7 +2,7 @@
"assets": "Assets",
"assets-liabilities": "Assets & Liabilities",
"collateral-value": "Collateral Value",
"cumulative-interest-chart": "Cumulative Interst Chart",
"cumulative-interest-chart": "Cumulative Interest Chart",
"daily-volume": "24h Volume",
"export": "Export {{dataType}}",
"funding-chart": "Funding Chart",

View File

@ -19,6 +19,7 @@
"all": "All",
"amount": "Amount",
"amount-owed": "Amount Owed",
"asked-sign-transaction": "You'll be asked to sign a transaction",
"asset-liability-weight": "Asset/Liability Weights",
"asset-liability-weight-desc": "Asset weight applies a haircut to the value of the collateral in your account health calculation. The lower the asset weight, the less the asset counts towards collateral. Liability weight does the opposite (adds to the value of the liability in your health calculation).",
"asset-weight": "Asset Weight",
@ -75,7 +76,9 @@
"edit": "Edit",
"edit-account": "Edit Account Name",
"edit-profile-image": "Edit Profile Image",
"error-token-positions-full": "No token positions available in your account.",
"enable-notifications": "Enable Notifications",
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
"error-token-positions-full": "Not enough token positions available in your account.",
"explorer": "Explorer",
"fee": "Fee",
"feedback-survey": "Feedback Survey",
@ -193,6 +196,8 @@
"list-market-token": "List Market/Token",
"vote": "Vote",
"yes": "Yes",
"you": "You"
"you": "You",
"using-ledger": "Using Ledger",
"sign-to-in-app-notifications": "Sign to in app notifications"
}

View File

@ -4,6 +4,7 @@
"empty-state-title": "Nothing to see here",
"notifications": "Notifications",
"sign-message": "Sign Message",
"sign-using-ledger": "Sign with Ledger",
"unauth-desc": "Sign with your wallet to start receiving notifications",
"unauth-title": "Notifications Inbox"
}

View File

@ -66,8 +66,11 @@
"percentage-of-max": "{{size}}% of Max",
"perp-open-orders": "Perp Open Orders",
"perp-positions": "Perp Positions",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"privacy-disable": "Disable Privacy Mode",
"privacy-enable": "Enable Privacy Mode",
"private-account": "Private Account",
"privacy-mode": "Privacy Mode",
"recent-trades": "Recent Trades",
"rpc": "RPC",
"rpc-provider": "RPC Provider",
@ -89,10 +92,12 @@
"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-perp-positions": "The number of perp markets you can have positions in. The maximum for new accounts is {{max}}",
"tooltip-perp-open-orders": "The number of perp markets you can have open orders in. The maximum for new accounts is {{max}}",
"tooltip-privacy-mode": "Masks the USD values of your account",
"tooltip-private-account": "Your account won't show on leaderboards and it won't be viewable via URL",
"tooltip-spot-open-orders": "The number of spot markets you can have open orders in. The maximum for new accounts is {{max}}",
"tooltip-token-accounts": "The number of tokens you can hold in your account. The maximum for new accounts 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",

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