merge main

This commit is contained in:
saml33 2023-07-26 09:27:17 +10:00
commit ed1f680409
61 changed files with 2708 additions and 963 deletions

View File

@ -0,0 +1,56 @@
import {
AuctionHouse,
Metaplex,
LazyListing,
LazyBid,
} from '@metaplex-foundation/js'
import { ALL_FILTER } from 'hooks/market/useAuctionHouse'
import { AUCTION_HOUSE_ID } from 'utils/constants'
export const fetchAuctionHouse = async (metaplex: Metaplex) => {
const auctionHouse = await metaplex
.auctionHouse()
.findByAddress({ address: AUCTION_HOUSE_ID })
return auctionHouse
}
export const fetchFilteredListing = async (
metaplex: Metaplex,
auctionHouse: AuctionHouse,
filter: string,
page: number,
perPage: number,
) => {
const listings = (
await metaplex.auctionHouse().findListings({
auctionHouse,
})
)
.filter((x) =>
filter === ALL_FILTER
? true
: x.sellerAddress.equals(metaplex.identity().publicKey),
)
.filter((x) => !x.canceledAt && !x.purchaseReceiptAddress)
const filteredListings = listings.slice(
(page - 1) * perPage,
page * perPage,
) as LazyListing[]
return {
results: filteredListings,
totalPages: Math.ceil(listings.length / perPage),
}
}
export const fetchFilteredBids = async (
metaplex: Metaplex,
auctionHouse: AuctionHouse,
) => {
const bids = await metaplex.auctionHouse().findBids({
auctionHouse,
})
return bids.filter(
(x) => !x.canceledAt && !x.purchaseReceiptAddress,
) as LazyBid[]
}

View File

@ -0,0 +1,18 @@
import { PhotoIcon } from '@heroicons/react/20/solid'
import { useState } from 'react'
export const ImgWithLoader = (props: {
className: string
src: string
alt: string
}) => {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative">
{isLoading && (
<PhotoIcon className="absolute left-1/2 top-1/2 z-10 h-1/4 w-1/4 -translate-x-1/2 -translate-y-1/2 animate-pulse text-th-fgd-4" />
)}
<img {...props} onLoad={() => setIsLoading(false)} alt={props.alt} />
</div>
)
}

