chore: Migrate from Relay to Apollo (#5754)

* feat: initial apollo configutation (#5565)

* initial apollo configutation

* add new files

* check in types-and-hooks

* config unused export

* deduplicate

* ignore checked in schema for linting

* remove prettier ignore

* test unchecking types and hooks file

* undo

* rename codegen, respond to comments

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* Remove maybe value from codegen

* add babel gql codegen

* correct ts graphql-tag

* remove plugin from craco

* chore: migrate Assets Query to Apollo (#5665)

* chore: migrate Assets Query to Apollo

* delete comment

* move length check back to collectionAssets

* remove uneeded check

* respond to comments

* working switching and filters

* change sweep fetch policy

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* chore: migrate collection query to apollo (#5647)

* migrate collection query to apollo

* remove page level suspense

* undo removing page level suspense

* rename query and hook

* guard returns

* add return type prop

* cleanup nullables

* memoizing

* use gql from apollo

* use babel gql and move empty trait

* add fetch policy

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* chore: migrate NFT details query to apollo (#5648)

* chore: migrate NFT details query to apollo

* update todo

* update imports

* remove no longer used hook

* rename query

* use babel gql and nonnullable type

* working page

* add fetchpolicy

* respond to comments

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* chore: migrate NftBalanceQuery (#5653)

* chore: migrate NftBalanceQuery

* cleanup

* update pagination

* better undefined handling

* move brake listing for invalid asset higher

* better handle loading

* memoize and cleanup

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* remove named gql query consts

* set default fetchPolicy

* null suspense

* chore: Migrate The Graph queries (#5727)

* migrate TheGraph queries to Apollo

* add new files

* ignore thegraph generated types

* use standard fetchPolicy

* update apollo codegen commands

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* chore: migrate token queries to Apollo (#5682)

* migrate utils to types-and-hooks

* too many TokenTable re-renders

* working token queries

* fixed sparkline for native asset

* onChangeTimePeriod

* define inline

* use query instead of data in naming

* sparklineQuery instead of sparklineData

* rename to usePriceHistory

* multiline if else

* remove optional

* remove unneeded eslint ignore

* rename tokenQueryLoading

* rename OnChangeTimePeriod

* token address fallback

* just address

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* chore: deprecate Relay (#5747)

* chore: deprecate Relay

* remove graph:ql generate step

* add new files

* apollo to graphql centric naming

* add new files

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* remove no longer needed config exclusions

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2022-12-20 13:42:52 -08:00 committed by GitHub
parent a286e5b114
commit 2aa1b18d14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 8918 additions and 1343 deletions

@ -1,2 +1,4 @@
*.config.ts *.config.ts
*.d.ts *.d.ts
/src/graphql/data/__generated__/types-and-hooks.ts
/src/graphql/thegraph/__generated__/types-and-hooks.ts

1
.gitignore vendored

@ -9,7 +9,6 @@
/src/locales/**/pseudo.po /src/locales/**/pseudo.po
# generated graphql types # generated graphql types
__generated__/
schema.graphql schema.graphql
# dependencies # dependencies

23
apollo-codegen.ts Normal file

@ -0,0 +1,23 @@
import type { CodegenConfig } from '@graphql-codegen/cli'
// Generates TS objects from the schemas returned by graphql queries
// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project
const config: CodegenConfig = {
overwrite: true,
schema: './src/graphql/data/schema.graphql',
documents: ['./src/graphql/data/**', '!./src/graphql/data/__generated__/**', '!**/thegraph/**'],
generates: {
'src/graphql/data/__generated__/types-and-hooks.ts': {
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
config: {
withHooks: true,
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
maybeValue: 'T',
},
},
},
}
// This is used in package.json when generating apollo schemas however the linter stills flags this as unused
// eslint-disable-next-line import/no-unused-modules
export default config

@ -0,0 +1,23 @@
import type { CodegenConfig } from '@graphql-codegen/cli'
// Generates TS objects from the schemas returned by graphql queries
// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project
const config: CodegenConfig = {
overwrite: true,
schema: './src/graphql/thegraph/schema.graphql',
documents: ['!./src/graphql/data/**', '!./src/graphql/thegraph/__generated__/**', './src/graphql/thegraph/**'],
generates: {
'src/graphql/thegraph/__generated__/types-and-hooks.ts': {
plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'],
config: {
withHooks: true,
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
maybeValue: 'T',
},
},
},
}
// This is used in package.json when generating apollo schemas however the linter stills flags this as unused
// eslint-disable-next-line import/no-unused-modules
export default config

@ -1,8 +1,8 @@
/* eslint-disable */ /* eslint-disable */
require('dotenv').config({ path: '.env.production' }) require('dotenv').config({ path: '.env.production' })
const { exec } = require('child_process') const { exec } = require('child_process')
const dataConfig = require('./relay.config') const dataConfig = require('./graphql.config')
const thegraphConfig = require('./relay_thegraph.config') const thegraphConfig = require('./graphql_thegraph.config')
/* eslint-enable */ /* eslint-enable */
function fetchSchema(url, outputFile) { function fetchSchema(url, outputFile) {

@ -1,5 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const defaultConfig = require('./relay.config') const defaultConfig = require('./graphql.config')
module.exports = { module.exports = {
src: defaultConfig.src, src: defaultConfig.src,

@ -8,10 +8,10 @@
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"", "contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"", "contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3", "contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
"relay": "relay-compiler relay.config.js",
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
"graphql:fetch": "node fetch-schema.js", "graphql:fetch": "node fetch-schema.js",
"graphql:generate": "yarn relay && yarn relay-thegraph", "graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
"prei18n:extract": "node prei18n-extract.js", "prei18n:extract": "node prei18n-extract.js",
"i18n:extract": "lingui extract --locale en-US", "i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile", "i18n:compile": "yarn i18n:extract && lingui compile",
@ -94,7 +94,6 @@
"@typescript-eslint/parser": "^4", "@typescript-eslint/parser": "^4",
"@vanilla-extract/babel-plugin": "^1.1.7", "@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11", "@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-relay": "^14.1.0",
"cypress": "^10.3.1", "cypress": "^10.3.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"eslint": "^7.11.0", "eslint": "^7.11.0",
@ -113,16 +112,23 @@
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"react-scripts": "^4.0.3", "react-scripts": "^4.0.3",
"relay-compiler": "^14.1.0",
"serve": "^11.3.2", "serve": "^11.3.2",
"ts-transform-graphql-tag": "^0.2.1",
"typechain": "^5.0.0", "typechain": "^5.0.0",
"typescript": "^4.4.3", "typescript": "^4.4.3",
"yarn-deduplicate": "^6.0.0" "yarn-deduplicate": "^6.0.0"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.7.2",
"@coinbase/wallet-sdk": "^3.3.0", "@coinbase/wallet-sdk": "^3.3.0",
"@fontsource/ibm-plex-mono": "^4.5.1", "@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1", "@fontsource/inter": "^4.5.1",
"@graphql-codegen/cli": "^2.15.0",
"@graphql-codegen/client-preset": "^1.2.1",
"@graphql-codegen/typescript": "^2.8.3",
"@graphql-codegen/typescript-operations": "^2.5.8",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@graphql-codegen/typescript-resolvers": "^2.7.8",
"@lingui/core": "^3.14.0", "@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0", "@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0", "@lingui/react": "^3.14.0",
@ -135,7 +141,6 @@
"@react-hook/window-scroll": "^1.3.0", "@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1", "@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "7.20.1", "@sentry/react": "7.20.1",
"@types/react-relay": "^13.0.2",
"@types/react-window-infinite-loader": "^1.0.6", "@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "1.2.0", "@uniswap/analytics": "1.2.0",
"@uniswap/analytics-events": "^1.5.0", "@uniswap/analytics-events": "^1.5.0",
@ -215,8 +220,6 @@
"react-popper": "^2.2.3", "react-popper": "^2.2.3",
"react-query": "^3.39.1", "react-query": "^3.39.1",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-relay": "^14.1.0",
"react-relay-network-modern": "^6.2.1",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-spring": "^9.5.5", "react-spring": "^9.5.5",
"react-table": "^7.8.0", "react-table": "^7.8.0",

@ -2,7 +2,6 @@ import { SparkLineLoadingBubble } from 'components/Tokens/TokenTable/TokenRow'
import { curveCardinal, scaleLinear } from 'd3' import { curveCardinal, scaleLinear } from 'd3'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens' import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { PricePoint } from 'graphql/data/util' import { PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
import { memo } from 'react' import { memo } from 'react'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
@ -21,18 +20,10 @@ interface SparklineChartProps {
height: number height: number
tokenData: TopToken tokenData: TopToken
pricePercentChange: number | undefined | null pricePercentChange: number | undefined | null
timePeriod: TimePeriod
sparklineMap: SparklineMap sparklineMap: SparklineMap
} }
function _SparklineChart({ function _SparklineChart({ width, height, tokenData, pricePercentChange, sparklineMap }: SparklineChartProps) {
width,
height,
tokenData,
pricePercentChange,
timePeriod,
sparklineMap,
}: SparklineChartProps) {
const theme = useTheme() const theme = useTheme()
// for sparkline // for sparkline
const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null

@ -1,22 +1,19 @@
import { ParentSize } from '@visx/responsive' import { ParentSize } from '@visx/responsive'
import { ChartContainer, LoadingChart } from 'components/Tokens/TokenDetails/Skeleton' import { ChartContainer, LoadingChart } from 'components/Tokens/TokenDetails/Skeleton'
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice' import { TokenPriceQuery } from 'graphql/data/TokenPrice'
import { isPricePoint, PricePoint } from 'graphql/data/util' import { isPricePoint, PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { pageTimePeriodAtom } from 'pages/TokenDetails' import { pageTimePeriodAtom } from 'pages/TokenDetails'
import { startTransition, Suspense, useMemo } from 'react' import { startTransition, Suspense, useMemo } from 'react'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { PriceChart } from './PriceChart' import { PriceChart } from './PriceChart'
import TimePeriodSelector from './TimeSelector' import TimePeriodSelector from './TimeSelector'
function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPriceQuery>): PricePoint[] | undefined { function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
const queryData = usePreloadedQuery(tokenPriceQuery, priceQueryReference)
// Appends the current price to the end of the priceHistory array // Appends the current price to the end of the priceHistory array
const priceHistory = useMemo(() => { const priceHistory = useMemo(() => {
const market = queryData.tokens?.[0]?.market const market = tokenPriceData.tokens?.[0]?.market
const priceHistory = market?.priceHistory?.filter(isPricePoint) const priceHistory = market?.priceHistory?.filter(isPricePoint)
const currentPrice = market?.price?.value const currentPrice = market?.price?.value
if (Array.isArray(priceHistory) && currentPrice !== undefined) { if (Array.isArray(priceHistory) && currentPrice !== undefined) {
@ -24,39 +21,39 @@ function usePreloadedTokenPriceQuery(priceQueryReference: PreloadedQuery<TokenPr
return [...priceHistory, { timestamp, value: currentPrice }] return [...priceHistory, { timestamp, value: currentPrice }]
} }
return priceHistory return priceHistory
}, [queryData]) }, [tokenPriceData])
return priceHistory return priceHistory
} }
export default function ChartSection({ export default function ChartSection({
priceQueryReference, tokenPriceQuery,
refetchTokenPrices, onChangeTimePeriod,
}: { }: {
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined tokenPriceQuery?: TokenPriceQuery
refetchTokenPrices: RefetchPricesFunction onChangeTimePeriod: OnChangeTimePeriod
}) { }) {
if (!priceQueryReference) { if (!tokenPriceQuery) {
return <LoadingChart /> return <LoadingChart />
} }
return ( return (
<Suspense fallback={<LoadingChart />}> <Suspense fallback={<LoadingChart />}>
<ChartContainer> <ChartContainer>
<Chart priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} /> <Chart tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
</ChartContainer> </ChartContainer>
</Suspense> </Suspense>
) )
} }
export type RefetchPricesFunction = (t: TimePeriod) => void export type OnChangeTimePeriod = (t: TimePeriod) => void
function Chart({ function Chart({
priceQueryReference, tokenPriceQuery,
refetchTokenPrices, onChangeTimePeriod,
}: { }: {
priceQueryReference: PreloadedQuery<TokenPriceQuery> tokenPriceQuery: TokenPriceQuery
refetchTokenPrices: RefetchPricesFunction onChangeTimePeriod: OnChangeTimePeriod
}) { }) {
const prices = usePreloadedTokenPriceQuery(priceQueryReference) const prices = usePriceHistory(tokenPriceQuery)
// Initializes time period to global & maintain separate time period for subsequent changes // Initializes time period to global & maintain separate time period for subsequent changes
const timePeriod = useAtomValue(pageTimePeriodAtom) const timePeriod = useAtomValue(pageTimePeriodAtom)
@ -68,7 +65,7 @@ function Chart({
<TimePeriodSelector <TimePeriodSelector
currentTimePeriod={timePeriod} currentTimePeriod={timePeriod}
onTimeChange={(t: TimePeriod) => { onTimeChange={(t: TimePeriod) => {
startTransition(() => refetchTokenPrices(t)) startTransition(() => onChangeTimePeriod(t))
}} }}
/> />
</ChartContainer> </ChartContainer>

@ -27,21 +27,20 @@ import Widget from 'components/Widget'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { TokenPriceQuery } from 'graphql/data/__generated__/TokenPriceQuery.graphql' import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token' import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
import { QueryToken, tokenQuery } from 'graphql/data/Token' import { QueryToken } from 'graphql/data/Token'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens' import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency' import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
import { useCallback, useMemo, useState, useTransition } from 'react' import { useCallback, useMemo, useState, useTransition } from 'react'
import { ArrowLeft } from 'react-feather' import { ArrowLeft } from 'react-feather'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { isAddress } from 'utils' import { isAddress } from 'utils'
import { RefetchPricesFunction } from './ChartSection' import { OnChangeTimePeriod } from './ChartSection'
import InvalidTokenDetails from './InvalidTokenDetails' import InvalidTokenDetails from './InvalidTokenDetails'
const TokenSymbol = styled.span` const TokenSymbol = styled.span`
@ -75,7 +74,7 @@ function useRelevantToken(
const queryToken = useMemo(() => { const queryToken = useMemo(() => {
if (!address) return undefined if (!address) return undefined
if (address === NATIVE_CHAIN_ID) return nativeOnChain(pageChainId) if (address === NATIVE_CHAIN_ID) return nativeOnChain(pageChainId)
if (tokenQueryData) return new QueryToken(tokenQueryData) if (tokenQueryData) return new QueryToken(address, tokenQueryData)
return undefined return undefined
}, [pageChainId, address, tokenQueryData]) }, [pageChainId, address, tokenQueryData])
// fetches on-chain token if query data is missing and page chain matches global chain (else fetch won't work) // fetches on-chain token if query data is missing and page chain matches global chain (else fetch won't work)
@ -91,16 +90,16 @@ function useRelevantToken(
type TokenDetailsProps = { type TokenDetailsProps = {
urlAddress: string | undefined urlAddress: string | undefined
chain: Chain chain: Chain
tokenQueryReference: PreloadedQuery<TokenQuery> tokenQuery: TokenQuery
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined tokenPriceQuery: TokenPriceQuery | undefined
refetchTokenPrices: RefetchPricesFunction onChangeTimePeriod: OnChangeTimePeriod
} }
export default function TokenDetails({ export default function TokenDetails({
urlAddress, urlAddress,
chain, chain,
tokenQueryReference, tokenQuery,
priceQueryReference, tokenPriceQuery,
refetchTokenPrices, onChangeTimePeriod,
}: TokenDetailsProps) { }: TokenDetailsProps) {
if (!urlAddress) { if (!urlAddress) {
throw new Error('Invalid token details route: tokenAddress param is undefined') throw new Error('Invalid token details route: tokenAddress param is undefined')
@ -112,7 +111,7 @@ export default function TokenDetails({
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain] const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const tokenQueryData = usePreloadedQuery(tokenQuery, tokenQueryReference).tokens?.[0] const tokenQueryData = tokenQuery.tokens?.[0]
const crossChainMap = useMemo( const crossChainMap = useMemo(
() => () =>
tokenQueryData?.project?.tokens.reduce((map, current) => { tokenQueryData?.project?.tokens.reduce((map, current) => {
@ -200,7 +199,7 @@ export default function TokenDetails({
<ShareButton currency={token} /> <ShareButton currency={token} />
</TokenActions> </TokenActions>
</TokenInfoContainer> </TokenInfoContainer>
<ChartSection priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} /> <ChartSection tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
<StatsSection <StatsSection
TVL={tokenQueryData?.market?.totalValueLocked?.value} TVL={tokenQueryData?.market?.totalValueLocked?.value}
volume24H={tokenQueryData?.market?.volume24H?.value} volume24H={tokenQueryData?.market?.volume24H?.value}

@ -459,7 +459,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
return ( return (
<div ref={ref} data-testid={`token-table-row-${tokenName}`}> <div ref={ref} data-testid={`token-table-row-${tokenName}`}>
<StyledLink <StyledLink
to={getTokenDetailsURL(token.address, token.chain)} to={getTokenDetailsURL(token.address ?? '', token.chain)}
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)} onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
> >
<TokenRow <TokenRow
@ -512,7 +512,6 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
height={height} height={height}
tokenData={token} tokenData={token}
pricePercentChange={token.market?.pricePercentChange?.value} pricePercentChange={token.market?.pricePercentChange?.value}
timePeriod={timePeriod}
sparklineMap={props.sparklineMap} sparklineMap={props.sparklineMap}
/> />
) )

@ -64,7 +64,7 @@ const LoadingRows = ({ rowCount }: { rowCount: number }) => (
</> </>
) )
export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) { function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
return ( return (
<GridContainer> <GridContainer>
<HeaderRow /> <HeaderRow />
@ -75,14 +75,15 @@ export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number
) )
} }
export default function TokenTable({ setRowCount }: { setRowCount: (c: number) => void }) { export default function TokenTable() {
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s // TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName) const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { tokens, sparklines } = useTopTokens(chainName) const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
setRowCount(tokens?.length ?? PAGE_SIZE)
/* loading and error state */ /* loading and error state */
if (!tokens) { if (loadingTokens) {
return <LoadingTokenTable rowCount={PAGE_SIZE} />
} else if (!tokens) {
return ( return (
<NoTokensState <NoTokensState
message={ message={

@ -1,57 +0,0 @@
import ms from 'ms.macro'
import {
RelayNetworkLayer,
RelayNetworkLayerResponse,
retryMiddleware,
urlMiddleware,
} from 'react-relay-network-modern'
import { Environment, RecordSource, Store } from 'relay-runtime'
// This makes it possible (and more likely) to be able to reuse data when navigating back to a page,
// tab or piece of content that has been visited before. These settings together configure the cache
// to serve the last 250 records, so long as they are less than 5 minutes old:
const gcReleaseBufferSize = 250
const queryCacheExpirationTime = ms`5m`
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
if (!GRAPHQL_URL) {
throw new Error('AWS URL MISSING FROM ENVIRONMENT')
}
const RETRY_TIME_MS = [3200, 6400, 12800]
// This network layer must not cache, or it will break cache-evicting network policies
const network = new RelayNetworkLayer(
[
urlMiddleware({
url: GRAPHQL_URL,
headers: {
'Content-Type': 'application/json',
},
}),
function logAndIgnoreErrors(next) {
return async (req) => {
try {
const res = await next(req)
if (!res || !res.data) throw new Error('Missing response data')
return res
} catch (e) {
console.error(e)
return RelayNetworkLayerResponse.createFromGraphQL({ data: [] })
}
}
},
retryMiddleware({
fetchTimeout: ms`30s`, // mirrors backend's timeout in case that fails
retryDelays: RETRY_TIME_MS,
statusCodes: (statusCode) => statusCode >= 500 && statusCode < 600,
}),
],
{ noThrow: true }
)
const CachingEnvironment = new Environment({
network,
store: new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime }),
})
export default CachingEnvironment

@ -1,8 +1,8 @@
import graphql from 'babel-plugin-relay/macro'
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens' import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
import gql from 'graphql-tag'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo' import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { TokenQuery$data } from './__generated__/TokenQuery.graphql' import { TokenQuery } from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID } from './util' import { CHAIN_NAME_TO_CHAIN_ID } from './util'
/* /*
@ -13,14 +13,14 @@ The difference between Token and TokenProject:
TokenMarket is per-chain market data for contracts pulled from the graph. TokenMarket is per-chain market data for contracts pulled from the graph.
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko. TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/ */
export const tokenQuery = graphql` gql`
query TokenQuery($contract: ContractInput!) { query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) { tokens(contracts: [$contract]) {
id @required(action: LOG) id
decimals decimals
name name
chain @required(action: LOG) chain
address @required(action: LOG) address
symbol symbol
market(currency: USD) { market(currency: USD) {
totalValueLocked { totalValueLocked {
@ -48,23 +48,24 @@ export const tokenQuery = graphql`
twitterName twitterName
logoUrl logoUrl
tokens { tokens {
chain @required(action: LOG) chain
address @required(action: LOG) address
} }
} }
} }
} }
` `
export type { Chain, TokenQuery } from './__generated__/TokenQuery.graphql'
export type TokenQueryData = NonNullable<TokenQuery$data['tokens']>[number] export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces. // TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo { export class QueryToken extends WrappedTokenInfo {
constructor(data: NonNullable<TokenQueryData>, logoSrc?: string) { constructor(address: string, data: NonNullable<TokenQueryData>, logoSrc?: string) {
super({ super({
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain], chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
address: data.address, address,
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS, decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
symbol: data.symbol ?? '', symbol: data.symbol ?? '',
name: data.name ?? '', name: data.name ?? '',

@ -1,19 +1,19 @@
import graphql from 'babel-plugin-relay/macro' import gql from 'graphql-tag'
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support // TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
export const tokenPriceQuery = graphql` gql`
query TokenPriceQuery($contract: ContractInput!, $duration: HistoryDuration!) { query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) { tokens(contracts: [$contract]) {
market(currency: USD) @required(action: LOG) { market(currency: USD) {
price { price {
value @required(action: LOG) value
} }
priceHistory(duration: $duration) { priceHistory(duration: $duration) {
timestamp @required(action: LOG) timestamp
value @required(action: LOG) value
} }
} }
} }
} }
` `
export type { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql' export type { TokenPriceQuery } from './__generated__/types-and-hooks'

@ -1,4 +1,3 @@
import graphql from 'babel-plugin-relay/macro'
import { import {
filterStringAtom, filterStringAtom,
filterTimeAtom, filterTimeAtom,
@ -6,22 +5,25 @@ import {
sortMethodAtom, sortMethodAtom,
TokenSortMethod, TokenSortMethod,
} from 'components/Tokens/state' } from 'components/Tokens/state'
import gql from 'graphql-tag'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { useEffect, useMemo, useState } from 'react' import { useMemo } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql' import {
import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql' Chain,
import { isPricePoint, PricePoint } from './util' TopTokens100Query,
import { CHAIN_NAME_TO_CHAIN_ID, toHistoryDuration, unwrapToken } from './util' useTopTokens100Query,
useTopTokensSparklineQuery,
} from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
const topTokens100Query = graphql` gql`
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) { query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) { topTokens(pageSize: 100, page: 1, chain: $chain) {
id @required(action: LOG) id
name name
chain @required(action: LOG) chain
address @required(action: LOG) address
symbol symbol
market(currency: USD) { market(currency: USD) {
totalValueLocked { totalValueLocked {
@ -48,21 +50,21 @@ const topTokens100Query = graphql`
} }
` `
const tokenSparklineQuery = graphql` gql`
query TopTokensSparklineQuery($duration: HistoryDuration!, $chain: Chain!) { query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) { topTokens(pageSize: 100, page: 1, chain: $chain) {
address address
market(currency: USD) { market(currency: USD) {
priceHistory(duration: $duration) { priceHistory(duration: $duration) {
timestamp @required(action: LOG) timestamp
value @required(action: LOG) value
} }
} }
} }
} }
` `
function useSortedTokens(tokens: NonNullable<TopTokens100Query['response']['topTokens']>) { function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
const sortMethod = useAtomValue(sortMethodAtom) const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom) const sortAscending = useAtomValue(sortAscendingAtom)
@ -91,7 +93,7 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['response']['topT
}, [tokens, sortMethod, sortAscending]) }, [tokens, sortMethod, sortAscending])
} }
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']['topTokens']>) { function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
const filterString = useAtomValue(filterStringAtom) const filterString = useAtomValue(filterStringAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString]) const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
@ -112,11 +114,12 @@ function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']['to
// Number of items to render in each fetch in infinite scroll. // Number of items to render in each fetch in infinite scroll.
export const PAGE_SIZE = 20 export const PAGE_SIZE = 20
export type TopToken = NonNullable<NonNullable<TopTokens100Query['response']>['topTokens']>[number]
export type SparklineMap = { [key: string]: PricePoint[] | undefined } export type SparklineMap = { [key: string]: PricePoint[] | undefined }
export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number]
interface UseTopTokensReturnValue { interface UseTopTokensReturnValue {
tokens: TopToken[] | undefined tokens: TopToken[] | undefined
loadingTokens: boolean
sparklines: SparklineMap sparklines: SparklineMap
} }
@ -124,33 +127,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain] const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const duration = toHistoryDuration(useAtomValue(filterTimeAtom)) const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const environment = useRelayEnvironment() const { data: sparklineQuery } = useTopTokensSparklineQuery({
const [sparklines, setSparklines] = useState<SparklineMap>({}) variables: { duration, chain },
useEffect(() => { })
const subscription = fetchQuery<TopTokensSparklineQuery>(environment, tokenSparklineQuery, { duration, chain })
.map((data) => ({
topTokens: data.topTokens?.map((token) => unwrapToken(chainId, token)),
}))
.subscribe({
next(data) {
const map: SparklineMap = {}
data.topTokens?.forEach(
(current) =>
current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint))
)
setSparklines(map)
},
})
return () => subscription.unsubscribe()
}, [chain, chainId, duration, environment])
useEffect(() => { const sparklines = useMemo(() => {
setSparklines({}) const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
}, [duration]) const map: SparklineMap = {}
unwrappedTokens?.forEach(
(current) => current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint))
)
return map
}, [chainId, sparklineQuery?.topTokens])
const { topTokens } = useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain }) const { data, loading: loadingTokens } = useTopTokens100Query({
const mappedTokens = useMemo(() => topTokens?.map((token) => unwrapToken(chainId, token)) ?? [], [chainId, topTokens]) variables: { duration, chain },
})
const mappedTokens = useMemo(
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
[chainId, data]
)
const filteredTokens = useFilteredTokens(mappedTokens) const filteredTokens = useFilteredTokens(mappedTokens)
const sortedTokens = useSortedTokens(filteredTokens) const sortedTokens = useSortedTokens(filteredTokens)
return useMemo(() => ({ tokens: sortedTokens, sparklines }), [sortedTokens, sparklines]) return useMemo(() => ({ tokens: sortedTokens, loadingTokens, sparklines }), [loadingTokens, sortedTokens, sparklines])
} }

File diff suppressed because it is too large Load Diff

@ -0,0 +1,30 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { relayStylePagination } from '@apollo/client/utilities'
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
if (!GRAPHQL_URL) {
throw new Error('AWS URL MISSING FROM ENVIRONMENT')
}
export const apolloClient = new ApolloClient({
uri: GRAPHQL_URL,
headers: {
'Content-Type': 'application/json',
Origin: 'https://app.uniswap.org',
},
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
nftBalances: relayStylePagination(),
nftAssets: relayStylePagination(),
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
})

@ -1,25 +1,30 @@
import graphql from 'babel-plugin-relay/macro'
import { parseEther } from 'ethers/lib/utils' import { parseEther } from 'ethers/lib/utils'
import useInterval from 'lib/hooks/useInterval' import gql from 'graphql-tag'
import ms from 'ms.macro' import { GenieAsset, Markets, Trait } from 'nft/types'
import { GenieAsset, Trait } from 'nft/types'
import { wrapScientificNotation } from 'nft/utils' import { wrapScientificNotation } from 'nft/utils'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useMemo } from 'react'
import { fetchQuery, useLazyLoadQuery, usePaginationFragment, useQueryLoader, useRelayEnvironment } from 'react-relay'
import { AssetPaginationQuery } from './__generated__/AssetPaginationQuery.graphql'
import { import {
AssetQuery, AssetQueryVariables,
AssetQuery$variables, NftAssetEdge,
NftAssetsFilterInput, NftAssetsFilterInput,
NftAssetSortableField, NftAssetSortableField,
NftAssetTraitInput, NftAssetTraitInput,
NftMarketplace, NftMarketplace,
} from './__generated__/AssetQuery.graphql' useAssetQuery,
import { AssetQuery_nftAssets$data } from './__generated__/AssetQuery_nftAssets.graphql' } from '../__generated__/types-and-hooks'
const assetPaginationQuery = graphql` gql`
fragment AssetQuery_nftAssets on Query @refetchable(queryName: "AssetPaginationQuery") { query Asset(
$address: String!
$orderBy: NftAssetSortableField
$asc: Boolean
$filter: NftAssetsFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
nftAssets( nftAssets(
address: $address address: $address
orderBy: $orderBy orderBy: $orderBy
@ -29,7 +34,7 @@ const assetPaginationQuery = graphql`
after: $after after: $after
last: $last last: $last
before: $before before: $before
) @connection(key: "AssetQuery_nftAssets") { ) {
edges { edges {
node { node {
id id
@ -99,52 +104,38 @@ const assetPaginationQuery = graphql`
} }
metadataUrl metadataUrl
} }
cursor
} }
totalCount totalCount
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
} }
} }
` `
const assetQuery = graphql` function formatAssetQueryData(queryAsset: NftAssetEdge, totalCount?: number) {
query AssetQuery(
$address: String!
$orderBy: NftAssetSortableField
$asc: Boolean
$filter: NftAssetsFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
...AssetQuery_nftAssets
}
`
type NftAssetsQueryAsset = NonNullable<
NonNullable<NonNullable<AssetQuery_nftAssets$data['nftAssets']>['edges']>[number]
>
function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: number) {
const asset = queryAsset.node const asset = queryAsset.node
const ethPrice = parseEther(wrapScientificNotation(asset.listings?.edges[0]?.node.price.value ?? 0)).toString() const ethPrice = parseEther(wrapScientificNotation(asset.listings?.edges[0]?.node.price.value ?? 0)).toString()
return { return {
id: asset.id, id: asset.id,
address: asset?.collection?.nftContracts?.[0]?.address, address: asset?.collection?.nftContracts?.[0]?.address ?? '',
notForSale: asset.listings?.edges?.length === 0, notForSale: asset.listings?.edges?.length === 0,
collectionName: asset.collection?.name, collectionName: asset.collection?.name,
collectionSymbol: asset.collection?.image?.url, collectionSymbol: asset.collection?.image?.url,
imageUrl: asset.image?.url, imageUrl: asset.image?.url,
animationUrl: asset.animationUrl, animationUrl: asset.animationUrl,
marketplace: asset.listings?.edges[0]?.node?.marketplace?.toLowerCase(), marketplace: asset.listings?.edges[0]?.node?.marketplace?.toLowerCase() as unknown as Markets,
name: asset.name, name: asset.name,
priceInfo: asset.listings priceInfo: {
? { ETHPrice: ethPrice,
ETHPrice: ethPrice, baseAsset: 'ETH',
baseAsset: 'ETH', baseDecimals: '18',
baseDecimals: '18', basePrice: ethPrice,
basePrice: ethPrice, },
}
: undefined,
susFlag: asset.suspiciousFlag, susFlag: asset.suspiciousFlag,
sellorders: asset.listings?.edges.map((listingNode) => { sellorders: asset.listings?.edges.map((listingNode) => {
return { return {
@ -155,7 +146,7 @@ function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: numb
} }
}), }),
smallImageUrl: asset.smallImage?.url, smallImageUrl: asset.smallImage?.url,
tokenId: asset.tokenId, tokenId: asset.tokenId ?? '',
tokenType: asset.collection?.nftContracts?.[0]?.standard, tokenType: asset.collection?.nftContracts?.[0]?.standard,
totalCount, totalCount,
collectionIsVerified: asset.collection?.isVerified, collectionIsVerified: asset.collection?.isVerified,
@ -168,7 +159,7 @@ function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: numb
} }
}), }),
}, },
owner: asset.ownerAddress, ownerAddress: asset.ownerAddress,
creator: { creator: {
profile_img_url: asset.collection?.creator?.profileImage?.url, profile_img_url: asset.collection?.creator?.profileImage?.url,
address: asset.collection?.creator?.address, address: asset.collection?.creator?.address,
@ -190,57 +181,50 @@ export interface AssetFetcherParams {
before?: string before?: string
} }
const defaultAssetFetcherParams: Omit<AssetQuery$variables, 'address'> = { const defaultAssetFetcherParams: Omit<AssetQueryVariables, 'address'> = {
orderBy: 'PRICE', orderBy: NftAssetSortableField.Price,
asc: true, asc: true,
// tokenSearchQuery must be specified so that this exactly matches the initial query. // tokenSearchQuery must be specified so that this exactly matches the initial query.
filter: { listed: false, tokenSearchQuery: '' }, filter: { listed: false, tokenSearchQuery: '' },
first: ASSET_PAGE_SIZE, first: ASSET_PAGE_SIZE,
} }
export function useLoadAssetsQuery(address?: string) { export function useNftAssets(params: AssetFetcherParams) {
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery) const variables = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params])
useEffect(() => {
if (address) {
loadQuery({ ...defaultAssetFetcherParams, address })
}
}, [address, loadQuery])
}
export function useLazyLoadAssetsQuery(params: AssetFetcherParams) { const { data, loading, fetchMore } = useAssetQuery({
const vars = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params]) variables,
const [fetchKey, setFetchKey] = useState(0) })
// Use the store if it is available (eg from polling), or the network if it is not (eg from an incorrect preload). const hasNext = data?.nftAssets?.pageInfo?.hasNextPage
const fetchPolicy = 'store-or-network' const loadMore = useCallback(
const queryData = useLazyLoadQuery<AssetQuery>(assetQuery, vars, { fetchKey, fetchPolicy }) // this will suspend if not yet loaded () =>
fetchMore({
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<AssetPaginationQuery, any>( variables: {
assetPaginationQuery, after: data?.nftAssets?.pageInfo?.endCursor,
queryData },
}),
[data, fetchMore]
) )
// Poll for updates. // TODO: setup polling while handling pagination
const POLLING_INTERVAL = ms`5s`
const environment = useRelayEnvironment()
const poll = useCallback(async () => {
if (data.nftAssets?.edges?.length > ASSET_PAGE_SIZE) return
// Initiate a network request. When it resolves, refresh the UI from store (to avoid re-triggering Suspense);
// see: https://relay.dev/docs/guided-tour/refetching/refreshing-queries/#if-you-need-to-avoid-suspense-1.
await fetchQuery<AssetQuery>(environment, assetQuery, { ...vars }).toPromise()
setFetchKey((fetchKey) => fetchKey + 1)
}, [data.nftAssets?.edges?.length, environment, vars])
useInterval(poll, isLoadingNext ? null : POLLING_INTERVAL, /* leading= */ false)
// It is especially important for this to be memoized to avoid re-rendering from polling if data is unchanged. // It is especially important for this to be memoized to avoid re-rendering from polling if data is unchanged.
const assets: GenieAsset[] = useMemo( const assets: GenieAsset[] | undefined = useMemo(
() => () =>
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => { data?.nftAssets?.edges?.map((queryAsset) => {
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount) return formatAssetQueryData(queryAsset as NonNullable<NftAssetEdge>, data.nftAssets?.totalCount)
}), }),
[data.nftAssets?.edges, data.nftAssets?.totalCount] [data?.nftAssets?.edges, data?.nftAssets?.totalCount]
) )
return { assets, hasNext, isLoadingNext, loadNext } return useMemo(() => {
return {
data: assets,
hasNext,
loading,
loadMore,
}
}, [assets, hasNext, loadMore, loading])
} }
const DEFAULT_SWEEP_AMOUNT = 50 const DEFAULT_SWEEP_AMOUNT = 50
@ -252,7 +236,7 @@ export interface SweepFetcherParams {
traits?: Trait[] traits?: Trait[]
} }
function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQuery$variables { function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQueryVariables {
const filter: NftAssetsFilterInput = useMemo( const filter: NftAssetsFilterInput = useMemo(
() => ({ () => ({
listed: true, listed: true,
@ -272,7 +256,7 @@ function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepF
return useMemo( return useMemo(
() => ({ () => ({
address: contractAddress, address: contractAddress,
orderBy: 'PRICE', orderBy: NftAssetSortableField.Price,
asc: true, asc: true,
first: DEFAULT_SWEEP_AMOUNT, first: DEFAULT_SWEEP_AMOUNT,
filter, filter,
@ -281,28 +265,19 @@ function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepF
) )
} }
export function useLoadSweepAssetsQuery(params: SweepFetcherParams, enabled = true) { export function useSweepNftAssets(params: SweepFetcherParams) {
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery) const variables = useSweepFetcherVars(params)
const vars = useSweepFetcherVars(params) const { data, loading } = useAssetQuery({
useEffect(() => { variables,
if (enabled) { // This prevents overwriting the page's call to assets for cards shown
loadQuery(vars) fetchPolicy: 'no-cache',
} })
}, [loadQuery, enabled, vars]) const assets = useMemo<GenieAsset[] | undefined>(
}
// Lazy-loads an already loaded AssetsQuery.
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to
// prevent waterfalling. Use useLoadSweepAssetsQuery to trigger the query.
export function useLazyLoadSweepAssetsQuery(params: SweepFetcherParams): GenieAsset[] {
const vars = useSweepFetcherVars(params)
const queryData = useLazyLoadQuery(assetQuery, vars, { fetchPolicy: 'store-only' }) // this will suspend if not yet loaded
const { data } = usePaginationFragment<AssetPaginationQuery, any>(assetPaginationQuery, queryData)
return useMemo<GenieAsset[]>(
() => () =>
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => { data?.nftAssets?.edges?.map((queryAsset) => {
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount) return formatAssetQueryData(queryAsset as NonNullable<NftAssetEdge>, data.nftAssets?.totalCount)
}), }),
[data.nftAssets?.edges, data.nftAssets?.totalCount] [data?.nftAssets?.edges, data?.nftAssets?.totalCount]
) )
return useMemo(() => ({ data: assets, loading }), [assets, loading])
} }

@ -1,12 +1,11 @@
import graphql from 'babel-plugin-relay/macro' import gql from 'graphql-tag'
import { GenieCollection, Trait } from 'nft/types' import { GenieCollection, Trait } from 'nft/types'
import { useEffect } from 'react' import { useMemo } from 'react'
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
import { CollectionQuery } from './__generated__/CollectionQuery.graphql' import { NftCollection, useCollectionQuery } from '../__generated__/types-and-hooks'
const collectionQuery = graphql` gql`
query CollectionQuery($addresses: [String!]!) { query Collection($addresses: [String!]!) {
nftCollections(filter: { addresses: $addresses }) { nftCollections(filter: { addresses: $addresses }) {
edges { edges {
cursor cursor
@ -87,28 +86,23 @@ const collectionQuery = graphql`
} }
` `
export function useLoadCollectionQuery(address?: string | string[]): void { interface useCollectionReturnProps {
const [, loadQuery] = useQueryLoader(collectionQuery) data: GenieCollection
useEffect(() => { loading: boolean
if (address) {
loadQuery({ addresses: Array.isArray(address) ? address : [address] })
}
}, [address, loadQuery])
} }
// Lazy-loads an already loaded CollectionQuery. export function useCollection(address: string): useCollectionReturnProps {
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to const { data: queryData, loading } = useCollectionQuery({
// prevent waterfalling. Use useLoadCollectionQuery to trigger the query. variables: {
export function useCollectionQuery(address: string): GenieCollection { addresses: address,
const queryData = useLazyLoadQuery<CollectionQuery>( // this will suspend if not yet loaded },
collectionQuery, })
{ addresses: [address] },
{ fetchPolicy: 'store-or-network' }
)
const queryCollection = queryData.nftCollections?.edges[0]?.node const queryCollection = queryData?.nftCollections?.edges?.[0]?.node as NonNullable<NftCollection>
const market = queryCollection?.markets && queryCollection?.markets[0] const market = queryCollection?.markets?.[0]
const traits = {} as Record<string, Trait[]> const traits = useMemo(() => {
return {} as Record<string, Trait[]>
}, [])
if (queryCollection?.traits) { if (queryCollection?.traits) {
queryCollection?.traits.forEach((trait) => { queryCollection?.traits.forEach((trait) => {
if (trait.name && trait.stats) { if (trait.name && trait.stats) {
@ -122,42 +116,43 @@ export function useCollectionQuery(address: string): GenieCollection {
} }
}) })
} }
return { return useMemo(() => {
address, return {
isVerified: queryCollection?.isVerified ?? undefined, data: {
name: queryCollection?.name ?? undefined, address,
description: queryCollection?.description ?? undefined, isVerified: queryCollection?.isVerified,
standard: queryCollection?.nftContracts ? queryCollection?.nftContracts[0]?.standard ?? undefined : undefined, name: queryCollection?.name,
bannerImageUrl: queryCollection?.bannerImage?.url ?? undefined, description: queryCollection?.description,
stats: queryCollection?.markets standard: queryCollection?.nftContracts?.[0]?.standard,
? { bannerImageUrl: queryCollection?.bannerImage?.url,
num_owners: market?.owners ?? undefined, stats: {
floor_price: market?.floorPrice?.value ?? undefined, num_owners: market?.owners,
one_day_volume: market?.volume?.value ?? undefined, floor_price: market?.floorPrice?.value,
one_day_change: market?.volumePercentChange?.value ?? undefined, one_day_volume: market?.volume?.value,
one_day_floor_change: market?.floorPricePercentChange?.value ?? undefined, one_day_change: market?.volumePercentChange?.value,
banner_image_url: queryCollection?.bannerImage?.url ?? undefined, one_day_floor_change: market?.floorPricePercentChange?.value,
total_supply: queryCollection?.numAssets ?? undefined, banner_image_url: queryCollection?.bannerImage?.url,
total_listings: market?.listings?.value ?? undefined, total_supply: queryCollection?.numAssets,
total_volume: market?.totalVolume?.value ?? undefined, total_listings: market?.listings?.value,
} total_volume: market?.totalVolume?.value,
: {}, },
traits, traits,
marketplaceCount: queryCollection?.markets marketplaceCount: market?.marketplaces?.map((market) => {
? market?.marketplaces?.map((market) => {
return { return {
marketplace: market.marketplace?.toLowerCase() ?? '', marketplace: market.marketplace?.toLowerCase() ?? '',
count: market.listings ?? 0, count: market.listings ?? 0,
floorPrice: market.floorPrice ?? 0, floorPrice: market.floorPrice ?? 0,
} }
}) }),
: undefined, imageUrl: queryCollection?.image?.url ?? '',
imageUrl: queryCollection?.image?.url ?? '', twitterUrl: queryCollection?.twitterName,
twitterUrl: queryCollection?.twitterName ?? '', instagram: queryCollection?.instagramName,
instagram: queryCollection?.instagramName ?? undefined, discordUrl: queryCollection?.discordUrl,
discordUrl: queryCollection?.discordUrl ?? undefined, externalUrl: queryCollection?.homepageUrl,
externalUrl: queryCollection?.homepageUrl ?? undefined, rarityVerified: false, // TODO update when backend supports
rarityVerified: false, // TODO update when backend supports // isFoundation: boolean, // TODO ask backend to add
// isFoundation: boolean, // TODO ask backend to add },
} loading,
}
}, [address, loading, market, queryCollection, traits])
} }

@ -1,13 +1,12 @@
import { parseEther } from '@ethersproject/units' import { parseEther } from '@ethersproject/units'
import graphql from 'babel-plugin-relay/macro' import gql from 'graphql-tag'
import { CollectionInfoForAsset, GenieAsset, SellOrder, TokenType } from 'nft/types' import { CollectionInfoForAsset, GenieAsset, Markets, SellOrder } from 'nft/types'
import { useEffect } from 'react' import { useMemo } from 'react'
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
import { DetailsQuery } from './__generated__/DetailsQuery.graphql' import { NftAsset, useDetailsQuery } from '../__generated__/types-and-hooks'
const detailsQuery = graphql` gql`
query DetailsQuery($address: String!, $tokenId: String!) { query Details($address: String!, $tokenId: String!) {
nftAssets(address: $address, filter: { listed: false, tokenIds: [$tokenId] }) { nftAssets(address: $address, filter: { listed: false, tokenIds: [$tokenId] }) {
edges { edges {
node { node {
@ -92,92 +91,87 @@ const detailsQuery = graphql`
} }
` `
export function useLoadDetailsQuery(address?: string, tokenId?: string): void { export function useNftAssetDetails(
const [, loadQuery] = useQueryLoader(detailsQuery) address: string,
useEffect(() => { tokenId: string
if (address && tokenId) { ): { data: [GenieAsset, CollectionInfoForAsset]; loading: boolean } {
loadQuery({ address, tokenId }) const { data: queryData, loading } = useDetailsQuery({
} variables: {
}, [address, tokenId, loadQuery])
}
export function useDetailsQuery(address: string, tokenId: string): [GenieAsset, CollectionInfoForAsset] | undefined {
const queryData = useLazyLoadQuery<DetailsQuery>(
detailsQuery,
{
address, address,
tokenId, tokenId,
}, },
{ fetchPolicy: 'store-or-network' } })
)
const asset = queryData.nftAssets?.edges[0]?.node const asset = queryData?.nftAssets?.edges[0]?.node as NonNullable<NftAsset> | undefined
const collection = asset?.collection const collection = asset?.collection
const listing = asset?.listings?.edges[0]?.node const listing = asset?.listings?.edges[0]?.node
const ethPrice = parseEther(listing?.price?.value?.toString() ?? '0').toString() const ethPrice = parseEther(listing?.price?.value?.toString() ?? '0').toString()
return [ return useMemo(
{ () => ({
id: asset?.id, data: [
address, {
notForSale: asset?.listings === null, id: asset?.id,
collectionName: asset?.collection?.name ?? undefined, address,
collectionSymbol: asset?.collection?.image?.url ?? undefined, notForSale: asset?.listings === null,
imageUrl: asset?.image?.url ?? undefined, collectionName: asset?.collection?.name,
animationUrl: asset?.animationUrl ?? undefined, collectionSymbol: asset?.collection?.image?.url,
// todo: fix the back/frontend discrepency here and drop the any imageUrl: asset?.image?.url,
marketplace: listing?.marketplace.toLowerCase() as any, animationUrl: asset?.animationUrl,
name: asset?.name ?? undefined, marketplace: listing?.marketplace.toLowerCase() as unknown as Markets,
priceInfo: { name: asset?.name,
ETHPrice: ethPrice, priceInfo: {
baseAsset: 'ETH', ETHPrice: ethPrice,
baseDecimals: '18', baseAsset: 'ETH',
basePrice: ethPrice, baseDecimals: '18',
}, basePrice: ethPrice,
susFlag: asset?.suspiciousFlag ?? undefined, },
sellorders: asset?.listings?.edges.map((listingNode) => { susFlag: asset?.suspiciousFlag,
return { sellorders: asset?.listings?.edges.map((listingNode) => {
...listingNode.node, return {
protocolParameters: listingNode.node.protocolParameters ...listingNode.node,
? JSON.parse(listingNode.node.protocolParameters.toString()) protocolParameters: listingNode.node.protocolParameters
: undefined, ? JSON.parse(listingNode.node.protocolParameters.toString())
} as SellOrder : undefined,
}), } as SellOrder
smallImageUrl: asset?.smallImage?.url ?? undefined, }),
tokenId, smallImageUrl: asset?.smallImage?.url,
tokenType: (asset?.collection?.nftContracts && asset?.collection.nftContracts[0]?.standard) as TokenType, tokenId,
collectionIsVerified: asset?.collection?.isVerified ?? undefined, tokenType: asset?.collection?.nftContracts?.[0]?.standard,
rarity: { collectionIsVerified: asset?.collection?.isVerified,
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers rarity: {
providers: asset?.rarities primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
? asset?.rarities?.map((rarity) => { providers: asset?.rarities?.map((rarity) => {
return { return {
rank: rarity.rank ?? undefined, rank: rarity.rank,
score: rarity.score ?? undefined, score: rarity.score,
provider: 'Rarity Sniper', provider: 'Rarity Sniper',
} }
}) }),
: undefined, },
}, ownerAddress: asset?.ownerAddress,
owner: { address: asset?.ownerAddress ?? '' }, creator: {
creator: { profile_img_url: asset?.creator?.profileImage?.url ?? '',
profile_img_url: asset?.creator?.profileImage?.url ?? '', address: asset?.creator?.address ?? '',
address: asset?.creator?.address ?? '', },
}, metadataUrl: asset?.metadataUrl ?? '',
metadataUrl: asset?.metadataUrl ?? '', traits: asset?.traits?.map((trait) => {
traits: asset?.traits?.map((trait) => { return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' }
return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' } }),
}), },
}, {
{ collectionDescription: collection?.description,
collectionDescription: collection?.description ?? undefined, collectionImageUrl: collection?.image?.url,
collectionImageUrl: collection?.image?.url ?? undefined, collectionName: collection?.name,
collectionName: collection?.name ?? undefined, isVerified: collection?.isVerified,
isVerified: collection?.isVerified ?? undefined, totalSupply: collection?.numAssets,
totalSupply: collection?.numAssets ?? undefined, twitterUrl: collection?.twitterName,
twitterUrl: collection?.twitterName ?? undefined, discordUrl: collection?.discordUrl,
discordUrl: collection?.discordUrl ?? undefined, externalUrl: collection?.homepageUrl,
externalUrl: collection?.homepageUrl ?? undefined, },
}, ],
] loading,
}),
[address, asset, collection, ethPrice, listing?.marketplace, loading, tokenId]
)
} }

@ -1,17 +1,20 @@
import graphql from 'babel-plugin-relay/macro'
import { parseEther } from 'ethers/lib/utils' import { parseEther } from 'ethers/lib/utils'
import { DEFAULT_WALLET_ASSET_QUERY_AMOUNT } from 'nft/components/profile/view/ProfilePage' import gql from 'graphql-tag'
import { WalletAsset } from 'nft/types' import { GenieCollection, WalletAsset } from 'nft/types'
import { wrapScientificNotation } from 'nft/utils' import { wrapScientificNotation } from 'nft/utils'
import { useEffect } from 'react' import { useCallback, useMemo } from 'react'
import { useLazyLoadQuery, usePaginationFragment, useQueryLoader } from 'react-relay'
import { NftBalancePaginationQuery } from './__generated__/NftBalancePaginationQuery.graphql' import { NftAsset, useNftBalanceQuery } from '../__generated__/types-and-hooks'
import { NftBalanceQuery } from './__generated__/NftBalanceQuery.graphql'
import { NftBalanceQuery_nftBalances$data } from './__generated__/NftBalanceQuery_nftBalances.graphql'
const nftBalancePaginationQuery = graphql` gql`
fragment NftBalanceQuery_nftBalances on Query @refetchable(queryName: "NftBalancePaginationQuery") { query NftBalance(
$ownerAddress: String!
$filter: NftBalancesFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
nftBalances( nftBalances(
ownerAddress: $ownerAddress ownerAddress: $ownerAddress
filter: $filter filter: $filter
@ -19,7 +22,7 @@ const nftBalancePaginationQuery = graphql`
after: $after after: $after
last: $last last: $last
before: $before before: $before
) @connection(key: "NftBalanceQuery_nftBalances") { ) {
edges { edges {
node { node {
ownedAsset { ownedAsset {
@ -99,43 +102,7 @@ const nftBalancePaginationQuery = graphql`
} }
` `
const nftBalanceQuery = graphql` export function useNftBalance(
query NftBalanceQuery(
$ownerAddress: String!
$filter: NftBalancesFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
...NftBalanceQuery_nftBalances
}
`
type NftBalanceQueryAsset = NonNullable<
NonNullable<NonNullable<NftBalanceQuery_nftBalances$data['nftBalances']>['edges']>[number]
>
export function useLoadNftBalanceQuery(
ownerAddress?: string,
collectionAddress?: string | string[],
tokenId?: string
): void {
const [, loadQuery] = useQueryLoader(nftBalanceQuery)
useEffect(() => {
if (ownerAddress) {
loadQuery({
ownerAddress,
filter: tokenId
? { assets: [{ address: collectionAddress, tokenId }] }
: { addresses: Array.isArray(collectionAddress) ? collectionAddress : [collectionAddress] },
first: tokenId ? 1 : DEFAULT_WALLET_ASSET_QUERY_AMOUNT,
})
}
}, [ownerAddress, loadQuery, collectionAddress, tokenId])
}
export function useNftBalanceQuery(
ownerAddress: string, ownerAddress: string,
collectionFilters?: string[], collectionFilters?: string[],
assetsFilter?: { address: string; tokenId: string }[], assetsFilter?: { address: string; tokenId: string }[],
@ -144,9 +111,8 @@ export function useNftBalanceQuery(
last?: number, last?: number,
before?: string before?: string
) { ) {
const queryData = useLazyLoadQuery<NftBalanceQuery>( const { data, loading, fetchMore } = useNftBalanceQuery({
nftBalanceQuery, variables: {
{
ownerAddress, ownerAddress,
filter: filter:
assetsFilter && assetsFilter.length > 0 assetsFilter && assetsFilter.length > 0
@ -161,14 +127,21 @@ export function useNftBalanceQuery(
last, last,
before, before,
}, },
{ fetchPolicy: 'store-or-network' } })
const hasNext = data?.nftBalances?.pageInfo?.hasNextPage
const loadMore = useCallback(
() =>
fetchMore({
variables: {
after: data?.nftBalances?.pageInfo?.endCursor,
},
}),
[data?.nftBalances?.pageInfo?.endCursor, fetchMore]
) )
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<NftBalancePaginationQuery, any>(
nftBalancePaginationQuery, const walletAssets: WalletAsset[] | undefined = data?.nftBalances?.edges?.map((queryAsset) => {
queryData const asset = queryAsset?.node.ownedAsset as NonNullable<NftAsset>
)
const walletAssets: WalletAsset[] = data.nftBalances?.edges?.map((queryAsset: NftBalanceQueryAsset) => {
const asset = queryAsset.node.ownedAsset
const ethPrice = parseEther(wrapScientificNotation(asset?.listings?.edges[0]?.node.price.value ?? 0)).toString() const ethPrice = parseEther(wrapScientificNotation(asset?.listings?.edges[0]?.node.price.value ?? 0)).toString()
return { return {
id: asset?.id, id: asset?.id,
@ -177,35 +150,32 @@ export function useNftBalanceQuery(
notForSale: asset?.listings?.edges?.length === 0, notForSale: asset?.listings?.edges?.length === 0,
animationUrl: asset?.animationUrl, animationUrl: asset?.animationUrl,
susFlag: asset?.suspiciousFlag, susFlag: asset?.suspiciousFlag,
priceInfo: asset?.listings priceInfo: {
? { ETHPrice: ethPrice,
ETHPrice: ethPrice, baseAsset: 'ETH',
baseAsset: 'ETH', baseDecimals: '18',
baseDecimals: '18', basePrice: ethPrice,
basePrice: ethPrice, },
}
: undefined,
name: asset?.name, name: asset?.name,
tokenId: asset?.tokenId, tokenId: asset?.tokenId,
asset_contract: { asset_contract: {
address: asset?.collection?.nftContracts?.[0]?.address, address: asset?.collection?.nftContracts?.[0]?.address,
schema_name: asset?.collection?.nftContracts?.[0]?.standard, tokenType: asset?.collection?.nftContracts?.[0]?.standard,
name: asset?.collection?.name, name: asset?.collection?.name,
description: asset?.description, description: asset?.description,
image_url: asset?.collection?.image?.url, image_url: asset?.collection?.image?.url,
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress, payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
tokenType: asset?.collection?.nftContracts?.[0].standard,
}, },
collection: asset?.collection, collection: asset?.collection as unknown as GenieCollection,
collectionIsVerified: asset?.collection?.isVerified, collectionIsVerified: asset?.collection?.isVerified,
lastPrice: queryAsset.node.lastPrice?.value, lastPrice: queryAsset.node.lastPrice?.value,
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value, floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,
basisPoints: queryAsset?.node?.listingFees?.[0]?.basisPoints ?? 0 / 10000, basisPoints: queryAsset?.node?.listingFees?.[0]?.basisPoints ?? 0 / 10000,
listing_date: asset?.listings?.edges?.[0]?.node?.createdAt, listing_date: asset?.listings?.edges?.[0]?.node?.createdAt?.toString(),
date_acquired: queryAsset.node.lastPrice?.timestamp, date_acquired: queryAsset.node.lastPrice?.timestamp?.toString(),
sellOrders: asset?.listings?.edges.map((edge: any) => edge.node), sellOrders: asset?.listings?.edges.map((edge: any) => edge.node),
floor_sell_order_price: asset?.listings?.edges?.[0]?.node?.price?.value, floor_sell_order_price: asset?.listings?.edges?.[0]?.node?.price?.value,
} }
}) })
return { walletAssets, hasNext, isLoadingNext, loadNext } return useMemo(() => ({ walletAssets, hasNext, loadMore, loading }), [hasNext, loadMore, loading, walletAssets])
} }

@ -2,7 +2,7 @@ import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc' import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { Chain, HistoryDuration } from './__generated__/TopTokens100Query.graphql' import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
export enum TimePeriod { export enum TimePeriod {
HOUR, HOUR,
@ -15,15 +15,15 @@ export enum TimePeriod {
export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration { export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
switch (timePeriod) { switch (timePeriod) {
case TimePeriod.HOUR: case TimePeriod.HOUR:
return 'HOUR' return HistoryDuration.Hour
case TimePeriod.DAY: case TimePeriod.DAY:
return 'DAY' return HistoryDuration.Day
case TimePeriod.WEEK: case TimePeriod.WEEK:
return 'WEEK' return HistoryDuration.Week
case TimePeriod.MONTH: case TimePeriod.MONTH:
return 'MONTH' return HistoryDuration.Month
case TimePeriod.YEAR: case TimePeriod.YEAR:
return 'YEAR' return HistoryDuration.Year
} }
} }
@ -34,16 +34,16 @@ export function isPricePoint(p: PricePoint | null): p is PricePoint {
} }
export const CHAIN_ID_TO_BACKEND_NAME: { [key: number]: Chain } = { export const CHAIN_ID_TO_BACKEND_NAME: { [key: number]: Chain } = {
[SupportedChainId.MAINNET]: 'ETHEREUM', [SupportedChainId.MAINNET]: Chain.Ethereum,
[SupportedChainId.GOERLI]: 'ETHEREUM_GOERLI', [SupportedChainId.GOERLI]: Chain.EthereumGoerli,
[SupportedChainId.POLYGON]: 'POLYGON', [SupportedChainId.POLYGON]: Chain.Polygon,
[SupportedChainId.POLYGON_MUMBAI]: 'POLYGON', [SupportedChainId.POLYGON_MUMBAI]: Chain.Polygon,
[SupportedChainId.CELO]: 'CELO', [SupportedChainId.CELO]: Chain.Celo,
[SupportedChainId.CELO_ALFAJORES]: 'CELO', [SupportedChainId.CELO_ALFAJORES]: Chain.Celo,
[SupportedChainId.ARBITRUM_ONE]: 'ARBITRUM', [SupportedChainId.ARBITRUM_ONE]: Chain.Arbitrum,
[SupportedChainId.ARBITRUM_RINKEBY]: 'ARBITRUM', [SupportedChainId.ARBITRUM_RINKEBY]: Chain.Arbitrum,
[SupportedChainId.OPTIMISM]: 'OPTIMISM', [SupportedChainId.OPTIMISM]: Chain.Optimism,
[SupportedChainId.OPTIMISM_GOERLI]: 'OPTIMISM', [SupportedChainId.OPTIMISM_GOERLI]: Chain.Optimism,
} }
export function chainIdToBackendName(chainId: number | undefined) { export function chainIdToBackendName(chainId: number | undefined) {
@ -53,15 +53,15 @@ export function chainIdToBackendName(chainId: number | undefined) {
} }
const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: Chain } = { const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: Chain } = {
ethereum: 'ETHEREUM', ethereum: Chain.Ethereum,
polygon: 'POLYGON', polygon: Chain.Polygon,
celo: 'CELO', celo: Chain.Celo,
arbitrum: 'ARBITRUM', arbitrum: Chain.Arbitrum,
optimism: 'OPTIMISM', optimism: Chain.Optimism,
} }
export function validateUrlChainParam(chainName: string | undefined) { export function validateUrlChainParam(chainName: string | undefined) {
return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : 'ETHEREUM' return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : Chain.Ethereum
} }
export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = { export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
@ -72,7 +72,7 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
OPTIMISM: SupportedChainId.OPTIMISM, OPTIMISM: SupportedChainId.OPTIMISM,
} }
export const BACKEND_CHAIN_NAMES: Chain[] = ['ETHEREUM', 'POLYGON', 'OPTIMISM', 'ARBITRUM', 'CELO'] export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
export function isValidBackendChainName(chainName: string | undefined): chainName is Chain { export function isValidBackendChainName(chainName: string | undefined): chainName is Chain {
if (!chainName) return false if (!chainName) return false
@ -95,7 +95,11 @@ export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?:
} }
} }
export function unwrapToken<T extends { address: string | null } | null>(chainId: number, token: T): T { export function unwrapToken<
T extends {
address?: string | null | undefined
} | null
>(chainId: number, token: T): T {
if (!token?.address) return token if (!token?.address) return token
const address = token.address.toLowerCase() const address = token.address.toLowerCase()

@ -1,17 +1,12 @@
import graphql from 'babel-plugin-relay/macro' import { useQuery } from '@apollo/client'
import useInterval from 'lib/hooks/useInterval' import gql from 'graphql-tag'
import { useCallback, useEffect, useState } from 'react' import { useMemo } from 'react'
import { fetchQuery } from 'react-relay'
import { useAppSelector } from 'state/hooks'
import type { import { AllV3TicksQuery } from './__generated__/types-and-hooks'
AllV3TicksQuery as AllV3TicksQueryType, import { apolloClient } from './apollo'
AllV3TicksQuery$data,
} from './__generated__/AllV3TicksQuery.graphql'
import environment from './RelayEnvironment'
const query = graphql` const query = gql`
query AllV3TicksQuery($poolAddress: String!, $skip: Int!) { query AllV3Ticks($poolAddress: String!, $skip: Int!) {
ticks(first: 1000, skip: $skip, where: { poolAddress: $poolAddress }, orderBy: tickIdx) { ticks(first: 1000, skip: $skip, where: { poolAddress: $poolAddress }, orderBy: tickIdx) {
tick: tickIdx tick: tickIdx
liquidityNet liquidityNet
@ -21,33 +16,29 @@ const query = graphql`
} }
` `
export type Ticks = AllV3TicksQuery$data['ticks'] export type Ticks = AllV3TicksQuery['ticks']
export type TickData = Ticks[number] export type TickData = Ticks[number]
export default function useAllV3TicksQuery(poolAddress: string | undefined, skip: number, interval: number) { export default function useAllV3TicksQuery(poolAddress: string | undefined, skip: number, interval: number) {
const [data, setData] = useState<AllV3TicksQuery$data | null>(null) const {
const [error, setError] = useState<any>(null) data,
const [isLoading, setIsLoading] = useState(true) loading: isLoading,
const chainId = useAppSelector((state) => state.application.chainId) error,
} = useQuery(query, {
variables: {
poolAddress: poolAddress?.toLowerCase(),
skip,
},
pollInterval: interval,
client: apolloClient,
})
const refreshData = useCallback(() => { return useMemo(
if (poolAddress && chainId) { () => ({
fetchQuery<AllV3TicksQueryType>(environment, query, { error,
poolAddress: poolAddress.toLowerCase(), isLoading,
skip, data,
}).subscribe({ }),
next: setData, [data, error, isLoading]
error: setError, )
complete: () => setIsLoading(false),
})
} else {
setIsLoading(false)
}
}, [poolAddress, skip, chainId])
// Trigger fetch on first load
useEffect(refreshData, [refreshData, poolAddress, skip])
useInterval(refreshData, interval, true)
return { error, isLoading, data }
} }

@ -1,17 +1,12 @@
import graphql from 'babel-plugin-relay/macro' import { ApolloError, useQuery } from '@apollo/client'
import useInterval from 'lib/hooks/useInterval' import gql from 'graphql-tag'
import { useCallback, useEffect, useState } from 'react' import { useMemo } from 'react'
import { fetchQuery } from 'react-relay'
import { useAppSelector } from 'state/hooks'
import type { import { FeeTierDistributionQuery } from './__generated__/types-and-hooks'
FeeTierDistributionQuery as FeeTierDistributionQueryType, import { apolloClient } from './apollo'
FeeTierDistributionQuery$data,
} from './__generated__/FeeTierDistributionQuery.graphql'
import environment from './RelayEnvironment'
const query = graphql` const query = gql`
query FeeTierDistributionQuery($token0: String!, $token1: String!) { query FeeTierDistribution($token0: String!, $token1: String!) {
_meta { _meta {
block { block {
number number
@ -42,28 +37,26 @@ export default function useFeeTierDistributionQuery(
token0: string | undefined, token0: string | undefined,
token1: string | undefined, token1: string | undefined,
interval: number interval: number
) { ): { error: ApolloError | undefined; isLoading: boolean; data: FeeTierDistributionQuery } {
const [data, setData] = useState<FeeTierDistributionQuery$data | null>(null) const {
const [error, setError] = useState<any>(null) data,
const [isLoading, setIsLoading] = useState(true) loading: isLoading,
const chainId = useAppSelector((state) => state.application.chainId) error,
} = useQuery(query, {
variables: {
token0: token0?.toLowerCase(),
token1: token1?.toLowerCase(),
},
pollInterval: interval,
client: apolloClient,
})
const refreshData = useCallback(() => { return useMemo(
if (token0 && token1 && chainId) { () => ({
fetchQuery<FeeTierDistributionQueryType>(environment, query, { error,
token0: token0.toLowerCase(), isLoading,
token1: token1.toLowerCase(), data,
}).subscribe({ }),
next: setData, [data, error, isLoading]
error: setError, )
complete: () => setIsLoading(false),
})
}
}, [token0, token1, chainId])
// Trigger fetch on first load
useEffect(refreshData, [refreshData, token0, token1])
useInterval(refreshData, interval, true)
return { error, isLoading, data }
} }

@ -1,9 +0,0 @@
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
import fetchGraphQL from './fetchGraphQL'
// Export a singleton instance of Relay Environment configured with our network function:
export default new Environment({
network: Network.create(fetchGraphQL),
store: new Store(new RecordSource()),
})

File diff suppressed because it is too large Load Diff

@ -0,0 +1,40 @@
import { ApolloClient, ApolloLink, concat, HttpLink, InMemoryCache } from '@apollo/client'
import { SupportedChainId } from 'constants/chains'
import store, { AppState } from '../../state/index'
const CHAIN_SUBGRAPH_URL: Record<number, string> = {
[SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
[SupportedChainId.RINKEBY]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
[SupportedChainId.ARBITRUM_ONE]: 'https://api.thegraph.com/subgraphs/name/ianlapham/arbitrum-minimal',
[SupportedChainId.OPTIMISM]: 'https://api.thegraph.com/subgraphs/name/ianlapham/optimism-post-regenesis',
[SupportedChainId.POLYGON]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-polygon',
[SupportedChainId.CELO]: 'https://api.thegraph.com/subgraphs/name/jesse-sawa/uniswap-celo',
}
const httpLink = new HttpLink({ uri: CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET] })
// This middleware will allow us to dynamically update the uri for the requests based off chainId
// For more information: https://www.apollographql.com/docs/react/networking/advanced-http-networking/
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const chainId = (store.getState() as AppState).application.chainId
operation.setContext(() => ({
uri:
chainId && CHAIN_SUBGRAPH_URL[chainId]
? CHAIN_SUBGRAPH_URL[chainId]
: CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET],
}))
return forward(operation)
})
export const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: concat(authMiddleware, httpLink),
})

@ -1,53 +0,0 @@
/**
* Helpful Resources
* https://github.com/sibelius/create-react-app-relay-modern/blob/master/src/relay/fetchQuery.js
* https://github.com/relay-tools/relay-compiler-language-typescript/blob/master/example/ts/app.tsx
*/
import { SupportedChainId } from 'constants/chains'
import { Variables } from 'react-relay'
import { GraphQLResponse, ObservableFromValue, RequestParameters } from 'relay-runtime'
import store, { AppState } from '../../state/index'
const CHAIN_SUBGRAPH_URL: Record<number, string> = {
[SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
[SupportedChainId.RINKEBY]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
[SupportedChainId.ARBITRUM_ONE]: 'https://api.thegraph.com/subgraphs/name/ianlapham/arbitrum-minimal',
[SupportedChainId.OPTIMISM]: 'https://api.thegraph.com/subgraphs/name/ianlapham/optimism-post-regenesis',
[SupportedChainId.POLYGON]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-polygon',
[SupportedChainId.CELO]: 'https://api.thegraph.com/subgraphs/name/jesse-sawa/uniswap-celo',
}
const headers = {
Accept: 'application/json',
'Content-type': 'application/json',
}
// Define a function that fetches the results of a request (query/mutation/etc)
// and returns its results as a Promise:
const fetchQuery = (params: RequestParameters, variables: Variables): ObservableFromValue<GraphQLResponse> => {
const chainId = (store.getState() as AppState).application.chainId
const subgraphUrl =
chainId && CHAIN_SUBGRAPH_URL[chainId] ? CHAIN_SUBGRAPH_URL[chainId] : CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET]
const body = JSON.stringify({
query: params.text, // GraphQL text from input
variables,
})
const response = fetch(subgraphUrl, {
method: 'POST',
headers,
body,
}).then((res) => res.json())
return response
}
export default fetchQuery

@ -165,7 +165,7 @@ function useAllV3Ticks(
): { ): {
isLoading: boolean isLoading: boolean
error: unknown error: unknown
ticks: readonly TickData[] | undefined ticks: TickData[] | undefined
} { } {
const useSubgraph = currencyA ? !CHAIN_IDS_MISSING_SUBGRAPH_DATA.includes(currencyA.chainId) : true const useSubgraph = currencyA ? !CHAIN_IDS_MISSING_SUBGRAPH_DATA.includes(currencyA.chainId) : true

@ -3,16 +3,16 @@ import 'inter-ui'
import 'polyfills' import 'polyfills'
import 'components/analytics' import 'components/analytics'
import { ApolloProvider } from '@apollo/client'
import * as Sentry from '@sentry/react' import * as Sentry from '@sentry/react'
import { FeatureFlagsProvider } from 'featureFlags' import { FeatureFlagsProvider } from 'featureFlags'
import RelayEnvironment from 'graphql/data/RelayEnvironment' import { apolloClient } from 'graphql/data/apollo'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber' import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall' import { MulticallUpdater } from 'lib/state/multicall'
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from 'react-query' import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { RelayEnvironmentProvider } from 'react-relay'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
import { isProductionEnv } from 'utils/env' import { isProductionEnv } from 'utils/env'
@ -66,7 +66,7 @@ createRoot(container).render(
<HashRouter> <HashRouter>
<LanguageProvider> <LanguageProvider>
<Web3Provider> <Web3Provider>
<RelayEnvironmentProvider environment={RelayEnvironment}> <ApolloProvider client={apolloClient}>
<BlockNumberProvider> <BlockNumberProvider>
<Updaters /> <Updaters />
<ThemeProvider> <ThemeProvider>
@ -74,7 +74,7 @@ createRoot(container).render(
<App /> <App />
</ThemeProvider> </ThemeProvider>
</BlockNumberProvider> </BlockNumberProvider>
</RelayEnvironmentProvider> </ApolloProvider>
</Web3Provider> </Web3Provider>
</LanguageProvider> </LanguageProvider>
</HashRouter> </HashRouter>

@ -71,7 +71,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
for (const listing of asset.newListings) { for (const listing of asset.newListings) {
if (!listing.price) listingsMissingPrice.push([asset, listing]) if (!listing.price) listingsMissingPrice.push([asset, listing])
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing]) else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
else if (listing.price < asset.floorPrice && !listing.overrideFloorPrice) else if (listing.price < (asset?.floorPrice ?? 0) && !listing.overrideFloorPrice)
listingsBelowFloor.push([asset, listing]) listingsBelowFloor.push([asset, listing])
else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price) else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing]) listingsAboveSellOrderFloor.push([asset, listing])

@ -88,7 +88,7 @@ export const ListingSection = ({
return ( return (
<Column key={index} gap="8"> <Column key={index} gap="8">
<Row> <Row>
{row.images.map((image, index) => { {row.images?.map((image, index) => {
return ( return (
<Box <Box
as="img" as="img"

@ -58,14 +58,15 @@ export async function approveCollectionRow(
: marketplace.name === 'X2Y2' : marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT ? X2Y2_TRANSFER_CONTRACT
: looksRareAddress : looksRareAddress
await approveCollection(spender ?? '', collectionAddress, signer, (newStatus: ListingStatus) => !!collectionAddress &&
updateStatus({ (await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
listing: collectionRow, updateStatus({
newStatus, listing: collectionRow,
rows: collectionsRequiringApproval, newStatus,
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>, rows: collectionsRequiringApproval,
}) setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
) })
))
if (collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) pauseAllRows() if (collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) pauseAllRows()
} }
@ -127,7 +128,7 @@ export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
// LooksRare is a unique case where creator royalties are a flat 0.5% or 50 basis points // LooksRare is a unique case where creator royalties are a flat 0.5% or 50 basis points
const maxFee = const maxFee =
maxListing.marketplace.fee + maxListing.marketplace.fee +
(maxListing.marketplace.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset.basisPoints) / 100 (maxListing.marketplace.name === 'LooksRare' ? LOOKS_RARE_CREATOR_BASIS_POINTS : asset?.basisPoints ?? 0) / 100
return total + (maxListing.price ?? 0) - (maxListing.price ?? 0) * (maxFee / 100) return total + (maxListing.price ?? 0) - (maxListing.price ?? 0) * (maxFee / 100)
} }
return total return total

@ -2,6 +2,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import clsx from 'clsx' import clsx from 'clsx'
import { OpacityHoverState } from 'components/Common' import { OpacityHoverState } from 'components/Common'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex' import { Row } from 'nft/components/Flex'
import { import {
@ -16,7 +17,7 @@ import {
import { body, bodySmall, buttonTextMedium, subhead } from 'nft/css/common.css' import { body, bodySmall, buttonTextMedium, subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useIsMobile } from 'nft/hooks' import { useIsMobile } from 'nft/hooks'
import { GenieAsset, Rarity, TokenType, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types' import { GenieAsset, Rarity, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types'
import { fallbackProvider, isAudio, isVideo, putCommas } from 'nft/utils' import { fallbackProvider, isAudio, isVideo, putCommas } from 'nft/utils'
import { floorFormatter } from 'nft/utils/numbers' import { floorFormatter } from 'nft/utils/numbers'
import { import {
@ -579,8 +580,7 @@ const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
return !!asset.name ? asset.name : `#${asset.tokenId}` return !!asset.name ? asset.name : `#${asset.tokenId}`
} }
const shouldShowUserListedPrice = const shouldShowUserListedPrice = !asset.notForSale && asset.asset_contract.tokenType !== NftStandard.Erc1155
!!asset.floor_sell_order_price && !asset.notForSale && asset.asset_contract.tokenType !== TokenType.ERC1155
return ( return (
<Box overflow="hidden" width="full" flexWrap="nowrap"> <Box overflow="hidden" width="full" flexWrap="nowrap">
@ -605,7 +605,9 @@ const ProfileNftDetails = ({ asset, hideDetails }: ProfileNftDetailsProps) => {
{asset.susFlag && <Suspicious />} {asset.susFlag && <Suspicious />}
</Row> </Row>
<TruncatedTextRow className={buttonTextMedium} style={{ color: themeVars.colors.textPrimary }}> <TruncatedTextRow className={buttonTextMedium} style={{ color: themeVars.colors.textPrimary }}>
{shouldShowUserListedPrice ? `${floorFormatter(asset.floor_sell_order_price)} ETH` : ' '} {shouldShowUserListedPrice && asset.floor_sell_order_price
? `${floorFormatter(asset.floor_sell_order_price)} ETH`
: ' '}
</TruncatedTextRow> </TruncatedTextRow>
</Box> </Box>
) )

@ -4,10 +4,11 @@ import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { EventName, PageName } from '@uniswap/analytics-events' import { EventName, PageName } from '@uniswap/analytics-events'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { bodySmall } from 'nft/css/common.css' import { bodySmall } from 'nft/css/common.css'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { GenieAsset, isPooledMarket, TokenType, UniformAspectRatio } from 'nft/types' import { GenieAsset, isPooledMarket, UniformAspectRatio } from 'nft/types'
import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils' import { formatWeiToDecimal, rarityProviderLogo } from 'nft/utils'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
@ -212,7 +213,7 @@ export const CollectionAsset = ({
</Card.SecondaryInfo> </Card.SecondaryInfo>
{isPooledMarket(asset.marketplace) && <Card.Pool />} {isPooledMarket(asset.marketplace) && <Card.Pool />}
</Card.SecondaryDetails> </Card.SecondaryDetails>
{asset.tokenType !== TokenType.ERC1155 && asset.marketplace && ( {asset.tokenType !== NftStandard.Erc1155 && asset.marketplace && (
<Card.MarketplaceIcon marketplace={asset.marketplace} /> <Card.MarketplaceIcon marketplace={asset.marketplace} />
)} )}
</Card.SecondaryRow> </Card.SecondaryRow>

@ -5,13 +5,8 @@ import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx' import clsx from 'clsx'
import { OpacityHoverState } from 'components/Common' import { OpacityHoverState } from 'components/Common'
import { parseEther } from 'ethers/lib/utils' import { parseEther } from 'ethers/lib/utils'
import { NftAssetTraitInput, NftMarketplace } from 'graphql/data/nft/__generated__/AssetQuery.graphql' import { NftAssetTraitInput, NftMarketplace, NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { import { ASSET_PAGE_SIZE, AssetFetcherParams, useNftAssets } from 'graphql/data/nft/Asset'
ASSET_PAGE_SIZE,
AssetFetcherParams,
useLazyLoadAssetsQuery,
useLoadSweepAssetsQuery,
} from 'graphql/data/nft/Asset'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import { useScreenSize } from 'hooks/useScreenSize' import { useScreenSize } from 'hooks/useScreenSize'
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
@ -41,7 +36,6 @@ import {
GenieCollection, GenieCollection,
isPooledMarket, isPooledMarket,
Markets, Markets,
TokenType,
UniformAspectRatio, UniformAspectRatio,
UniformAspectRatios, UniformAspectRatios,
} from 'nft/types' } from 'nft/types'
@ -63,7 +57,7 @@ import { ThemedText } from 'theme'
import { CollectionAssetLoading } from './CollectionAssetLoading' import { CollectionAssetLoading } from './CollectionAssetLoading'
import { MARKETPLACE_ITEMS, MarketplaceLogo } from './MarketplaceSelect' import { MARKETPLACE_ITEMS, MarketplaceLogo } from './MarketplaceSelect'
import { Sweep, useSweepFetcherParams } from './Sweep' import { Sweep } from './Sweep'
import { TraitChip } from './TraitChip' import { TraitChip } from './TraitChip'
interface CollectionNftsProps { interface CollectionNftsProps {
@ -282,15 +276,6 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const [renderedHeight, setRenderedHeight] = useState<number | undefined>() const [renderedHeight, setRenderedHeight] = useState<number | undefined>()
const [sweepIsOpen, setSweepOpen] = useState(false) const [sweepIsOpen, setSweepOpen] = useState(false)
// Load all sweep queries. Loading them on the parent allows lazy-loading, but avoids waterfalling requests.
const collectionParams = useSweepFetcherParams(contractAddress, 'others', debouncedMinPrice, debouncedMaxPrice)
const sudoSwapParams = useSweepFetcherParams(contractAddress, Markets.Sudoswap, debouncedMinPrice, debouncedMaxPrice)
const nftxParams = useSweepFetcherParams(contractAddress, Markets.NFTX, debouncedMinPrice, debouncedMaxPrice)
const nft20Params = useSweepFetcherParams(contractAddress, Markets.NFT20, debouncedMinPrice, debouncedMaxPrice)
useLoadSweepAssetsQuery(collectionParams, sweepIsOpen)
useLoadSweepAssetsQuery(sudoSwapParams, sweepIsOpen)
useLoadSweepAssetsQuery(nftxParams, sweepIsOpen)
useLoadSweepAssetsQuery(nft20Params, sweepIsOpen)
const assetQueryParams: AssetFetcherParams = { const assetQueryParams: AssetFetcherParams = {
address: contractAddress, address: contractAddress,
@ -312,8 +297,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
first: ASSET_PAGE_SIZE, first: ASSET_PAGE_SIZE,
} }
const { assets: collectionNfts, loadNext, hasNext, isLoadingNext } = useLazyLoadAssetsQuery(assetQueryParams) const { data: collectionNfts, loading, hasNext, loadMore } = useNftAssets(assetQueryParams)
const handleNextPageLoad = useCallback(() => loadNext(ASSET_PAGE_SIZE), [loadNext])
const getPoolPosition = useCallback( const getPoolPosition = useCallback(
(asset: GenieAsset) => { (asset: GenieAsset) => {
@ -394,8 +378,8 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const screenSize = useScreenSize() const screenSize = useScreenSize()
useEffect(() => { useEffect(() => {
setIsCollectionNftsLoading(isLoadingNext) setIsCollectionNftsLoading(loading)
}, [isLoadingNext, setIsCollectionNftsLoading]) }, [loading, setIsCollectionNftsLoading])
const hasRarity = useMemo(() => { const hasRarity = useMemo(() => {
const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionAssets) ?? false const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionAssets) ?? false
@ -434,7 +418,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
}, [collectionAssets, isMobile, currentTokenPlayingMedia, rarityVerified, uniformAspectRatio, renderedHeight]) }, [collectionAssets, isMobile, currentTokenPlayingMedia, rarityVerified, uniformAspectRatio, renderedHeight])
const hasNfts = collectionAssets && collectionAssets.length > 0 const hasNfts = collectionAssets && collectionAssets.length > 0
const hasErc1155s = hasNfts && collectionAssets[0] && collectionAssets[0].tokenType === TokenType.ERC1155 const hasErc1155s = hasNfts && collectionAssets[0] && collectionAssets[0]?.tokenType === NftStandard.Erc1155
const minMaxPriceChipText: string | undefined = useMemo(() => { const minMaxPriceChipText: string | undefined = useMemo(() => {
if (debouncedMinPrice && debouncedMaxPrice) { if (debouncedMinPrice && debouncedMaxPrice) {
@ -619,35 +603,37 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
</InfiniteScrollWrapper> </InfiniteScrollWrapper>
</AnimatedBox> </AnimatedBox>
<InfiniteScrollWrapper> <InfiniteScrollWrapper>
<InfiniteScroll {loading ? (
next={handleNextPageLoad} <CollectionNftsLoading height={renderedHeight} />
hasMore={hasNext} ) : (
loader={Boolean(hasNext && hasNfts) && <LoadingAssets height={renderedHeight} />} <InfiniteScroll
dataLength={collectionAssets?.length ?? 0} next={loadMore}
style={{ overflow: 'unset' }} hasMore={hasNext ?? false}
className={hasNfts || isLoadingNext ? styles.assetList : undefined} loader={Boolean(hasNext && hasNfts) && <LoadingAssets />}
> dataLength={collectionAssets?.length ?? 0}
{hasNfts ? ( style={{ overflow: 'unset' }}
assets className={hasNfts ? styles.assetList : undefined}
) : collectionAssets?.length === 0 ? ( >
<Center width="full" color="textSecondary" textAlign="center" style={{ height: '60vh' }}> {!hasNfts ? (
<EmptyCollectionWrapper> <Center width="full" color="textSecondary" textAlign="center" style={{ height: '60vh' }}>
<p className={headlineMedium}>No NFTS found</p> <EmptyCollectionWrapper>
<Box <p className={headlineMedium}>No NFTS found</p>
onClick={reset} <Box
type="button" onClick={reset}
className={clsx(bodySmall, buttonTextMedium)} type="button"
color="accentAction" className={clsx(bodySmall, buttonTextMedium)}
cursor="pointer" color="accentAction"
> cursor="pointer"
<ViewFullCollection>View full collection</ViewFullCollection> >
</Box> <ViewFullCollection>View full collection</ViewFullCollection>
</EmptyCollectionWrapper> </Box>
</Center> </EmptyCollectionWrapper>
) : ( </Center>
<CollectionNftsLoading height={renderedHeight} /> ) : (
)} assets
</InfiniteScroll> )}
</InfiniteScroll>
)}
</InfiniteScrollWrapper> </InfiniteScrollWrapper>
</> </>
) )

@ -2,7 +2,7 @@ import 'rc-slider/assets/index.css'
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { formatEther, parseEther } from '@ethersproject/units' import { formatEther, parseEther } from '@ethersproject/units'
import { SweepFetcherParams, useLazyLoadSweepAssetsQuery } from 'graphql/data/nft/Asset' import { SweepFetcherParams, useSweepNftAssets } from 'graphql/data/nft/Asset'
import { useBag, useCollectionFilters } from 'nft/hooks' import { useBag, useCollectionFilters } from 'nft/hooks'
import { GenieAsset, isPooledMarket, Markets } from 'nft/types' import { GenieAsset, isPooledMarket, Markets } from 'nft/types'
import { calcPoolPrice, calcSudoSwapPrice, formatWeiToDecimal, isInSameSudoSwapPool } from 'nft/utils' import { calcPoolPrice, calcSudoSwapPrice, formatWeiToDecimal, isInSameSudoSwapPool } from 'nft/utils'
@ -178,13 +178,13 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
const nftxParams = useSweepFetcherParams(contractAddress, Markets.NFTX, minPrice, maxPrice) const nftxParams = useSweepFetcherParams(contractAddress, Markets.NFTX, minPrice, maxPrice)
const nft20Params = useSweepFetcherParams(contractAddress, Markets.NFT20, minPrice, maxPrice) const nft20Params = useSweepFetcherParams(contractAddress, Markets.NFT20, minPrice, maxPrice)
// These calls will suspend if the query is not yet loaded. // These calls will suspend if the query is not yet loaded.
const collectionAssets = useLazyLoadSweepAssetsQuery(collectionParams) const { data: collectionAssets } = useSweepNftAssets(collectionParams)
const sudoSwapAsssets = useLazyLoadSweepAssetsQuery(sudoSwapParams) const { data: sudoSwapAssets } = useSweepNftAssets(sudoSwapParams)
const nftxAssets = useLazyLoadSweepAssetsQuery(nftxParams) const { data: nftxAssets } = useSweepNftAssets(nftxParams)
const nft20Assets = useLazyLoadSweepAssetsQuery(nft20Params) const { data: nft20Assets } = useSweepNftAssets(nft20Params)
const { sortedAssets, sortedAssetsTotalEth } = useMemo(() => { const { sortedAssets, sortedAssetsTotalEth } = useMemo(() => {
if (!collectionAssets && !sudoSwapAsssets && !nftxAssets && !nft20Assets) { if (!collectionAssets && !sudoSwapAssets && !nftxAssets && !nft20Assets) {
return { sortedAssets: undefined, sortedAssetsTotalEth: BigNumber.from(0) } return { sortedAssets: undefined, sortedAssetsTotalEth: BigNumber.from(0) }
} }
@ -193,7 +193,7 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
let jointCollections: GenieAsset[] = [] let jointCollections: GenieAsset[] = []
if (sudoSwapAsssets) jointCollections = [...jointCollections, ...sudoSwapAsssets] if (sudoSwapAssets) jointCollections = [...jointCollections, ...sudoSwapAssets]
if (nftxAssets) jointCollections = [...jointCollections, ...nftxAssets] if (nftxAssets) jointCollections = [...jointCollections, ...nftxAssets]
if (nft20Assets) jointCollections = [...jointCollections, ...nft20Assets] if (nft20Assets) jointCollections = [...jointCollections, ...nft20Assets]
@ -236,7 +236,7 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
0, 0,
Math.max( Math.max(
collectionAssets?.length ?? 0, collectionAssets?.length ?? 0,
sudoSwapAsssets?.length ?? 0, sudoSwapAssets?.length ?? 0,
nftxAssets?.length ?? 0, nftxAssets?.length ?? 0,
nft20Assets?.length ?? 0 nft20Assets?.length ?? 0
) )
@ -249,7 +249,7 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
BigNumber.from(0) BigNumber.from(0)
), ),
} }
}, [collectionAssets, sudoSwapAsssets, nftxAssets, nft20Assets]) }, [collectionAssets, sudoSwapAssets, nftxAssets, nft20Assets])
const { sweepItemsInBag, sweepEthPrice } = useMemo(() => { const { sweepItemsInBag, sweepEthPrice } = useMemo(() => {
const sweepItemsInBag = itemsInBag const sweepItemsInBag = itemsInBag
@ -435,7 +435,7 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => {
const ALL_OTHER_MARKETS = [Markets.Opensea, Markets.X2Y2, Markets.LooksRare] const ALL_OTHER_MARKETS = [Markets.Opensea, Markets.X2Y2, Markets.LooksRare]
export function useSweepFetcherParams( function useSweepFetcherParams(
contractAddress: string, contractAddress: string,
market: Markets.Sudoswap | Markets.NFTX | Markets.NFT20 | 'others', market: Markets.Sudoswap | Markets.NFTX | Markets.NFT20 | 'others',
minPrice: string, minPrice: string,

@ -263,7 +263,7 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
return MediaType.Audio return MediaType.Audio
} else if (isVideo(asset.animationUrl ?? '')) { } else if (isVideo(asset.animationUrl ?? '')) {
return MediaType.Video return MediaType.Video
} else if (asset.animationUrl !== undefined) { } else if (!!asset.animationUrl) {
return MediaType.Embed return MediaType.Embed
} }
return MediaType.Image return MediaType.Image

@ -3,7 +3,7 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events' import { EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { OpacityHoverState } from 'components/Common' import { OpacityHoverState } from 'components/Common'
import { useNftBalanceQuery } from 'graphql/data/nft/NftBalance' import { useNftBalance } from 'graphql/data/nft/NftBalance'
import { CancelListingIcon, VerifiedIcon } from 'nft/components/icons' import { CancelListingIcon, VerifiedIcon } from 'nft/components/icons'
import { useBag, useProfilePageState, useSellAsset } from 'nft/hooks' import { useBag, useProfilePageState, useSellAsset } from 'nft/hooks'
import { CollectionInfoForAsset, GenieAsset, ProfilePageStateType, WalletAsset } from 'nft/types' import { CollectionInfoForAsset, GenieAsset, ProfilePageStateType, WalletAsset } from 'nft/types'
@ -218,10 +218,10 @@ const OwnerContainer = ({ asset }: { asset: WalletAsset }) => {
const resetSellAssets = useSellAsset((state) => state.reset) const resetSellAssets = useSellAsset((state) => state.reset)
const listing = asset.sellOrders && asset.sellOrders.length > 0 ? asset.sellOrders[0] : undefined const listing = asset.sellOrders && asset.sellOrders.length > 0 ? asset.sellOrders[0] : undefined
const expirationDate = listing ? new Date(listing.endAt) : undefined const expirationDate = listing?.endAt ? new Date(listing.endAt) : undefined
const USDPrice = useMemo( const USDPrice = useMemo(
() => (USDValue ? USDValue * asset.floor_sell_order_price : undefined), () => (USDValue && asset.floor_sell_order_price ? USDValue * asset.floor_sell_order_price : undefined),
[USDValue, asset.floor_sell_order_price] [USDValue, asset.floor_sell_order_price]
) )
const trace = useTrace() const trace = useTrace()
@ -254,7 +254,7 @@ const OwnerContainer = ({ asset }: { asset: WalletAsset }) => {
{listing ? ( {listing ? (
<> <>
<ThemedText.MediumHeader fontSize="28px" lineHeight="36px"> <ThemedText.MediumHeader fontSize="28px" lineHeight="36px">
{formatEthPrice(asset.priceInfo.ETHPrice)} ETH {formatEthPrice(asset.priceInfo?.ETHPrice)} ETH
</ThemedText.MediumHeader> </ThemedText.MediumHeader>
{USDPrice && ( {USDPrice && (
<ThemedText.BodySecondary lineHeight="24px"> <ThemedText.BodySecondary lineHeight="24px">
@ -320,7 +320,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const { account } = useWeb3React() const { account } = useWeb3React()
const cheapestOrder = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined const cheapestOrder = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined
const expirationDate = cheapestOrder ? new Date(cheapestOrder.endAt) : undefined const expirationDate = cheapestOrder?.endAt ? new Date(cheapestOrder.endAt) : undefined
const itemsInBag = useBag((s) => s.itemsInBag) const itemsInBag = useBag((s) => s.itemsInBag)
const addAssetsToBag = useBag((s) => s.addAssetsToBag) const addAssetsToBag = useBag((s) => s.addAssetsToBag)
@ -331,11 +331,8 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const USDPrice = useUsdPrice(asset) const USDPrice = useUsdPrice(asset)
const assetsFilter = [{ address: asset.address, tokenId: asset.tokenId }] const assetsFilter = [{ address: asset.address, tokenId: asset.tokenId }]
const { walletAssets: ownerAssets } = useNftBalanceQuery(account ?? '', [], assetsFilter, 1) const { walletAssets: ownerAssets } = useNftBalance(account ?? '', [], assetsFilter, 1)
const walletAsset: WalletAsset | undefined = useMemo( const walletAsset: WalletAsset | undefined = useMemo(() => ownerAssets?.[0], [ownerAssets])
() => (ownerAssets?.length > 0 ? ownerAssets[0] : undefined),
[ownerAssets]
)
const { assetInBag } = useMemo(() => { const { assetInBag } = useMemo(() => {
return { return {
@ -355,7 +352,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
) )
} }
const isOwner = asset.owner && !!walletAsset && account?.toLowerCase() === asset.owner?.address?.toLowerCase() const isOwner = asset.ownerAddress && !!walletAsset && account?.toLowerCase() === asset.ownerAddress?.toLowerCase()
const isForSale = cheapestOrder && asset.priceInfo const isForSale = cheapestOrder && asset.priceInfo
return ( return (
@ -424,20 +421,20 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
)} )}
{isForSale && ( {isForSale && (
<OwnerInformationContainer> <OwnerInformationContainer>
{asset.tokenType !== 'ERC1155' && asset.owner.address && ( {asset.tokenType !== 'ERC1155' && asset.ownerAddress && (
<ThemedText.BodySmall color="textSecondary" lineHeight="20px"> <ThemedText.BodySmall color="textSecondary" lineHeight="20px">
Seller: Seller:
</ThemedText.BodySmall> </ThemedText.BodySmall>
)} )}
<OwnerText <OwnerText
target="_blank" target="_blank"
href={`https://etherscan.io/address/${asset.owner.address}`} href={`https://etherscan.io/address/${asset.ownerAddress}`}
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{asset.tokenType === 'ERC1155' ? ( {asset.tokenType === 'ERC1155' ? (
'' ''
) : ( ) : (
<span> {isOwner ? 'You' : asset.owner.address && shortenAddress(asset.owner.address, 2, 4)}</span> <span> {isOwner ? 'You' : asset.ownerAddress && shortenAddress(asset.ownerAddress, 2, 4)}</span>
)} )}
</OwnerText> </OwnerText>
</OwnerInformationContainer> </OwnerInformationContainer>

@ -1,8 +1,7 @@
import { useLoadCollectionQuery } from 'graphql/data/nft/Collection'
import { fetchTrendingCollections } from 'nft/queries' import { fetchTrendingCollections } from 'nft/queries'
import { TimePeriod } from 'nft/types' import { TimePeriod } from 'nft/types'
import { calculateCardIndex } from 'nft/utils' import { calculateCardIndex } from 'nft/utils'
import { Suspense, useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
@ -137,10 +136,6 @@ const Banner = () => {
[data] [data]
) )
// Trigger queries for the top trending collections, so that the data is immediately available if the user clicks through.
const collectionAddresses = useMemo(() => collections?.map(({ address }) => address), [collections])
useLoadCollectionQuery(collectionAddresses)
const [activeCollectionIdx, setActiveCollectionIdx] = useState(0) const [activeCollectionIdx, setActiveCollectionIdx] = useState(0)
const onToggleNextSlide = useCallback( const onToggleNextSlide = useCallback(
(direction: number) => { (direction: number) => {
@ -169,13 +164,11 @@ const Banner = () => {
{collections ? ( {collections ? (
<Carousel activeIndex={activeCollectionIdx} toggleNextSlide={onToggleNextSlide}> <Carousel activeIndex={activeCollectionIdx} toggleNextSlide={onToggleNextSlide}>
{collections.map((collection) => ( {collections.map((collection) => (
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}> <CarouselCard
<CarouselCard key={collection.address}
key={collection.address} collection={collection}
collection={collection} onClick={() => navigate(`/nfts/collection/${collection.address}`)}
onClick={() => navigate(`/nfts/collection/${collection.address}`)} />
/>
</Suspense>
))} ))}
</Carousel> </Carousel>
) : ( ) : (

@ -1,7 +1,7 @@
import { formatNumberOrString, NumberType } from '@uniswap/conedison/format' import { formatNumberOrString, NumberType } from '@uniswap/conedison/format'
import { loadingAnimation } from 'components/Loader/styled' import { loadingAnimation } from 'components/Loader/styled'
import { LoadingBubble } from 'components/Tokens/loading' import { LoadingBubble } from 'components/Tokens/loading'
import { useCollectionQuery } from 'graphql/data/nft/Collection' import { useCollection } from 'graphql/data/nft/Collection'
import { VerifiedIcon } from 'nft/components/icons' import { VerifiedIcon } from 'nft/components/icons'
import { Markets, TrendingCollection } from 'nft/types' import { Markets, TrendingCollection } from 'nft/types'
import { formatWeiToDecimal } from 'nft/utils' import { formatWeiToDecimal } from 'nft/utils'
@ -235,7 +235,9 @@ const MARKETS_ENUM_TO_NAME = {
} }
export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => { export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => {
const gqlCollection = useCollectionQuery(collection.address) const { data: gqlCollection, loading } = useCollection(collection.address)
if (loading) return <LoadingCarouselCard />
return ( return (
<CarouselCardBorder> <CarouselCardBorder>

@ -152,7 +152,7 @@ const PriceTextInput = ({
inputRef.current.value = listPrice !== undefined ? `${listPrice}` : '' inputRef.current.value = listPrice !== undefined ? `${listPrice}` : ''
setWarningType(WarningType.NONE) setWarningType(WarningType.NONE)
if (!warning && listPrice) { if (!warning && listPrice) {
if (listPrice < asset.floorPrice) setWarningType(WarningType.BELOW_FLOOR) if (listPrice < (asset?.floorPrice ?? 0)) setWarningType(WarningType.BELOW_FLOOR)
else if (asset.floor_sell_order_price && listPrice >= asset.floor_sell_order_price) else if (asset.floor_sell_order_price && listPrice >= asset.floor_sell_order_price)
setWarningType(WarningType.ALREADY_LISTED) setWarningType(WarningType.ALREADY_LISTED)
} else if (warning && listPrice && listPrice >= 0) removeMarketplaceWarning(asset, warning) } else if (warning && listPrice && listPrice >= 0) removeMarketplaceWarning(asset, warning)
@ -226,10 +226,14 @@ const PriceTextInput = ({
> >
{focused ? ( {focused ? (
<> <>
<Row display={asset.lastPrice ? 'flex' : 'none'} marginRight="8"> {!!asset.lastPrice && (
LAST: {formatEth(asset.lastPrice)} ETH <Row display={asset.lastPrice ? 'flex' : 'none'} marginRight="8">
</Row> LAST: {formatEth(asset.lastPrice)} ETH
<Row display={asset.floorPrice ? 'flex' : 'none'}>FLOOR: {formatEth(asset.floorPrice)} ETH</Row> </Row>
)}
{!!asset.floorPrice && (
<Row display={asset.floorPrice ? 'flex' : 'none'}>FLOOR: {formatEth(asset.floorPrice)} ETH</Row>
)}
</> </>
) : ( ) : (
<> <>
@ -239,8 +243,8 @@ const PriceTextInput = ({
<> <>
{warningType} {warningType}
{warningType === WarningType.BELOW_FLOOR {warningType === WarningType.BELOW_FLOOR
? formatEth(asset.floorPrice) ? formatEth(asset?.floorPrice ?? 0)
: formatEth(asset.floor_sell_order_price)} : formatEth(asset?.floor_sell_order_price ?? 0)}
ETH ETH
<Box <Box
color={warningType === WarningType.BELOW_FLOOR ? 'accentAction' : 'orange'} color={warningType === WarningType.BELOW_FLOOR ? 'accentAction' : 'orange'}
@ -335,7 +339,7 @@ const MarketplaceRow = ({
const royalties = const royalties =
(selectedMarkets.length === 1 && selectedMarkets[0].name === 'LooksRare' (selectedMarkets.length === 1 && selectedMarkets[0].name === 'LooksRare'
? LOOKS_RARE_CREATOR_BASIS_POINTS ? LOOKS_RARE_CREATOR_BASIS_POINTS
: asset.basisPoints) * 0.01 : asset?.basisPoints ?? 0) * 0.01
const feeInEth = price && (price * (royalties + marketplaceFee)) / 100 const feeInEth = price && (price * (royalties + marketplaceFee)) / 100
const userReceives = price && feeInEth && price - feeInEth const userReceives = price && feeInEth && price - feeInEth

@ -1,4 +1,4 @@
import { useNftBalanceQuery } from 'graphql/data/nft/NftBalance' import { useNftBalance } from 'graphql/data/nft/NftBalance'
import { AnimatedBox, Box } from 'nft/components/Box' import { AnimatedBox, Box } from 'nft/components/Box'
import { ClearAllButton, LoadingAssets } from 'nft/components/collection/CollectionNfts' import { ClearAllButton, LoadingAssets } from 'nft/components/collection/CollectionNfts'
import { assetList } from 'nft/components/collection/CollectionNfts.css' import { assetList } from 'nft/components/collection/CollectionNfts.css'
@ -198,10 +198,10 @@ const ProfilePageNfts = ({
const { const {
walletAssets: ownerAssets, walletAssets: ownerAssets,
loadNext, loading,
hasNext, hasNext,
isLoadingNext, loadMore,
} = useNftBalanceQuery(address, collectionFilters, [], DEFAULT_WALLET_ASSET_QUERY_AMOUNT) } = useNftBalance(address, collectionFilters, [], DEFAULT_WALLET_ASSET_QUERY_AMOUNT)
const { gridX } = useSpring({ const { gridX } = useSpring({
gridX: isFiltersExpanded ? FILTER_SIDEBAR_WIDTH : -PADDING, gridX: isFiltersExpanded ? FILTER_SIDEBAR_WIDTH : -PADDING,
@ -211,6 +211,8 @@ const ProfilePageNfts = ({
}, },
}) })
if (loading) return <ProfileBodyLoadingSkeleton />
return ( return (
<Column width="full"> <Column width="full">
{ownerAssets?.length === 0 ? ( {ownerAssets?.length === 0 ? (
@ -242,13 +244,13 @@ const ProfilePageNfts = ({
/> />
</Row> </Row>
<InfiniteScroll <InfiniteScroll
next={() => loadNext(DEFAULT_WALLET_ASSET_QUERY_AMOUNT)} next={loadMore}
hasMore={hasNext} hasMore={hasNext ?? false}
loader={ loader={
Boolean(hasNext && ownerAssets?.length) && <LoadingAssets count={DEFAULT_WALLET_ASSET_QUERY_AMOUNT} /> Boolean(hasNext && ownerAssets?.length) && <LoadingAssets count={DEFAULT_WALLET_ASSET_QUERY_AMOUNT} />
} }
dataLength={ownerAssets?.length ?? 0} dataLength={ownerAssets?.length ?? 0}
className={ownerAssets?.length || isLoadingNext ? assetList : undefined} className={ownerAssets?.length ? assetList : undefined}
style={{ overflow: 'unset' }} style={{ overflow: 'unset' }}
> >
{ownerAssets?.length {ownerAssets?.length

@ -4,13 +4,14 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events' import { EventName } from '@uniswap/analytics-events'
import { MouseoverTooltip } from 'components/Tooltip' import { MouseoverTooltip } from 'components/Tooltip'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import * as Card from 'nft/components/collection/Card' import * as Card from 'nft/components/collection/Card'
import { AssetMediaType } from 'nft/components/collection/Card' import { AssetMediaType } from 'nft/components/collection/Card'
import { bodySmall } from 'nft/css/common.css' import { bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useSellAsset } from 'nft/hooks' import { useBag, useIsMobile, useSellAsset } from 'nft/hooks'
import { TokenType, WalletAsset } from 'nft/types' import { WalletAsset } from 'nft/types'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
const TOOLTIP_TIMEOUT = 2000 const TOOLTIP_TIMEOUT = 2000
@ -39,7 +40,7 @@ const getNftDisplayComponent = (
const getUnsupportedNftTextComponent = (asset: WalletAsset) => ( const getUnsupportedNftTextComponent = (asset: WalletAsset) => (
<Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}> <Box as="span" className={bodySmall} style={{ color: themeVars.colors.textPrimary }}>
{asset.asset_contract.tokenType === TokenType.ERC1155 ? ( {asset.asset_contract.tokenType === NftStandard.Erc1155 ? (
<Trans>Selling ERC-1155s coming soon</Trans> <Trans>Selling ERC-1155s coming soon</Trans>
) : ( ) : (
<Trans>Blocked from trading</Trans> <Trans>Blocked from trading</Trans>
@ -109,7 +110,7 @@ export const ViewMyNftsAsset = ({
}, [isSelected, isSelectedRef]) }, [isSelected, isSelectedRef])
const assetMediaType = Card.useAssetMediaType(asset) const assetMediaType = Card.useAssetMediaType(asset)
const isDisabled = asset.asset_contract.tokenType === TokenType.ERC1155 || asset.susFlag const isDisabled = asset.asset_contract.tokenType === NftStandard.Erc1155 || asset.susFlag
return ( return (
<Card.Container <Card.Container

@ -1,5 +1,6 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { BagItem, BagItemStatus, BagStatus, TokenType, UpdatedGenieAsset } from 'nft/types' import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
@ -88,7 +89,7 @@ export const useBag = create<BagState>()(
const itemsInBagCopy = [...itemsInBag] const itemsInBagCopy = [...itemsInBag]
assets.forEach((asset) => { assets.forEach((asset) => {
let index = -1 let index = -1
if (asset.tokenType !== TokenType.ERC1155) { if (asset.tokenType !== NftStandard.Erc1155) {
index = itemsInBag.findIndex( index = itemsInBag.findIndex(
(n) => n.asset.tokenId === asset.tokenId && n.asset.address === asset.address (n) => n.asset.tokenId === asset.tokenId && n.asset.address === asset.address
) )

@ -1,4 +1,4 @@
import { NftAssetSortableField } from 'graphql/data/nft/__generated__/AssetPaginationQuery.graphql' import { NftAssetSortableField } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'

@ -1,3 +1,4 @@
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import create from 'zustand' import create from 'zustand'
import { devtools } from 'zustand/middleware' import { devtools } from 'zustand/middleware'
@ -28,7 +29,7 @@ export const useWalletCollections = create<WalletCollectionState>()(
setWalletAssets: (assets) => setWalletAssets: (assets) =>
set(() => { set(() => {
return { return {
walletAssets: assets?.filter((asset) => asset.asset_contract?.schema_name === 'ERC721'), walletAssets: assets?.filter((asset) => asset.asset_contract?.tokenType === NftStandard.Erc721),
} }
}), }),
setWalletCollections: (collections) => setWalletCollections: (collections) =>

@ -1,13 +1,9 @@
import { Trace } from '@uniswap/analytics' import { Trace } from '@uniswap/analytics'
import { PageName } from '@uniswap/analytics-events' import { PageName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core' import { useNftAssetDetails } from 'graphql/data/nft/Details'
import { useDetailsQuery, useLoadDetailsQuery } from 'graphql/data/nft/Details'
import { useLoadNftBalanceQuery } from 'graphql/data/nft/NftBalance'
import { AssetDetails } from 'nft/components/details/AssetDetails' import { AssetDetails } from 'nft/components/details/AssetDetails'
import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading' import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading'
import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails' import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails'
import { useBag } from 'nft/hooks'
import { Suspense, useEffect, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
@ -38,11 +34,13 @@ const AssetPriceDetailsContainer = styled.div`
} }
` `
const Asset = () => { const AssetPage = () => {
const { tokenId = '', contractAddress = '' } = useParams() const { tokenId = '', contractAddress = '' } = useParams()
const data = useDetailsQuery(contractAddress, tokenId) const { data, loading } = useNftAssetDetails(contractAddress, tokenId)
const [asset, collection] = useMemo(() => data ?? [], [data]) const [asset, collection] = data
if (loading) return <AssetDetailsLoading />
return ( return (
<> <>
@ -51,35 +49,17 @@ const Asset = () => {
properties={{ collection_address: contractAddress, token_id: tokenId }} properties={{ collection_address: contractAddress, token_id: tokenId }}
shouldLogImpression shouldLogImpression
> >
{asset && collection ? ( {!!asset && !!collection && (
<AssetContainer> <AssetContainer>
<AssetDetails collection={collection} asset={asset} /> <AssetDetails collection={collection} asset={asset} />
<AssetPriceDetailsContainer> <AssetPriceDetailsContainer>
<AssetPriceDetails collection={collection} asset={asset} /> <AssetPriceDetails collection={collection} asset={asset} />
</AssetPriceDetailsContainer> </AssetPriceDetailsContainer>
</AssetContainer> </AssetContainer>
) : null} )}
</Trace> </Trace>
</> </>
) )
} }
const AssetPage = () => {
const { tokenId, contractAddress } = useParams()
const { account } = useWeb3React()
const setBagExpanded = useBag((state) => state.setBagExpanded)
useLoadDetailsQuery(contractAddress, tokenId)
useLoadNftBalanceQuery(account, contractAddress, tokenId)
useEffect(() => {
setBagExpanded({ bagExpanded: false, manualClose: false })
}, []) // eslint-disable-line react-hooks/exhaustive-deps
return (
<Suspense fallback={<AssetDetailsLoading />}>
<Asset />
</Suspense>
)
}
export default AssetPage export default AssetPage

@ -5,8 +5,7 @@ import Column from 'components/Column'
import { OpacityHoverState } from 'components/Common' import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row' import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading' import { LoadingBubble } from 'components/Tokens/loading'
import { useLoadAssetsQuery } from 'graphql/data/nft/Asset' import { useCollection } from 'graphql/data/nft/Collection'
import { useCollectionQuery, useLoadCollectionQuery } from 'graphql/data/nft/Collection'
import { useScreenSize } from 'hooks/useScreenSize' import { useScreenSize } from 'hooks/useScreenSize'
import { BAG_WIDTH, XXXL_BAG_WIDTH } from 'nft/components/bag/Bag' import { BAG_WIDTH, XXXL_BAG_WIDTH } from 'nft/components/bag/Bag'
import { MobileHoverBag } from 'nft/components/bag/MobileHoverBag' import { MobileHoverBag } from 'nft/components/bag/MobileHoverBag'
@ -16,7 +15,6 @@ import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPage
import { BagCloseIcon } from 'nft/components/icons' import { BagCloseIcon } from 'nft/components/icons'
import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks' import { useBag, useCollectionFilters, useFiltersExpanded, useIsMobile } from 'nft/hooks'
import * as styles from 'nft/pages/collection/index.css' import * as styles from 'nft/pages/collection/index.css'
import { GenieCollection } from 'nft/types'
import { Suspense, useEffect } from 'react' import { Suspense, useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { animated, easings, useSpring } from 'react-spring' import { animated, easings, useSpring } from 'react-spring'
@ -26,6 +24,7 @@ import { TRANSITION_DURATIONS } from 'theme/styles'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
const FILTER_WIDTH = 332 const FILTER_WIDTH = 332
const EMPTY_TRAIT_OBJ = {}
export const CollectionBannerLoading = styled(LoadingBubble)` export const CollectionBannerLoading = styled(LoadingBubble)`
width: 100%; width: 100%;
@ -133,7 +132,7 @@ const Collection = () => {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const screenSize = useScreenSize() const screenSize = useScreenSize()
const collectionStats = useCollectionQuery(contractAddress as string) const { data: collectionStats, loading } = useCollection(contractAddress as string)
const { CollectionContainerWidthChange } = useSpring({ const { CollectionContainerWidthChange } = useSpring({
CollectionContainerWidthChange: CollectionContainerWidthChange:
@ -169,6 +168,8 @@ const Collection = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
if (loading) return <CollectionPageSkeleton />
const toggleActivity = () => { const toggleActivity = () => {
isActivityToggled isActivityToggled
? navigate(`/nfts/collection/${contractAddress}`) ? navigate(`/nfts/collection/${contractAddress}`)
@ -197,9 +198,7 @@ const Collection = () => {
/> />
</BannerWrapper> </BannerWrapper>
<CollectionDescriptionSection> <CollectionDescriptionSection>
{collectionStats && ( {collectionStats && <CollectionStats stats={collectionStats} isMobile={isMobile} />}
<CollectionStats stats={collectionStats || ({} as GenieCollection)} isMobile={isMobile} />
)}
<div id="nft-anchor" /> <div id="nft-anchor" />
<ActivitySwitcher <ActivitySwitcher
showActivity={isActivityToggled} showActivity={isActivityToggled}
@ -221,7 +220,7 @@ const Collection = () => {
</IconWrapper> </IconWrapper>
</MobileFilterHeader> </MobileFilterHeader>
)} )}
<Filters traitsByGroup={collectionStats?.traits ?? {}} /> <Filters traitsByGroup={collectionStats?.traits ?? EMPTY_TRAIT_OBJ} />
</> </>
)} )}
</FiltersContainer> </FiltersContainer>
@ -246,7 +245,7 @@ const Collection = () => {
collectionStats && ( collectionStats && (
<Suspense fallback={<CollectionNftsAndMenuLoading />}> <Suspense fallback={<CollectionNftsAndMenuLoading />}>
<CollectionNfts <CollectionNfts
collectionStats={collectionStats || ({} as GenieCollection)} collectionStats={collectionStats}
contractAddress={contractAddress} contractAddress={contractAddress}
rarityVerified={collectionStats?.rarityVerified} rarityVerified={collectionStats?.rarityVerified}
/> />
@ -265,21 +264,4 @@ const Collection = () => {
) )
} }
// The page is responsible for any queries that must be run on initial load. export default Collection
// Triggering query load from the page prevents waterfalled requests, as lazy-loading them in components would prevent
// any children from rendering.
const CollectionPage = () => {
const { contractAddress } = useParams()
useLoadCollectionQuery(contractAddress)
useLoadAssetsQuery(contractAddress)
// The Collection must be wrapped in suspense so that it does not suspend the CollectionPage,
// which is needed to trigger query loads.
return (
<Suspense fallback={<CollectionPageSkeleton />}>
<Collection />
</Suspense>
)
}
export default CollectionPage

@ -1,4 +1,5 @@
import { GenieAsset, RouteResponse, TokenType } from '../../types' import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { GenieAsset, RouteResponse } from 'nft/types'
export const fetchRoute = async ({ export const fetchRoute = async ({
toSell, toSell,
@ -41,7 +42,7 @@ type RouteItem = {
decimals: number decimals: number
address: string address: string
priceInfo: ApiPriceInfo priceInfo: ApiPriceInfo
tokenType: TokenType tokenType?: NftStandard
tokenId: string tokenId: string
amount: number amount: number
marketplace?: string marketplace?: string

@ -1,3 +1,4 @@
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SortBy } from 'nft/hooks' import { SortBy } from 'nft/hooks'
import { SellOrder } from '../sell' import { SellOrder } from '../sell'
@ -81,7 +82,7 @@ export interface Trait {
export interface GenieAsset { export interface GenieAsset {
id?: string // This would be a random id created and assigned by front end id?: string // This would be a random id created and assigned by front end
address: string address: string
notForSale: boolean notForSale?: boolean
collectionName?: string collectionName?: string
collectionSymbol?: string collectionSymbol?: string
imageUrl?: string imageUrl?: string
@ -93,17 +94,15 @@ export interface GenieAsset {
sellorders?: SellOrder[] sellorders?: SellOrder[]
smallImageUrl?: string smallImageUrl?: string
tokenId: string tokenId: string
tokenType: TokenType tokenType?: NftStandard
totalCount?: number // The totalCount from the query to /assets totalCount?: number // The totalCount from the query to /assets
collectionIsVerified?: boolean collectionIsVerified?: boolean
rarity?: Rarity rarity?: Rarity
owner: { ownerAddress?: string
address: string metadataUrl?: string
}
metadataUrl: string
creator: { creator: {
address: string address?: string
profile_img_url: string profile_img_url?: string
} }
traits?: Trait[] traits?: Trait[]
} }

@ -1,6 +1,6 @@
import { NftMarketplace, OrderStatus, OrderType } from 'graphql/data/nft/__generated__/DetailsQuery.graphql' import { NftMarketplace, NftStandard, OrderStatus, OrderType } from 'graphql/data/__generated__/types-and-hooks'
import { GenieCollection, PriceInfo, TokenType } from '../common' import { GenieCollection, PriceInfo } from '../common'
export interface ListingMarket { export interface ListingMarket {
name: string name: string
@ -15,20 +15,20 @@ export interface ListingWarning {
export interface SellOrder { export interface SellOrder {
address: string address: string
createdAt: number createdAt: number
endAt: number endAt?: number
id: string id: string
maker: string maker: string
marketplace: NftMarketplace marketplace: NftMarketplace
marketplaceUrl: string marketplaceUrl: string
orderHash: string orderHash?: string
price: { price: {
currency: string currency?: string
value: number value: number
} }
quantity: number quantity: number
startAt: number startAt: number
status: OrderStatus status: OrderStatus
tokenId: string tokenId?: string
type: OrderType type: OrderType
protocolParameters: Record<string, unknown> protocolParameters: Record<string, unknown>
} }
@ -41,32 +41,31 @@ export interface Listing {
export interface WalletAsset { export interface WalletAsset {
id?: string id?: string
imageUrl: string imageUrl?: string
smallImageUrl: string smallImageUrl?: string
notForSale: boolean notForSale: boolean
animationUrl: string animationUrl?: string
susFlag: boolean susFlag?: boolean
priceInfo: PriceInfo priceInfo?: PriceInfo
name: string name?: string
tokenId: string tokenId?: string
asset_contract: { asset_contract: {
address: string address?: string
schema_name: 'ERC1155' | 'ERC721' | string name?: string
name: string description?: string
description: string image_url?: string
image_url: string payout_address?: string
payout_address: string tokenType?: NftStandard
tokenType: TokenType
} }
collection: GenieCollection collection?: GenieCollection
collectionIsVerified: boolean collectionIsVerified?: boolean
lastPrice: number lastPrice?: number
floorPrice: number floorPrice?: number
basisPoints: number basisPoints?: number
listing_date: string listing_date?: string
date_acquired: string date_acquired?: string
sellOrders: SellOrder[] sellOrders?: SellOrder[]
floor_sell_order_price: number floor_sell_order_price?: number
// Used for creating new listings // Used for creating new listings
expirationTime?: number expirationTime?: number
marketAgnosticPrice?: number marketAgnosticPrice?: number
@ -95,8 +94,8 @@ export enum ListingStatus {
} }
export interface AssetRow { export interface AssetRow {
images: string[] images: (string | undefined)[]
name: string name?: string
status: ListingStatus status: ListingStatus
callback?: () => Promise<void> callback?: () => Promise<void>
} }
@ -108,7 +107,7 @@ export interface ListingRow extends AssetRow {
} }
export interface CollectionRow extends AssetRow { export interface CollectionRow extends AssetRow {
collectionAddress: string collectionAddress?: string
marketplace: ListingMarket marketplace: ListingMarket
} }

@ -62,7 +62,7 @@ const getConsiderationItems = (
creatorFee?: ConsiderationInputItem creatorFee?: ConsiderationInputItem
} => { } => {
const openSeaBasisPoints = OPENSEA_DEFAULT_FEE * INVERSE_BASIS_POINTS const openSeaBasisPoints = OPENSEA_DEFAULT_FEE * INVERSE_BASIS_POINTS
const creatorFeeBasisPoints = asset.basisPoints const creatorFeeBasisPoints = asset?.basisPoints ?? 0
const sellerBasisPoints = INVERSE_BASIS_POINTS - openSeaBasisPoints - creatorFeeBasisPoints const sellerBasisPoints = INVERSE_BASIS_POINTS - openSeaBasisPoints - creatorFeeBasisPoints
const openseaFee = price.mul(BigNumber.from(openSeaBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString() const openseaFee = price.mul(BigNumber.from(openSeaBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
@ -76,7 +76,9 @@ const getConsiderationItems = (
sellerFee: createConsiderationItem(sellerFee, signerAddress), sellerFee: createConsiderationItem(sellerFee, signerAddress),
openseaFee: createConsiderationItem(openseaFee, OPENSEA_FEE_ADDRESS), openseaFee: createConsiderationItem(openseaFee, OPENSEA_FEE_ADDRESS),
creatorFee: creatorFee:
creatorFeeBasisPoints > 0 ? createConsiderationItem(creatorFee, asset.asset_contract.payout_address) : undefined, creatorFeeBasisPoints > 0
? createConsiderationItem(creatorFee, asset?.asset_contract?.payout_address ?? '')
: undefined,
} }
} }
@ -128,7 +130,7 @@ export async function signListing(
const signerAddress = await signer.getAddress() const signerAddress = await signer.getAddress()
const listingPrice = asset.newListings?.find((listing) => listing.marketplace.name === marketplace.name)?.price const listingPrice = asset.newListings?.find((listing) => listing.marketplace.name === marketplace.name)?.price
if (!listingPrice || !asset.expirationTime) return false if (!listingPrice || !asset.expirationTime || !asset.asset_contract.address || !asset.tokenId) return false
switch (marketplace.name) { switch (marketplace.name) {
case 'OpenSea': case 'OpenSea':
try { try {

@ -6,9 +6,6 @@ import TopLevelModals from 'components/TopLevelModals'
import { useFeatureFlagsIsLoaded } from 'featureFlags' import { useFeatureFlagsIsLoaded } from 'featureFlags'
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader' import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { CollectionPageSkeleton } from 'nft/components/collection/CollectionPageSkeleton'
import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading'
import { ProfilePageLoadingSkeleton } from 'nft/components/profile/view/ProfilePageLoadingSkeleton'
import { lazy, Suspense, useEffect, useState } from 'react' import { lazy, Suspense, useEffect, useState } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom' import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks' import { useIsDarkMode } from 'state/user/hooks'
@ -251,7 +248,6 @@ export default function App() {
<Route <Route
path="/nfts" path="/nfts"
element={ element={
// TODO: replace loading state during Apollo migration
<Suspense fallback={null}> <Suspense fallback={null}>
<NftExplore /> <NftExplore />
</Suspense> </Suspense>
@ -260,7 +256,7 @@ export default function App() {
<Route <Route
path="/nfts/asset/:contractAddress/:tokenId" path="/nfts/asset/:contractAddress/:tokenId"
element={ element={
<Suspense fallback={<AssetDetailsLoading />}> <Suspense fallback={null}>
<Asset /> <Asset />
</Suspense> </Suspense>
} }
@ -268,7 +264,7 @@ export default function App() {
<Route <Route
path="/nfts/profile" path="/nfts/profile"
element={ element={
<Suspense fallback={<ProfilePageLoadingSkeleton />}> <Suspense fallback={null}>
<Profile /> <Profile />
</Suspense> </Suspense>
} }
@ -276,7 +272,7 @@ export default function App() {
<Route <Route
path="/nfts/collection/:contractAddress" path="/nfts/collection/:contractAddress"
element={ element={
<Suspense fallback={<CollectionPageSkeleton />}> <Suspense fallback={null}>
<Collection /> <Collection />
</Suspense> </Suspense>
} }
@ -284,7 +280,7 @@ export default function App() {
<Route <Route
path="/nfts/collection/:contractAddress/activity" path="/nfts/collection/:contractAddress/activity"
element={ element={
<Suspense fallback={<CollectionPageSkeleton />}> <Suspense fallback={null}>
<Collection /> <Collection />
</Suspense> </Suspense>
} }

@ -1,13 +1,11 @@
import TokenDetails from 'components/Tokens/TokenDetails' import TokenDetails from 'components/Tokens/TokenDetails'
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton' import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { TokenQuery, tokenQuery } from 'graphql/data/Token' import { useTokenPriceQuery, useTokenQuery } from 'graphql/data/__generated__/types-and-hooks'
import { TokenPriceQuery, tokenPriceQuery } from 'graphql/data/TokenPrice'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util' import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage } from 'jotai/utils'
import { Suspense, useCallback, useEffect, useMemo } from 'react' import { useMemo } from 'react'
import { useQueryLoader } from 'react-relay'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY) export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
@ -26,35 +24,28 @@ export default function TokenDetailsPage() {
[chain, isNative, pageChainId, timePeriod, tokenAddress] [chain, isNative, pageChainId, timePeriod, tokenAddress]
) )
const [tokenQueryReference, loadTokenQuery] = useQueryLoader<TokenQuery>(tokenQuery) const { data: tokenQuery, loading: tokenQueryLoading } = useTokenQuery({
const [priceQueryReference, loadPriceQuery] = useQueryLoader<TokenPriceQuery>(tokenPriceQuery) variables: {
contract,
useEffect(() => {
loadTokenQuery({ contract })
loadPriceQuery({ contract, duration })
}, [contract, duration, loadPriceQuery, loadTokenQuery, timePeriod])
const refetchTokenPrices = useCallback(
(t: TimePeriod) => {
loadPriceQuery({ contract, duration: toHistoryDuration(t) })
setTimePeriod(t)
}, },
[contract, loadPriceQuery, setTimePeriod] })
)
if (!tokenQueryReference) { const { data: tokenPriceQuery } = useTokenPriceQuery({
return <TokenDetailsPageSkeleton /> variables: {
} contract,
duration,
},
})
if (!tokenQuery || tokenQueryLoading) return <TokenDetailsPageSkeleton />
return ( return (
<Suspense fallback={<TokenDetailsPageSkeleton />}> <TokenDetails
<TokenDetails urlAddress={tokenAddress}
urlAddress={tokenAddress} chain={chain}
chain={chain} tokenQuery={tokenQuery}
tokenQueryReference={tokenQueryReference} tokenPriceQuery={tokenPriceQuery}
priceQueryReference={priceQueryReference} onChangeTimePeriod={setTimePeriod}
refetchTokenPrices={refetchTokenPrices} />
/>
</Suspense>
) )
} }

@ -7,12 +7,11 @@ import { filterStringAtom } from 'components/Tokens/state'
import NetworkFilter from 'components/Tokens/TokenTable/NetworkFilter' import NetworkFilter from 'components/Tokens/TokenTable/NetworkFilter'
import SearchBar from 'components/Tokens/TokenTable/SearchBar' import SearchBar from 'components/Tokens/TokenTable/SearchBar'
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector' import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable' import TokenTable from 'components/Tokens/TokenTable/TokenTable'
import { PAGE_SIZE } from 'graphql/data/TopTokens'
import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util' import { chainIdToBackendName, isValidBackendChainName } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { useResetAtom } from 'jotai/utils' import { useResetAtom } from 'jotai/utils'
import { Suspense, useEffect, useState } from 'react' import { useEffect } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom' import { useLocation, useNavigate, useParams } from 'react-router-dom'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
@ -75,8 +74,6 @@ const Tokens = () => {
const { chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const connectedChainName = chainIdToBackendName(connectedChainId) const connectedChainName = chainIdToBackendName(connectedChainId)
const [rowCount, setRowCount] = useState(PAGE_SIZE)
useEffect(() => { useEffect(() => {
resetFilterString() resetFilterString()
}, [location, resetFilterString]) }, [location, resetFilterString])
@ -110,9 +107,7 @@ const Tokens = () => {
<SearchBar /> <SearchBar />
</SearchContainer> </SearchContainer>
</FiltersWrapper> </FiltersWrapper>
<Suspense fallback={<LoadingTokenTable rowCount={rowCount} />}> <TokenTable />
<TokenTable setRowCount={setRowCount} />
</Suspense>
</ExploreContainer> </ExploreContainer>
</Trace> </Trace>
) )

@ -25,7 +25,3 @@ declare module 'multihashes' {
declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array } declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array }
declare function toB58String(hash: Uint8Array): string declare function toB58String(hash: Uint8Array): string
} }
declare module 'babel-plugin-relay/macro' {
export { graphql as default } from 'react-relay'
}

2180
yarn.lock

File diff suppressed because it is too large Load Diff