Merge branch 'main' into chinese-localization
This commit is contained in:
commit
f266bfbec4
91
README.md
91
README.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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
|
||||
${
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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])
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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],
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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..." />
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }) => (
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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')} />
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
${
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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'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}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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')} />
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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')}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}{' '}
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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%">
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() ? (
|
||||
<>
|
||||
|
|
|
@ -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) => (
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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', '']
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
|
12
package.json
12
package.json
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 |
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue