diff --git a/README.md b/README.md index 12468a37..cfeeb4dc 100644 --- a/README.md +++ b/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. diff --git a/apis/datafeed.ts b/apis/datafeed.ts index ea0923e2..1f3bf501 100644 --- a/apis/datafeed.ts +++ b/apis/datafeed.ts @@ -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 => { 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 } diff --git a/apis/mngo/datafeed.ts b/apis/mngo/datafeed.ts index 417572b6..a559319c 100644 --- a/apis/mngo/datafeed.ts +++ b/apis/mngo/datafeed.ts @@ -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 diff --git a/components/MangoProvider.tsx b/components/MangoProvider.tsx index d01eb07c..d100f965 100644 --- a/components/MangoProvider.tsx +++ b/components/MangoProvider.tsx @@ -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) diff --git a/components/RepayForm.tsx b/components/RepayForm.tsx index c9e8e746..0ed5f8e3 100644 --- a/components/RepayForm.tsx +++ b/components/RepayForm.tsx @@ -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) { +
+
+

{t('enable-notifications')}

+

{t('asked-sign-transaction')}

+
+ setSignToNotifications(checked)} + /> +
{maxSolDeposit <= 0 ? ( ) : null} + ) } diff --git a/components/account/HealthContributionsChart.tsx b/components/account/HealthContributionsChart.tsx index c82c2925..951b21c3 100644 --- a/components/account/HealthContributionsChart.tsx +++ b/components/account/HealthContributionsChart.tsx @@ -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) diff --git a/components/account/HideMangoAccount.tsx b/components/account/HideMangoAccount.tsx new file mode 100644 index 00000000..6b44b79d --- /dev/null +++ b/components/account/HideMangoAccount.tsx @@ -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 ( + <> +
+ +

{t('settings:private-account')}

+
+ {signingForHide ? ( + + ) : ( + + )} +
+ + ) +} + +export default HideMangoAccount diff --git a/components/account/MarketsHealthTable.tsx b/components/account/MarketsHealthTable.tsx index 7c2bc213..644e16ca 100644 --- a/components/account/MarketsHealthTable.tsx +++ b/components/account/MarketsHealthTable.tsx @@ -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 ? ( diff --git a/components/account/TokensHealthTable.tsx b/components/account/TokensHealthTable.tsx index 07a227cc..825d00c9 100644 --- a/components/account/TokensHealthTable.tsx +++ b/components/account/TokensHealthTable.tsx @@ -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 ? ( diff --git a/components/forms/ButtonGroup.tsx b/components/forms/ButtonGroup.tsx index 444b4d17..fe7a79d1 100644 --- a/components/forms/ButtonGroup.tsx +++ b/components/forms/ButtonGroup.tsx @@ -22,11 +22,11 @@ const ButtonGroup = ({ large, }: ButtonGroupProps) => { return ( -
+
{activeValue && values.includes(activeValue) ? (
v === activeValue) * 100 @@ -37,7 +37,7 @@ const ButtonGroup = ({ ) : null} {values.map((v, i) => (
- + @@ -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 (
DateNFTNFT Offer Buy Now Price Buyer -
+
+
+

+ {x.asset.json?.name || 'Unknown'} +

+

+ {x.asset.json?.collection?.family || 'Unknown'} +

+
@@ -178,14 +240,28 @@ const AllBidsView = () => { ) } colorClass="fgd-3" - text="Accept Offer" + text={ + accepting === + x.asset.mint.address.toString() ? ( + + ) : ( + 'Accept Offer' + ) + } /> ) : ( <> {publicKey && x.buyerAddress.equals(publicKey) ? ( + ) : ( + 'Cancel Offer' + ) + } onClick={() => cancelBid(x)} /> ) : listing ? ( @@ -198,7 +274,14 @@ const AllBidsView = () => { {listing ? ( + ) : ( + 'Buy Now' + ) + } onClick={() => buyAsset(listing)} /> ) : null} @@ -211,6 +294,14 @@ const AllBidsView = () => { })}
+ ) : loading ? ( +
+ {[...Array(4)].map((x, i) => ( + +
+ + ))} +
) : ( )} diff --git a/components/nftMarket/AssetBidsModal.tsx b/components/nftMarket/AssetBidsModal.tsx index 56237dbe..25844a3a 100644 --- a/components/nftMarket/AssetBidsModal.tsx +++ b/components/nftMarket/AssetBidsModal.tsx @@ -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 ( -
- {assetBids?.map((x) => ( -

-

{x.createdAt.toNumber()}
-
{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}
-
- -
-

- ))} +
+

Offers

+
+ {assetBids && assetBids.length ? ( + assetBids.map((x) => ( +
+
+

+ {dayjs(x.createdAt.toNumber() * 1000).format( + 'DD MMM YY h:mma', + )} +

+ + {toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}{' '} + MNGO + +
+ + ) : ( + t('accept') + ) + } + colorClass="error" + onClick={() => acceptBid(x)} + /> +
+ )) + ) : ( + + )} +
) diff --git a/components/nftMarket/BidNftModal.tsx b/components/nftMarket/BidNftModal.tsx index d868befb..1136e45a 100644 --- a/components/nftMarket/BidNftModal.tsx +++ b/components/nftMarket/BidNftModal.tsx @@ -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 (

Make an Offer

{listing ? ( -
- - - - Buy Now:{' '} - - {toUiDecimals( - listing.price.basisPoints.toNumber(), - MANGO_MINT_DECIMALS, - )}{' '} - MNGO - - - +
+ {listing.asset?.json?.image ? ( + + ) : null} +

+ {listing.asset?.json?.name || 'Unknown'} +

+

+ {listing.asset?.json?.collection?.family || 'Unknown'} +

) : ( <> @@ -94,7 +124,7 @@ const BidNftModal = ({ isOpen, onClose, listing }: ListingModalProps) => { /> )} -
+
{ {submittingOffer ? : 'Make Offer'}
+ {listing ? ( + buying ? ( +
+ +
+ ) : ( + handleBuyNow(listing)}> + + Buy Now:{' '} + + {toUiDecimals( + listing.price.basisPoints.toNumber(), + MANGO_MINT_DECIMALS, + )}{' '} + MNGO + + + + ) + ) : null}
) diff --git a/components/nftMarket/ListingsView.tsx b/components/nftMarket/ListingsView.tsx index e928eb1e..c7794d60 100644 --- a/components/nftMarket/ListingsView.tsx +++ b/components/nftMarket/ListingsView.tsx @@ -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( + 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 ( -
+

{`Filter Results`}

- {asssetBidsModal && assetBidsListing ? ( - - ) : null}
- {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 = () => {
) : null}
-
-
-

Buy Now

-
- {/* */} - - {formatNumericValue( - toUiDecimals( - x.price.basisPoints.toNumber(), - MANGO_MINT_DECIMALS, - ), - )}{' '} - MNGO - -
-
+

+ {x.asset.json?.name || x.asset.name || 'Unknown'} +

+

+ {x.asset.json?.collection?.family || 'Unknown'} +

+
+ + {formatNumericValue( + toUiDecimals( + x.price.basisPoints.toNumber(), + MANGO_MINT_DECIMALS, + ), + )}{' '} + MNGO +
-

+

{bestBid ? `Best Offer: ${bestBid} MNGO` : 'No offers'}

@@ -202,13 +278,6 @@ const ListingsView = () => { colorClass="fgd-3" onClick={() => openBidModal(x)} /> - {bidNftModal && bidListing && ( - - )}
)} {publicKey && x.sellerAddress.equals(publicKey) && ( @@ -257,6 +326,20 @@ const ListingsView = () => { onPageChange={handlePageClick} />
*/} + {asssetBidsModal && assetBidsListing ? ( + + ) : null} + {bidNftModal && bidListing ? ( + + ) : null}
) } diff --git a/components/nftMarket/MyBidsModal.tsx b/components/nftMarket/MyBidsModal.tsx index 4ae487ce..895c9ac0 100644 --- a/components/nftMarket/MyBidsModal.tsx +++ b/components/nftMarket/MyBidsModal.tsx @@ -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 (

Your Offers

- {bids?.map((x) => ( -
-
- -
-

- {dayjs(x.createdAt.toNumber()).format('DD MMM YY h:mma')} -

- - {toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)} MNGO - + {bids && bids.length ? ( + bids + .sort((a, b) => b.createdAt.toNumber() - a.createdAt.toNumber()) + .map((x) => ( +
+
+ {x.asset?.json?.image ? ( + + ) : null} +
+

+ {x.asset?.json?.name || 'Unknown'} +

+

+ {x.asset?.json?.collection?.family || 'Unknown'} +

+ + {toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}{' '} + MNGO + +
+
+ + ) : ( + t('cancel') + ) + } + colorClass="error" + onClick={() => cancelBid(x)} + />
-
- cancelBid(x)} - /> -
- ))} + )) + ) : ( + + )}
) diff --git a/components/nftMarket/SellNftModal.tsx b/components/nftMarket/SellNftModal.tsx index ce8feb2a..bcc95d39 100644 --- a/components/nftMarket/SellNftModal.tsx +++ b/components/nftMarket/SellNftModal.tsx @@ -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) diff --git a/components/notifications/NotificationsDrawer.tsx b/components/notifications/NotificationsDrawer.tsx index 3d4ef9c3..a601c8c1 100644 --- a/components/notifications/NotificationsDrawer.tsx +++ b/components/notifications/NotificationsDrawer.tsx @@ -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} >

{t('notifications')}

@@ -268,13 +222,22 @@ const NotificationsDrawer = ({

{t('unauth-title')}

-

{t('unauth-desc')}

+

{t('unauth-desc')}

+ + createLedgerMessage(wallet, setCookie, connection) + } + > + {t('sign-using-ledger')} +
)} diff --git a/components/rewards/Leaderboards.tsx b/components/rewards/Leaderboards.tsx index 90b4585c..b82c77fb 100644 --- a/components/rewards/Leaderboards.tsx +++ b/components/rewards/Leaderboards.tsx @@ -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 (
: null}

diff --git a/components/search/SearchPage.tsx b/components/search/SearchPage.tsx index 5e894110..a0560f2a 100644 --- a/components/search/SearchPage.tsx +++ b/components/search/SearchPage.tsx @@ -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) diff --git a/components/settings/AccountSettings.tsx b/components/settings/AccountSettings.tsx index d20fd082..c339935e 100644 --- a/components/settings/AccountSettings.tsx +++ b/components/settings/AccountSettings.tsx @@ -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 ? ( <> -

-

{t('account')}

+

{t('account')}

+
+ +
+
+

{t('settings:account-size')}

{!isAccountFull ? ( { {t('settings:increase-account-size')} - ) : ( -
- -

- {t('settings:error-account-size-full')} -

-
- )} + ) : null}
{({ open }) => ( diff --git a/components/settings/DisplaySettings.tsx b/components/settings/DisplaySettings.tsx index 99e0db6e..f31747ce 100644 --- a/components/settings/DisplaySettings.tsx +++ b/components/settings/DisplaySettings.tsx @@ -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)} />
+
+ +

{t('settings:privacy-mode')}