View File

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Listbox } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { ReactNode } from 'react'
interface SelectProps {
value: string | ReactNode
onChange: (x: string) => void
onChange: (x: any) => void
children: ReactNode
className?: string
dropdownPanelClassName?: string

View File

@ -8,11 +8,7 @@ import { handleGetRoutes } from '@components/swap/useQuoteRoutes'
import { JUPITER_PRICE_API_MAINNET, USDC_MINT } from 'utils/constants'
import { AccountMeta, PublicKey, SYSVAR_RENT_PUBKEY } from '@solana/web3.js'
import { useWallet } from '@solana/wallet-adapter-react'
import {
OPENBOOK_PROGRAM_ID,
RouteInfo,
toNative,
} from '@blockworks-foundation/mango-v4'
import { OPENBOOK_PROGRAM_ID, toNative } from '@blockworks-foundation/mango-v4'
import {
MANGO_DAO_WALLET,
MANGO_DAO_WALLET_GOVERNANCE,
@ -214,7 +210,12 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
)
const handleGetRoutesWithFixedArgs = useCallback(
(amount: number, tokenMint: PublicKey, mode: 'ExactIn' | 'ExactOut') => {
(
amount: number,
tokenMint: PublicKey,
mode: 'ExactIn' | 'ExactOut',
onlyDirect = false,
) => {
const SLIPPAGE_BPS = 50
const FEE = 0
const walletForCheck = wallet.publicKey
@ -230,6 +231,7 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
FEE,
walletForCheck,
'JUPITER',
onlyDirect,
)
},
[wallet.publicKey],
@ -282,12 +284,7 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
: 'UNTRUSTED'
setCoinTier(tier)
setPriceImpact(midTierCheck ? midTierCheck.priceImpactPct * 100 : 100)
handleGetPoolParams(
swaps.find(
(x) => x.bestRoute!.amount.toString() === TWENTY_K_USDC_BASE,
)!.routes,
)
handleGetPoolParams(tier, tokenMint)
return tier
} catch (e) {
notify({
@ -300,8 +297,24 @@ const ListToken = ({ goBack }: { goBack: () => void }) => {
[t, handleGetRoutesWithFixedArgs],
)
const handleGetPoolParams = (routes: never[] | RouteInfo[]) => {
const marketInfos = routes.flatMap((x) => x.marketInfos)
const handleGetPoolParams = async (
tier: LISTING_PRESETS_KEYS,
tokenMint: PublicKey,
) => {
const tierToSwapValue: { [key: string]: number } = {
PREMIUM: 100000,
MID: 20000,
MEME: 5000,
SHIT: 1000,
UNTRUSTED: 100,
}
const swaps = await handleGetRoutesWithFixedArgs(
tierToSwapValue[tier],
tokenMint,
'ExactIn',
true,
)
const marketInfos = swaps.routes.flatMap((x) => x.marketInfos)
const orcaPool = marketInfos.find((x) => x.label === 'Orca')
const raydiumPool = marketInfos.find((x) => x.label === 'Raydium')
setOrcaPoolAddress(orcaPool?.id || '')

View File

@ -0,0 +1,227 @@
// import { useTranslation } from 'next-i18next'
import { useEffect, useState } from 'react'
import {
useAuctionHouse,
useBids,
useListings,
useLoadBids,
} from 'hooks/market/useAuctionHouse'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
import { useWallet } from '@solana/wallet-adapter-react'
import metaplexStore from '@store/metaplexStore'
import { Bid, Listing, PublicBid, PublicKey } from '@metaplex-foundation/js'
import BidNftModal from './BidNftModal'
import mangoStore from '@store/mangoStore'
import {
Table,
TableDateDisplay,
Td,
Th,
TrBody,
TrHead,
} from '@components/shared/TableElements'
import { ImgWithLoader } from '@components/ImgWithLoader'
import NftMarketButton from './NftMarketButton'
import { abbreviateAddress } from 'utils/formatting'
import { NoSymbolIcon } from '@heroicons/react/20/solid'
const AllBidsView = () => {
const { publicKey } = useWallet()
const { data: auctionHouse } = useAuctionHouse()
const metaplex = metaplexStore((s) => s.metaplex)
// const { t } = useTranslation(['nft-market'])
const [showBidModal, setShowBidModal] = useState(false)
const [bidListing, setBidListing] = useState<null | Listing>(null)
const { data: bids, refetch } = useBids()
const bidsToLoad = bids ? bids : []
const { data: loadedBids } = useLoadBids(bidsToLoad)
const connection = mangoStore((s) => s.connection)
const fetchNfts = mangoStore((s) => s.actions.fetchNfts)
const nfts = mangoStore((s) => s.wallet.nfts.data)
const { data: listings } = useListings()
useEffect(() => {
if (publicKey) {
fetchNfts(connection, publicKey!)
}
}, [publicKey])
const cancelBid = async (bid: Bid) => {
await metaplex!.auctionHouse().cancelBid({
auctionHouse: auctionHouse!,
bid,
})
refetch()
}
const sellAsset = async (bid: Bid, tokenAccountPk: string) => {
console.log(tokenAccountPk)
const tokenAccount = await metaplex
?.tokens()
.findTokenByAddress({ address: new PublicKey(tokenAccountPk) })
await metaplex!.auctionHouse().sell({
auctionHouse: auctionHouse!,
bid: bid as PublicBid,
sellerToken: tokenAccount!,
})
refetch()
}
const buyAsset = async (listing: Listing) => {
await metaplex!.auctionHouse().buy({
auctionHouse: auctionHouse!,
listing,
})
refetch()
}
const openBidModal = (listing: Listing) => {
setBidListing(listing)
setShowBidModal(true)
}
return (
<>
<div className="flex flex-col">
{loadedBids?.length ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">Date</Th>
<Th className="text-right">NFT</Th>
<Th className="text-right">Offer</Th>
<Th className="text-right">Buy Now Price</Th>
<Th className="text-right">Buyer</Th>
<Th className="text-right">Actions</Th>
</TrHead>
</thead>
<tbody>
{loadedBids
.sort((a, b) => b.createdAt.toNumber() - a.createdAt.toNumber())
.map((x, idx) => {
const listing = listings?.results?.find(
(nft: Listing) =>
nft.asset.mint.toString() === x.asset.mint.toString(),
)
return (
<TrBody key={idx}>
<Td>
<TableDateDisplay
date={x.createdAt.toNumber()}
showSeconds
/>
</Td>
<Td>
<div className="flex justify-end">
<ImgWithLoader
className="w-12 rounded-md"
alt={x.asset.name}
src={x.asset.json!.image!}
/>
</div>
</Td>
<Td>
<p className="text-right">
{toUiDecimals(
x.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)}
<span className="font-body">{' MNGO'}</span>
</p>
</Td>
<Td>
<p className="text-right">
<p className="text-right">
{listing ? (
<>
{toUiDecimals(
listing.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)}
<span className="font-body">{' MNGO'}</span>
</>
) : (
''
)}
</p>
</p>
</Td>
<Td>
<p className="text-right">
{abbreviateAddress(x.buyerAddress)}
</p>
</Td>
<Td>
<div className="flex justify-end space-x-2">
{publicKey &&
!x.buyerAddress.equals(publicKey) &&
nfts.find(
(ownNft) =>
ownNft.mint === x.asset.address.toBase58(),
) ? (
<NftMarketButton
onClick={() =>
sellAsset(
x,
nfts.find(
(ownNft) =>
ownNft.mint ===
x.asset.address.toBase58(),
)!.tokenAccount,
)
}
colorClass="fgd-3"
text="Accept Offer"
/>
) : (
<>
{publicKey && x.buyerAddress.equals(publicKey) ? (
<NftMarketButton
colorClass="error"
text="Cancel Offer"
onClick={() => cancelBid(x)}
/>
) : listing ? (
<NftMarketButton
colorClass="fgd-3"
text="Make Offer"
onClick={() => openBidModal(listing)}
/>
) : null}
{listing ? (
<NftMarketButton
colorClass="success"
text="Buy Now"
onClick={() => buyAsset(listing)}
/>
) : null}
</>
)}
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="mt-4 flex flex-col items-center rounded-md border border-th-bkg-3 p-4">
<NoSymbolIcon className="mb-1 h-7 w-7 text-th-fgd-4" />
<p>No offers to show...</p>
</div>
)}
</div>
{showBidModal ? (
<BidNftModal
listing={bidListing ? bidListing : undefined}
isOpen={showBidModal}
onClose={() => setShowBidModal(false)}
/>
) : null}
</>
)
}
export default AllBidsView

View File

@ -0,0 +1,61 @@
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import {
useAuctionHouse,
useBids,
useLazyListings,
} 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'
const AssetBidsModal = ({
isOpen,
onClose,
listing,
}: ModalProps & { listing: Listing }) => {
const { t } = useTranslation(['nft-market'])
const metaplex = metaplexStore((s) => s.metaplex)
const { data: auctionHouse } = useAuctionHouse()
const { data: lazyBids, refetch: reftechBids } = useBids()
const { refetch: refetchLazyListings } = useLazyListings()
const assetBids = lazyBids?.filter((x) =>
x.metadataAddress.equals(listing.asset.metadataAddress),
)
const acceptBid = async (lazyBid: LazyBid) => {
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()
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="flex max-h-[500px] min-h-[264px] flex-col overflow-auto">
{assetBids?.map((x) => (
<p className="flex space-x-2" key={x.createdAt.toNumber()}>
<div>{x.createdAt.toNumber()}</div>
<div>{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)}</div>
<div>
<Button onClick={() => acceptBid(x)}>{t('accept-bid')}</Button>
</div>
</p>
))}
</div>
</Modal>
)
}
export default AssetBidsModal

View File

@ -0,0 +1,112 @@
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import { useState, useCallback } from 'react'
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
import Button, { LinkButton } from '@components/shared/Button'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
import { Listing, PublicKey, token } from '@metaplex-foundation/js'
import metaplexStore from '@store/metaplexStore'
import { useAuctionHouse, useBids } from 'hooks/market/useAuctionHouse'
import { ImgWithLoader } from '@components/ImgWithLoader'
// import { useTranslation } from 'next-i18next'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
type ListingModalProps = {
listing?: Listing
} & ModalProps
const BidNftModal = ({ isOpen, onClose, listing }: ListingModalProps) => {
const metaplex = metaplexStore((s) => s.metaplex)
const { data: auctionHouse } = useAuctionHouse()
const { refetch } = useBids()
// const { t } = useTranslation(['nft-market'])
const noneListedAssetMode = !listing
const [bidPrice, setBidPrice] = useState('')
const [assetMint, setAssetMint] = useState('')
const bid = useCallback(async () => {
await metaplex!.auctionHouse().bid({
auctionHouse: auctionHouse!,
price: token(bidPrice, MANGO_MINT_DECIMALS),
mintAccount: noneListedAssetMode
? new PublicKey(assetMint)
: listing!.asset.mint.address,
})
onClose()
refetch()
}, [
metaplex,
auctionHouse,
bidPrice,
noneListedAssetMode,
assetMint,
listing,
onClose,
refetch,
])
return (
<Modal isOpen={isOpen} onClose={onClose}>
<h2 className="mb-4 text-center text-lg">Make an Offer</h2>
<div className="flex flex-col items-center">
{listing ? (
<div className="flex flex-col items-center">
<ImgWithLoader
alt={listing.asset.name}
className="mb-3 h-40 w-40 flex-shrink-0 rounded-md"
src={listing.asset.json!.image!}
/>
<LinkButton>
<span className="font-body font-normal">
Buy Now:{' '}
<span className="font-display">
{toUiDecimals(
listing.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)}{' '}
<span className="font-bold">MNGO</span>
</span>
</span>
</LinkButton>
</div>
) : (
<>
<Label text="NFT Mint" />
<Input
className="mb-2"
type="text"
value={assetMint}
onChange={(e) => {
setAssetMint(e.target.value)
}}
/>
</>
)}
<div className="mt-4 flex w-full items-end">
<div className="w-full">
<Label text="Offer Price"></Label>
<Input
value={bidPrice}
onChange={(e) => {
setBidPrice(e.target.value)
}}
suffix="MNGO"
/>
</div>
<Button
className="ml-2 whitespace-nowrap"
onClick={bid}
disabled={!bidPrice}
size="large"
>
Make Offer
</Button>
</div>
</div>
</Modal>
)
}
export default BidNftModal

View File

@ -0,0 +1,211 @@
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import Select from '@components/forms/Select'
import BidNftModal from '@components/nftMarket/BidNftModal'
import AssetBidsModal from '@components/nftMarket/AssetBidsModal'
import { Listing } from '@metaplex-foundation/js'
import { useWallet } from '@solana/wallet-adapter-react'
import metaplexStore from '@store/metaplexStore'
import {
ALL_FILTER,
useAuctionHouse,
useBids,
useLazyListings,
useListings,
} from 'hooks/market/useAuctionHouse'
import { useState } from 'react'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
// import { useTranslation } from 'next-i18next'
// import ResponsivePagination from 'react-responsive-pagination'
import { ImgWithLoader } from '@components/ImgWithLoader'
import NftMarketButton from './NftMarketButton'
const filter = [ALL_FILTER, 'My Listings']
const ListingsView = () => {
const { publicKey } = useWallet()
const metaplex = metaplexStore((s) => s.metaplex)
// const { t } = useTranslation(['nft-market'])
const [currentFilter, setCurrentFilter] = useState(ALL_FILTER)
const { data: bids } = useBids()
// const [page, setPage] = useState(1)
const [bidListing, setBidListing] = useState<null | Listing>(null)
const [assetBidsListing, setAssetBidsListing] = useState<null | Listing>(null)
const { data: auctionHouse } = useAuctionHouse()
const [asssetBidsModal, setAssetBidsModal] = useState(false)
const [bidNftModal, setBidNftModal] = useState(false)
const { refetch } = useLazyListings()
// const { data: listings } = useListings(currentFilter, page)
const { data: listings } = useListings()
const cancelListing = async (listing: Listing) => {
await metaplex!.auctionHouse().cancelListing({
auctionHouse: auctionHouse!,
listing: listing,
})
refetch()
}
const buyAsset = async (listing: Listing) => {
await metaplex!.auctionHouse().buy({
auctionHouse: auctionHouse!,
listing,
})
refetch()
}
const openBidModal = (listing: Listing) => {
setBidListing(listing)
setBidNftModal(true)
}
const closeBidModal = () => {
setBidNftModal(false)
setBidListing(null)
}
const openBidsModal = (listing: Listing) => {
setAssetBidsModal(true)
setAssetBidsListing(listing)
}
const closeBidsModal = () => {
setAssetBidsModal(false)
setAssetBidsListing(null)
}
// const handlePageClick = (page: number) => {
// setPage(page)
// }
return (
<div className="flex flex-col">
<div className="mb-4 mt-2 flex items-center justify-between rounded-md bg-th-bkg-2 p-2 pl-4">
<h3 className="text-sm font-normal text-th-fgd-3">{`Filter Results`}</h3>
<Select
value={currentFilter}
onChange={(filter) => setCurrentFilter(filter)}
className="w-[150px]"
>
{filter.map((filter) => (
<Select.Option key={filter} value={filter}>
<div className="flex w-full items-center justify-between">
{filter}
</div>
</Select.Option>
))}
</Select>
{asssetBidsModal && assetBidsListing && (
<AssetBidsModal
listing={assetBidsListing}
isOpen={asssetBidsModal}
onClose={closeBidsModal}
></AssetBidsModal>
)}
</div>
<div className="grid auto-cols-max grid-flow-row grid-flow-col auto-rows-max gap-4">
{listings?.results?.map((x, idx) => {
const imgSource = x.asset.json?.image
const nftBids = bids?.filter((bid) =>
bid.metadataAddress.equals(x.asset.metadataAddress),
)
const bestBid = nftBids
? nftBids.reduce((a, c) => {
const price = toUiDecimals(
c.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)
if (price > a) {
a = price
}
return a
}, 0)
: 0
return (
<div className="w-60 rounded-lg border border-th-bkg-3" key={idx}>
{imgSource ? (
<div className="flex h-60 w-full items-start overflow-hidden rounded-t-lg">
<ImgWithLoader
alt="nft"
className="h-auto w-60 flex-shrink-0"
src={imgSource}
/>
</div>
) : null}
<div className="p-4">
<div className="flex justify-between">
<div>
<p className="text-xs">Buy Now</p>
<div className="flex items-center">
{/* <img
className="mr-1 h-3.5 w-auto"
src="/icons/mngo.svg"
/> */}
<span className="font-display text-base">
{toUiDecimals(
x.price.basisPoints.toNumber(),
MANGO_MINT_DECIMALS,
)}{' '}
<span className="font-body font-bold">MNGO</span>
</span>
</div>
</div>
</div>
<div>
<p className="mt-2 text-xs">
{bestBid ? `Best Offer: ${bestBid} MNGO` : 'No offers'}
</p>
</div>
{publicKey && !x.sellerAddress.equals(publicKey) && (
<div className="mt-3 flex space-x-2 border-t border-th-bkg-3 pt-4">
<NftMarketButton
className="w-1/2"
text="Buy Now"
colorClass="success"
onClick={() => buyAsset(x)}
/>
<NftMarketButton
className="w-1/2"
text="Make Offer"
colorClass="fgd-3"
onClick={() => openBidModal(x)}
/>
{bidNftModal && bidListing && (
<BidNftModal
listing={bidListing}
isOpen={bidNftModal}
onClose={closeBidModal}
></BidNftModal>
)}
</div>
)}
{publicKey && x.sellerAddress.equals(publicKey) && (
<div className="mt-3 flex space-x-2 border-t border-th-bkg-3 pt-4">
<NftMarketButton
className="w-1/2"
text="Delist"
colorClass="error"
onClick={() => cancelListing(x)}
/>
<NftMarketButton
className="w-1/2"
text={`Offers (${nftBids?.length})`}
colorClass="fgd-3"
onClick={() => openBidsModal(x)}
/>
</div>
)}
</div>
</div>
)
})}
</div>
{/* <div>
<ResponsivePagination
current={page}
total={listings?.totalPages || 0}
onPageChange={handlePageClick}
/>
</div> */}
</div>
)
}
export default ListingsView

View File

@ -0,0 +1,75 @@
import { useWallet } from '@solana/wallet-adapter-react'
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import {
useAuctionHouse,
useBids,
useLoadBids,
} from 'hooks/market/useAuctionHouse'
import { toUiDecimals } from '@blockworks-foundation/mango-v4'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
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'
const MyBidsModal = ({ isOpen, onClose }: ModalProps) => {
const { publicKey } = useWallet()
const metaplex = metaplexStore((s) => s.metaplex)
const { t } = useTranslation(['nft-market'])
const { data: auctionHouse } = useAuctionHouse()
const { data: lazyBids, refetch } = useBids()
const myBids =
lazyBids && publicKey
? lazyBids.filter((x) => x.buyerAddress.equals(publicKey))
: []
const { data: bids } = useLoadBids(myBids)
const cancelBid = async (bid: Bid) => {
await metaplex!.auctionHouse().cancelBid({
auctionHouse: auctionHouse!,
bid,
})
refetch()
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<h2 className="mb-4 text-center text-lg">Your Offers</h2>
<div className="space-y-4">
{bids?.map((x) => (
<div
className="flex items-center justify-between"
key={x.createdAt.toNumber()}
>
<div className="flex items-center">
<ImgWithLoader
className="mr-3 w-12 rounded-md"
alt={x.asset.name}
src={x.asset.json!.image!}
/>
<div>
<p className="text-xs">
{dayjs(x.createdAt.toNumber()).format('DD MMM YY h:mma')}
</p>
<span className="font-display text-th-fgd-2">
{toUiDecimals(x.price.basisPoints, MANGO_MINT_DECIMALS)} MNGO
</span>
</div>
</div>
<NftMarketButton
text={t('cancel')}
colorClass="error"
onClick={() => cancelBid(x)}
/>
</div>
))}
</div>
</Modal>
)
}
export default MyBidsModal

View File

@ -0,0 +1,22 @@
const NftMarketButton = ({
className,
colorClass,
text,
onClick,
}: {
className?: string
colorClass: string
text: string
onClick: () => void
}) => {
return (
<button
className={`flex justify-center rounded-b rounded-t border border-th-${colorClass} py-1 px-2 text-th-${colorClass} font-bold focus:outline-none md:hover:brightness-75 ${className}`}
onClick={onClick}
>
{text}
</button>
)
}
export default NftMarketButton

View File

@ -0,0 +1,122 @@
import { useWallet } from '@solana/wallet-adapter-react'
import { ModalProps } from '../../types/modal'
import Modal from '../shared/Modal'
import { useEffect, useState } from 'react'
import mangoStore from '@store/mangoStore'
import { useTranslation } from 'next-i18next'
import { ImgWithLoader } from '@components/ImgWithLoader'
import { NFT } from 'types'
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
import Button from '@components/shared/Button'
import { MANGO_MINT_DECIMALS } from 'utils/governance/constants'
import { PublicKey } from '@solana/web3.js'
import { token } from '@metaplex-foundation/js'
import metaplexStore from '@store/metaplexStore'
import { useAuctionHouse, useLazyListings } from 'hooks/market/useAuctionHouse'
const SellNftModal = ({ isOpen, onClose }: ModalProps) => {
const { publicKey } = useWallet()
const { t } = useTranslation()
const metaplex = metaplexStore((s) => s.metaplex)
const { data: auctionHouse } = useAuctionHouse()
const { refetch } = useLazyListings()
const connection = mangoStore((s) => s.connection)
const nfts = mangoStore((s) => s.wallet.nfts.data)
const isLoadingNfts = mangoStore((s) => s.wallet.nfts.loading)
const fetchNfts = mangoStore((s) => s.actions.fetchNfts)
const [minPrice, setMinPrice] = useState('')
const [selectedNft, setSelectedNft] = useState<NFT | null>(null)
useEffect(() => {
if (publicKey) {
fetchNfts(connection, publicKey)
}
}, [publicKey])
const listAsset = async (mint: string, price: number) => {
const currentListings = await metaplex?.auctionHouse().findListings({
auctionHouse: auctionHouse!,
seller: publicKey!,
mint: new PublicKey(mint),
})
const isCurrentlyListed = currentListings?.filter((x) => !x.canceledAt)
.length
if (isCurrentlyListed) {
throw 'Item is currently listed by you'
}
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()
onClose()
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<div className="hide-scroll h-[320px] overflow-y-auto">
{nfts.length > 0 ? (
<div className="flex flex-col items-center">
<div className="grid grid-flow-row auto-rows-max grid-cols-4 gap-1">
{nfts.map((n) => (
<button
className={`flex h-24 w-24 items-center justify-center rounded-md border bg-th-bkg-2 py-3 sm:py-4 md:hover:bg-th-bkg-3 ${
selectedNft?.address === n.address
? 'border-th-active'
: 'border-th-bkg-3'
}`}
key={n.address}
onClick={() => setSelectedNft(n)}
>
<ImgWithLoader
alt={n.name}
className="h-16 w-16 flex-shrink-0 rounded-full"
src={n.image}
/>
</button>
))}
</div>
</div>
) : isLoadingNfts ? (
<div className="grid grid-flow-row auto-rows-max grid-cols-4 gap-1">
{[...Array(12)].map((x, i) => (
<div
className="h-24 w-24 animate-pulse rounded-md bg-th-bkg-3"
key={i}
/>
))}
</div>
) : (
<div className="flex h-60 items-center justify-center">
<p>{t('profile:no-nfts')}</p>
</div>
)}
</div>
<div className="flex items-end pt-6">
<div className="w-full">
<Label text="Buy Now Price"></Label>
<Input
value={minPrice}
onChange={(e) => {
setMinPrice(e.target.value)
}}
suffix="MNGO"
></Input>
</div>
<Button
className="ml-2"
disabled={!selectedNft || !minPrice}
onClick={() => listAsset(selectedNft!.mint, Number(minPrice))}
size="large"
>
{t('nftMarket:list')}
</Button>
</div>
</Modal>
)
}
export default SellNftModal

View File

@ -1,28 +1,13 @@
import { useState, useEffect } from 'react'
import mangoStore from '@store/mangoStore'
import { useWallet } from '@solana/wallet-adapter-react'
import { ArrowLeftIcon, PhotoIcon } from '@heroicons/react/20/solid'
import { ArrowLeftIcon } from '@heroicons/react/20/solid'
import { useTranslation } from 'next-i18next'
import Button, { LinkButton } from '../shared/Button'
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { notify } from 'utils/notifications'
import { MANGO_DATA_API_URL } from 'utils/constants'
const ImgWithLoader = (props: {
className: string
src: string
alt: string
}) => {
const [isLoading, setIsLoading] = useState(true)
return (
<div className="relative">
{isLoading && (
<PhotoIcon className="absolute left-1/2 top-1/2 z-10 h-1/4 w-1/4 -translate-x-1/2 -translate-y-1/2 animate-pulse text-th-fgd-4" />
)}
<img {...props} onLoad={() => setIsLoading(false)} alt={props.alt} />
</div>
)
}
import { ImgWithLoader } from '@components/ImgWithLoader'
const EditNftProfilePic = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation(['common', 'profile'])

View File

@ -2,9 +2,11 @@ import { MinusSmallIcon } from '@heroicons/react/20/solid'
import { DownTriangle, UpTriangle } from './DirectionTriangles'
import FormatNumericValue from './FormatNumericValue'
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
import use24HourChange from 'hooks/use24HourChange'
import { useMemo } from 'react'
import SheenLoader from './SheenLoader'
import useMarketsData from 'hooks/useMarketsData'
import mangoStore from '@store/mangoStore'
import { MarketData } from 'types'
const MarketChange = ({
market,
@ -13,12 +15,40 @@ const MarketChange = ({
market: PerpMarket | Serum3Market | undefined
size?: 'small'
}) => {
const { loading, spotChange, perpChange } = use24HourChange(market)
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const currentSpotPrice = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !market || market instanceof PerpMarket) return 0
const baseBank = group.getFirstBankByTokenIndex(market.baseTokenIndex)
const quoteBank = group.getFirstBankByTokenIndex(market.quoteTokenIndex)
if (!baseBank || !quoteBank) return 0
return baseBank.uiPrice / quoteBank.uiPrice
}, [market])
const change = useMemo(() => {
if (!market) return
return market instanceof PerpMarket ? perpChange : spotChange
}, [perpChange, spotChange])
if (!market || !marketsData) return
let pastPrice = 0
if (market instanceof PerpMarket) {
const perpData: MarketData = marketsData?.perpData
const perpEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
pastPrice = perpEntries ? perpEntries[1][0]?.price_24h : 0
} else {
const spotData: MarketData = marketsData?.spotData
const spotEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
pastPrice = spotEntries ? spotEntries[1][0]?.price_24h : 0
}
const currentPrice =
market instanceof PerpMarket ? market.uiPrice : currentSpotPrice
const change = ((currentPrice - pastPrice) / pastPrice) * 100
return change
}, [marketsData, currentSpotPrice])
const loading = isLoading || isFetching
return loading ? (
<SheenLoader className="mt-0.5">
@ -60,7 +90,20 @@ const MarketChange = ({
</p>
</div>
) : (
<p></p>
<div className="flex items-center space-x-1.5">
<MinusSmallIcon
className={`-mr-1 ${
size === 'small' ? 'h-4 w-4' : 'h-6 w-6'
} text-th-fgd-4`}
/>
<p
className={`font-mono font-normal ${
size === 'small' ? 'text-xs' : 'text-sm'
} text-th-fgd-2`}
>
0%
</p>
</div>
)
}

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'next-i18next'
const SoonBadge = () => {
const { t } = useTranslation('common')
return (
<div className="flex items-center rounded-full border border-th-active px-1.5 py-0.5 text-xs uppercase text-th-active">
<div className="flex items-center rounded-full border border-th-active px-1.5 py-0.5 text-xxs uppercase text-th-active leading-none">
{t('soon')}&trade;
</div>
)

View File

@ -8,6 +8,7 @@ interface TabUnderlineProps<T extends Values> {
values: T[]
names?: Array<string>
small?: boolean
fillWidth?: boolean
}
const TabUnderline = <T extends Values>({
@ -16,6 +17,7 @@ const TabUnderline = <T extends Values>({
names,
onChange,
small,
fillWidth = true,
}: TabUnderlineProps<T>) => {
const { t } = useTranslation('common')
@ -36,7 +38,7 @@ const TabUnderline = <T extends Values>({
: 'bg-th-active'
}`}
style={{
// maxWidth: '176px',
maxWidth: !fillWidth ? '176px' : '',
transform: `translateX(${
values.findIndex((v) => v === activeValue) * 100
}%)`,
@ -47,7 +49,9 @@ const TabUnderline = <T extends Values>({
{values.map((value, i) => (
<button
onClick={() => onChange(value)}
className={`relative flex h-10 w-1/2
className={`relative flex h-10 w-1/2 ${
fillWidth ? '' : 'max-w-[176px]'
}
cursor-pointer items-center justify-center whitespace-nowrap rounded py-1 focus-visible:text-th-fgd-2 md:h-auto md:rounded-none md:hover:opacity-100 ${
small ? 'text-sm' : 'text-sm lg:text-base'
}

View File

@ -1,11 +1,9 @@
import { I80F48, PerpMarket } from '@blockworks-foundation/mango-v4'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { useViewport } from '../../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { COLORS } from '../../styles/colors'
import { breakpoints } from '../../utils/theme'
import ContentBox from '../shared/ContentBox'
import Change from '../shared/Change'
import MarketLogos from '@components/trade/MarketLogos'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import {
@ -16,33 +14,17 @@ import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/20/solid'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { getDecimalCount, numberCompacter } from 'utils/numbers'
import Tooltip from '@components/shared/Tooltip'
import { PerpStatsItem } from 'types'
import useMangoGroup from 'hooks/useMangoGroup'
import { NextRouter, useRouter } from 'next/router'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { Disclosure, Transition } from '@headlessui/react'
import { LinkButton } from '@components/shared/Button'
import SoonBadge from '@components/shared/SoonBadge'
import { DAILY_SECONDS } from 'utils/constants'
import MarketChange from '@components/shared/MarketChange'
import useThemeWrapper from 'hooks/useThemeWrapper'
export const getOneDayPerpStats = (
stats: PerpStatsItem[] | null,
marketName: string,
) => {
return stats
? stats
.filter((s) => s.perp_market === marketName)
.filter((f) => {
const seconds = DAILY_SECONDS
const dataTime = new Date(f.date_hour).getTime() / 1000
const now = new Date().getTime() / 1000
const limit = now - seconds
return dataTime >= limit
})
.reverse()
: []
}
import useListedMarketsWithMarketData, {
PerpMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
import { sortPerpMarkets } from 'utils/markets'
export const goToPerpMarketDetails = (
market: PerpMarket,
@ -54,15 +36,15 @@ export const goToPerpMarketDetails = (
const PerpMarketsOverviewTable = () => {
const { t } = useTranslation(['common', 'trade'])
const perpMarkets = mangoStore((s) => s.perpMarkets)
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const perpStats = mangoStore((s) => s.perpStats.data)
const { theme } = useThemeWrapper()
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const rate = usePerpFundingRate()
const { group } = useMangoGroup()
const router = useRouter()
const { perpMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const loadingMarketData = isLoading || isFetching
return (
<ContentBox hideBorder hidePadding>
@ -72,15 +54,8 @@ const PerpMarketsOverviewTable = () => {
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right"></Th>
<Th className="text-right">
<Tooltip content={t('trade:tooltip-stable-price')}>
<span className="tooltip-underline">
{t('trade:stable-price')}
</span>
</Tooltip>
</Th>
<Th className="text-right">{t('rolling-change')}</Th>
<Th className="text-right"></Th>
<Th className="text-right">{t('trade:24h-volume')}</Th>
<Th className="text-right">{t('trade:funding-rate')}</Th>
<Th className="text-right">{t('trade:open-interest')}</Th>
@ -88,210 +63,193 @@ const PerpMarketsOverviewTable = () => {
</TrHead>
</thead>
<tbody>
{perpMarkets.map((market) => {
const symbol = market.name.split('-')[0]
const marketStats = getOneDayPerpStats(perpStats, market.name)
{sortPerpMarkets(perpMarketsWithData, 'quote_volume_24h').map(
(market) => {
const symbol = market.name.split('-')[0]
const change = marketStats.length
? ((market.uiPrice - marketStats[0].price) /
marketStats[0].price) *
100
: 0
const priceHistory = market?.marketData?.price_history
const volume = marketStats.length
? marketStats[marketStats.length - 1].cumulative_quote_volume -
marketStats[0].cumulative_quote_volume
: 0
const volumeData = market?.marketData?.quote_volume_24h
let fundingRate
let fundingRateApr
if (rate.isSuccess) {
const marketRate = rate?.data?.find(
(r) => r.market_index === market.perpMarketIndex,
)
if (marketRate) {
fundingRate = formatFunding.format(
marketRate.funding_rate_hourly,
)
fundingRateApr = formatFunding.format(
marketRate.funding_rate_hourly * 8760,
const volume = volumeData ? volumeData : 0
let fundingRate
let fundingRateApr
if (rate.isSuccess) {
const marketRate = rate?.data?.find(
(r) => r.market_index === market.perpMarketIndex,
)
if (marketRate) {
fundingRate = formatFunding.format(
marketRate.funding_rate_hourly,
)
fundingRateApr = formatFunding.format(
marketRate.funding_rate_hourly * 8760,
)
} else {
fundingRate = ''
fundingRateApr = ''
}
} else {
fundingRate = ''
fundingRateApr = ''
}
} else {
fundingRate = ''
fundingRateApr = ''
}
const openInterest = market.baseLotsToUi(market.openInterest)
const isComingSoon = market.oracleLastUpdatedSlot == 0
const openInterest = market.baseLotsToUi(market.openInterest)
const isComingSoon = market.oracleLastUpdatedSlot == 0
const isUp =
priceHistory && priceHistory.length
? market.uiPrice >= priceHistory[0].price
: false
return (
<TrBody
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
key={market.publicKey.toString()}
onClick={() => goToPerpMarketDetails(market, router)}
>
<Td>
<div className="flex items-center">
<MarketLogos market={market} size="large" />
<p className="mr-2 whitespace-nowrap font-body">
{market.name}
</p>
{isComingSoon ? <SoonBadge /> : null}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{market.uiPrice ? (
<FormatNumericValue value={market.uiPrice} isUsd />
) : (
''
)}
</p>
</div>
</Td>
<Td>
{!loadingPerpStats ? (
marketStats.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
change >= 0
? COLORS.UP[theme]
: COLORS.DOWN[theme]
}
data={marketStats.concat([
{
...marketStats[marketStats.length - 1],
date_hour: new Date().toString(),
price: market.uiPrice,
},
])}
name={symbol}
xKey="date_hour"
yKey="price"
/>
</div>
) : symbol === 'USDC' || symbol === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">{t('unavailable')}</p>
)
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{group && market.uiPrice ? (
<FormatNumericValue
value={group.toUiPrice(
I80F48.fromNumber(
market.stablePriceModel.stablePrice,
),
market.baseDecimals,
)}
isUsd
/>
) : (
''
)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<Change change={change} suffix="%" />
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{volume ? `$${numberCompacter.format(volume)}` : ''}
</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
{fundingRate !== '' ? (
<Tooltip
content={
<>
{fundingRateApr ? (
<div className="">
The 1hr rate as an APR is{' '}
<span className="font-mono text-th-fgd-2">
{fundingRateApr}
</span>
</div>
) : null}
<div className="mt-2">
Funding is paid continuously. The 1hr rate
displayed is a rolling average of the past 60
mins.
</div>
<div className="mt-2">
When positive, longs will pay shorts and when
negative shorts pay longs.
</div>
</>
}
>
<p className="tooltip-underline">{fundingRate}</p>
</Tooltip>
) : (
<p></p>
)}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
{openInterest ? (
<>
<p>
<FormatNumericValue
value={openInterest}
decimals={getDecimalCount(market.minOrderSize)}
return (
<TrBody
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
key={market.publicKey.toString()}
onClick={() => goToPerpMarketDetails(market, router)}
>
<Td>
<div className="flex items-center">
<MarketLogos market={market} size="large" />
<p className="mr-2 whitespace-nowrap font-body">
{market.name}
</p>
{isComingSoon ? <SoonBadge /> : null}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{market.uiPrice ? (
<FormatNumericValue value={market.uiPrice} isUsd />
) : (
''
)}
</p>
</div>
</Td>
<Td>
<div className="flex flex-col items-end">
<MarketChange market={market} />
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={priceHistory.concat([
{
time: new Date().toString(),
price: market.uiPrice,
},
])}
name={symbol}
xKey="time"
yKey="price"
/>
</div>
) : symbol === 'USDC' || symbol === 'USDT' ? null : (
<p className="mb-0 text-th-fgd-4">
{t('unavailable')}
</p>
<p className="text-th-fgd-4">
$
{numberCompacter.format(
openInterest * market.uiPrice,
)}
</p>
</>
)
) : (
<>
<p></p>
<p className="text-th-fgd-4"></p>
</>
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
</div>
</Td>
</TrBody>
)
})}
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{volume ? `$${numberCompacter.format(volume)}` : '$0'}
</p>
</div>
</Td>
<Td>
<div className="flex items-center justify-end">
{fundingRate !== '' ? (
<Tooltip
content={
<>
{fundingRateApr ? (
<div className="">
The 1hr rate as an APR is{' '}
<span className="font-mono text-th-fgd-2">
{fundingRateApr}
</span>
</div>
) : null}
<div className="mt-2">
Funding is paid continuously. The 1hr rate
displayed is a rolling average of the past 60
mins.
</div>
<div className="mt-2">
When positive, longs will pay shorts and when
negative shorts pay longs.
</div>
</>
}
>
<p className="tooltip-underline">{fundingRate}</p>
</Tooltip>
) : (
<p></p>
)}
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
{openInterest ? (
<>
<p>
<FormatNumericValue
value={openInterest}
decimals={getDecimalCount(market.minOrderSize)}
/>
</p>
<p className="text-th-fgd-4">
$
{numberCompacter.format(
openInterest * market.uiPrice,
)}
</p>
</>
) : (
<>
<p></p>
<p className="text-th-fgd-4"></p>
</>
)}
</div>
</Td>
<Td>
<div className="flex justify-end">
<ChevronRightIcon className="h-5 w-5 text-th-fgd-3" />
</div>
</Td>
</TrBody>
)
},
)}
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-3">
{perpMarkets.map((market) => {
return (
<MobilePerpMarketItem
key={market.publicKey.toString()}
market={market}
/>
)
})}
{sortPerpMarkets(perpMarketsWithData, 'quote_volume_24h').map(
(market) => {
return (
<MobilePerpMarketItem
key={market.publicKey.toString()}
loadingMarketData={loadingMarketData}
market={market}
/>
)
},
)}
</div>
)}
</ContentBox>
@ -300,29 +258,32 @@ const PerpMarketsOverviewTable = () => {
export default PerpMarketsOverviewTable
const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
const MobilePerpMarketItem = ({
market,
loadingMarketData,
}: {
market: PerpMarketWithMarketData
loadingMarketData: boolean
}) => {
const { t } = useTranslation('common')
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const perpStats = mangoStore((s) => s.perpStats.data)
const { theme } = useThemeWrapper()
const router = useRouter()
const rate = usePerpFundingRate()
const priceHistory = market?.marketData?.price_history
const volumeData = market?.marketData?.quote_volume_24h
const volume = volumeData ? volumeData : 0
const symbol = market.name.split('-')[0]
const marketStats = getOneDayPerpStats(perpStats, market.name)
const change = marketStats.length
? ((market.uiPrice - marketStats[0].price) / marketStats[0].price) * 100
: 0
const volume = marketStats.length
? marketStats[marketStats.length - 1].cumulative_quote_volume -
marketStats[0].cumulative_quote_volume
: 0
const openInterest = market.baseLotsToUi(market.openInterest)
const isComingSoon = market.oracleLastUpdatedSlot == 0
const isUp =
priceHistory && priceHistory.length
? market.uiPrice >= priceHistory[0].price
: false
let fundingRate: string
let fundingRateApr: string
@ -360,16 +321,19 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
{isComingSoon ? <SoonBadge /> : null}
</div>
<div className="flex items-center space-x-3">
{!loadingPerpStats ? (
marketStats.length ? (
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="ml-4 h-10 w-20">
<SimpleAreaChart
color={
change >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={marketStats}
name={market.name}
xKey="date_hour"
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
data={priceHistory.concat([
{
time: new Date().toString(),
price: market.uiPrice,
},
])}
name={symbol}
xKey="time"
yKey="price"
/>
</div>
@ -379,7 +343,7 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
<Change change={change} suffix="%" />
<MarketChange market={market} />
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
@ -413,7 +377,7 @@ const MobilePerpMarketItem = ({ market }: { market: PerpMarket }) => {
{volume ? (
<span>{numberCompacter.format(volume)}</span>
) : (
''
'$0'
)}
</p>
</div>

View File

@ -1,47 +1,34 @@
import { Serum3Market } from '@blockworks-foundation/mango-v4'
import { useTranslation } from 'next-i18next'
import { useMemo } from 'react'
import { useViewport } from '../../hooks/useViewport'
import mangoStore from '@store/mangoStore'
import { COLORS } from '../../styles/colors'
import { breakpoints } from '../../utils/theme'
import ContentBox from '../shared/ContentBox'
import Change from '../shared/Change'
import MarketLogos from '@components/trade/MarketLogos'
import useMangoGroup from 'hooks/useMangoGroup'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import FormatNumericValue from '@components/shared/FormatNumericValue'
import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices'
import { floorToDecimal, getDecimalCount, numberCompacter } from 'utils/numbers'
import SimpleAreaChart from '@components/shared/SimpleAreaChart'
import { useQuery } from '@tanstack/react-query'
import { fetchSpotVolume } from '@components/trade/AdvancedMarketHeader'
import { TickerData } from 'types'
import { Disclosure, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import MarketChange from '@components/shared/MarketChange'
import useThemeWrapper from 'hooks/useThemeWrapper'
import useListedMarketsWithMarketData, {
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
import { sortSpotMarkets } from 'utils/markets'
const SpotMarketsTable = () => {
const { t } = useTranslation('common')
const { group } = useMangoGroup()
const serumMarkets = mangoStore((s) => s.serumMarkets)
const { theme } = useThemeWrapper()
const { width } = useViewport()
const showTableView = width ? width > breakpoints.md : false
const { data: birdeyePrices, isLoading: loadingPrices } =
useBirdeyeMarketPrices()
const { serumMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const { data: spotVolumeData } = useQuery(
['spot-market-volume'],
() => fetchSpotVolume(),
{
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
},
)
const loadingMarketData = isLoading || isFetching
return (
<ContentBox hideBorder hidePadding>
@ -51,16 +38,14 @@ const SpotMarketsTable = () => {
<TrHead>
<Th className="text-left">{t('market')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="hidden text-right md:block"></Th>
<Th className="text-right">{t('rolling-change')}</Th>
<Th className="hidden text-right md:block"></Th>
<Th className="text-right">{t('trade:24h-volume')}</Th>
</TrHead>
</thead>
<tbody>
{serumMarkets
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((mkt) => {
{sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map(
(mkt) => {
const baseBank = group?.getFirstBankByTokenIndex(
mkt.baseTokenIndex,
)
@ -77,25 +62,17 @@ const SpotMarketsTable = () => {
getDecimalCount(market.tickSize),
).toNumber()
}
let tickerData: TickerData | undefined
if (spotVolumeData && spotVolumeData.length) {
tickerData = spotVolumeData.find(
(m: TickerData) => m.ticker_id === mkt.name,
)
}
const birdeyeData = birdeyePrices.find(
(m) => m.mint === mkt.serumMarketExternal.toString(),
)
const priceHistory = mkt?.marketData?.price_history
const birdeyeChange =
birdeyeData && price
? ((price - birdeyeData.data[0].value) /
birdeyeData.data[0].value) *
100
: 0
const volumeData = mkt?.marketData?.quote_volume_24h
const chartData = birdeyeData ? birdeyeData.data : undefined
const volume = volumeData ? volumeData : 0
const isUp =
price && priceHistory && priceHistory.length
? price >= priceHistory[0].price
: false
return (
<TrBody key={mkt.publicKey.toString()}>
@ -127,19 +104,22 @@ const SpotMarketsTable = () => {
</div>
</Td>
<Td>
{!loadingPrices ? (
chartData !== undefined ? (
<div className="flex flex-col items-end">
<MarketChange market={mkt} />
</div>
</Td>
<Td>
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-24">
<SimpleAreaChart
color={
birdeyeChange >= 0
? COLORS.UP[theme]
: COLORS.DOWN[theme]
isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={chartData}
data={priceHistory}
name={baseBank!.name + quoteBank!.name}
xKey="unixTime"
yKey="value"
xKey="time"
yKey="price"
/>
</div>
) : baseBank?.name === 'USDC' ||
@ -152,48 +132,46 @@ const SpotMarketsTable = () => {
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
</Td>
<Td>
<div className="flex flex-col items-end">
<MarketChange market={mkt} />
</div>
</Td>
<Td>
<div className="flex flex-col text-right">
<p>
{tickerData ? (
{volume ? (
<span>
{numberCompacter.format(
parseFloat(tickerData.target_volume),
)}{' '}
{numberCompacter.format(volume)}{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
</span>
) : (
''
<span>
0{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
</span>
)}
</p>
</div>
</Td>
</TrBody>
)
})}
},
)}
</tbody>
</Table>
) : (
<div className="border-b border-th-bkg-3">
{serumMarkets
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((market) => {
{sortSpotMarkets(serumMarketsWithData, 'quote_volume_24h').map(
(market) => {
return (
<MobileSpotMarketItem
key={market.publicKey.toString()}
loadingMarketData={loadingMarketData}
market={market}
spotVolumeData={spotVolumeData}
/>
)
})}
},
)}
</div>
)}
</ContentBox>
@ -204,14 +182,12 @@ export default SpotMarketsTable
const MobileSpotMarketItem = ({
market,
spotVolumeData,
loadingMarketData,
}: {
market: Serum3Market
spotVolumeData: TickerData[] | undefined
market: SerumMarketWithMarketData
loadingMarketData: boolean
}) => {
const { t } = useTranslation('common')
const { data: birdeyePrices, isLoading: loadingPrices } =
useBirdeyeMarketPrices()
const { group } = useMangoGroup()
const { theme } = useThemeWrapper()
const baseBank = group?.getFirstBankByTokenIndex(market.baseTokenIndex)
@ -226,37 +202,16 @@ const MobileSpotMarketItem = ({
).toNumber()
}, [baseBank, quoteBank, serumMarket])
const birdeyeData = useMemo(() => {
if (!loadingPrices) {
return birdeyePrices.find(
(m) => m.mint === market.serumMarketExternal.toString(),
)
}
return null
}, [loadingPrices])
const priceHistory = market?.marketData?.price_history
const change = useMemo(() => {
if (birdeyeData && price) {
return (
((price - birdeyeData.data[0].value) / birdeyeData.data[0].value) * 100
)
}
return 0
}, [birdeyeData, price])
const volueData = market?.marketData?.quote_volume_24h
const chartData = useMemo(() => {
if (birdeyeData) {
return birdeyeData.data
}
return undefined
}, [birdeyeData])
const volume = volueData ? volueData : 0
let tickerData: TickerData | undefined
if (spotVolumeData && spotVolumeData.length) {
tickerData = spotVolumeData.find(
(m: TickerData) => m.ticker_id === market.name,
)
}
const isUp =
price && priceHistory && priceHistory.length
? price >= priceHistory[0].price
: false
return (
<Disclosure>
@ -273,17 +228,15 @@ const MobileSpotMarketItem = ({
<p className="leading-none text-th-fgd-1">{market.name}</p>
</div>
<div className="flex items-center space-x-3">
{!loadingPrices ? (
chartData !== undefined ? (
{!loadingMarketData ? (
priceHistory && priceHistory.length ? (
<div className="h-10 w-20">
<SimpleAreaChart
color={
change >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]
}
data={chartData}
color={isUp ? COLORS.UP[theme] : COLORS.DOWN[theme]}
data={priceHistory}
name={baseBank!.name + quoteBank!.name}
xKey="unixTime"
yKey="value"
xKey="time"
yKey="price"
/>
</div>
) : (
@ -292,7 +245,7 @@ const MobileSpotMarketItem = ({
) : (
<div className="h-10 w-[104px] animate-pulse rounded bg-th-bkg-3" />
)}
<Change change={change} suffix="%" />
<MarketChange market={market} />
<ChevronDownIcon
className={`${
open ? 'rotate-180' : 'rotate-360'
@ -333,17 +286,20 @@ const MobileSpotMarketItem = ({
{t('trade:24h-volume')}
</p>
<p className="font-mono text-th-fgd-2">
{tickerData ? (
{volume ? (
<span>
{numberCompacter.format(
parseFloat(tickerData.target_volume),
)}{' '}
{numberCompacter.format(volume)}{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
</span>
) : (
''
<span>
0{' '}
<span className="font-body text-th-fgd-4">
{quoteBank?.name}
</span>
</span>
)}
</p>
</div>

View File

@ -4,14 +4,18 @@ import { useEffect, useMemo, useState } from 'react'
import { floorToDecimal } from 'utils/numbers'
import { useTokenMax } from './useTokenMax'
const DEFAULT_VALUES = ['10', '25', '50', '75', '100']
const PercentageSelectButtons = ({
amountIn,
setAmountIn,
useMargin,
values,
}: {
amountIn: string
setAmountIn: (x: string) => void
useMargin: boolean
values?: string[]
}) => {
const [sizePercentage, setSizePercentage] = useState('')
const {
@ -49,7 +53,7 @@ const PercentageSelectButtons = ({
<ButtonGroup
activeValue={sizePercentage}
onChange={(p) => handleSizePercentage(p)}
values={['10', '25', '50', '75', '100']}
values={values || DEFAULT_VALUES}
unit="%"
/>
</div>

View File

@ -28,6 +28,7 @@ const fetchJupiterRoutes = async (
slippage = 50,
swapMode = 'ExactIn',
feeBps = 0,
onlyDirectRoutes = true,
) => {
{
const paramsString = new URLSearchParams({
@ -37,15 +38,14 @@ const fetchJupiterRoutes = async (
slippageBps: Math.ceil(slippage * 100).toString(),
feeBps: feeBps.toString(),
swapMode,
onlyDirectRoutes: `${onlyDirectRoutes}`,
}).toString()
const response = await fetch(
`https://quote-api.jup.ag/v4/quote?${paramsString}`,
)
const res = await response.json()
const data = res.data
return {
routes: res.data as RouteInfo[],
bestRoute: (data.length ? data[0] : null) as RouteInfo | null,
@ -119,6 +119,7 @@ export const handleGetRoutes = async (
feeBps = 0,
wallet: string | undefined | null,
mode: SwapModes = 'ALL',
jupiterOnlyDirectRoutes = false,
) => {
try {
wallet ||= PublicKey.default.toBase58()
@ -138,6 +139,7 @@ export const handleGetRoutes = async (
slippage,
swapMode,
feeBps,
jupiterOnlyDirectRoutes,
)
const routes = []

View File

@ -1,36 +1,23 @@
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { IconButton, LinkButton } from '@components/shared/Button'
import { getOneDayPerpStats } from '@components/stats/PerpMarketsOverviewTable'
import { ChartBarIcon, InformationCircleIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import { useEffect, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import { numberCompacter } from 'utils/numbers'
import MarketSelectDropdown from './MarketSelectDropdown'
import PerpFundingRate from './PerpFundingRate'
import SheenLoader from '@components/shared/SheenLoader'
import PerpMarketDetailsModal from '@components/modals/PerpMarketDetailsModal'
import useMangoGroup from 'hooks/useMangoGroup'
import OraclePrice from './OraclePrice'
import SpotMarketDetailsModal from '@components/modals/SpotMarketDetailsModal'
import { useQuery } from '@tanstack/react-query'
import { MANGO_DATA_OPENBOOK_URL } from 'utils/constants'
import { TickerData } from 'types'
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'
export const fetchSpotVolume = async () => {
try {
const data = await fetch(`${MANGO_DATA_OPENBOOK_URL}/coingecko/tickers`)
const res = await data.json()
return res
} catch (e) {
console.log('Failed to fetch spot volume data', e)
}
}
import useMarketsData from 'hooks/useMarketsData'
const AdvancedMarketHeader = ({
showChart,
@ -40,58 +27,31 @@ const AdvancedMarketHeader = ({
setShowChart?: (x: boolean) => void
}) => {
const { t } = useTranslation(['common', 'trade'])
const perpStats = mangoStore((s) => s.perpStats.data)
const { serumOrPerpMarket, selectedMarket } = useSelectedMarket()
const selectedMarketName = mangoStore((s) => s.selectedMarket.name)
const [showMarketDetails, setShowMarketDetails] = useState(false)
const { group } = useMangoGroup()
const { width } = useViewport()
const isMobile = width ? width < breakpoints.md : false
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const {
data: spotVolumeData,
isLoading: loadingSpotVolume,
isFetching: fetchingSpotVolume,
} = useQuery(['spot-volume', selectedMarketName], () => fetchSpotVolume(), {
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: selectedMarket instanceof Serum3Market,
})
const spotMarketVolume = useMemo(() => {
if (!spotVolumeData || !spotVolumeData.length) return
return spotVolumeData.find(
(mkt: TickerData) => mkt.ticker_id === selectedMarketName,
)
}, [selectedMarketName, spotVolumeData])
useEffect(() => {
if (group) {
const actions = mangoStore.getState().actions
actions.fetchPerpStats()
const volume = useMemo(() => {
if (!selectedMarketName || !serumOrPerpMarket || !marketsData) return 0
if (serumOrPerpMarket instanceof PerpMarket) {
const perpData: MarketData = marketsData?.perpData
const perpEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === selectedMarketName.toLowerCase(),
)
return perpEntries ? perpEntries[1][0]?.quote_volume_24h : 0
} else {
const spotData: MarketData = marketsData?.spotData
const spotEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === selectedMarketName.toLowerCase(),
)
return spotEntries ? spotEntries[1][0]?.quote_volume_24h : 0
}
}, [group])
}, [marketsData, selectedMarketName, serumOrPerpMarket])
const oneDayPerpStats = useMemo(() => {
if (
!perpStats ||
!perpStats.length ||
!selectedMarketName ||
!selectedMarketName.includes('PERP')
)
return []
return getOneDayPerpStats(perpStats, selectedMarketName)
}, [perpStats, selectedMarketName])
const perpVolume = useMemo(() => {
if (!oneDayPerpStats.length) return
return (
oneDayPerpStats[oneDayPerpStats.length - 1].cumulative_quote_volume -
oneDayPerpStats[0].cumulative_quote_volume
)
}, [oneDayPerpStats])
const loadingVolume = isLoading || isFetching
return (
<>
@ -118,12 +78,16 @@ const AdvancedMarketHeader = ({
<div className="mb-0.5 text-th-fgd-4 ">
{t('trade:24h-volume')}
</div>
{perpVolume ? (
{loadingVolume ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : volume ? (
<span className="font-mono">
${numberCompacter.format(perpVolume)}{' '}
${numberCompacter.format(volume)}
</span>
) : (
'-'
<span className="font-mono">$0</span>
)}
</div>
<PerpFundingRate />
@ -155,21 +119,24 @@ const AdvancedMarketHeader = ({
<div className="mb-0.5 text-th-fgd-4 ">
{t('trade:24h-volume')}
</div>
{!loadingSpotVolume && !fetchingSpotVolume ? (
spotMarketVolume ? (
<span className="font-mono">
{numberCompacter.format(spotMarketVolume.target_volume)}{' '}
<span className="font-body text-th-fgd-3">
{selectedMarketName?.split('/')[1]}
</span>
</span>
) : (
'-'
)
) : (
{loadingVolume ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : volume ? (
<span className="font-mono">
{numberCompacter.format(volume)}{' '}
<span className="font-body text-th-fgd-3">
{selectedMarketName?.split('/')[1]}
</span>
</span>
) : (
<span className="font-mono">
0{' '}
<span className="font-body text-th-fgd-3">
{selectedMarketName?.split('/')[1]}
</span>
</span>
)}
</div>
)}

View File

@ -510,9 +510,7 @@ const AdvancedTradeForm = () => {
</div>
{tradeForm.tradeType === 'Market' &&
selectedMarket instanceof Serum3Market ? (
<>
<SpotMarketOrderSwapForm />
</>
<SpotMarketOrderSwapForm />
) : (
<>
<form onSubmit={(e) => handleSubmit(e)}>

View File

@ -28,6 +28,8 @@ const GroupSize = ({
formatSize(10),
formatSize(50),
formatSize(100),
formatSize(500),
formatSize(1000),
],
[tickSize],
)

View File

@ -1,7 +1,6 @@
import FavoriteMarketButton from '@components/shared/FavoriteMarketButton'
import { Popover } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import mangoStore from '@store/mangoStore'
import useMangoGroup from 'hooks/useMangoGroup'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
@ -12,6 +11,7 @@ import {
formatCurrencyValue,
formatNumericValue,
getDecimalCount,
numberCompacter,
} from 'utils/numbers'
import MarketLogos from './MarketLogos'
import SoonBadge from '@components/shared/SoonBadge'
@ -19,15 +19,23 @@ import TabButtons from '@components/shared/TabButtons'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import Loading from '@components/shared/Loading'
import MarketChange from '@components/shared/MarketChange'
const MARKET_LINK_WRAPPER_CLASSES =
'flex items-center justify-between px-4 md:pl-6 md:pr-4'
import SheenLoader from '@components/shared/SheenLoader'
// import Select from '@components/forms/Select'
import useListedMarketsWithMarketData from 'hooks/useListedMarketsWithMarketData'
import { AllowedKeys, sortPerpMarkets, sortSpotMarkets } from 'utils/markets'
const MARKET_LINK_CLASSES =
'mr-1 -ml-3 flex w-full items-center justify-between rounded-md py-2 px-3 focus:outline-none focus-visible:text-th-active md:hover:cursor-pointer md:hover:bg-th-bkg-3 md:hover:text-th-fgd-1'
'grid grid-cols-3 md:grid-cols-4 flex items-center w-full py-2 px-4 rounded-r-md focus:outline-none focus-visible:text-th-active md:hover:cursor-pointer md:hover:bg-th-bkg-3 md:hover:text-th-fgd-1'
const MARKET_LINK_DISABLED_CLASSES =
'mr-2 -ml-3 flex w-full items-center justify-between rounded-md py-2 px-3 md:hover:cursor-not-allowed'
'flex w-full items-center justify-between py-2 px-4 md:hover:cursor-not-allowed'
// const SORT_KEYS = [
// 'quote_volume_24h',
// 'quote_volume_1h',
// 'change_24h',
// 'change_1h',
// ]
const MarketSelectDropdown = () => {
const { t } = useTranslation('common')
@ -35,27 +43,21 @@ const MarketSelectDropdown = () => {
const [spotOrPerp, setSpotOrPerp] = useState(
selectedMarket instanceof PerpMarket ? 'perp' : 'spot',
)
const serumMarkets = mangoStore((s) => s.serumMarkets)
const allPerpMarkets = mangoStore((s) => s.perpMarkets)
const [sortByKey] = useState<AllowedKeys>('quote_volume_24h')
const { group } = useMangoGroup()
const [spotBaseFilter, setSpotBaseFilter] = useState('All')
const { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching } =
useListedMarketsWithMarketData()
const perpMarkets = useMemo(() => {
return allPerpMarkets
.filter(
(p) =>
p.publicKey.toString() !==
'9Y8paZ5wUpzLFfQuHz8j2RtPrKsDtHx9sbgFmWb5abCw',
)
.sort((a, b) =>
a.oracleLastUpdatedSlot == 0 ? -1 : a.name.localeCompare(b.name),
)
}, [allPerpMarkets])
const perpMarketsToShow = useMemo(() => {
if (!perpMarketsWithData.length) return []
return sortPerpMarkets(perpMarketsWithData, sortByKey)
}, [perpMarketsWithData, sortByKey])
const spotBaseTokens: string[] = useMemo(() => {
if (serumMarkets.length) {
if (serumMarketsWithData.length) {
const baseTokens: string[] = ['All']
serumMarkets.map((m) => {
serumMarketsWithData.map((m) => {
const base = m.name.split('/')[1]
if (!baseTokens.includes(base)) {
baseTokens.push(base)
@ -64,19 +66,23 @@ const MarketSelectDropdown = () => {
return baseTokens.sort((a, b) => a.localeCompare(b))
}
return ['All']
}, [serumMarkets])
}, [serumMarketsWithData])
const serumMarketsToShow = useMemo(() => {
if (!serumMarkets || !serumMarkets.length) return []
if (!serumMarketsWithData.length) return []
if (spotBaseFilter !== 'All') {
return serumMarkets.filter((m) => {
const filteredMarkets = serumMarketsWithData.filter((m) => {
const base = m.name.split('/')[1]
return base === spotBaseFilter
})
return sortSpotMarkets(filteredMarkets, sortByKey)
} else {
return serumMarkets
return sortSpotMarkets(serumMarketsWithData, sortByKey)
}
}, [serumMarkets, spotBaseFilter])
}, [serumMarketsWithData, sortByKey, spotBaseFilter])
const loadingMarketData = isLoading || isFetching
return (
<Popover>
@ -107,7 +113,7 @@ const MarketSelectDropdown = () => {
} mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-2`}
/>
</Popover.Button>
<Popover.Panel className="absolute -left-4 top-12 z-40 mr-4 w-screen rounded-none border-y border-r border-th-bkg-3 bg-th-bkg-2 md:-left-6 md:w-[420px] md:rounded-br-md">
<Popover.Panel className="absolute top-12 z-40 w-screen border-y md:border-r border-th-bkg-3 bg-th-bkg-2 -left-4 md:w-[560px]">
<div className="border-b border-th-bkg-3">
<TabButtons
activeValue={spotOrPerp}
@ -119,15 +125,28 @@ const MarketSelectDropdown = () => {
fillWidth
/>
</div>
<div className="py-3">
{spotOrPerp === 'perp' && perpMarkets?.length
? perpMarkets.map((m) => {
<div className="py-3 max-h-[calc(100vh-160px)] thin-scroll overflow-auto">
{spotOrPerp === 'perp' && perpMarketsToShow.length ? (
<>
<div className="grid grid-cols-3 md:grid-cols-4 pl-4 pr-14 text-xxs border-b border-th-bkg-3 pb-1 mb-2">
<p className="col-span-1">{t('market')}</p>
<p className="col-span-1 text-right">{t('price')}</p>
<p className="col-span-1 text-right">
{t('rolling-change')}
</p>
<p className="col-span-1 text-right hidden md:block">
{t('daily-volume')}
</p>
</div>
{perpMarketsToShow.map((m) => {
const isComingSoon = m.oracleLastUpdatedSlot == 0
const volumeData = m?.marketData?.quote_volume_24h
const volume = volumeData ? volumeData : 0
return (
<div
className={MARKET_LINK_WRAPPER_CLASSES}
key={m.publicKey.toString()}
>
<div className="flex items-center w-full" key={m.name}>
{!isComingSoon ? (
<>
<Link
@ -141,116 +160,196 @@ const MarketSelectDropdown = () => {
}}
shallow={true}
>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="text-th-fgd-2">{m.name}</span>
<div className="col-span-1 flex items-center">
<MarketLogos market={m} size="small" />
<span className="text-th-fgd-2 text-xs">
{m.name}
</span>
</div>
<div className="flex items-center">
<span className="mr-3 font-mono text-xs text-th-fgd-2">
<div className="col-span-1 flex justify-end">
<span className="font-mono text-xs text-th-fgd-2">
{formatCurrencyValue(
m.uiPrice,
getDecimalCount(m.tickSize),
)}
</span>
</div>
<div className="col-span-1 flex justify-end">
<MarketChange market={m} size="small" />
</div>
<div className="col-span-1 md:flex justify-end hidden">
{loadingMarketData ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : (
<span>
{volume ? (
<span className="font-mono text-xs text-th-fgd-2">
${numberCompacter.format(volume)}
</span>
) : (
<span className="font-mono text-xs text-th-fgd-2">
$0
</span>
)}
</span>
)}
</div>
</Link>
<FavoriteMarketButton market={m} />
<div className="px-3">
<FavoriteMarketButton market={m} />
</div>
</>
) : (
<span className={MARKET_LINK_DISABLED_CLASSES}>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="mr-2">{m.name}</span>
<MarketLogos market={m} size="small" />
<span className="mr-2 text-xs">{m.name}</span>
<SoonBadge />
</div>
</span>
)}
</div>
)
})
: null}
{spotOrPerp === 'spot' && serumMarkets?.length ? (
})}
</>
) : null}
{spotOrPerp === 'spot' && serumMarketsToShow.length ? (
<>
<div className="mb-3 px-4 md:px-6">
{spotBaseTokens.map((tab) => (
<button
className={`rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
spotBaseFilter === tab
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
}`}
onClick={() => setSpotBaseFilter(tab)}
key={tab}
>
{t(tab)}
</button>
))}
</div>
{serumMarketsToShow
.map((x) => x)
.sort((a, b) => a.name.localeCompare(b.name))
.map((m) => {
const baseBank = group?.getFirstBankByTokenIndex(
m.baseTokenIndex,
)
const quoteBank = group?.getFirstBankByTokenIndex(
m.quoteTokenIndex,
)
const market = group?.getSerum3ExternalMarket(
m.serumMarketExternal,
)
let price
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
}
return (
<div
className={MARKET_LINK_WRAPPER_CLASSES}
key={m.publicKey.toString()}
<div className="flex items-center justify-between mb-3 px-4">
<div>
{spotBaseTokens.map((tab) => (
<button
className={`rounded-md py-1.5 px-2.5 text-sm font-medium focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1 ${
spotBaseFilter === tab
? 'bg-th-bkg-3 text-th-active md:hover:text-th-active'
: 'text-th-fgd-3 md:hover:text-th-fgd-2'
}`}
onClick={() => setSpotBaseFilter(tab)}
key={tab}
>
<Link
className={MARKET_LINK_CLASSES}
href={{
pathname: '/trade',
query: { name: m.name },
}}
onClick={() => {
close()
}}
shallow={true}
>
<div className="flex items-center">
<MarketLogos market={m} />
<span className="text-th-fgd-2">{m.name}</span>
</div>
<div className="flex items-center">
{price && market?.tickSize ? (
<span className="mr-3 font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{getDecimalCount(market.tickSize) <= 6
? formatNumericValue(
price,
getDecimalCount(market.tickSize),
)
: price.toExponential(3)}{' '}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{quoteBank?.name}
</span>
) : null}
</span>
) : null}
<MarketChange market={m} size="small" />
</div>
</Link>
{t(tab)}
</button>
))}
</div>
{/* need to sort out change before enabling more sorting options */}
{/* <div>
<Select
value={sortByKey}
onChange={(sortBy) => setSortByKey(sortBy)}
className="w-full"
>
{SORT_KEYS.map((sortBy) => {
return (
<Select.Option key={sortBy} value={sortBy}>
<div className="flex w-full items-center justify-between">
{sortBy}
</div>
</Select.Option>
)
})}
</Select>
</div> */}
</div>
<div className="grid grid-cols-3 md:grid-cols-4 pl-4 pr-14 text-xxs border-b border-th-bkg-3 pb-1 mb-2">
<p className="col-span-1">{t('market')}</p>
<p className="col-span-1 text-right">{t('price')}</p>
<p className="col-span-1 text-right">
{t('rolling-change')}
</p>
<p className="col-span-1 text-right hidden md:block">
{t('daily-volume')}
</p>
</div>
{serumMarketsToShow.map((m) => {
const baseBank = group?.getFirstBankByTokenIndex(
m.baseTokenIndex,
)
const quoteBank = group?.getFirstBankByTokenIndex(
m.quoteTokenIndex,
)
const market = group?.getSerum3ExternalMarket(
m.serumMarketExternal,
)
let price
if (baseBank && market && quoteBank) {
price = floorToDecimal(
baseBank.uiPrice / quoteBank.uiPrice,
getDecimalCount(market.tickSize),
).toNumber()
}
const volumeData = m?.marketData?.quote_volume_24h
const volume = volumeData ? volumeData : 0
return (
<div className="flex items-center w-full" key={m.name}>
<Link
className={MARKET_LINK_CLASSES}
href={{
pathname: '/trade',
query: { name: m.name },
}}
onClick={() => {
close()
}}
shallow={true}
>
<div className="col-span-1 flex items-center">
<MarketLogos market={m} size="small" />
<span className="text-th-fgd-2 text-xs">
{m.name}
</span>
</div>
<div className="col-span-1 flex justify-end">
{price && market?.tickSize ? (
<span className="font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{getDecimalCount(market.tickSize) <= 6
? formatNumericValue(
price,
getDecimalCount(market.tickSize),
)
: price.toExponential(3)}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{' '}
{quoteBank?.name}
</span>
) : null}
</span>
) : null}
</div>
<div className="col-span-1 flex justify-end">
<MarketChange market={m} size="small" />
</div>
<div className="col-span-1 md:flex justify-end hidden">
{loadingMarketData ? (
<SheenLoader className="mt-0.5">
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : (
<span className="font-mono text-xs text-th-fgd-2">
{quoteBank?.name === 'USDC' ? '$' : ''}
{volume ? numberCompacter.format(volume) : 0}
{quoteBank?.name !== 'USDC' ? (
<span className="font-body text-th-fgd-3">
{' '}
{quoteBank?.name}
</span>
) : null}
</span>
)}
</div>
</Link>
<div className="px-3">
<FavoriteMarketButton market={m} />
</div>
)
})}
</div>
)
})}
</>
) : null}
</div>

View File

@ -0,0 +1,61 @@
import MaxAmountButton from '@components/shared/MaxAmountButton'
import { useTokenMax } from '@components/swap/useTokenMax'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useTranslation } from 'next-i18next'
import { useCallback, useMemo } from 'react'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
const MaxMarketSwapAmount = ({
setAmountIn,
useMargin,
}: {
setAmountIn: (x: string) => void
useMargin: boolean
}) => {
const { t } = useTranslation('common')
const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket()
const mangoAccountLoading = mangoStore((s) => s.mangoAccount.initialLoad)
const {
amount: tokenMax,
amountWithBorrow,
decimals,
} = useTokenMax(useMargin)
const tickDecimals = useMemo(() => {
if (!serumOrPerpMarket) return decimals
return getDecimalCount(serumOrPerpMarket.tickSize)
}, [decimals, serumOrPerpMarket])
const maxAmount = useMemo(() => {
const balanceMax = useMargin ? amountWithBorrow : tokenMax
const { side } = mangoStore.getState().tradeForm
const sideMax =
side === 'buy' ? balanceMax.toNumber() / oraclePrice : balanceMax
return floorToDecimal(sideMax, tickDecimals).toFixed()
}, [amountWithBorrow, oraclePrice, tickDecimals, tokenMax, useMargin])
const setMax = useCallback(() => {
const { side } = mangoStore.getState().tradeForm
const max = useMargin ? amountWithBorrow : tokenMax
const maxDecimals = side === 'buy' ? tickDecimals : decimals
setAmountIn(floorToDecimal(max, maxDecimals).toFixed())
}, [decimals, setAmountIn, tickDecimals, useMargin])
if (mangoAccountLoading) return null
return (
<div className="mb-2 mt-3 flex items-center justify-between w-full">
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
<MaxAmountButton
className="text-xs"
decimals={decimals}
label={t('max')}
onClick={() => setMax()}
value={maxAmount}
/>
</div>
)
}
export default MaxMarketSwapAmount

View File

@ -10,7 +10,6 @@ import {
} from 'utils/numbers'
import {
ANIMATION_SETTINGS_KEY,
DEPTH_CHART_KEY,
// USE_ORDERBOOK_FEED_KEY,
} from 'utils/constants'
import { useTranslation } from 'next-i18next'
@ -22,7 +21,6 @@ import { BookSide, Serum3Market } from '@blockworks-foundation/mango-v4'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
import Switch from '@components/forms/Switch'
import { breakpoints } from 'utils/theme'
import {
decodeBook,
@ -55,12 +53,7 @@ const Orderbook = () => {
// ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
// : true
// )
const [showDepthChart, setShowDepthChart] = useLocalStorageState<boolean>(
DEPTH_CHART_KEY,
false,
)
const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
const currentOrderbookData = useRef<OrderbookL2>()
@ -430,19 +423,10 @@ const Orderbook = () => {
return (
<div>
<div className="flex h-10 items-center justify-between border-b border-th-bkg-3 px-4">
{!isMobile ? (
<Switch
checked={showDepthChart}
onChange={() => setShowDepthChart(!showDepthChart)}
small
>
<span className="text-xxs">{t('trade:depth')}</span>
</Switch>
) : null}
<div className="h-10 flex items-center justify-between border-b border-th-bkg-3 px-4">
{market ? (
<>
<p className="text-xs lg:hidden">{t('trade:grouping')}:</p>
<p className="text-xs">{t('trade:grouping')}:</p>
<div id="trade-step-four">
<Tooltip
className="hidden md:block"
@ -485,6 +469,9 @@ const Orderbook = () => {
size={orderbookData?.asks[index].size}
side="sell"
sizePercent={orderbookData?.asks[index].sizePercent}
averagePrice={orderbookData?.asks[index].averagePrice}
cumulativeValue={orderbookData?.asks[index].cumulativeValue}
cumulativeSize={orderbookData?.asks[index].cumulativeSize}
cumulativeSizePercent={
orderbookData?.asks[index].cumulativeSizePercent
}
@ -526,6 +513,9 @@ const Orderbook = () => {
size={orderbookData?.bids[index].size}
side="buy"
sizePercent={orderbookData?.bids[index].sizePercent}
averagePrice={orderbookData?.bids[index].averagePrice}
cumulativeValue={orderbookData?.bids[index].cumulativeValue}
cumulativeSize={orderbookData?.bids[index].cumulativeSize}
cumulativeSizePercent={
orderbookData?.bids[index].cumulativeSizePercent
}
@ -547,6 +537,9 @@ const OrderbookRow = ({
// invert,
hasOpenOrder,
minOrderSize,
averagePrice,
cumulativeValue,
cumulativeSize,
cumulativeSizePercent,
tickSize,
grouping,
@ -555,6 +548,9 @@ const OrderbookRow = ({
price: number
size: number
sizePercent: number
averagePrice: number
cumulativeValue: number
cumulativeSize: number
cumulativeSizePercent: number
hasOpenOrder: boolean
// invert: boolean
@ -632,12 +628,35 @@ const OrderbookRow = ({
[minOrderSize],
)
const handleMouseOver = useCallback(() => {
const { set } = mangoStore.getState()
if (averagePrice && cumulativeSize && cumulativeValue) {
set((state) => {
state.orderbookTooltip = {
averagePrice,
cumulativeSize,
cumulativeValue,
side,
}
})
}
}, [averagePrice, cumulativeSize, cumulativeValue])
const handleMouseLeave = useCallback(() => {
const { set } = mangoStore.getState()
set((state) => {
state.orderbookTooltip = undefined
})
}, [])
if (!minOrderSize) return null
return (
<div
className={`relative flex h-[20px] cursor-pointer justify-between border-b border-b-th-bkg-1 text-sm`}
ref={element}
onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave}
>
<>
<div className="flex h-full w-full items-center justify-between text-th-fgd-3 hover:bg-th-bkg-2">

View File

@ -1,12 +1,8 @@
import TabButtons from '@components/shared/TabButtons'
import { useEffect, useMemo, useState } from 'react'
import { useState } from 'react'
import Orderbook from './Orderbook'
import RecentTrades from './RecentTrades'
import DepthChart from './DepthChart'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { DEPTH_CHART_KEY } from 'utils/constants'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
export const TABS: [string, number][] = [
['trade:book', 0],
@ -16,45 +12,24 @@ export const TABS: [string, number][] = [
const OrderbookAndTrades = () => {
const [activeTab, setActiveTab] = useState('trade:book')
const [showDepthChart] = useLocalStorageState<boolean>(DEPTH_CHART_KEY, false)
const { width } = useViewport()
const hideDepthTab = width ? width > breakpoints.lg : false
const tabsToShow = useMemo(() => {
if (hideDepthTab) {
return TABS.filter((t) => !t[0].includes('depth'))
}
return TABS
}, [hideDepthTab])
useEffect(() => {
if (hideDepthTab && activeTab === 'trade:depth') {
setActiveTab('trade:book')
}
}, [activeTab, hideDepthTab])
return (
<div className="hide-scroll h-full">
<div className="h-full">
<div className="hide-scroll overflow-x-auto border-b border-th-bkg-3">
<TabButtons
activeValue={activeTab}
onChange={(tab: string) => setActiveTab(tab)}
values={tabsToShow}
fillWidth={!showDepthChart || !hideDepthTab}
values={TABS}
fillWidth
showBorders
/>
</div>
<div
className={`flex ${activeTab === 'trade:book' ? 'visible' : 'hidden'}`}
className={`h-full ${
activeTab === 'trade:book' ? 'visible' : 'hidden'
}`}
>
{showDepthChart ? (
<div className="hidden w-1/2 border-r border-th-bkg-3 lg:block">
<DepthChart />
</div>
) : null}
<div className={showDepthChart ? 'w-full lg:w-1/2' : 'w-full'}>
<Orderbook />
</div>
<Orderbook />
</div>
<div
className={`h-full ${

View File

@ -0,0 +1,50 @@
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
const OrderbookTooltip = () => {
const { t } = useTranslation(['common', 'trade'])
const orderbookTooltip = mangoStore((s) => s.orderbookTooltip)
const { serumOrPerpMarket, baseSymbol, quoteSymbol } = useSelectedMarket()
const [minOrderDecimals, tickDecimals] = useMemo(() => {
if (!serumOrPerpMarket) return [0, 0]
return [
getDecimalCount(serumOrPerpMarket.minOrderSize),
getDecimalCount(serumOrPerpMarket.tickSize),
]
}, [serumOrPerpMarket])
if (!orderbookTooltip) return null
const { averagePrice, cumulativeSize, cumulativeValue, side } =
orderbookTooltip
const isBid = side === 'buy'
const isPerp = serumOrPerpMarket instanceof PerpMarket
return (
<div
className={`absolute max-w-[75%] w-full top-4 left-1/2 -translate-x-1/2 p-3 rounded-md bg-th-bkg-1 border text-center ${
isBid ? 'border-th-up' : 'border-th-down'
}`}
>
<p>
<span className={isBid ? 'text-th-up' : 'text-th-down'}>{t(side)}</span>
{` ${formatNumericValue(cumulativeSize, minOrderDecimals)} ${
isPerp ? '' : baseSymbol
} ${t('trade:for')} ${isPerp ? '$' : ''}${formatNumericValue(
cumulativeValue,
tickDecimals,
)} ${isPerp ? '' : quoteSymbol} ${t('trade:average-price-of')} ${
isPerp ? '$' : ''
}${formatNumericValue(averagePrice, tickDecimals)} ${
isPerp ? '' : quoteSymbol
}`}
</p>
</div>
)
}
export default OrderbookTooltip

View File

@ -4,6 +4,7 @@ import NumberFormat, {
SourceInfo,
} from 'react-number-format'
import {
DEFAULT_CHECKBOX_SETTINGS,
INPUT_PREFIX_CLASSNAMES,
INPUT_SUFFIX_CLASSNAMES,
} from './AdvancedTradeForm'
@ -29,12 +30,18 @@ import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
import SwapSlider from '@components/swap/SwapSlider'
import PercentageSelectButtons from '@components/swap/PercentageSelectButtons'
import { SIZE_INPUT_UI_KEY } from 'utils/constants'
import { SIZE_INPUT_UI_KEY, TRADE_CHECKBOXES_KEY } from 'utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
import MaxSwapAmount from '@components/swap/MaxSwapAmount'
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 { 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'
const set = mangoStore.getState().set
const slippage = 100
@ -49,12 +56,14 @@ function stringToNumberOrZero(s: string): number {
export default function SpotMarketOrderSwapForm() {
const { t } = useTranslation()
const { baseSize, price, quoteSize, side } = mangoStore((s) => s.tradeForm)
const { baseSize, quoteSize, side } = mangoStore((s) => s.tradeForm)
const { isUnownedAccount } = useUnownedAccount()
const [placingOrder, setPlacingOrder] = useState(false)
const { ipAllowed, ipCountry } = useIpAddress()
const { connected, publicKey, connect } = useWallet()
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [savedCheckboxSettings, setSavedCheckboxSettings] =
useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS)
const {
selectedMarket,
price: oraclePrice,
@ -64,6 +73,9 @@ export default function SpotMarketOrderSwapForm() {
quoteSymbol,
serumOrPerpMarket,
} = useSelectedMarket()
const { amount: tokenMax, amountWithBorrow } = useTokenMax(
savedCheckboxSettings.margin,
)
const handleBaseSizeChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
@ -274,28 +286,76 @@ export default function SpotMarketOrderSwapForm() {
: Math.trunc(simulatedHealthRatio)
}, [inputBank, outputBank, baseSize, quoteSize, side])
const [balance, borrowAmount] = useMemo(() => {
if (!inputBank) return [0, 0]
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount) return [0, 0]
let borrowAmount
const balance = mangoAccount.getTokenDepositsUi(inputBank)
if (side === 'buy') {
const remainingBalance = balance - parseFloat(quoteSize)
borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
} else {
const remainingBalance = balance - parseFloat(baseSize)
borrowAmount = remainingBalance < 0 ? Math.abs(remainingBalance) : 0
}
return [balance, borrowAmount]
}, [baseSize, inputBank, quoteSize])
const orderValue = useMemo(() => {
if (
!inputBank ||
!outputBank ||
!oraclePrice ||
!baseSize ||
isNaN(parseFloat(baseSize))
)
return 0
const quotePriceDecimal =
side === 'buy'
? new Decimal(inputBank.uiPrice)
: new Decimal(outputBank.uiPrice)
const basePriceDecimal = new Decimal(oraclePrice)
const sizeDecimal = new Decimal(baseSize)
return floorToDecimal(
basePriceDecimal.mul(quotePriceDecimal).mul(sizeDecimal),
2,
)
}, [baseSize, inputBank, outputBank, oraclePrice, side])
const tooMuchSize = useMemo(() => {
if (!baseSize || !quoteSize || !amountWithBorrow || !tokenMax) return false
const size = side === 'buy' ? new Decimal(quoteSize) : new Decimal(baseSize)
const useMargin = savedCheckboxSettings.margin
return useMargin ? size.gt(amountWithBorrow) : size.gt(tokenMax)
}, [
amountWithBorrow,
baseSize,
quoteSize,
side,
tokenMax,
savedCheckboxSettings.margin,
])
const disabled =
(connected && (!baseSize || !price)) ||
(connected && (!baseSize || !oraclePrice)) ||
!serumOrPerpMarket ||
parseFloat(baseSize) < serumOrPerpMarket.minOrderSize ||
isLoading
const useMargin = true
isLoading ||
tooMuchSize
return (
<>
<form onSubmit={(e) => handleSubmit(e)}>
<div className="mt-3 px-3 md:px-4">
<div className="mb-2 flex items-end justify-end">
{!isUnownedAccount ? (
<>
<MaxSwapAmount
useMargin={useMargin}
setAmountIn={setAmountFromSlider}
/>
</>
) : null}
</div>
{!isUnownedAccount ? (
<MaxMarketSwapAmount
useMargin={savedCheckboxSettings.margin}
setAmountIn={setAmountFromSlider}
/>
) : null}
<div className="flex flex-col">
<div className="relative">
<NumberFormat
@ -363,124 +423,255 @@ export default function SpotMarketOrderSwapForm() {
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
</div>
</div>
</div>
<div className="mt-6 mb-4 flex px-3 md:px-4">
{swapFormSizeUi === 'slider' ? (
<SwapSlider
useMargin={useMargin}
amount={
side === 'buy'
? stringToNumberOrZero(quoteSize)
: stringToNumberOrZero(baseSize)
}
onChange={setAmountFromSlider}
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
/>
<div className="mt-2">
<SwapSlider
useMargin={savedCheckboxSettings.margin}
amount={
side === 'buy'
? stringToNumberOrZero(quoteSize)
: stringToNumberOrZero(baseSize)
}
onChange={setAmountFromSlider}
step={1 / 10 ** (inputBank?.mintDecimals || 6)}
/>
</div>
) : (
<PercentageSelectButtons
amountIn={side === 'buy' ? quoteSize : baseSize}
setAmountIn={setAmountFromSlider}
useMargin={useMargin}
useMargin={savedCheckboxSettings.margin}
values={['25', '50', '75', '100']}
/>
)}
</div>
<div className="mt-6 mb-4 flex px-3 md:px-4">
{ipAllowed ? (
<Button
className={`flex w-full items-center justify-center ${
!connected
? ''
: side === 'buy'
? 'bg-th-up-dark text-white md:hover:bg-th-up-dark md:hover:brightness-90'
: 'bg-th-down-dark text-white md:hover:bg-th-down-dark md:hover:brightness-90'
}`}
disabled={disabled}
size="large"
type="submit"
<div className="mt-4">
<Tooltip
className="hidden md:block"
delay={100}
placement="left"
content={t('trade:tooltip-enable-margin')}
>
<Checkbox
checked={savedCheckboxSettings.margin}
onChange={(e) =>
setSavedCheckboxSettings({
...savedCheckboxSettings,
margin: e.target.checked,
})
}
>
{t('trade:margin')}
</Checkbox>
</Tooltip>
</div>
<div className="mt-6 mb-4 flex">
{ipAllowed ? (
<Button
className={`flex w-full items-center justify-center ${
!connected
? ''
: side === 'buy'
? 'bg-th-up-dark text-white md:hover:bg-th-up-dark md:hover:brightness-90'
: 'bg-th-down-dark text-white md:hover:bg-th-down-dark md:hover:brightness-90'
}`}
disabled={disabled}
size="large"
type="submit"
>
{isLoading ? (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('common:fetching-route')}
</span>
</div>
) : !connected ? (
<div className="flex items-center">
<LinkIcon className="mr-2 h-5 w-5" />
{t('connect')}
</div>
) : tooMuchSize ? (
<span>
{t('swap:insufficient-balance', {
symbol: '',
})}
</span>
) : !placingOrder ? (
<span>
{t('trade:place-order', {
side: side === 'buy' ? t('buy') : t('sell'),
})}
</span>
) : (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('trade:placing-order')}
</span>
</div>
)}
</Button>
) : (
<Button disabled className="w-full leading-tight" size="large">
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
</div>
<div className="space-y-2">
<div className="flex justify-between text-xs">
<p>{t('trade:order-value')}</p>
<p className="font-mono text-th-fgd-2">
{orderValue ? (
<FormatNumericValue value={orderValue} isUsd />
) : (
''
)}
</p>
</div>
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
<div className="flex justify-between text-xs">
<Tooltip
content={
<>
<p>
The price impact is the difference observed between the
total value of the entry tokens swapped and the
destination tokens obtained.
</p>
<p className="mt-1">
The bigger the trade is, the bigger the price impact can
be.
</p>
</>
}
>
<p className="tooltip-underline">{t('swap:price-impact')}</p>
</Tooltip>
{isLoading ? (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('common:fetching-route')}
</span>
</div>
) : !connected ? (
<div className="flex items-center">
<LinkIcon className="mr-2 h-5 w-5" />
{t('connect')}
</div>
) : !placingOrder ? (
<span>
{t('trade:place-order', {
side: side === 'buy' ? t('buy') : t('sell'),
})}
</span>
<SheenLoader>
<div className="h-3.5 w-12 bg-th-bkg-2" />
</SheenLoader>
) : (
<div className="flex items-center space-x-2">
<Loading />
<span className="hidden sm:block">
{t('trade:placing-order')}
</span>
<p className="text-right font-mono text-th-fgd-2">
{selectedRoute
? selectedRoute?.priceImpactPct * 100 < 0.1
? '<0.1%'
: `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%`
: '-'}
</p>
)}
</div>
{borrowAmount && inputBank ? (
<>
<div className="flex justify-between text-xs">
<Tooltip
content={
balance
? t('trade:tooltip-borrow-balance', {
balance: formatNumericValue(balance),
borrowAmount: formatNumericValue(borrowAmount),
token: formatTokenSymbol(inputBank.name),
rate: formatNumericValue(
inputBank.getBorrowRateUi(),
2,
),
})
: t('trade:tooltip-borrow-no-balance', {
borrowAmount: formatNumericValue(borrowAmount),
token: formatTokenSymbol(inputBank.name),
rate: formatNumericValue(
inputBank.getBorrowRateUi(),
2,
),
})
}
delay={100}
>
<p className="tooltip-underline">{t('borrow-amount')}</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
<FormatNumericValue
value={borrowAmount}
decimals={inputBank.mintDecimals}
/>{' '}
<span className="font-body text-th-fgd-4">
{formatTokenSymbol(inputBank.name)}
</span>
</p>
</div>
<div className="flex justify-between text-xs">
<Tooltip
content={t('loan-origination-fee-tooltip', {
fee: `${(
inputBank.loanOriginationFeeRate.toNumber() * 100
).toFixed(3)}%`,
})}
delay={100}
>
<p className="tooltip-underline">
{t('loan-origination-fee')}
</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
<FormatNumericValue
value={
borrowAmount *
inputBank.loanOriginationFeeRate.toNumber()
}
decimals={inputBank.mintDecimals}
/>{' '}
<span className="font-body text-th-fgd-4">
{formatTokenSymbol(inputBank.name)}
</span>
</p>
</div>
</>
) : null}
<div className="flex items-center justify-between text-xs">
<p className="pr-2 text-th-fgd-3">{t('common:route')}</p>
{isLoading ? (
<SheenLoader>
<div className="h-3.5 w-20 bg-th-bkg-2" />
</SheenLoader>
) : (
<div className="flex items-center overflow-hidden text-th-fgd-2">
<Tooltip
content={selectedRoute?.marketInfos.map((info, index) => {
let includeSeparator = false
if (
selectedRoute?.marketInfos.length > 1 &&
index !== selectedRoute?.marketInfos.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
})}
>
<div className="tooltip-underline truncate whitespace-nowrap max-w-[140px]">
{selectedRoute?.marketInfos.map((info, index) => {
let includeSeparator = false
if (
selectedRoute?.marketInfos.length > 1 &&
index !== selectedRoute?.marketInfos.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
})}
</div>
</Tooltip>
</div>
)}
</Button>
) : (
<Button disabled className="w-full leading-tight" size="large">
{t('country-not-allowed', {
country: ipCountry ? `(${ipCountry})` : '',
})}
</Button>
)}
</div>
<div className="space-y-2 px-3 md:px-4">
<div className="">
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
</div>
<div className="flex justify-between text-xs">
<Tooltip
content={
<>
<p>
The price impact is the difference observed between the
total value of the entry tokens swapped and the destination
tokens obtained.
</p>
<p className="mt-1">
The bigger the trade is, the bigger the price impact can be.
</p>
</>
}
>
<p className="tooltip-underline">{t('swap:price-impact')}</p>
</Tooltip>
<p className="text-right font-mono text-th-fgd-2">
{selectedRoute
? selectedRoute?.priceImpactPct * 100 < 0.1
? '<0.1%'
: `${(selectedRoute?.priceImpactPct * 100).toFixed(2)}%`
: '-'}
</p>
</div>
<div className="flex items-center justify-between text-xs">
<p className="pr-2 text-th-fgd-3">{t('common:route')}</p>
<div className="flex items-center overflow-hidden text-th-fgd-3">
<div className="truncate whitespace-nowrap">
{selectedRoute?.marketInfos.map((info, index) => {
let includeSeparator = false
if (
selectedRoute?.marketInfos.length > 1 &&
index !== selectedRoute?.marketInfos.length - 1
) {
includeSeparator = true
}
return (
<span key={index}>{`${info?.label} ${
includeSeparator ? 'x ' : ''
}`}</span>
)
})}
</div>
</div>
</div>
</div>

View File

@ -20,12 +20,9 @@ import OrderbookAndTrades from './OrderbookAndTrades'
// import TradeOnboardingTour from '@components/tours/TradeOnboardingTour'
import FavoriteMarketsBar from './FavoriteMarketsBar'
import useLocalStorageState from 'hooks/useLocalStorageState'
import {
DEPTH_CHART_KEY,
SIDEBAR_COLLAPSE_KEY,
TRADE_LAYOUT_KEY,
} from 'utils/constants'
import { SIDEBAR_COLLAPSE_KEY, TRADE_LAYOUT_KEY } from 'utils/constants'
import TradeHotKeys from './TradeHotKeys'
import OrderbookTooltip from './OrderbookTooltip'
export type TradeLayout =
| 'chartLeft'
@ -59,7 +56,6 @@ const TradeAdvancedPage = () => {
'chartLeft',
)
const [isCollapsed] = useLocalStorageState(SIDEBAR_COLLAPSE_KEY, false)
const [showDepthChart] = useLocalStorageState<boolean>(DEPTH_CHART_KEY, false)
const totalCols = 24
const gridBreakpoints = useMemo(() => {
@ -89,31 +85,31 @@ const TradeAdvancedPage = () => {
chartLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
chartMiddleOBRight: { xxxl: 4, xxl: 5, xl: 5, lg: 5 },
chartMiddleOBLeft: {
xxxl: showDepthChart ? 7 : 4,
xxl: showDepthChart ? 7 : 4,
xl: showDepthChart ? 7 : 4,
lg: showDepthChart ? 8 : 5,
xxxl: 4,
xxl: 4,
xl: 5,
lg: 5,
},
chartRight: {
xxxl: showDepthChart ? 12 : 9,
xxl: showDepthChart ? 12 : 9,
xl: showDepthChart ? 12 : 9,
lg: showDepthChart ? 14 : 11,
xxxl: 9,
xxl: 9,
xl: 10,
lg: 11,
},
}
const bookXPos = {
chartLeft: {
xxxl: showDepthChart ? 13 : 16,
xxl: showDepthChart ? 12 : 15,
xl: showDepthChart ? 12 : 15,
lg: showDepthChart ? 11 : 14,
xxxl: 16,
xxl: 15,
xl: 14,
lg: 14,
},
chartMiddleOBRight: {
xxxl: showDepthChart ? 17 : 20,
xxl: showDepthChart ? 17 : 20,
xl: showDepthChart ? 17 : 20,
lg: showDepthChart ? 16 : 19,
xxxl: 20,
xxl: 20,
xl: 20,
lg: 19,
},
chartMiddleOBLeft: { xxxl: 0, xxl: 0, xl: 0, lg: 0 },
chartRight: { xxxl: 4, xxl: 5, xl: 5, lg: 5 },
@ -133,14 +129,14 @@ const TradeAdvancedPage = () => {
i: 'tv-chart',
x: chartXPos[tradeLayout].xxxl,
y: 1,
w: showDepthChart ? 13 : 16,
w: 16,
h: 640,
},
{
i: 'orderbook',
x: bookXPos[tradeLayout].xxxl,
y: 1,
w: showDepthChart ? 7 : 4,
w: 4,
h: 640,
},
{
@ -164,14 +160,14 @@ const TradeAdvancedPage = () => {
i: 'tv-chart',
x: chartXPos[tradeLayout].xxl,
y: 1,
w: showDepthChart ? 12 : 15,
w: 15,
h: 552,
},
{
i: 'orderbook',
x: bookXPos[tradeLayout].xxl,
y: 1,
w: showDepthChart ? 7 : 4,
w: 4,
h: 552,
},
{
@ -195,14 +191,14 @@ const TradeAdvancedPage = () => {
i: 'tv-chart',
x: chartXPos[tradeLayout].xl,
y: 1,
w: showDepthChart ? 12 : 15,
w: 14,
h: 552,
},
{
i: 'orderbook',
x: bookXPos[tradeLayout].xl,
y: 1,
w: showDepthChart ? 7 : 4,
w: 5,
h: 552,
},
{
@ -226,14 +222,14 @@ const TradeAdvancedPage = () => {
i: 'tv-chart',
x: chartXPos[tradeLayout].lg,
y: 1,
w: showDepthChart ? 11 : 14,
w: 14,
h: 552,
},
{
i: 'orderbook',
x: bookXPos[tradeLayout].lg,
y: 1,
w: showDepthChart ? 8 : 5,
w: 5,
h: 552,
},
{
@ -259,7 +255,7 @@ const TradeAdvancedPage = () => {
{ i: 'balances', x: 0, y: 2, w: 17, h: 428 + marketHeaderHeight },
],
}
}, [height, showDepthChart, tradeLayout])
}, [height, tradeLayout])
const [layouts, setLayouts] = useState<Layouts>(defaultLayouts)
const [breakpoint, setBreakpoint] = useState('')
@ -273,7 +269,7 @@ const TradeAdvancedPage = () => {
useLayoutEffect(() => {
handleLayoutChange(undefined, defaultLayouts)
}, [breakpoint, showDepthChart, tradeLayout])
}, [breakpoint, tradeLayout])
return showMobileView ? (
<MobileTradeAdvancedPage />
@ -309,6 +305,7 @@ const TradeAdvancedPage = () => {
className="h-full border border-x-0 border-th-bkg-3"
>
<div className={`relative h-full overflow-auto`}>
<OrderbookTooltip />
<TradingChartContainer />
</div>
</div>

View File

@ -0,0 +1,132 @@
import { useQuery } from '@tanstack/react-query'
import {
fetchAuctionHouse,
fetchFilteredListing,
fetchFilteredBids,
} from 'apis/market/auctionHouse'
import metaplexStore from '@store/metaplexStore'
import { Bid, LazyBid, LazyListing } from '@metaplex-foundation/js'
export const ALL_FILTER = 'All'
//10min
const refetchMs = 600000
export function useAuctionHouse() {
const metaplex = metaplexStore((s) => s.metaplex)
const criteria = metaplex?.cluster
return useQuery(
['auctionHouse', criteria],
() => fetchAuctionHouse(metaplex!),
{
enabled: !!metaplex,
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,
},
)
}
export function useLazyListings(filter = ALL_FILTER, page = 1, perPage = 9) {
const metaplex = metaplexStore((s) => s.metaplex)
const { data } = useAuctionHouse()
const criteria = metaplex && [
data?.address.toBase58(),
filter,
metaplex.identity().publicKey.toBase58(),
page,
]
return useQuery(
['lazyListings', criteria],
() => fetchFilteredListing(metaplex!, data!, filter, page, perPage),
{
enabled: !!(metaplex && data),
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,
},
)
}
export function useListings(filter = ALL_FILTER, page = 1) {
const { data: lazyListings } = useLazyListings(filter, page)
const metaplex = metaplexStore((s) => s.metaplex)
const criteria = lazyListings?.results
? [...lazyListings.results!.map((x) => x.tradeStateAddress.toBase58())]
: []
const loadMetadatas = async (
lazyListings: LazyListing[],
totalPages: number,
) => {
const listingsWithMeta = []
for (const listing of lazyListings) {
const listingWithMeta = await metaplex!.auctionHouse().loadListing({
lazyListing: {
...listing,
},
loadJsonMetadata: true,
})
listingsWithMeta.push({ ...listingWithMeta })
}
return { results: listingsWithMeta, totalPages: totalPages }
}
return useQuery(
['listings', criteria],
() => loadMetadatas(lazyListings!.results!, lazyListings!.totalPages),
{
enabled: !!(metaplex && lazyListings?.results),
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,
},
)
}
export function useBids() {
const metaplex = metaplexStore((s) => s.metaplex)
const { data } = useAuctionHouse()
const criteria = metaplex && data?.address.toBase58()
return useQuery(
['bids', criteria],
() => fetchFilteredBids(metaplex!, data!),
{
enabled: !!(metaplex && data),
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,
},
)
}
export function useLoadBids(lazyBids: LazyBid[]) {
const metaplex = metaplexStore((s) => s.metaplex)
const criteria = [...lazyBids.map((x) => x.createdAt.toNumber())]
const loadBids = async (lazyBids: LazyBid[]) => {
const bids: Bid[] = []
for (const lazyBid of lazyBids) {
const bid = await metaplex!.auctionHouse().loadBid({
lazyBid: {
...lazyBid,
},
loadJsonMetadata: true,
})
bids.push({ ...bid })
}
return bids
}
return useQuery(['loadedBids', criteria], () => loadBids(lazyBids), {
enabled: !!(metaplex && lazyBids.length),
staleTime: refetchMs,
retry: 1,
refetchInterval: refetchMs,
})
}

View File

@ -1,100 +0,0 @@
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
import { getOneDayPerpStats } from '@components/stats/PerpMarketsOverviewTable'
import mangoStore from '@store/mangoStore'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { useMemo } from 'react'
import { DAILY_SECONDS, MANGO_DATA_API_URL } from 'utils/constants'
dayjs.extend(utc)
const fetchPrices = async (market: Serum3Market | PerpMarket | undefined) => {
if (!market || market instanceof PerpMarket) return
const { baseTokenIndex, quoteTokenIndex } = market
const nowTimestamp = Date.now() / 1000
const changePriceTimestamp = nowTimestamp - DAILY_SECONDS
const changePriceTime = dayjs
.unix(changePriceTimestamp)
.utc()
.format('YYYY-MM-DDTHH:mm:ss[Z]')
const promises = [
fetch(
`${MANGO_DATA_API_URL}/stats/token-price?token-index=${baseTokenIndex}&price-time=${changePriceTime}`,
),
fetch(
`${MANGO_DATA_API_URL}/stats/token-price?token-index=${quoteTokenIndex}&price-time=${changePriceTime}`,
),
]
try {
const data = await Promise.all(promises)
const baseTokenPriceData = await data[0].json()
const quoteTokenPriceData = await data[1].json()
const baseTokenPrice = baseTokenPriceData ? baseTokenPriceData.price : 1
const quoteTokenPrice = quoteTokenPriceData ? quoteTokenPriceData.price : 1
return { baseTokenPrice, quoteTokenPrice }
} catch (e) {
console.log('failed to fetch 24hr price data', e)
return { baseTokenPrice: 1, quoteTokenPrice: 1 }
}
}
export default function use24HourChange(
market: Serum3Market | PerpMarket | undefined,
) {
const perpStats = mangoStore((s) => s.perpStats.data)
const loadingPerpStats = mangoStore((s) => s.perpStats.loading)
const {
data: priceData,
isLoading: loadingPriceData,
isFetching: fetchingPriceData,
} = useQuery(['token-prices', market?.name], () => fetchPrices(market), {
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
enabled: market && market instanceof Serum3Market,
})
const [currentBasePrice, currentQuotePrice] = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !market || market instanceof PerpMarket)
return [undefined, undefined]
const baseBank = group.getFirstBankByTokenIndex(market.baseTokenIndex)
const quoteBank = group.getFirstBankByTokenIndex(market.quoteTokenIndex)
return [baseBank?.uiPrice, quoteBank?.uiPrice]
}, [market])
const perpChange = useMemo(() => {
if (
!market ||
market instanceof Serum3Market ||
!perpStats ||
!perpStats.length
)
return
const oneDayStats = getOneDayPerpStats(perpStats, market.name)
const currentPrice = market.uiPrice
const change = oneDayStats.length
? ((currentPrice - oneDayStats[0].price) / oneDayStats[0].price) * 100
: undefined
return change
}, [market, perpStats])
const spotChange = useMemo(() => {
if (!market) return
if (!currentBasePrice || !currentQuotePrice || !priceData) return
const currentPrice = currentBasePrice / currentQuotePrice
const oneDayPrice = priceData.baseTokenPrice / priceData.quoteTokenPrice
const change = ((currentPrice - oneDayPrice) / oneDayPrice) * 100
return change
}, [market, priceData])
const loading = useMemo(() => {
if (!market) return false
if (market instanceof PerpMarket) return loadingPerpStats
return loadingPriceData || fetchingPriceData
}, [market, loadingPerpStats, loadingPriceData, fetchingPriceData])
return { loading, perpChange, spotChange }
}

View File

@ -0,0 +1,69 @@
import { MarketData, MarketsDataItem } from 'types'
import useMarketsData from './useMarketsData'
import { useMemo } from 'react'
import mangoStore from '@store/mangoStore'
import { PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4'
type ApiData = {
marketData: MarketsDataItem | undefined
}
export type SerumMarketWithMarketData = Serum3Market & ApiData
export type PerpMarketWithMarketData = PerpMarket & ApiData
export default function useListedMarketsWithMarketData() {
const { data: marketsData, isLoading, isFetching } = useMarketsData()
const serumMarkets = mangoStore((s) => s.serumMarkets)
const perpMarkets = mangoStore((s) => s.perpMarkets)
const perpData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.perpData || []
}, [marketsData])
const spotData: MarketData = useMemo(() => {
if (!marketsData) return []
return marketsData?.spotData || []
}, [marketsData])
const serumMarketsWithData = useMemo(() => {
if (!serumMarkets || !serumMarkets.length) return []
const allSpotMarkets: SerumMarketWithMarketData[] =
serumMarkets as SerumMarketWithMarketData[]
if (spotData) {
for (const market of allSpotMarkets) {
const spotEntries = Object.entries(spotData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
market.marketData = spotEntries ? spotEntries[1][0] : undefined
}
}
return [...allSpotMarkets].sort((a, b) => a.name.localeCompare(b.name))
}, [spotData, serumMarkets])
const perpMarketsWithData = useMemo(() => {
if (!perpMarkets || !perpMarkets.length) return []
const allPerpMarkets: PerpMarketWithMarketData[] =
perpMarkets as PerpMarketWithMarketData[]
if (perpData) {
for (const market of allPerpMarkets) {
const perpEntries = Object.entries(perpData).find(
(e) => e[0].toLowerCase() === market.name.toLowerCase(),
)
market.marketData = perpEntries ? perpEntries[1][0] : undefined
}
}
return allPerpMarkets
.filter(
(p) =>
p.publicKey.toString() !==
'9Y8paZ5wUpzLFfQuHz8j2RtPrKsDtHx9sbgFmWb5abCw',
)
.sort((a, b) =>
a.oracleLastUpdatedSlot == 0 ? -1 : a.name.localeCompare(b.name),
)
}, [perpData, perpMarkets])
return { perpMarketsWithData, serumMarketsWithData, isLoading, isFetching }
}

27
hooks/useMarketsData.ts Normal file
View File

@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query'
import { MANGO_DATA_API_URL } from 'utils/constants'
const fetchMarketData = async () => {
const promises = [
fetch(`${MANGO_DATA_API_URL}/stats/perp-market-summary`),
fetch(`${MANGO_DATA_API_URL}/stats/spot-market-summary`),
]
try {
const data = await Promise.all(promises)
const perpData = await data[0].json()
const spotData = await data[1].json()
return { perpData, spotData }
} catch (e) {
console.log('failed to fetch market data', e)
return { perpData: [], spotData: [] }
}
}
export default function useMarketsData() {
return useQuery(['market-data'], () => fetchMarketData(), {
cacheTime: 1000 * 60 * 10,
staleTime: 1000 * 60,
retry: 3,
refetchOnWindowFocus: false,
})
}

19
hooks/useMetaplex.ts Normal file
View File

@ -0,0 +1,19 @@
import { Metaplex, walletAdapterIdentity } from '@metaplex-foundation/js'
import { useWallet } from '@solana/wallet-adapter-react'
import mangoStore from '@store/mangoStore'
import metaplexStore from '@store/metaplexStore'
import { useEffect } from 'react'
export default function useMetaplex() {
const connection = mangoStore((s) => s.connection)
const wallet = useWallet()
const setMetaplexInstance = metaplexStore((s) => s.setMetaplexInstance)
useEffect(() => {
let meta = new Metaplex(connection)
if (wallet?.publicKey) {
meta = meta.use(walletAdapterIdentity(wallet))
}
setMetaplexInstance(meta)
}, [connection, setMetaplexInstance, wallet])
}

View File

@ -22,10 +22,11 @@
},
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-v4": "^0.17.26",
"@blockworks-foundation/mango-v4": "^0.17.27",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
"@metaplex-foundation/js": "0.18.3",
"@metaplex-foundation/js": "0.19.4",
"@next/font": "13.4.4",
"@project-serum/anchor": "0.25.0",
"@pythnetwork/client": "2.15.0",
"@sentry/nextjs": "7.58.0",
@ -76,7 +77,8 @@
"tsparticles": "2.2.4",
"walktour": "5.1.1",
"webpack-node-externals": "3.0.0",
"zustand": "4.1.3"
"zustand": "4.1.3",
"react-responsive-pagination": "^2.1.0"
},
"peerDependencies": {
"@project-serum/anchor": "0.25.0",

108
pages/nft/index.tsx Normal file
View File

@ -0,0 +1,108 @@
import useMetaplex from 'hooks/useMetaplex'
import type { NextPage } from 'next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useState } from 'react'
import ListingsView from '@components/nftMarket/ListingsView'
import AllBidsView from '@components/nftMarket/AllBidsView'
import Button from '@components/shared/Button'
// import { useTranslation } from 'next-i18next'
import TabUnderline from '@components/shared/TabUnderline'
import SellNftModal from '@components/nftMarket/SellNftModal'
import MyBidsModal from '@components/nftMarket/MyBidsModal'
import { useIsWhiteListed } from 'hooks/useIsWhiteListed'
const LISTINGS = 'Listings'
const BIDS_WITHOUT_LISTINGS = 'Offers'
const TABS = [LISTINGS, BIDS_WITHOUT_LISTINGS]
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, [
'common',
'notifications',
'onboarding',
'profile',
'search',
'settings',
'token',
'trade',
'nft-market',
])),
},
}
}
const Market: NextPage = () => {
// const { t } = useTranslation('nft-market')
useMetaplex()
const [activeTab, setActiveTab] = useState('Listings')
const [sellNftModal, setSellNftModal] = useState(false)
const [myBidsModal, setMyBidsModal] = useState(false)
const { data: isWhiteListed } = useIsWhiteListed()
//TODO leave for release
// const create = async () => {
// const auctionMint = new PublicKey(
// 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'
// )
// const owner = new PublicKey('8SSLjXBEVk9nesbhi9UMCA32uijbVBUqWoKPPQPTekzt')
// const auctionHouseSettingsObj = {
// treasuryMint: auctionMint,
// sellerFeeBasisPoints: 0,
// authority: owner,
// feeWithdrawalDestination: owner,
// treasuryWithdrawalDestinationOwner: owner,
// requireSignOff: false,
// canChangeSalePrice: false,
// }
// const elo = await metaplex!.auctionHouse().create({
// ...auctionHouseSettingsObj,
// })
// console.log(elo)
// }
return isWhiteListed ? (
<>
<div className="mx-auto flex max-w-[1140px] flex-col px-6">
<div className="flex items-center justify-between pt-8 pb-6">
<h1>NFT Market</h1>
<div className="flex space-x-2">
<Button onClick={() => setSellNftModal(true)}>
Sell your NFTs
</Button>
<Button onClick={() => setMyBidsModal(true)} secondary>
Your Offers
</Button>
</div>
</div>
<div>
<TabUnderline
activeValue={activeTab}
values={TABS}
onChange={(v) => setActiveTab(v)}
fillWidth={false}
/>
</div>
<div className="">
{activeTab === LISTINGS ? <ListingsView /> : <AllBidsView />}
</div>
</div>
{sellNftModal && (
<SellNftModal
isOpen={sellNftModal}
onClose={() => setSellNftModal(false)}
></SellNftModal>
)}
{myBidsModal && (
<MyBidsModal
isOpen={myBidsModal}
onClose={() => setMyBidsModal(false)}
></MyBidsModal>
)}
</>
) : null
}
export default Market

View File

@ -0,0 +1,12 @@
{
"sell": "Sell",
"my-bids": "My Bids",
"price": "Price",
"buy": "Buy",
"bid": "Bid",
"cancel-listing": "Cancel Listing",
"bids": "Bids",
"cancel": "Cancel",
"accept-bid": "Accept bid",
"list": "List"
}

View File

@ -3,6 +3,7 @@
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"average-funding": "Average {{interval}} Funding",
"average-price-of": "at an average price of",
"base": "Base",
"book": "Book",
"buys": "Buys",
@ -20,6 +21,7 @@
"est-liq-price": "Est. Liq. Price",
"avg-entry-price": "Avg. Entry Price",
"est-slippage": "Est. Slippage",
"for": "for",
"funding-limits": "Funding Limits",
"funding-rate": "1h Avg Funding Rate",
"grouping": "Grouping",

View File

@ -7,7 +7,6 @@
"modify-order-details": "Edit your {{marketName}} order from {{orderSide}} {{orderSize}} at {{currentOrderPrice}} to {{orderSide}} {{orderSize}} at {{updatedOrderPrice}}",
"order-details": " ({{orderType}} {{orderSide}}) if price is {{triggerCondition}} {{triggerPrice}}",
"outside-range": "Order Price Outside Range",
"toggle-stable-price": "Toggle stable price line",
"slippage-accept": "Use the trade order form if you wish to accept the potential slippage.",
"slippage-warning": "{{updatedOrderPrice}} is greater than 5% {{aboveBelow}} the current market price of {{selectedMarketPrice}}. Executing this trade could incur significant slippage.",
"toggle-order-line": "Toggle order line visibility",

View File

@ -0,0 +1,12 @@
{
"sell": "Sell",
"my-bids": "My Bids",
"price": "Price",
"buy": "Buy",
"bid": "Bid",
"cancel-listing": "Cancel Listing",
"bids": "Bids",
"cancel": "Cancel",
"accept-bid": "Accept bid",
"list": "List"
}

View File

@ -3,6 +3,7 @@
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"average-funding": "Average {{interval}} Funding",
"average-price-of": "at an average price of",
"base": "Base",
"book": "Book",
"buys": "Buys",
@ -20,6 +21,7 @@
"est-liq-price": "Est. Liq. Price",
"avg-entry-price": "Avg. Entry Price",
"est-slippage": "Est. Slippage",
"for": "for",
"funding-limits": "Funding Limits",
"funding-rate": "1h Avg Funding Rate",
"grouping": "Grouping",

View File

@ -7,7 +7,6 @@
"modify-order-details": "Edit your {{marketName}} order from {{orderSide}} {{orderSize}} at {{currentOrderPrice}} to {{orderSide}} {{orderSize}} at {{updatedOrderPrice}}",
"order-details": " ({{orderType}} {{orderSide}}) if price is {{triggerCondition}} {{triggerPrice}}",
"outside-range": "Order Price Outside Range",
"toggle-stable-price": "Toggle stable price line",
"slippage-accept": "Use the trade order form if you wish to accept the potential slippage.",
"slippage-warning": "{{updatedOrderPrice}} is greater than 5% {{aboveBelow}} the current market price of {{selectedMarketPrice}}. Executing this trade could incur significant slippage.",
"toggle-order-line": "Toggle order line visibility",

View File

@ -0,0 +1,12 @@
{
"sell": "Sell",
"my-bids": "My Bids",
"price": "Price",
"buy": "Buy",
"bid": "Bid",
"cancel-listing": "Cancel Listing",
"bids": "Bids",
"cancel": "Cancel",
"accept-bid": "Accept bid",
"list": "List"
}

View File

@ -3,6 +3,7 @@
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"average-funding": "Average {{interval}} Funding",
"average-price-of": "at an average price of",
"base": "Base",
"book": "Book",
"buys": "Buys",
@ -20,6 +21,7 @@
"est-liq-price": "Est. Liq. Price",
"avg-entry-price": "Avg. Entry Price",
"est-slippage": "Est. Slippage",
"for": "for",
"funding-limits": "Funding Limits",
"funding-rate": "1h Avg Funding Rate",
"grouping": "Grouping",

View File

@ -7,7 +7,6 @@
"modify-order-details": "Edit your {{marketName}} order from {{orderSide}} {{orderSize}} at {{currentOrderPrice}} to {{orderSide}} {{orderSize}} at {{updatedOrderPrice}}",
"order-details": " ({{orderType}} {{orderSide}}) if price is {{triggerCondition}} {{triggerPrice}}",
"outside-range": "Order Price Outside Range",
"toggle-stable-price": "Toggle stable price line",
"slippage-accept": "Use the trade order form if you wish to accept the potential slippage.",
"slippage-warning": "{{updatedOrderPrice}} is greater than 5% {{aboveBelow}} the current market price of {{selectedMarketPrice}}. Executing this trade could incur significant slippage.",
"toggle-order-line": "Toggle order line visibility",

View File

@ -0,0 +1,12 @@
{
"sell": "Sell",
"my-bids": "My Bids",
"price": "Price",
"buy": "Buy",
"bid": "Bid",
"cancel-listing": "Cancel Listing",
"bids": "Bids",
"cancel": "Cancel",
"accept-bid": "Accept bid",
"list": "List"
}

View File

@ -3,6 +3,7 @@
"activate-volume-alert": "Activate Volume Alert",
"amount": "Amount",
"average-funding": "Average {{interval}} Funding",
"average-price-of": "at an average price of",
"base": "Base",
"book": "Book",
"buys": "Buys",
@ -20,6 +21,7 @@
"est-liq-price": "Est. Liq. Price",
"avg-entry-price": "Avg. Entry Price",
"est-slippage": "Est. Slippage",
"for": "for",
"funding-limits": "Funding Limits",
"funding-rate": "1h Avg Funding Rate",
"grouping": "Grouping",

View File

@ -10,6 +10,5 @@
"slippage-accept": "若您接受潜在的下滑请使用交易表格进行。",
"slippage-warning": "您的订单价格({{updatedOrderPrice}})多余5%{{aboveBelow}}市场价格({{selectedMarketPrice}})表是您也许遭受可观的下滑。",
"toggle-order-line": "切换订单线可见性",
"toggle-stable-price": "切换稳定价格线",
"toggle-trade-executions": "显示成交。限于全部试场的最近250交易。"
}

View File

@ -0,0 +1,12 @@
{
"sell": "Sell",
"my-bids": "My Bids",
"price": "Price",
"buy": "Buy",
"bid": "Bid",
"cancel-listing": "Cancel Listing",
"bids": "Bids",
"cancel": "Cancel",
"accept-bid": "Accept bid",
"list": "List"
}

View File

@ -4,6 +4,7 @@
"amount": "數量",
"average-funding": "平均 {{interval}} 資金費",
"avg-entry-price": "平均開倉價格",
"average-price-of": "at an average price of",
"base": "基礎",
"book": "單薄",
"buys": "買",
@ -20,6 +21,7 @@
"edit-order": "編輯訂單",
"est-liq-price": "預計清算價格",
"est-slippage": "預計下滑",
"for": "for",
"funding-limits": "資金費限制",
"funding-rate": "1小時平均資金費",
"grouping": "分組",

View File

@ -10,6 +10,5 @@
"slippage-accept": "若您接受潛在的下滑請使用交易表格進行。",
"slippage-warning": "您的訂單價格({{updatedOrderPrice}})多餘5%{{aboveBelow}}市場價格({{selectedMarketPrice}})表是您也許遭受可觀的下滑。",
"toggle-order-line": "切換訂單線可見性",
"toggle-stable-price": "切換穩定價格線",
"toggle-trade-executions": "顯示成交。限於全部試場的最近250交易。"
}

View File

@ -60,6 +60,7 @@ import {
MangoTokenStatsItem,
ThemeData,
PositionStat,
OrderbookTooltip,
} from 'types'
import spotBalancesUpdater from './spotBalancesUpdater'
import { PerpMarket } from '@blockworks-foundation/mango-v4/'
@ -85,8 +86,9 @@ const ENDPOINTS = [
},
{
name: 'devnet',
url: 'https://mango.devnet.rpcpool.com',
websocket: 'https://mango.devnet.rpcpool.com',
url: 'https://realms-develope-935c.devnet.rpcpool.com/67f608dc-a353-4191-9c34-293a5061b536',
websocket:
'https://realms-develope-935c.devnet.rpcpool.com/67f608dc-a353-4191-9c34-293a5061b536',
custom: false,
},
]
@ -175,6 +177,7 @@ export type MangoStore = {
closestToLiq: PositionStat[]
}
}
orderbookTooltip: OrderbookTooltip | undefined
profile: {
details: ProfileDetails | null
loadDetails: boolean
@ -326,6 +329,7 @@ const mangoStore = create<MangoStore>()(
closestToLiq: [],
},
},
orderbookTooltip: undefined,
profile: {
loadDetails: false,
details: { profile_name: '', trader_category: '', wallet_pk: '' },

22
store/metaplexStore.ts Normal file
View File

@ -0,0 +1,22 @@
import { Metaplex } from '@metaplex-foundation/js'
import produce from 'immer'
import create from 'zustand'
type IMetaplexStore = {
metaplex: Metaplex | null
set: (x: (x: IMetaplexStore) => void) => void
setMetaplexInstance: (metaplex: Metaplex) => void
}
const MetaplexStore = create<IMetaplexStore>((set, get) => ({
metaplex: null,
set: (fn) => set(produce(fn)),
setMetaplexInstance: (metaplex) => {
const set = get().set
set((state) => {
state.metaplex = metaplex
})
},
}))
export default MetaplexStore

View File

@ -690,3 +690,30 @@ input[type='range']::-webkit-slider-runnable-track {
box-shadow: 0 0 var(--up-dark);
top: 6px;
}
.pagination {
margin-top: 15px;
margin-bottom: 15px;
justify-content: center;
display: flex;
padding-left: 0;
list-style: none;
}
.page-link {
display: block;
border: 1px solid #cccccc;
border-radius: 5px;
padding: 5px 10px;
margin: 0 2px;
text-decoration: none;
}
a.page-link:hover {
background-color: #cccccc;
}
.page-item.active .page-link {
color: #ffffff;
background-color: #007bff;
}

View File

@ -303,6 +303,8 @@ export interface NFT {
collectionAddress?: string
image: string
name: string
mint: string
tokenAccount: string
}
export interface PerpStatsItem {
@ -422,21 +424,29 @@ export function isMangoError(error: unknown): error is MangoError {
)
}
export type TickerData = {
base_currency: string
base_volume: string
high: string
last_price: string
low: string
target_currency: string
target_volume: string
ticker_id: string
export type MarketData = { [key: string]: MarketsDataItem[] }
export type MarketsDataItem = {
base_volume_1h: number
base_volume_24h: number
change_1h: number
change_7d: number
change_24h: number
change_30d: number
last_price: number
price_1h: number
price_24h: number
price_history: { price: number; time: string }[]
quote_volume_1h: number
quote_volume_24h: number
}
export type cumOrderbookSide = {
price: number
size: number
averagePrice: number
cumulativeSize: number
cumulativeValue: number
sizePercent: number
maxSizePercent: number
cumulativeSizePercent: number
@ -450,6 +460,13 @@ export type OrderbookData = {
spreadPercentage: number
}
export type OrderbookTooltip = {
averagePrice: number
cumulativeSize: number
cumulativeValue: number
side: 'buy' | 'sell'
}
export interface HealthContribution {
asset: string
contribution: number

View File

@ -1,3 +1,5 @@
import { PublicKey } from '@metaplex-foundation/js'
export const LAST_ACCOUNT_KEY = 'mangoAccount-0.4'
export const CLIENT_TX_TIMEOUT = 90000
@ -57,8 +59,6 @@ export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1'
export const TRADE_LAYOUT_KEY = 'tradeLayoutKey-0.1'
export const DEPTH_CHART_KEY = 'showDepthChart-0.1'
export const STATS_TAB_KEY = 'activeStatsTab-0.1'
export const USE_ORDERBOOK_FEED_KEY = 'useOrderbookFeed-0.1'
@ -112,6 +112,9 @@ export const NOTIFICATION_API_WEBSOCKET =
export const SWITCHBOARD_PROGRAM_ID =
'SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f'
export const AUCTION_HOUSE_ID = new PublicKey(
'BGBBt6G9bp36i5qt7PWjBWg3VNef1zPozAN9RFsEPDkh',
)
export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
bonk: true,
btc: true,

View File

@ -71,7 +71,11 @@ export const getPythOracle = async ({
x.base === baseSymbol.toUpperCase() &&
x.quote_currency === quoteSymbol.toUpperCase(),
)
return product?.price_account || ''
const isLive =
product &&
pythAccounts.productPrice.get(product.symbol)?.price !== undefined
return isLive && product?.price_account ? product.price_account : ''
} catch (e) {
notify({
title: 'Pyth oracle fetch error',

60
utils/markets.ts Normal file
View File

@ -0,0 +1,60 @@
import {
PerpMarketWithMarketData,
SerumMarketWithMarketData,
} from 'hooks/useListedMarketsWithMarketData'
export type AllowedKeys =
| 'quote_volume_24h'
| 'quote_volume_1h'
| 'change_24h'
| 'change_1h'
export const sortSpotMarkets = (
spotMarkets: SerumMarketWithMarketData[],
sortByKey: AllowedKeys,
) => {
return spotMarkets.sort(
(a: SerumMarketWithMarketData, b: SerumMarketWithMarketData) => {
const aValue: number | undefined = a?.marketData?.[sortByKey]
const bValue: number | undefined = b?.marketData?.[sortByKey]
// Handle marketData[sortByKey] is undefined
if (typeof aValue === 'undefined' && typeof bValue === 'undefined') {
return 0 // Consider them equal
}
if (typeof aValue === 'undefined') {
return 1 // b should come before a
}
if (typeof bValue === 'undefined') {
return -1 // a should come before b
}
return bValue - aValue
},
)
}
export const sortPerpMarkets = (
perpMarkets: PerpMarketWithMarketData[],
sortByKey: AllowedKeys,
) => {
return perpMarkets.sort(
(a: PerpMarketWithMarketData, b: PerpMarketWithMarketData) => {
const aValue: number | undefined = a?.marketData?.[sortByKey]
const bValue: number | undefined = b?.marketData?.[sortByKey]
// Handle marketData[sortByKey] is undefined
if (typeof aValue === 'undefined' && typeof bValue === 'undefined') {
return 0 // Consider them equal
}
if (typeof aValue === 'undefined') {
return 1 // b should come before a
}
if (typeof bValue === 'undefined') {
return -1 // a should come before b
}
return bValue - aValue
},
)
}

View File

@ -94,11 +94,15 @@ export const getCumulativeOrderbookSide = (
isGrouped: boolean,
): cumOrderbookSide[] => {
let cumulativeSize = 0
let cumulativeValue = 0
return orders.slice(0, depth).map(([price, size]) => {
cumulativeSize += size
cumulativeValue += price * size
return {
price: Number(price),
size,
averagePrice: cumulativeValue / cumulativeSize,
cumulativeValue: cumulativeValue,
cumulativeSize,
sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100),
cumulativeSizePercent: Math.round((size / (cumulativeSize || 1)) * 100),

View File

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

109
yarn.lock
View File

@ -26,10 +26,10 @@
dependencies:
ws "^8.13.0"
"@blockworks-foundation/mango-v4@^0.17.26":
version "0.17.26"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.17.26.tgz#89b2eeed3da2b11f2d2382ee3b4ca7f274348416"
integrity sha512-B3MIesTpyueXfdCZXNHvHjUenLh/66/ihXhP2aOp+j2ZXygxoAGtYiJGc+09NB3oChKXE7hR7ofL77EDy1VFdg==
"@blockworks-foundation/mango-v4@^0.17.27":
version "0.17.27"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.17.27.tgz#70998e21e8fa2ae340fec0c36a7f8c07400c6fd4"
integrity sha512-oXvA08MqZ3dvd1ODHwx/XjMGyFvdC+6MTfzNL3vhylJA2JLlck48Fscl0mHL1h9kOdFIHJJPrnCkIW5z5g9pIQ==
dependencies:
"@coral-xyz/anchor" "^0.27.0"
"@project-serum/serum" "0.13.65"
@ -710,20 +710,20 @@
resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.1.tgz#5bd16082261d7364eabb511c788f00937dac588d"
integrity sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w==
"@metaplex-foundation/beet-solana@^0.3.0", "@metaplex-foundation/beet-solana@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.3.1.tgz#4b37cda5c7f32ffd2bdd8b3164edc05c6463ab35"
integrity sha512-tgyEl6dvtLln8XX81JyBvWjIiEcjTkUwZbrM5dIobTmoqMuGewSyk9CClno8qsMsFdB5T3jC91Rjeqmu/6xk2g==
"@metaplex-foundation/beet-solana@0.4.0", "@metaplex-foundation/beet-solana@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz#52891e78674aaa54e0031f1bca5bfbc40de12e8d"
integrity sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==
dependencies:
"@metaplex-foundation/beet" ">=0.1.0"
"@solana/web3.js" "^1.56.2"
bs58 "^5.0.0"
debug "^4.3.4"
"@metaplex-foundation/beet-solana@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz#52891e78674aaa54e0031f1bca5bfbc40de12e8d"
integrity sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==
"@metaplex-foundation/beet-solana@^0.3.0", "@metaplex-foundation/beet-solana@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.3.1.tgz#4b37cda5c7f32ffd2bdd8b3164edc05c6463ab35"
integrity sha512-tgyEl6dvtLln8XX81JyBvWjIiEcjTkUwZbrM5dIobTmoqMuGewSyk9CClno8qsMsFdB5T3jC91Rjeqmu/6xk2g==
dependencies:
"@metaplex-foundation/beet" ">=0.1.0"
"@solana/web3.js" "^1.56.2"
@ -762,20 +762,22 @@
resolved "https://registry.yarnpkg.com/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz#dc2032a452d6c269e25f016aa4dd63600e2af975"
integrity sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==
"@metaplex-foundation/js@0.18.3":
version "0.18.3"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/js/-/js-0.18.3.tgz#d61d25b9e3ab91d069cd6a87fa9eb5d01ce56ed3"
integrity sha512-rqI8vI+V5Bt3pgrv8E7leqR8gxxdw6Q/pbWg4EznbuYSmpNGRQkjMaZE0C+rQrmtQbMqUD9rUsUuYOoppSlI4A==
"@metaplex-foundation/js@0.19.4":
version "0.19.4"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/js/-/js-0.19.4.tgz#992fe6b48e8dd0374d99028a07f75547cb6381d7"
integrity sha512-fiAaMl4p7v1tcU7ZoEr1lCzE6JR2gEWGeHOBarLTpiCjMe8ni3E+cukJQC6p0Ik+Z6IIFtEVNNy5OnMS3LLS4A==
dependencies:
"@bundlr-network/client" "^0.8.8"
"@metaplex-foundation/beet" "0.7.1"
"@metaplex-foundation/mpl-auction-house" "^2.3.0"
"@metaplex-foundation/mpl-bubblegum" "^0.6.2"
"@metaplex-foundation/mpl-candy-guard" "^0.3.0"
"@metaplex-foundation/mpl-candy-machine" "^5.0.0"
"@metaplex-foundation/mpl-candy-machine-core" "^0.1.2"
"@metaplex-foundation/mpl-token-metadata" "^2.8.6"
"@metaplex-foundation/mpl-token-metadata" "^2.11.0"
"@noble/ed25519" "^1.7.1"
"@noble/hashes" "^1.1.3"
"@solana/spl-account-compression" "^0.1.8"
"@solana/spl-token" "^0.3.5"
"@solana/web3.js" "^1.63.1"
bignumber.js "^9.0.2"
@ -802,6 +804,21 @@
"@solana/web3.js" "^1.56.2"
bn.js "^5.2.0"
"@metaplex-foundation/mpl-bubblegum@^0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-bubblegum/-/mpl-bubblegum-0.6.2.tgz#e1b098ccef10899b0d759a03e3d4b1ae7bdc9f0c"
integrity sha512-4tF7/FFSNtpozuIGD7gMKcqK2D49eVXZ144xiowC5H1iBeu009/oj2m8Tj6n4DpYFKWJ2JQhhhk0a2q7x0Begw==
dependencies:
"@metaplex-foundation/beet" "0.7.1"
"@metaplex-foundation/beet-solana" "0.4.0"
"@metaplex-foundation/cusper" "^0.0.2"
"@metaplex-foundation/mpl-token-metadata" "^2.5.2"
"@solana/spl-account-compression" "^0.1.4"
"@solana/spl-token" "^0.1.8"
"@solana/web3.js" "^1.50.1"
bn.js "^5.2.0"
js-sha3 "^0.8.0"
"@metaplex-foundation/mpl-candy-guard@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-candy-guard/-/mpl-candy-guard-0.3.2.tgz#426e89793676b42e9bbb5e523303fba36ccd5281"
@ -852,10 +869,10 @@
"@solana/spl-token" "^0.1.8"
"@solana/web3.js" "^1.31.0"
"@metaplex-foundation/mpl-token-metadata@^2.8.6":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.9.1.tgz#79b548b60ac4065b438b78e28b0139751f16b186"
integrity sha512-QmeWBG7y2Uu9FyD1JiclPmJtkYA1sd/Vh9US9H9zTGNWnyogM60hqZ9yVcibvkO+aSsWd0ZJIsMXZlewXIx0IQ==
"@metaplex-foundation/mpl-token-metadata@^2.11.0", "@metaplex-foundation/mpl-token-metadata@^2.5.2":
version "2.12.0"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.12.0.tgz#9817b2d133c5af46c28ab284316b6985ef62b331"
integrity sha512-DetC2F5MwMRt4TmLXwj8PJ8nClRYGMecSQ4pr9iKKa+rWertHgKoJHl2XhheRa084GtL7i0ssOKbX2gfYFosuQ==
dependencies:
"@metaplex-foundation/beet" "^0.7.1"
"@metaplex-foundation/beet-solana" "^0.4.0"
@ -877,6 +894,11 @@
dependencies:
glob "7.1.7"
"@next/font@13.4.4":
version "13.4.4"
resolved "https://registry.yarnpkg.com/@next/font/-/font-13.4.4.tgz#513632591a1041e39c1020022c0995ca78dde4fc"
integrity sha512-iYmL/O0rV9NS8a2UXuRoZOzImz8Q0mM8bAmxtj8nccrpwZ6iOOZlbf2d0Genczl4wtuXRXVPR8goGjJM4C2SDg==
"@next/swc-darwin-arm64@13.4.4":
version "13.4.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.4.tgz#8c14083c2478e2a9a8d140cce5900f76b75667ff"
@ -1402,6 +1424,18 @@
dependencies:
buffer "~6.0.3"
"@solana/spl-account-compression@^0.1.4", "@solana/spl-account-compression@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@solana/spl-account-compression/-/spl-account-compression-0.1.8.tgz#0c1fd052befddd90c2e8704b0b685761799d4bae"
integrity sha512-vsvsx358pVFPtyNd8zIZy0lezR0NuvOykQ29Zq+8oto+kHfTXMGXXQ1tKHUYke6XkINIWLFVg/jDi+1D9RYaqQ==
dependencies:
"@metaplex-foundation/beet" "^0.7.1"
"@metaplex-foundation/beet-solana" "^0.4.0"
bn.js "^5.2.1"
borsh "^0.7.0"
js-sha3 "^0.8.0"
typescript-collections "^1.3.3"
"@solana/spl-governance@0.3.27":
version "0.3.27"
resolved "https://registry.yarnpkg.com/@solana/spl-governance/-/spl-governance-0.3.27.tgz#54ab8310a142b3d581d8abc3df37e3511f02619c"
@ -1888,10 +1922,10 @@
"@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.31.0", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.44.3", "@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.0", "@solana/web3.js@^1.73.2":
version "1.77.2"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.77.2.tgz#4b4d71f07efb9aca1f7ab3ae8746c2e79389fe39"
integrity sha512-pKu9S21NGAi6Nsayz2KEdhqOlPUJIr3L911bgQvPg2Dbk/U4gJsk41XGdxyfsfnwKPEI/KbitcByterst4VQ3g==
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.22.0", "@solana/web3.js@^1.31.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.0", "@solana/web3.js@^1.73.2":
version "1.77.3"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.77.3.tgz#2cbeaa1dd24f8fa386ac924115be82354dfbebab"
integrity sha512-PHaO0BdoiQRPpieC1p31wJsBaxwIOWLh8j2ocXNKX8boCQVldt26Jqm2tZE4KlrvnCIV78owPLv1pEUgqhxZ3w==
dependencies:
"@babel/runtime" "^7.12.5"
"@noble/curves" "^1.0.0"
@ -1904,7 +1938,7 @@
bs58 "^4.0.1"
buffer "6.0.3"
fast-stable-stringify "^1.0.0"
jayson "^3.4.4"
jayson "^4.1.0"
node-fetch "^2.6.7"
rpc-websockets "^7.5.1"
superstruct "^0.14.2"
@ -5915,10 +5949,10 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
jayson@^3.4.4:
version "3.7.0"
resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25"
integrity sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==
jayson@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.0.tgz#60dc946a85197317f2b1439d672a8b0a99cea2f9"
integrity sha512-R6JlbyLN53Mjku329XoRT2zJAE6ZgOQ8f91ucYdMCD4nkGCF9kZSrcGXpHIU4jeKj58zUZke2p+cdQchU7Ly7A==
dependencies:
"@types/connect" "^3.4.33"
"@types/node" "^12.12.54"
@ -5930,7 +5964,6 @@ jayson@^3.4.4:
eyes "^0.1.8"
isomorphic-ws "^4.0.1"
json-stringify-safe "^5.0.1"
lodash "^4.17.20"
uuid "^8.3.2"
ws "^7.4.5"
@ -6173,7 +6206,7 @@ lodash.zipobject@^4.1.3:
resolved "https://registry.yarnpkg.com/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz#b399f5aba8ff62a746f6979bf20b214f964dbef8"
integrity sha512-A9SzX4hMKWS25MyalwcOnNoplyHbkNVsjidhTp8ru0Sj23wY9GWBKS8gAIGDSAqeWjIjvE4KBEl24XXAs+v4wQ==
lodash@4.17.21, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
lodash@4.17.21, lodash@^4.17.19, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -7264,6 +7297,13 @@ react-resize-detector@^8.0.4:
dependencies:
lodash "^4.17.21"
react-responsive-pagination@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-responsive-pagination/-/react-responsive-pagination-2.2.0.tgz#9987225e22a6c2d6e707a6977a2ba511b31c7159"
integrity sha512-lqpYdrKTO7d9gLroDFeLeEubrwnqvQcyq1y4HdYC5/if4kmxb695I47LHgFbAwuK5C6Z8JS2lgoVDNdNRPhO7w==
dependencies:
prop-types "^15.8.1"
react-simple-animate@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/react-simple-animate/-/react-simple-animate-3.0.2.tgz#67f29b0c64155d2dfd540c1c74f634ef521536a8"
@ -8590,6 +8630,11 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typescript-collections@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/typescript-collections/-/typescript-collections-1.3.3.tgz#62d50d93c018c094d425eabee649f00ec5cc0fea"
integrity sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ==
typescript@4.9.4, typescript@^4.6.2:
version "4.9.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"