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
*.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
# generated graphql types
__generated__/
schema.graphql
# 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 */
require('dotenv').config({ path: '.env.production' })
const { exec } = require('child_process')
const dataConfig = require('./relay.config')
const thegraphConfig = require('./relay_thegraph.config')
const dataConfig = require('./graphql.config')
const thegraphConfig = require('./graphql_thegraph.config')
/* eslint-enable */
function fetchSchema(url, outputFile) {

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

@ -8,10 +8,10 @@
"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": "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: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",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
@ -94,7 +94,6 @@
"@typescript-eslint/parser": "^4",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-relay": "^14.1.0",
"cypress": "^10.3.1",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
@ -113,16 +112,23 @@
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1",
"react-scripts": "^4.0.3",
"relay-compiler": "^14.1.0",
"serve": "^11.3.2",
"ts-transform-graphql-tag": "^0.2.1",
"typechain": "^5.0.0",
"typescript": "^4.4.3",
"yarn-deduplicate": "^6.0.0"
},
"dependencies": {
"@apollo/client": "^3.7.2",
"@coinbase/wallet-sdk": "^3.3.0",
"@fontsource/ibm-plex-mono": "^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/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
@ -135,7 +141,6 @@
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "7.20.1",
"@types/react-relay": "^13.0.2",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "1.2.0",
"@uniswap/analytics-events": "^1.5.0",
@ -215,8 +220,6 @@
"react-popper": "^2.2.3",
"react-query": "^3.39.1",
"react-redux": "^8.0.2",
"react-relay": "^14.1.0",
"react-relay-network-modern": "^6.2.1",
"react-router-dom": "^6.3.0",
"react-spring": "^9.5.5",
"react-table": "^7.8.0",

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

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

@ -27,21 +27,20 @@ import Widget from 'components/Widget'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
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 { QueryToken, tokenQuery } from 'graphql/data/Token'
import { QueryToken } from 'graphql/data/Token'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
import { useCallback, useMemo, useState, useTransition } from 'react'
import { ArrowLeft } from 'react-feather'
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { isAddress } from 'utils'
import { RefetchPricesFunction } from './ChartSection'
import { OnChangeTimePeriod } from './ChartSection'
import InvalidTokenDetails from './InvalidTokenDetails'
const TokenSymbol = styled.span`
@ -75,7 +74,7 @@ function useRelevantToken(
const queryToken = useMemo(() => {
if (!address) return undefined
if (address === NATIVE_CHAIN_ID) return nativeOnChain(pageChainId)
if (tokenQueryData) return new QueryToken(tokenQueryData)
if (tokenQueryData) return new QueryToken(address, tokenQueryData)
return undefined
}, [pageChainId, address, tokenQueryData])
// 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 = {
urlAddress: string | undefined
chain: Chain
tokenQueryReference: PreloadedQuery<TokenQuery>
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined
refetchTokenPrices: RefetchPricesFunction
tokenQuery: TokenQuery
tokenPriceQuery: TokenPriceQuery | undefined
onChangeTimePeriod: OnChangeTimePeriod
}
export default function TokenDetails({
urlAddress,
chain,
tokenQueryReference,
priceQueryReference,
refetchTokenPrices,
tokenQuery,
tokenPriceQuery,
onChangeTimePeriod,
}: TokenDetailsProps) {
if (!urlAddress) {
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 tokenQueryData = usePreloadedQuery(tokenQuery, tokenQueryReference).tokens?.[0]
const tokenQueryData = tokenQuery.tokens?.[0]
const crossChainMap = useMemo(
() =>
tokenQueryData?.project?.tokens.reduce((map, current) => {
@ -200,7 +199,7 @@ export default function TokenDetails({
<ShareButton currency={token} />
</TokenActions>
</TokenInfoContainer>
<ChartSection priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} />
<ChartSection tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
<StatsSection
TVL={tokenQueryData?.market?.totalValueLocked?.value}
volume24H={tokenQueryData?.market?.volume24H?.value}

@ -459,7 +459,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
return (
<div ref={ref} data-testid={`token-table-row-${tokenName}`}>
<StyledLink
to={getTokenDetailsURL(token.address, token.chain)}
to={getTokenDetailsURL(token.address ?? '', token.chain)}
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
>
<TokenRow
@ -512,7 +512,6 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
height={height}
tokenData={token}
pricePercentChange={token.market?.pricePercentChange?.value}
timePeriod={timePeriod}
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 (
<GridContainer>
<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
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { tokens, sparklines } = useTopTokens(chainName)
setRowCount(tokens?.length ?? PAGE_SIZE)
const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
/* loading and error state */
if (!tokens) {
if (loadingTokens) {
return <LoadingTokenTable rowCount={PAGE_SIZE} />
} else if (!tokens) {
return (
<NoTokensState
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 gql from 'graphql-tag'
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'
/*
@ -13,14 +13,14 @@ The difference between Token and TokenProject:
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.
*/
export const tokenQuery = graphql`
query TokenQuery($contract: ContractInput!) {
gql`
query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) {
id @required(action: LOG)
id
decimals
name
chain @required(action: LOG)
address @required(action: LOG)
chain
address
symbol
market(currency: USD) {
totalValueLocked {
@ -48,23 +48,24 @@ export const tokenQuery = graphql`
twitterName
logoUrl
tokens {
chain @required(action: LOG)
address @required(action: LOG)
chain
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.
export class QueryToken extends WrappedTokenInfo {
constructor(data: NonNullable<TokenQueryData>, logoSrc?: string) {
constructor(address: string, data: NonNullable<TokenQueryData>, logoSrc?: string) {
super({
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
address: data.address,
address,
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
symbol: data.symbol ?? '',
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
export const tokenPriceQuery = graphql`
query TokenPriceQuery($contract: ContractInput!, $duration: HistoryDuration!) {
gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) {
market(currency: USD) @required(action: LOG) {
market(currency: USD) {
price {
value @required(action: LOG)
value
}
priceHistory(duration: $duration) {
timestamp @required(action: LOG)
value @required(action: LOG)
timestamp
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 {
filterStringAtom,
filterTimeAtom,
@ -6,22 +5,25 @@ import {
sortMethodAtom,
TokenSortMethod,
} from 'components/Tokens/state'
import gql from 'graphql-tag'
import { useAtomValue } from 'jotai/utils'
import { useEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
import { useMemo } from 'react'
import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql'
import { isPricePoint, PricePoint } from './util'
import { CHAIN_NAME_TO_CHAIN_ID, toHistoryDuration, unwrapToken } from './util'
import {
Chain,
TopTokens100Query,
useTopTokens100Query,
useTopTokensSparklineQuery,
} from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
const topTokens100Query = graphql`
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
gql`
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id @required(action: LOG)
id
name
chain @required(action: LOG)
address @required(action: LOG)
chain
address
symbol
market(currency: USD) {
totalValueLocked {
@ -48,21 +50,21 @@ const topTokens100Query = graphql`
}
`
const tokenSparklineQuery = graphql`
query TopTokensSparklineQuery($duration: HistoryDuration!, $chain: Chain!) {
gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
address
market(currency: USD) {
priceHistory(duration: $duration) {
timestamp @required(action: LOG)
value @required(action: LOG)
timestamp
value
}
}
}
}
`
function useSortedTokens(tokens: NonNullable<TopTokens100Query['response']['topTokens']>) {
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
@ -91,7 +93,7 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['response']['topT
}, [tokens, sortMethod, sortAscending])
}
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']['topTokens']>) {
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
const filterString = useAtomValue(filterStringAtom)
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.
export const PAGE_SIZE = 20
export type TopToken = NonNullable<NonNullable<TopTokens100Query['response']>['topTokens']>[number]
export type SparklineMap = { [key: string]: PricePoint[] | undefined }
export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number]
interface UseTopTokensReturnValue {
tokens: TopToken[] | undefined
loadingTokens: boolean
sparklines: SparklineMap
}
@ -124,33 +127,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const environment = useRelayEnvironment()
const [sparklines, setSparklines] = useState<SparklineMap>({})
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)
},
const { data: sparklineQuery } = useTopTokensSparklineQuery({
variables: { duration, chain },
})
return () => subscription.unsubscribe()
}, [chain, chainId, duration, environment])
useEffect(() => {
setSparklines({})
}, [duration])
const sparklines = useMemo(() => {
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
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 mappedTokens = useMemo(() => topTokens?.map((token) => unwrapToken(chainId, token)) ?? [], [chainId, topTokens])
const { data, loading: loadingTokens } = useTopTokens100Query({
variables: { duration, chain },
})
const mappedTokens = useMemo(
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
[chainId, data]
)
const filteredTokens = useFilteredTokens(mappedTokens)
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 useInterval from 'lib/hooks/useInterval'
import ms from 'ms.macro'
import { GenieAsset, Trait } from 'nft/types'
import gql from 'graphql-tag'
import { GenieAsset, Markets, Trait } from 'nft/types'
import { wrapScientificNotation } from 'nft/utils'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { fetchQuery, useLazyLoadQuery, usePaginationFragment, useQueryLoader, useRelayEnvironment } from 'react-relay'
import { useCallback, useMemo } from 'react'
import { AssetPaginationQuery } from './__generated__/AssetPaginationQuery.graphql'
import {
AssetQuery,
AssetQuery$variables,
AssetQueryVariables,
NftAssetEdge,
NftAssetsFilterInput,
NftAssetSortableField,
NftAssetTraitInput,
NftMarketplace,
} from './__generated__/AssetQuery.graphql'
import { AssetQuery_nftAssets$data } from './__generated__/AssetQuery_nftAssets.graphql'
useAssetQuery,
} from '../__generated__/types-and-hooks'
const assetPaginationQuery = graphql`
fragment AssetQuery_nftAssets on Query @refetchable(queryName: "AssetPaginationQuery") {
gql`
query Asset(
$address: String!
$orderBy: NftAssetSortableField
$asc: Boolean
$filter: NftAssetsFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
nftAssets(
address: $address
orderBy: $orderBy
@ -29,7 +34,7 @@ const assetPaginationQuery = graphql`
after: $after
last: $last
before: $before
) @connection(key: "AssetQuery_nftAssets") {
) {
edges {
node {
id
@ -99,52 +104,38 @@ const assetPaginationQuery = graphql`
}
metadataUrl
}
cursor
}
totalCount
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`
const assetQuery = graphql`
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) {
function formatAssetQueryData(queryAsset: NftAssetEdge, totalCount?: number) {
const asset = queryAsset.node
const ethPrice = parseEther(wrapScientificNotation(asset.listings?.edges[0]?.node.price.value ?? 0)).toString()
return {
id: asset.id,
address: asset?.collection?.nftContracts?.[0]?.address,
address: asset?.collection?.nftContracts?.[0]?.address ?? '',
notForSale: asset.listings?.edges?.length === 0,
collectionName: asset.collection?.name,
collectionSymbol: asset.collection?.image?.url,
imageUrl: asset.image?.url,
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,
priceInfo: asset.listings
? {
priceInfo: {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
}
: undefined,
},
susFlag: asset.suspiciousFlag,
sellorders: asset.listings?.edges.map((listingNode) => {
return {
@ -155,7 +146,7 @@ function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: numb
}
}),
smallImageUrl: asset.smallImage?.url,
tokenId: asset.tokenId,
tokenId: asset.tokenId ?? '',
tokenType: asset.collection?.nftContracts?.[0]?.standard,
totalCount,
collectionIsVerified: asset.collection?.isVerified,
@ -168,7 +159,7 @@ function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: numb
}
}),
},
owner: asset.ownerAddress,
ownerAddress: asset.ownerAddress,
creator: {
profile_img_url: asset.collection?.creator?.profileImage?.url,
address: asset.collection?.creator?.address,
@ -190,57 +181,50 @@ export interface AssetFetcherParams {
before?: string
}
const defaultAssetFetcherParams: Omit<AssetQuery$variables, 'address'> = {
orderBy: 'PRICE',
const defaultAssetFetcherParams: Omit<AssetQueryVariables, 'address'> = {
orderBy: NftAssetSortableField.Price,
asc: true,
// tokenSearchQuery must be specified so that this exactly matches the initial query.
filter: { listed: false, tokenSearchQuery: '' },
first: ASSET_PAGE_SIZE,
}
export function useLoadAssetsQuery(address?: string) {
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery)
useEffect(() => {
if (address) {
loadQuery({ ...defaultAssetFetcherParams, address })
}
}, [address, loadQuery])
}
export function useNftAssets(params: AssetFetcherParams) {
const variables = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params])
export function useLazyLoadAssetsQuery(params: AssetFetcherParams) {
const vars = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params])
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 fetchPolicy = 'store-or-network'
const queryData = useLazyLoadQuery<AssetQuery>(assetQuery, vars, { fetchKey, fetchPolicy }) // this will suspend if not yet loaded
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<AssetPaginationQuery, any>(
assetPaginationQuery,
queryData
const { data, loading, fetchMore } = useAssetQuery({
variables,
})
const hasNext = data?.nftAssets?.pageInfo?.hasNextPage
const loadMore = useCallback(
() =>
fetchMore({
variables: {
after: data?.nftAssets?.pageInfo?.endCursor,
},
}),
[data, fetchMore]
)
// Poll for updates.
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)
// TODO: setup polling while handling pagination
// 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) => {
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount)
data?.nftAssets?.edges?.map((queryAsset) => {
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
@ -252,7 +236,7 @@ export interface SweepFetcherParams {
traits?: Trait[]
}
function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQuery$variables {
function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQueryVariables {
const filter: NftAssetsFilterInput = useMemo(
() => ({
listed: true,
@ -272,7 +256,7 @@ function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepF
return useMemo(
() => ({
address: contractAddress,
orderBy: 'PRICE',
orderBy: NftAssetSortableField.Price,
asc: true,
first: DEFAULT_SWEEP_AMOUNT,
filter,
@ -281,28 +265,19 @@ function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepF
)
}
export function useLoadSweepAssetsQuery(params: SweepFetcherParams, enabled = true) {
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery)
const vars = useSweepFetcherVars(params)
useEffect(() => {
if (enabled) {
loadQuery(vars)
}
}, [loadQuery, enabled, vars])
}
// 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[]>(
export function useSweepNftAssets(params: SweepFetcherParams) {
const variables = useSweepFetcherVars(params)
const { data, loading } = useAssetQuery({
variables,
// This prevents overwriting the page's call to assets for cards shown
fetchPolicy: 'no-cache',
})
const assets = useMemo<GenieAsset[] | undefined>(
() =>
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => {
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount)
data?.nftAssets?.edges?.map((queryAsset) => {
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 { useEffect } from 'react'
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
import { useMemo } from 'react'
import { CollectionQuery } from './__generated__/CollectionQuery.graphql'
import { NftCollection, useCollectionQuery } from '../__generated__/types-and-hooks'
const collectionQuery = graphql`
query CollectionQuery($addresses: [String!]!) {
gql`
query Collection($addresses: [String!]!) {
nftCollections(filter: { addresses: $addresses }) {
edges {
cursor
@ -87,28 +86,23 @@ const collectionQuery = graphql`
}
`
export function useLoadCollectionQuery(address?: string | string[]): void {
const [, loadQuery] = useQueryLoader(collectionQuery)
useEffect(() => {
if (address) {
loadQuery({ addresses: Array.isArray(address) ? address : [address] })
}
}, [address, loadQuery])
interface useCollectionReturnProps {
data: GenieCollection
loading: boolean
}
// Lazy-loads an already loaded CollectionQuery.
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to
// prevent waterfalling. Use useLoadCollectionQuery to trigger the query.
export function useCollectionQuery(address: string): GenieCollection {
const queryData = useLazyLoadQuery<CollectionQuery>( // this will suspend if not yet loaded
collectionQuery,
{ addresses: [address] },
{ fetchPolicy: 'store-or-network' }
)
export function useCollection(address: string): useCollectionReturnProps {
const { data: queryData, loading } = useCollectionQuery({
variables: {
addresses: address,
},
})
const queryCollection = queryData.nftCollections?.edges[0]?.node
const market = queryCollection?.markets && queryCollection?.markets[0]
const traits = {} as Record<string, Trait[]>
const queryCollection = queryData?.nftCollections?.edges?.[0]?.node as NonNullable<NftCollection>
const market = queryCollection?.markets?.[0]
const traits = useMemo(() => {
return {} as Record<string, Trait[]>
}, [])
if (queryCollection?.traits) {
queryCollection?.traits.forEach((trait) => {
if (trait.name && trait.stats) {
@ -122,42 +116,43 @@ export function useCollectionQuery(address: string): GenieCollection {
}
})
}
return useMemo(() => {
return {
data: {
address,
isVerified: queryCollection?.isVerified ?? undefined,
name: queryCollection?.name ?? undefined,
description: queryCollection?.description ?? undefined,
standard: queryCollection?.nftContracts ? queryCollection?.nftContracts[0]?.standard ?? undefined : undefined,
bannerImageUrl: queryCollection?.bannerImage?.url ?? undefined,
stats: queryCollection?.markets
? {
num_owners: market?.owners ?? undefined,
floor_price: market?.floorPrice?.value ?? undefined,
one_day_volume: market?.volume?.value ?? undefined,
one_day_change: market?.volumePercentChange?.value ?? undefined,
one_day_floor_change: market?.floorPricePercentChange?.value ?? undefined,
banner_image_url: queryCollection?.bannerImage?.url ?? undefined,
total_supply: queryCollection?.numAssets ?? undefined,
total_listings: market?.listings?.value ?? undefined,
total_volume: market?.totalVolume?.value ?? undefined,
}
: {},
isVerified: queryCollection?.isVerified,
name: queryCollection?.name,
description: queryCollection?.description,
standard: queryCollection?.nftContracts?.[0]?.standard,
bannerImageUrl: queryCollection?.bannerImage?.url,
stats: {
num_owners: market?.owners,
floor_price: market?.floorPrice?.value,
one_day_volume: market?.volume?.value,
one_day_change: market?.volumePercentChange?.value,
one_day_floor_change: market?.floorPricePercentChange?.value,
banner_image_url: queryCollection?.bannerImage?.url,
total_supply: queryCollection?.numAssets,
total_listings: market?.listings?.value,
total_volume: market?.totalVolume?.value,
},
traits,
marketplaceCount: queryCollection?.markets
? market?.marketplaces?.map((market) => {
marketplaceCount: market?.marketplaces?.map((market) => {
return {
marketplace: market.marketplace?.toLowerCase() ?? '',
count: market.listings ?? 0,
floorPrice: market.floorPrice ?? 0,
}
})
: undefined,
}),
imageUrl: queryCollection?.image?.url ?? '',
twitterUrl: queryCollection?.twitterName ?? '',
instagram: queryCollection?.instagramName ?? undefined,
discordUrl: queryCollection?.discordUrl ?? undefined,
externalUrl: queryCollection?.homepageUrl ?? undefined,
twitterUrl: queryCollection?.twitterName,
instagram: queryCollection?.instagramName,
discordUrl: queryCollection?.discordUrl,
externalUrl: queryCollection?.homepageUrl,
rarityVerified: false, // TODO update when backend supports
// isFoundation: boolean, // TODO ask backend to add
},
loading,
}
}, [address, loading, market, queryCollection, traits])
}

@ -1,13 +1,12 @@
import { parseEther } from '@ethersproject/units'
import graphql from 'babel-plugin-relay/macro'
import { CollectionInfoForAsset, GenieAsset, SellOrder, TokenType } from 'nft/types'
import { useEffect } from 'react'
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
import gql from 'graphql-tag'
import { CollectionInfoForAsset, GenieAsset, Markets, SellOrder } from 'nft/types'
import { useMemo } from 'react'
import { DetailsQuery } from './__generated__/DetailsQuery.graphql'
import { NftAsset, useDetailsQuery } from '../__generated__/types-and-hooks'
const detailsQuery = graphql`
query DetailsQuery($address: String!, $tokenId: String!) {
gql`
query Details($address: String!, $tokenId: String!) {
nftAssets(address: $address, filter: { listed: false, tokenIds: [$tokenId] }) {
edges {
node {
@ -92,49 +91,42 @@ const detailsQuery = graphql`
}
`
export function useLoadDetailsQuery(address?: string, tokenId?: string): void {
const [, loadQuery] = useQueryLoader(detailsQuery)
useEffect(() => {
if (address && tokenId) {
loadQuery({ address, tokenId })
}
}, [address, tokenId, loadQuery])
}
export function useDetailsQuery(address: string, tokenId: string): [GenieAsset, CollectionInfoForAsset] | undefined {
const queryData = useLazyLoadQuery<DetailsQuery>(
detailsQuery,
{
export function useNftAssetDetails(
address: string,
tokenId: string
): { data: [GenieAsset, CollectionInfoForAsset]; loading: boolean } {
const { data: queryData, loading } = useDetailsQuery({
variables: {
address,
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 listing = asset?.listings?.edges[0]?.node
const ethPrice = parseEther(listing?.price?.value?.toString() ?? '0').toString()
return [
return useMemo(
() => ({
data: [
{
id: asset?.id,
address,
notForSale: asset?.listings === null,
collectionName: asset?.collection?.name ?? undefined,
collectionSymbol: asset?.collection?.image?.url ?? undefined,
imageUrl: asset?.image?.url ?? undefined,
animationUrl: asset?.animationUrl ?? undefined,
// todo: fix the back/frontend discrepency here and drop the any
marketplace: listing?.marketplace.toLowerCase() as any,
name: asset?.name ?? undefined,
collectionName: asset?.collection?.name,
collectionSymbol: asset?.collection?.image?.url,
imageUrl: asset?.image?.url,
animationUrl: asset?.animationUrl,
marketplace: listing?.marketplace.toLowerCase() as unknown as Markets,
name: asset?.name,
priceInfo: {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
},
susFlag: asset?.suspiciousFlag ?? undefined,
susFlag: asset?.suspiciousFlag,
sellorders: asset?.listings?.edges.map((listingNode) => {
return {
...listingNode.node,
@ -143,23 +135,21 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
: undefined,
} as SellOrder
}),
smallImageUrl: asset?.smallImage?.url ?? undefined,
smallImageUrl: asset?.smallImage?.url,
tokenId,
tokenType: (asset?.collection?.nftContracts && asset?.collection.nftContracts[0]?.standard) as TokenType,
collectionIsVerified: asset?.collection?.isVerified ?? undefined,
tokenType: asset?.collection?.nftContracts?.[0]?.standard,
collectionIsVerified: asset?.collection?.isVerified,
rarity: {
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
providers: asset?.rarities
? asset?.rarities?.map((rarity) => {
providers: asset?.rarities?.map((rarity) => {
return {
rank: rarity.rank ?? undefined,
score: rarity.score ?? undefined,
rank: rarity.rank,
score: rarity.score,
provider: 'Rarity Sniper',
}
})
: undefined,
}),
},
owner: { address: asset?.ownerAddress ?? '' },
ownerAddress: asset?.ownerAddress,
creator: {
profile_img_url: asset?.creator?.profileImage?.url ?? '',
address: asset?.creator?.address ?? '',
@ -170,14 +160,18 @@ export function useDetailsQuery(address: string, tokenId: string): [GenieAsset,
}),
},
{
collectionDescription: collection?.description ?? undefined,
collectionImageUrl: collection?.image?.url ?? undefined,
collectionName: collection?.name ?? undefined,
isVerified: collection?.isVerified ?? undefined,
totalSupply: collection?.numAssets ?? undefined,
twitterUrl: collection?.twitterName ?? undefined,
discordUrl: collection?.discordUrl ?? undefined,
externalUrl: collection?.homepageUrl ?? undefined,
collectionDescription: collection?.description,
collectionImageUrl: collection?.image?.url,
collectionName: collection?.name,
isVerified: collection?.isVerified,
totalSupply: collection?.numAssets,
twitterUrl: collection?.twitterName,
discordUrl: collection?.discordUrl,
externalUrl: collection?.homepageUrl,
},
]
],
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 { DEFAULT_WALLET_ASSET_QUERY_AMOUNT } from 'nft/components/profile/view/ProfilePage'
import { WalletAsset } from 'nft/types'
import gql from 'graphql-tag'
import { GenieCollection, WalletAsset } from 'nft/types'
import { wrapScientificNotation } from 'nft/utils'
import { useEffect } from 'react'
import { useLazyLoadQuery, usePaginationFragment, useQueryLoader } from 'react-relay'
import { useCallback, useMemo } from 'react'
import { NftBalancePaginationQuery } from './__generated__/NftBalancePaginationQuery.graphql'
import { NftBalanceQuery } from './__generated__/NftBalanceQuery.graphql'
import { NftBalanceQuery_nftBalances$data } from './__generated__/NftBalanceQuery_nftBalances.graphql'
import { NftAsset, useNftBalanceQuery } from '../__generated__/types-and-hooks'
const nftBalancePaginationQuery = graphql`
fragment NftBalanceQuery_nftBalances on Query @refetchable(queryName: "NftBalancePaginationQuery") {
gql`
query NftBalance(
$ownerAddress: String!
$filter: NftBalancesFilterInput
$first: Int
$after: String
$last: Int
$before: String
) {
nftBalances(
ownerAddress: $ownerAddress
filter: $filter
@ -19,7 +22,7 @@ const nftBalancePaginationQuery = graphql`
after: $after
last: $last
before: $before
) @connection(key: "NftBalanceQuery_nftBalances") {
) {
edges {
node {
ownedAsset {
@ -99,43 +102,7 @@ const nftBalancePaginationQuery = graphql`
}
`
const nftBalanceQuery = graphql`
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(
export function useNftBalance(
ownerAddress: string,
collectionFilters?: string[],
assetsFilter?: { address: string; tokenId: string }[],
@ -144,9 +111,8 @@ export function useNftBalanceQuery(
last?: number,
before?: string
) {
const queryData = useLazyLoadQuery<NftBalanceQuery>(
nftBalanceQuery,
{
const { data, loading, fetchMore } = useNftBalanceQuery({
variables: {
ownerAddress,
filter:
assetsFilter && assetsFilter.length > 0
@ -161,14 +127,21 @@ export function useNftBalanceQuery(
last,
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,
queryData
)
const walletAssets: WalletAsset[] = data.nftBalances?.edges?.map((queryAsset: NftBalanceQueryAsset) => {
const asset = queryAsset.node.ownedAsset
const walletAssets: WalletAsset[] | undefined = data?.nftBalances?.edges?.map((queryAsset) => {
const asset = queryAsset?.node.ownedAsset as NonNullable<NftAsset>
const ethPrice = parseEther(wrapScientificNotation(asset?.listings?.edges[0]?.node.price.value ?? 0)).toString()
return {
id: asset?.id,
@ -177,35 +150,32 @@ export function useNftBalanceQuery(
notForSale: asset?.listings?.edges?.length === 0,
animationUrl: asset?.animationUrl,
susFlag: asset?.suspiciousFlag,
priceInfo: asset?.listings
? {
priceInfo: {
ETHPrice: ethPrice,
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: ethPrice,
}
: undefined,
},
name: asset?.name,
tokenId: asset?.tokenId,
asset_contract: {
address: asset?.collection?.nftContracts?.[0]?.address,
schema_name: asset?.collection?.nftContracts?.[0]?.standard,
tokenType: asset?.collection?.nftContracts?.[0]?.standard,
name: asset?.collection?.name,
description: asset?.description,
image_url: asset?.collection?.image?.url,
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,
lastPrice: queryAsset.node.lastPrice?.value,
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,
basisPoints: queryAsset?.node?.listingFees?.[0]?.basisPoints ?? 0 / 10000,
listing_date: asset?.listings?.edges?.[0]?.node?.createdAt,
date_acquired: queryAsset.node.lastPrice?.timestamp,
listing_date: asset?.listings?.edges?.[0]?.node?.createdAt?.toString(),
date_acquired: queryAsset.node.lastPrice?.timestamp?.toString(),
sellOrders: asset?.listings?.edges.map((edge: any) => edge.node),
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 { 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 {
HOUR,
@ -15,15 +15,15 @@ export enum TimePeriod {
export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
switch (timePeriod) {
case TimePeriod.HOUR:
return 'HOUR'
return HistoryDuration.Hour
case TimePeriod.DAY:
return 'DAY'
return HistoryDuration.Day
case TimePeriod.WEEK:
return 'WEEK'
return HistoryDuration.Week
case TimePeriod.MONTH:
return 'MONTH'
return HistoryDuration.Month
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 } = {
[SupportedChainId.MAINNET]: 'ETHEREUM',
[SupportedChainId.GOERLI]: 'ETHEREUM_GOERLI',
[SupportedChainId.POLYGON]: 'POLYGON',
[SupportedChainId.POLYGON_MUMBAI]: 'POLYGON',
[SupportedChainId.CELO]: 'CELO',
[SupportedChainId.CELO_ALFAJORES]: 'CELO',
[SupportedChainId.ARBITRUM_ONE]: 'ARBITRUM',
[SupportedChainId.ARBITRUM_RINKEBY]: 'ARBITRUM',
[SupportedChainId.OPTIMISM]: 'OPTIMISM',
[SupportedChainId.OPTIMISM_GOERLI]: 'OPTIMISM',
[SupportedChainId.MAINNET]: Chain.Ethereum,
[SupportedChainId.GOERLI]: Chain.EthereumGoerli,
[SupportedChainId.POLYGON]: Chain.Polygon,
[SupportedChainId.POLYGON_MUMBAI]: Chain.Polygon,
[SupportedChainId.CELO]: Chain.Celo,
[SupportedChainId.CELO_ALFAJORES]: Chain.Celo,
[SupportedChainId.ARBITRUM_ONE]: Chain.Arbitrum,
[SupportedChainId.ARBITRUM_RINKEBY]: Chain.Arbitrum,
[SupportedChainId.OPTIMISM]: Chain.Optimism,
[SupportedChainId.OPTIMISM_GOERLI]: Chain.Optimism,
}
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 } = {
ethereum: 'ETHEREUM',
polygon: 'POLYGON',
celo: 'CELO',
arbitrum: 'ARBITRUM',
optimism: 'OPTIMISM',
ethereum: Chain.Ethereum,
polygon: Chain.Polygon,
celo: Chain.Celo,
arbitrum: Chain.Arbitrum,
optimism: Chain.Optimism,
}
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 } = {
@ -72,7 +72,7 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
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 {
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
const address = token.address.toLowerCase()

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

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

@ -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
error: unknown
ticks: readonly TickData[] | undefined
ticks: TickData[] | undefined
} {
const useSubgraph = currencyA ? !CHAIN_IDS_MISSING_SUBGRAPH_DATA.includes(currencyA.chainId) : true

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

@ -71,7 +71,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
for (const listing of asset.newListings) {
if (!listing.price) listingsMissingPrice.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])
else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing])

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

@ -58,14 +58,15 @@ export async function approveCollectionRow(
: marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT
: looksRareAddress
await approveCollection(spender ?? '', collectionAddress, signer, (newStatus: ListingStatus) =>
!!collectionAddress &&
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
updateStatus({
listing: collectionRow,
newStatus,
rows: collectionsRequiringApproval,
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
})
)
))
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
const maxFee =
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

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

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

@ -5,13 +5,8 @@ import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { OpacityHoverState } from 'components/Common'
import { parseEther } from 'ethers/lib/utils'
import { NftAssetTraitInput, NftMarketplace } from 'graphql/data/nft/__generated__/AssetQuery.graphql'
import {
ASSET_PAGE_SIZE,
AssetFetcherParams,
useLazyLoadAssetsQuery,
useLoadSweepAssetsQuery,
} from 'graphql/data/nft/Asset'
import { NftAssetTraitInput, NftMarketplace, NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { ASSET_PAGE_SIZE, AssetFetcherParams, useNftAssets } from 'graphql/data/nft/Asset'
import useDebounce from 'hooks/useDebounce'
import { useScreenSize } from 'hooks/useScreenSize'
import { AnimatedBox, Box } from 'nft/components/Box'
@ -41,7 +36,6 @@ import {
GenieCollection,
isPooledMarket,
Markets,
TokenType,
UniformAspectRatio,
UniformAspectRatios,
} from 'nft/types'
@ -63,7 +57,7 @@ import { ThemedText } from 'theme'
import { CollectionAssetLoading } from './CollectionAssetLoading'
import { MARKETPLACE_ITEMS, MarketplaceLogo } from './MarketplaceSelect'
import { Sweep, useSweepFetcherParams } from './Sweep'
import { Sweep } from './Sweep'
import { TraitChip } from './TraitChip'
interface CollectionNftsProps {
@ -282,15 +276,6 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const [renderedHeight, setRenderedHeight] = useState<number | undefined>()
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 = {
address: contractAddress,
@ -312,8 +297,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
first: ASSET_PAGE_SIZE,
}
const { assets: collectionNfts, loadNext, hasNext, isLoadingNext } = useLazyLoadAssetsQuery(assetQueryParams)
const handleNextPageLoad = useCallback(() => loadNext(ASSET_PAGE_SIZE), [loadNext])
const { data: collectionNfts, loading, hasNext, loadMore } = useNftAssets(assetQueryParams)
const getPoolPosition = useCallback(
(asset: GenieAsset) => {
@ -394,8 +378,8 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
const screenSize = useScreenSize()
useEffect(() => {
setIsCollectionNftsLoading(isLoadingNext)
}, [isLoadingNext, setIsCollectionNftsLoading])
setIsCollectionNftsLoading(loading)
}, [loading, setIsCollectionNftsLoading])
const hasRarity = useMemo(() => {
const hasRarity = getRarityStatus(rarityStatusCache, collectionStats?.address, collectionAssets) ?? false
@ -434,7 +418,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
}, [collectionAssets, isMobile, currentTokenPlayingMedia, rarityVerified, uniformAspectRatio, renderedHeight])
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(() => {
if (debouncedMinPrice && debouncedMaxPrice) {
@ -619,17 +603,18 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
</InfiniteScrollWrapper>
</AnimatedBox>
<InfiniteScrollWrapper>
{loading ? (
<CollectionNftsLoading height={renderedHeight} />
) : (
<InfiniteScroll
next={handleNextPageLoad}
hasMore={hasNext}
loader={Boolean(hasNext && hasNfts) && <LoadingAssets height={renderedHeight} />}
next={loadMore}
hasMore={hasNext ?? false}
loader={Boolean(hasNext && hasNfts) && <LoadingAssets />}
dataLength={collectionAssets?.length ?? 0}
style={{ overflow: 'unset' }}
className={hasNfts || isLoadingNext ? styles.assetList : undefined}
className={hasNfts ? styles.assetList : undefined}
>
{hasNfts ? (
assets
) : collectionAssets?.length === 0 ? (
{!hasNfts ? (
<Center width="full" color="textSecondary" textAlign="center" style={{ height: '60vh' }}>
<EmptyCollectionWrapper>
<p className={headlineMedium}>No NFTS found</p>
@ -645,9 +630,10 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
</EmptyCollectionWrapper>
</Center>
) : (
<CollectionNftsLoading height={renderedHeight} />
assets
)}
</InfiniteScroll>
)}
</InfiniteScrollWrapper>
</>
)

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

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

@ -3,7 +3,7 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
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 { useBag, useProfilePageState, useSellAsset } from 'nft/hooks'
import { CollectionInfoForAsset, GenieAsset, ProfilePageStateType, WalletAsset } from 'nft/types'
@ -218,10 +218,10 @@ const OwnerContainer = ({ asset }: { asset: WalletAsset }) => {
const resetSellAssets = useSellAsset((state) => state.reset)
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(
() => (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]
)
const trace = useTrace()
@ -254,7 +254,7 @@ const OwnerContainer = ({ asset }: { asset: WalletAsset }) => {
{listing ? (
<>
<ThemedText.MediumHeader fontSize="28px" lineHeight="36px">
{formatEthPrice(asset.priceInfo.ETHPrice)} ETH
{formatEthPrice(asset.priceInfo?.ETHPrice)} ETH
</ThemedText.MediumHeader>
{USDPrice && (
<ThemedText.BodySecondary lineHeight="24px">
@ -320,7 +320,7 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const { account } = useWeb3React()
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 addAssetsToBag = useBag((s) => s.addAssetsToBag)
@ -331,11 +331,8 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
const USDPrice = useUsdPrice(asset)
const assetsFilter = [{ address: asset.address, tokenId: asset.tokenId }]
const { walletAssets: ownerAssets } = useNftBalanceQuery(account ?? '', [], assetsFilter, 1)
const walletAsset: WalletAsset | undefined = useMemo(
() => (ownerAssets?.length > 0 ? ownerAssets[0] : undefined),
[ownerAssets]
)
const { walletAssets: ownerAssets } = useNftBalance(account ?? '', [], assetsFilter, 1)
const walletAsset: WalletAsset | undefined = useMemo(() => ownerAssets?.[0], [ownerAssets])
const { assetInBag } = useMemo(() => {
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
return (
@ -424,20 +421,20 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps)
)}
{isForSale && (
<OwnerInformationContainer>
{asset.tokenType !== 'ERC1155' && asset.owner.address && (
{asset.tokenType !== 'ERC1155' && asset.ownerAddress && (
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
Seller:
</ThemedText.BodySmall>
)}
<OwnerText
target="_blank"
href={`https://etherscan.io/address/${asset.owner.address}`}
href={`https://etherscan.io/address/${asset.ownerAddress}`}
rel="noopener noreferrer"
>
{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>
</OwnerInformationContainer>

@ -1,8 +1,7 @@
import { useLoadCollectionQuery } from 'graphql/data/nft/Collection'
import { fetchTrendingCollections } from 'nft/queries'
import { TimePeriod } from 'nft/types'
import { calculateCardIndex } from 'nft/utils'
import { Suspense, useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useQuery } from 'react-query'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
@ -137,10 +136,6 @@ const Banner = () => {
[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 onToggleNextSlide = useCallback(
(direction: number) => {
@ -169,13 +164,11 @@ const Banner = () => {
{collections ? (
<Carousel activeIndex={activeCollectionIdx} toggleNextSlide={onToggleNextSlide}>
{collections.map((collection) => (
<Suspense fallback={<LoadingCarouselCard collection={collection} />} key={collection.address}>
<CarouselCard
key={collection.address}
collection={collection}
onClick={() => navigate(`/nfts/collection/${collection.address}`)}
/>
</Suspense>
))}
</Carousel>
) : (

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

@ -152,7 +152,7 @@ const PriceTextInput = ({
inputRef.current.value = listPrice !== undefined ? `${listPrice}` : ''
setWarningType(WarningType.NONE)
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)
setWarningType(WarningType.ALREADY_LISTED)
} else if (warning && listPrice && listPrice >= 0) removeMarketplaceWarning(asset, warning)
@ -226,10 +226,14 @@ const PriceTextInput = ({
>
{focused ? (
<>
{!!asset.lastPrice && (
<Row display={asset.lastPrice ? 'flex' : 'none'} marginRight="8">
LAST: {formatEth(asset.lastPrice)} ETH
</Row>
)}
{!!asset.floorPrice && (
<Row display={asset.floorPrice ? 'flex' : 'none'}>FLOOR: {formatEth(asset.floorPrice)} ETH</Row>
)}
</>
) : (
<>
@ -239,8 +243,8 @@ const PriceTextInput = ({
<>
{warningType}
{warningType === WarningType.BELOW_FLOOR
? formatEth(asset.floorPrice)
: formatEth(asset.floor_sell_order_price)}
? formatEth(asset?.floorPrice ?? 0)
: formatEth(asset?.floor_sell_order_price ?? 0)}
ETH
<Box
color={warningType === WarningType.BELOW_FLOOR ? 'accentAction' : 'orange'}
@ -335,7 +339,7 @@ const MarketplaceRow = ({
const royalties =
(selectedMarkets.length === 1 && selectedMarkets[0].name === 'LooksRare'
? LOOKS_RARE_CREATOR_BASIS_POINTS
: asset.basisPoints) * 0.01
: asset?.basisPoints ?? 0) * 0.01
const feeInEth = price && (price * (royalties + marketplaceFee)) / 100
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 { ClearAllButton, LoadingAssets } from 'nft/components/collection/CollectionNfts'
import { assetList } from 'nft/components/collection/CollectionNfts.css'
@ -198,10 +198,10 @@ const ProfilePageNfts = ({
const {
walletAssets: ownerAssets,
loadNext,
loading,
hasNext,
isLoadingNext,
} = useNftBalanceQuery(address, collectionFilters, [], DEFAULT_WALLET_ASSET_QUERY_AMOUNT)
loadMore,
} = useNftBalance(address, collectionFilters, [], DEFAULT_WALLET_ASSET_QUERY_AMOUNT)
const { gridX } = useSpring({
gridX: isFiltersExpanded ? FILTER_SIDEBAR_WIDTH : -PADDING,
@ -211,6 +211,8 @@ const ProfilePageNfts = ({
},
})
if (loading) return <ProfileBodyLoadingSkeleton />
return (
<Column width="full">
{ownerAssets?.length === 0 ? (
@ -242,13 +244,13 @@ const ProfilePageNfts = ({
/>
</Row>
<InfiniteScroll
next={() => loadNext(DEFAULT_WALLET_ASSET_QUERY_AMOUNT)}
hasMore={hasNext}
next={loadMore}
hasMore={hasNext ?? false}
loader={
Boolean(hasNext && ownerAssets?.length) && <LoadingAssets count={DEFAULT_WALLET_ASSET_QUERY_AMOUNT} />
}
dataLength={ownerAssets?.length ?? 0}
className={ownerAssets?.length || isLoadingNext ? assetList : undefined}
className={ownerAssets?.length ? assetList : undefined}
style={{ overflow: 'unset' }}
>
{ownerAssets?.length

@ -4,13 +4,14 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { EventName } from '@uniswap/analytics-events'
import { MouseoverTooltip } from 'components/Tooltip'
import Tooltip from 'components/Tooltip'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { Box } from 'nft/components/Box'
import * as Card from 'nft/components/collection/Card'
import { AssetMediaType } from 'nft/components/collection/Card'
import { bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
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'
const TOOLTIP_TIMEOUT = 2000
@ -39,7 +40,7 @@ const getNftDisplayComponent = (
const getUnsupportedNftTextComponent = (asset: WalletAsset) => (
<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>Blocked from trading</Trans>
@ -109,7 +110,7 @@ export const ViewMyNftsAsset = ({
}, [isSelected, isSelectedRef])
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 (
<Card.Container

@ -1,5 +1,6 @@
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 create from 'zustand'
import { devtools } from 'zustand/middleware'
@ -88,7 +89,7 @@ export const useBag = create<BagState>()(
const itemsInBagCopy = [...itemsInBag]
assets.forEach((asset) => {
let index = -1
if (asset.tokenType !== TokenType.ERC1155) {
if (asset.tokenType !== NftStandard.Erc1155) {
index = itemsInBag.findIndex(
(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 { devtools } from 'zustand/middleware'

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

@ -1,13 +1,9 @@
import { Trace } from '@uniswap/analytics'
import { PageName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { useDetailsQuery, useLoadDetailsQuery } from 'graphql/data/nft/Details'
import { useLoadNftBalanceQuery } from 'graphql/data/nft/NftBalance'
import { useNftAssetDetails } from 'graphql/data/nft/Details'
import { AssetDetails } from 'nft/components/details/AssetDetails'
import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading'
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 styled from 'styled-components/macro'
@ -38,11 +34,13 @@ const AssetPriceDetailsContainer = styled.div`
}
`
const Asset = () => {
const AssetPage = () => {
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 (
<>
@ -51,35 +49,17 @@ const Asset = () => {
properties={{ collection_address: contractAddress, token_id: tokenId }}
shouldLogImpression
>
{asset && collection ? (
{!!asset && !!collection && (
<AssetContainer>
<AssetDetails collection={collection} asset={asset} />
<AssetPriceDetailsContainer>
<AssetPriceDetails collection={collection} asset={asset} />
</AssetPriceDetailsContainer>
</AssetContainer>
) : null}
)}
</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

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

@ -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 ({
toSell,
@ -41,7 +42,7 @@ type RouteItem = {
decimals: number
address: string
priceInfo: ApiPriceInfo
tokenType: TokenType
tokenType?: NftStandard
tokenId: string
amount: number
marketplace?: string

@ -1,3 +1,4 @@
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SortBy } from 'nft/hooks'
import { SellOrder } from '../sell'
@ -81,7 +82,7 @@ export interface Trait {
export interface GenieAsset {
id?: string // This would be a random id created and assigned by front end
address: string
notForSale: boolean
notForSale?: boolean
collectionName?: string
collectionSymbol?: string
imageUrl?: string
@ -93,17 +94,15 @@ export interface GenieAsset {
sellorders?: SellOrder[]
smallImageUrl?: string
tokenId: string
tokenType: TokenType
tokenType?: NftStandard
totalCount?: number // The totalCount from the query to /assets
collectionIsVerified?: boolean
rarity?: Rarity
owner: {
address: string
}
metadataUrl: string
ownerAddress?: string
metadataUrl?: string
creator: {
address: string
profile_img_url: string
address?: string
profile_img_url?: string
}
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 {
name: string
@ -15,20 +15,20 @@ export interface ListingWarning {
export interface SellOrder {
address: string
createdAt: number
endAt: number
endAt?: number
id: string
maker: string
marketplace: NftMarketplace
marketplaceUrl: string
orderHash: string
orderHash?: string
price: {
currency: string
currency?: string
value: number
}
quantity: number
startAt: number
status: OrderStatus
tokenId: string
tokenId?: string
type: OrderType
protocolParameters: Record<string, unknown>
}
@ -41,32 +41,31 @@ export interface Listing {
export interface WalletAsset {
id?: string
imageUrl: string
smallImageUrl: string
imageUrl?: string
smallImageUrl?: string
notForSale: boolean
animationUrl: string
susFlag: boolean
priceInfo: PriceInfo
name: string
tokenId: string
animationUrl?: string
susFlag?: boolean
priceInfo?: PriceInfo
name?: string
tokenId?: string
asset_contract: {
address: string
schema_name: 'ERC1155' | 'ERC721' | string
name: string
description: string
image_url: string
payout_address: string
tokenType: TokenType
address?: string
name?: string
description?: string
image_url?: string
payout_address?: string
tokenType?: NftStandard
}
collection: GenieCollection
collectionIsVerified: boolean
lastPrice: number
floorPrice: number
basisPoints: number
listing_date: string
date_acquired: string
sellOrders: SellOrder[]
floor_sell_order_price: number
collection?: GenieCollection
collectionIsVerified?: boolean
lastPrice?: number
floorPrice?: number
basisPoints?: number
listing_date?: string
date_acquired?: string
sellOrders?: SellOrder[]
floor_sell_order_price?: number
// Used for creating new listings
expirationTime?: number
marketAgnosticPrice?: number
@ -95,8 +94,8 @@ export enum ListingStatus {
}
export interface AssetRow {
images: string[]
name: string
images: (string | undefined)[]
name?: string
status: ListingStatus
callback?: () => Promise<void>
}
@ -108,7 +107,7 @@ export interface ListingRow extends AssetRow {
}
export interface CollectionRow extends AssetRow {
collectionAddress: string
collectionAddress?: string
marketplace: ListingMarket
}

@ -62,7 +62,7 @@ const getConsiderationItems = (
creatorFee?: ConsiderationInputItem
} => {
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 openseaFee = price.mul(BigNumber.from(openSeaBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
@ -76,7 +76,9 @@ const getConsiderationItems = (
sellerFee: createConsiderationItem(sellerFee, signerAddress),
openseaFee: createConsiderationItem(openseaFee, OPENSEA_FEE_ADDRESS),
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 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) {
case 'OpenSea':
try {

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

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

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

@ -25,7 +25,3 @@ declare module 'multihashes' {
declare function decode(buff: Uint8Array): { code: number; name: string; length: number; digest: Uint8Array }
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