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:
parent
a286e5b114
commit
2aa1b18d14
@ -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
1
.gitignore
vendored
@ -9,7 +9,6 @@
|
||||
/src/locales/**/pseudo.po
|
||||
|
||||
# generated graphql types
|
||||
__generated__/
|
||||
schema.graphql
|
||||
|
||||
# dependencies
|
||||
|
23
apollo-codegen.ts
Normal file
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
|
23
apollo-codegen_thegraph.ts
Normal file
23
apollo-codegen_thegraph.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/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,
|
19
package.json
19
package.json
@ -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)
|
||||
},
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}, [chain, chainId, duration, environment])
|
||||
const { data: sparklineQuery } = useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
|
||||
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])
|
||||
}
|
||||
|
1595
src/graphql/data/__generated__/types-and-hooks.ts
generated
Normal file
1595
src/graphql/data/__generated__/types-and-hooks.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
src/graphql/data/apollo.ts
Normal file
30
src/graphql/data/apollo.ts
Normal file
@ -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
|
||||
? {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
}
|
||||
: undefined,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
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 {
|
||||
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,
|
||||
}
|
||||
: {},
|
||||
traits,
|
||||
marketplaceCount: queryCollection?.markets
|
||||
? market?.marketplaces?.map((market) => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
data: {
|
||||
address,
|
||||
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: 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,
|
||||
rarityVerified: false, // TODO update when backend supports
|
||||
// isFoundation: boolean, // TODO ask backend to add
|
||||
}
|
||||
}),
|
||||
imageUrl: queryCollection?.image?.url ?? '',
|
||||
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,92 +91,87 @@ 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 [
|
||||
{
|
||||
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,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
susFlag: asset?.suspiciousFlag ?? undefined,
|
||||
sellorders: asset?.listings?.edges.map((listingNode) => {
|
||||
return {
|
||||
...listingNode.node,
|
||||
protocolParameters: listingNode.node.protocolParameters
|
||||
? JSON.parse(listingNode.node.protocolParameters.toString())
|
||||
: undefined,
|
||||
} as SellOrder
|
||||
}),
|
||||
smallImageUrl: asset?.smallImage?.url ?? undefined,
|
||||
tokenId,
|
||||
tokenType: (asset?.collection?.nftContracts && asset?.collection.nftContracts[0]?.standard) as TokenType,
|
||||
collectionIsVerified: asset?.collection?.isVerified ?? undefined,
|
||||
rarity: {
|
||||
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
|
||||
providers: asset?.rarities
|
||||
? asset?.rarities?.map((rarity) => {
|
||||
return useMemo(
|
||||
() => ({
|
||||
data: [
|
||||
{
|
||||
id: asset?.id,
|
||||
address,
|
||||
notForSale: asset?.listings === null,
|
||||
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,
|
||||
sellorders: asset?.listings?.edges.map((listingNode) => {
|
||||
return {
|
||||
...listingNode.node,
|
||||
protocolParameters: listingNode.node.protocolParameters
|
||||
? JSON.parse(listingNode.node.protocolParameters.toString())
|
||||
: undefined,
|
||||
} as SellOrder
|
||||
}),
|
||||
smallImageUrl: asset?.smallImage?.url,
|
||||
tokenId,
|
||||
tokenType: asset?.collection?.nftContracts?.[0]?.standard,
|
||||
collectionIsVerified: asset?.collection?.isVerified,
|
||||
rarity: {
|
||||
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
|
||||
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 ?? '' },
|
||||
creator: {
|
||||
profile_img_url: asset?.creator?.profileImage?.url ?? '',
|
||||
address: asset?.creator?.address ?? '',
|
||||
},
|
||||
metadataUrl: asset?.metadataUrl ?? '',
|
||||
traits: asset?.traits?.map((trait) => {
|
||||
return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' }
|
||||
}),
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
]
|
||||
}),
|
||||
},
|
||||
ownerAddress: asset?.ownerAddress,
|
||||
creator: {
|
||||
profile_img_url: asset?.creator?.profileImage?.url ?? '',
|
||||
address: asset?.creator?.address ?? '',
|
||||
},
|
||||
metadataUrl: asset?.metadataUrl ?? '',
|
||||
traits: asset?.traits?.map((trait) => {
|
||||
return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' }
|
||||
}),
|
||||
},
|
||||
{
|
||||
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
|
||||
? {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
}
|
||||
: undefined,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
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 {
|
||||
data,
|
||||
loading: isLoading,
|
||||
error,
|
||||
} = useQuery(query, {
|
||||
variables: {
|
||||
poolAddress: poolAddress?.toLowerCase(),
|
||||
skip,
|
||||
},
|
||||
pollInterval: interval,
|
||||
client: apolloClient,
|
||||
})
|
||||
|
||||
const refreshData = useCallback(() => {
|
||||
if (poolAddress && chainId) {
|
||||
fetchQuery<AllV3TicksQueryType>(environment, query, {
|
||||
poolAddress: poolAddress.toLowerCase(),
|
||||
skip,
|
||||
}).subscribe({
|
||||
next: setData,
|
||||
error: setError,
|
||||
complete: () => setIsLoading(false),
|
||||
})
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [poolAddress, skip, chainId])
|
||||
|
||||
// Trigger fetch on first load
|
||||
useEffect(refreshData, [refreshData, poolAddress, skip])
|
||||
|
||||
useInterval(refreshData, interval, true)
|
||||
return { error, isLoading, data }
|
||||
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)
|
||||
): { 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,
|
||||
})
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
}, [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()),
|
||||
})
|
4750
src/graphql/thegraph/__generated__/types-and-hooks.ts
generated
Normal file
4750
src/graphql/thegraph/__generated__/types-and-hooks.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
src/graphql/thegraph/apollo.ts
Normal file
40
src/graphql/thegraph/apollo.ts
Normal file
@ -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) =>
|
||||
updateStatus({
|
||||
listing: collectionRow,
|
||||
newStatus,
|
||||
rows: collectionsRequiringApproval,
|
||||
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
|
||||
})
|
||||
)
|
||||
!!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,35 +603,37 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
</InfiniteScrollWrapper>
|
||||
</AnimatedBox>
|
||||
<InfiniteScrollWrapper>
|
||||
<InfiniteScroll
|
||||
next={handleNextPageLoad}
|
||||
hasMore={hasNext}
|
||||
loader={Boolean(hasNext && hasNfts) && <LoadingAssets height={renderedHeight} />}
|
||||
dataLength={collectionAssets?.length ?? 0}
|
||||
style={{ overflow: 'unset' }}
|
||||
className={hasNfts || isLoadingNext ? styles.assetList : undefined}
|
||||
>
|
||||
{hasNfts ? (
|
||||
assets
|
||||
) : collectionAssets?.length === 0 ? (
|
||||
<Center width="full" color="textSecondary" textAlign="center" style={{ height: '60vh' }}>
|
||||
<EmptyCollectionWrapper>
|
||||
<p className={headlineMedium}>No NFTS found</p>
|
||||
<Box
|
||||
onClick={reset}
|
||||
type="button"
|
||||
className={clsx(bodySmall, buttonTextMedium)}
|
||||
color="accentAction"
|
||||
cursor="pointer"
|
||||
>
|
||||
<ViewFullCollection>View full collection</ViewFullCollection>
|
||||
</Box>
|
||||
</EmptyCollectionWrapper>
|
||||
</Center>
|
||||
) : (
|
||||
<CollectionNftsLoading height={renderedHeight} />
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
{loading ? (
|
||||
<CollectionNftsLoading height={renderedHeight} />
|
||||
) : (
|
||||
<InfiniteScroll
|
||||
next={loadMore}
|
||||
hasMore={hasNext ?? false}
|
||||
loader={Boolean(hasNext && hasNfts) && <LoadingAssets />}
|
||||
dataLength={collectionAssets?.length ?? 0}
|
||||
style={{ overflow: 'unset' }}
|
||||
className={hasNfts ? styles.assetList : undefined}
|
||||
>
|
||||
{!hasNfts ? (
|
||||
<Center width="full" color="textSecondary" textAlign="center" style={{ height: '60vh' }}>
|
||||
<EmptyCollectionWrapper>
|
||||
<p className={headlineMedium}>No NFTS found</p>
|
||||
<Box
|
||||
onClick={reset}
|
||||
type="button"
|
||||
className={clsx(bodySmall, buttonTextMedium)}
|
||||
color="accentAction"
|
||||
cursor="pointer"
|
||||
>
|
||||
<ViewFullCollection>View full collection</ViewFullCollection>
|
||||
</Box>
|
||||
</EmptyCollectionWrapper>
|
||||
</Center>
|
||||
) : (
|
||||
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>
|
||||
<CarouselCard
|
||||
key={collection.address}
|
||||
collection={collection}
|
||||
onClick={() => navigate(`/nfts/collection/${collection.address}`)}
|
||||
/>
|
||||
))}
|
||||
</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 ? (
|
||||
<>
|
||||
<Row display={asset.lastPrice ? 'flex' : 'none'} marginRight="8">
|
||||
LAST: {formatEth(asset.lastPrice)} ETH
|
||||
</Row>
|
||||
<Row display={asset.floorPrice ? 'flex' : 'none'}>FLOOR: {formatEth(asset.floorPrice)} ETH</Row>
|
||||
{!!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}
|
||||
/>
|
||||
</Suspense>
|
||||
<TokenDetails
|
||||
urlAddress={tokenAddress}
|
||||
chain={chain}
|
||||
tokenQuery={tokenQuery}
|
||||
tokenPriceQuery={tokenPriceQuery}
|
||||
onChangeTimePeriod={setTimePeriod}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
4
src/react-app-env.d.ts
vendored
4
src/react-app-env.d.ts
vendored
@ -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'
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user