+
+ setPrivacyMode(!privacyMode)} + /> +
) } diff --git a/components/settings/NotificationSettings.tsx b/components/settings/NotificationSettings.tsx index 3e285e8d..2b717651 100644 --- a/components/settings/NotificationSettings.tsx +++ b/components/settings/NotificationSettings.tsx @@ -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 = () => {

{t('notifications:unauth-desc')}

- + + createLedgerMessage(wallet, setCookie, connection) + } + > + {t('notifications:sign-using-ledger')} +
) : ( diff --git a/components/settings/RpcSettings.tsx b/components/settings/RpcSettings.tsx index 97a62805..a5709d57 100644 --- a/components/settings/RpcSettings.tsx +++ b/components/settings/RpcSettings.tsx @@ -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 = () => { handlePriorityFee(v)} - values={PRIORITY_FEES.map((val) => val.label)} + values={PRIORITY_FEE_LEVELS.map((val) => val.label)} /> {/* {showCustomForm ? (
diff --git a/components/settings/SettingsPage.tsx b/components/settings/SettingsPage.tsx index c4055d47..0c4f3088 100644 --- a/components/settings/SettingsPage.tsx +++ b/components/settings/SettingsPage.tsx @@ -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 (
@@ -32,7 +30,7 @@ const SettingsPage = () => {
- {!isMobile ? ( + {isDesktop ? (
diff --git a/components/shared/BalancesTable.tsx b/components/shared/BalancesTable.tsx index 32b554cb..cefc5a79 100644 --- a/components/shared/BalancesTable.tsx +++ b/components/shared/BalancesTable.tsx @@ -241,6 +241,7 @@ const BalancesTable = () => { {({ open }) => ( <> { 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 (

- {!isUnownedAccount && !isMobile ? ( + {!isUnownedAccount && isDesktop ? ( asPath.includes('/trade') && isBaseOrQuote ? ( { + const [privacyMode] = useLocalStorageState(PRIVACY_MODE) return ( {isUsd - ? formatCurrencyValue(value, decimals) + ? privacyMode + ? '****' + : formatCurrencyValue(value, decimals) : formatNumericValue(value, decimals, roundUp)} ) diff --git a/components/shared/Modal.tsx b/components/shared/Modal.tsx index 963e630f..1aa1da82 100644 --- a/components/shared/Modal.tsx +++ b/components/shared/Modal.tsx @@ -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' diff --git a/components/shared/PnlTooltipContent.tsx b/components/shared/PnlTooltipContent.tsx index 8be57523..d18c7b8d 100644 --- a/components/shared/PnlTooltipContent.tsx +++ b/components/shared/PnlTooltipContent.tsx @@ -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)}

-
+

{t('trade:total-pnl')}

{formatCurrencyValue(totalPnl, 2)}
-
-

{t('trade:return-on-equity')}

- - - % - -
-
+

{t('trade:unsettled')} {t('pnl')}

diff --git a/components/shared/TabButtons.tsx b/components/shared/TabButtons.tsx index ec9de4b6..24dbf75a 100644 --- a/components/shared/TabButtons.tsx +++ b/components/shared/TabButtons.tsx @@ -25,7 +25,7 @@ const TabButtons = ({ {values.map(([label, count], i) => (
{loading ? ( -
+
) : ( diff --git a/components/swap/LimitSwapForm.tsx b/components/swap/LimitSwapForm.tsx index bf6dfdba..ec1ce844 100644 --- a/components/swap/LimitSwapForm.tsx +++ b/components/swap/LimitSwapForm.tsx @@ -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> + showTokenSelect: SwapFormTokenListType + setShowTokenSelect: Dispatch> } type LimitSwapForm = { amountIn: number - hasBorrows: number | undefined triggerPrice: string } type FormErrors = Partial> -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(null) const [submitting, setSubmitting] = useState(false) const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider') const [formErrors, setFormErrors] = useState({}) + 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 ( <> - + + {t('swap:trigger-beta')} + +
  • + Trigger orders on long-tail assets could be susceptible to + oracle manipulation. +
  • +
  • + Trigger orders rely on a sufficient amount of well + collateralized liquidators. +
  • +
  • + The slippage on existing orders could be higher/lower than + what's estimated. +
  • +
  • + The amount of tokens used to fill your order can vary and + depends on the final execution price. +
  • + + } + > + + {t('swap:important-info')} + +
    +
    + } + type="info" + /> +
    + handleTokenSelect('input')} + setShowTokenSelect={() => handleTokenSelect('reduce-input')} handleMax={handleMax} isTriggerOrder /> +
    + {swapFormSizeUi === 'slider' ? ( + handleAmountInUi(v)} + step={1 / 10 ** (inputBankDecimals || 6)} + maxAmount={useAbsInputPosition} + /> + ) : ( +
    + handleAmountInUi(v)} + useMargin={false} + /> +
    + )} +
    @@ -760,7 +789,8 @@ const LimitSwapForm = ({ }`} > {triggerPriceDifference - ? triggerPriceDifference.toFixed(2) + ? (triggerPriceDifference > 0 ? '+' : '') + + triggerPriceDifference.toFixed(2) : '0.00'} %

    @@ -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 = ({
    ) : null}
    -
    - -
    - handleTokenSelect('output')} - handleRepay={ - orderType === OrderTypes.REPAY_BORROW ? handleRepay : undefined - } + setShowTokenSelect={() => handleTokenSelect('reduce-output')} /> - {swapFormSizeUi === 'slider' ? ( - handleAmountInUi(v)} - step={1 / 10 ** (inputBankDecimals || 6)} - /> - ) : ( - handleAmountInUi(v)} - useMargin={false} - /> - )} - {orderType === OrderTypes.REPAY_BORROW && - !hasBorrowToRepay ? null : orderDescription ? ( + {orderDescription ? (
    - - {orderType !== OrderTypes.REPAY_BORROW ? ( - <> - {t('sell')}{' '} - - ) : null} - {orderDescription} - - } - type="info" - /> +
    ) : null} {ipAllowed ? ( )} - {showDepositModal ? ( - setShowDepositModal(false)} - token={freeCollateral > 0 ? inputBankName : ''} - /> + {borrowExceedsLimitInPeriod && + remainingBorrowsInPeriod && + timeToNextPeriod ? ( +
    + +
    ) : null} ) diff --git a/components/swap/MarketSwapForm.tsx b/components/swap/MarketSwapForm.tsx index 7e01d96a..50255b8f 100644 --- a/components/swap/MarketSwapForm.tsx +++ b/components/swap/MarketSwapForm.tsx @@ -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> + setShowTokenSelect: Dispatch> } const MAX_DIGITS = 11 @@ -243,6 +250,25 @@ const MarketSwapForm = ({ setShowTokenSelect }: MarketSwapFormProps) => { setShowTokenSelect={setShowTokenSelect} handleMax={handleMax} /> +
    + {swapFormSizeUi === 'slider' ? ( + setAmountInFormValue(v, true)} + step={1 / 10 ** (inputBank?.mintDecimals || 6)} + maxAmount={useTokenMax} + /> + ) : ( +
    + setAmountInFormValue(v, true)} + useMargin={useMargin} + /> +
    + )} +
    ) : null} + {borrowExceedsLimitInPeriod && + remainingBorrowsInPeriod && + timeToNextPeriod ? ( +
    + +
    + ) : null} {selectedRoute === null && amountIn.gt(0) ? (
    diff --git a/components/swap/MaxSwapAmount.tsx b/components/swap/MaxSwapAmount.tsx index ebf06675..484a4505 100644 --- a/components/swap/MaxSwapAmount.tsx +++ b/components/swap/MaxSwapAmount.tsx @@ -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 diff --git a/components/swap/ReduceInputTokenInput.tsx b/components/swap/ReduceInputTokenInput.tsx new file mode 100644 index 00000000..ba698d00 --- /dev/null +++ b/components/swap/ReduceInputTokenInput.tsx @@ -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> + 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 ( +
    +
    +

    {t('swap:reduce-position')}

    + {!isUnownedAccount ? ( + handleMax(v)} + maxAmount={useAbsInputPosition} + /> + ) : null} +
    +
    + +
    +
    + + {!isNaN(Number(amountInFormValue)) ? ( + + {inputBank + ? formatCurrencyValue( + inputBank.uiPrice * Number(amountInFormValue), + ) + : '–'} + + ) : null} +
    + {mangoAccountAddress && freeCollateral <= 0 ? ( +
    + +
    + ) : null} + {error ? ( +
    + +
    + ) : null} +
    + ) +} + +export default ReduceInputTokenInput diff --git a/components/swap/ReduceOutputTokenInput.tsx b/components/swap/ReduceOutputTokenInput.tsx new file mode 100644 index 00000000..5ead75e2 --- /dev/null +++ b/components/swap/ReduceOutputTokenInput.tsx @@ -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> +}) => { + 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 ( +
    +

    + {reducingLong ? t('buy') : t('sell')} +

    +
    + +
    +
    + {loading ? ( +
    + +
    + ) : ( + <> + + {!isNaN(Number(amountOutFormValue)) ? ( + + {outputBank + ? formatCurrencyValue( + outputBank.uiPrice * Number(amountOutFormValue), + ) + : '–'} + + ) : null} + + )} +
    + {error ? ( +
    + +
    + ) : null} +
    + ) +} + +export default ReduceOutputTokenInput diff --git a/components/swap/SellTokenInput.tsx b/components/swap/SellTokenInput.tsx index 11d093dd..abd4517a 100644 --- a/components/swap/SellTokenInput.tsx +++ b/components/swap/SellTokenInput.tsx @@ -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> + setShowTokenSelect: Dispatch> 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 ( -
    +

    {t('sell')}

    {!isUnownedAccount ? ( handleMax(v)} + maxAmount={useTokenMax} /> ) : null}
    @@ -96,7 +99,7 @@ const SellTokenInput = ({ ) : null}
    - {connected && freeCollateral <= 0 ? ( + {mangoAccountAddress && freeCollateral <= 0 ? (
    { const { t } = useTranslation(['common', 'swap', 'trade']) - const { data: isWhiteListed } = useIsWhiteListed() - const [showTokenSelect, setShowTokenSelect] = useState<'input' | 'output'>() + const [showTokenSelect, setShowTokenSelect] = + useState() const [showSettings, setShowSettings] = useState(false) - const [swapOrLimit, setSwapOrLimit] = useState('swap') const [, setSavedSwapMargin] = useLocalStorageState( 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 = () => { setShowTokenSelect(undefined)} onTokenSelect={ - showTokenSelect === 'input' + showTokenSelect === 'input' || showTokenSelect === 'reduce-input' ? handleTokenInSelect : handleTokenOutSelect } type={showTokenSelect} - useMargin={useMargin} + useMargin={swapOrTrigger === 'swap' ? useMargin : false} /> { setShowSettings(false)} />
    - {isWhiteListed ? ( -
    - handleSwapOrLimit(v)} - /> -
    - ) : null} - {swapOrLimit === 'swap' ? ( +
    + handleSwapOrTrigger(v)} + /> +
    + {swapOrTrigger === 'swap' ? ( ) : ( {
    - {swapOrLimit === 'swap' ? ( + {swapOrTrigger === 'swap' ? ( <>
    diff --git a/components/swap/SwapFormTokenList.tsx b/components/swap/SwapFormTokenList.tsx index e00a2d3e..b49ae237 100644 --- a/components/swap/SwapFormTokenList.tsx +++ b/components/swap/SwapFormTokenList.tsx @@ -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 = ({

    {bank?.name ? formatTokenSymbol(bank.name) : symbol || 'unknown'} + {type === 'reduce-input' && token.amount ? ( + + {t(`trade:${token.amount.gt(0) ? 'long' : 'short'}`)} + + ) : null} {isReduceOnly ? ( - + {t('reduce-only')} ) : null} @@ -92,7 +109,7 @@ const TokenItem = ({

    - {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(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 ( <> -

    - {type === 'input' - ? t('swap:you-sell') - : type === 'output' - ? t('swap:you-buy') - : ''} -

    +

    {listTitle}

    {t('token')}

    - {type === 'input' ? ( + {!type?.includes('output') ? (

    {t('max')}

    ) : null}
    diff --git a/components/swap/SwapInfoTabs.tsx b/components/swap/SwapInfoTabs.tsx index be225155..9c1565e5 100644 --- a/components/swap/SwapInfoTabs.tsx +++ b/components/swap/SwapInfoTabs.tsx @@ -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 (
    - setSelectedTab(tab)} - values={tabsWithCount} - showBorders - /> +
    + setSelectedTab(tab)} + values={tabsWithCount} + showBorders + /> +
    {selectedTab === 'balances' ? : null} - {selectedTab === 'trade:trigger-orders' ? : null} + {selectedTab === 'trade:trigger-orders' ? : null} {selectedTab === 'swap:swap-history' ? : null}
    ) diff --git a/components/swap/SwapSlider.tsx b/components/swap/SwapSlider.tsx index bfb41afd..2aaf7329 100644 --- a/components/swap/SwapSlider.tsx +++ b/components/swap/SwapSlider.tsx @@ -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 ( <> diff --git a/components/swap/SwapOrders.tsx b/components/swap/SwapTriggerOrders.tsx similarity index 87% rename from components/swap/SwapOrders.tsx rename to components/swap/SwapTriggerOrders.tsx index 23524116..0b19ba91 100644 --- a/components/swap/SwapOrders.tsx +++ b/components/swap/SwapTriggerOrders.tsx @@ -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 ( {pair} @@ -292,7 +314,7 @@ const SwapOrders = () => { {size} {' '} - {bank.name} + {formattedBaseName}

    @@ -301,7 +323,7 @@ const SwapOrders = () => { {filled}/{size} {' '} - {bank.name} + {formattedBaseName}

    @@ -310,16 +332,19 @@ const SwapOrders = () => { {currentPrice} {' '} - {buyBank.name} + {formattedQuoteName}

    + + {triggerDirection}{' '} + {triggerPrice} {' '} - {side === 'buy' ? sellBank.name : buyBank.name} + {formattedQuoteName}

    @@ -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 ( {({ open }) => ( @@ -379,7 +412,7 @@ const SwapOrders = () => { {size} {' '} - {bank.name} + {formattedBaseName} {' at '} @@ -387,7 +420,7 @@ const SwapOrders = () => { {triggerPrice} {' '} - {side === 'buy' ? sellBank.name : buyBank.name} + {formattedQuoteName}

    @@ -413,7 +446,7 @@ const SwapOrders = () => { {size} {' '} - {bank.name} + {formattedBaseName}

    @@ -425,7 +458,7 @@ const SwapOrders = () => { {filled}/{size} {' '} - {bank.name} + {formattedBaseName}

    @@ -437,7 +470,7 @@ const SwapOrders = () => { {currentPrice} {' '} - {buyBank.name} + {formattedQuoteName}

    @@ -446,10 +479,15 @@ const SwapOrders = () => { {t('trade:trigger-price')}

    + + {triggerDirection}{' '} + {triggerPrice} {' '} - {side === 'buy' ? sellBank.name : buyBank.name} + {side === 'buy' + ? formattedSellTokenName + : formattedBuyTokenName}

    diff --git a/components/swap/TokenSelect.tsx b/components/swap/TokenSelect.tsx index 55a08852..d7f7c2e3 100644 --- a/components/swap/TokenSelect.tsx +++ b/components/swap/TokenSelect.tsx @@ -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> - type: 'input' | 'output' + showTokenList: Dispatch> + 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 ? (
    -
    - {formatTokenSymbol(bank!.name)} +
    +
    + {formatTokenSymbol(bank.name)} +
    + {posType ? ( + + {t(`trade:${posType}`)} + + ) : null}
    - ) + ) : null } export default TokenSelect diff --git a/components/swap/useTokenMax.tsx b/components/swap/useTokenMax.tsx index 53c5558a..f4e63fa4 100644 --- a/components/swap/useTokenMax.tsx +++ b/components/swap/useTokenMax.tsx @@ -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, + } +} diff --git a/components/token/TokenPage.tsx b/components/token/TokenPage.tsx index 416d54be..72ad3217 100644 --- a/components/token/TokenPage.tsx +++ b/components/token/TokenPage.tsx @@ -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 = () => {
    - + {coingeckoTokenInfo ? (

    {coingeckoTokenInfo.name}{' '} diff --git a/components/trade/AdvancedMarketHeader.tsx b/components/trade/AdvancedMarketHeader.tsx index 9c02adee..d87a4cec 100644 --- a/components/trade/AdvancedMarketHeader.tsx +++ b/components/trade/AdvancedMarketHeader.tsx @@ -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 = ({ )}

    - setShowMarketDetails(true)} diff --git a/components/trade/AdvancedTradeForm.tsx b/components/trade/AdvancedTradeForm.tsx index c3adc147..09e74f44 100644 --- a/components/trade/AdvancedTradeForm.tsx +++ b/components/trade/AdvancedTradeForm.tsx @@ -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 (
    @@ -795,10 +835,22 @@ const AdvancedTradeForm = () => { )}
    - + {borrowExceedsLimitInPeriod && + remainingBorrowsInPeriod && + timeToNextPeriod ? ( +
    + +
    + ) : null} + )}
    diff --git a/components/trade/CloseAllPositionsModal.tsx b/components/trade/CloseAllPositionsModal.tsx new file mode 100644 index 00000000..e60e7abd --- /dev/null +++ b/components/trade/CloseAllPositionsModal.tsx @@ -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 = ({ + 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 ( + +

    {t('trade:close-all-positions')}

    +
    {t('trade:price-expect')}
    +
    + {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 ( +
    +
    + +

    {market.name}

    + +
    +

    + + | + +

    +
    + ) + })} +
    + + + {t('cancel')} + +
    + ) +} + +export default CloseAllPositionsModal diff --git a/components/trade/DepthChart.tsx b/components/trade/DepthChart.tsx index 6ed5bbf2..a12603e8 100644 --- a/components/trade/DepthChart.tsx +++ b/components/trade/DepthChart.tsx @@ -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 = () => {
    diff --git a/components/trade/MarketCloseModal.tsx b/components/trade/MarketCloseModal.tsx index a15c140e..aa71071e 100644 --- a/components/trade/MarketCloseModal.tsx +++ b/components/trade/MarketCloseModal.tsx @@ -124,15 +124,15 @@ const MarketCloseModal: FunctionComponent = ({ }, [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 } diff --git a/components/trade/OpenOrders.tsx b/components/trade/OpenOrders.tsx index f0648a5f..dc19d597 100644 --- a/components/trade/OpenOrders.tsx +++ b/components/trade/OpenOrders.tsx @@ -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 = () => { - - - - + + + + + {!isUnownedAccount ? ( - @@ -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" > - {modifyOrderId !== o.orderId.toString() ? ( <> - - - )} - + {!isUnownedAccount ? ( - - {!isUnownedAccount ? + {!isUnownedAccount ? ( + + ) : null} @@ -262,7 +281,7 @@ const PerpPositions = () => { )} + {!isUnownedAccount ? ( + {!isUnownedAccount ? ( @@ -220,6 +246,13 @@ const TradeHistory = () => {
    {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 (
    {
    {'taker' in trade ? ( - - - - - + !hiddenAccounts?.includes(counterpartyAddress) ? ( + + + + + + + + ) : ( + + + + + + ) ) : null} diff --git a/components/trade/TradeHotKeys.tsx b/components/trade/TradeHotKeys.tsx index dae4c4a7..e0b6aac3 100644 --- a/components/trade/TradeHotKeys.tsx +++ b/components/trade/TradeHotKeys.tsx @@ -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 ? ( - k.keySequence).toString()} - onKeyDown={onKeyDown} - > - {children} - - ) : ( - <>{children} - ) + return <>{children} } export default TradeHotKeys diff --git a/components/trade/TradeInfoTabs.tsx b/components/trade/TradeInfoTabs.tsx index 41168ea8..aa989e32 100644 --- a/components/trade/TradeInfoTabs.tsx +++ b/components/trade/TradeInfoTabs.tsx @@ -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 (
    -
    - setSelectedTab(tab)} - values={tabsWithCount} - showBorders - fillWidth={isMobile} +
    +
    + setSelectedTab(tab)} + values={tabsWithCount} + showBorders + fillWidth={fillTabWidth} + /> +
    +
    - {selectedTab === 'balances' ? : null} - {selectedTab === 'trade:orders' ? : null} - {selectedTab === 'trade:unsettled' ? ( - - ) : null} - {selectedTab === 'trade:positions' ? : null} - {selectedTab === 'trade-history' ? : null} +
    ) } export default TradeInfoTabs + +const TabContent = ({ selectedTab }: { selectedTab: string }) => { + const unsettledSpotBalances = useUnsettledSpotBalances() + const unsettledPerpPositions = useUnsettledPerpPositions() + switch (selectedTab) { + case 'balances': + return + case 'trade:orders': + return + case 'trade:unsettled': + return ( + + ) + case 'trade:positions': + return + case 'trade-history': + return + default: + return + } +} diff --git a/components/trade/TradeSummary.tsx b/components/trade/TradeSummary.tsx index b869ddcb..fd740bd6 100644 --- a/components/trade/TradeSummary.tsx +++ b/components/trade/TradeSummary.tsx @@ -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 diff --git a/components/trade/TradingViewChart.tsx b/components/trade/TradingViewChart.tsx index 45854537..2599227b 100644 --- a/components/trade/TradingViewChart.tsx +++ b/components/trade/TradingViewChart.tsx @@ -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( @@ -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( diff --git a/components/wallet/ConnectedMenu.tsx b/components/wallet/ConnectedMenu.tsx index 5255808f..118c6730 100644 --- a/components/wallet/ConnectedMenu.tsx +++ b/components/wallet/ConnectedMenu.tsx @@ -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 = () => {
    { ) : ( )} - {!loadProfileDetails && !isMobile ? ( + {!loadProfileDetails && isDesktop ? (

    {wallet?.adapter.name} @@ -123,7 +121,7 @@ const ConnectedMenu = () => { {t('profile:edit-profile-pic')}

    - {isMobile ? ( + {isTablet ? (
    {table.data.map((rowData, index: number) => { return ( - + {Object.values(rowData).map( (val, idx: number) => { return (
    {t('market')}{t('trade:size')}{t('price')}{t('value')}{t('market')}{t('trade:size')}{t('price')}{t('trade:filled')}{t('value')} + ) : null}
    + + + { ) : ( <> - + { } /> + { + + {fetchingFilledOrders ? ( +
    + +
    + +
    + ) : ( + + )} +
    {expiryTimestamp ? (
    {`Expires ${new Date( @@ -355,7 +392,7 @@ const OpenOrders = () => { ) : null}
    +
    {modifyOrderId !== o.orderId.toString() ? ( <> diff --git a/components/trade/Orderbook.tsx b/components/trade/Orderbook.tsx index 81d091c1..5945307f 100644 --- a/components/trade/Orderbook.tsx +++ b/components/trade/Orderbook.tsx @@ -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(null) const currentOrderbookData = useRef() 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 = () => { ) })}
    -
    -
    {t('trade:spread')}
    -
    +
    +

    {t('trade:spread')}

    +
    +
    + {orderbookData?.spreadPercentage.toFixed(2)}% -
    +
    - {orderbookData?.spread - ? orderbookData.spread < SHOW_EXPONENTIAL_THRESHOLD - ? orderbookData.spread.toExponential() - : formatNumericValue( - orderbookData.spread, - market ? getDecimalCount(market.tickSize) : undefined, - ) - : null} + + {orderbookData?.spread + ? orderbookData.spread < SHOW_EXPONENTIAL_THRESHOLD + ? orderbookData.spread.toExponential() + : formatNumericValue( + orderbookData.spread, + market ? getDecimalCount(market.tickSize) : undefined, + ) + : null} +
    {depthArray.map((_x, index) => ( diff --git a/components/trade/PerpFundingRate.tsx b/components/trade/PerpFundingRate.tsx index 53ef126d..53037c8e 100644 --- a/components/trade/PerpFundingRate.tsx +++ b/components/trade/PerpFundingRate.tsx @@ -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 ( <>
    @@ -138,20 +68,12 @@ const PerpFundingRate = () => {
    {typeof fundingRate === 'number' ? (
    - The 1hr rate as an APR is{' '} + The annualized funding rate is{' '} {formatFunding.format(fundingRate * 8760)}
    ) : null} - {instantaneousRate ? ( -
    - The latest instantaneous rate is{' '} - - {instantaneousRate}% - -
    - ) : null} { const { t } = useTranslation(['common', 'trade']) const { group } = useMangoGroup() const [showMarketCloseModal, setShowMarketCloseModal] = useState(false) + const [showCloseAllModal, setShowCloseAllModal] = useState(false) const [positionToClose, setPositionToClose] = useState( null, ) @@ -147,7 +150,23 @@ const PerpPositions = () => {
    {t('trade:unrealized-pnl')} : null} + ROE + {openPerpPositions?.length > 1 ? ( +
    +
    + + setShowCloseAllModal(true)} + > + {t('trade:close-all')} + +
    +
    + ) : null} +
    -
    +
    { realizedPnl={realizedPnl} totalPnl={totalPnl} unsettledPnl={unsettledPnl} - roe={roe} /> } delay={100} @@ -291,6 +309,13 @@ const PerpPositions = () => {
    + = 0 ? 'text-th-up' : 'text-th-down'} + > + % + +
    @@ -300,7 +325,7 @@ const PerpPositions = () => { size="small" onClick={() => showClosePositionModal(position)} > - Close + {t('close')} { realizedPnl={totalPnlStats.realized} totalPnl={totalPnlStats.total} unsettledPnl={totalPnlStats.unsettled} - roe={totalPnlStats.roe} /> } delay={100} @@ -368,6 +392,19 @@ const PerpPositions = () => {
    + = 0 ? 'text-th-up' : 'text-th-down' + } + > + + % + + {' '} @@ -581,7 +618,6 @@ const PerpPositions = () => { realizedPnl={realizedPnl} totalPnl={totalPnl} unsettledPnl={unsettledPnl} - roe={roe} /> } delay={100} @@ -613,14 +649,26 @@ const PerpPositions = () => {
    + {openPerpPositions?.length > 1 ? ( + + ) : null}
    ) @@ -734,6 +754,12 @@ const PerpPositions = () => { position={positionToClose} /> ) : null} + {showCloseAllModal ? ( + setShowCloseAllModal(false)} + /> + ) : null} ) } diff --git a/components/trade/SpotMarketOrderSwapForm.tsx b/components/trade/SpotMarketOrderSwapForm.tsx index fc421d66..6ae25f94 100644 --- a/components/trade/SpotMarketOrderSwapForm.tsx +++ b/components/trade/SpotMarketOrderSwapForm.tsx @@ -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() 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) => { 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() {
    handleSubmit(e)}>
    {!isUnownedAccount ? ( - ) : null}
    @@ -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} />
    ) : ( @@ -497,7 +464,7 @@ export default function SpotMarketOrderSwapForm() {
    -
    +
    {ipAllowed ? (
    {'taker' in trade ? (
    - - - - + + + + + + + ) : ( + + + - - + + )}
    ) : null}
    diff --git a/pages/nft/index.tsx b/pages/nft/index.tsx index 189bed56..5f3c24bc 100644 --- a/pages/nft/index.tsx +++ b/pages/nft/index.tsx @@ -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() diff --git a/pages/trade.tsx b/pages/trade.tsx index 5fba68df..9f1b6a60 100644 --- a/pages/trade.tsx +++ b/pages/trade.tsx @@ -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 ( -
    +
    ) diff --git a/public/icons/usdh.svg b/public/icons/usdh.svg new file mode 100644 index 00000000..5b4d18f2 --- /dev/null +++ b/public/icons/usdh.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 86eb4165..fba480fe 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -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", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 8b2d576d..3678e28c 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/en/notifications.json b/public/locales/en/notifications.json index 2632a51a..ff554347 100644 --- a/public/locales/en/notifications.json +++ b/public/locales/en/notifications.json @@ -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" } \ No newline at end of file diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 4108d933..31c83d2d 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -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", diff --git a/public/locales/en/swap.json b/public/locales/en/swap.json index d6fb99dd..1d24b72a 100644 --- a/public/locales/en/swap.json +++ b/public/locales/en/swap.json @@ -10,6 +10,7 @@ "health-impact": "Health Impact", "hide-fees": "Hide Fees", "hide-swap-history": "Hide Swap History", + "important-info": "Important Info", "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", @@ -29,6 +30,9 @@ "price-impact": "Price Impact", "rate": "Rate", "received": "Received", + "reduce-position": "Reduce Position", + "reduce-position-buy": "Reduce long by buying", + "reduce-position-sell": "Reduce short by selling", "review-swap": "Review Swap", "route-info": "Route Info", "show-fees": "Show Fees", @@ -45,6 +49,7 @@ "tooltip-max-slippage": "If price slips beyond your maximum slippage your swap will not be executed", "tooltip-favorite-swap-add": "Add pair to favorites", "tooltip-favorite-swap-remove": "Remove pair from favorites", + "trigger-beta": "Trigger orders are in beta. Use with caution.", "use-margin": "Allow Margin", "warning-no-collateral": "You have no free collateral", "you-buy": "You're buying", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 5f6f74db..035f63ad 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -11,6 +11,8 @@ "cancel-all": "Cancel All", "cancel-order": "Cancel Order", "cancel-order-error": "Failed to cancel order", + "close-all": "Close All", + "close-all-positions": "Close All Positions", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", "connect-orders": "Connect to view your open orders", @@ -23,7 +25,7 @@ "edit-order": "Edit Order", "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", - "falls-to": "falls to", + "falls-to": "falls below", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -79,7 +81,7 @@ "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "return-on-equity": "Return on Equity", - "rises-to": "rises to", + "rises-to": "rises above", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -104,19 +106,22 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", + "tooltip-private-counterparty": "Counterparty has Private Account enabled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "tooltip-volume-alert": "Volume Alert Settings", "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", + "tooltip-view-counterparty": "View counterparty {{pk}}", + "tooltip-volume-alert": "Volume Alert Settings", "total-pnl": "Total PnL", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", - "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You'll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade", "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" -} \ No newline at end of file +} diff --git a/public/locales/es/account.json b/public/locales/es/account.json index 86eb4165..fba480fe 100644 --- a/public/locales/es/account.json +++ b/public/locales/es/account.json @@ -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", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 8b2d576d..3678e28c 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/es/notifications.json b/public/locales/es/notifications.json index 2632a51a..ff554347 100644 --- a/public/locales/es/notifications.json +++ b/public/locales/es/notifications.json @@ -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" } \ No newline at end of file diff --git a/public/locales/es/settings.json b/public/locales/es/settings.json index 4108d933..7110b664 100644 --- a/public/locales/es/settings.json +++ b/public/locales/es/settings.json @@ -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", + "privacy-mode": "Privacy Mode", + "private-account": "Private Account", "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", diff --git a/public/locales/es/swap.json b/public/locales/es/swap.json index 7d48918f..7cfc59cc 100644 --- a/public/locales/es/swap.json +++ b/public/locales/es/swap.json @@ -10,6 +10,7 @@ "health-impact": "Health Impact", "hide-fees": "Hide Fees", "hide-swap-history": "Hide Swap History", + "important-info": "Important Info", "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", @@ -29,6 +30,9 @@ "price-impact": "Price Impact", "rate": "Rate", "received": "Received", + "reduce-position": "Reduce Position", + "reduce-position-buy": "Reduce long by buying", + "reduce-position-sell": "Reduce short by selling", "review-swap": "Review Swap", "route-info": "Route Info", "show-fees": "Show Fees", @@ -43,6 +47,7 @@ "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-margin": "When margin is enabled you can add leverage to your swaps. Borrows are opened automatically if a swap exceeds your balance.", "tooltip-max-slippage": "If price slips beyond your maximum slippage your swap will not be executed", + "trigger-beta": "Trigger orders are in beta. Use with caution.", "use-margin": "Allow Margin", "warning-no-collateral": "You have no free collateral", "you-buy": "You're buying", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 5f6f74db..ac9eb0c6 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -11,6 +11,8 @@ "cancel-all": "Cancel All", "cancel-order": "Cancel Order", "cancel-order-error": "Failed to cancel order", + "close-all": "Close All", + "close-all-positions": "Close All Positions", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", "connect-orders": "Connect to view your open orders", @@ -23,7 +25,7 @@ "edit-order": "Edit Order", "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", - "falls-to": "falls to", + "falls-to": "falls below", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -79,7 +81,7 @@ "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "return-on-equity": "Return on Equity", - "rises-to": "rises to", + "rises-to": "rises above", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -104,19 +106,23 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", + "tooltip-private-counterparty": "Counterparty has Private Account enabled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "tooltip-volume-alert": "Volume Alert Settings", "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", + "tooltip-view-counterparty": "View counterparty {{pk}}", + "tooltip-volume-alert": "Volume Alert Settings", "total-pnl": "Total PnL", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You'll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade", "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", + "view-counterparty": "View counterparty {{pk}}", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/public/locales/ru/account.json b/public/locales/ru/account.json index 86eb4165..fba480fe 100644 --- a/public/locales/ru/account.json +++ b/public/locales/ru/account.json @@ -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", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 8b2d576d..3678e28c 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -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" } \ No newline at end of file diff --git a/public/locales/ru/notifications.json b/public/locales/ru/notifications.json index 2632a51a..ff554347 100644 --- a/public/locales/ru/notifications.json +++ b/public/locales/ru/notifications.json @@ -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" } \ No newline at end of file diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index 4108d933..7110b664 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -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", + "privacy-mode": "Privacy Mode", + "private-account": "Private Account", "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", diff --git a/public/locales/ru/swap.json b/public/locales/ru/swap.json index 7d48918f..7cfc59cc 100644 --- a/public/locales/ru/swap.json +++ b/public/locales/ru/swap.json @@ -10,6 +10,7 @@ "health-impact": "Health Impact", "hide-fees": "Hide Fees", "hide-swap-history": "Hide Swap History", + "important-info": "Important Info", "input-reduce-only-warning": "{{symbol}} is in reduce only mode. You can swap your balance to another token", "insufficient-balance": "Insufficient {{symbol}} Balance", "insufficient-collateral": "Insufficient Collateral", @@ -29,6 +30,9 @@ "price-impact": "Price Impact", "rate": "Rate", "received": "Received", + "reduce-position": "Reduce Position", + "reduce-position-buy": "Reduce long by buying", + "reduce-position-sell": "Reduce short by selling", "review-swap": "Review Swap", "route-info": "Route Info", "show-fees": "Show Fees", @@ -43,6 +47,7 @@ "tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this swap. The current {{token}} variable borrow rate is {{rate}}%", "tooltip-margin": "When margin is enabled you can add leverage to your swaps. Borrows are opened automatically if a swap exceeds your balance.", "tooltip-max-slippage": "If price slips beyond your maximum slippage your swap will not be executed", + "trigger-beta": "Trigger orders are in beta. Use with caution.", "use-margin": "Allow Margin", "warning-no-collateral": "You have no free collateral", "you-buy": "You're buying", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 5f6f74db..ac9eb0c6 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -11,6 +11,8 @@ "cancel-all": "Cancel All", "cancel-order": "Cancel Order", "cancel-order-error": "Failed to cancel order", + "close-all": "Close All", + "close-all-positions": "Close All Positions", "close-confirm": "Market close your {{config_name}} position", "close-position": "Close Position", "connect-orders": "Connect to view your open orders", @@ -23,7 +25,7 @@ "edit-order": "Edit Order", "est-liq-price": "Est. Liq. Price", "est-slippage": "Est. Slippage", - "falls-to": "falls to", + "falls-to": "falls below", "filled": "Filled", "for": "for", "funding-limits": "Funding Limits", @@ -79,7 +81,7 @@ "repay-borrow-order-desc": "Repay {{amount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "repay-borrow-deposit-order-desc": "Repay {{borrowAmount}} and buy {{depositAmount}} {{symbol}} if the oracle price reaches {{triggerPrice}} {{priceUnit}}", "return-on-equity": "Return on Equity", - "rises-to": "rises to", + "rises-to": "rises above", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -104,19 +106,23 @@ "tooltip-ioc": "Immediate-Or-Cancel (IOC) orders are guaranteed to be the taker and must be executed immediately. Any portion of the order that can't be filled immediately will be cancelled", "tooltip-insured": "Whether or not {{tokenOrMarket}} losses can be recovered from the insurance fund in the event of bankruptcies.", "tooltip-post": "Post orders are guaranteed to be the maker or they will be canceled", + "tooltip-private-counterparty": "Counterparty has Private Account enabled", "tooltip-slippage": "An estimate of the difference between the current price and the price your trade will be executed at", - "tooltip-volume-alert": "Volume Alert Settings", "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", + "tooltip-view-counterparty": "View counterparty {{pk}}", + "tooltip-volume-alert": "Volume Alert Settings", "total-pnl": "Total PnL", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", "trigger-price": "Trigger Price", "trigger-order": "Trigger Order", "trigger-order-desc": "{{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}", + "trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You'll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade", "trigger-orders": "Trigger Orders", "tweet-position": "Tweet", "unrealized-pnl": "Unrealized PnL", "unsettled": "Unsettled", + "view-counterparty": "View counterparty {{pk}}", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" } \ No newline at end of file diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index f7b7ba84..e6887df1 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -19,6 +19,7 @@ "all": "全部", "amount": "数量", "amount-owed": "欠款", + "asked-sign-transaction": "You'll be asked to sign a transaction", "asset-liability-weight": "资产/债务权重", "asset-liability-weight-desc": "资产权重在账户健康计算中对质押品价值进行扣减。资产权重越低,资产对质押品的影响越小。债务权重恰恰相反(在健康计算中增加债务价值)。", "asset-weight": "资产权重", @@ -75,6 +76,8 @@ "edit": "编辑", "edit-account": "编辑帐户标签", "edit-profile-image": "切换头像", + "enable-notifications": "Enable Notifications", + "error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}", "error-token-positions-full": "你帐户的币种位置已占满", "explorer": "浏览器", "fee": "费用", @@ -192,5 +195,8 @@ "list-market-token": "上架市场/币种", "vote": "投票", "yes": "是", - "you": "你" -} \ No newline at end of file + "you": "你", + "using-ledger": "Using Ledger", + "sign-to-in-app-notifications": "Sign to in app notifications" + } + diff --git a/public/locales/zh/notifications.json b/public/locales/zh/notifications.json index f960c1a8..6574b4c7 100644 --- a/public/locales/zh/notifications.json +++ b/public/locales/zh/notifications.json @@ -4,6 +4,7 @@ "empty-state-title": "这里没什么", "notifications": "通知", "sign-message": "签署讯息", + "sign-using-ledger": "Sign with Ledger", "unauth-desc": "连接钱包而受到通知", "unauth-title": "通知收件匣" } \ No newline at end of file diff --git a/public/locales/zh/settings.json b/public/locales/zh/settings.json index a1f605a5..c2582271 100644 --- a/public/locales/zh/settings.json +++ b/public/locales/zh/settings.json @@ -68,6 +68,10 @@ "perp-positions": "合约持仓", "placing-order": "正在下单...", "preferred-explorer": "首选探索器", + "privacy-disable": "Disable Privacy Mode", + "privacy-enable": "Enable Privacy Mode", + "privacy-mode": "Privacy Mode", + "private-account": "Private Account", "recent-trades": "最近交易", "rpc": "RPC", "rpc-provider": "RPC提供者", @@ -91,10 +95,12 @@ "tooltip-hot-key-percentage-size": "将大小设置为最大杠杆的百分比。", "tooltip-hot-key-price": "将价格设置为预言机价格变化的百分比。", "tooltip-orderbook-bandwidth-saving": "使用链下服务进行挂单簿更新,将数据使用量减少约 1000 倍。如果未结订单在挂单簿中不正确显示,请禁用此选项。", - "tooltip-perp-open-orders": "你可以有未结订单的合约市场数量。最多是{{max}}", - "tooltip-perp-positions": "你可以有持仓的合约市场数量。最多是{{max}}", - "tooltip-spot-open-orders": "你可以有未结订单的现货市场数量。最多是{{max}}", - "tooltip-token-accounts": "你帐户可以持有的币种数量。最多是{{max}}", + "tooltip-perp-open-orders": "你可以有未结订单的合约市场数量。新帐户的最多为{{max}}", + "tooltip-perp-positions": "你可以有持仓的合约市场数量。新帐户的最多为{{max}}", + "tooltip-spot-open-orders": "你可以有未结订单的现货市场数量。新帐户的最多为{{max}}", + "tooltip-token-accounts": "你帐户可以持有的币种数量。新帐户的最多为{{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", "top-left": "左上", "top-right": "右上", "trade-chart": "交易图表", diff --git a/public/locales/zh/swap.json b/public/locales/zh/swap.json index ec9648a6..b4edaa58 100644 --- a/public/locales/zh/swap.json +++ b/public/locales/zh/swap.json @@ -10,6 +10,7 @@ "health-impact": "健康影响", "hide-fees": "隐藏费用", "hide-swap-history": "隐藏换币纪录", + "important-info": "Important Info", "input-reduce-only-warning": "{{symbol}}处于仅减少模式。您可以将余额换成另一个币种", "insufficient-balance": "{{symbol}}余额不够", "insufficient-collateral": "质押品不够", @@ -31,6 +32,9 @@ "rate": "汇率", "receive": "你将获取", "received": "获取", + "reduce-position": "Reduce Position", + "reduce-position-buy": "Reduce long by buying", + "reduce-position-sell": "Reduce short by selling", "review-swap": "检视换币", "route-info": "路线细节", "show-fees": "显示费用", @@ -45,6 +49,7 @@ "tooltip-borrow-no-balance": "您将借入 {{borrowAmount}} {{token}} 来执行此交换。当前的 {{token}} 可变借贷利率为 {{rate}}%", "tooltip-margin": "启用保证金时将能杠杆换币。如果换币数量超过您的余额将会自动借款。", "tooltip-max-slippage": "如果价格下滑超过您的最大滑点,换币交易将不会被执行", + "trigger-beta": "Trigger orders are in beta. Use with caution.", "use-margin": "允许杠杆", "warning-no-collateral": "你没有可用质押品", "you-buy": "你正在买", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index 310b05ae..ab8166ba 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -11,6 +11,8 @@ "cancel-all": "取消全部", "cancel-order": "取消订单", "cancel-order-error": "取消订单失败", + "close-all": "Close All", + "close-all-positions": "Close All Positions", "close-confirm": "市场平仓 {{config_name}}", "close-position": "平仓", "connect-orders": "连接以查看未结订单", @@ -23,7 +25,7 @@ "edit-order": "编辑订单", "est-liq-price": "预计清算价格", "est-slippage": "预计下滑", - "falls-to": "下降到", + "falls-to": "下降至", "filled": "成交", "for": "为", "funding-limits": "资金费限制", @@ -79,7 +81,7 @@ "repay-borrow-order-desc": "若预言机价格达到 {{triggerPrice}} {{priceUnit}}, 归还 {{amount}} {{symbol}}", "repay-borrow-deposit-order-desc": "若预言机价格达到 {{triggerPrice}} {{priceUnit}},归还 {{borrowAmount}} 以及买 {{depositAmount}} {{symbol}}", "return-on-equity": "股本回报率", - "rises-to": "上涨到", + "rises-to": "上涨至", "sells": "卖单", "settle-funds": "借清资金", "settle-funds-error": "借清出错", @@ -87,6 +89,7 @@ "show-asks": "显示要价", "show-bids": "显示出价", "side": "方向", + "trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You'll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade", "size": "数量", "spread": "差价", "stable-price": "稳定价格", @@ -104,8 +107,10 @@ "tooltip-insured": "如果发生破产,{{tokenOrMarket}}损失是否可以从保险基金中归还", "tooltip-ioc": "IOC交易若不吃单就会被取消。任何无法立刻成交的部分将被取消", "tooltip-post": "Post交易若不挂单就会被取消。", + "tooltip-private-counterparty": "Counterparty has Private Account enabled", "tooltip-slippage": "当前价格与您的交易将执行的价格之间的差值的估计", "tooltip-stable-price": "稳定价格用于一个安全机制。此机制可以限制用户在预言机价格快速波动时下风险高的订单", + "tooltip-view-counterparty": "View counterparty {{pk}}", "tooltip-volume-alert": "交易量警报设定", "total-pnl": "总盈亏", "trade-sounds-tooltip": "为每笔新交易播放警报声音", @@ -117,6 +122,7 @@ "tweet-position": "分享至Twitter", "unrealized-pnl": "未实现盈亏", "unsettled": "未结清", + "view-counterparty": "View counterparty {{pk}}", "volume-alert": "交易量警报", "volume-alert-desc": "交易量超过警报设定时播放声音" } \ No newline at end of file diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index 052335f4..8e04c0c7 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -19,6 +19,7 @@ "all": "全部", "amount": "數量", "amount-owed": "欠款", + "asked-sign-transaction": "You'll be asked to sign a transaction", "asset-liability-weight": "資產/債務權重", "asset-liability-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。債務權重恰恰相反(在健康計算中增加債務價值)。", "asset-weight": "資產權重", @@ -75,6 +76,8 @@ "edit": "編輯", "edit-account": "編輯帳戶標籤", "edit-profile-image": "切換頭像", + "enable-notifications": "Enable Notifications", + "error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}", "error-token-positions-full": "你帳戶的幣種位置已占滿", "explorer": "瀏覽器", "fee": "費用", @@ -192,5 +195,8 @@ "list-market-token": "上架市場/幣種", "vote": "投票", "yes": "是", - "you": "你" -} \ No newline at end of file + "you": "你", + "using-ledger": "Using Ledger", + "sign-to-in-app-notifications": "Sign to in app notifications" + "using-ledger": "Using Ledger" +} diff --git a/public/locales/zh_tw/notifications.json b/public/locales/zh_tw/notifications.json index 22a1b1ec..68639226 100644 --- a/public/locales/zh_tw/notifications.json +++ b/public/locales/zh_tw/notifications.json @@ -4,6 +4,7 @@ "empty-state-title": "這裡沒什麼", "notifications": "通知", "sign-message": "簽署訊息", + "sign-using-ledger": "Sign with Ledger", "unauth-desc": "連接錢包而受到通知", "unauth-title": "通知收件匣" } \ No newline at end of file diff --git a/public/locales/zh_tw/settings.json b/public/locales/zh_tw/settings.json index 3ee0e2ef..7cac849d 100644 --- a/public/locales/zh_tw/settings.json +++ b/public/locales/zh_tw/settings.json @@ -68,6 +68,10 @@ "perp-positions": "合約持倉", "placing-order": "正在下單...", "preferred-explorer": "首選探索器", + "privacy-disable": "Disable Privacy Mode", + "privacy-enable": "Enable Privacy Mode", + "privacy-mode": "Privacy Mode", + "private-account": "Private Account", "recent-trades": "最近交易", "rpc": "RPC", "rpc-provider": "RPC提供者", @@ -91,10 +95,12 @@ "tooltip-hot-key-percentage-size": "將大小設置為最大槓桿的百分比。", "tooltip-hot-key-price": "將價格設置為預言機價格變化的百分比。", "tooltip-orderbook-bandwidth-saving": "使用鏈下服務進行掛單簿更新,將數據使用量減少約 1000 倍。如果未結訂單在掛單簿中不正確顯示,請禁用此選項。", - "tooltip-perp-open-orders": "你可以有未結訂單的合約市場數量。最多是{{max}}", - "tooltip-perp-positions": "你可以有持倉的合約市場數量。最多是{{max}}", - "tooltip-spot-open-orders": "你可以有未結訂單的現貨市場數量。最多是{{max}}", - "tooltip-token-accounts": "你帳戶可以持有的幣種數量。最多是{{max}}", + "tooltip-perp-open-orders": "你可以有未結訂單的合約市場數量。新帳戶的最多為{{max}}", + "tooltip-perp-positions": "你可以有持倉的合約市場數量。新帳戶的最多為{{max}}", + "tooltip-spot-open-orders": "你可以有未結訂單的現貨市場數量。新帳戶的最多為{{max}}", + "tooltip-token-accounts": "你帳戶可以持有的幣種數量。新帳戶的最多為{{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", "top-left": "左上", "top-right": "右上", "trade-chart": "交易圖表", diff --git a/public/locales/zh_tw/swap.json b/public/locales/zh_tw/swap.json index 03bfd355..59cbbe4a 100644 --- a/public/locales/zh_tw/swap.json +++ b/public/locales/zh_tw/swap.json @@ -10,6 +10,7 @@ "health-impact": "健康影響", "hide-fees": "隱藏費用", "hide-swap-history": "隱藏換幣紀錄", + "important-info": "Important Info", "input-reduce-only-warning": "{{symbol}}處於僅減少模式。您可以將餘額換成另一個幣種", "insufficient-balance": "{{symbol}}餘額不夠", "insufficient-collateral": "質押品不夠", @@ -31,6 +32,9 @@ "rate": "匯率", "receive": "你將獲取", "received": "獲取", + "reduce-position": "Reduce Position", + "reduce-position-buy": "Reduce long by buying", + "reduce-position-sell": "Reduce short by selling", "review-swap": "檢視換幣", "route-info": "路線細節", "show-fees": "顯示費用", @@ -45,6 +49,7 @@ "tooltip-borrow-no-balance": "您將借入 {{borrowAmount}} {{token}} 來執行此交換。當前的 {{token}} 可變借貸利率為 {{rate}}%", "tooltip-margin": "啟用保證金時將能槓桿換幣。如果換幣數量超過您的餘額將會自動借款。", "tooltip-max-slippage": "如果價格下滑超過您的最大滑點,換幣交易將不會被執行", + "trigger-beta": "Trigger orders are in beta. Use with caution.", "use-margin": "允許槓桿", "warning-no-collateral": "你沒有可用質押品", "you-buy": "你正在買", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 961b8861..f8054cb3 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -11,6 +11,8 @@ "cancel-all": "取消全部", "cancel-order": "取消訂單", "cancel-order-error": "取消訂單失敗", + "close-all": "Close All", + "close-all-positions": "Close All Positions", "close-confirm": "市場平倉 {{config_name}}", "close-position": "平倉", "connect-orders": "連接以查看未結訂單", @@ -23,7 +25,7 @@ "edit-order": "編輯訂單", "est-liq-price": "預計清算價格", "est-slippage": "預計下滑", - "falls-to": "下降到", + "falls-to": "下降至", "filled": "成交", "for": "為", "funding-limits": "資金費限制", @@ -79,7 +81,7 @@ "repay-borrow-order-desc": "若預言機價格達到 {{triggerPrice}} {{priceUnit}}, 歸還 {{amount}} {{symbol}}", "repay-borrow-deposit-order-desc": "若預言機價格達到 {{triggerPrice}} {{priceUnit}},歸還 {{borrowAmount}} 以及買 {{depositAmount}} {{symbol}}", "return-on-equity": "股本回報率", - "rises-to": "上漲到", + "rises-to": "上漲至", "sells": "賣單", "settle-funds": "借清資金", "settle-funds-error": "借清出錯", @@ -104,8 +106,10 @@ "tooltip-insured": "如果發生破產,{{tokenOrMarket}}損失是否可以從保險基金中歸還", "tooltip-ioc": "IOC交易若不吃單就會被取消。任何無法立刻成交的部分將被取消", "tooltip-post": "Post交易若不掛單就會被取消。", + "tooltip-private-counterparty": "Counterparty has Private Account enabled", "tooltip-slippage": "當前價格與您的交易將執行的價格之間的差值的估計", "tooltip-stable-price": "穩定價格用於一個安全機制。此機制可以限制用戶在預言機價格快速波動時下風險高的訂單", + "tooltip-view-counterparty": "View counterparty {{pk}}", "tooltip-volume-alert": "交易量警報設定", "total-pnl": "總盈虧", "trade-sounds-tooltip": "為每筆新交易播放警報聲音", @@ -114,9 +118,11 @@ "trigger-order": "觸發訂單", "trigger-order-desc": "{{amount}} {{symbol}} 若預言機價格{{orderType}} {{triggerPrice}} {{priceUnit}}", "trigger-orders": "觸發訂單", + "trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You'll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade", "tweet-position": "分享至Twitter", "unrealized-pnl": "未實現盈虧", "unsettled": "未結清", + "view-counterparty": "View counterparty {{pk}}", "volume-alert": "交易量警報", "volume-alert-desc": "交易量超過警報設定時播放聲音" } \ No newline at end of file diff --git a/store/mangoStore.ts b/store/mangoStore.ts index f32f5ef9..8a59c864 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -3,7 +3,14 @@ import produce from 'immer' import create from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' import { AnchorProvider, BN, Wallet, web3 } from '@coral-xyz/anchor' -import { ConfirmOptions, Connection, Keypair, PublicKey } from '@solana/web3.js' +import { + ConfirmOptions, + Connection, + Keypair, + PublicKey, + RecentPrioritizationFees, + TransactionInstruction, +} from '@solana/web3.js' import { OpenOrders, Order } from '@project-serum/serum/lib/market' import { Orderbook } from '@project-serum/serum' import { Wallet as WalletAdapter } from '@solana/wallet-adapter-react' @@ -36,9 +43,9 @@ import { INPUT_TOKEN_DEFAULT, LAST_ACCOUNT_KEY, MANGO_DATA_API_URL, + MAX_PRIORITY_FEE_KEYS, OUTPUT_TOKEN_DEFAULT, PAGINATION_PAGE_LENGTH, - PRIORITY_FEE_KEY, RPC_PROVIDER_KEY, SWAP_MARGIN_KEY, } from '../utils/constants' @@ -62,6 +69,7 @@ import { ThemeData, PositionStat, OrderbookTooltip, + TriggerOrderTypes, } from 'types' import spotBalancesUpdater from './spotBalancesUpdater' import { PerpMarket } from '@blockworks-foundation/mango-v4/' @@ -75,6 +83,10 @@ import { IOrderLineAdapter, } from '@public/charting_library/charting_library' import { nftThemeMeta } from 'utils/theme' +import maxBy from 'lodash/maxBy' +import mapValues from 'lodash/mapValues' +import groupBy from 'lodash/groupBy' +import sampleSize from 'lodash/sampleSize' const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX') @@ -104,11 +116,19 @@ export const emptyWallet = new EmptyWallet(Keypair.generate()) const initMangoClient = ( provider: AnchorProvider, - opts = { prioritizationFee: DEFAULT_PRIORITY_FEE.value }, + opts: { + prioritizationFee: number + prependedGlobalAdditionalInstructions: TransactionInstruction[] + } = { + prioritizationFee: DEFAULT_PRIORITY_FEE, + prependedGlobalAdditionalInstructions: [], + }, ): MangoClient => { return MangoClient.connect(provider, CLUSTER, MANGO_V4_ID[CLUSTER], { prioritizationFee: opts.prioritizationFee, - idsSource: 'get-program-accounts', + prependedGlobalAdditionalInstructions: + opts.prependedGlobalAdditionalInstructions, + idsSource: 'api', postSendTxCallback: ({ txid }: { txid: string }) => { notify({ title: 'Transaction sent', @@ -120,7 +140,6 @@ const initMangoClient = ( }) } -let mangoGroupRetryAttempt = 0 export const DEFAULT_TRADE_FORM: TradeForm = { side: 'buy', price: undefined, @@ -184,6 +203,8 @@ export type MangoStore = { details: ProfileDetails | null loadDetails: boolean } + prependedGlobalAdditionalInstructions: TransactionInstruction[] + priorityFee: number selectedMarket: { name: string | undefined current: Serum3Market | PerpMarket | undefined @@ -220,6 +241,8 @@ export type MangoStore = { amountIn: string amountOut: string flipPrices: boolean + swapOrTrigger: TriggerOrderTypes + triggerPrice: string } set: (x: (x: MangoStore) => void) => void themeData: ThemeData @@ -241,6 +264,10 @@ export type MangoStore = { loading: boolean } } + window: { + width: number + height: number + } actions: { fetchAccountInterestTotals: (mangoAccountPk: string) => Promise fetchActivityFeed: ( @@ -269,6 +296,10 @@ export type MangoStore = { connectMangoClientWithWallet: (wallet: WalletAdapter) => Promise loadMarketFills: () => Promise updateConnection: (url: string) => void + setPrependedGlobalAdditionalInstructions: ( + instructions: TransactionInstruction[], + ) => void + estimatePriorityFee: (feeMultiplier: number) => Promise } } @@ -342,6 +373,8 @@ const mangoStore = create()( loadDetails: false, details: { profile_name: '', trader_category: '', wallet_pk: '' }, }, + priorityFee: DEFAULT_PRIORITY_FEE, + prependedGlobalAdditionalInstructions: [], selectedMarket: { name: 'SOL/USDC', current: undefined, @@ -387,6 +420,8 @@ const mangoStore = create()( amountIn: '', amountOut: '', flipPrices: false, + swapOrTrigger: 'swap', + triggerPrice: '', }, themeData: nftThemeMeta.default, tokenStats: { @@ -407,6 +442,10 @@ const mangoStore = create()( loading: false, }, }, + window: { + width: 0, + height: 0, + }, actions: { fetchAccountInterestTotals: async (mangoAccountPk: string) => { const set = get().set @@ -560,15 +599,9 @@ const mangoStore = create()( ) } }) - mangoGroupRetryAttempt = 0 } catch (e) { - if (mangoGroupRetryAttempt < 2) { - // get().actions.fetchGroup() - mangoGroupRetryAttempt++ - } else { - notify({ type: 'info', title: 'Unable to refresh data' }) - console.error('Error fetching group', e) - } + notify({ type: 'info', title: 'Unable to refresh data' }) + console.error('Error fetching group', e) } }, reloadMangoAccount: async (confirmationSlot) => { @@ -693,10 +726,7 @@ const mangoStore = create()( state.wallet.nfts.data = nfts }) } catch (error) { - notify({ - type: 'error', - title: 'Unable to fetch nfts', - }) + console.warn('Error: unable to fetch nfts.', error) } finally { set((state) => { state.wallet.nfts.loading = false @@ -1004,11 +1034,13 @@ const mangoStore = create()( options, ) provider.opts.skipPreflight = true - const prioritizationFee = Number( - localStorage.getItem(PRIORITY_FEE_KEY) ?? - DEFAULT_PRIORITY_FEE.value, - ) - const client = initMangoClient(provider, { prioritizationFee }) + const priorityFee = get().priorityFee ?? DEFAULT_PRIORITY_FEE + + const client = initMangoClient(provider, { + prioritizationFee: priorityFee, + prependedGlobalAdditionalInstructions: + get().prependedGlobalAdditionalInstructions, + }) set((s) => { s.client = client @@ -1023,6 +1055,25 @@ const mangoStore = create()( } } }, + async setPrependedGlobalAdditionalInstructions( + instructions: TransactionInstruction[], + ) { + const set = get().set + const client = mangoStore.getState().client + + const provider = client.program.provider as AnchorProvider + provider.opts.skipPreflight = true + + const newClient = initMangoClient(provider, { + prioritizationFee: get().priorityFee, + prependedGlobalAdditionalInstructions: instructions, + }) + + set((s) => { + s.client = newClient + s.prependedGlobalAdditionalInstructions = instructions + }) + }, async fetchProfileDetails(walletPk: string) { const set = get().set set((state) => { @@ -1118,12 +1169,70 @@ const mangoStore = create()( options, ) newProvider.opts.skipPreflight = true - const newClient = initMangoClient(newProvider) + const newClient = initMangoClient(newProvider, { + prependedGlobalAdditionalInstructions: + get().prependedGlobalAdditionalInstructions, + prioritizationFee: DEFAULT_PRIORITY_FEE, + }) set((state) => { state.connection = newConnection state.client = newClient }) }, + estimatePriorityFee: async (feeMultiplier) => { + const set = get().set + const group = mangoStore.getState().group + const client = mangoStore.getState().client + + const mangoAccount = get().mangoAccount.current + if (!mangoAccount || !group || !client) return + + const altResponse = await connection.getAddressLookupTable( + group.addressLookupTables[0], + ) + const altKeys = altResponse.value?.state.addresses + if (!altKeys) return + + const addresses = sampleSize(altKeys, MAX_PRIORITY_FEE_KEYS) + const fees = await connection.getRecentPrioritizationFees({ + lockedWritableAccounts: addresses, + }) + + if (fees.length < 1) return + + // get max priority fee per slot (and sort by slot from old to new) + const maxFeeBySlot = mapValues(groupBy(fees, 'slot'), (items) => + maxBy(items, 'prioritizationFee'), + ) + const maximumFees = Object.values(maxFeeBySlot).sort( + (a, b) => a!.slot - b!.slot, + ) as RecentPrioritizationFees[] + + // get median of last 20 fees + const recentFees = maximumFees.slice( + Math.max(maximumFees.length - 20, 0), + ) + const mid = Math.floor(recentFees.length / 2) + const medianFee = + recentFees.length % 2 !== 0 + ? recentFees[mid].prioritizationFee + : (recentFees[mid - 1].prioritizationFee + + recentFees[mid].prioritizationFee) / + 2 + const feeEstimate = Math.ceil(medianFee * feeMultiplier) + + const provider = client.program.provider as AnchorProvider + provider.opts.skipPreflight = true + const newClient = initMangoClient(provider, { + prioritizationFee: feeEstimate, + prependedGlobalAdditionalInstructions: + get().prependedGlobalAdditionalInstructions, + }) + set((state) => { + state.priorityFee = feeEstimate + state.client = newClient + }) + }, }, } }), diff --git a/types/index.ts b/types/index.ts index 2a865366..b514ed0b 100644 --- a/types/index.ts +++ b/types/index.ts @@ -7,6 +7,7 @@ import { Serum3Market, } from '@blockworks-foundation/mango-v4' import { Modify } from '@blockworks-foundation/mango-v4' +import { JsonMetadata } from '@metaplex-foundation/js' import { Event } from '@project-serum/serum/lib/queue' import { PublicKey } from '@solana/web3.js' import { formatTradeHistory } from 'hooks/useTradeHistory' @@ -305,6 +306,7 @@ export interface NFT { name: string mint: string tokenAccount: string + json: JsonMetadata | null } export interface PerpStatsItem { @@ -486,3 +488,15 @@ export interface ContributionDetails { perpMarketContributions: PerpMarketContribution[] spotUi: number } + +export interface FilledOrdersApiResponseType { + fills: FilledOrder[] +} + +export interface FilledOrder { + order_id: string + order_type: 'spot' | 'perp' + quantity: number +} + +export type TriggerOrderTypes = 'swap' | 'trade:trigger-order' diff --git a/utils/account.ts b/utils/account.ts index bc97d80c..48a015b2 100644 --- a/utils/account.ts +++ b/utils/account.ts @@ -2,6 +2,7 @@ import { AccountPerformanceData, AccountVolumeTotalData, EmptyObject, + FilledOrdersApiResponseType, FormattedHourlyAccountVolumeData, HourlyAccountVolumeData, PerformanceDataItem, @@ -150,3 +151,20 @@ export const fetchHourlyVolume = async (mangoAccountPk: string) => { console.log('Failed to fetch spot volume', e) } } + +export const fetchFilledOrders = async ( + mangoAccountPk: string, + orderIds: string[], +) => { + try { + const idString = orderIds.map((i) => `&id=${i}`).join('') + const resp = await fetch( + `https://api.mngo.cloud/data/v4/user-data/filled-orders?mango-account=${mangoAccountPk}` + + idString, + ) + const response: FilledOrdersApiResponseType = await resp.json() + return response + } catch (e) { + console.log('Failed to fetch filled orders', e) + } +} diff --git a/utils/constants.ts b/utils/constants.ts index eec66bcb..02c867d7 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -49,9 +49,9 @@ export const FAVORITE_SWAPS_KEY = 'favoriteSwaps-0.1' export const THEME_KEY = 'theme-0.1' -export const RPC_PROVIDER_KEY = 'rpcProviderKey-0.7' +export const RPC_PROVIDER_KEY = 'rpcProviderKey-0.9' -export const PRIORITY_FEE_KEY = 'priorityFeeKey-0.1' +export const PRIORITY_FEE_KEY = 'priorityFeeKey-0.2' export const SHOW_ORDER_LINES_KEY = 'showOrderLines-0.1' @@ -73,6 +73,8 @@ export const AUTO_CONNECT_WALLET = 'auto-connect-0.1' export const LAST_WALLET_NAME = 'lastWalletName' +export const PRIVACY_MODE = 'privacy-mode-0.1' + // Unused export const PROFILE_CATEGORIES = [ 'borrower', @@ -96,6 +98,8 @@ export const DEFAULT_MARKET_NAME = 'SOL/USDC' export const MIN_SOL_BALANCE = 0.001 +export const MAX_PRIORITY_FEE_KEYS = 128 + export const ACCOUNT_ACTION_MODAL_HEIGHT = '462px' export const ACCOUNT_ACTION_MODAL_INNER_HEIGHT = '400px' @@ -141,6 +145,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = { sol: true, stsol: true, usdc: true, + usdh: true, usdt: true, wbtcpo: true, 'wbtc (portal)': true, @@ -156,3 +161,12 @@ export const DEFAULT_FAVORITE_MKTS = [ export const WHITE_LIST_API = 'https://api.mngo.cloud/whitelist/v1/' export const DAILY_SECONDS = 86400 export const DAILY_MILLISECONDS = 86400000 + +// max slot numbers for mango account +export const MAX_ACCOUNTS = { + tokenAccounts: '8', + spotOpenOrders: '4', + perpAccounts: '4', + perpOpenOrders: '64', + tcsOrders: '64', +} diff --git a/utils/governance/listingTools.ts b/utils/governance/listingTools.ts index 832a1c90..ed656aaa 100644 --- a/utils/governance/listingTools.ts +++ b/utils/governance/listingTools.ts @@ -297,8 +297,14 @@ export const formatSuggestedValues = ( suggestedParams.netBorrowLimitPerWindowQuote, 6, ), - borrowWeightScale: toUiDecimals(suggestedParams.borrowWeightScale, 6), - depositWeightScale: toUiDecimals(suggestedParams.depositWeightScale, 6), + borrowWeightScale: toUiDecimals( + suggestedParams.borrowWeightScaleStartQuote, + 6, + ), + depositWeightScale: toUiDecimals( + suggestedParams.depositWeightScaleStartQuote, + 6, + ), } } diff --git a/utils/notifications.ts b/utils/notifications.ts index ac2a086c..480fd177 100644 --- a/utils/notifications.ts +++ b/utils/notifications.ts @@ -1,7 +1,16 @@ import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' import mangoStore from '@store/mangoStore' import { Howl } from 'howler' -import { SOUND_SETTINGS_KEY } from './constants' +import { NOTIFICATION_API, SOUND_SETTINGS_KEY } from './constants' +import { Payload, SIWS } from '@web3auth/sign-in-with-solana' +import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes' +import { WalletContextState } from '@solana/wallet-adapter-react' +import { + PublicKey, + Connection, + Transaction, + TransactionInstruction, +} from '@solana/web3.js' export type TransactionNotification = { type: 'success' | 'info' | 'error' | 'confirm' @@ -66,3 +75,117 @@ export function notify(newNotification: { state.transactionNotifications = [...notifications, newNotif] }) } + +const MEMO_PROGRAM_ID = new PublicKey( + 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', +) + +const PAYLOAD_STATEMENT = 'Login to Mango Notifications' +const PAYLOAD_VERSION = '1' +const PAYLOAD_CHAIN_ID = 1 + +export const createSolanaMessage = async ( + wallet: WalletContextState, + setCookie: (wallet: string, token: string) => void, +) => { + const payload = new Payload() + payload.domain = window.location.host + payload.address = wallet.publicKey!.toBase58() + payload.uri = window.location.origin + payload.statement = PAYLOAD_STATEMENT + payload.version = PAYLOAD_VERSION + payload.chainId = PAYLOAD_CHAIN_ID + + 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}`, + }) + }) +} + +export const createLedgerMessage = async ( + wallet: WalletContextState, + setCookie: (wallet: string, token: string) => void, + connection: Connection, +) => { + const payload = new Payload() + payload.domain = window.location.host + payload.address = wallet.publicKey!.toBase58() + payload.uri = window.location.origin + payload.statement = PAYLOAD_STATEMENT + payload.version = PAYLOAD_VERSION + payload.chainId = PAYLOAD_CHAIN_ID + + const message = new SIWS({ payload }) + + const messageText = message.prepareMessage() + const tx = new Transaction() + + tx.add( + new TransactionInstruction({ + programId: MEMO_PROGRAM_ID, + keys: [], + data: Buffer.from(messageText), + }), + ) + tx.feePayer = wallet.publicKey! + tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash + + const signedTx = await wallet.signTransaction!(tx) + const serializedTx = signedTx.serialize() + + const tokenResp = await fetch(`${NOTIFICATION_API}auth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...payload, + isLedger: true, + serializedTx: Array.from(serializedTx), + }), + }) + 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) +} diff --git a/utils/numbers.ts b/utils/numbers.ts index b00265eb..a515c26f 100644 --- a/utils/numbers.ts +++ b/utils/numbers.ts @@ -97,10 +97,29 @@ export const floorToDecimal = ( decimals: number, ): Decimal => { const decimal = value instanceof Decimal ? value : new Decimal(value) - return decimal.toDecimalPlaces(decimals, Decimal.ROUND_FLOOR) } +// Significant digits before the dot count against the number of decimals +// to show. When maxSignificantDecimals is 2: +// 0.012345 -> 0.012 +// 0.12345 -> 0.12 +// 1.12345 -> 1.1 +// 12.345 -> 12.3 +// 123.456 -> 123 +// 1234.567 -> 1234 +export const floorToDecimalSignificance = ( + value: number | string | Decimal, + maxSignificantDecimals: number, +): Decimal => { + const number = Number(value) + const log = Math.log10(Math.abs(number)) + const decimal = new Decimal(value) + return decimal.toDecimalPlaces( + Math.max(0, Math.floor(-log + maxSignificantDecimals - Number.EPSILON)), + ) +} + const usdFormatter0 = Intl.NumberFormat('en', { minimumFractionDigits: 0, maximumFractionDigits: 0, diff --git a/utils/theme.ts b/utils/theme.ts index f11379a9..103d476f 100644 --- a/utils/theme.ts +++ b/utils/theme.ts @@ -84,5 +84,5 @@ export const nftThemeMeta: NftThemeMeta = { export const CUSTOM_SKINS: { [key: string]: string } = { bonk: '6FUYsgvSPiLsMpKZqLWswkw7j4juudZyVopU6RYKLkQ3', - pepe: '6FUYsgvSPiLsMpKZqLWswkw7j4juudZyVopU6RYKLkQ3', + pepe: 'B4QhXJaSnUBT8aWiPz5GmB9sjXdQDtHXv8x1WsRFHNJx', } diff --git a/utils/tokens.ts b/utils/tokens.ts index 6953fd0f..73f15996 100644 --- a/utils/tokens.ts +++ b/utils/tokens.ts @@ -93,6 +93,7 @@ const enhanceNFT = (nft: NftWithATA) => { collectionAddress: nft.collection?.address.toBase58(), mint: nft.mint.address.toBase58(), tokenAccount: nft.tokenAccountAddress?.toBase58() || '', + json: nft.json, } } diff --git a/yarn.lock b/yarn.lock index a7ea25a8..6643f6d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,7 +12,7 @@ resolved "https://registry.yarnpkg.com/@apocentre/alias-sampling/-/alias-sampling-0.5.3.tgz#897ff181b48ad7b2bcb4ecf29400214888244f08" integrity sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.22.6": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== @@ -26,30 +26,31 @@ dependencies: ws "^8.13.0" -"@blockworks-foundation/mango-v4-settings@0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4-settings/-/mango-v4-settings-0.2.6.tgz#725a8cf669e164cd7694d97989472f7852afad68" - integrity sha512-RK8O8lbflIN9IgNE1uUkjrtlv/7f0BjIqTwcuLNFos6/e/Q2/AnlXRlD5Y9WnO6xS7mXNsw9kr05xCxeYZzM1Q== +"@blockworks-foundation/mango-v4-settings@0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4-settings/-/mango-v4-settings-0.2.10.tgz#d09a74b743b1eb44bcad46b09680035fb8ec5a85" + integrity sha512-pQ7TfAxBdoF0fNVQ7PsT4TW5cpGibTjZadXcYlbyfetxQbTQS9mgghfGaq/TDFz/rHa6FS2FJ0xCJBoy7FNDbg== dependencies: bn.js "^5.2.1" eslint-config-prettier "^9.0.0" -"@blockworks-foundation/mango-v4@^0.19.3": - version "0.19.3" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.19.3.tgz#c41b002a674b6ca7647a0c6ec007ffd705d1a53a" - integrity sha512-E+0yVLEkomrSuCrAu8odmBb/hZOOt8h4izDqrKC+Af+fg/n8hCylAax+Lt0fbtSgp4V3l4xNPCPHUkTiMA5zOQ== +"@blockworks-foundation/mango-v4@^0.19.27": + version "0.19.27" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.19.27.tgz#73203fecd7cca5ceea658ede8788b72974778d48" + integrity sha512-UEGTcxWLe0wga+RSTdBzoUUev2oCXV2az0s3RCcU7BlQ2UPnQDWSfbO/rmxPmveKYFSBGW+RsiSzLlXAmm9hPQ== dependencies: "@coral-xyz/anchor" "^0.27.0" "@project-serum/serum" "0.13.65" "@pythnetwork/client" "~2.14.0" "@solana/spl-token" "0.3.7" - "@solana/web3.js" "^1.73.2" + "@solana/web3.js" "^1.78.2" "@switchboard-xyz/sbv2-lite" "^0.1.6" big.js "^6.1.1" binance-api-node "^0.12.0" bs58 "^5.0.0" cross-fetch "^3.1.5" dotenv "^16.0.3" + lodash "^4.17.21" node-kraken-api "^2.2.2" "@blocto/sdk@^0.2.22": @@ -1014,11 +1015,16 @@ resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ== -"@noble/hashes@1.3.1", "@noble/hashes@^1.1.3", "@noble/hashes@^1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": +"@noble/hashes@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== +"@noble/hashes@^1.1.3", "@noble/hashes@^1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1994,7 +2000,7 @@ "@wallet-standard/app" "^1.0.1" "@wallet-standard/base" "^1.0.1" -"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.44.3", "@solana/web3.js@^1.50.1", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.63.1", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.73.2", "@solana/web3.js@^1.78.3": +"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.44.3", "@solana/web3.js@^1.50.1", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.63.1", "@solana/web3.js@^1.66.2", "@solana/web3.js@^1.68.0", "@solana/web3.js@^1.78.2", "@solana/web3.js@^1.78.3": version "1.78.4" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.78.4.tgz#e8ca9abe4ec2af5fc540c1d272efee24aaffedb3" integrity sha512-up5VG1dK+GPhykmuMIozJZBbVqpm77vbOG6/r5dS7NBGZonwHfTLdBbsYc3rjmaQ4DpCXUa3tUc4RZHRORvZrw== @@ -2547,13 +2553,6 @@ dependencies: "@types/react" "*" -"@types/react-window@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" - integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== - dependencies: - "@types/react" "*" - "@types/react@*": version "18.0.28" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" @@ -5569,11 +5568,6 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" -hotkeys-js@^3.8.1: - version "3.10.2" - resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.10.2.tgz#cf52661904f5a13a973565cb97085fea2f5ae257" - integrity sha512-Z6vLmJTYzkbZZXlBkhrYB962Q/rZGc/WHQiyEGu9ZZVF7bAeFDjjDa31grWREuw9Ygb4zmlov2bTkPYqj0aFnQ== - howler@2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.3.tgz#a2eff9b08b586798e7a2ee17a602a90df28715da" @@ -6371,11 +6365,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -"memoize-one@>=3.1.1 <6": - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== - merge-options@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" @@ -7319,13 +7308,10 @@ react-grid-layout@1.3.4: react-draggable "^4.0.0" react-resizable "^3.0.4" -react-hot-keys@2.7.2: - version "2.7.2" - resolved "https://registry.yarnpkg.com/react-hot-keys/-/react-hot-keys-2.7.2.tgz#7d2b02b7e2cf69182ea71ca01885446ebfae01d2" - integrity sha512-Z7eSh7SU6s52+zP+vkfFoNk0x4kgEmnwqDiyACKv53crK2AZ7FUaBLnf+vxLor3dvtId9murLmKOsrJeYgeHWw== - dependencies: - hotkeys-js "^3.8.1" - prop-types "^15.7.2" +react-hotkeys-hook@4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz#1f7a7a1c9c21d4fa3280bf340fcca8fd77d81994" + integrity sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw== react-i18next@13.0.2: version "13.0.2" @@ -7429,14 +7415,6 @@ react-tsparticles@2.2.4: fast-deep-equal "^3.1.3" tsparticles-engine "^2.2.4" -react-window@1.8.7: - version "1.8.7" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4" - integrity sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA== - dependencies: - "@babel/runtime" "^7.0.0" - memoize-one ">=3.1.1 <6" - react@16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"