Compare commits

..

57 Commits

Author SHA1 Message Date
cartcrom
bc92af6c15 fix: use latest version of tokenlists over cache (#5975)
fix: use constants in fetch interval callback to prevent updates
2023-02-14 15:48:53 -05:00
lynn
9a257e0ca8 fix: correct copy on TVL (#5968)
fix copy
2023-02-13 19:28:00 -05:00
cartcrom
82646b77dd feat: migrate search tokens to gql (#5802)
* init

* feat: search tokens hook

* feat: search ordering

* feat: separated FungibleToken parsing into sep function

* refactor: memoized search token sorting

* fix: cache waterfall issue

* fix: removed no longer relevant test

* feat: trending tokens from gql (#5805)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* fix: linted

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: unwrap native trending tokens

* feat: refetch recently searched (#5894)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* feat: recently searched tokens query

* fix: linted

* feat: combined query function

* feat: recently searched hooks

* feat: combined query

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: empty history loading state

* fix: revert change

* fix: revert unintended nft query change

* refactor: state functions

* fix: removed unnused query var

* fix: unwrap native trending tokens

* feat: remove fungible token type (#5896)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* feat: recently searched tokens query

* fix: linted

* feat: combined query function

* feat: recently searched hooks

* feat: combined query

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: empty history loading state

* fix: revert change

* fix: revert unintended nft query change

* refactor: state functions

* fix: removed unnused query var

* refactor: remove FungibleToken type

* refactor: use TokenStandard.Native instead of string

* refactor: improve boolean logic readability

* refactor: removed duplicate code

* fix: unwrap native trending tokens

* fix: type error

* fix: duplicate entry bug

* refactor: use undefined instead of null/string cast

* fix: update apollo types

* fix: polygon edge case & polish
2023-02-13 18:45:13 -05:00
Jordan Frankfurt
1992c5de06 fix: don't render LP position when token name or symbol includes a url (#5961) 2023-02-13 13:45:08 -06:00
Charles Bachmeier
0208ccd7d2 feat: [ListV2] Changes to Multiple Market handling and row hovering (#5953)
* add overlay to row hover

* add remove icon on hover

* working expand and collapse icons

* fixed margin

* don't show collpase on nonhovered rows

* display mutliple markets

* remove market

* 0 margin divider

* hide on mobile

* better mobile check

* margin adjustment and hover border radius

* address comments

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-09 11:58:59 -08:00
Jordan Frankfurt
12df4b3981 fix: update some urls to match current page structure (#5957) 2023-02-09 13:35:12 -06:00
Jack Short
3eaeb65b07 feat: implementing permit2 with pay with any token (#5926)
* feat: implementing permit2 with pay with any token

* permit2 hook

* usePayWithAnyTokenHook

* removing ternary operators

* weird export type bug

* resolving merge

* fixing nft test

* styles

* refactoring styles

* reformatting

* price impact warnings

* forgot trans tag

* responding to comments

* fixes

* disabling pay with any token when on the wrong chain

* missing enabled flag

* vertically centering button
2023-02-09 13:41:30 -05:00
Jordan Frankfurt
6df2f3677e fix: use general sort rank instead of only volume rank for row numbers (#5955) 2023-02-09 12:37:13 -06:00
lynn
80edf5a0d6 fix: follow custom design for token selector scrollbar (#5952)
* fix

* remove unused styled component

* update snapshot

* disable scroll on larger currency search modal

* update snapshot
2023-02-09 13:17:58 -05:00
eddie
96f6929127 fix: showCommonBases for widget token selector (#5951) 2023-02-08 16:50:21 -08:00
eddie
4ec95d0927 fix: URL params for widget (#5943)
* fix: URL params for widget

* fix: remove output token from tokenDetails callsite

* fix: combine props, rename initial state values

* fix: better prop types

* fix: rename prop type
2023-02-08 16:50:11 -08:00
yyip-dev
fba6cc9e02 fix: test pr for github<>jira integration (#5950)
* test pr for github<>jira integration

* remove diff
2023-02-08 18:13:04 -05:00
lynn
bc2f68565b fix: Web 1992 token details update blocked token detail pages (#5948)
* init

* Update src/components/Tokens/TokenDetails/index.tsx

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* oops last commit leads to error :/

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-02-08 18:03:36 -05:00
Zach Pomerantz
f232643d8e feat: upgrade conedison to improve signing (#5947)
* feat: upgrade conedison to improve signing

* fix: deduplicate conedison
2023-02-08 13:32:19 -08:00
lynn
527270e33f fix: handle wallets that don't send default 4001 error code for user rejected txns (#5941)
* init

* add more to comment

* zzmp comments

* regex

* oops

* fix
2023-02-08 13:34:21 -05:00
cartcrom
18cd5ec9d9 fix: default to null when query address is undefined (#5937) 2023-02-08 00:17:39 -05:00
Jordan Frankfurt
5ddb565805 feat: add feedback link to ... menu (#5933)
add feedback link to ... menu
2023-02-07 18:21:00 -06:00
Jack Short
f0b4b92b88 chore: setting up feature flags and boilerplate gql for routing gql endpoint (#5935)
* feat: routing gql endpoint

* adding feature flags

* adding to modal

* adding mocked provider to test

* adding apollo client to mocked providers

* comment
2023-02-07 18:33:41 -05:00
Charles Bachmeier
4d82f9fb3a fix: Handle Different Listing Failure Responses (#5940)
fix: handle different failure responses

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-07 10:26:36 -08:00
Charles Bachmeier
654b26dc54 feat: [ListV2] Enable ListV2 feature flag (#5939)
enable v2 feature flag

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-07 09:05:43 -08:00
Charles Bachmeier
c0753ae52f fix: [ListV2] Better Collection and Listing Retry and Removal Logic (#5938)
* keep approved collection status when removing

* better map name

* improve callback logic

* improve callback logic for collection approval

* reduce zustand calls

* additional zustand reductions

* add back correct check

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-07 08:29:58 -08:00
Charles Bachmeier
16bb9470ae feat: [ListV2] Changes based off Bug Bash (#5936)
* duration wrap

* better styling a different breakpoints

* throw error on listing same price

* don't show spinner on v2

* remove unused check

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 19:04:56 -08:00
Jordan Frankfurt
6dcfca24cb fix: remove roll list (#5934) 2023-02-06 20:41:01 -06:00
Charles Bachmeier
9cac9f8299 feat: [ListV2] error and warning states (#5921)
* update error message for price inputs

* add grid and button warning states

* add list below floor warning modal

* only warn for prices 20% below floor

* highlight unentered price in red on button press

* missing dependency

* updated modal name and mobile height

* add new file

* fix column presence

* rookie mistake

* bulk zustand imports

* below floor threshold

* move issue check higher

* cleanup mouseEvent

* rename color var

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 13:00:29 -08:00
Charles Bachmeier
5def0dd166 feat: [ListV2] Add retry and remove logic to listings and collection approvals (#5923)
* add remove/retry buttons

* add retry logic functionality

* add scroll to active row

* properly update rejected status

* replace loadingicon with loader

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:55:05 -08:00
Charles Bachmeier
7229637c4c feat: [ListV2] Update price and duration dropdowns (#5925)
* add custom option and style market dropdown

* working dropdown for price

* hide dropdown on mobile

* update duration dropdown

* themed opacity hover

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:54:56 -08:00
Charles Bachmeier
f26b09537d feat: [ListV2] Success modal + Twitter Share (#5924)
* added success screen with images

* add listing proceeds

* working return

* working single tweet

* working tweet multiple

* update how we parse twitterName

* add scrollbar styles

* usestablecoinvalue

* math.min

* add collection name backup to tweet

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:45:13 -08:00
Charles Bachmeier
8f922b665a feat: [ListV2] Add extra bottom padding to collection assets (#5930)
add extra bottom padding to collection assets

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:43:59 -08:00
Zach Pomerantz
134b1d708f revert: "fix: surface connection error" (#5932)
Revert "fix: surface connection error (#5931)"

This reverts commit e9bddcb670.
2023-02-06 12:20:11 -08:00
Zach Pomerantz
e9bddcb670 fix: surface connection error (#5931)
* fix: surface connection error

* fix: to spec

* fix: sentence case
2023-02-06 15:01:12 -05:00
Callil Capuozzo
19e45fd119 fix: style collect and create a proposal buttons (#5927)
* Add responsive button styles

* Clean up responsive

* Use SmallButtonPrimary

* Allow button to accept padding props

* Fix vote padding and update link

* Hide position NFT in mobile
2023-02-06 12:49:58 -05:00
Noah Zinsmeister
ae4135fa49 fix: block time is 12 seconds now (#5929)
* block time is 12 seconds now

* fix lint error

* Update governance.ts
2023-02-06 11:18:30 -05:00
Jordan Frankfurt
89e438bcc5 chore: update generated types (#5922) 2023-02-03 15:15:03 -06:00
cartcrom
92af2167ee refactor: L2 icons in component (#5901)
* feat: feature complete

* fix: hide l2 icons on most currencylogos

* linted

* refactor: remove unecessary logo container skeleton

* fix: removed todo comment and linted
2023-02-03 14:15:08 -05:00
eddie
db6084d717 feat: widget on tx success callback (#5916)
feat: log SWAP_TRANSACTION_COMPLETED for widget transactions
2023-02-03 10:23:41 -08:00
Jack Short
927d35d59e fix: useStablecoinPrice switching between WETH and ETH (#5904) 2023-02-03 11:05:47 -05:00
eddie
b4e981b2fd fix: landing page styling of widget (#5917) 2023-02-02 18:08:48 -08:00
Zach Pomerantz
967a698178 fix: upgrade @web3-react connectors (#5920) 2023-02-02 16:24:31 -08:00
Zach Pomerantz
7818426b53 fix: use conedison signTypedData (#5875)
* fix: use conedison signTypedData

* chore: rm old signTypedData

* fix: deduplicate
2023-02-02 14:02:45 -08:00
Jordan Frankfurt
93e0054f10 feat: add jest coverage to github action (#5850)
* add jest coverage to github action

* working on action config

* fix yaml syntax
2023-02-02 14:06:15 -06:00
eddie
661d2b6a33 feat: add zIndex to widget theme (#5915)
* feat: create feature flag for swap widget

* feat: add new flag to modal

* fix: missing defaultField usage

* feat: add zIndex to widget theme
2023-02-02 11:28:15 -08:00
eddie
c560b94366 feat: create feature flag for swap widget (#5909)
* feat: create feature flag for swap widget

* feat: add new flag to modal

* fix: missing defaultField usage
2023-02-02 11:26:24 -08:00
Jack Short
93a4f00287 fix: adding usd_value back to trace event (#5912) 2023-02-02 12:05:29 -07:00
Charles Bachmeier
48833f27e3 feat: [ListV2] Connect modal styling to functionality (#5905)
* dynamic styles based on listing status

* connect collection approval to v2 modal

* import cleanup

* add comments

* connect sign listing logic

* correct pending styles

* correct pending status for collection approval

* correct pending status for os listings

* use check from react-feather

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-01 13:41:50 -08:00
Jack Short
35a03e2681 fix: using the swap token selector for pay with any token (#5902)
* feat: adds input token quote for nfts

* remove eslint

* correct usdc pricing

* fix: uses swap token selector for pay with any token selector

* check when balances are loaded

* removing token selector

* only showing active tokens in pay with any token
2023-02-01 15:59:06 -05:00
Charles Bachmeier
ac0badfb1d feat: [ListV2] Add mobile styles (#5906)
* correct mobile modal width

* better mobile styling for listing page

* add height to modal
2023-02-01 12:54:55 -08:00
eddie
149b18f02e fix: about footer link weights (#5898) 2023-02-01 09:13:15 -08:00
Jack Short
52a43f3db0 chore: removeing fetch usd price for bag footer (#5903)
* chore: removing fetchUsdprice

* removing console
2023-02-01 12:00:39 -05:00
Jack Short
0a2a46d506 feat: quote token price for nfts (#5897)
* feat: adds input token quote for nfts

* remove eslint

* correct usdc pricing
2023-01-31 15:45:02 -05:00
Charles Bachmeier
a7c1bd4391 feat: [ListV2] Setup styles for Listing Modal (#5870)
* init modal title

* active and closed section headers

* added left border to section

* add tooltip

* complete collection row

* enforce max height

* add sign listings section

* fix top margin for section

* file re-org

* make modal section re-useable and move to its own file

* format

* define section props

* use accentAction for icons

* improved index

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-30 12:22:48 -08:00
cartcrom
13221e6935 feat: caching and polling on apollo token queries (#5874)
* fix: caching on apollo token queries

* refactor: rename state variable

* added documentation for state variable purpose

* added documentation for nullish operator usage
2023-01-30 14:53:53 -05:00
lynn
26fc3caa55 fix: Revert "fix: remove gwei indicator", restore block number (#5891)
* Revert "fix: remove gwei indicator (#5873)"

This reverts commit 409ba72f9f.

* add back block number
2023-01-30 14:30:01 -05:00
Charles Bachmeier
6072bb1be0 fix: Broken NFT Details test (#5893)
fix broken NFT test

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-30 10:09:45 -08:00
eddie
302af21a22 fix: widget theme integration (#5880)
* fix: add missing colors to widget theme integration

* feat: upgrade widget version

* fix: conedison in jest tests
2023-01-25 17:12:23 -05:00
Rachel-Eichenberger
b61a2d4111 fix: Update footer Github and Discord links (#5841)
Update footer github and discord links

Discord and github links were reversed
2023-01-25 16:15:23 -05:00
Zach Pomerantz
9be26788a2 test: skip flaky universal search test (#5881)
test: skip flaky test
2023-01-24 12:57:08 -05:00
Zach Pomerantz
ed393de481 fix: use wallet modal for widget-prompted connection (#5879)
* fix: use wallet modal for widget-prompted connection

* fix: prevent widget modal

* fix: invoke modal toggle
2023-01-24 12:36:50 -05:00
123 changed files with 4759 additions and 1914 deletions

View File

@@ -30,6 +30,11 @@ jobs:
- uses: ./.github/actions/setup
- run: yarn prepare
- run: yarn test
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
cypress-build:
runs-on: ubuntu-latest

View File

@@ -1,5 +1,7 @@
# Uniswap Labs Interface
[![codecov](https://codecov.io/gh/Uniswap/interface/branch/main/graph/badge.svg?token=YVT2Y86O82)](https://codecov.io/gh/Uniswap/interface)
[![Unit Tests](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
[![Integration Tests](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
[![Lint](https://github.com/Uniswap/interface/actions/workflows/lint.yml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
@@ -40,10 +42,10 @@ For steps on local deployment, development, and code contribution, please see [C
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
## Accessing Uniswap V1

View File

@@ -12,9 +12,10 @@ module.exports = {
jest: {
configure(jestConfig) {
return Object.assign({}, jestConfig, {
transformIgnorePatterns: ['@uniswap/conedison/format'],
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
moduleNameMapper: {
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
},
})
},

View File

@@ -1,6 +1,7 @@
import { getTestSelector } from '../utils'
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
describe('Testing nfts', () => {
beforeEach(() => {
@@ -16,7 +17,7 @@ describe('Testing nfts', () => {
})
it('should load pudgy penguin collection page', () => {
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-collection-asset')).should('exist')
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
cy.get(getTestSelector('nft-filter')).first().click()
@@ -24,13 +25,13 @@ describe('Testing nfts', () => {
})
it('should be able to navigate to activity', () => {
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-activity')).first().click()
cy.get(getTestSelector('nft-activity-row')).should('exist')
})
it('should go to the details page', () => {
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-filter')).first().click()
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
cy.get(getTestSelector('nft-details-link')).first().click()
@@ -41,7 +42,7 @@ describe('Testing nfts', () => {
})
it('should toggle buy now on details page', () => {
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/2043`)
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
cy.get(getTestSelector('nft-details-description-text')).should('exist')
cy.get(getTestSelector('nft-details-description')).click()
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')

View File

@@ -10,14 +10,6 @@ describe('Universal search bar', () => {
})
})
it('should yield no results found when contract address is search term', () => {
// Search for uni token contract address.
cy.get('[data-cy="search-bar-input"]').last().type('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('[data-cy="search-bar"]')
.should('contain.text', 'No tokens found.')
.and('contain.text', 'No NFT collections found.')
})
it('should yield clickable result for regular token or nft collection search term', () => {
// Search for uni token by name.
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
@@ -64,7 +56,7 @@ describe('Universal search bar', () => {
.should('be.eq', 3)
})
it('should show blocked badge when blocked token is searched for', () => {
it.skip('should show blocked badge when blocked token is searched for', () => {
// Search for mTSLA, which is a blocked token.
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')

View File

@@ -135,7 +135,7 @@
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "1.2.0",
"@uniswap/analytics-events": "^2.1.0",
"@uniswap/conedison": "^1.1.1",
"@uniswap/conedison": "^1.3.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
@@ -152,7 +152,7 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "2.25.1",
"@uniswap/widgets": "^2.27.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -165,16 +165,16 @@
"@visx/responsive": "^2.10.0",
"@visx/shape": "^2.11.1",
"@walletconnect/ethereum-provider": "^1.8.0",
"@web3-react/coinbase-wallet": "8.0.34-beta.0",
"@web3-react/coinbase-wallet": "8.0.35-beta.0",
"@web3-react/core": "8.0.35-beta.0",
"@web3-react/eip1193": "8.0.26-beta.0",
"@web3-react/eip1193": "8.0.27-beta.0",
"@web3-react/empty": "8.0.20-beta.0",
"@web3-react/gnosis-safe": "8.0.7-beta.0",
"@web3-react/metamask": "8.0.29-beta.0",
"@web3-react/metamask": "8.0.30-beta.0",
"@web3-react/network": "8.0.27-beta.0",
"@web3-react/types": "8.0.20-beta.0",
"@web3-react/url": "8.0.25-beta.0",
"@web3-react/walletconnect": "8.0.36-beta.0",
"@web3-react/walletconnect": "8.0.37-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"cids": "^1.0.0",

View File

@@ -1,9 +1,8 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { Link } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ExternalLink } from 'theme'
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
@@ -97,14 +96,10 @@ const ExternalTextLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textSecondary};
`
const TextLink = styled(Link)`
const TextLink = styled(StyledRouterLink)`
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.textSecondary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
`
const Copyright = styled.span`
@@ -120,7 +115,7 @@ const LogoSectionContent = () => {
<>
<StyledLogo src={isDarkMode ? darkUnicornImgSrc : lightUnicornImgSrc} alt="Uniswap Logo" />
<SocialLinks>
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
<DiscordIcon size={32} />
</SocialLink>
<TraceEvent
@@ -132,7 +127,7 @@ const LogoSectionContent = () => {
<TwitterIcon size={32} />
</SocialLink>
</TraceEvent>
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
<GithubIcon size={32} />
</SocialLink>
</SocialLinks>
@@ -158,15 +153,9 @@ export const AboutFooter = () => {
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Protocol</LinkGroupTitle>
<ExternalTextLink href="https://uniswap.org/community" target="_blank" rel="noopener noreferrer">
Community
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/governance" target="_blank" rel="noopener noreferrer">
Governance
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/developers" target="_blank" rel="noopener noreferrer">
Developers
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/community">Community</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/governance">Governance</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/developers">Developers</ExternalTextLink>
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Company</LinkGroupTitle>
@@ -175,18 +164,14 @@ export const AboutFooter = () => {
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.CAREERS_LINK}
>
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs" target="_blank" rel="noopener noreferrer">
Careers
</ExternalTextLink>
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs">Careers</ExternalTextLink>
</TraceEvent>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.BLOG_LINK}
>
<ExternalTextLink href="https://uniswap.org/blog" target="_blank" rel="noopener noreferrer">
Blog
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/blog">Blog</ExternalTextLink>
</TraceEvent>
</LinkGroup>
<LinkGroup>
@@ -209,9 +194,7 @@ export const AboutFooter = () => {
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.SUPPORT_LINK}
>
<ExternalTextLink href="https://support.uniswap.org/hc/en-us" target="_blank" rel="noopener noreferrer">
Help Center
</ExternalTextLink>
<ExternalTextLink href="https://support.uniswap.org/hc/en-us">Help Center</ExternalTextLink>
</TraceEvent>
</LinkGroup>
</FooterLinks>

View File

@@ -97,7 +97,8 @@ export const ButtonPrimary = styled(BaseButton)`
export const SmallButtonPrimary = styled(ButtonPrimary)`
width: auto;
font-size: 16px;
padding: 10px 16px;
padding: ${({ padding }) => padding ?? '8px 12px'};
border-radius: 12px;
`

View File

@@ -17,10 +17,10 @@ interface DoubleCurrencyLogoProps {
currency1?: Currency
}
const HigherLogo = styled(CurrencyLogo)`
const HigherLogoWrapper = styled.div`
z-index: 1;
`
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
const CoveredLogoWapper = styled.div<{ sizeraw: number }>`
position: absolute;
left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
`
@@ -33,8 +33,16 @@ export default function DoubleCurrencyLogo({
}: DoubleCurrencyLogoProps) {
return (
<Wrapper sizeraw={size} margin={margin}>
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
{currency0 && (
<HigherLogoWrapper>
<CurrencyLogo hideL2Icon currency={currency0} size={size.toString() + 'px'} />
</HigherLogoWrapper>
)}
{currency1 && (
<CoveredLogoWapper sizeraw={size}>
<CurrencyLogo hideL2Icon currency={currency1} size={size.toString() + 'px'} />
</CoveredLogoWapper>
)}
</Wrapper>
)
}

View File

@@ -1,8 +1,10 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
@@ -229,6 +231,18 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.payWithAnyToken}
label="Pay With Any Token"
/>
<FeatureFlagOption
variant={SwapWidgetVariant}
value={useSwapWidgetFlag()}
featureFlag={FeatureFlag.swapWidget}
label="Swap Widget"
/>
<FeatureFlagOption
variant={GqlRoutingVariant}
value={useGqlRoutingFlag()}
featureFlag={FeatureFlag.gqlRouting}
label="GraphQL NFT Routing"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}

View File

@@ -1,3 +1,4 @@
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import React from 'react'
@@ -29,10 +30,28 @@ export type AssetLogoBaseProps = {
backupImg?: string | null
size?: string
style?: React.CSSProperties
hideL2Icon?: boolean
}
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
// TODO(cartcrom): add prop to optionally render an L2Icon w/ the logo
const LogoContainer = styled.div`
position: relative;
display: flex;
`
const L2NetworkLogo = styled.div<{ networkUrl?: string; parentSize: string }>`
--size: ${({ parentSize }) => `calc(${parentSize} / 2)`};
width: var(--size);
height: var(--size);
position: absolute;
left: 50%;
bottom: 0;
background: url(${({ networkUrl }) => networkUrl});
background-repeat: no-repeat;
background-size: ${({ parentSize }) => `calc(${parentSize} / 2) calc(${parentSize} / 2)`};
display: ${({ networkUrl }) => !networkUrl && 'none'};
`
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
@@ -44,25 +63,27 @@ export default function AssetLogo({
backupImg,
size = '24px',
style,
...rest
hideL2Icon = false,
}: AssetLogoProps) {
const imageProps = {
alt: `${symbol ?? 'token'} logo`,
size,
style,
...rest,
}
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
if (src) {
return <LogoImage {...imageProps} src={src} onError={nextSrc} />
} else {
return (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}
return (
<LogoContainer style={style}>
{src ? (
<LogoImage {...imageProps} src={src} onError={nextSrc} />
) : (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)}
{!hideL2Icon && <L2NetworkLogo networkUrl={L2Icon} parentSize={size} />}
</LogoContainer>
)
}

View File

@@ -15,6 +15,7 @@ export default function CurrencyLogo(
address={props.currency?.wrapped.address}
symbol={props.symbol ?? props.currency?.symbol}
backupImg={(props.currency as TokenInfo)?.logoURI}
hideL2Icon={props.hideL2Icon ?? true}
{...props}
/>
)

View File

@@ -1,4 +1,6 @@
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
export default function QueryTokenLogo(
props: AssetLogoBaseProps & {
token?: TopToken | TokenQueryData
token?: TopToken | TokenQueryData | SearchToken
}
) {
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
return (
<AssetLogo
isNative={props.token?.address === NATIVE_CHAIN_ID}
isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
chainId={chainId}
address={props.token?.address}
symbol={props.token?.symbol}

View File

@@ -166,6 +166,9 @@ export const MenuDropdown = () => {
<SecondaryLinkedText href="https://docs.uniswap.org/">
<Trans>Documentation</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
<Trans>Feedback</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText
onClick={() => {
toggleOpen()

View File

@@ -0,0 +1,102 @@
import { SupportedChainId } from 'constants/chains'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useAtom } from 'jotai'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { GenieCollection } from 'nft/types'
import { useCallback, useMemo } from 'react'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
type RecentlySearchedAsset = {
isNft?: boolean
address: string
chain: Chain
}
// Temporary measure used until backend supports addressing by "NATIVE"
const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string
function getQueryAddress(chain: Chain) {
return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT
}
const recentlySearchedAssetsAtom = atomWithStorage<RecentlySearchedAsset[]>('recentlySearchedAssets', [])
export function useAddRecentlySearchedAsset() {
const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom)
return useCallback(
(asset: RecentlySearchedAsset) => {
// Removes the new asset if it was already in the array
const newHistory = searchHistory.filter(
(oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain)
)
newHistory.unshift(asset)
updateSearchHistory(newHistory)
},
[searchHistory, updateSearchHistory]
)
}
export function useRecentlySearchedAssets() {
const history = useAtomValue(recentlySearchedAssetsAtom)
const shortenedHistory = useMemo(() => history.slice(0, 4), [history])
const { data: queryData, loading } = useRecentlySearchedAssetsQuery({
variables: {
collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address),
contracts: shortenedHistory
.filter((asset) => !asset.isNft)
.map((token) => ({
address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address,
chain: token.chain,
})),
},
})
const data = useMemo(() => {
if (shortenedHistory.length === 0) return []
else if (!queryData) return undefined
// Collects both tokens and collections in a map, so they can later be returned in original order
const resultsMap: { [key: string]: GenieCollection | SearchToken } = {}
const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable<NftCollection>)
const collections = queryCollections?.map(
(queryCollection): GenieCollection => {
return {
address: queryCollection.nftContracts?.[0]?.address ?? '',
isVerified: queryCollection?.isVerified,
name: queryCollection?.name,
stats: {
floor_price: queryCollection?.markets?.[0]?.floorPrice?.value,
total_supply: queryCollection?.numAssets,
},
imageUrl: queryCollection?.image?.url ?? '',
}
},
[queryCollections]
)
collections?.forEach((collection) => (resultsMap[collection.address] = collection))
queryData.tokens?.filter(Boolean).forEach((token) => {
resultsMap[token.address ?? `NATIVE-${token.chain}`] = token
})
const data: (SearchToken | GenieCollection)[] = []
shortenedHistory.forEach((asset) => {
if (asset.address === 'NATIVE') {
// Handles special case where wMATIC data needs to be used for MATIC
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET)
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
const result = resultsMap[queryAddress]
if (result) data.push({ ...result, address: 'NATIVE', ...native })
} else {
const result = resultsMap[asset.address]
if (result) data.push(result)
}
})
return data
}, [queryData, shortenedHistory])
return { data, loading }
}

View File

@@ -2,7 +2,9 @@
import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { useSearchTokens } from 'graphql/data/SearchTokens'
import useDebounce from 'hooks/useDebounce'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
import { magicalGradientOnHover } from 'nft/css/common.css'
import { useIsMobile, useIsTablet } from 'nft/hooks'
import { fetchSearchCollections } from 'nft/queries'
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
@@ -64,16 +65,8 @@ export const SearchBar = () => {
}
)
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
['searchTokens', debouncedSearchValue],
() => fetchSearchTokens(debouncedSearchValue),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!debouncedSearchValue.length,
}
)
const { chainId } = useWeb3React()
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
const isNFTPage = useIsNftPage()

View File

@@ -1,30 +1,33 @@
import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics'
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import useTrendingTokens from 'graphql/data/TrendingTokens'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css'
import { useSearchHistory } from 'nft/hooks'
import { fetchTrendingCollections } from 'nft/queries'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
return (suggestion as FungibleToken).decimals === undefined
function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
return (suggestion as SearchToken).decimals === undefined
}
interface SearchBarDropdownSectionProps {
toggleOpen: () => void
suggestions: (GenieCollection | FungibleToken)[]
suggestions: (GenieCollection | SearchToken)[]
header: JSX.Element
headerIcon?: JSX.Element
hoveredIndex: number | undefined
@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
) : (
<TokenRow
key={suggestion.address}
token={suggestion as FungibleToken}
token={suggestion as SearchToken}
isHovered={hoveredIndex === index + startingIndex}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
)
}
function isKnownToken(token: SearchToken) {
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
}
interface SearchBarDropdownProps {
toggleOpen: () => void
tokens: FungibleToken[]
tokens: SearchToken[]
collections: GenieCollection[]
queryText: string
hasInput: boolean
@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
isLoading,
}: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
const { data: searchHistory } = useRecentlySearchedAssets()
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
const { pathname } = useLocation()
const isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens')
@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
[isNFTPage, trendingCollectionResults]
)
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
useEffect(() => {
trendingTokenResults?.forEach(updateSearchHistory)
}, [trendingTokenResults, updateSearchHistory])
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
const trendingTokensLength = isTokenPage ? 3 : 2
const trendingTokens = useMemo(
() =>
trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
() => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
[trendingTokenData, trendingTokensLength]
)
const totalSuggestions = hasInput
@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({
}, [toggleOpen, hoveredIndex, totalSuggestions])
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
const hasVerifiedToken = tokens.some((token) => token.onDefaultList)
const hasKnownToken = tokens.some(isKnownToken)
const showCollectionsFirst =
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
(isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
}}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
isLoading={!searchHistory}
/>
)}
{!isNFTPage && (
@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
}}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
isLoading={!trendingTokenData}
/>
)}
{!isTokenPage && (
@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
trendingCollections,
trendingCollectionsAreLoading,
trendingTokens,
trendingTokensAreLoading,
trendingTokenData,
hoveredIndex,
toggleOpen,
shortenedHistory,
@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
queryText,
totalSuggestions,
trace,
searchHistory,
])
return (

View File

@@ -1,21 +1,18 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import AssetLogo from 'components/Logo/AssetLogo'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { checkSearchTokenWarning } from 'constants/tokenSafety'
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { getTokenDetailsURL } from 'graphql/data/util'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
import { vars } from 'nft/css/sprinkles.css'
import { useSearchHistory } from 'nft/hooks'
import { FungibleToken, GenieCollection } from 'nft/types'
import { GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react'
@@ -23,11 +20,9 @@ import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
const StyledLogoContainer = styled(LogoContainer)`
margin-right: 8px;
`
const PriceChangeContainer = styled.div`
display: flex;
align-items: center;
@@ -63,16 +58,15 @@ export const CollectionRow = ({
}: CollectionRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(collection)
addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
}, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
@@ -130,17 +124,8 @@ export const CollectionRow = ({
)
}
function useBridgedAddress(token: FungibleToken): [string | undefined, number | undefined, string | undefined] {
const { chainId: connectedChainId } = useWeb3React()
const bridgedAddress = connectedChainId ? token.extensions?.bridgeInfo?.[connectedChainId]?.tokenAddress : undefined
if (bridgedAddress && connectedChainId) {
return [bridgedAddress, connectedChainId, getChainInfo(connectedChainId)?.circleLogoUrl]
}
return [undefined, undefined, undefined]
}
interface TokenRowProps {
token: FungibleToken
token: SearchToken
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
@@ -149,19 +134,18 @@ interface TokenRowProps {
}
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(token)
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
address && addRecentlySearchedAsset({ address, chain: token.chain })
toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, toggleOpen, token, eventProperties])
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
const tokenDetailsPath = getTokenDetailsURL(token)
// Close the modal on escape
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
@@ -177,7 +161,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
}
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
const arrow = getDeltaArrow(token.price24hChange, 18)
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
return (
<Link
@@ -190,37 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
>
<Row style={{ width: '65%' }}>
<StyledLogoContainer>
<AssetLogo
isNative={token.address === NATIVE_CHAIN_ID}
address={token.address}
chainId={token.chainId}
symbol={token.symbol}
size="36px"
backupImg={token.logoURI}
/>
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</StyledLogoContainer>
<QueryTokenLogo
token={token}
symbol={token.symbol}
size="36px"
backupImg={token.project?.logoUrl}
style={{ paddingRight: '8px' }}
/>
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
<TokenSafetyIcon warning={checkWarning(token.address)} />
<TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
</Row>
<Column className={styles.suggestionSecondaryContainer}>
{token.priceUsd && (
{token.market?.price?.value && (
<Row gap="4">
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box>
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
</Row>
)}
{token.price24hChange && (
{token.market?.pricePercentChange?.value && (
<PriceChangeContainer>
<ArrowCell>{arrow}</ArrowCell>
<PriceChangeText isNegative={token.price24hChange < 0}>
{Math.abs(token.price24hChange).toFixed(2)}%
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
</PriceChangeText>
</PriceChangeContainer>
)}

View File

@@ -57,8 +57,7 @@ export const PageTabs = () => {
pathname.startsWith('/pool') ||
pathname.startsWith('/add') ||
pathname.startsWith('/remove') ||
pathname.startsWith('/increase') ||
pathname.startsWith('/find')
pathname.startsWith('/increase')
const isNftPage = useIsNftPage()

View File

@@ -0,0 +1,81 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault, L2ChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
const BodyRow = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 12px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
`
const CautionTriangle = styled(AlertTriangle)`
color: ${({ theme }) => theme.accentWarning};
`
const Link = styled(ExternalLink)`
color: ${({ theme }) => theme.black};
text-decoration: underline;
`
const TitleRow = styled.div`
align-items: center;
display: flex;
justify-content: flex-start;
margin-bottom: 8px;
`
const TitleText = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin: 0px 12px;
`
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
bottom: 60px;
display: none;
max-width: 348px;
padding: 16px 20px;
position: fixed;
right: 16px;
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) {
display: block;
}
`
export function ChainConnectivityWarning() {
const { chainId } = useWeb3React()
const info = getChainInfoOrDefault(chainId)
const label = info?.label
return (
<Wrapper>
<TitleRow>
<CautionTriangle />
<TitleText>
<Trans>Network Warning</Trans>
</TitleText>
</TitleRow>
<BodyRow>
{chainId === SupportedChainId.MAINNET ? (
<Trans>You may have lost your network connection.</Trans>
) : (
<Trans>{label} might be down right now, or you may have lost your network connection.</Trans>
)}{' '}
{(info as L2ChainInfo).statusPage !== undefined && (
<span>
<Trans>Check network status</Trans>{' '}
<Link href={(info as L2ChainInfo).statusPage || ''}>
<Trans>here.</Trans>
</Link>
</span>
)}
</BodyRow>
</Wrapper>
)
}

View File

@@ -0,0 +1,164 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import { useIsLandingPage } from 'hooks/useIsLandingPage'
import { useIsNftPage } from 'hooks/useIsNftPage'
import useMachineTimeMs from 'hooks/useMachineTime'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useEffect, useMemo, useState } from 'react'
import styled, { keyframes } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { MouseoverTooltip } from '../Tooltip'
import { ChainConnectivityWarning } from './ChainConnectivityWarning'
const StyledPolling = styled.div`
align-items: center;
bottom: 0;
color: ${({ theme }) => theme.textTertiary};
display: none;
padding: 1rem;
position: fixed;
right: 0;
transition: 250ms ease color;
a {
color: unset;
}
a:hover {
color: unset;
text-decoration: none;
}
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
display: flex;
}
`
const StyledPollingBlockNumber = styled(ThemedText.DeprecatedSmall)<{
breathe: boolean
hovering: boolean
warning: boolean
}>`
color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
transition: opacity 0.25s ease;
opacity: ${({ breathe, hovering }) => (hovering ? 0.7 : breathe ? 1 : 0.5)};
:hover {
opacity: 1;
}
a {
color: unset;
}
a:hover {
text-decoration: none;
color: unset;
}
`
const StyledPollingDot = styled.div<{ warning: boolean }>`
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
position: relative;
background-color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
transition: 250ms ease background-color;
`
const rotate360 = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const Spinner = styled.div<{ warning: boolean }>`
animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite;
transform: translateZ(0);
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-left: 2px solid ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
background: transparent;
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
transition: 250ms ease border-color;
left: -3px;
top: -3px;
`
const DEFAULT_MS_BEFORE_WARNING = ms`10m`
const NETWORK_HEALTH_CHECK_MS = ms`10s`
export default function Polling() {
const { chainId } = useWeb3React()
const blockNumber = useBlockNumber()
const [isMounting, setIsMounting] = useState(false)
const [isHover, setIsHover] = useState(false)
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
const blockTime = useCurrentBlockTimestamp()
const isNftPage = useIsNftPage()
const isLandingPage = useIsLandingPage()
const waitMsBeforeWarning =
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
const warning = Boolean(!!blockTime && machineTime - blockTime.mul(1000).toNumber() > waitMsBeforeWarning)
useEffect(
() => {
if (!blockNumber) {
return
}
setIsMounting(true)
const mountingTimer = setTimeout(() => setIsMounting(false), 1000)
// this will clear Timeout when component unmount like in willComponentUnmount
return () => {
clearTimeout(mountingTimer)
}
},
[blockNumber] //useEffect will run only one time
//if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run)
)
//TODO - chainlink gas oracle is really slow. Can we get a better data source?
const blockExternalLinkHref = useMemo(() => {
if (!chainId || !blockNumber) return ''
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
}, [blockNumber, chainId])
if (isNftPage || isLandingPage) {
return null
}
return (
<RowFixed>
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
<StyledPollingBlockNumber breathe={isMounting} hovering={isHover} warning={warning}>
<ExternalLink href={blockExternalLinkHref}>
<MouseoverTooltip
text={<Trans>The most recent block number on this network. Prices update on every block.</Trans>}
>
{blockNumber}&ensp;
</MouseoverTooltip>
</ExternalLink>
</StyledPollingBlockNumber>
<StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '}
</StyledPolling>
{warning && <ChainConnectivityWarning />}
</RowFixed>
)
}

View File

@@ -99,9 +99,9 @@ export default function PositionList({
</ToggleLabel>
</ToggleWrap>
</MobileHeader>
{positions.map((p) => {
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
})}
{positions.map((p) => (
<PositionListItem key={p.tokenId.toString()} {...p} />
))}
</>
)
}

View File

@@ -0,0 +1,38 @@
import { BigNumber } from '@ethersproject/bignumber'
import { render, screen } from 'test-utils'
import PositionListItem from '.'
jest.mock('hooks/Tokens', () => {
const originalModule = jest.requireActual('hooks/Tokens')
const uniSDK = jest.requireActual('@uniswap/sdk-core')
return {
__esModule: true,
...originalModule,
useToken: jest.fn(
() =>
new uniSDK.Token(
1,
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
8,
'https://www.example.com',
'example.com coin'
)
),
}
})
test('PositionListItem should not render when the name contains a url', () => {
const positionDetails = {
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
render(<PositionListItem {...positionDetails} />)
screen.debug()
expect(screen.queryByText('.com', { exact: false })).toBe(null)
})

View File

@@ -1,3 +1,4 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import { Percent, Price, Token } from '@uniswap/sdk-core'
import { Position } from '@uniswap/v3-sdk'
@@ -15,9 +16,9 @@ import { Link } from 'react-router-dom'
import { Bound } from 'state/mint/v3/actions'
import styled from 'styled-components/macro'
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
import { PositionDetails } from 'types/position'
import { formatTickPrice } from 'utils/formatTickPrice'
import { unwrappedToken } from 'utils/unwrappedToken'
import { hasURL } from 'utils/urlChecks'
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
@@ -109,7 +110,13 @@ const DataText = styled.div`
`
interface PositionListItemProps {
positionDetails: PositionDetails
token0: string
token1: string
tokenId: BigNumber
fee: number
liquidity: BigNumber
tickLower: number
tickUpper: number
}
export function getPriceOrderingFromPositionForUI(position?: Position): {
@@ -166,16 +173,15 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
}
}
export default function PositionListItem({ positionDetails }: PositionListItemProps) {
const {
token0: token0Address,
token1: token1Address,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
} = positionDetails
export default function PositionListItem({
token0: token0Address,
token1: token1Address,
tokenId,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
}: PositionListItemProps) {
const token0 = useToken(token0Address)
const token1 = useToken(token1Address)
@@ -203,10 +209,23 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
// check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
const positionSummaryLink = '/pool/' + positionDetails.tokenId
const positionSummaryLink = '/pool/' + tokenId
const removed = liquidity?.eq(0)
const containsURL = useMemo(
() =>
[token0?.name, token0?.symbol, token1?.name, token1?.symbol].reduce(
(acc, testString) => acc || Boolean(testString && hasURL(testString)),
false
),
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
)
if (containsURL) {
return null
}
return (
<LinkRow to={positionSummaryLink}>
<RowBetween>

View File

@@ -105,235 +105,240 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
}
<div
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
style="padding-right: 8px; padding-top: 8px;"
>
<div
style="height: 168px; width: 100%;"
class="CurrencyList_scrollbarStyle__1pi21y70"
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
>
<div
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
tabindex="0"
style="height: 168px; width: 100%;"
>
<div
class="c4"
>
CurrencyLogo currency=DAI
</div>
<div
class="c5"
style="opacity: 1;"
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c0 c1"
class="c4"
>
CurrencyLogo currency=DAI
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c6 css-vurnku"
title="Dai Stablecoin"
>
Dai Stablecoin
</div>
<div
class="c7"
class="c0 c1"
>
<div
class="c8"
class="c6 css-vurnku"
title="Dai Stablecoin"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
Dai Stablecoin
</div>
<div
class="c7"
>
<div
class="c8"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="c10 css-yfjwjl"
>
DAI
</div>
</div>
<div
class="c4"
>
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=USDC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c6 css-vurnku"
title="USD//C"
class="c10 css-yfjwjl"
>
USD//C
DAI
</div>
</div>
<div
class="c4"
>
<div
class="c7"
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=USDC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c8"
class="c6 css-vurnku"
title="USD//C"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
USD//C
</div>
<div
class="c7"
>
<div
class="c8"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="c10 css-yfjwjl"
>
USDC
</div>
</div>
<div
class="c4"
>
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=WBTC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c6 css-vurnku"
title="Wrapped BTC"
class="c10 css-yfjwjl"
>
Wrapped BTC
USDC
</div>
</div>
<div
class="c4"
>
<div
class="c7"
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=WBTC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c8"
class="c6 css-vurnku"
title="Wrapped BTC"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
Wrapped BTC
</div>
<div
class="c7"
>
<div
class="c8"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
</div>
<div
class="c10 css-yfjwjl"
>
WBTC
</div>
</div>
<div
class="c10 css-yfjwjl"
class="c4"
>
WBTC
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c4"
>
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
</div>
</div>
@@ -383,31 +388,36 @@ exports[`renders loading rows when isLoading is true 1`] = `
}
<div
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
style="padding-right: 8px; padding-top: 8px;"
>
<div
style="height: 560px; width: 100%;"
class="CurrencyList_scrollbarStyle__1pi21y70"
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
>
<div
class="c0 c1"
style="height: 560px; width: 100%;"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { style } from '@vanilla-extract/css'
import { themeVars } from 'nft/css/sprinkles.css'
export const scrollbarStyle = style([
{
scrollbarWidth: 'thin',
scrollbarColor: `${themeVars.colors.backgroundOutline} transparent`,
height: '100%',
selectors: {
'&::-webkit-scrollbar': {
background: 'transparent',
width: '4px',
},
'&::-webkit-scrollbar-thumb': {
background: `${themeVars.colors.backgroundOutline}`,
borderRadius: '8px',
},
},
},
])

View File

@@ -20,6 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
import Row, { RowFixed } from '../../Row'
import { MouseoverTooltip } from '../../Tooltip'
import { LoadingRows, MenuItem } from '../styleds'
import * as styles from './index.css'
function currencyKey(currency: Currency): string {
return currency.isToken ? currency.address : 'ETHER'
@@ -288,21 +289,34 @@ export default function CurrencyList({
return currencyKey(currency)
}, [])
return isLoading ? (
<FixedSizeList height={height} ref={fixedListRef as any} width="100%" itemData={[]} itemCount={10} itemSize={56}>
{LoadingRow}
</FixedSizeList>
) : (
<FixedSizeList
height={height}
ref={fixedListRef as any}
width="100%"
itemData={itemData}
itemCount={itemData.length}
itemSize={56}
itemKey={itemKey}
>
{Row}
</FixedSizeList>
return (
<div style={{ paddingRight: '8px', paddingTop: '8px' }}>
{isLoading ? (
<FixedSizeList
className={styles.scrollbarStyle}
height={height}
ref={fixedListRef as any}
width="100%"
itemData={[]}
itemCount={10}
itemSize={56}
>
{LoadingRow}
</FixedSizeList>
) : (
<FixedSizeList
className={styles.scrollbarStyle}
height={height}
ref={fixedListRef as any}
width="100%"
itemData={itemData}
itemCount={itemData.length}
itemSize={56}
itemKey={itemKey}
>
{Row}
</FixedSizeList>
)}
</div>
)
}

View File

@@ -32,6 +32,7 @@ import { PaddedColumn, SearchInput, Separator } from './styleds'
const ContentWrapper = styled(Column)`
background-color: ${({ theme }) => theme.backgroundSurface};
width: 100%;
overflow: hidden;
flex: 1 1;
position: relative;
`
@@ -45,6 +46,7 @@ interface CurrencySearchProps {
showCommonBases?: boolean
showCurrencyAmount?: boolean
disableNonToken?: boolean
onlyShowCurrenciesWithBalance?: boolean
}
export function CurrencySearch({
@@ -56,6 +58,7 @@ export function CurrencySearch({
disableNonToken,
onDismiss,
isOpen,
onlyShowCurrenciesWithBalance,
}: CurrencySearchProps) {
const { chainId } = useWeb3React()
const theme = useTheme()
@@ -92,6 +95,10 @@ export function CurrencySearch({
!balancesAreLoading
? filteredTokens
.filter((token) => {
if (onlyShowCurrenciesWithBalance) {
return balances[token.address]?.greaterThan(0)
}
// If there is no query, filter out unselected user-added tokens with no balance.
if (!debouncedQuery && token instanceof UserAddedToken) {
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
@@ -101,7 +108,15 @@ export function CurrencySearch({
})
.sort(tokenComparator.bind(null, balances))
: [],
[balances, balancesAreLoading, debouncedQuery, filteredTokens, otherSelectedCurrency, selectedCurrency]
[
balances,
balancesAreLoading,
debouncedQuery,
filteredTokens,
otherSelectedCurrency,
selectedCurrency,
onlyShowCurrenciesWithBalance,
]
)
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
@@ -114,11 +129,23 @@ export function CurrencySearch({
const s = debouncedQuery.toLowerCase().trim()
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
)
const shouldShowWrapped =
!onlyShowCurrenciesWithBalance || (!balancesAreLoading && balances[wrapped.address]?.greaterThan(0))
const natives = (
disableNonToken || native.equals(wrapped) ? [wrapped] : shouldShowWrapped ? [native, wrapped] : [native]
).filter((n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1)
return [...natives, ...tokens]
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
}, [
debouncedQuery,
filteredSortedTokens,
onlyShowCurrenciesWithBalance,
balancesAreLoading,
balances,
wrapped,
disableNonToken,
native,
])
const handleCurrencySelect = useCallback(
(currency: Currency, hasWarning?: boolean) => {
@@ -168,7 +195,9 @@ export function CurrencySearch({
// if no results on main list, show option to expand into inactive
const filteredInactiveTokens = useSearchInactiveTokenLists(
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
!onlyShowCurrenciesWithBalance && (filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch))
? debouncedQuery
: undefined
)
// Timeout token loader after 3 seconds to avoid hanging in a loading state.

View File

@@ -17,6 +17,7 @@ interface CurrencySearchModalProps {
showCommonBases?: boolean
showCurrencyAmount?: boolean
disableNonToken?: boolean
onlyShowCurrenciesWithBalance?: boolean
}
enum CurrencyModalView {
@@ -34,6 +35,7 @@ export default memo(function CurrencySearchModal({
showCommonBases = false,
showCurrencyAmount = true,
disableNonToken = false,
onlyShowCurrenciesWithBalance = false,
}: CurrencySearchModalProps) {
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
const lastOpen = useLast(isOpen)
@@ -84,6 +86,7 @@ export default memo(function CurrencySearchModal({
showCommonBases={showCommonBases}
showCurrencyAmount={showCurrencyAmount}
disableNonToken={disableNonToken}
onlyShowCurrenciesWithBalance={onlyShowCurrenciesWithBalance}
/>
)
break

View File

@@ -81,7 +81,7 @@ export default function BalanceSummary({ token }: { token: Currency }) {
<Trans>Your balance on {label}</Trans>
</ThemedText.SubHeaderSmall>
<BalanceRow>
<CurrencyLogo currency={token} size="2rem" />
<CurrencyLogo currency={token} size="2rem" hideL2Icon={false} />
<BalanceContainer>
<BalanceAmountsContainer>
<BalanceItem>

View File

@@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector'
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
// Appends the current price to the end of the priceHistory array
const priceHistory = useMemo(() => {
const market = tokenPriceData.tokens?.[0]?.market
const market = tokenPriceData.token?.market
const priceHistory = market?.priceHistory?.filter(isPricePoint)
const currentPrice = market?.price?.value
if (Array.isArray(priceHistory) && currentPrice !== undefined) {

View File

@@ -1,12 +1,11 @@
import { WidgetSkeleton } from 'components/Widget'
import { WIDGET_WIDTH } from 'components/Widget'
import { DEFAULT_WIDGET_WIDTH } from 'components/Widget'
import { ArrowLeft } from 'react-feather'
import { useParams } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { textFadeIn } from 'theme/styles'
import { LoadingBubble } from '../loading'
import { LogoContainer } from '../TokenTable/TokenRow'
import { AboutContainer, AboutHeader } from './About'
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
import { TokenPrice } from './PriceChart'
@@ -44,7 +43,7 @@ export const RightPanel = styled.div`
display: none;
flex-direction: column;
gap: 20px;
width: ${WIDGET_WIDTH}px;
width: ${DEFAULT_WIDGET_WIDTH}px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
display: flex;
@@ -227,9 +226,7 @@ export default function TokenDetailsSkeleton() {
</BreadcrumbNavLink>
<TokenInfoContainer>
<TokenNameCell>
<LogoContainer>
<TokenLogoBubble />
</LogoContainer>
<TokenLogoBubble />
<TitleBubble />
</TokenNameCell>
</TokenInfoContainer>

View File

@@ -20,7 +20,6 @@ import TokenDetailsSkeleton, {
TokenNameCell,
} from 'components/Tokens/TokenDetails/Skeleton'
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget from 'components/Widget'
@@ -111,7 +110,7 @@ export default function TokenDetails({
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const tokenQueryData = tokenQuery.tokens?.[0]
const tokenQueryData = tokenQuery.token
const crossChainMap = useMemo(
() =>
tokenQueryData?.project?.tokens.reduce((map, current) => {
@@ -134,25 +133,27 @@ export default function TokenDetails({
if (!address) return
const bridgedAddress = crossChainMap[update]
if (bridgedAddress) {
startTokenTransition(() => navigate(getTokenDetailsURL(bridgedAddress, update)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
} else if (didFetchFromChain || token?.isNative) {
startTokenTransition(() => navigate(getTokenDetailsURL(address, update)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
}
},
[address, crossChainMap, didFetchFromChain, navigate, token?.isNative]
[address, chain, crossChainMap, didFetchFromChain, navigate, token?.isNative]
)
useOnGlobalChainSwitch(navigateToTokenForChain)
const navigateToWidgetSelectedToken = useCallback(
(token: Currency) => {
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
startTokenTransition(() => navigate(getTokenDetailsURL(address, chain)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
},
[chain, navigate]
)
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false)
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(address, pageChainId) && tokenWarning !== null
const onReviewSwapClick = useCallback(
@@ -168,8 +169,6 @@ export default function TokenDetails({
[continueSwap, setContinueSwap]
)
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
// address will never be undefined if token is defined; address is checked here to appease typechecker
if (token === undefined || !address) {
return <InvalidTokenDetails chainName={address && getChainInfo(pageChainId)?.label} />
@@ -188,10 +187,8 @@ export default function TokenDetails({
</BreadcrumbNavLink>
<TokenInfoContainer data-testid="token-info-container">
<TokenNameCell>
<LogoContainer>
<CurrencyLogo currency={token} size="32px" />
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</LogoContainer>
<CurrencyLogo currency={token} size="32px" hideL2Icon={false} />
{token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
</TokenNameCell>
@@ -220,22 +217,28 @@ export default function TokenDetails({
<TokenDetailsSkeleton />
)}
<RightPanel>
<Widget
token={token ?? undefined}
onTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
<Widget
defaultTokens={{
default: token ?? undefined,
}}
onDefaultTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
</div>
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
{token && <BalanceSummary token={token} />}
</RightPanel>
{token && <MobileBalanceSummaryFooter token={token} />}
<TokenSafetyModal
isOpen={isBlockedToken || !!continueSwap}
isOpen={openTokenSafetyModal || !!continueSwap}
tokenAddress={address}
onContinue={() => onResolveSwap(true)}
onBlocked={() => navigate(-1)}
onBlocked={() => {
setOpenTokenSafetyModal(false)
}}
onCancel={() => onResolveSwap(false)}
showCancel={true}
/>

View File

@@ -6,7 +6,6 @@ import { ParentSize } from '@visx/responsive'
import SparklineChart from 'components/Charts/SparklineChart'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import { MouseoverTooltip } from 'components/Tooltip'
import { getChainInfo } from 'constants/chainInfo'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
@@ -279,23 +278,6 @@ export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
height: 4px;
`
export const L2NetworkLogo = styled.div<{ networkUrl?: string; size?: string }>`
height: ${({ size }) => size ?? '12px'};
width: ${({ size }) => size ?? '12px'};
position: absolute;
left: 50%;
bottom: 0;
background: url(${({ networkUrl }) => networkUrl});
background-repeat: no-repeat;
background-size: ${({ size }) => (size ? `${size} ${size}` : '12px 12px')};
display: ${({ networkUrl }) => !networkUrl && 'none'};
`
export const LogoContainer = styled.div`
position: relative;
align-items: center;
display: flex;
`
const InfoIconContainer = styled.div`
margin-left: 2px;
display: flex;
@@ -307,7 +289,9 @@ export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined>
[TokenSortMethod.PRICE]: undefined,
[TokenSortMethod.PERCENT_CHANGE]: undefined,
[TokenSortMethod.TOTAL_VALUE_LOCKED]: (
<Trans>Total value locked (TVL) is the amount of the asset thats currently in a Uniswap v3 liquidity pool.</Trans>
<Trans>
Total value locked (TVL) is the aggregate amount of the asset available across all Uniswap v3 liquidity pools.
</Trans>
),
[TokenSortMethod.VOLUME]: (
<Trans>Volume is the amount of the asset that has been traded on Uniswap v3 during the selected time frame.</Trans>
@@ -442,18 +426,17 @@ interface LoadedRowProps {
tokenListLength: number
token: NonNullable<TopToken>
sparklineMap: SparklineMap
volumeRank: number
sortRank: number
}
/* Loaded State: row component with token information */
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { tokenListIndex, tokenListLength, token, volumeRank } = props
const { tokenListIndex, tokenListLength, token, sortRank } = props
const filterString = useAtomValue(filterStringAtom)
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
const filterNetwork = lowercaseChainName.toUpperCase()
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value
const arrow = getDeltaArrow(delta)
@@ -465,7 +448,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
token_address: token.address,
token_symbol: token.symbol,
token_list_index: tokenListIndex,
token_list_rank: volumeRank,
token_list_rank: sortRank,
token_list_length: tokenListLength,
time_frame: timePeriod,
search_token_address_input: filterString,
@@ -475,20 +458,17 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
return (
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
<StyledLink
to={getTokenDetailsURL(token.address ?? '', token.chain)}
to={getTokenDetailsURL(token)}
onClick={() =>
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
}
>
<TokenRow
header={false}
listNumber={volumeRank}
listNumber={sortRank}
tokenInfo={
<ClickableName>
<LogoContainer>
<QueryTokenLogo token={token} />
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<QueryTokenLogo token={token} />
<TokenInfoCell>
<TokenName data-cy="token-name">{token.name}</TokenName>
<TokenSymbol>{token.symbol}</TokenSymbol>

View File

@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
}
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, tokenVolumeRank, loadingTokens, sparklines } = useTopTokens(chainName)
const { tokens, tokenSortRank, loadingTokens, sparklines } = useTopTokens(chainName)
/* loading and error state */
if (loadingTokens) {
if (loadingTokens && !tokens) {
return <LoadingTokenTable rowCount={PAGE_SIZE} />
} else if (!tokens) {
return (
@@ -110,7 +109,7 @@ export default function TokenTable() {
tokenListLength={tokens.length}
token={token}
sparklineMap={sparklines}
volumeRank={tokenVolumeRank[token.address]}
sortRank={tokenSortRank[token.address]}
/>
)
)}

View File

@@ -26,16 +26,17 @@ import {
getTokenAddress,
} from 'lib/utils/analytics'
import { useCallback, useState } from 'react'
import { useToggleWalletModal } from 'state/application/hooks'
import { useIsDarkMode } from 'state/user/hooks'
import { computeRealizedPriceImpact } from 'utils/prices'
import { switchChain } from 'utils/switchChain'
import { useSyncWidgetInputs } from './inputs'
import { DefaultTokens, useSyncWidgetInputs } from './inputs'
import { useSyncWidgetSettings } from './settings'
import { DARK_THEME, LIGHT_THEME } from './theme'
import { useSyncWidgetTransactions } from './transactions'
export const WIDGET_WIDTH = 360
export const DEFAULT_WIDGET_WIDTH = 360
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
@@ -44,19 +45,34 @@ function useWidgetTheme() {
}
interface WidgetProps {
token?: Currency
onTokenChange?: (token: Currency) => void
defaultTokens: DefaultTokens
width?: number | string
onDefaultTokenChange?: (token: Currency) => void
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
export default function Widget({
defaultTokens,
width = DEFAULT_WIDGET_WIDTH,
onDefaultTokenChange,
onReviewSwapClick,
}: WidgetProps) {
const { connector, provider } = useWeb3React()
const locale = useActiveLocale()
const theme = useWidgetTheme()
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
const { inputs, tokenSelector } = useSyncWidgetInputs({
defaultTokens,
onDefaultTokenChange,
})
const { settings } = useSyncWidgetSettings()
const { transactions } = useSyncWidgetTransactions()
const toggleWalletModal = useToggleWalletModal()
const onConnectWalletClick = useCallback(() => {
toggleWalletModal()
return false // prevents the in-widget wallet modal from opening
}, [toggleWalletModal])
const onSwitchChain = useCallback(
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
@@ -152,8 +168,9 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
routerUrl={WIDGET_ROUTER_URL}
locale={locale}
theme={theme}
width={WIDGET_WIDTH}
width={width}
// defaultChainId is excluded - it is always inferred from the passed provider
onConnectWalletClick={onConnectWalletClick}
provider={provider}
onSwitchChain={onSwitchChain}
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
@@ -172,7 +189,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
)
}
export function WidgetSkeleton() {
export function WidgetSkeleton({ width = DEFAULT_WIDGET_WIDTH }: { width?: number | string }) {
const theme = useWidgetTheme()
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
return <SwapWidgetSkeleton theme={theme} width={width} />
}

View File

@@ -8,10 +8,11 @@ const EMPTY_AMOUNT = ''
type SwapValue = Required<SwapController>['value']
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
export type DefaultTokens = Partial<SwapTokens>
function includesDefaultToken(tokens: SwapTokens) {
if (!tokens.default) return true
return tokens[Field.INPUT]?.equals(tokens.default) || tokens[Field.OUTPUT]?.equals(tokens.default)
function missingDefaultToken(tokens: SwapTokens) {
if (!tokens.default) return false
return !tokens[Field.INPUT]?.equals(tokens.default) && !tokens[Field.OUTPUT]?.equals(tokens.default)
}
/**
@@ -20,27 +21,31 @@ function includesDefaultToken(tokens: SwapTokens) {
* Enforces that token is a part of the returned value.
*/
export function useSyncWidgetInputs({
token,
onTokenChange,
defaultTokens,
onDefaultTokenChange,
}: {
token?: Currency
onTokenChange?: (token: Currency) => void
defaultTokens: DefaultTokens
onDefaultTokenChange?: (token: Currency) => void
}) {
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: token, default: token })
const [tokens, setTokens] = useState<SwapTokens>(defaultTokens)
useEffect(() => {
setTokens((tokens) => {
const update = { ...tokens, default: token }
if (!includesDefaultToken(update)) {
return { [Field.OUTPUT]: update.default, default: update.default }
}
return update
})
}, [token])
if (!tokens[Field.INPUT] && !tokens[Field.OUTPUT]) {
setTokens((tokens) => {
const update = {
...tokens,
[Field.INPUT]: defaultTokens[Field.INPUT] ?? tokens[Field.INPUT],
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? tokens[Field.OUTPUT] ?? defaultTokens.default,
default: defaultTokens.default,
}
return update
})
}
}, [defaultTokens, tokens])
const onAmountChange = useCallback(
(field: Field, amount: string, origin?: 'max') => {
@@ -96,13 +101,14 @@ export function useSyncWidgetInputs({
return type
})
if (!includesDefaultToken(update)) {
onTokenChange?.(update[Field.OUTPUT] || selectingToken)
if (missingDefaultToken(update)) {
onDefaultTokenChange?.(update[Field.OUTPUT] ?? selectingToken)
}
setTokens(update)
},
[onTokenChange, selectingField, tokens]
[onDefaultTokenChange, selectingField, tokens]
)
const tokenSelector = (
<CurrencySearchModal
isOpen={selectingField !== undefined}
@@ -110,6 +116,7 @@ export function useSyncWidgetInputs({
selectedCurrency={selectingField && tokens[selectingField]}
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
onCurrencySelect={onTokenSelect}
showCommonBases
/>
)
@@ -117,11 +124,11 @@ export function useSyncWidgetInputs({
() => ({
type,
amount,
// If the default has not yet been handled, preemptively disable the widget by passing no tokens. Effectively,
// If the initial state has not yet been set, preemptively disable the widget by passing no tokens. Effectively,
// this resets the widget - avoiding rendering stale state - because with no tokens the skeleton will be rendered.
...(token && tokens.default?.equals(token) ? tokens : undefined),
...(tokens[Field.INPUT] || tokens[Field.OUTPUT] ? tokens : undefined),
}),
[amount, token, tokens, type]
[amount, tokens, type]
)
const valueHandlers: SwapEventHandlers = useMemo(
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),

View File

@@ -1,5 +1,5 @@
import { Percent } from '@uniswap/sdk-core'
import { Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
import { RouterPreference, Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
import { useCallback, useMemo, useState } from 'react'
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
@@ -37,6 +37,8 @@ export function useSyncWidgetSettings() {
[setAppSlippage]
)
const [routerPreference, onRouterPreferenceChange] = useState(RouterPreference.API)
const onSettingsReset = useCallback(() => {
setWidgetTtl(undefined)
setAppTtl(DEFAULT_DEADLINE_FROM_NOW)
@@ -46,11 +48,15 @@ export function useSyncWidgetSettings() {
const settings: SwapController['settings'] = useMemo(() => {
const auto = appSlippage === 'auto'
return { slippage: { auto, max: widgetSlippage }, transactionTtl: widgetTtl }
}, [widgetSlippage, widgetTtl, appSlippage])
return {
slippage: { auto, max: widgetSlippage },
transactionTtl: widgetTtl,
routerPreference,
}
}, [appSlippage, widgetSlippage, widgetTtl, routerPreference])
const settingsHandlers: SwapEventHandlers = useMemo(
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange }),
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange]
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange }),
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange]
)
return { settings: { settings, ...settingsHandlers } }

View File

@@ -1,45 +1,68 @@
import { Theme } from '@uniswap/widgets'
import { darkTheme, lightTheme } from 'theme/colors'
import { Z_INDEX } from 'theme/zIndex'
const zIndex = {
modal: Z_INDEX.modal,
}
const fonts = {
fontFamily: 'Inter custom',
}
export const LIGHT_THEME = {
export const LIGHT_THEME: Theme = {
// surface
container: lightTheme.backgroundSurface,
interactive: lightTheme.backgroundInteractive,
module: lightTheme.backgroundModule,
accent: lightTheme.accentAction,
dialog: lightTheme.backgroundBackdrop,
accentSoft: lightTheme.accentActionSoft,
container: lightTheme.backgroundSurface,
module: lightTheme.backgroundModule,
interactive: lightTheme.backgroundInteractive,
outline: lightTheme.backgroundOutline,
dialog: lightTheme.backgroundBackdrop,
scrim: lightTheme.backgroundScrim,
// text
onAccent: lightTheme.white,
primary: lightTheme.textPrimary,
secondary: lightTheme.textSecondary,
hint: lightTheme.textTertiary,
onInteractive: lightTheme.accentTextDarkPrimary,
// shadow
deepShadow: lightTheme.deepShadow,
networkDefaultShadow: lightTheme.networkDefaultShadow,
// state
success: lightTheme.accentSuccess,
warning: lightTheme.accentWarning,
error: lightTheme.accentCritical,
...fonts,
zIndex,
}
export const DARK_THEME = {
export const DARK_THEME: Theme = {
// surface
container: darkTheme.backgroundSurface,
interactive: darkTheme.backgroundInteractive,
module: darkTheme.backgroundModule,
accent: darkTheme.accentAction,
dialog: darkTheme.backgroundBackdrop,
accentSoft: darkTheme.accentActionSoft,
container: darkTheme.backgroundSurface,
module: darkTheme.backgroundModule,
interactive: darkTheme.backgroundInteractive,
outline: darkTheme.backgroundOutline,
dialog: darkTheme.backgroundBackdrop,
scrim: darkTheme.backgroundScrim,
// text
onAccent: darkTheme.white,
primary: darkTheme.textPrimary,
secondary: darkTheme.textSecondary,
hint: darkTheme.textTertiary,
onInteractive: darkTheme.accentTextLightPrimary,
// shadow
deepShadow: darkTheme.deepShadow,
networkDefaultShadow: darkTheme.networkDefaultShadow,
// state
success: darkTheme.accentSuccess,
warning: darkTheme.accentWarning,
error: darkTheme.accentCritical,
...fonts,
zIndex,
}

View File

@@ -1,15 +1,23 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceEventName, InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core'
import {
OnTxSuccess,
TradeType,
Transaction,
TransactionEventHandlers,
TransactionInfo,
TransactionType,
TransactionType as WidgetTransactionType,
} from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { WrapType } from 'hooks/useWrapCallback'
import { formatSwapSignedAnalyticsEventProperties, formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
import {
formatPercentInBasisPointsNumber,
formatSwapSignedAnalyticsEventProperties,
formatToDecimal,
getTokenAddress,
} from 'lib/utils/analytics'
import { useCallback, useMemo } from 'react'
import { useTransactionAdder } from 'state/transactions/hooks'
import {
@@ -19,6 +27,42 @@ import {
WrapTransactionInfo,
} from 'state/transactions/types'
import { currencyId } from 'utils/currencyId'
import { computeRealizedPriceImpact } from 'utils/prices'
interface AnalyticsEventProps {
trade: Trade<Currency, Currency, TradeType>
gasUsed: string | undefined
blockNumber: number | undefined
hash: string | undefined
allowedSlippage: Percent
succeeded: boolean
}
const formatAnalyticsEventProperties = ({
trade,
hash,
allowedSlippage,
succeeded,
gasUsed,
blockNumber,
}: AnalyticsEventProps) => ({
estimated_network_fee_usd: gasUsed,
transaction_hash: hash,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
swap_quote_block_number: blockNumber,
succeeded,
})
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
export function useSyncWidgetTransactions() {
@@ -46,7 +90,7 @@ export function useSyncWidgetTransactions() {
amount: transactionAmount
? formatToDecimal(transactionAmount, transactionAmount?.currency.decimals)
: undefined,
type: type === WidgetTransactionType.WRAP ? WrapType.WRAP : WrapType.UNWRAP,
type: type === WidgetTransactionType.WRAP ? TransactionType.WRAP : TransactionType.UNWRAP,
...trace,
}
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, eventProperties)
@@ -94,7 +138,24 @@ export function useSyncWidgetTransactions() {
[addTransaction, chainId, trace]
)
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
const onTxSuccess: OnTxSuccess = useCallback((hash: string, tx) => {
if (tx.info.type === TransactionType.SWAP) {
const { trade, slippageTolerance } = tx.info
sendAnalyticsEvent(
SwapEventName.SWAP_TRANSACTION_COMPLETED,
formatAnalyticsEventProperties({
trade,
hash,
gasUsed: tx.receipt?.gasUsed?.toString(),
blockNumber: tx.receipt?.blockNumber,
allowedSlippage: slippageTolerance,
succeeded: tx.receipt?.status === 1,
})
)
}
}, [])
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit, onTxSuccess }), [onTxSubmit, onTxSuccess])
return { transactions: { ...txHandlers } }
}

View File

@@ -42,7 +42,7 @@ interface L1ChainInfo extends BaseChainInfo {
readonly defaultListUrl?: string
}
interface L2ChainInfo extends BaseChainInfo {
export interface L2ChainInfo extends BaseChainInfo {
readonly networkType: NetworkType.L2
readonly bridge: string
readonly statusPage?: string

View File

@@ -20,7 +20,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
},
}
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
// in PoS, ethereum block time is 12s, see https://ethereum.org/en/developers/docs/blocks/#block-time
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 12
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {

View File

@@ -8,7 +8,6 @@ const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
const KLEROS_LIST = 't2crtokens.eth'
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
@@ -30,7 +29,6 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
GEMINI_LIST,
WRAPPED_LIST,
SET_LIST,
ROLL_LIST,
ARBITRUM_LIST,
OPTIMISM_LIST,
CELO_LIST,

View File

@@ -1,4 +1,6 @@
import { Plural, Trans } from '@lingui/macro'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens'
@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) {
return BlockedWarning
}
}
// TODO(cartcrom): Replace all usage of WARNING_LEVEL with SafetyLevel
export function checkSearchTokenWarning(token: SearchToken) {
if (!token.address) {
return token.standard === TokenStandard.Native ? null : StrongWarning
}
return checkWarning(token.address)
}

View File

@@ -4,4 +4,6 @@ export enum FeatureFlag {
permit2 = 'permit2',
nftListV2 = 'nftListV2',
payWithAnyToken = 'payWithAnyToken',
swapWidget = 'swapWidget',
gqlRouting = 'gqlRouting',
}

View File

@@ -0,0 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useGqlRoutingFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.gqlRouting)
}
export { BaseVariant as GqlRoutingVariant }

View File

@@ -1,7 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
import { BaseVariant } from '../index'
export function useNftListV2Flag(): BaseVariant {
return useBaseFlag(FeatureFlag.nftListV2)
return BaseVariant.Enabled
}
export { BaseVariant as NftListV2Variant }

View File

@@ -1,7 +1,24 @@
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function usePayWithAnyTokenFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.payWithAnyToken)
}
export function usePayWithAnyTokenEnabled(): boolean {
const flagEnabled = usePayWithAnyTokenFlag() === BaseVariant.Enabled
const { chainId } = useWeb3React()
try {
// Detect if the Universal Router is not yet deployed to chainId.
// This is necessary so that we can fallback correctly on chains without a Universal Router deployment.
// It will be removed once Universal Router is deployed on all supported chains.
chainId && UNIVERSAL_ROUTER_ADDRESS(chainId)
return flagEnabled
} catch {
return false
}
}
export { BaseVariant as PayWithAnyTokenVariant }

View File

@@ -0,0 +1,11 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useSwapWidgetFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.swapWidget, BaseVariant.Control)
}
export function useSwapWidgetEnabled(): boolean {
return useSwapWidgetFlag() === BaseVariant.Enabled
}
export { BaseVariant as SwapWidgetVariant }

View File

@@ -0,0 +1,59 @@
import gql from 'graphql-tag'
gql`
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
nftCollections(filter: { addresses: $collectionAddresses }) {
edges {
node {
collectionId
image {
url
}
isVerified
name
numAssets
nftContracts {
address
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
}
tokens(contracts: $contracts) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`

View File

@@ -0,0 +1,102 @@
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { Chain, SearchTokensQuery, useSearchTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName } from './util'
gql`
query SearchTokens($searchQuery: String!) {
searchTokens(searchQuery: $searchQuery) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
export type SearchToken = NonNullable<NonNullable<SearchTokensQuery['searchTokens']>[number]>
function isMoreRevelantToken(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
if (!existing) return true
// Always priotize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return true
// Prefer tokens on the searched chain, otherwise prefer mainnet tokens
return current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum)
}
// Places natives first, wrapped native on current chain next, then sorts by volume
function searchTokenSortFunction(
searchChain: Chain,
wrappedNativeAddress: string | undefined,
a: SearchToken,
b: SearchToken
) {
if (a.standard === 'NATIVE') {
if (b.standard === 'NATIVE') {
if (a.chain === searchChain) return -1
else if (b.chain === searchChain) return 1
else return 0
} else return -1
} else if (b.standard === 'NATIVE') return 1
else if (wrappedNativeAddress && a.address === wrappedNativeAddress) return -1
else if (wrappedNativeAddress && b.address === wrappedNativeAddress) return 1
else return (b.market?.volume24H?.value ?? 0) - (a.market?.volume24H?.value ?? 0)
}
export function useSearchTokens(searchQuery: string, chainId: number) {
const { data, loading, error } = useSearchTokensQuery({
variables: {
searchQuery,
},
})
const sortedTokens = useMemo(() => {
const searchChain = chainIdToBackendName(chainId)
// Stores results, allowing overwriting cross-chain tokens w/ more 'relevant token'
const selectionMap: { [projectId: string]: SearchToken } = {}
data?.searchTokens?.forEach((token) => {
if (token.project?.id) {
const existing = selectionMap[token.project.id]
if (isMoreRevelantToken(token, existing, searchChain)) selectionMap[token.project.id] = token
}
})
return Object.values(selectionMap).sort(
searchTokenSortFunction.bind(null, searchChain, WRAPPED_NATIVE_CURRENCY[chainId]?.address)
)
}, [data, chainId])
return {
data: sortedTokens,
loading,
error,
}
}

View File

@@ -14,40 +14,49 @@ The difference between Token and TokenProject:
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/
gql`
query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) {
query Token($chain: Chain!, $address: String = null) {
token(chain: $chain, address: $address) {
id
decimals
name
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
volume24H: volume(duration: DAY) {
id
value
currency
}
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
id
value
}
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
id
value
}
}
project {
id
description
homepageUrl
twitterName
logoUrl
tokens {
id
chain
address
}
@@ -58,7 +67,7 @@ gql`
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
export type TokenQueryData = TokenQuery['token']
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo {

View File

@@ -1,14 +1,19 @@
import gql from 'graphql-tag'
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) {
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
token(chain: $chain, address: $address) {
id
address
chain
market(currency: USD) {
id
price {
id
value
}
priceHistory(duration: $duration) {
id
timestamp
value
}

View File

@@ -15,7 +15,15 @@ import {
useTopTokens100Query,
useTopTokensSparklineQuery,
} from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
import {
CHAIN_NAME_TO_CHAIN_ID,
isPricePoint,
PollingInterval,
PricePoint,
toHistoryDuration,
unwrapToken,
usePollQueryWhileMounted,
} from './util'
gql`
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
@@ -25,25 +33,32 @@ gql`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
pricePercentChange(duration: $duration) {
id
currency
value
}
volume(duration: $duration) {
id
value
currency
}
}
project {
id
logoUrl
}
}
@@ -53,9 +68,13 @@ gql`
gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id
address
chain
market(currency: USD) {
id
priceHistory(duration: $duration) {
id
timestamp
value
}
@@ -64,11 +83,12 @@ gql`
}
`
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
return useMemo(() => {
if (!tokens) return undefined
let tokenArray = Array.from(tokens)
switch (sortMethod) {
case TokenSortMethod.PRICE:
@@ -93,12 +113,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
}, [tokens, sortMethod, sortAscending])
}
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
const filterString = useAtomValue(filterStringAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
return useMemo(() => {
if (!tokens) return undefined
let returnTokens = tokens
if (lowercaseFilterString) {
returnTokens = returnTokens?.filter((token) => {
@@ -119,7 +140,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
interface UseTopTokensReturnValue {
tokens: TopToken[] | undefined
tokenVolumeRank: Record<string, number>
tokenSortRank: Record<string, number>
loadingTokens: boolean
sparklines: SparklineMap
}
@@ -128,9 +149,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const { data: sparklineQuery } = useTopTokensSparklineQuery({
variables: { duration, chain },
})
const { data: sparklineQuery } = usePollQueryWhileMounted(
useTopTokensSparklineQuery({
variables: { duration, chain },
}),
PollingInterval.Slow
)
const sparklines = useMemo(() => {
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
@@ -141,33 +165,29 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
return map
}, [chainId, sparklineQuery?.topTokens])
const { data, loading: loadingTokens } = useTopTokens100Query({
variables: { duration, chain },
})
const unwrappedTokens = useMemo(
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
[chainId, data]
const { data, loading: loadingTokens } = usePollQueryWhileMounted(
useTopTokens100Query({
variables: { duration, chain },
}),
PollingInterval.Fast
)
const tokenVolumeRank = useMemo(
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
const sortedTokens = useSortedTokens(unwrappedTokens)
const tokenSortRank = useMemo(
() =>
unwrappedTokens
.sort((a, b) => {
if (!a.market?.volume || !b.market?.volume) return 0
return a.market.volume.value > b.market.volume.value ? -1 : 1
})
.reduce((acc, cur, i) => {
if (!cur.address) return acc
return {
...acc,
[cur.address]: i + 1,
}
}, {}),
[unwrappedTokens]
sortedTokens?.reduce((acc, cur, i) => {
if (!cur.address) return acc
return {
...acc,
[cur.address]: i + 1,
}
}, {}) ?? {},
[sortedTokens]
)
const filteredTokens = useFilteredTokens(unwrappedTokens)
const sortedTokens = useSortedTokens(filteredTokens)
const filteredTokens = useFilteredTokens(sortedTokens)
return useMemo(
() => ({ tokens: sortedTokens, tokenVolumeRank, loadingTokens, sparklines }),
[loadingTokens, tokenVolumeRank, sortedTokens, sparklines]
() => ({ tokens: filteredTokens, tokenSortRank, loadingTokens, sparklines }),
[filteredTokens, tokenSortRank, loadingTokens, sparklines]
)
}

View File

@@ -0,0 +1,51 @@
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { useTrendingTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName, unwrapToken } from './util'
gql`
query TrendingTokens($chain: Chain!) {
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
export default function useTrendingTokens(chainId?: number) {
const chain = chainIdToBackendName(chainId)
const { data, loading } = useTrendingTokensQuery({ variables: { chain } })
return useMemo(
() => ({ data: data?.topTokens?.map((token) => unwrapToken(chainId ?? 1, token)), loading }),
[chainId, data?.topTokens, loading]
)
}

View File

@@ -95,6 +95,11 @@ export enum Currency {
Usd = 'USD'
}
export enum DatasourceProvider {
Alternate = 'ALTERNATE',
Legacy = 'LEGACY'
}
export type Dimensions = {
__typename?: 'Dimensions';
height?: Maybe<Scalars['Float']>;
@@ -386,6 +391,7 @@ export type NftCollectionTraitStats = {
export type NftCollectionsFilterInput = {
addresses?: InputMaybe<Array<Scalars['String']>>;
nameQuery?: InputMaybe<Scalars['String']>;
};
export type NftContract = IContract & {
@@ -468,12 +474,45 @@ export enum NftRarityProvider {
RaritySniper = 'RARITY_SNIPER'
}
export type NftRouteResponse = {
__typename?: 'NftRouteResponse';
calldata: Scalars['String'];
id: Scalars['ID'];
route?: Maybe<Array<NftTrade>>;
sendAmount: TokenAmount;
toAddress: Scalars['String'];
};
export enum NftStandard {
Erc721 = 'ERC721',
Erc1155 = 'ERC1155',
Noncompliant = 'NONCOMPLIANT'
}
export type NftTrade = {
__typename?: 'NftTrade';
amount: Scalars['Int'];
contractAddress: Scalars['String'];
id: Scalars['ID'];
marketplace: NftMarketplace;
/** price represents the current price of the NFT, which can be different from quotePrice */
price: TokenAmount;
/** quotePrice represents the last quoted price of the NFT */
quotePrice?: Maybe<TokenAmount>;
tokenId: Scalars['String'];
tokenType: NftStandard;
};
export type NftTradeInput = {
amount: Scalars['Int'];
contractAddress: Scalars['String'];
id: Scalars['ID'];
marketplace: NftMarketplace;
quotePrice?: InputMaybe<TokenAmountInput>;
tokenId: Scalars['String'];
tokenType: NftStandard;
};
export type NftTransfer = {
__typename?: 'NftTransfer';
asset: NftAsset;
@@ -504,6 +543,36 @@ export type PageInfo = {
startCursor?: Maybe<Scalars['String']>;
};
/** v2 pool parameters as defined by https://github.com/Uniswap/v2-sdk/blob/main/src/entities/pair.ts */
export type PairInput = {
tokenAmountA: TokenAmountInput;
tokenAmountB: TokenAmountInput;
};
export type PermitDetailsInput = {
amount: Scalars['String'];
expiration: Scalars['String'];
nonce: Scalars['String'];
token: Scalars['String'];
};
export type PermitInput = {
details: PermitDetailsInput;
sigDeadline: Scalars['String'];
signature: Scalars['String'];
spender: Scalars['String'];
};
/** v3 pool parameters as defined by https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts */
export type PoolInput = {
fee: Scalars['Int'];
liquidity: Scalars['String'];
sqrtRatioX96: Scalars['String'];
tickCurrent: Scalars['String'];
tokenA: TokenInput;
tokenB: TokenInput;
};
export type Portfolio = {
__typename?: 'Portfolio';
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
@@ -529,11 +598,11 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
export type Query = {
__typename?: 'Query';
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
nftAssets?: Maybe<NftAssetConnection>;
nftBalances?: Maybe<NftBalanceConnection>;
nftCollections?: Maybe<NftCollectionConnection>;
nftCollectionsById?: Maybe<Array<Maybe<NftCollection>>>;
nftRoute?: Maybe<NftRouteResponse>;
portfolios?: Maybe<Array<Maybe<Portfolio>>>;
searchTokenProjects?: Maybe<Array<Maybe<TokenProject>>>;
searchTokens?: Maybe<Array<Maybe<Token>>>;
@@ -544,13 +613,6 @@ export type Query = {
};
export type QueryAssetActivitiesArgs = {
address: Scalars['String'];
page?: InputMaybe<Scalars['Int']>;
pageSize?: InputMaybe<Scalars['Int']>;
};
export type QueryNftAssetsArgs = {
address: Scalars['String'];
after?: InputMaybe<Scalars['String']>;
@@ -589,6 +651,14 @@ export type QueryNftCollectionsByIdArgs = {
};
export type QueryNftRouteArgs = {
chain?: InputMaybe<Chain>;
nftTrades: Array<NftTradeInput>;
senderAddress: Scalars['String'];
tokenTrades?: InputMaybe<Array<TokenTradeInput>>;
};
export type QueryPortfoliosArgs = {
ownerAddresses: Array<Scalars['String']>;
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
@@ -661,6 +731,18 @@ export type TokenMarketArgs = {
currency?: InputMaybe<Currency>;
};
export type TokenAmount = {
__typename?: 'TokenAmount';
currency: Currency;
id: Scalars['ID'];
value: Scalars['String'];
};
export type TokenAmountInput = {
amount: Scalars['String'];
token: TokenInput;
};
export type TokenApproval = {
__typename?: 'TokenApproval';
approvedAddress: Scalars['String'];
@@ -683,6 +765,13 @@ export type TokenBalance = {
tokenProjectMarket?: Maybe<TokenProjectMarket>;
};
export type TokenInput = {
address: Scalars['String'];
chainId: Scalars['Int'];
decimals: Scalars['Int'];
isNative: Scalars['Boolean'];
};
export type TokenMarket = {
__typename?: 'TokenMarket';
id: Scalars['ID'];
@@ -788,6 +877,31 @@ export enum TokenStandard {
Native = 'NATIVE'
}
export type TokenTradeInput = {
permit?: InputMaybe<PermitInput>;
routes?: InputMaybe<TokenTradeRoutesInput>;
slippageToleranceBasisPoints?: InputMaybe<Scalars['Int']>;
tokenAmount: TokenAmountInput;
};
export type TokenTradeRouteInput = {
inputAmount: TokenAmountInput;
outputAmount: TokenAmountInput;
pools: Array<TradePoolInput>;
};
export type TokenTradeRoutesInput = {
mixedRoutes?: InputMaybe<Array<TokenTradeRouteInput>>;
tradeType: TokenTradeType;
v2Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
v3Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
};
export enum TokenTradeType {
ExactInput = 'EXACT_INPUT',
ExactOutput = 'EXACT_OUTPUT'
}
export type TokenTransfer = {
__typename?: 'TokenTransfer';
asset: Token;
@@ -800,6 +914,11 @@ export type TokenTransfer = {
transactedValue?: Maybe<Amount>;
};
export type TradePoolInput = {
pair?: InputMaybe<PairInput>;
pool?: InputMaybe<PoolInput>;
};
export type Transaction = {
__typename?: 'Transaction';
blockNumber: Scalars['Int'];
@@ -825,20 +944,37 @@ export enum TransactionStatus {
Pending = 'PENDING'
}
export type TokenQueryVariables = Exact<{
contract: ContractInput;
export type RecentlySearchedAssetsQueryVariables = Exact<{
collectionAddresses: Array<Scalars['String']> | Scalars['String'];
contracts: Array<ContractInput> | ContractInput;
}>;
export type TokenQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', value: number }, priceLow52W?: { __typename?: 'Amount', value: number } }, project?: { __typename?: 'TokenProject', description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', chain: Chain, address?: string }> } }> };
export type RecentlySearchedAssetsQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', collectionId: string, isVerified?: boolean, name?: string, numAssets?: number, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number } }> } }> }, tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type SearchTokensQueryVariables = Exact<{
searchQuery: Scalars['String'];
}>;
export type SearchTokensQuery = { __typename?: 'Query', searchTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type TokenQueryVariables = Exact<{
chain: Chain;
address?: InputMaybe<Scalars['String']>;
}>;
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
export type TokenPriceQueryVariables = Exact<{
contract: ContractInput;
chain: Chain;
address?: InputMaybe<Scalars['String']>;
duration: HistoryDuration;
}>;
export type TokenPriceQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', market?: { __typename?: 'TokenMarket', price?: { __typename?: 'Amount', value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
export type TokenPriceQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } } };
export type TopTokens100QueryVariables = Exact<{
duration: HistoryDuration;
@@ -846,7 +982,7 @@ export type TopTokens100QueryVariables = Exact<{
}>;
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', currency?: Currency, value: number }, volume?: { __typename?: 'Amount', value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', logoUrl?: string } }> };
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
export type TopTokensSparklineQueryVariables = Exact<{
duration: HistoryDuration;
@@ -854,7 +990,14 @@ export type TopTokensSparklineQueryVariables = Exact<{
}>;
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', address?: string, market?: { __typename?: 'TokenMarket', priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
export type TrendingTokensQueryVariables = Exact<{
chain: Chain;
}>;
export type TrendingTokensQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type AssetQueryVariables = Exact<{
address: Scalars['String'];
@@ -895,44 +1038,212 @@ export type NftBalanceQueryVariables = Exact<{
}>;
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type NftRouteQueryVariables = Exact<{
chain?: InputMaybe<Chain>;
senderAddress: Scalars['String'];
nftTrades: Array<NftTradeInput> | NftTradeInput;
tokenTrades?: InputMaybe<Array<TokenTradeInput> | TokenTradeInput>;
}>;
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', currency: Currency, value: string } } };
export const RecentlySearchedAssetsDocument = gql`
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
nftCollections(filter: {addresses: $collectionAddresses}) {
edges {
node {
collectionId
image {
url
}
isVerified
name
numAssets
nftContracts {
address
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
}
tokens(contracts: $contracts) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`;
/**
* __useRecentlySearchedAssetsQuery__
*
* To run a query within a React component, call `useRecentlySearchedAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `useRecentlySearchedAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useRecentlySearchedAssetsQuery({
* variables: {
* collectionAddresses: // value for 'collectionAddresses'
* contracts: // value for 'contracts'
* },
* });
*/
export function useRecentlySearchedAssetsQuery(baseOptions: Apollo.QueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
}
export function useRecentlySearchedAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
}
export type RecentlySearchedAssetsQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsQuery>;
export type RecentlySearchedAssetsLazyQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsLazyQuery>;
export type RecentlySearchedAssetsQueryResult = Apollo.QueryResult<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>;
export const SearchTokensDocument = gql`
query SearchTokens($searchQuery: String!) {
searchTokens(searchQuery: $searchQuery) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`;
/**
* __useSearchTokensQuery__
*
* To run a query within a React component, call `useSearchTokensQuery` and pass it any options that fit your needs.
* When your component renders, `useSearchTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSearchTokensQuery({
* variables: {
* searchQuery: // value for 'searchQuery'
* },
* });
*/
export function useSearchTokensQuery(baseOptions: Apollo.QueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
}
export function useSearchTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
}
export type SearchTokensQueryHookResult = ReturnType<typeof useSearchTokensQuery>;
export type SearchTokensLazyQueryHookResult = ReturnType<typeof useSearchTokensLazyQuery>;
export type SearchTokensQueryResult = Apollo.QueryResult<SearchTokensQuery, SearchTokensQueryVariables>;
export const TokenDocument = gql`
query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) {
query Token($chain: Chain!, $address: String = null) {
token(chain: $chain, address: $address) {
id
decimals
name
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
volume24H: volume(duration: DAY) {
id
value
currency
}
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
id
value
}
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
id
value
}
}
project {
id
description
homepageUrl
twitterName
logoUrl
tokens {
id
chain
address
}
@@ -953,7 +1264,8 @@ export const TokenDocument = gql`
* @example
* const { data, loading, error } = useTokenQuery({
* variables: {
* contract: // value for 'contract'
* chain: // value for 'chain'
* address: // value for 'address'
* },
* });
*/
@@ -969,13 +1281,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>;
export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>;
export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>;
export const TokenPriceDocument = gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) {
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
token(chain: $chain, address: $address) {
id
address
chain
market(currency: USD) {
id
price {
id
value
}
priceHistory(duration: $duration) {
id
timestamp
value
}
@@ -996,7 +1314,8 @@ export const TokenPriceDocument = gql`
* @example
* const { data, loading, error } = useTokenPriceQuery({
* variables: {
* contract: // value for 'contract'
* chain: // value for 'chain'
* address: // value for 'address'
* duration: // value for 'duration'
* },
* });
@@ -1020,25 +1339,32 @@ export const TopTokens100Document = gql`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
pricePercentChange(duration: $duration) {
id
currency
value
}
volume(duration: $duration) {
id
value
currency
}
}
project {
id
logoUrl
}
}
@@ -1076,9 +1402,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT
export const TopTokensSparklineDocument = gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id
address
chain
market(currency: USD) {
id
priceHistory(duration: $duration) {
id
timestamp
value
}
@@ -1115,6 +1445,69 @@ export function useTopTokensSparklineLazyQuery(baseOptions?: Apollo.LazyQueryHoo
export type TopTokensSparklineQueryHookResult = ReturnType<typeof useTopTokensSparklineQuery>;
export type TopTokensSparklineLazyQueryHookResult = ReturnType<typeof useTopTokensSparklineLazyQuery>;
export type TopTokensSparklineQueryResult = Apollo.QueryResult<TopTokensSparklineQuery, TopTokensSparklineQueryVariables>;
export const TrendingTokensDocument = gql`
query TrendingTokens($chain: Chain!) {
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`;
/**
* __useTrendingTokensQuery__
*
* To run a query within a React component, call `useTrendingTokensQuery` and pass it any options that fit your needs.
* When your component renders, `useTrendingTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTrendingTokensQuery({
* variables: {
* chain: // value for 'chain'
* },
* });
*/
export function useTrendingTokensQuery(baseOptions: Apollo.QueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
}
export function useTrendingTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
}
export type TrendingTokensQueryHookResult = ReturnType<typeof useTrendingTokensQuery>;
export type TrendingTokensLazyQueryHookResult = ReturnType<typeof useTrendingTokensLazyQuery>;
export type TrendingTokensQueryResult = Apollo.QueryResult<TrendingTokensQuery, TrendingTokensQueryVariables>;
export const AssetDocument = gql`
query Asset($address: String!, $orderBy: NftAssetSortableField, $asc: Boolean, $filter: NftAssetsFilterInput, $first: Int, $after: String, $last: Int, $before: String) {
nftAssets(
@@ -1487,6 +1880,7 @@ export const NftBalanceDocument = gql`
url
}
name
twitterName
nftContracts {
address
chain
@@ -1586,4 +1980,68 @@ export function useNftBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
}
export type NftBalanceQueryHookResult = ReturnType<typeof useNftBalanceQuery>;
export type NftBalanceLazyQueryHookResult = ReturnType<typeof useNftBalanceLazyQuery>;
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
export const NftRouteDocument = gql`
query NftRoute($chain: Chain = ETHEREUM, $senderAddress: String!, $nftTrades: [NftTradeInput!]!, $tokenTrades: [TokenTradeInput!]) {
nftRoute(
chain: $chain
senderAddress: $senderAddress
nftTrades: $nftTrades
tokenTrades: $tokenTrades
) {
calldata
route {
amount
contractAddress
id
marketplace
price {
currency
value
}
quotePrice {
currency
value
}
tokenId
tokenType
}
sendAmount {
currency
value
}
toAddress
}
}
`;
/**
* __useNftRouteQuery__
*
* To run a query within a React component, call `useNftRouteQuery` and pass it any options that fit your needs.
* When your component renders, `useNftRouteQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useNftRouteQuery({
* variables: {
* chain: // value for 'chain'
* senderAddress: // value for 'senderAddress'
* nftTrades: // value for 'nftTrades'
* tokenTrades: // value for 'tokenTrades'
* },
* });
*/
export function useNftRouteQuery(baseOptions: Apollo.QueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
}
export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
}
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;

View File

@@ -1,5 +1,5 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { relayStylePagination } from '@apollo/client/utilities'
import { Reference, relayStylePagination } from '@apollo/client/utilities'
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
if (!GRAPHQL_URL) {
@@ -7,6 +7,7 @@ if (!GRAPHQL_URL) {
}
export const apolloClient = new ApolloClient({
connectToDevTools: true,
uri: GRAPHQL_URL,
headers: {
'Content-Type': 'application/json',
@@ -18,6 +19,33 @@ export const apolloClient = new ApolloClient({
fields: {
nftBalances: relayStylePagination(),
nftAssets: relayStylePagination(),
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
token: {
read(_, { args, toReference }): Reference | undefined {
return toReference({
__typename: 'Token',
chain: args?.chain,
address: args?.address,
})
},
},
},
},
Token: {
// key by chain, address combination so that Token(chain, address) endpoint can read from cache
/**
* NOTE: In any query for `token` or `tokens`, you must include the `chain` and `address` fields
* in order for result to normalize properly in the cache.
*/
keyFields: ['chain', 'address'],
fields: {
address: {
read(address: string | null): string | null {
// backend endpoint sometimes returns checksummed, sometimes lowercased addresses
// always use lowercased addresses in our app for consistency
return address?.toLowerCase() ?? null
},
},
},
},
},

View File

@@ -34,6 +34,7 @@ gql`
url
}
name
twitterName
nftContracts {
address
chain
@@ -166,7 +167,12 @@ export function useNftBalance(
image_url: asset?.collection?.image?.url,
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
},
collection: asset?.collection as unknown as GenieCollection,
collection: {
name: asset.collection?.name,
isVerified: asset.collection?.isVerified,
imageUrl: asset.collection?.image?.url,
twitterUrl: asset.collection?.twitterName ? `@${asset.collection?.twitterName}` : undefined,
} as GenieCollection,
collectionIsVerified: asset?.collection?.isVerified,
lastPrice: queryAsset.node.lastPrice?.value,
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,

View File

@@ -0,0 +1,47 @@
import gql from 'graphql-tag'
import { NftTradeInput, TokenTradeInput, useNftRouteQuery } from '../__generated__/types-and-hooks'
gql`
query NftRoute(
$chain: Chain = ETHEREUM
$senderAddress: String!
$nftTrades: [NftTradeInput!]!
$tokenTrades: [TokenTradeInput!]
) {
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
calldata
route {
amount
contractAddress
id
marketplace
price {
currency
value
}
quotePrice {
currency
value
}
tokenId
tokenType
}
sendAmount {
currency
value
}
toAddress
}
}
`
export function useNftRoute(senderAddress: string, nftTrades: NftTradeInput[], tokenTrades?: TokenTradeInput[]) {
return useNftRouteQuery({
variables: {
senderAddress,
nftTrades,
tokenTrades,
},
})
}

View File

@@ -1,9 +1,30 @@
import { QueryResult } from '@apollo/client'
import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import ms from 'ms.macro'
import { useEffect } from 'react'
import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
export enum PollingInterval {
Slow = ms`5m`,
Normal = ms`1m`,
Fast = ms`12s`, // 12 seconds, block times for mainnet
LightningMcQueen = ms`3s`, // 3 seconds, approx block times for polygon
}
// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount
export function usePollQueryWhileMounted<T, K>(queryResult: QueryResult<T, K>, interval: PollingInterval) {
const { startPolling, stopPolling } = queryResult
useEffect(() => {
startPolling(interval)
return stopPolling
}, [interval, startPolling, stopPolling])
return queryResult
}
export enum TimePeriod {
HOUR,
DAY,
@@ -74,17 +95,8 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?: number) {
if (address === ZERO_ADDRESS && chainId && chainId === SupportedChainId.MAINNET) {
return `/tokens/${CHAIN_ID_TO_BACKEND_NAME[chainId].toLowerCase()}/${NATIVE_CHAIN_ID}`
} else if (chainName) {
return `/tokens/${chainName.toLowerCase()}/${address}`
} else if (chainId) {
const chainName = CHAIN_ID_TO_BACKEND_NAME[chainId]
return chainName ? `/tokens/${chainName.toLowerCase()}/${address}` : ''
} else {
return ''
}
export function getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
}
export function unwrapToken<

View File

@@ -37,6 +37,7 @@ export type Bundle = {
export type Bundle_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
ethPriceUSD?: InputMaybe<Scalars['BigDecimal']>;
ethPriceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
ethPriceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -53,6 +54,7 @@ export type Bundle_Filter = {
id_lte?: InputMaybe<Scalars['ID']>;
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
or?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
};
export enum Bundle_OrderBy {
@@ -114,6 +116,7 @@ export type Burn_Filter = {
amount_lte?: InputMaybe<Scalars['BigInt']>;
amount_not?: InputMaybe<Scalars['BigInt']>;
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
and?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -130,6 +133,7 @@ export type Burn_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
origin?: InputMaybe<Scalars['Bytes']>;
origin_contains?: InputMaybe<Scalars['Bytes']>;
origin_gt?: InputMaybe<Scalars['Bytes']>;
@@ -320,6 +324,7 @@ export type Collect_Filter = {
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
and?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -336,6 +341,7 @@ export type Collect_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
@@ -448,6 +454,7 @@ export type Factory = {
export type Factory_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -456,6 +463,7 @@ export type Factory_Filter = {
id_lte?: InputMaybe<Scalars['ID']>;
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
or?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
owner?: InputMaybe<Scalars['ID']>;
owner_gt?: InputMaybe<Scalars['ID']>;
owner_gte?: InputMaybe<Scalars['ID']>;
@@ -629,6 +637,7 @@ export type Flash_Filter = {
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
and?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -645,6 +654,7 @@ export type Flash_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
pool_?: InputMaybe<Pool_Filter>;
pool_contains?: InputMaybe<Scalars['String']>;
@@ -787,6 +797,7 @@ export type Mint_Filter = {
amount_lte?: InputMaybe<Scalars['BigInt']>;
amount_not?: InputMaybe<Scalars['BigInt']>;
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
and?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -803,6 +814,7 @@ export type Mint_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
origin?: InputMaybe<Scalars['Bytes']>;
origin_contains?: InputMaybe<Scalars['Bytes']>;
origin_gt?: InputMaybe<Scalars['Bytes']>;
@@ -1098,6 +1110,7 @@ export type PoolDayData = {
export type PoolDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -1178,6 +1191,7 @@ export type PoolDayData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
pool_?: InputMaybe<Pool_Filter>;
pool_contains?: InputMaybe<Scalars['String']>;
@@ -1323,6 +1337,7 @@ export type PoolHourData = {
export type PoolHourData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -1395,6 +1410,7 @@ export type PoolHourData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
periodStartUnix?: InputMaybe<Scalars['Int']>;
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
@@ -1524,6 +1540,7 @@ export enum PoolHourData_OrderBy {
export type Pool_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
burns_?: InputMaybe<Burn_Filter>;
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
@@ -1631,6 +1648,7 @@ export type Pool_Filter = {
observationIndex_lte?: InputMaybe<Scalars['BigInt']>;
observationIndex_not?: InputMaybe<Scalars['BigInt']>;
observationIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
poolDayData_?: InputMaybe<PoolDayData_Filter>;
poolHourData_?: InputMaybe<PoolHourData_Filter>;
sqrtPrice?: InputMaybe<Scalars['BigInt']>;
@@ -1874,6 +1892,7 @@ export type PositionSnapshot = {
export type PositionSnapshot_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
blockNumber?: InputMaybe<Scalars['BigInt']>;
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
@@ -1946,6 +1965,7 @@ export type PositionSnapshot_Filter = {
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
liquidity_not?: InputMaybe<Scalars['BigInt']>;
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
@@ -2067,6 +2087,7 @@ export enum PositionSnapshot_OrderBy {
export type Position_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -2131,6 +2152,7 @@ export type Position_Filter = {
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
liquidity_not?: InputMaybe<Scalars['BigInt']>;
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
@@ -3173,6 +3195,7 @@ export type Swap_Filter = {
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
and?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -3189,6 +3212,7 @@ export type Swap_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
origin?: InputMaybe<Scalars['Bytes']>;
origin_contains?: InputMaybe<Scalars['Bytes']>;
origin_gt?: InputMaybe<Scalars['Bytes']>;
@@ -3391,6 +3415,7 @@ export type TickDayData = {
export type TickDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
date?: InputMaybe<Scalars['Int']>;
date_gt?: InputMaybe<Scalars['Int']>;
date_gte?: InputMaybe<Scalars['Int']>;
@@ -3447,6 +3472,7 @@ export type TickDayData_Filter = {
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
pool_?: InputMaybe<Pool_Filter>;
pool_contains?: InputMaybe<Scalars['String']>;
@@ -3547,6 +3573,7 @@ export type TickHourData = {
export type TickHourData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
feesUSD?: InputMaybe<Scalars['BigDecimal']>;
feesUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
feesUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -3579,6 +3606,7 @@ export type TickHourData_Filter = {
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
periodStartUnix?: InputMaybe<Scalars['Int']>;
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
@@ -3671,6 +3699,7 @@ export enum TickHourData_OrderBy {
export type Tick_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -3767,6 +3796,7 @@ export type Tick_Filter = {
liquidityProviderCount_lte?: InputMaybe<Scalars['BigInt']>;
liquidityProviderCount_not?: InputMaybe<Scalars['BigInt']>;
liquidityProviderCount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
poolAddress?: InputMaybe<Scalars['String']>;
poolAddress_contains?: InputMaybe<Scalars['String']>;
@@ -3950,6 +3980,7 @@ export type TokenDayData = {
export type TokenDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -4006,6 +4037,7 @@ export type TokenDayData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
priceUSD?: InputMaybe<Scalars['BigDecimal']>;
priceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
priceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -4115,6 +4147,7 @@ export type TokenHourData = {
export type TokenHourData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -4163,6 +4196,7 @@ export type TokenHourData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
periodStartUnix?: InputMaybe<Scalars['Int']>;
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
@@ -4262,6 +4296,7 @@ export enum TokenHourData_OrderBy {
export type Token_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
decimals?: InputMaybe<Scalars['BigInt']>;
decimals_gt?: InputMaybe<Scalars['BigInt']>;
decimals_gte?: InputMaybe<Scalars['BigInt']>;
@@ -4314,6 +4349,7 @@ export type Token_Filter = {
name_not_starts_with_nocase?: InputMaybe<Scalars['String']>;
name_starts_with?: InputMaybe<Scalars['String']>;
name_starts_with_nocase?: InputMaybe<Scalars['String']>;
or?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
poolCount?: InputMaybe<Scalars['BigInt']>;
poolCount_gt?: InputMaybe<Scalars['BigInt']>;
poolCount_gte?: InputMaybe<Scalars['BigInt']>;
@@ -4498,6 +4534,7 @@ export type TransactionSwapsArgs = {
export type Transaction_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
blockNumber?: InputMaybe<Scalars['BigInt']>;
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
@@ -4534,6 +4571,7 @@ export type Transaction_Filter = {
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
mints_?: InputMaybe<Mint_Filter>;
or?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
swaps_?: InputMaybe<Swap_Filter>;
timestamp?: InputMaybe<Scalars['BigInt']>;
timestamp_gt?: InputMaybe<Scalars['BigInt']>;
@@ -4573,6 +4611,7 @@ export type UniswapDayData = {
export type UniswapDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
date?: InputMaybe<Scalars['Int']>;
date_gt?: InputMaybe<Scalars['Int']>;
date_gte?: InputMaybe<Scalars['Int']>;
@@ -4597,6 +4636,7 @@ export type UniswapDayData_Filter = {
id_lte?: InputMaybe<Scalars['ID']>;
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
or?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
tvlUSD?: InputMaybe<Scalars['BigDecimal']>;
tvlUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
tvlUSD_gte?: InputMaybe<Scalars['BigDecimal']>;

View File

@@ -9,30 +9,25 @@ import { useAppDispatch } from 'state/hooks'
import { fetchTokenList } from '../state/lists/actions'
export function useFetchListCallback(): (
listUrl: string,
sendDispatch?: boolean,
skipValidation?: boolean
) => Promise<TokenList> {
export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> {
const dispatch = useAppDispatch()
// note: prevent dispatch if using for list search or unsupported list
return useCallback(
async (listUrl: string, sendDispatch = true, skipValidation?: boolean) => {
async (listUrl: string, skipValidation?: boolean) => {
const requestId = nanoid()
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
return getTokenList(
listUrl,
(ensName: string) => resolveENSContentHash(ensName, RPC_PROVIDERS[SupportedChainId.MAINNET]),
skipValidation
)
.then((tokenList) => {
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
return tokenList
})
.catch((error) => {
console.debug(`Failed to get list at url ${listUrl}`, error)
sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
throw error
})
},

View File

@@ -0,0 +1,6 @@
import { useLocation } from 'react-router-dom'
export function useIsLandingPage() {
const { pathname } = useLocation()
return pathname.endsWith('/')
}

View File

@@ -0,0 +1,16 @@
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useState } from 'react'
const useMachineTimeMs = (updateInterval: number): number => {
const [now, setNow] = useState(Date.now())
useInterval(
useCallback(() => {
setNow(Date.now())
}, []),
updateInterval
)
return now
}
export default useMachineTimeMs

View File

@@ -1,5 +1,4 @@
import 'utils/signTypedData'
import { signTypedData } from '@uniswap/conedison/provider'
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
@@ -77,7 +76,8 @@ export function useUpdatePermitAllowance(
}
const { domain, types, values } = AllowanceTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId)
const signature = await provider.getSigner(account)._signTypedData(domain, types, values)
// Use conedison's signTypedData for better x-wallet compatibility.
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
onPermitSignature?.({ ...permit, signature })
return
} catch (e: unknown) {

View File

@@ -48,7 +48,12 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
}, [currency, stablecoin, trade])
const lastPrice = useRef(price)
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
if (
!price ||
!lastPrice.current ||
!price.equalTo(lastPrice.current) ||
!price.baseCurrency.equals(lastPrice.current.baseCurrency)
) {
lastPrice.current = price
}
return lastPrice.current

View File

@@ -1,4 +1,5 @@
import { FungibleToken, GenieCollection } from 'nft/types'
import { SearchToken } from 'graphql/data/SearchTokens'
import { GenieCollection } from 'nft/types'
/**
* Organizes the number of Token and NFT results to be shown to a user depending on if they're in the NFT or Token experience
@@ -10,9 +11,9 @@ import { FungibleToken, GenieCollection } from 'nft/types'
*/
export function organizeSearchResults(
isNFTPage: boolean,
tokenResults: FungibleToken[],
tokenResults: SearchToken[],
collectionResults: GenieCollection[]
): [FungibleToken[], GenieCollection[]] {
): [SearchToken[], GenieCollection[]] {
const reducedTokens =
tokenResults?.slice(0, isNFTPage ? 3 : collectionResults.length < 3 ? 8 - collectionResults.length : 5) ?? []
const reducedCollections = collectionResults.slice(0, 8 - reducedTokens.length)

View File

@@ -1,9 +1,10 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { useNftRoute } from 'graphql/data/nft/Routing'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal'
@@ -24,14 +25,13 @@ import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import {
buildSellObject,
fetchPrice,
formatAssetEventProperties,
recalculateBagUsingPooledAssets,
sortUpdatedAssets,
} from 'nft/utils'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery, useQueryClient } from 'react-query'
import { useQueryClient } from 'react-query'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
import shallow from 'zustand/shallow'
@@ -127,7 +127,6 @@ const Bag = () => {
bagExpanded,
toggleBag,
setTotalEthPrice,
setTotalUsdPrice,
setBagExpanded,
} = useBag((state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }), shallow)
const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag }))
@@ -137,6 +136,7 @@ const Bag = () => {
const isNFTPage = useIsNftPage()
const isMobile = useIsMobile()
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const usingGqlRouting = useGqlRoutingFlag() === GqlRoutingVariant.Enabled
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const transactionState = useSendTransaction((state) => state.state)
@@ -158,9 +158,7 @@ const Bag = () => {
}
}
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
const { totalEthPrice, totalUsdPrice } = useMemo(() => {
const { totalEthPrice } = useMemo(() => {
const totalEthPrice = itemsInBag.reduce(
(total, item) =>
item.status !== BagItemStatus.UNAVAILABLE
@@ -172,10 +170,9 @@ const Bag = () => {
: total,
BigNumber.from(0)
)
const totalUsdPrice = fetchedPriceData ? parseFloat(formatEther(totalEthPrice)) * fetchedPriceData : undefined
return { totalEthPrice, totalUsdPrice }
}, [itemsInBag, fetchedPriceData])
return { totalEthPrice }
}, [itemsInBag])
const purchaseAssets = async (routingData: RouteResponse) => {
if (!provider || !routingData) return
@@ -200,6 +197,7 @@ const Bag = () => {
setBagExpanded({ bagExpanded: false, manualClose: true })
}, [setBagExpanded])
useNftRoute(usingGqlRouting ? account ?? '' : '', [])
const fetchAssets = async () => {
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
const ethSellObject = buildSellObject(
@@ -283,8 +281,7 @@ const Bag = () => {
useEffect(() => {
setTotalEthPrice(totalEthPrice)
setTotalUsdPrice(totalUsdPrice)
}, [totalEthPrice, totalUsdPrice, setTotalEthPrice, setTotalUsdPrice])
}, [totalEthPrice, setTotalEthPrice])
const hasAssetsToShow = itemsInBag.length > 0
@@ -305,10 +302,9 @@ const Bag = () => {
const eventProperties = useMemo(
() => ({
usd_value: totalUsdPrice,
...formatAssetEventProperties(itemsInBag.map((item) => item.asset)),
}),
[itemsInBag, totalUsdPrice]
[itemsInBag]
)
if (!bagExpanded || !isNFTPage) {
@@ -334,7 +330,6 @@ const Bag = () => {
{hasAssetsToShow && !isProfilePage && (
<BagFooter
totalEthPrice={totalEthPrice}
totalUsdPrice={totalUsdPrice}
bagStatus={bagStatus}
fetchAssets={fetchAssets}
eventProperties={eventProperties}

View File

@@ -1,30 +1,44 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { parseEther } from '@ethersproject/units'
import { Trans } from '@lingui/macro'
import { t, Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { formatPriceImpact } from '@uniswap/conedison/format'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import Loader from 'components/Loader'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row from 'components/Row'
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
import { LoadingBubble } from 'components/Tokens/loading'
import { MouseoverTooltip } from 'components/Tooltip'
import { SupportedChainId } from 'constants/chains'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
import { useCurrency } from 'hooks/Tokens'
import { AllowanceState } from 'hooks/usePermit2Allowance'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useBag } from 'nft/hooks/useBag'
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
import usePermit2Approval from 'nft/hooks/usePermit2Approval'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
import { BagStatus } from 'nft/types'
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
import { PropsWithChildren, useMemo, useReducer } from 'react'
import { PropsWithChildren, useMemo, useState } from 'react'
import { AlertTriangle, ChevronDown } from 'react-feather'
import { useToggleWalletModal } from 'state/application/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { warningSeverity } from 'utils/prices'
import { switchChain } from 'utils/switchChain'
import { BagTokenSelectorModal } from './tokenSelector/BagTokenSelectorModal'
const LOW_SEVERITY_THRESHOLD = 1
const MEDIUM_SEVERITY_THRESHOLD = 3
const FooterContainer = styled.div`
padding: 0px 12px;
@@ -41,14 +55,12 @@ const Footer = styled.div`
border-bottom-right-radius: 12px;
`
const FooterHeader = styled(Column)<{ warningText?: boolean }>`
const FooterHeader = styled(Column)<{ usingPayWithAnyToken?: boolean }>`
padding-top: 8px;
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
padding-bottom: ${({ usingPayWithAnyToken }) => (usingPayWithAnyToken ? '16px' : '20px')};
`
const CurrencyRow = styled(Row)<{ warningText?: boolean }>`
padding-top: 4px;
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
const CurrencyRow = styled(Row)`
justify-content: space-between;
align-items: start;
`
@@ -67,17 +79,26 @@ const WarningText = styled(ThemedText.BodyPrimary)`
color: ${({ theme }) => theme.accentWarning};
display: flex;
justify-content: center;
margin: 12px 0 !important;
margin-bottom: 10px !important;
text-align: center;
`
const HelperText = styled(ThemedText.Caption)<{ $color: string }>`
color: ${({ $color }) => $color};
display: flex;
justify-content: center;
text-align: center;
margin-bottom: 10px !important;
`
const CurrencyInput = styled(Row)`
gap: 8px;
cursor: pointer;
`
const PayButton = styled(Row)<{ disabled?: boolean }>`
background: ${({ theme }) => theme.accentAction};
const PayButton = styled.button<{ $backgroundColor: string }>`
display: flex;
background: ${({ $backgroundColor }) => $backgroundColor};
color: ${({ theme }) => theme.accentTextLightPrimary};
font-weight: 600;
line-height: 24px;
@@ -87,18 +108,41 @@ const PayButton = styled(Row)<{ disabled?: boolean }>`
border: none;
border-radius: 12px;
padding: 12px 0px;
opacity: ${({ disabled }) => (disabled ? 0.6 : 1)};
cursor: ${({ disabled }) => (disabled ? 'auto' : 'pointer')};
cursor: pointer;
align-items: center;
&:disabled {
opacity: 0.6;
cursor: auto;
}
`
const FiatLoadingBubble = styled(LoadingBubble)`
border-radius: 4px;
width: 4rem;
height: 1rem;
align-self: end;
`
const PriceImpactContainer = styled(Row)`
align-items: center;
gap: 8px;
width: 100%;
justify-content: flex-end;
`
const PriceImpactRow = styled(Row)`
align-items: center;
gap: 8px;
`
interface ActionButtonProps {
disabled?: boolean
onClick: () => void
backgroundColor: string
}
const ActionButton = ({ disabled, children, onClick }: PropsWithChildren<ActionButtonProps>) => {
const ActionButton = ({ disabled, children, onClick, backgroundColor }: PropsWithChildren<ActionButtonProps>) => {
return (
<PayButton disabled={disabled} onClick={onClick}>
<PayButton disabled={disabled} onClick={onClick} $backgroundColor={backgroundColor}>
{children}
</PayButton>
)
@@ -116,9 +160,99 @@ const Warning = ({ children }: PropsWithChildren<unknown>) => {
)
}
interface HelperTextProps {
color: string
}
const Helper = ({ children, color }: PropsWithChildren<HelperTextProps>) => {
if (!children) {
return null
}
return (
<HelperText lineHeight="16px" $color={color}>
{children}
</HelperText>
)
}
// TODO: ask design about no route found
const InputCurrencyValue = ({
usingPayWithAnyToken,
totalEthPrice,
activeCurrency,
tradeState,
trade,
}: {
usingPayWithAnyToken: boolean
totalEthPrice: BigNumber
activeCurrency: Currency | undefined | null
tradeState: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
}) => {
if (!usingPayWithAnyToken) {
return (
<ThemedText.BodyPrimary lineHeight="20px" fontWeight="500">
{formatWeiToDecimal(totalEthPrice.toString())}
&nbsp;{activeCurrency?.symbol ?? 'ETH'}
</ThemedText.BodyPrimary>
)
}
if (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING) {
return (
<ThemedText.BodyPrimary
lineHeight="20px"
fontWeight="500"
color={tradeState === TradeState.VALID ? 'textPrimary' : 'textTertiary'}
>
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
</ThemedText.BodyPrimary>
)
}
return (
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
<Trans>Fetching price...</Trans>
</ThemedText.BodyPrimary>
)
}
const FiatValue = ({
usdcValue,
priceImpact,
priceImpactColor,
}: {
usdcValue: CurrencyAmount<Token> | null
priceImpact: Percent | undefined
priceImpactColor: string | undefined
}) => {
if (!usdcValue) {
return <FiatLoadingBubble />
}
return (
<PriceImpactContainer>
{priceImpact && priceImpactColor && (
<>
<MouseoverTooltip text={t`The estimated difference between the USD values of input and output amounts.`}>
<PriceImpactRow>
<AlertTriangle color={priceImpactColor} size="16px" />
<ThemedText.BodySmall style={{ color: priceImpactColor }} lineHeight="20px">
(<Trans>{formatPriceImpact(priceImpact)}</Trans>)
</ThemedText.BodySmall>
</PriceImpactRow>
</MouseoverTooltip>
</>
)}
<ThemedText.BodySmall color="textTertiary" lineHeight="20px">
{`${ethNumberStandardFormatter(usdcValue?.toExact(), true)}`}
</ThemedText.BodySmall>
</PriceImpactContainer>
)
}
interface BagFooterProps {
totalEthPrice: BigNumber
totalUsdPrice: number | undefined
bagStatus: BagStatus
fetchAssets: () => void
eventProperties: Record<string, unknown>
@@ -131,24 +265,18 @@ const PENDING_BAG_STATUSES = [
BagStatus.PROCESSING_TRANSACTION,
]
export const BagFooter = ({
totalEthPrice,
totalUsdPrice,
bagStatus,
fetchAssets,
eventProperties,
}: BagFooterProps) => {
export const BagFooter = ({ totalEthPrice, bagStatus, fetchAssets, eventProperties }: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal()
const theme = useTheme()
const { account, chainId, connector } = useWeb3React()
const connected = Boolean(account && chainId)
const shouldUsePayWithAnyToken = usePayWithAnyTokenFlag() === PayWithAnyTokenVariant.Enabled
const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled()
const inputCurrency = useTokenInput((state) => state.inputCurrency)
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
const defaultCurrency = useCurrency('ETH')
const setBagExpanded = useBag((state) => state.setBagExpanded)
const [showTokenSelector, toggleTokenSelector] = useReducer((state) => !state, false)
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
const { balance: balanceInEth } = useWalletBalance()
const sufficientBalance = useMemo(() => {
@@ -158,11 +286,52 @@ export const BagFooter = ({
return parseEther(balanceInEth).gte(totalEthPrice)
}, [connected, chainId, balanceInEth, totalEthPrice])
const { buttonText, disabled, warningText, handleClick } = useMemo(() => {
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
const activeCurrency = inputCurrency ?? defaultCurrency
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken
const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice])
const { state: tradeState, trade, maximumAmountIn } = usePayWithAnyTokenSwap(inputCurrency, parsedOutputAmount)
const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
maximumAmountIn,
shouldUsePayWithAnyToken
)
const fiatValueTradeInput = useStablecoinValue(trade?.inputAmount)
const fiatValueTradeOutput = useStablecoinValue(parsedOutputAmount)
const usdcValue = usingPayWithAnyToken ? fiatValueTradeInput : fiatValueTradeOutput
const stablecoinPriceImpact = useMemo(
() =>
tradeState === TradeState.SYNCING || !trade
? undefined
: computeFiatValuePriceImpact(fiatValueTradeInput, fiatValueTradeOutput),
[fiatValueTradeInput, fiatValueTradeOutput, tradeState, trade]
)
const { priceImpactWarning, priceImpactColor } = useMemo(() => {
const severity = warningSeverity(stablecoinPriceImpact)
if (severity < LOW_SEVERITY_THRESHOLD) {
return { priceImpactWarning: false, priceImpactColor: undefined }
}
if (severity < MEDIUM_SEVERITY_THRESHOLD) {
return { priceImpactWarning: false, priceImpactColor: theme.accentWarning }
}
return { priceImpactWarning: true, priceImpactColor: theme.accentCritical }
}, [stablecoinPriceImpact, theme.accentCritical, theme.accentWarning])
const { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor } = useMemo(() => {
let handleClick = fetchAssets
let buttonText = <Trans>Something went wrong</Trans>
let disabled = true
let warningText = null
let warningText = undefined
let helperText = undefined
let helperTextColor = theme.textSecondary
let buttonColor = theme.accentAction
if (connected && chainId !== SupportedChainId.MAINNET) {
handleClick = () => switchChain(connector, SupportedChainId.MAINNET)
@@ -182,98 +351,139 @@ export const BagFooter = ({
}
disabled = false
buttonText = <Trans>Connect wallet</Trans>
} else if (usingPayWithAnyToken && tradeState !== TradeState.VALID) {
disabled = true
buttonText = <Trans>Fetching Route</Trans>
} else if (allowance.state === AllowanceState.REQUIRED || allowance.state === AllowanceState.LOADING) {
handleClick = () => updateAllowance()
disabled = isAllowancePending || isApprovalLoading || allowance.state === AllowanceState.LOADING
if (allowance.state === AllowanceState.LOADING) {
buttonText = <Trans>Loading Allowance</Trans>
} else if (isAllowancePending) {
buttonText = <Trans>Approve in your wallet</Trans>
} else if (isApprovalLoading) {
buttonText = <Trans>Approval pending</Trans>
} else {
helperText = <Trans>An approval is needed to use this token. </Trans>
buttonText = <Trans>Approve</Trans>
}
} else if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE || bagStatus === BagStatus.CONFIRMING_IN_WALLET) {
disabled = true
buttonText = <Trans>Proceed in wallet</Trans>
} else if (bagStatus === BagStatus.PROCESSING_TRANSACTION) {
disabled = true
buttonText = <Trans>Transaction pending</Trans>
} else if (priceImpactWarning && priceImpactColor) {
disabled = false
buttonColor = priceImpactColor
helperText = <Trans>Price impact warning</Trans>
helperTextColor = priceImpactColor
buttonText = <Trans>Pay Anyway</Trans>
} else if (sufficientBalance === true) {
disabled = false
buttonText = <Trans>Pay</Trans>
}
return { buttonText, disabled, warningText, handleClick }
}, [bagStatus, chainId, connected, connector, fetchAssets, setBagExpanded, sufficientBalance, toggleWalletModal])
return { buttonText, disabled, warningText, helperText, helperTextColor, handleClick, buttonColor }
}, [
fetchAssets,
theme.textSecondary,
theme.accentAction,
connected,
chainId,
sufficientBalance,
bagStatus,
usingPayWithAnyToken,
tradeState,
allowance.state,
priceImpactWarning,
priceImpactColor,
connector,
toggleWalletModal,
setBagExpanded,
isAllowancePending,
isApprovalLoading,
updateAllowance,
])
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
const activeCurrency = inputCurrency ?? defaultCurrency
const traceEventProperties = {
usd_value: usdcValue?.toExact(),
...eventProperties,
}
return (
<FooterContainer>
<Footer>
{shouldUsePayWithAnyToken && (
<CurrencyRow>
<Column gap="xs">
<ThemedText.SubHeaderSmall>
<Trans>Pay with</Trans>
</ThemedText.SubHeaderSmall>
<CurrencyInput onClick={toggleTokenSelector}>
<CurrencyLogo currency={activeCurrency} size="24px" />
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
{activeCurrency?.symbol}
</ThemedText.HeadlineSmall>
<ChevronDown size={20} color={theme.textSecondary} />
</CurrencyInput>
</Column>
<TotalColumn gap="xs">
<ThemedText.SubHeaderSmall marginBottom="4px">
<Trans>Total</Trans>
</ThemedText.SubHeaderSmall>
<ThemedText.HeadlineSmall>
{formatWeiToDecimal(totalEthPrice.toString())}&nbsp;{activeCurrency?.symbol ?? 'ETH'}
</ThemedText.HeadlineSmall>
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
totalUsdPrice,
true
)}`}</ThemedText.BodySmall>
</TotalColumn>
</CurrencyRow>
<FooterHeader gap="xs" usingPayWithAnyToken={shouldUsePayWithAnyToken}>
<CurrencyRow>
<Column gap="xs">
<ThemedText.SubHeaderSmall>
<Trans>Pay with</Trans>
</ThemedText.SubHeaderSmall>
<CurrencyInput onClick={() => setTokenSelectorOpen(true)}>
<CurrencyLogo currency={activeCurrency} size="24px" />
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
{activeCurrency?.symbol}
</ThemedText.HeadlineSmall>
<ChevronDown size={20} color={theme.textSecondary} />
</CurrencyInput>
</Column>
<TotalColumn gap="xs">
<ThemedText.SubHeaderSmall marginBottom="4px">
<Trans>Total</Trans>
</ThemedText.SubHeaderSmall>
<InputCurrencyValue
usingPayWithAnyToken={usingPayWithAnyToken}
totalEthPrice={totalEthPrice}
activeCurrency={activeCurrency}
tradeState={tradeState}
trade={trade}
/>
</TotalColumn>
</CurrencyRow>
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
</FooterHeader>
)}
{!shouldUsePayWithAnyToken && (
<FooterHeader gap="xs" warningText={!!warningText}>
<FooterHeader gap="xs">
<Row justify="space-between">
<div>
<ThemedText.HeadlineSmall>Total</ThemedText.HeadlineSmall>
</div>
<div>
<ThemedText.HeadlineSmall>
{formatWeiToDecimal(totalEthPrice.toString())}&nbsp;ETH
{formatWeiToDecimal(totalEthPrice.toString())}
&nbsp;{activeCurrency?.symbol ?? 'ETH'}
</ThemedText.HeadlineSmall>
</div>
</Row>
<Row justify="flex-end">
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
totalUsdPrice,
true
)}`}</ThemedText.BodySmall>
</Row>
<FiatValue usdcValue={usdcValue} priceImpact={stablecoinPriceImpact} priceImpactColor={priceImpactColor} />
</FooterHeader>
)}
<TraceEvent
events={[BrowserEvent.onClick]}
name={NFTEventName.NFT_BUY_BAG_PAY}
element={InterfaceElementName.NFT_BUY_BAG_PAY_BUTTON}
properties={{ ...eventProperties }}
properties={{ ...traceEventProperties }}
shouldLogImpression={connected && !disabled}
>
<Warning>{warningText}</Warning>
<ActionButton onClick={handleClick} disabled={disabled}>
<Helper color={helperTextColor}>{helperText}</Helper>
<ActionButton onClick={handleClick} disabled={disabled} backgroundColor={buttonColor}>
{isPending && <Loader size="20px" stroke="white" />}
{buttonText}
</ActionButton>
</TraceEvent>
</Footer>
{showTokenSelector && (
<BagTokenSelectorModal
selectedCurrency={activeCurrency ?? undefined}
handleCurrencySelect={(currency: Currency) => {
setInputCurrency(currency)
toggleTokenSelector()
}}
overlayClick={toggleTokenSelector}
/>
)}
<CurrencySearchModal
isOpen={tokenSelectorOpen}
onDismiss={() => setTokenSelectorOpen(false)}
onCurrencySelect={(currency: Currency) => setInputCurrency(currency.isNative ? undefined : currency)}
selectedCurrency={activeCurrency ?? undefined}
onlyShowCurrenciesWithBalance={true}
/>
</FooterContainer>
)
}

View File

@@ -1,18 +1,25 @@
import { Plural, t } from '@lingui/macro'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import ms from 'ms.macro'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { ArrowRightIcon, HazardIcon, LoadingIcon, XMarkIcon } from 'nft/components/icons'
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
import { bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useNFTList, useSellAsset } from 'nft/hooks'
import { Listing, ListingStatus, WalletAsset } from 'nft/types'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTheme } from 'styled-components/macro'
import shallow from 'zustand/shallow'
import * as styles from './ListingModal.css'
import { getListings } from './utils'
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
interface ListingButtonProps {
onClick: () => void
buttonText: string
@@ -20,18 +27,46 @@ interface ListingButtonProps {
}
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
const sellAssets = useSellAsset((state) => state.sellAssets)
const addMarketplaceWarning = useSellAsset((state) => state.addMarketplaceWarning)
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings)
const listingStatus = useNFTList((state) => state.listingStatus)
const setListingStatus = useNFTList((state) => state.setListingStatus)
const setListings = useNFTList((state) => state.setListings)
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
const {
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
} = useSellAsset(
({
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
}) => ({
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
}),
shallow
)
const { listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
({ listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
listingStatus,
setListingStatus,
setListings,
setCollectionsRequiringApproval,
}),
shallow
)
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const [showWarning, setShowWarning] = useState(false)
const [canContinue, setCanContinue] = useState(false)
const [issues, setIssues] = useState(0)
const theme = useTheme()
const warningRef = useRef<HTMLDivElement>(null)
useOnClickOutside(warningRef, () => {
setShowWarning(false)
!isNftListV2 && setShowWarning(false)
})
useEffect(() => {
@@ -71,13 +106,30 @@ 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 ?? 0) && !listing.overrideFloorPrice)
else if (
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
!listing.overrideFloorPrice
)
listingsBelowFloor.push([asset, listing])
else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price)
else if (asset.floor_sell_order_price && listing.price >= asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing])
}
}
}
// set number of issues
if (isNftListV2) {
const foundIssues =
Number(missingExpiration) +
Number(overMaxExpiration) +
listingsMissingPrice.length +
listingsAboveSellOrderFloor.length
setIssues(foundIssues)
!foundIssues && showResolveIssues && toggleShowResolveIssues()
// Only show Resolve Issue text if there was a user submitted error (ie not when page loads with no prices set)
if ((missingExpiration || overMaxExpiration || listingsAboveSellOrderFloor.length) && !showResolveIssues)
toggleShowResolveIssues()
}
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
setCanContinue(continueCheck)
return [
@@ -90,7 +142,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
listingsAboveSellOrderFloor,
invalidPrices,
]
}, [sellAssets])
}, [isNftListV2, sellAssets, showResolveIssues, toggleShowResolveIssues])
const [disableListButton, warningMessage] = useMemo(() => {
const disableListButton =
@@ -158,94 +210,116 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
}
const warningWrappedClick = () => {
if ((!disableListButton && canContinue) || showWarningOverride) onClick()
else addWarningMessages()
if ((!disableListButton && canContinue) || showWarningOverride) {
if (issues && isNftListV2) !showResolveIssues && toggleShowResolveIssues()
else if (listingsBelowFloor.length) setShowWarning(true)
else onClick()
} else addWarningMessages()
}
return (
<Box position="relative" width="full">
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
<Row
className={`${bodySmall} ${styles.warningTooltip}`}
transition="250"
onClick={() => setShowWarning(false)}
color="textSecondary"
zIndex="3"
borderRadius="4"
backgroundColor="backgroundSurface"
height={!disableListButton ? '64' : '36'}
maxWidth="276"
position="absolute"
left="24"
bottom="52"
flexWrap={!disableListButton ? 'wrap' : 'nowrap'}
style={{ maxWidth: !disableListButton ? '225px' : '' }}
ref={warningRef}
>
<HazardIcon />
<Box marginLeft="4" marginRight="8">
{warningMessage}
</Box>
{disableListButton ? (
<Box paddingTop="6">
<XMarkIcon fill={themeVars.colors.textSecondary} height="20" width="20" />
<>
<Box position="relative" width="full">
{!showWarningOverride && showWarning && warningMessage.length > 0 && (
<Row
className={`${bodySmall} ${styles.warningTooltip}`}
transition="250"
onClick={() => setShowWarning(false)}
color="textSecondary"
zIndex="3"
borderRadius="4"
backgroundColor="backgroundSurface"
height={!disableListButton ? '64' : '36'}
maxWidth="276"
position="absolute"
left="24"
bottom="52"
flexWrap={!disableListButton ? 'wrap' : 'nowrap'}
style={{ maxWidth: !disableListButton ? '225px' : '' }}
ref={warningRef}
>
<HazardIcon />
<Box marginLeft="4" marginRight="8">
{warningMessage}
</Box>
) : (
<Row
marginLeft="72"
cursor="pointer"
color="accentAction"
onClick={() => {
setShowWarning(false)
setCanContinue(true)
onClick()
}}
>
Continue
<ArrowRightIcon height="20" width="20" />
</Row>
)}
</Row>
)}
<Box
as="button"
border="none"
backgroundColor="accentAction"
cursor={
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
disableListButton
? 'default'
: 'pointer'
}
color="explicitWhite"
className={styles.button}
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
type="button"
style={{
opacity:
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
disableListButton
? 0.3
: 1,
}}
>
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
<Row gap="8">
<LoadingIcon stroke="backgroundSurface" height="20" width="20" />
{listingStatus === ListingStatus.PENDING ? 'Pending' : 'Proceed in wallet'}
{disableListButton ? (
<Box paddingTop="6">
<XMarkIcon fill={themeVars.colors.textSecondary} height="20" width="20" />
</Box>
) : (
<Row
marginLeft="72"
cursor="pointer"
color="accentAction"
onClick={() => {
setShowWarning(false)
setCanContinue(true)
onClick()
}}
>
Continue
<ArrowRightIcon height="20" width="20" />
</Row>
)}
</Row>
) : listingStatus === ListingStatus.APPROVED ? (
'Complete!'
) : listingStatus === ListingStatus.PAUSED ? (
'Paused'
) : listingStatus === ListingStatus.FAILED ? (
'Try again'
) : listingStatus === ListingStatus.CONTINUE ? (
'Continue'
) : (
buttonText
)}
<Box
as="button"
border="none"
backgroundColor={showResolveIssues ? 'accentFailure' : 'accentAction'}
cursor={
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
disableListButton
? 'default'
: 'pointer'
}
className={styles.button}
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
type="button"
style={{
color: showResolveIssues ? theme.accentTextDarkPrimary : theme.white,
opacity:
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
(disableListButton && !showResolveIssues)
? 0.3
: 1,
}}
>
{listingStatus === ListingStatus.SIGNING || listingStatus === ListingStatus.PENDING ? (
isNftListV2 ? (
listingStatus === ListingStatus.PENDING ? (
'Pending'
) : (
'Proceed in wallet'
)
) : (
<Row gap="8">
<LoadingIcon stroke="backgroundSurface" height="20" width="20" />
{listingStatus === ListingStatus.PENDING ? 'Pending' : 'Proceed in wallet'}
</Row>
)
) : listingStatus === ListingStatus.APPROVED ? (
'Complete!'
) : listingStatus === ListingStatus.PAUSED ? (
'Paused'
) : listingStatus === ListingStatus.FAILED ? (
'Try again'
) : listingStatus === ListingStatus.CONTINUE ? (
'Continue'
) : showResolveIssues ? (
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
) : (
buttonText
)}
</Box>
</Box>
</Box>
{showWarning && (
<BelowFloorWarningModal
listingsBelowFloor={listingsBelowFloor}
closeModal={() => setShowWarning(false)}
startListing={onClick}
/>
)}
</>
)
}

View File

@@ -1,4 +1,3 @@
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
@@ -13,6 +12,7 @@ import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
import shallow from 'zustand/shallow'
import { ListingButton } from './ListingButton'
import * as styles from './ListingModal.css'
@@ -22,18 +22,49 @@ import { approveCollectionRow, getTotalEthValue, pauseRow, resetRow, signListing
const ListingModal = () => {
const { provider } = useWeb3React()
const sellAssets = useSellAsset((state) => state.sellAssets)
const {
listingStatus,
setListingStatus,
setListings,
setCollectionsRequiringApproval,
setListingStatusAndCallback,
setCollectionStatusAndCallback,
looksRareNonce,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
} = useNFTList(
({
listingStatus,
setListingStatus,
setListings,
setCollectionsRequiringApproval,
setListingStatusAndCallback,
setCollectionStatusAndCallback,
looksRareNonce,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
}) => ({
listingStatus,
setListingStatus,
setListings,
setCollectionsRequiringApproval,
setListingStatusAndCallback,
setCollectionStatusAndCallback,
looksRareNonce,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
}),
shallow
)
const signer = provider?.getSigner()
const listings = useNFTList((state) => state.listings)
const setListings = useNFTList((state) => state.setListings)
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval)
const [openIndex, setOpenIndex] = useState(0)
const listingStatus = useNFTList((state) => state.listingStatus)
const setListingStatus = useNFTList((state) => state.setListingStatus)
const [allCollectionsApproved, setAllCollectionsApproved] = useState(false)
const looksRareNonce = useNFTList((state) => state.looksRareNonce)
const setLooksRareNonce = useNFTList((state) => state.setLooksRareNonce)
const getLooksRareNonce = useNFTList((state) => state.getLooksRareNonce)
const toggleCart = useBag((state) => state.toggleBag)
const looksRareNonceRef = useRef(looksRareNonce)
const isMobile = useIsMobile()
@@ -109,7 +140,6 @@ const ListingModal = () => {
if (!signer) return
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
setListingStatus(ListingStatus.SIGNING)
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const signerAddress = await signer.getAddress()
const nonce = await looksRareNonceFetcher(signerAddress)
setLooksRareNonce(nonce ?? 0)
@@ -118,27 +148,12 @@ const ListingModal = () => {
setListingStatus(ListingStatus.SIGNING)
setOpenIndex(1)
}
const looksRareAddress = addresses.TRANSFER_MANAGER_ERC721
// for all unique collection, marketplace combos -> approve collections
for (const collectionRow of collectionsRequiringApproval) {
verifyStatus(collectionRow.status) &&
(isMobile
? await approveCollectionRow(
collectionRow,
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer,
looksRareAddress,
pauseAllRows
)
: approveCollectionRow(
collectionRow,
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer,
looksRareAddress,
pauseAllRows
))
? await approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
: approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows))
}
}
@@ -151,12 +166,11 @@ const ListingModal = () => {
verifyStatus(listing.status) &&
(await signListingRow(
listing,
listings,
setListings,
signer,
provider,
getLooksRareNonce,
setLooksRareNonce,
setListingStatusAndCallback,
pauseAllRows
))
}

View File

@@ -1,4 +1,5 @@
import type { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { LOOKSRARE_MARKETPLACE_CONTRACT, X2Y2_TRANSFER_CONTRACT } from 'nft/queries'
import { OPENSEA_CROSS_CHAIN_CONDUIT } from 'nft/queries/openSea'
import { AssetRow, CollectionRow, ListingMarket, ListingRow, ListingStatus, WalletAsset } from 'nft/types'
@@ -28,28 +29,18 @@ const updateStatus = ({
export async function approveCollectionRow(
collectionRow: CollectionRow,
collectionsRequiringApproval: CollectionRow[],
setCollectionsRequiringApproval: Dispatch<CollectionRow[]>,
signer: JsonRpcSigner,
looksRareAddress: string,
pauseAllRows: () => void
setCollectionStatusAndCallback: (
collection: CollectionRow,
status: ListingStatus,
callback?: () => Promise<void>
) => void,
pauseAllRows?: () => void
) {
updateStatus({
listing: collectionRow,
newStatus: ListingStatus.SIGNING,
rows: collectionsRequiringApproval,
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
callback: () =>
approveCollectionRow(
collectionRow,
collectionsRequiringApproval,
setCollectionsRequiringApproval,
signer,
looksRareAddress,
pauseAllRows
),
})
const callback = () => approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback, pauseAllRows)
setCollectionStatusAndCallback(collectionRow, ListingStatus.SIGNING, callback)
const { marketplace, collectionAddress } = collectionRow
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const spender =
marketplace.name === 'OpenSea'
? OPENSEA_CROSS_CHAIN_CONDUIT
@@ -57,67 +48,48 @@ export async function approveCollectionRow(
? LOOKSRARE_MARKETPLACE_CONTRACT
: marketplace.name === 'X2Y2'
? X2Y2_TRANSFER_CONTRACT
: looksRareAddress
: addresses.TRANSFER_MANAGER_ERC721
!!collectionAddress &&
(await approveCollection(spender, collectionAddress, signer, (newStatus: ListingStatus) =>
updateStatus({
listing: collectionRow,
newStatus,
rows: collectionsRequiringApproval,
setRows: setCollectionsRequiringApproval as Dispatch<AssetRow[]>,
})
setCollectionStatusAndCallback(collectionRow, newStatus, callback)
))
if (collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) pauseAllRows()
if (
(collectionRow.status === ListingStatus.REJECTED || collectionRow.status === ListingStatus.FAILED) &&
pauseAllRows
)
pauseAllRows()
}
export async function signListingRow(
listing: ListingRow,
listings: ListingRow[],
setListings: Dispatch<ListingRow[]>,
signer: JsonRpcSigner,
provider: Web3Provider,
getLooksRareNonce: () => number,
setLooksRareNonce: (nonce: number) => void,
pauseAllRows: () => void
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void,
pauseAllRows?: () => void
) {
const looksRareNonce = getLooksRareNonce()
updateStatus({
listing,
newStatus: ListingStatus.SIGNING,
rows: listings,
setRows: setListings as Dispatch<AssetRow[]>,
callback: () => {
return signListingRow(
listing,
listings,
setListings,
signer,
provider,
getLooksRareNonce,
setLooksRareNonce,
pauseAllRows
)
},
})
const callback = () => {
return signListingRow(
listing,
signer,
provider,
getLooksRareNonce,
setLooksRareNonce,
setListingStatusAndCallback,
pauseAllRows
)
}
setListingStatusAndCallback(listing, ListingStatus.SIGNING, callback)
const { asset, marketplace } = listing
const res = await signListing(marketplace, asset, signer, provider, looksRareNonce, (newStatus: ListingStatus) =>
updateStatus({
listing,
newStatus,
rows: listings,
setRows: setListings as Dispatch<AssetRow[]>,
})
setListingStatusAndCallback(listing, newStatus, callback)
)
if (listing.status === ListingStatus.REJECTED) pauseAllRows()
else {
if (listing.status === ListingStatus.REJECTED && pauseAllRows) {
pauseAllRows()
} else {
res && listing.marketplace.name === 'LooksRare' && setLooksRareNonce(looksRareNonce + 1)
const newStatus = res ? ListingStatus.APPROVED : ListingStatus.FAILED
updateStatus({
listing,
newStatus,
rows: listings,
setRows: setListings as Dispatch<AssetRow[]>,
})
}
}
@@ -163,6 +135,7 @@ export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], Listin
name: asset.asset_contract.name,
status: ListingStatus.DEFINED,
collectionAddress: asset.asset_contract.address,
isVerified: asset.collectionIsVerified,
marketplace,
}
newCollectionsToApprove.push(newCollectionRow)

View File

@@ -1,119 +0,0 @@
import { Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import Column from 'components/Column'
import Row from 'components/Row'
import { useAllTokens } from 'hooks/Tokens'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { tokenComparator } from 'lib/hooks/useTokenList/sorting'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import { useMemo } from 'react'
import { X } from 'react-feather'
import { useAllTokenBalances } from 'state/connection/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { CurrencyRow } from './CurrencyRow'
const ModalWrapper = styled(Column)`
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 420px;
height: 368px;
z-index: ${Z_INDEX.modalOverTooltip};
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
box-shadow: ${({ theme }) => theme.deepShadow};
`
const TitleRow = styled(Row)`
padding: 20px;
justify-content: space-between;
`
const TokenSelectorContainer = styled(Column)`
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
padding: 20px;
height: 100%;
overflow-y: scroll;
gap: 8px;
::-webkit-scrollbar {
width: 0px;
height: 0px;
}
`
interface BagTokenSelectorModalProps {
selectedCurrency: Currency | undefined
handleCurrencySelect: (currency: Currency) => void
overlayClick: () => void
}
export const BagTokenSelectorModal = ({
selectedCurrency,
handleCurrencySelect,
overlayClick,
}: BagTokenSelectorModalProps) => {
const defaultTokens = useAllTokens()
const [balances, balancesAreLoading] = useAllTokenBalances()
const sortedTokens: Token[] = useMemo(
() =>
!balancesAreLoading
? Object.values(defaultTokens)
.filter((token) => {
return balances[token.address]?.greaterThan(0)
})
.sort(tokenComparator.bind(null, balances))
: [],
[balances, balancesAreLoading, defaultTokens]
)
const native = useNativeCurrency()
const wrapped = native.wrapped
const currencies: Currency[] = useMemo(() => {
const tokens = sortedTokens.filter((t) => !t.equals(wrapped))
const natives: Currency[] = []
if (native.equals(wrapped)) {
natives.push(wrapped)
} else {
natives.push(native)
if (balances[wrapped.address]?.greaterThan(0)) {
natives.push(wrapped)
}
}
return [...natives, ...tokens]
}, [sortedTokens, native, wrapped, balances])
return (
<Portal>
<ModalWrapper>
<TitleRow>
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
<Trans>Select a token</Trans>
</ThemedText.SubHeader>
<X size={24} cursor="pointer" onClick={overlayClick} />
</TitleRow>
<TokenSelectorContainer>
{currencies.map((currency) => {
return (
<CurrencyRow
key={currency.isToken ? currency.wrapped.address : currency.name}
currency={currency}
selected={
(!selectedCurrency && currency.isNative) || (!!selectedCurrency && selectedCurrency.equals(currency))
}
selectCurrency={handleCurrencySelect}
/>
)
})}
</TokenSelectorContainer>
</ModalWrapper>
<Overlay onClick={overlayClick} />
</Portal>
)
}

View File

@@ -1,70 +0,0 @@
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row from 'components/Row'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { Check } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const TokenRow = styled(Row)`
padding: 8px 0px;
gap: 12px;
justify-content: space-between;
cursor: pointer;
`
const TokenInfoRow = styled(Row)`
gap: 8px;
`
const StyledBalanceText = styled(ThemedText.SubHeader)`
white-space: nowrap;
overflow: hidden;
width: 100%;
text-overflow: ellipsis;
text-align: right;
`
const StyledCheck = styled(Check)`
color: ${({ theme }) => theme.accentAction};
flex-shrink: 0;
`
interface CurrencyRowProps {
currency: Currency
selected: boolean
selectCurrency: (currency: Currency) => void
}
export const CurrencyRow = ({ currency, selected, selectCurrency }: CurrencyRowProps) => {
const { account } = useWeb3React()
const balance = useCurrencyBalance(account ?? undefined, currency)
return (
<TokenRow onClick={() => selectCurrency(currency)}>
<TokenInfoRow>
<CurrencyLogo currency={currency} size="36px" />
<Column>
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
{currency.name}
</ThemedText.SubHeader>
<ThemedText.BodySmall lineHeight="20px" color="textSecondary">
{currency.symbol}
</ThemedText.BodySmall>
</Column>
</TokenInfoRow>
{balance && <Balance balance={balance} />}
{selected && <StyledCheck size={20} />}
</TokenRow>
)
}
const Balance = ({ balance }: { balance: CurrencyAmount<Currency> }) => {
return (
<StyledBalanceText fontWeight={500} lineHeight="24px">
{balance.toSignificant(4)}
</StyledBalanceText>
)
}

View File

@@ -24,12 +24,12 @@ export const ChevronUpIcon = ({
...props
}: SVGProps & { secondaryWidth?: string; secondaryHeight?: string; secondaryColor?: string }) => (
<svg
{...props}
width={secondaryWidth || '29'}
height={secondaryHeight || '28'}
viewBox="0 0 29 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_564_11230)">
<path
@@ -757,3 +757,16 @@ export const CancelListingIcon = (props: SVGProps) => (
/>
</svg>
)
export const ListingModalWindowActive = (props: SVGProps) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="8" cy="8" r="8" fill={props.fill ? props.fill : themeVars.colors.accentAction} fillOpacity="0.24" />
<circle cx="8" cy="8" r="5" fill={props.fill ? props.fill : themeVars.colors.accentAction} />
</svg>
)
export const ListingModalWindowClosed = (props: SVGProps) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx="8" cy="8" r="7" stroke="#333D59" strokeWidth="2" />
</svg>
)

View File

@@ -0,0 +1,46 @@
import Column from 'components/Column'
import Row from 'components/Row'
import { DropDownOption } from 'nft/types'
import { Check } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
const DropdownWrapper = styled(Column)<{ $width: number }>`
gap: 4px;
background: ${({ theme }) => theme.backgroundSurface};
padding: 8px;
width: ${({ $width }) => $width}px;
border-radius: 12px;
box-shadow: ${({ theme }) => theme.deepShadow};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
`
const DropdownRow = styled(Row)`
justify-content: space-between;
padding: 8px;
cursor: pointer;
border-radius: 12px;
&:hover {
background: ${({ theme }) => theme.backgroundInteractive};
}
`
interface DropdownArgs {
dropDownOptions: DropDownOption[]
width: number
}
export const Dropdown = ({ dropDownOptions, width }: DropdownArgs) => {
const theme = useTheme()
return (
<DropdownWrapper $width={width}>
{dropDownOptions.map((option) => (
<DropdownRow key={option.displayText} onClick={option.onClick}>
<ThemedText.BodyPrimary lineHeight="24px">{option.displayText}</ThemedText.BodyPrimary>
{option.isSelected && <Check height={20} width={20} color={theme.accentAction} />}
</DropdownRow>
))}
</DropdownWrapper>
)
}

View File

@@ -1,30 +0,0 @@
import ListingModal from 'nft/components/bag/profile/ListingModal'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
const ListModalWrapper = styled.div`
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 420px;
z-index: ${Z_INDEX.modalOverTooltip};
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 0px 12px 4px;
`
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
return (
<Portal>
<ListModalWrapper>
<ListingModal />
</ListModalWrapper>
<Overlay onClick={overlayClick} />
</Portal>
)
}

View File

@@ -1,24 +1,29 @@
import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import Row from 'components/Row'
import { SMALL_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { ListingButton } from 'nft/components/bag/profile/ListingButton'
import { getListingState, getTotalEthValue } from 'nft/components/bag/profile/utils'
import { approveCollectionRow, getListingState, getTotalEthValue, verifyStatus } from 'nft/components/bag/profile/utils'
import { BackArrowIcon } from 'nft/components/icons'
import { headlineLarge, headlineSmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useNFTList, useProfilePageState, useSellAsset } from 'nft/hooks'
import { LIST_PAGE_MARGIN } from 'nft/pages/profile/shared'
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE, LIST_PAGE_MARGIN_TABLET } from 'nft/pages/profile/shared'
import { looksRareNonceFetcher } from 'nft/queries'
import { ListingStatus, ProfilePageStateType } from 'nft/types'
import { fetchPrice, formatEth, formatUsdPrice } from 'nft/utils'
import { ListingMarkets } from 'nft/utils/listNfts'
import { useEffect, useMemo, useReducer, useState } from 'react'
import styled, { css } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import shallow from 'zustand/shallow'
import { ListModal } from './ListModal'
import { ListModal } from './Modal/ListModal'
import { NFTListingsGrid } from './NFTListingsGrid'
import { SelectMarketplacesDropdown } from './SelectMarketplacesDropdown'
import { SetDurationModal } from './SetDurationModal'
@@ -68,10 +73,7 @@ const ListingHeader = styled(Row)`
const GridWrapper = styled.div`
margin-top: 24px;
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
margin-left: 40px;
}
margin-bottom: 48px;
`
const MobileListButtonWrapper = styled.div`
@@ -98,6 +100,16 @@ const FloatingConfirmationBar = styled(Row)`
transform: translateX(-50%);
max-width: 1200px;
z-index: ${Z_INDEX.under_dropdown};
@media screen and (max-width: ${BREAKPOINTS.lg}px) {
width: calc(100% - ${LIST_PAGE_MARGIN_TABLET * 2}px);
bottom: 68px;
padding: 16px 12px;
}
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: calc(100% - ${LIST_PAGE_MARGIN_MOBILE * 2}px);
}
`
const Overlay = styled.div`
@@ -108,9 +120,23 @@ const Overlay = styled.div`
background: ${({ theme }) => `linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, ${theme.backgroundBackdrop} 100%)`};
`
const UsdValue = styled(ThemedText.SubHeader)`
line-height: 24px;
color: ${({ theme }) => theme.textSecondary};
display: none;
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
display: flex;
}
`
const ProceedsAndButtonWrapper = styled(Row)`
width: min-content;
gap: 40px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
gap: 20px;
}
`
const ProceedsWrapper = styled(Row)`
@@ -118,27 +144,72 @@ const ProceedsWrapper = styled(Row)`
gap: 16px;
`
const EthValueWrapper = styled.span<{ totalEthListingValue: boolean }>`
font-weight: 500;
font-size: 20px;
line-height: 28px;
color: ${({ theme, totalEthListingValue }) => (totalEthListingValue ? theme.textPrimary : theme.textSecondary)};
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
font-size: 16px;
line-height: 24px;
}
`
const ListingButtonWrapper = styled.div`
width: 170px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: max-content;
}
`
export const ListPage = () => {
const { setProfilePageState: setSellPageState } = useProfilePageState()
const setGlobalMarketplaces = useSellAsset((state) => state.setGlobalMarketplaces)
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
const { provider } = useWeb3React()
const toggleBag = useBag((s) => s.toggleBag)
const listings = useNFTList((state) => state.listings)
const collectionsRequiringApproval = useNFTList((state) => state.collectionsRequiringApproval)
const listingStatus = useNFTList((state) => state.listingStatus)
const setListingStatus = useNFTList((state) => state.setListingStatus)
const sellAssets = useSellAsset((state) => state.sellAssets)
const isMobile = useIsMobile()
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const { setGlobalMarketplaces, sellAssets } = useSellAsset(
({ setGlobalMarketplaces, sellAssets }) => ({
setGlobalMarketplaces,
sellAssets,
}),
shallow
)
const {
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionStatusAndCallback,
} = useNFTList(
({
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionStatusAndCallback,
}) => ({
listings,
collectionsRequiringApproval,
listingStatus,
setListingStatus,
setLooksRareNonce,
setCollectionStatusAndCallback,
}),
shallow
)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const anyListingsMissingPrice = useMemo(() => !!listings.find((listing) => !listing.price), [listings])
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
const [showListModal, toggleShowListModal] = useReducer((s) => !s, false)
const [selectedMarkets, setSelectedMarkets] = useState([ListingMarkets[0]]) // default marketplace: x2y2
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
const signer = provider?.getSigner()
useEffect(() => {
fetchPrice().then((price) => {
@@ -146,6 +217,7 @@ export const ListPage = () => {
})
}, [])
// TODO with removal of list v1 see if this logic can be removed
useEffect(() => {
const state = getListingState(collectionsRequiringApproval, listings)
@@ -162,8 +234,48 @@ export const ListPage = () => {
useEffect(() => {
setGlobalMarketplaces(selectedMarkets)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMarkets])
}, [selectedMarkets, setGlobalMarketplaces])
const startListingEventProperties = {
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
token_ids: sellAssets.map((asset) => asset.tokenId),
marketplaces: Array.from(new Set(listings.map((asset) => asset.marketplace.name))),
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
}
const startListingFlow = async () => {
if (!signer) return
sendAnalyticsEvent(NFTEventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
setListingStatus(ListingStatus.SIGNING)
const signerAddress = await signer.getAddress()
const nonce = await looksRareNonceFetcher(signerAddress)
setLooksRareNonce(nonce ?? 0)
// for all unique collection, marketplace combos -> approve collections
for (const collectionRow of collectionsRequiringApproval) {
verifyStatus(collectionRow.status) &&
(isMobile
? await approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback)
: approveCollectionRow(collectionRow, signer, setCollectionStatusAndCallback))
}
}
const handleV2Click = () => {
toggleShowListModal()
startListingFlow()
}
const BannerText = isMobile ? (
<ThemedText.SubHeader lineHeight="24px">
<Trans>Proceeds</Trans>
</ThemedText.SubHeader>
) : (
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>Proceeds if sold</Trans>
</ThemedText.HeadlineSmall>
)
return (
<Column>
@@ -191,27 +303,21 @@ export const ListPage = () => {
{isNftListV2 && (
<>
<FloatingConfirmationBar>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>Proceeds if sold</Trans>
</ThemedText.HeadlineSmall>
{BannerText}
<ProceedsAndButtonWrapper>
<ProceedsWrapper>
<ThemedText.HeadlineSmall
lineHeight="28px"
color={totalEthListingValue ? 'textPrimary' : 'textTertiary'}
>
<EthValueWrapper totalEthListingValue={!!totalEthListingValue}>
{totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH
</ThemedText.HeadlineSmall>
</EthValueWrapper>
{!!totalEthListingValue && !!ethPriceInUSD && (
<ThemedText.HeadlineSmall lineHeight="28px" color="textSecondary">
{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}
</ThemedText.HeadlineSmall>
<UsdValue>{formatUsdPrice(totalEthListingValue * ethPriceInUSD)}</UsdValue>
)}
</ProceedsWrapper>
<ListingButtonWrapper>
<ListingButton
onClick={isNftListV2 ? toggleShowListModal : toggleBag}
buttonText={anyListingsMissingPrice ? t`Set prices to continue` : t`Start listing`}
onClick={handleV2Click}
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
showWarningOverride={true}
/>
</ListingButtonWrapper>
</ProceedsAndButtonWrapper>

View File

@@ -3,12 +3,13 @@ import { t } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { RowsCollpsedIcon, RowsExpandedIcon } from 'nft/components/icons'
import { useSellAsset } from 'nft/hooks'
import { ListingMarket, ListingWarning, WalletAsset } from 'nft/types'
import { LOOKS_RARE_CREATOR_BASIS_POINTS } from 'nft/utils'
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { Dispatch, useEffect, useMemo, useState } from 'react'
import React, { Dispatch, DispatchWithoutAction, useEffect, useMemo, useReducer, useState } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
@@ -22,26 +23,51 @@ const PastPriceInfo = styled(Column)`
display: none;
flex: 1;
@media screen and (min-width: ${BREAKPOINTS.xxl}px) {
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
display: flex;
}
`
const RemoveMarketplaceWrap = styled(RemoveIconWrap)`
top: 11px;
top: 8px;
left: 16px;
z-index: 3;
`
const MarketIconsWrapper = styled(Row)`
position: relative;
margin-right: 12px;
width: 44px;
justify-content: flex-end;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const MarketIconWrapper = styled(Column)`
position: relative;
cursor: pointer;
margin-right: 16px;
`
const MarketIcon = styled.img`
width: 28px;
height: 28px;
const MarketIcon = styled.img<{ index: number }>`
width: 20px;
height: 20px;
border-radius: 4px;
object-fit: cover;
z-index: ${({ index }) => 2 - index};
margin-left: ${({ index }) => `${index === 0 ? 0 : -8}px`};
outline: 1px solid ${({ theme }) => theme.backgroundInteractive};
`
const ExpandMarketIconWrapper = styled.div`
cursor: pointer;
margin-left: 4px;
height: 28px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const FeeColumnWrapper = styled(Column)`
@@ -49,7 +75,7 @@ const FeeColumnWrapper = styled(Column)`
align-items: flex-end;
display: none;
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
@media screen and (min-width: ${BREAKPOINTS.md}px) {
display: flex;
}
`
@@ -63,7 +89,7 @@ const ReturnColumn = styled(Column)`
flex: 1.5;
display: none;
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
@media screen and (min-width: ${BREAKPOINTS.md}px) {
display: flex;
}
`
@@ -84,6 +110,8 @@ interface MarketplaceRowProps {
asset: WalletAsset
showMarketplaceLogo: boolean
expandMarketplaceRows?: boolean
rowHovered?: boolean
toggleExpandMarketplaceRows: DispatchWithoutAction
}
export const MarketplaceRow = ({
@@ -95,14 +123,16 @@ export const MarketplaceRow = ({
asset,
showMarketplaceLogo,
expandMarketplaceRows,
toggleExpandMarketplaceRows,
rowHovered,
}: MarketplaceRowProps) => {
const [listPrice, setListPrice] = useState<number>()
const [globalOverride, setGlobalOverride] = useState(false)
const showGlobalPrice = globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride && globalPrice
const setAssetListPrice = useSellAsset((state) => state.setAssetListPrice)
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
const [marketIconHovered, toggleMarketIconHovered] = useReducer((s) => !s, false)
const [marketRowHovered, toggleMarketRowHovered] = useReducer((s) => !s, false)
const price = showGlobalPrice ? globalPrice : listPrice
@@ -135,7 +165,7 @@ export const MarketplaceRow = ({
if (globalPriceMethod === SetPriceMethod.FLOOR_PRICE) {
setListPrice(asset?.floorPrice)
setGlobalPrice(asset.floorPrice)
} else if (globalPriceMethod === SetPriceMethod.PREV_LISTING) {
} else if (globalPriceMethod === SetPriceMethod.LAST_PRICE) {
setListPrice(asset.lastPrice)
setGlobalPrice(asset.lastPrice)
} else if (globalPriceMethod === SetPriceMethod.SAME_PRICE)
@@ -186,7 +216,7 @@ export const MarketplaceRow = ({
}
return (
<Row>
<Row onMouseEnter={toggleMarketRowHovered} onMouseLeave={toggleMarketRowHovered}>
<PastPriceInfo>
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">
{asset.floorPrice ? `${asset.floorPrice.toFixed(3)} ETH` : '-'}
@@ -198,22 +228,25 @@ export const MarketplaceRow = ({
</ThemedText.BodySmall>
</PastPriceInfo>
<Row flex="2">
{showMarketplaceLogo && (
<MarketIconWrapper
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={(e) => {
e.stopPropagation()
removeAssetMarketplace(asset, selectedMarkets[0])
removeMarket && removeMarket()
}}
>
<MarketIcon alt={selectedMarkets[0].name} src={selectedMarkets[0].icon} />
<RemoveMarketplaceWrap hovered={hovered}>
<img width="32px" src="/nft/svgs/minusCircle.svg" alt="Remove item" />
</RemoveMarketplaceWrap>
</MarketIconWrapper>
<Row flex="3">
{(expandMarketplaceRows || selectedMarkets.length > 1) && (
<MarketIconsWrapper onMouseEnter={toggleMarketIconHovered} onMouseLeave={toggleMarketIconHovered}>
{selectedMarkets.map((market, index) => (
<MarketIconWrapper
key={market.name + asset.collection?.address + asset.tokenId}
onClick={(e) => {
e.stopPropagation()
removeAssetMarketplace(asset, selectedMarkets[0])
removeMarket && removeMarket()
}}
>
<MarketIcon alt={selectedMarkets[0].name} src={market.icon} index={index} />
<RemoveMarketplaceWrap hovered={marketIconHovered && (expandMarketplaceRows ?? false)}>
<img width="20px" src="/nft/svgs/minusCircle.svg" alt="Remove item" />
</RemoveMarketplaceWrap>
</MarketIconWrapper>
))}
</MarketIconsWrapper>
)}
{globalPriceMethod === SetPriceMethod.SAME_PRICE && !globalOverride ? (
<PriceTextInput
@@ -224,7 +257,6 @@ export const MarketplaceRow = ({
globalOverride={globalOverride}
warning={warning}
asset={asset}
shrink={expandMarketplaceRows}
/>
) : (
<PriceTextInput
@@ -235,9 +267,13 @@ export const MarketplaceRow = ({
globalOverride={globalOverride}
warning={warning}
asset={asset}
shrink={expandMarketplaceRows}
/>
)}
{rowHovered && ((expandMarketplaceRows && marketRowHovered) || selectedMarkets.length > 1) && (
<ExpandMarketIconWrapper onClick={toggleExpandMarketplaceRows}>
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}
</ExpandMarketIconWrapper>
)}
</Row>
<FeeColumnWrapper>

View File

@@ -0,0 +1,124 @@
import { Plural, t, Trans } from '@lingui/macro'
import { ButtonPrimary } from 'components/Button'
import Column from 'components/Column'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import { Listing, WalletAsset } from 'nft/types'
import React from 'react'
import { AlertTriangle, X } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
const ModalWrapper = styled(Column)`
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 420px;
z-index: ${Z_INDEX.modal};
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 20px 24px 24px;
display: flex;
flex-direction: column;
gap: 12px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: 100%;
}
`
const CloseIconWrapper = styled.div`
display: flex;
width: 100%;
justify-content: flex-end;
`
const CloseIcon = styled(X)`
cursor: pointer;
&:hover {
opacity: 0.6;
}
`
const HazardIconWrap = styled.div`
display: flex;
width: 100%;
justify-content: center;
align-items: center;
padding: 32px 120px;
`
const ContinueButton = styled(ButtonPrimary)`
font-weight: 600;
font-size: 20px;
line-height: 24px;
margin-top: 12px;
`
const EditListings = styled.span`
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.accentAction};
text-align: center;
cursor: pointer;
padding: 12px 16px;
&:hover {
opacity: 0.6;
}
`
export const BelowFloorWarningModal = ({
listingsBelowFloor,
closeModal,
startListing,
}: {
listingsBelowFloor: [WalletAsset, Listing][]
closeModal: () => void
startListing: () => void
}) => {
const theme = useTheme()
const clickContinue = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
startListing()
closeModal()
}
return (
<Portal>
<ModalWrapper>
<CloseIconWrapper>
<CloseIcon width={24} height={24} onClick={closeModal} />{' '}
</CloseIconWrapper>
<HazardIconWrap>
<AlertTriangle height={90} width={90} color={theme.accentCritical} />
</HazardIconWrap>
<ThemedText.HeadlineSmall lineHeight="28px" textAlign="center">
<Trans>Low listing price</Trans>
</ThemedText.HeadlineSmall>
<ThemedText.BodyPrimary textAlign="center">
<Plural
value={listingsBelowFloor.length !== 1 ? 2 : 1}
_1={t`One NFT is listed ${(
(1 - (listingsBelowFloor[0][1].price ?? 0) / (listingsBelowFloor[0][0].floorPrice ?? 0)) *
100
).toFixed(0)}% `}
other={t`${listingsBelowFloor.length} NFTs are listed significantly `}
/>
&nbsp;
<Trans>below the collections floor price. Are you sure you want to continue?</Trans>
</ThemedText.BodyPrimary>
<ContinueButton onClick={clickContinue}>
<Trans>Continue</Trans>
</ContinueButton>
<EditListings onClick={closeModal}>
<Trans>Edit listings</Trans>
</EditListings>
</ModalWrapper>
<Overlay onClick={closeModal} />
</Portal>
)
}

View File

@@ -0,0 +1,181 @@
import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import Loader from 'components/Loader'
import Row from 'components/Row'
import { VerifiedIcon } from 'nft/components/icons'
import { AssetRow, CollectionRow, ListingStatus } from 'nft/types'
import { useEffect, useRef } from 'react'
import { Check, XOctagon } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { opacify } from 'theme/utils'
const ContentColumn = styled(Column)<{ failed: boolean }>`
background-color: ${({ theme, failed }) => failed && opacify(12, theme.accentCritical)};
border-radius: 12px;
padding-bottom: ${({ failed }) => failed && '16px'};
`
const ContentRowWrapper = styled(Row)<{ active: boolean; failed: boolean }>`
padding: 16px;
border: ${({ failed, theme }) => !failed && `1px solid ${theme.backgroundOutline}`};
border-radius: 12px;
opacity: ${({ active, failed }) => (active || failed ? '1' : '0.6')};
`
const CollectionIcon = styled.img`
border-radius: 100px;
height: 24px;
width: 24px;
z-index: 1;
`
const AssetIcon = styled.img`
border-radius: 4px;
height: 24px;
width: 24px;
z-index: 1;
`
const MarketplaceIcon = styled.img`
border-radius: 4px;
height: 24px;
width: 24px;
margin-left: -4px;
margin-right: 12px;
`
const ContentName = styled(ThemedText.SubHeaderSmall)`
color: ${({ theme }) => theme.textPrimary};
line-height: 20px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 40%;
`
const ProceedText = styled.span`
font-weight: 600;
font-size: 12px;
line-height: 16px;
color: ${({ theme }) => theme.textSecondary};
`
const FailedText = styled.span`
font-weight: 600;
font-size: 10px;
line-height: 12px;
color: ${({ theme }) => theme.accentCritical};
margin-left: 4px;
`
const StyledVerifiedIcon = styled(VerifiedIcon)`
height: 16px;
width: 16px;
margin-left: 4px;
`
const IconWrapper = styled.div`
margin-left: auto;
margin-right: 0px;
`
const ButtonRow = styled(Row)`
padding: 0px 16px;
justify-content: space-between;
`
const failedButtonStyle = css`
width: 152px;
cursor: pointer;
padding: 8px 0px;
text-align: center;
font-weight: 600;
font-size: 14px;
line-height: 16px;
border-radius: 12px;
border: none;
&:hover {
opacity: 0.6;
}
`
const RemoveButton = styled.button`
background-color: ${({ theme }) => theme.accentCritical};
color: ${({ theme }) => theme.accentTextDarkPrimary};
${failedButtonStyle}
`
const RetryButton = styled.button`
background-color: ${({ theme }) => theme.backgroundInteractive};
color: ${({ theme }) => theme.textPrimary};
${failedButtonStyle}
`
export const ContentRow = ({
row,
isCollectionApprovalSection,
removeRow,
}: {
row: AssetRow
isCollectionApprovalSection: boolean
removeRow: (row: AssetRow) => void
}) => {
const theme = useTheme()
const rowRef = useRef<HTMLDivElement>()
const failed = row.status === ListingStatus.FAILED || row.status === ListingStatus.REJECTED
useEffect(() => {
row.status === ListingStatus.SIGNING && rowRef.current?.scroll
}, [row.status])
return (
<ContentColumn failed={failed}>
<ContentRowWrapper
active={row.status === ListingStatus.SIGNING || row.status === ListingStatus.APPROVED}
failed={failed}
ref={rowRef}
>
{isCollectionApprovalSection ? <CollectionIcon src={row.images[0]} /> : <AssetIcon src={row.images[0]} />}
<MarketplaceIcon src={row.images[1]} />
<ContentName>{row.name}</ContentName>
{isCollectionApprovalSection && (row as CollectionRow).isVerified && <StyledVerifiedIcon />}
<IconWrapper>
{row.status === ListingStatus.DEFINED || row.status === ListingStatus.PENDING ? (
<Loader
height="14px"
width="14px"
stroke={row.status === ListingStatus.PENDING ? theme.accentAction : theme.textTertiary}
/>
) : row.status === ListingStatus.SIGNING ? (
<ProceedText>
<Trans>Proceed in wallet</Trans>
</ProceedText>
) : row.status === ListingStatus.APPROVED ? (
<Check height="20" width="20" stroke={theme.accentSuccess} />
) : (
failed && (
<Row>
<XOctagon height="20" width="20" color={theme.accentCritical} />
<FailedText>
{row.status === ListingStatus.FAILED ? <Trans>Failed</Trans> : <Trans>Rejected</Trans>}
</FailedText>
</Row>
)
)}
</IconWrapper>
</ContentRowWrapper>
{failed && (
<ButtonRow justify="space-between">
<RemoveButton onClick={() => removeRow(row)}>
<Trans>Remove</Trans>
</RemoveButton>
<RetryButton onClick={row.callback}>
<Trans>Retry</Trans>
</RetryButton>
</ButtonRow>
)}
</ContentColumn>
)
}

View File

@@ -0,0 +1,153 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, useTrace } from '@uniswap/analytics'
import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { getTotalEthValue, signListingRow } from 'nft/components/bag/profile/utils'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import { useNFTList, useSellAsset } from 'nft/hooks'
import { ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils'
import { useEffect, useMemo, useReducer, useState } from 'react'
import { X } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import shallow from 'zustand/shallow'
import { TitleRow } from '../shared'
import { ListModalSection, Section } from './ListModalSection'
import { SuccessScreen } from './SuccessScreen'
const ListModalWrapper = styled.div`
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 420px;
z-index: ${Z_INDEX.modal};
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 20px 24px 24px;
display: flex;
flex-direction: column;
gap: 16px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: 100%;
height: 100%;
}
`
export const ListModal = ({ overlayClick }: { overlayClick: () => void }) => {
const { provider } = useWeb3React()
const signer = provider?.getSigner()
const trace = useTrace({ modal: InterfaceModalName.NFT_LISTING })
const sellAssets = useSellAsset((state) => state.sellAssets)
const {
listingStatus,
setListingStatusAndCallback,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
} = useNFTList(
({
listingStatus,
setListingStatusAndCallback,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
}) => ({
listingStatus,
setListingStatusAndCallback,
setLooksRareNonce,
getLooksRareNonce,
collectionsRequiringApproval,
listings,
}),
shallow
)
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const [openSection, toggleOpenSection] = useReducer(
(s) => (s === Section.APPROVE ? Section.SIGN : Section.APPROVE),
Section.APPROVE
)
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
useEffect(() => {
fetchPrice().then((price) => {
setEthPriceInUSD(price || 0)
})
}, [])
const allCollectionsApproved = useMemo(
() => collectionsRequiringApproval.every((collection) => collection.status === ListingStatus.APPROVED),
[collectionsRequiringApproval]
)
const signListings = async () => {
if (!signer || !provider) return
// sign listings
for (const listing of listings) {
await signListingRow(listing, signer, provider, getLooksRareNonce, setLooksRareNonce, setListingStatusAndCallback)
}
sendAnalyticsEvent(NFTEventName.NFT_LISTING_COMPLETED, {
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
})
}
useEffect(() => {
if (allCollectionsApproved) {
signListings()
openSection === Section.APPROVE && toggleOpenSection()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [allCollectionsApproved])
// In the case that a user removes all listings via retry logic, close modal
useEffect(() => {
!listings.length && overlayClick()
}, [listings, overlayClick])
return (
<Portal>
<Trace modal={InterfaceModalName.NFT_LISTING}>
<ListModalWrapper>
{listingStatus === ListingStatus.APPROVED ? (
<SuccessScreen overlayClick={overlayClick} />
) : (
<>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>List NFTs</Trans>
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
</TitleRow>
<ListModalSection
sectionType={Section.APPROVE}
active={openSection === Section.APPROVE}
content={collectionsRequiringApproval}
toggleSection={toggleOpenSection}
/>
<ListModalSection
sectionType={Section.SIGN}
active={openSection === Section.SIGN}
content={listings}
toggleSection={toggleOpenSection}
/>
</>
)}
</ListModalWrapper>
</Trace>
<Overlay onClick={overlayClick} />
</Portal>
)
}

View File

@@ -0,0 +1,148 @@
import { Plural, Trans } from '@lingui/macro'
import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { ChevronUpIcon, ListingModalWindowActive, ListingModalWindowClosed } from 'nft/components/icons'
import { useSellAsset } from 'nft/hooks'
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import { useMemo } from 'react'
import { Info } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { colors } from 'theme/colors'
import { TRANSITION_DURATIONS } from 'theme/styles'
import { ContentRow } from './ContentRow'
const SectionHeader = styled(Row)`
justify-content: space-between;
`
const SectionTitle = styled(ThemedText.SubHeader)<{ active: boolean; approved: boolean }>`
line-height: 24px;
color: ${({ theme, active, approved }) =>
approved ? theme.accentSuccess : active ? theme.textPrimary : theme.textSecondary};
`
const SectionArrow = styled(ChevronUpIcon)<{ active: boolean }>`
height: 24px;
width: 24px;
cursor: pointer;
transition: ${TRANSITION_DURATIONS.medium}ms;
transform: rotate(${({ active }) => (active ? 0 : 180)}deg);
`
const SectionBody = styled(Column)`
border-left: 1.5px solid ${colors.gray650};
margin-top: 4px;
margin-left: 7px;
padding-top: 4px;
padding-left: 20px;
max-height: 394px;
overflow-y: auto;
${ScrollBarStyles}
`
const StyledInfoIcon = styled(Info)`
height: 16px;
width: 16px;
margin-left: 4px;
color: ${({ theme }) => theme.textSecondary};
`
const ContentRowContainer = styled(Column)`
gap: 8px;
scroll-behavior: smooth;
`
export const enum Section {
APPROVE,
SIGN,
}
interface ListModalSectionProps {
sectionType: Section
active: boolean
content: AssetRow[]
toggleSection: React.DispatchWithoutAction
}
export const ListModalSection = ({ sectionType, active, content, toggleSection }: ListModalSectionProps) => {
const theme = useTheme()
const sellAssets = useSellAsset((state) => state.sellAssets)
const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace)
const allContentApproved = useMemo(() => !content.some((row) => row.status !== ListingStatus.APPROVED), [content])
const isCollectionApprovalSection = sectionType === Section.APPROVE
const removeRow = (row: AssetRow) => {
// collections
if (isCollectionApprovalSection) {
const collectionRow = row as CollectionRow
for (const asset of sellAssets)
if (asset.asset_contract.address === collectionRow.collectionAddress)
removeAssetMarketplace(asset, collectionRow.marketplace)
}
// listings
else {
const listingRow = row as ListingRow
removeAssetMarketplace(listingRow.asset, listingRow.marketplace)
}
}
return (
<Column>
<SectionHeader>
<Row>
{active || allContentApproved ? (
<ListingModalWindowActive fill={allContentApproved ? theme.accentSuccess : theme.accentAction} />
) : (
<ListingModalWindowClosed />
)}
<SectionTitle active={active} marginLeft="12px" approved={allContentApproved}>
{isCollectionApprovalSection ? (
<>
<Trans>Approve</Trans>&nbsp;{content.length}&nbsp;
<Plural value={content.length} _1="Collection" other="Collections" />
</>
) : (
<>
<Trans>Sign</Trans> &nbsp;{content.length}&nbsp;{' '}
<Plural value={content.length} _1="Listing" other="Listings" />
</>
)}
</SectionTitle>
</Row>
<SectionArrow
active={active}
secondaryColor={active ? theme.textPrimary : theme.textSecondary}
onClick={toggleSection}
/>
</SectionHeader>
{active && (
<SectionBody>
{isCollectionApprovalSection && (
<Row height="16px" marginBottom="16px">
<ThemedText.Caption lineHeight="16px" color="textSecondary">
<Trans>Why is a transaction required?</Trans>
</ThemedText.Caption>
<MouseoverTooltip
text={<Trans>Listing an NFT requires a one-time marketplace approval for each NFT collection.</Trans>}
>
<StyledInfoIcon />
</MouseoverTooltip>
</Row>
)}
<ContentRowContainer>
{content.map((row: AssetRow) => (
<ContentRow
row={row}
key={(row?.name ?? '') + row?.images[1]}
removeRow={removeRow}
isCollectionApprovalSection={isCollectionApprovalSection}
/>
))}
</ContentRowContainer>
</SectionBody>
)}
</Column>
)
}

View File

@@ -0,0 +1,129 @@
import { Trans } from '@lingui/macro'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import Row from 'components/Row'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { getTotalEthValue } from 'nft/components/bag/profile/utils'
import { useSellAsset } from 'nft/hooks'
import { formatEth, generateTweetForList, pluralize } from 'nft/utils'
import { useMemo } from 'react'
import { Twitter, X } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { TitleRow } from '../shared'
const SuccessImage = styled.img<{ numImages: number }>`
width: calc(${({ numImages }) => (numImages > 1 ? (numImages > 2 ? '33%' : '50%') : '100%')} - 12px);
border-radius: 12px;
`
const SuccessImageWrapper = styled(Row)`
flex-wrap: wrap;
gap: 12px;
justify-content: center;
overflow-y: scroll;
margin-bottom: 16px;
${ScrollBarStyles}
`
const ProceedsColumn = styled(Column)`
text-align: right;
`
const buttonStyle = css`
width: 182px;
cursor: pointer;
padding: 12px 0px;
text-align: center;
font-weight: 600;
font-size: 16px;
line-height: 20px;
border-radius: 12px;
border: none;
&:hover {
opacity: 0.6;
}
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: 100%;
margin-bottom: 8px;
}
`
const ReturnButton = styled.button`
background-color: ${({ theme }) => theme.backgroundInteractive};
color: ${({ theme }) => theme.textPrimary};
${buttonStyle}
`
const TweetButton = styled.a`
background-color: ${({ theme }) => theme.accentAction};
color: ${({ theme }) => theme.textPrimary};
text-decoration: none;
${buttonStyle}
`
const TweetRow = styled(Row)`
justify-content: center;
gap: 4px;
`
export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => {
const theme = useTheme()
const sellAssets = useSellAsset((state) => state.sellAssets)
const nativeCurrency = useNativeCurrency()
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const parsedAmount = tryParseCurrencyAmount(totalEthListingValue.toString(), nativeCurrency)
const usdcValue = useStablecoinValue(parsedAmount)
return (
<>
<TitleRow>
<ThemedText.HeadlineSmall lineHeight="28px">
<Trans>Successfully listed</Trans>&nbsp;{sellAssets.length > 1 ? ` ${sellAssets.length} ` : ''}
NFT{pluralize(sellAssets.length)}!
</ThemedText.HeadlineSmall>
<X size={24} cursor="pointer" onClick={overlayClick} />
</TitleRow>
<SuccessImageWrapper>
{sellAssets.map((asset) => (
<SuccessImage
src={asset.imageUrl}
key={asset?.asset_contract?.address ?? '' + asset?.tokenId}
numImages={sellAssets.length}
/>
))}
</SuccessImageWrapper>
<Row justify="space-between" align="flex-start" marginBottom="16px">
<ThemedText.SubHeader lineHeight="24px">
<Trans>Proceeds if sold</Trans>
</ThemedText.SubHeader>
<ProceedsColumn>
<ThemedText.SubHeader lineHeight="24px">{formatEth(totalEthListingValue)} ETH</ThemedText.SubHeader>
{usdcValue && (
<ThemedText.BodySmall lineHeight="20px" color="textSecondary">
{formatCurrencyAmount(usdcValue, NumberType.FiatTokenPrice)}
</ThemedText.BodySmall>
)}
</ProceedsColumn>
</Row>
<Row justify="space-between" flexWrap="wrap">
<ReturnButton onClick={() => window.location.reload()}>
<Trans>Return to My NFTs</Trans>
</ReturnButton>
<TweetButton href={generateTweetForList(sellAssets)} target="_blank" rel="noreferrer">
<TweetRow>
<Twitter height={20} width={20} color={theme.textPrimary} fill={theme.textPrimary} />
<Trans>Share on Twitter</Trans>
</TweetRow>
</TweetButton>
</Row>
</>
)
}

View File

@@ -1,53 +1,56 @@
import Column from 'components/Column'
import Row from 'components/Row'
import { RowsCollpsedIcon, RowsExpandedIcon, VerifiedIcon } from 'nft/components/icons'
import { VerifiedIcon } from 'nft/components/icons'
import { useSellAsset } from 'nft/hooks'
import { ListingMarket, WalletAsset } from 'nft/types'
import { Dispatch, useEffect, useState } from 'react'
import styled, { css } from 'styled-components/macro'
import { Dispatch, useEffect, useReducer, useState } from 'react'
import { Trash2 } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { opacify } from 'theme/utils'
import { MarketplaceRow } from './MarketplaceRow'
import { SetPriceMethod } from './NFTListingsGrid'
import { RemoveIconWrap } from './shared'
const NFTListRowWrapper = styled(Row)`
margin: 24px 0px;
padding: 24px 0px;
align-items: center;
border-radius: 8px;
&:hover {
background: ${({ theme }) => opacify(24, theme.backgroundOutline)};
}
`
const RemoveIconContainer = styled.div`
width: 48px;
height: 44px;
padding-left: 12px;
align-self: flex-start;
align-items: center;
display: flex;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: none;
}
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const NFTInfoWrapper = styled(Row)`
align-items: center;
min-width: 0px;
flex: 2;
flex: 1.5;
margin-bottom: auto;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 1.5;
}
`
const ExpandMarketIconWrapper = styled.div`
cursor: pointer;
margin-right: 8px;
`
const NFTImageWrapper = styled.div`
position: relative;
cursor: pointer;
height: 48px;
margin-right: 8px;
`
const NFTImage = styled.img`
width: 48px;
height: 48px;
border-radius: 8px;
`
const RemoveIcon = styled.img`
width: 32px;
height: 32px;
margin-right: 8px;
`
const HideTextOverflow = css`
@@ -77,7 +80,12 @@ const CollectionName = styled(ThemedText.BodySmall)`
const MarketPlaceRowWrapper = styled(Column)`
gap: 24px;
flex: 1;
flex: 1.5;
margin-right: 12px;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 2;
}
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 3;
@@ -103,37 +111,41 @@ export const NFTListRow = ({
setGlobalPrice,
selectedMarkets,
}: NFTListRowProps) => {
const [expandMarketplaceRows, setExpandMarketplaceRows] = useState(false)
const [expandMarketplaceRows, toggleExpandMarketplaceRows] = useReducer((s) => !s, false)
const removeAsset = useSellAsset((state) => state.removeSellAsset)
const [localMarkets, setLocalMarkets] = useState([])
const [hovered, setHovered] = useState(false)
const handleHover = () => setHovered(!hovered)
const [localMarkets, setLocalMarkets] = useState<ListingMarket[]>([])
const [hovered, toggleHovered] = useReducer((s) => !s, false)
const theme = useTheme()
useEffect(() => {
setLocalMarkets(JSON.parse(JSON.stringify(selectedMarkets)))
selectedMarkets.length < 2 && setExpandMarketplaceRows(false)
}, [selectedMarkets])
selectedMarkets.length < 2 && expandMarketplaceRows && toggleExpandMarketplaceRows()
}, [expandMarketplaceRows, selectedMarkets])
return (
<NFTListRowWrapper>
<NFTInfoWrapper>
{localMarkets.length > 1 && (
<ExpandMarketIconWrapper onClick={() => setExpandMarketplaceRows(!expandMarketplaceRows)}>
{expandMarketplaceRows ? <RowsExpandedIcon /> : <RowsCollpsedIcon />}
</ExpandMarketIconWrapper>
<NFTListRowWrapper
onMouseEnter={() => {
!hovered && toggleHovered()
}}
onMouseLeave={() => {
hovered && toggleHovered()
}}
>
<RemoveIconContainer>
{hovered && (
<Trash2
size={20}
color={theme.textSecondary}
cursor="pointer"
onClick={() => {
removeAsset(asset)
}}
/>
)}
<NFTImageWrapper
onMouseEnter={handleHover}
onMouseLeave={handleHover}
onClick={() => {
removeAsset(asset)
}}
>
<RemoveIconWrap hovered={hovered}>
<RemoveIcon src="/nft/svgs/minusCircle.svg" alt="Remove item" />
</RemoveIconWrap>
<NFTImage alt={asset.name} src={asset.imageUrl || '/nft/svgs/image-placeholder.svg'} />
</NFTImageWrapper>
</RemoveIconContainer>
<NFTInfoWrapper>
<NFTImage alt={asset.name} src={asset.imageUrl || '/nft/svgs/image-placeholder.svg'} />
<TokenInfoWrapper>
<TokenName>{asset.name ? asset.name : `#${asset.tokenId}`}</TokenName>
<CollectionName>
@@ -154,8 +166,10 @@ export const NFTListRow = ({
removeMarket={() => localMarkets.splice(index, 1)}
asset={asset}
showMarketplaceLogo={true}
key={index}
key={asset.name + market.name}
expandMarketplaceRows={expandMarketplaceRows}
rowHovered={hovered}
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
/>
)
})
@@ -167,6 +181,8 @@ export const NFTListRow = ({
selectedMarkets={localMarkets}
asset={asset}
showMarketplaceLogo={false}
rowHovered={hovered}
toggleExpandMarketplaceRows={toggleExpandMarketplaceRows}
/>
)}
</MarketPlaceRowWrapper>

View File

@@ -1,15 +1,16 @@
import { Trans } from '@lingui/macro'
// eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { SortDropdown } from 'nft/components/common/SortDropdown'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useSellAsset } from 'nft/hooks'
import { DropDownOption, ListingMarket } from 'nft/types'
import { useMemo, useState } from 'react'
import { useMemo, useReducer, useRef, useState } from 'react'
import { ChevronDown } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { Dropdown } from './Dropdown'
import { NFTListRow } from './NFTListRow'
const TableHeader = styled.div`
@@ -25,18 +26,19 @@ const TableHeader = styled.div`
font-size: 14px;
font-weight: normal;
line-height: 20px;
`
const NFTHeader = styled.div`
flex: 2;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 1.5;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
margin-left: 48px;
}
`
const NFTHeader = styled.div`
flex: 1.5;
`
const PriceHeaders = styled(Row)`
flex: 1;
flex: 1.5;
margin-right: 12px;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
flex: 3;
@@ -52,14 +54,58 @@ const PriceInfoHeader = styled.div`
}
`
const DropdownWrapper = styled.div`
flex: 2;
const DropdownAndHeaderWrapper = styled(Row)`
flex: 3;
gap: 4px;
`
const DropdownPromptContainer = styled(Column)`
position: relative;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
display: none;
}
`
const DropdownPrompt = styled(Row)`
gap: 4px;
background-color: ${({ theme }) => theme.backgroundInteractive};
cursor: pointer;
font-weight: 600;
font-size: 12px;
line-height: 16px;
border-radius: 4px;
padding: 2px 6px;
width: min-content;
white-space: nowrap;
color: ${({ theme }) => theme.textPrimary};
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const DropdownChevron = styled(ChevronDown)<{ isOpen: boolean }>`
height: 16px;
width: 16px;
color: ${({ theme }) => theme.textSecondary};
transform: ${({ isOpen }) => isOpen && 'rotate(180deg)'};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `transform ${duration.fast} ${timing.ease}`};
`
const DropdownContainer = styled.div`
position: absolute;
top: 36px;
right: 0px;
`
const FeeUserReceivesSharedStyles = css`
display: none;
justify-content: flex-end;
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
@media screen and (min-width: ${BREAKPOINTS.md}px) {
display: flex;
}
`
@@ -80,38 +126,81 @@ const RowDivider = styled.hr`
border-radius: 20px;
border-width: 0.5px;
border-style: solid;
margin: 0;
border-color: ${({ theme }) => theme.backgroundInteractive};
`
export enum SetPriceMethod {
SAME_PRICE,
FLOOR_PRICE,
PREV_LISTING,
LAST_PRICE,
CUSTOM,
}
export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingMarket[] }) => {
const sellAssets = useSellAsset((state) => state.sellAssets)
const [globalPriceMethod, setGlobalPriceMethod] = useState<SetPriceMethod>()
const [globalPriceMethod, setGlobalPriceMethod] = useState(SetPriceMethod.CUSTOM)
const [globalPrice, setGlobalPrice] = useState<number>()
const [showDropdown, toggleShowDropdown] = useReducer((s) => !s, false)
const dropdownRef = useRef<HTMLDivElement>(null)
useOnClickOutside(dropdownRef, showDropdown ? toggleShowDropdown : undefined)
const priceDropdownOptions: DropDownOption[] = useMemo(
() => [
{
displayText: 'Same price',
onClick: () => setGlobalPriceMethod(SetPriceMethod.SAME_PRICE),
displayText: 'Custom',
isSelected: globalPriceMethod === SetPriceMethod.CUSTOM,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.CUSTOM)
toggleShowDropdown()
},
},
{
displayText: 'Floor price',
onClick: () => setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE),
isSelected: globalPriceMethod === SetPriceMethod.FLOOR_PRICE,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.FLOOR_PRICE)
toggleShowDropdown()
},
},
{
displayText: 'Prev. listing',
onClick: () => setGlobalPriceMethod(SetPriceMethod.PREV_LISTING),
displayText: 'Last price',
isSelected: globalPriceMethod === SetPriceMethod.LAST_PRICE,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.LAST_PRICE)
toggleShowDropdown()
},
},
{
displayText: 'Same price',
isSelected: globalPriceMethod === SetPriceMethod.SAME_PRICE,
onClick: () => {
setGlobalPriceMethod(SetPriceMethod.SAME_PRICE)
toggleShowDropdown()
},
},
],
[]
[globalPriceMethod]
)
let prompt
switch (globalPriceMethod) {
case SetPriceMethod.CUSTOM:
prompt = <Trans>Custom</Trans>
break
case SetPriceMethod.FLOOR_PRICE:
prompt = <Trans>Floor price</Trans>
break
case SetPriceMethod.LAST_PRICE:
prompt = <Trans>Last Price</Trans>
break
case SetPriceMethod.SAME_PRICE:
prompt = <Trans>Same Price</Trans>
break
default:
break
}
return (
<Column>
<TableHeader>
@@ -126,9 +215,19 @@ export const NFTListingsGrid = ({ selectedMarkets }: { selectedMarkets: ListingM
<Trans>Last</Trans>
</PriceInfoHeader>
<DropdownWrapper>
<SortDropdown dropDownOptions={priceDropdownOptions} mini miniPrompt={t`Set price by`} />
</DropdownWrapper>
<DropdownAndHeaderWrapper ref={dropdownRef}>
<Trans>Price</Trans>
<DropdownPromptContainer>
<DropdownPrompt onClick={toggleShowDropdown}>
{prompt} <DropdownChevron isOpen={showDropdown} />
</DropdownPrompt>
{showDropdown && (
<DropdownContainer>
<Dropdown dropDownOptions={priceDropdownOptions} width={200} />
</DropdownContainer>
)}
</DropdownPromptContainer>
</DropdownAndHeaderWrapper>
<FeeHeader>
<Trans>Fees</Trans>

View File

@@ -8,6 +8,7 @@ import { useSellAsset } from 'nft/hooks'
import { ListingWarning, WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils/currency'
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { colors } from 'theme/colors'
@@ -42,7 +43,11 @@ const GlobalPriceIcon = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
`
const WarningMessage = styled(Row)<{ warningType: WarningType }>`
const WarningRow = styled(Row)`
gap: 4px;
`
const WarningMessage = styled(Row)<{ $color: string }>`
top: 52px;
width: max-content;
position: absolute;
@@ -50,17 +55,16 @@ const WarningMessage = styled(Row)<{ warningType: WarningType }>`
font-weight: 600;
font-size: 10px;
line-height: 12px;
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? colors.red400 : theme.textSecondary)};
color: ${({ $color }) => $color};
@media screen and (min-width: ${BREAKPOINTS.md}px) {
right: unset;
}
`
const WarningAction = styled.div<{ warningType: WarningType }>`
margin-left: 8px;
const WarningAction = styled.div`
cursor: pointer;
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? theme.accentAction : colors.red400)};
color: ${({ theme }) => theme.accentAction};
`
enum WarningType {
@@ -73,10 +77,10 @@ const getWarningMessage = (warning: WarningType) => {
let message = <></>
switch (warning) {
case WarningType.BELOW_FLOOR:
message = <Trans>LISTING BELOW FLOOR </Trans>
message = <Trans>below floor price.</Trans>
break
case WarningType.ALREADY_LISTED:
message = <Trans>ALREADY LISTED FOR </Trans>
message = <Trans>Already listed at</Trans>
break
}
return message
@@ -107,6 +111,7 @@ export const PriceTextInput = ({
const [warningType, setWarningType] = useState(WarningType.NONE)
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
const showResolveIssues = useSellAsset((state) => state.showResolveIssues)
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
const theme = useTheme()
@@ -121,9 +126,16 @@ export const PriceTextInput = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPrice])
const borderColor =
warningType !== WarningType.NONE && !focused
const percentBelowFloor = (1 - (listPrice ?? 0) / (asset.floorPrice ?? 0)) * 100
const warningColor =
showResolveIssues && !listPrice
? colors.red400
: warningType !== WarningType.NONE && !focused
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
warningType === WarningType.ALREADY_LISTED
? colors.red400
: theme.accentWarning
: isGlobalPrice
? theme.accentAction
: listPrice != null
@@ -132,7 +144,7 @@ export const PriceTextInput = ({
return (
<PriceTextInputWrapper>
<InputWrapper borderColor={borderColor}>
<InputWrapper borderColor={warningColor}>
<NumericInput
as="input"
pattern="[0-9]"
@@ -164,27 +176,27 @@ export const PriceTextInput = ({
</GlobalPriceIcon>
)}
</InputWrapper>
<WarningMessage warningType={warningType}>
<WarningMessage $color={warningColor}>
{warning
? warning.message
: warningType !== WarningType.NONE && (
<>
{getWarningMessage(warningType)}
&nbsp;
{warningType === WarningType.BELOW_FLOOR
? formatEth(asset?.floorPrice ?? 0)
: formatEth(asset?.floor_sell_order_price ?? 0)}
ETH
<WarningRow>
<AlertTriangle height={16} width={16} color={warningColor} />
<span>
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
{getWarningMessage(warningType)}
&nbsp;
{warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
</span>
<WarningAction
warningType={warningType}
onClick={() => {
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
setWarningType(WarningType.NONE)
}}
>
{warningType === WarningType.BELOW_FLOOR ? <Trans>DISMISS</Trans> : <Trans>REMOVE ITEM</Trans>}
{warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
</WarningAction>
</>
</WarningRow>
)}
</WarningMessage>
</PriceTextInputWrapper>

View File

@@ -79,10 +79,10 @@ const HeaderButtonWrap = styled(Row)`
border-radius: 12px;
width: 180px;
justify-content: space-between;
background: ${({ theme }) => theme.backgroundModule};
background: ${({ theme }) => theme.backgroundInteractive};
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
opacity: ${({ theme }) => theme.opacity.hover};
}
@media screen and (min-width: ${SMALL_MEDIA_BREAKPOINT}) {
width: 220px;
@@ -124,7 +124,7 @@ const ModalWrapper = styled.div`
const DropdownWrapper = styled(Column)<{ isOpen: boolean }>`
padding: 16px 0px;
background-color: ${({ theme }) => theme.backgroundModule};
background-color: ${({ theme }) => theme.backgroundSurface};
display: ${({ isOpen }) => (isOpen ? 'flex' : 'none')};
position: absolute;
top: 52px;

View File

@@ -1,15 +1,17 @@
import { Plural } from '@lingui/macro'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import ms from 'ms.macro'
import { SortDropdown } from 'nft/components/common/SortDropdown'
import { Column, Row } from 'nft/components/Flex'
import { NumericInput } from 'nft/components/layout/Input'
import { bodySmall, buttonTextMedium, caption } from 'nft/css/common.css'
import { bodySmall, caption } from 'nft/css/common.css'
import { useSellAsset } from 'nft/hooks'
import { DropDownOption } from 'nft/types'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { useEffect, useMemo, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import { useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { AlertTriangle, ChevronDown } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { Dropdown } from './Dropdown'
const ModalWrapper = styled(Column)`
gap: 4px;
@@ -25,17 +27,41 @@ const InputWrapper = styled(Row)<{ isInvalid: boolean }>`
border-color: ${({ isInvalid, theme }) => (isInvalid ? theme.accentCritical : theme.backgroundOutline)};
`
const DropdownWrapper = styled(ThemedText.BodyPrimary)`
const DropdownPrompt = styled(Row)`
gap: 4px;
background-color: ${({ theme }) => theme.backgroundInteractive};
cursor: pointer;
display: flex;
justify-content: flex-end;
height: min-content;
width: 80px;
font-weight: 600;
font-size: 12px;
line-height: 16px;
border-radius: 8px;
padding: 6px 4px 6px 8px;
width: min-content;
white-space: nowrap;
color: ${({ theme }) => theme.textPrimary};
&:hover {
background-color: ${({ theme }) => theme.backgroundInteractive};
opacity: ${({ theme }) => theme.opacity.hover};
}
border-radius: 12px;
padding: 8px;
`
const DropdownChevron = styled(ChevronDown)<{ isOpen: boolean }>`
height: 16px;
width: 16px;
color: ${({ theme }) => theme.textSecondary};
transform: ${({ isOpen }) => isOpen && 'rotate(180deg)'};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `transform ${duration.fast} ${timing.ease}`};
`
const DropdownContainer = styled.div`
position: absolute;
top: 48px;
right: 0px;
z-index: ${Z_INDEX.dropdown};
`
const ErrorMessage = styled(Row)`
@@ -43,6 +69,7 @@ const ErrorMessage = styled(Row)`
gap: 4px;
position: absolute;
top: 44px;
white-space: nowrap;
`
const WarningIcon = styled(AlertTriangle)`
@@ -65,39 +92,73 @@ enum ErrorState {
export const SetDurationModal = () => {
const [duration, setDuration] = useState(Duration.day)
const [displayDuration, setDisplayDuration] = useState(Duration.day)
const [amount, setAmount] = useState('7')
const [errorState, setErrorState] = useState(ErrorState.valid)
const setGlobalExpiration = useSellAsset((state) => state.setGlobalExpiration)
const [showDropdown, toggleShowDropdown] = useReducer((s) => !s, false)
const durationDropdownRef = useRef<HTMLDivElement>(null)
useOnClickOutside(durationDropdownRef, showDropdown ? toggleShowDropdown : undefined)
const setCustomExpiration = (event: React.ChangeEvent<HTMLInputElement>) => {
setAmount(event.target.value.length ? event.target.value : '')
setDuration(displayDuration)
}
const selectDuration = (duration: Duration) => {
setDuration(duration)
setDisplayDuration(duration)
}
const durationOptions: DropDownOption[] = useMemo(
() => [
{
displayText: 'Hours',
onClick: () => selectDuration(Duration.hour),
displayText: 'hours',
isSelected: duration === Duration.hour,
onClick: () => {
setDuration(Duration.hour)
toggleShowDropdown()
},
},
{
displayText: 'Days',
onClick: () => selectDuration(Duration.day),
displayText: 'days',
isSelected: duration === Duration.day,
onClick: () => {
setDuration(Duration.day)
toggleShowDropdown()
},
},
{
displayText: 'Weeks',
onClick: () => selectDuration(Duration.week),
displayText: 'weeks',
isSelected: duration === Duration.week,
onClick: () => {
setDuration(Duration.week)
toggleShowDropdown()
},
},
{
displayText: 'Months',
onClick: () => selectDuration(Duration.month),
displayText: 'months',
isSelected: duration === Duration.month,
onClick: () => {
setDuration(Duration.month)
toggleShowDropdown()
},
},
],
[]
[duration]
)
let prompt
switch (duration) {
case Duration.hour:
prompt = <Plural value={amount} _1="hour" other="hours" />
break
case Duration.day:
prompt = <Plural value={amount} _1="day" other="days" />
break
case Duration.week:
prompt = <Plural value={amount} _1="week" other="weeks" />
break
case Duration.month:
prompt = <Plural value={amount} _1="month" other="months" />
break
default:
break
}
useEffect(() => {
const expiration = convertDurationToExpiration(parseFloat(amount), duration)
@@ -108,7 +169,7 @@ export const SetDurationModal = () => {
}, [amount, duration, setGlobalExpiration])
return (
<ModalWrapper>
<ModalWrapper ref={durationDropdownRef}>
<InputWrapper isInvalid={errorState !== ErrorState.valid}>
<NumericInput
as="input"
@@ -124,15 +185,14 @@ export const SetDurationModal = () => {
onChange={setCustomExpiration}
flexShrink="0"
/>
<DropdownWrapper className={buttonTextMedium}>
<SortDropdown
dropDownOptions={durationOptions}
mini
miniPrompt={displayDuration + (displayDuration === duration ? pluralize(parseFloat(amount)) : 's')}
left={38}
top={38}
/>
</DropdownWrapper>
<DropdownPrompt onClick={toggleShowDropdown}>
{prompt} <DropdownChevron isOpen={showDropdown} />
</DropdownPrompt>
{showDropdown && (
<DropdownContainer>
<Dropdown dropDownOptions={durationOptions} width={125} />
</DropdownContainer>
)}
</InputWrapper>
{errorState !== ErrorState.valid && (
<ErrorMessage className={caption}>

View File

@@ -1,3 +1,4 @@
import Row from 'components/Row'
import styled from 'styled-components/macro'
export const RemoveIconWrap = styled.div<{ hovered: boolean }>`
@@ -8,3 +9,8 @@ export const RemoveIconWrap = styled.div<{ hovered: boolean }>`
width: 32px;
visibility: ${({ hovered }) => (hovered ? 'visible' : 'hidden')};
`
export const TitleRow = styled(Row)`
justify-content: space-between;
margin-bottom: 8px;
`

View File

@@ -8,7 +8,6 @@ export * from './useMarketplaceSelect'
export * from './useNFTList'
export * from './useNFTSelect'
export * from './useProfilePageState'
export * from './useSearchHistory'
export * from './useSelectAsset'
export * from './useSellAsset'
export * from './useSendTransaction'

View File

@@ -12,6 +12,12 @@ interface NFTListState {
setListingStatus: (status: ListingStatus) => void
setListings: (listings: ListingRow[]) => void
setCollectionsRequiringApproval: (collections: CollectionRow[]) => void
setListingStatusAndCallback: (listing: ListingRow, status: ListingStatus, callback?: () => Promise<void>) => void
setCollectionStatusAndCallback: (
collection: CollectionRow,
status: ListingStatus,
callback?: () => Promise<void>
) => void
}
export const useNFTList = create<NFTListState>()(
@@ -34,19 +40,23 @@ export const useNFTList = create<NFTListState>()(
setListings: (listings) =>
set(() => {
const updatedListings = listings.map((listing) => {
const oldStatus = get().listings.find(
const oldListing = get().listings.find(
(oldListing) =>
oldListing.asset.asset_contract.address === listing.asset.asset_contract.address &&
oldListing.asset.tokenId === listing.asset.tokenId &&
oldListing.marketplace.name === listing.marketplace.name &&
oldListing.price === listing.price
)?.status
)
const oldStatus = oldListing?.status
const oldCallback = oldListing?.callback
const status = () => {
switch (oldStatus) {
case ListingStatus.APPROVED:
return ListingStatus.APPROVED
case ListingStatus.FAILED:
return listing.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.FAILED
case ListingStatus.REJECTED:
return listing.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.REJECTED
default:
return listing.status
}
@@ -54,6 +64,7 @@ export const useNFTList = create<NFTListState>()(
return {
...listing,
status: status(),
callback: oldCallback ?? listing.callback,
}
})
return {
@@ -62,7 +73,75 @@ export const useNFTList = create<NFTListState>()(
}),
setCollectionsRequiringApproval: (collections) =>
set(() => {
return { collectionsRequiringApproval: collections }
const updatedCollections = collections.map((collection) => {
const oldCollection = get().collectionsRequiringApproval.find(
(oldCollection) =>
oldCollection.collectionAddress === collection.collectionAddress &&
oldCollection.marketplace.name === collection.marketplace.name
)
const oldStatus = oldCollection?.status
const oldCallback = oldCollection?.callback
const status = () => {
switch (oldStatus) {
case ListingStatus.APPROVED:
return ListingStatus.APPROVED
case ListingStatus.FAILED:
return collection.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.FAILED
case ListingStatus.REJECTED:
return collection.status === ListingStatus.SIGNING ? ListingStatus.SIGNING : ListingStatus.REJECTED
default:
return collection.status
}
}
return {
...collection,
status: status(),
callback: oldCallback ?? collection.callback,
}
})
return {
collectionsRequiringApproval: updatedCollections,
}
}),
setListingStatusAndCallback: (listing, status, callback) =>
set(({ listings }) => {
const listingsCopy = [...listings]
const oldListingIndex = listingsCopy.findIndex(
(oldListing) =>
oldListing.name === listing.name &&
oldListing.price === listing.price &&
oldListing.marketplace.name === listing.marketplace.name
)
if (oldListingIndex > -1) {
const updatedListing = {
...listings[oldListingIndex],
status,
callback: callback ?? listings[oldListingIndex].callback,
}
listingsCopy.splice(oldListingIndex, 1, updatedListing)
}
return {
listings: listingsCopy,
}
}),
setCollectionStatusAndCallback: (collection, status, callback) =>
set(({ collectionsRequiringApproval }) => {
const collectionsCopy = [...collectionsRequiringApproval]
const oldCollectionIndex = collectionsCopy.findIndex(
(oldCollection) =>
oldCollection.name === collection.name && oldCollection.marketplace.name === collection.marketplace.name
)
if (oldCollectionIndex > -1) {
const updatedCollection = {
...collectionsCopy[oldCollectionIndex],
status,
callback: callback ?? collectionsCopy[oldCollectionIndex].callback,
}
collectionsCopy.splice(oldCollectionIndex, 1, updatedCollection)
}
return {
collectionsRequiringApproval: collectionsCopy,
}
}),
}))
)

View File

@@ -0,0 +1,29 @@
import { Currency, CurrencyAmount, NativeCurrency, Token, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useBestTrade } from 'hooks/useBestTrade'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
export default function usePayWithAnyTokenSwap(
inputCurrency?: Currency,
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
maximumAmountIn: CurrencyAmount<Token> | undefined
} {
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined)
const allowedSlippage = useAutoSlippageTolerance(trade)
const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
}, [allowedSlippage, trade])
return useMemo(() => {
return {
state,
trade,
maximumAmountIn,
}
}, [maximumAmountIn, state, trade])
}

View File

@@ -0,0 +1,48 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
import { useCallback, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
export default function usePermit2Approval(
amount?: CurrencyAmount<Token>,
maximumAmount?: CurrencyAmount<Token>,
enabled?: boolean
) {
const { chainId } = useWeb3React()
const allowance = usePermit2Allowance(
enabled ? maximumAmount ?? (amount?.currency.isToken ? (amount as CurrencyAmount<Token>) : undefined) : undefined,
enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
)
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
const [isAllowancePending, setIsAllowancePending] = useState(false)
const updateAllowance = useCallback(async () => {
invariant(allowance.state === AllowanceState.REQUIRED)
setIsAllowancePending(true)
try {
await allowance.approveAndPermit()
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
chain_id: chainId,
token_symbol: maximumAmount?.currency.symbol,
token_address: maximumAmount?.currency.address,
})
} catch (e) {
console.error(e)
} finally {
setIsAllowancePending(false)
}
}, [allowance, chainId, maximumAmount?.currency.address, maximumAmount?.currency.symbol])
return useMemo(() => {
return {
allowance,
isApprovalLoading,
isAllowancePending,
updateAllowance,
}
}, [allowance, isAllowancePending, isApprovalLoading, updateAllowance])
}

View File

@@ -1,35 +0,0 @@
import { FungibleToken, GenieCollection } from 'nft/types'
import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface SearchHistoryProps {
history: (FungibleToken | GenieCollection)[]
addItem: (item: FungibleToken | GenieCollection) => void
updateItem: (update: FungibleToken | GenieCollection) => void
}
export const useSearchHistory = create<SearchHistoryProps>()(
persist(
devtools((set) => ({
history: [],
addItem: (item: FungibleToken | GenieCollection) => {
set(({ history }) => {
const historyCopy = [...history]
if (historyCopy.length === 0 || historyCopy[0].address !== item.address) historyCopy.unshift(item)
return { history: historyCopy }
})
},
updateItem: (update: FungibleToken | GenieCollection) => {
set(({ history }) => {
const index = history.findIndex((item) => item.address === update.address)
if (index === -1) return { history }
const historyCopy = [...history]
historyCopy[index] = update
return { history: historyCopy }
})
},
})),
{ name: 'useSearchHistory' }
)
)

View File

@@ -5,6 +5,7 @@ import { ListingMarket, ListingWarning, WalletAsset } from '../types'
interface SellAssetState {
sellAssets: WalletAsset[]
showResolveIssues: boolean
selectSellAsset: (asset: WalletAsset) => void
removeSellAsset: (asset: WalletAsset) => void
reset: () => void
@@ -12,15 +13,18 @@ interface SellAssetState {
setAssetListPrice: (asset: WalletAsset, price?: number, marketplace?: ListingMarket) => void
setGlobalMarketplaces: (marketplaces: ListingMarket[]) => void
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
// TODO: After merging v2, see if this marketplace logic can be removed
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
removeAllMarketplaceWarnings: () => void
toggleShowResolveIssues: () => void
}
export const useSellAsset = create<SellAssetState>()(
devtools(
(set) => ({
sellAssets: [],
showResolveIssues: false,
selectSellAsset: (asset) =>
set(({ sellAssets }) => {
if (sellAssets.length === 0) return { sellAssets: [asset] }
@@ -152,6 +156,11 @@ export const useSellAsset = create<SellAssetState>()(
return { sellAssets: assetsCopy }
})
},
toggleShowResolveIssues: () => {
set(({ showResolveIssues }) => {
return { showResolveIssues: !showResolveIssues }
})
},
}),
{ name: 'useSelectAsset' }
)

View File

@@ -14,14 +14,14 @@ import { useToggleWalletModal } from 'state/application/hooks'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { LIST_PAGE_MARGIN } from './shared'
import { LIST_PAGE_MARGIN, LIST_PAGE_MARGIN_MOBILE, LIST_PAGE_MARGIN_TABLET } from './shared'
const ProfilePageWrapper = styled.div`
height: 100%;
width: 100%;
scrollbar-width: none;
@media screen and (min-width: ${BREAKPOINTS.md}px) {
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
height: auto;
}
`
@@ -33,6 +33,16 @@ const LoadedAccountPage = styled.div<{ cartExpanded: boolean; isOnV2ListPage: bo
isOnV2ListPage ? LIST_PAGE_MARGIN * 2 : cartExpanded ? XXXL_BAG_WIDTH : 0}px
);
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN : 0)}px;
@media screen and (max-width: ${BREAKPOINTS.lg}px) {
width: calc(100% - ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_TABLET * 2 : 0)}px);
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_TABLET : 0)}px;
}
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: calc(100% - ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_MOBILE * 2 : 0)}px);
margin: 0px ${({ isOnV2ListPage }) => (isOnV2ListPage ? LIST_PAGE_MARGIN_MOBILE : 0)}px;
}
`
const Center = styled.div`

View File

@@ -1 +1,3 @@
export const LIST_PAGE_MARGIN = 156
export const LIST_PAGE_MARGIN_TABLET = 60
export const LIST_PAGE_MARGIN_MOBILE = 16

View File

@@ -1,21 +0,0 @@
import { FungibleToken } from '../../types'
const TOKEN_API_URL = process.env.REACT_APP_TEMP_API_URL
export const fetchSearchTokens = async (tokenQuery: string): Promise<FungibleToken[]> => {
if (!TOKEN_API_URL) return Promise.resolve([])
const url = `${TOKEN_API_URL}/tokens/search?tokenQuery=${tokenQuery}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await r.json()
// TODO Undo favoritism
return data.data
? data.data.sort((a: FungibleToken, b: FungibleToken) => (b.name === 'Uniswap' ? 1 : b.volume24h - a.volume24h))
: []
}

View File

@@ -1,19 +0,0 @@
import { unwrapToken } from 'graphql/data/util'
import { FungibleToken } from '../../types'
const TOKEN_API_URL = process.env.REACT_APP_TEMP_API_URL
export const fetchTrendingTokens = async (numTokens?: number): Promise<FungibleToken[]> => {
if (!TOKEN_API_URL) return Promise.resolve([])
const url = `${TOKEN_API_URL}/tokens/trending${numTokens ? `?numTokens=${numTokens}` : ''}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const { data } = (await r.json()) as { data: FungibleToken[] }
return data ? data.map((token) => unwrapToken(token.chainId, token)) : []
}

Some files were not shown because too many files have changed in this diff Show More