merge main
This commit is contained in:
commit
ed1f680409
|
@ -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[]
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 || '')
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'])
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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')}™
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -510,9 +510,7 @@ const AdvancedTradeForm = () => {
|
|||
</div>
|
||||
{tradeForm.tradeType === 'Market' &&
|
||||
selectedMarket instanceof Serum3Market ? (
|
||||
<>
|
||||
<SpotMarketOrderSwapForm />
|
||||
</>
|
||||
<SpotMarketOrderSwapForm />
|
||||
) : (
|
||||
<>
|
||||
<form onSubmit={(e) => handleSubmit(e)}>
|
||||
|
|
|
@ -28,6 +28,8 @@ const GroupSize = ({
|
|||
formatSize(10),
|
||||
formatSize(50),
|
||||
formatSize(100),
|
||||
formatSize(500),
|
||||
formatSize(1000),
|
||||
],
|
||||
[tickSize],
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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">
|
||||
|
|
|
@ -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 ${
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -10,6 +10,5 @@
|
|||
"slippage-accept": "若您接受潜在的下滑请使用交易表格进行。",
|
||||
"slippage-warning": "您的订单价格({{updatedOrderPrice}})多余5%{{aboveBelow}}市场价格({{selectedMarketPrice}})表是您也许遭受可观的下滑。",
|
||||
"toggle-order-line": "切换订单线可见性",
|
||||
"toggle-stable-price": "切换稳定价格线",
|
||||
"toggle-trade-executions": "显示成交。限于全部试场的最近250交易。"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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": "分組",
|
||||
|
|
|
@ -10,6 +10,5 @@
|
|||
"slippage-accept": "若您接受潛在的下滑請使用交易表格進行。",
|
||||
"slippage-warning": "您的訂單價格({{updatedOrderPrice}})多餘5%{{aboveBelow}}市場價格({{selectedMarketPrice}})表是您也許遭受可觀的下滑。",
|
||||
"toggle-order-line": "切換訂單線可見性",
|
||||
"toggle-stable-price": "切換穩定價格線",
|
||||
"toggle-trade-executions": "顯示成交。限於全部試場的最近250交易。"
|
||||
}
|
|
@ -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: '' },
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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
109
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue