Compare commits

..

51 Commits

Author SHA1 Message Date
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
100 changed files with 3982 additions and 1667 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

@@ -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

@@ -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

@@ -4,7 +4,6 @@ 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 TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
@@ -25,9 +24,6 @@ import styled from 'styled-components/macro'
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import * as styles from './SearchBar.css'
const StyledLogoContainer = styled(LogoContainer)`
margin-right: 8px;
`
const PriceChangeContainer = styled.div`
display: flex;
align-items: center;
@@ -160,7 +156,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, toggleOpen, token, eventProperties])
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
const [bridgedAddress, bridgedChain] = useBridgedAddress(token)
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
// Close the modal on escape
useEffect(() => {
@@ -190,17 +186,15 @@ 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>
<AssetLogo
isNative={token.address === NATIVE_CHAIN_ID}
address={token.address}
chainId={token.chainId}
symbol={token.symbol}
size="36px"
backupImg={token.logoURI}
style={{ margin: '8px 8px 8px 0' }}
/>
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>

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

@@ -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) => {
@@ -153,6 +152,8 @@ export default function TokenDetails({
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;
@@ -442,18 +424,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 +446,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,
@@ -482,13 +463,10 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
>
<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

@@ -31,12 +31,12 @@ 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/'
@@ -45,16 +45,25 @@ 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()
@@ -159,7 +168,7 @@ 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}
@@ -180,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

@@ -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

@@ -14,8 +14,8 @@ 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
@@ -23,31 +23,39 @@ gql`
address
symbol
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 +66,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!) {
@@ -26,24 +34,30 @@ gql`
address
symbol
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 +67,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 +82,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 +112,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 +139,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 +148,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 +164,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

@@ -386,6 +386,7 @@ export type NftCollectionTraitStats = {
export type NftCollectionsFilterInput = {
addresses?: InputMaybe<Array<Scalars['String']>>;
nameQuery?: InputMaybe<Scalars['String']>;
};
export type NftContract = IContract & {
@@ -468,12 +469,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 +538,22 @@ 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;
};
/** 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 +579,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 +594,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 +632,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 +712,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 +746,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 +858,30 @@ export enum TokenStandard {
Native = 'NATIVE'
}
export type TokenTradeInput = {
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 +894,11 @@ export type TokenTransfer = {
transactedValue?: Maybe<Amount>;
};
export type TradePoolInput = {
pair?: InputMaybe<PairInput>;
pool?: InputMaybe<PoolInput>;
};
export type Transaction = {
__typename?: 'Transaction';
blockNumber: Scalars['Int'];
@@ -826,19 +925,21 @@ export enum TransactionStatus {
}
export type TokenQueryVariables = Exact<{
contract: ContractInput;
chain: Chain;
address?: InputMaybe<Scalars['String']>;
}>;
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 TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, 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 +947,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, 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 +955,7 @@ 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 AssetQueryVariables = Exact<{
address: Scalars['String'];
@@ -895,12 +996,22 @@ 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 TokenDocument = gql`
query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) {
query Token($chain: Chain!, $address: String = null) {
token(chain: $chain, address: $address) {
id
decimals
name
@@ -908,31 +1019,39 @@ export const TokenDocument = gql`
address
symbol
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 +1072,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 +1089,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 +1122,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'
* },
* });
@@ -1021,24 +1148,30 @@ export const TopTokens100Document = gql`
address
symbol
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 +1209,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
}
@@ -1487,6 +1624,7 @@ export const NftBalanceDocument = gql`
url
}
name
twitterName
nftContracts {
address
chain
@@ -1586,4 +1724,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,31 @@
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,

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

@@ -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,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

@@ -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

@@ -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

@@ -171,6 +171,7 @@ export interface DropDownOption {
reverseIndex?: number
reverseOnClick?: () => void
sortBy?: SortBy
isSelected?: boolean
}
export enum DetailsOrigin {

View File

@@ -109,6 +109,7 @@ export interface ListingRow extends AssetRow {
export interface CollectionRow extends AssetRow {
collectionAddress?: string
isVerified?: boolean
marketplace: ListingMarket
}
@@ -117,8 +118,3 @@ export enum ProfilePageStateType {
VIEWING,
LISTING,
}
export enum ListingResponse {
TRY_AGAIN,
SUCCESS,
}

View File

@@ -1,4 +1,4 @@
import { DetailsOrigin, GenieAsset, UpdatedGenieAsset, WalletAsset } from 'nft/types'
import { DetailsOrigin, GenieAsset, Listing, UpdatedGenieAsset, WalletAsset } from 'nft/types'
export function getRarityStatus(
rarityStatusCache: Map<string, boolean>,
@@ -44,3 +44,38 @@ export const generateTweetForPurchase = (assets: UpdatedGenieAsset[], txHashUrl:
} with @Uniswap 🦄\n\nhttps://app.uniswap.org/#/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769\n${txHashUrl}`
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
}
function getMinListingPrice(listings: Listing[]): number {
return Math.min(...listings.map((listing) => listing.price ?? 0)) ?? 0
}
function mapAssetsToCollections(assets: WalletAsset[]): { collection: string; items: string[] }[] {
const collections = assets.map((asset) => asset.collection?.twitterUrl ?? asset.collection?.name ?? '')
const uniqueCollections = [...new Set(collections)]
return uniqueCollections.map((collection) => {
return {
collection,
items: assets
.filter((asset) => asset.collection?.twitterUrl === collection || asset.collection?.name === collection)
.map((asset) => asset.name ?? ''),
}
})
}
export const generateTweetForList = (assets: WalletAsset[]): string => {
const tweetText =
assets.length == 1
? `I just listed ${
assets[0].collection?.twitterUrl
? `${assets[0].collection?.twitterUrl} `
: `${assets[0].collection?.name} ` ?? ''
}${assets[0].name} for ${getMinListingPrice(assets[0].newListings ?? [])} ETH on ${assets[0].marketplaces
?.map((market) => market.name)
.join(', ')}. Buy it on @Uniswap at https://app.uniswap.org/#${getAssetHref(assets[0])}`
: `I just listed ${
assets.length
} items on @Uniswap at https://app.uniswap.org/#/nfts/profile\n\nCollections: ${mapAssetsToCollections(assets)
.map(({ collection, items }) => `${collection} ${items.map((item) => item).join(', ')}`)
.join(', ')} \n\nMarketplaces: ${assets[0].marketplaces?.map((market) => market.name).join(', ')}`
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
}

View File

@@ -92,7 +92,7 @@ export async function approveCollection(
// setApprovalForAll() method
const ERC721Contract = new Contract(collectionAddress, ERC721, signer)
const signerAddress = await signer.getAddress()
setStatus(ListingStatus.PENDING)
try {
const approved = await ERC721Contract.isApprovedForAll(signerAddress, operator)
if (approved) {
@@ -160,8 +160,9 @@ export async function signListing(
)
const order = await executeAllActions()
setStatus(ListingStatus.PENDING)
const res = await PostOpenSeaSellOrder(order)
if (res) setStatus(ListingStatus.APPROVED)
res ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
return res
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
@@ -228,7 +229,7 @@ export async function signListing(
params: [],
}
const res = await createLooksRareOrder(payload)
if (res) setStatus(ListingStatus.APPROVED)
res ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
return res
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
@@ -262,7 +263,7 @@ export async function signListing(
setStatus(ListingStatus.PENDING)
// call server api
const resp = await newX2Y2Order(payload)
if (resp) setStatus(ListingStatus.APPROVED)
resp ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
return resp
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)

View File

@@ -20,6 +20,7 @@ import { useAnalyticsReporter } from '../components/analytics'
import ErrorBoundary from '../components/ErrorBoundary'
import { PageTabs } from '../components/NavBar'
import NavBar from '../components/NavBar'
import Polling from '../components/Polling'
import Popups from '../components/Popups'
import { useIsExpertMode } from '../state/user/hooks'
import DarkModeQueryParamReader from '../theme/components/DarkModeQueryParamReader'
@@ -193,6 +194,7 @@ export default function App() {
</HeaderWrapper>
<BodyWrapper>
<Popups />
<Polling />
<TopLevelModals />
<Suspense fallback={<Loader />}>
{isLoaded ? (

View File

@@ -6,6 +6,7 @@ import Card, { CardType } from 'components/About/Card'
import { MAIN_CARDS, MORE_CARDS } from 'components/About/constants'
import ProtocolBanner from 'components/About/ProtocolBanner'
import { BaseButton } from 'components/Button'
import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget'
import Swap from 'pages/Swap'
import { parse } from 'qs'
import { useEffect, useRef, useState } from 'react'
@@ -14,7 +15,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Link as NativeLink } from 'react-router-dom'
import { useAppSelector } from 'state/hooks'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import styled, { css } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
@@ -255,23 +256,40 @@ const LandingSwapContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
z-index: 1;
`
const LandingSwap = styled(Swap)`
const SwapCss = css`
* {
pointer-events: none;
}
&:hover {
border: 1px solid ${({ theme }) => theme.accentAction};
transform: translateY(-4px);
transition: ${({ theme }) => `transform ${theme.transition.duration.medium} ${theme.transition.timing.ease}`};
}
`
const LinkCss = css`
text-decoration: none;
max-width: 480px;
width: 100%;
`
const LandingSwap = styled(Swap)`
${SwapCss}
&:hover {
border: 1px solid ${({ theme }) => theme.accentAction};
}
`
const Link = styled(NativeLink)`
text-decoration: none;
max-width: 480px;
width: 100%;
${LinkCss}
`
const WidgetLandingLink = styled(NativeLink)`
${LinkCss}
${SwapCss}
`
export default function Landing() {
@@ -287,6 +305,8 @@ export default function Landing() {
ignoreQueryPrefix: true,
})
const swapWidgetEnabled = useSwapWidgetEnabled()
// This can be simplified significantly once the flag is removed! For now being explicit is clearer.
useEffect(() => {
if (queryParams.intro || !selectedWallet) {
@@ -306,9 +326,15 @@ export default function Landing() {
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.LANDING_PAGE_SWAP_ELEMENT}
>
<Link to="/swap">
<LandingSwap />
</Link>
{swapWidgetEnabled ? (
<WidgetLandingLink to="/swap">
<Swap />
</WidgetLandingLink>
) : (
<Link to="/swap">
<LandingSwap />
</Link>
)}
</TraceEvent>
</LandingSwapContainer>
<Gradient isDarkMode={isDarkMode} />

View File

@@ -173,7 +173,7 @@ export default function MigrateV2() {
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
<Trans>
Dont see one of your v2 positions?{' '}
<StyledInternalLink id="import-pool-link" to="/find?origin=/migrate/v2">
<StyledInternalLink id="import-pool-link" to="/pool/v2/find">
Import it.
</StyledInternalLink>
</Trans>

View File

@@ -97,7 +97,7 @@ export default function CTACards() {
return (
<CTASection>
<CTA1 href="https://support.uniswap.org/hc/en-us/articles/7423608592781">
<CTA1 href="https://support.uniswap.org/hc/en-us/categories/8122334631437-Providing-Liquidity-">
<ResponsiveColumn>
<HeaderText>
<Trans>Learn about providing liquidity</Trans>

View File

@@ -32,13 +32,14 @@ import { Link, useParams } from 'react-router-dom'
import { Bound } from 'state/mint/v3/actions'
import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { ExternalLink, HideExtraSmall, ThemedText } from 'theme'
import { ExternalLink, HideExtraSmall, HideSmall, ThemedText } from 'theme'
import { currencyId } from 'utils/currencyId'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { formatTickPrice } from 'utils/formatTickPrice'
import { unwrappedToken } from 'utils/unwrappedToken'
import RangeBadge from '../../components/Badge/RangeBadge'
import { SmallButtonPrimary } from '../../components/Button/index'
import { getPriceOrderingFromPositionForUI } from '../../components/PositionListItem'
import RateToggle from '../../components/RateToggle'
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
@@ -49,38 +50,20 @@ import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { LoadingRows } from './styleds'
const PageWrapper = styled.div`
padding: 68px 8px 0px;
padding: 68px 16px 16px 16px;
min-width: 800px;
max-width: 960px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
padding: 48px 8px 0px;
min-width: 100%;
padding: 16px;
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
padding-top: 20px;
min-width: 100%;
padding: 16px;
}
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
min-width: 680px;
max-width: 680px;
`};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
min-width: 600px;
max-width: 600px;
`};
@media only screen and (max-width: 620px) {
min-width: 500px;
max-width: 500px;
}
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
min-width: 340px;
max-width: 340px;
`};
`
const BadgeText = styled.div`
@@ -120,23 +103,40 @@ const DoubleArrow = styled.span`
margin: 0 1rem;
`
const ResponsiveRow = styled(RowBetween)`
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
flex-direction: column;
align-items: flex-start;
row-gap: 16px;
width: 100%:
`};
width: 100%;
}
`
const ResponsiveButtonPrimary = styled(ButtonPrimary)`
const ActionButtonResponsiveRow = styled(ResponsiveRow)`
width: 50%;
justify-content: flex-end;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: 100%;
flex-direction: row;
* {
width: 100%;
}
}
`
const ResponsiveButtonConfirmed = styled(ButtonConfirmed)`
border-radius: 12px;
padding: 6px 8px;
width: fit-content;
font-size: 16px;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
flex: 1 1 auto;
width: 49%;
`};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
width: fit-content;
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: fit-content;
}
`
const NFTGrid = styled.div`
@@ -624,13 +624,13 @@ export function PositionPage() {
<RangeBadge removed={removed} inRange={inRange} />
</RowFixed>
{ownsNFT && (
<RowFixed>
<ActionButtonResponsiveRow>
{currency0 && currency1 && feeAmount && tokenId ? (
<ButtonGray
as={Link}
to={`/increase/${currencyId(currency0)}/${currencyId(currency1)}/${feeAmount}/${tokenId}`}
width="fit-content"
padding="6px 8px"
width="fit-content"
$borderRadius="12px"
style={{ marginRight: '8px' }}
>
@@ -638,55 +638,58 @@ export function PositionPage() {
</ButtonGray>
) : null}
{tokenId && !removed ? (
<ResponsiveButtonPrimary
<SmallButtonPrimary
as={Link}
to={`/remove/${tokenId}`}
width="fit-content"
padding="6px 8px"
width="fit-content"
$borderRadius="12px"
>
<Trans>Remove Liquidity</Trans>
</ResponsiveButtonPrimary>
</SmallButtonPrimary>
) : null}
</RowFixed>
</ActionButtonResponsiveRow>
)}
</ResponsiveRow>
<RowBetween></RowBetween>
</AutoColumn>
<ResponsiveRow align="flex-start">
{'result' in metadata ? (
<DarkCard
width="100%"
height="100%"
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'space-around',
marginRight: '12px',
}}
>
<div style={{ marginRight: 12 }}>
<HideSmall
style={{
marginRight: '12px',
}}
>
{'result' in metadata ? (
<DarkCard
width="100%"
height="100%"
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'space-around',
minWidth: '340px',
}}
>
<NFT image={metadata.result.image} height={400} />
</div>
{typeof chainId === 'number' && owner && !ownsNFT ? (
<ExternalLink href={getExplorerLink(chainId, owner, ExplorerDataType.ADDRESS)}>
<Trans>Owner</Trans>
</ExternalLink>
) : null}
</DarkCard>
) : (
<DarkCard
width="100%"
height="100%"
style={{
marginRight: '12px',
minWidth: '340px',
}}
>
<Loader />
</DarkCard>
)}
{typeof chainId === 'number' && owner && !ownsNFT ? (
<ExternalLink href={getExplorerLink(chainId, owner, ExplorerDataType.ADDRESS)}>
<Trans>Owner</Trans>
</ExternalLink>
) : null}
</DarkCard>
) : (
<DarkCard
width="100%"
height="100%"
style={{
minWidth: '340px',
}}
>
<Loader />
</DarkCard>
)}
</HideSmall>
<AutoColumn gap="sm" style={{ width: '100%', height: '100%' }}>
<DarkCard>
<AutoColumn gap="md" style={{ width: '100%' }}>
@@ -714,7 +717,7 @@ export function PositionPage() {
</ThemedText.DeprecatedMain>
{typeof ratio === 'number' && !removed ? (
<Badge style={{ marginLeft: '10px' }}>
<ThemedText.DeprecatedMain fontSize={11}>
<ThemedText.DeprecatedMain color={theme.textSecondary} fontSize={11}>
<Trans>{inverted ? ratio : 100 - ratio}%</Trans>
</ThemedText.DeprecatedMain>
</Badge>
@@ -768,7 +771,7 @@ export function PositionPage() {
</AutoColumn>
{ownsNFT &&
(feeValue0?.greaterThan(0) || feeValue1?.greaterThan(0) || !!collectMigrationHash) ? (
<ButtonConfirmed
<ResponsiveButtonConfirmed
disabled={collecting || !!collectMigrationHash}
confirmed={!!collectMigrationHash && !isCollectPending}
width="fit-content"
@@ -794,7 +797,7 @@ export function PositionPage() {
</ThemedText.DeprecatedMain>
</>
)}
</ButtonConfirmed>
</ResponsiveButtonConfirmed>
) : null}
</RowBetween>
</AutoColumn>

View File

@@ -19,8 +19,10 @@ import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { MouseoverTooltip } from 'components/Tooltip'
import Widget from 'components/Widget'
import { isSupportedChain } from 'constants/chains'
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget'
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
import { useSwapCallback } from 'hooks/useSwapCallback'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
@@ -157,6 +159,7 @@ export default function Swap({ className }: { className?: string }) {
const loadedUrlParams = useDefaultsFromURLSearch()
const [newSwapQuoteNeedsLogging, setNewSwapQuoteNeedsLogging] = useState(true)
const [fetchingSwapQuoteStartTime, setFetchingSwapQuoteStartTime] = useState<Date | undefined>()
const swapWidgetEnabled = useSwapWidgetEnabled()
// token warning stuff
const [loadedInputCurrency, loadedOutputCurrency] = [
@@ -559,319 +562,335 @@ export default function Swap({ className }: { className?: string }) {
showCancel={true}
/>
<PageWrapper>
<SwapWrapper className={className} id="swap-page">
<SwapHeader allowedSlippage={allowedSlippage} />
<ConfirmSwapModal
isOpen={showConfirm}
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
recipient={recipient}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueTradeInput}
fiatValueOutput={fiatValueTradeOutput}
{swapWidgetEnabled ? (
<Widget
defaultTokens={{
[Field.INPUT]: loadedInputCurrency ?? undefined,
[Field.OUTPUT]: loadedOutputCurrency ?? undefined,
}}
width="100%"
/>
) : (
<SwapWrapper className={className} id="swap-page">
<SwapHeader allowedSlippage={allowedSlippage} />
<ConfirmSwapModal
isOpen={showConfirm}
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
recipient={recipient}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueTradeInput}
fiatValueOutput={fiatValueTradeOutput}
/>
<div style={{ display: 'relative' }}>
<SwapSection>
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
<SwapCurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? (
<Trans>From (at most)</Trans>
) : (
<Trans>From</Trans>
)
}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
currency={currencies[Field.INPUT] ?? null}
onUserInput={handleTypeInput}
onMax={handleMaxInput}
fiatValue={fiatValueInput ?? undefined}
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
</Trace>
</SwapSection>
<ArrowWrapper clickable={isSupportedChain(chainId)}>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SwapEventName.SWAP_TOKENS_REVERSED}
element={InterfaceElementName.SWAP_TOKENS_REVERSE_ARROW_BUTTON}
>
<ArrowContainer
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={theme.textPrimary}
>
<ArrowDown
size="16"
color={
currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary
}
/>
</ArrowContainer>
</TraceEvent>
</ArrowWrapper>
</div>
<AutoColumn gap="md">
<div>
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
<div style={{ display: 'relative' }}>
<SwapSection>
<Trace section={InterfaceSectionName.CURRENCY_INPUT_PANEL}>
<SwapCurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={
independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>
independentField === Field.OUTPUT && !showWrap ? (
<Trans>From (at most)</Trans>
) : (
<Trans>From</Trans>
)
}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput ?? undefined}
priceImpact={stablecoinPriceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
currency={currencies[Field.INPUT] ?? null}
onUserInput={handleTypeInput}
onMax={handleMaxInput}
fiatValue={fiatValueInput ?? undefined}
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing}
id={InterfaceSectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
</Trace>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.textSecondary} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
<Trans>- Remove recipient</Trans>
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
</OutputSwapSection>
{showDetailsDropdown && (
<DetailsSwapSection>
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
/>
</DetailsSwapSection>
)}
</div>
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Unsupported Asset</Trans>
</ThemedText.DeprecatedMain>
</ButtonPrimary>
) : !account ? (
</SwapSection>
<ArrowWrapper clickable={isSupportedChain(chainId)}>
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }}
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
name={SwapEventName.SWAP_TOKENS_REVERSED}
element={InterfaceElementName.SWAP_TOKENS_REVERSE_ARROW_BUTTON}
>
<ButtonLight onClick={toggleWalletModal} fontWeight={600}>
<Trans>Connect Wallet</Trans>
</ButtonLight>
</TraceEvent>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap} fontWeight={600}>
{wrapInputError ? (
<WrapErrorText wrapInputError={wrapInputError} />
) : wrapType === WrapType.WRAP ? (
<Trans>Wrap</Trans>
) : wrapType === WrapType.UNWRAP ? (
<Trans>Unwrap</Trans>
) : null}
</ButtonPrimary>
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GrayCard style={{ textAlign: 'center' }}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.DeprecatedMain>
</GrayCard>
) : showApproveFlow ? (
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
<AutoColumn style={{ width: '100%' }} gap="12px">
<ButtonConfirmed
fontWeight={600}
onClick={handleApprove}
disabled={approveTokenButtonDisabled}
width="100%"
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
confirmed={
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
<ArrowContainer
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={theme.textPrimary}
>
<ArrowDown
size="16"
color={
currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.textPrimary : theme.textTertiary
}
>
<AutoRow justify="space-between" style={{ flexWrap: 'nowrap' }} height="20px">
{/* we need to shorten this string on mobile */}
{approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED ? (
<ThemedText.SubHeader width="100%" textAlign="center" color="textSecondary">
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
</ThemedText.SubHeader>
/>
</ArrowContainer>
</TraceEvent>
</ArrowWrapper>
</div>
<AutoColumn gap="md">
<div>
<OutputSwapSection showDetailsDropdown={showDetailsDropdown}>
<Trace section={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}>
<SwapCurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={
independentField === Field.INPUT && !showWrap ? (
<Trans>To (at least)</Trans>
) : (
<ThemedText.SubHeader width="100%" textAlign="center" color="white">
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
</ThemedText.SubHeader>
)}
<Trans>To</Trans>
)
}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput ?? undefined}
priceImpact={stablecoinPriceImpact}
currency={currencies[Field.OUTPUT] ?? null}
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id={InterfaceSectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</Trace>
{approvalPending || approvalState === ApprovalState.PENDING ? (
<Loader stroke={theme.white} />
) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.textSecondary} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
<Trans>- Remove recipient</Trans>
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
</OutputSwapSection>
{showDetailsDropdown && (
<DetailsSwapSection>
<SwapDetailsDropdown
trade={trade}
syncing={routeIsSyncing}
loading={routeIsLoading}
allowedSlippage={allowedSlippage}
/>
</DetailsSwapSection>
)}
</div>
{showPriceImpactWarning && <PriceImpactWarning priceImpact={largerPriceImpact} />}
<div>
{swapIsUnsupported ? (
<ButtonPrimary disabled={true}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Unsupported Asset</Trans>
</ThemedText.DeprecatedMain>
</ButtonPrimary>
) : !account ? (
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
properties={{ received_swap_quote: getIsValidSwapQuote(trade, tradeState, swapInputError) }}
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
>
<ButtonLight onClick={toggleWalletModal} fontWeight={600}>
<Trans>Connect Wallet</Trans>
</ButtonLight>
</TraceEvent>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap} fontWeight={600}>
{wrapInputError ? (
<WrapErrorText wrapInputError={wrapInputError} />
) : wrapType === WrapType.WRAP ? (
<Trans>Wrap</Trans>
) : wrapType === WrapType.UNWRAP ? (
<Trans>Unwrap</Trans>
) : null}
</ButtonPrimary>
) : routeNotFound && userHasSpecifiedInputOutput && !routeIsLoading && !routeIsSyncing ? (
<GrayCard style={{ textAlign: 'center' }}>
<ThemedText.DeprecatedMain mb="4px">
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.DeprecatedMain>
</GrayCard>
) : showApproveFlow ? (
<AutoRow style={{ flexWrap: 'nowrap', width: '100%' }}>
<AutoColumn style={{ width: '100%' }} gap="12px">
<ButtonConfirmed
fontWeight={600}
onClick={handleApprove}
disabled={approveTokenButtonDisabled}
width="100%"
altDisabledStyle={approvalState === ApprovalState.PENDING} // show solid button while waiting
confirmed={
approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED
}
>
<AutoRow justify="space-between" style={{ flexWrap: 'nowrap' }} height="20px">
{/* we need to shorten this string on mobile */}
{approvalState === ApprovalState.APPROVED ||
signatureState === UseERC20PermitState.SIGNED ? (
<CheckCircle size="20" color={theme.accentSuccess} />
) : (
<ThemedText.SubHeader width="100%" textAlign="center" color="textSecondary">
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
</ThemedText.SubHeader>
) : (
<ThemedText.SubHeader width="100%" textAlign="center" color="white">
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
</ThemedText.SubHeader>
)}
{approvalPending || approvalState === ApprovalState.PENDING ? (
<Loader stroke={theme.white} />
) : (approvalSubmitted && approvalState === ApprovalState.APPROVED) ||
signatureState === UseERC20PermitState.SIGNED ? (
<CheckCircle size="20" color={theme.accentSuccess} />
) : (
<MouseoverTooltip
text={
<Trans>
You must give the Uniswap smart contracts permission to use your{' '}
{currencies[Field.INPUT]?.symbol}. You only have to do this once per token.
</Trans>
}
>
<HelpCircle size="20" color={theme.white} style={{ marginLeft: '8px' }} />
</MouseoverTooltip>
)}
</AutoRow>
</ButtonConfirmed>
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
width="100%"
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
(approvalState !== ApprovalState.APPROVED &&
signatureState !== UseERC20PermitState.SIGNED) ||
priceImpactTooHigh
}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={600}>
{priceImpactTooHigh ? (
<Trans>High Price Impact</Trans>
) : trade && priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
</AutoColumn>
</AutoRow>
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
<ButtonPrimary
onClick={updateAllowance}
disabled={isAllowancePending || isApprovalLoading}
style={{ gap: 14 }}
>
{isAllowancePending ? (
<>
<Loader size="20px" />
<Trans>Approve in your wallet</Trans>
</>
) : isApprovalLoading ? (
<>
<Loader size="20px" />
<Trans>Approval pending</Trans>
</>
) : (
<>
<div style={{ height: 20 }}>
<MouseoverTooltip
text={
<Trans>
You must give the Uniswap smart contracts permission to use your{' '}
{currencies[Field.INPUT]?.symbol}. You only have to do this once per token.
Permission is required for Uniswap to swap each token. This will expire after one
month for your security.
</Trans>
}
>
<HelpCircle size="20" color={theme.white} style={{ marginLeft: '8px' }} />
<Info size={20} />
</MouseoverTooltip>
)}
</AutoRow>
</ButtonConfirmed>
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
width="100%"
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
(approvalState !== ApprovalState.APPROVED && signatureState !== UseERC20PermitState.SIGNED) ||
priceImpactTooHigh
}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={600}>
{priceImpactTooHigh ? (
<Trans>High Price Impact</Trans>
) : trade && priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
</AutoColumn>
</AutoRow>
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
<ButtonPrimary
onClick={updateAllowance}
disabled={isAllowancePending || isApprovalLoading}
style={{ gap: 14 }}
>
{isAllowancePending ? (
<>
<Loader size="20px" />
<Trans>Approve in your wallet</Trans>
</>
) : isApprovalLoading ? (
<>
<Loader size="20px" />
<Trans>Approval pending</Trans>
</>
) : (
<>
<div style={{ height: 20 }}>
<MouseoverTooltip
text={
<Trans>
Permission is required for Uniswap to swap each token. This will expire after one month
for your security.
</Trans>
}
>
<Info size={20} />
</MouseoverTooltip>
</div>
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
</>
)}
</ButtonPrimary>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
priceImpactTooHigh ||
(permit2Enabled ? allowance.state !== AllowanceState.ALLOWED : Boolean(swapCallbackError))
}
error={
isValid &&
priceImpactSeverity > 2 &&
(permit2Enabled ? allowance.state === AllowanceState.ALLOWED : !swapCallbackError)
}
>
<Text fontSize={20} fontWeight={600}>
{swapInputError ? (
swapInputError
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
</div>
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
</>
)}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</div>
</AutoColumn>
</SwapWrapper>
</ButtonPrimary>
) : (
<ButtonError
onClick={() => {
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined,
})
}
}}
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
priceImpactTooHigh ||
(permit2Enabled ? allowance.state !== AllowanceState.ALLOWED : Boolean(swapCallbackError))
}
error={
isValid &&
priceImpactSeverity > 2 &&
(permit2Enabled ? allowance.state === AllowanceState.ALLOWED : !swapCallbackError)
}
>
<Text fontSize={20} fontWeight={600}>
{swapInputError ? (
swapInputError
) : routeIsSyncing || routeIsLoading ? (
<Trans>Swap</Trans>
) : priceImpactTooHigh ? (
<Trans>Price Impact Too High</Trans>
) : priceImpactSeverity > 2 ? (
<Trans>Swap Anyway</Trans>
) : (
<Trans>Swap</Trans>
)}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</div>
</AutoColumn>
</SwapWrapper>
)}
<NetworkAlert />
</PageWrapper>
<SwitchLocaleLink />

View File

@@ -1,51 +1,57 @@
import TokenDetails from 'components/Tokens/TokenDetails'
import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { useTokenPriceQuery, useTokenQuery } from 'graphql/data/__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
import { TimePeriod, toHistoryDuration, validateUrlChainParam } from 'graphql/data/util'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
export const pageTimePeriodAtom = atomWithStorage<TimePeriod>('tokenDetailsTimePeriod', TimePeriod.DAY)
export default function TokenDetailsPage() {
const { tokenAddress, chainName } = useParams<{ tokenAddress?: string; chainName?: string }>()
const { tokenAddress, chainName } = useParams<{ tokenAddress: string; chainName?: string }>()
const chain = validateUrlChainParam(chainName)
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const isNative = tokenAddress === NATIVE_CHAIN_ID
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom)
const [contract, duration] = useMemo(
() => [
{ address: isNative ? nativeOnChain(pageChainId).wrapped.address : tokenAddress ?? '', chain },
toHistoryDuration(timePeriod),
],
[chain, isNative, pageChainId, timePeriod, tokenAddress]
const [address, duration] = useMemo(
/* tokenAddress will always be defined in the path for for this page to render, but useParams will always
return optional arguments; nullish coalescing operator is present here to appease typechecker */
() => [isNative ? getNativeTokenDBAddress(chain) : tokenAddress ?? '', toHistoryDuration(timePeriod)],
[chain, isNative, timePeriod, tokenAddress]
)
const { data: tokenQuery, loading: tokenQueryLoading } = useTokenQuery({
const { data: tokenQuery } = useTokenQuery({
variables: {
contract: isNative ? { address: getNativeTokenDBAddress(chain), chain } : contract,
address,
chain,
},
})
const { data: tokenPriceQuery } = useTokenPriceQuery({
variables: {
contract,
address,
chain,
duration,
},
})
if (!tokenQuery || tokenQueryLoading) return <TokenDetailsPageSkeleton />
// Saves already-loaded chart data into state to display while tokenPriceQuery is undefined timePeriod input changes
const [currentPriceQuery, setCurrentPriceQuery] = useState(tokenPriceQuery)
useEffect(() => {
if (tokenPriceQuery) setCurrentPriceQuery(tokenPriceQuery)
}, [setCurrentPriceQuery, tokenPriceQuery])
if (!tokenQuery) return <TokenDetailsPageSkeleton />
return (
<TokenDetails
urlAddress={tokenAddress}
chain={chain}
tokenQuery={tokenQuery}
tokenPriceQuery={tokenPriceQuery}
tokenPriceQuery={currentPriceQuery}
onChangeTimePeriod={setTimePeriod}
/>
)

View File

@@ -3,7 +3,7 @@ import { Trace } from '@uniswap/analytics'
import { InterfacePageName } from '@uniswap/analytics-events'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ButtonPrimary } from 'components/Button'
import { ButtonPrimary, SmallButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { CardBGImage, CardNoise, CardSection, DataCard } from 'components/earn/styled'
import FormattedCurrencyAmount from 'components/FormattedCurrencyAmount'
@@ -106,12 +106,12 @@ const TextButton = styled(ThemedText.DeprecatedMain)`
`
const AddressButton = styled.div`
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
padding: 2px 4px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
color: ${({ theme }) => theme.accentAction};
`
const StyledExternalLink = styled(ExternalLink)`
@@ -223,14 +223,14 @@ export default function Landing() {
) : (
''
)}
<ButtonPrimary
<SmallButtonPrimary
as={Link}
to="/create-proposal"
style={{ width: 'fit-content', borderRadius: '8px' }}
padding="8px"
padding="6px 8px"
>
<Trans>Create Proposal</Trans>
</ButtonPrimary>
</SmallButtonPrimary>
</AutoRow>
</WrapSmall>
{!showUnlockVoting && (
@@ -263,6 +263,7 @@ export default function Landing() {
{allProposals?.length > 0 && (
<AutoColumn gap="md">
<RowBetween></RowBetween>
<RowBetween>
<ThemedText.DeprecatedMain>
<Trans>Show Cancelled</Trans>

View File

@@ -1,3 +1,4 @@
import { MockedProvider } from '@apollo/client/testing'
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { render, renderHook } from '@testing-library/react'
@@ -32,9 +33,11 @@ const WithProviders = ({ children }: { children?: ReactNode }) => {
<QueryClientProvider client={queryClient}>
<HashRouter>
<Web3Provider>
<BlockNumberProvider>
<ThemeProvider>{children}</ThemeProvider>
</BlockNumberProvider>
<MockedProvider>
<BlockNumberProvider>
<ThemeProvider>{children}</ThemeProvider>
</BlockNumberProvider>
</MockedProvider>
</Web3Provider>
</HashRouter>
</QueryClientProvider>

View File

@@ -182,6 +182,12 @@ const StyledLink = styled.a`
${ClickableStyle}
${LinkStyle}
`
export const StyledRouterLink = styled(Link)`
${ClickableStyle}
${LinkStyle}
`
/**
* Outbound link that handles firing google analytics events
*/

View File

@@ -2,7 +2,7 @@ import { nativeOnChain } from 'constants/tokens'
import { Chain } from 'graphql/data/__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
export function getNativeTokenDBAddress(chain: Chain): string {
export function getNativeTokenDBAddress(chain: Chain): string | undefined {
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
switch (chain) {
case Chain.Celo:
@@ -12,6 +12,6 @@ export function getNativeTokenDBAddress(chain: Chain): string {
case Chain.Arbitrum:
case Chain.EthereumGoerli:
case Chain.Optimism:
return 'ETH'
return undefined
}
}

View File

@@ -1,44 +0,0 @@
import { _TypedDataEncoder } from '@ethersproject/hash'
import { JsonRpcSigner } from '@ethersproject/providers'
/**
* Overrides the _signTypedData method to add support for wallets without EIP-712 support (eg Zerion) by adding a fallback to eth_sign.
* The implementation is copied from ethers (and linted), except for the catch statement, which removes the logger and adds the fallback.
* @see https://github.com/ethers-io/ethers.js/blob/c80fcddf50a9023486e9f9acb1848aba4c19f7b6/packages/providers/src.ts/json-rpc-provider.ts#L334
*/
JsonRpcSigner.prototype._signTypedData = async function signTypedDataWithFallbacks(this, domain, types, value) {
// Populate any ENS names (in-place)
const populated = await _TypedDataEncoder.resolveNames(domain, types, value, (name: string) => {
return this.provider.resolveName(name) as Promise<string>
})
const address = await this.getAddress()
try {
try {
// We must try the unversioned eth_signTypedData first, because some wallets (eg SafePal) will hang on _v4.
return await this.provider.send('eth_signTypedData', [
address.toLowerCase(),
JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value)),
])
} catch (error) {
// MetaMask complains that the unversioned eth_signTypedData is formatted incorrectly (32602) - it prefers _v4.
if (error.code === -32602) {
console.warn('eth_signTypedData failed, falling back to eth_signTypedData_v4:', error)
return await this.provider.send('eth_signTypedData_v4', [
address.toLowerCase(),
JSON.stringify(_TypedDataEncoder.getPayload(populated.domain, types, populated.value)),
])
}
throw error
}
} catch (error) {
// If neither other method is available (eg Zerion), fallback to eth_sign.
if (typeof error.message === 'string' && error.message.match(/not found/i)) {
console.warn('eth_signTypedData_* failed, falling back to eth_sign:', error)
const hash = _TypedDataEncoder.hash(populated.domain, types, populated.value)
return await this.provider.send('eth_sign', [address, hash])
}
throw error
}
}

View File

@@ -22,6 +22,21 @@ export function swapErrorToUserReadableMessage(error: any): string {
error = error.error ?? error.data?.originalError
}
// The 4001 error code doesn't capture the case where users reject a transaction for all wallets,
// so we need to parse the reason for these special cases:
if (
// For Rainbow :
(reason?.match(/request/i) && reason?.match(/reject/i)) ||
// For Frame:
reason?.match(/declined/i) ||
// For SafePal:
reason?.match(/cancelled by user/i) ||
// For Coinbase:
reason?.match(/user denied/i)
) {
return t`Transaction rejected`
}
if (reason?.indexOf('execution reverted: ') === 0) reason = reason.substr('execution reverted: '.length)
switch (reason) {

View File

@@ -4945,10 +4945,10 @@
react "^18.2.0"
react-dom "^18.2.0"
"@uniswap/conedison@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.1.1.tgz#affec246613d1f52da3cdd0571ef8195b7b54d17"
integrity sha512-xFHAcWRrU+/+/BInXy6SRiiNwUG0vxLWsoYgod66wWifUvnjfpItzlvJHUer1OOpLDsz0CL5Fb70vFJOGAGi8w==
"@uniswap/conedison@^1.2.1", "@uniswap/conedison@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.3.0.tgz#998aca2bad27f0780a05b40e4512acfcadfece79"
integrity sha512-zpZ52svBJ2btwl09mLOw7HlBxFDuYAjAZXLAR7WQZJeRgjD1yD2QuI3v7JliXvHzJh3ePYH6820EMp7xQbdAGQ==
"@uniswap/default-token-list@^2.0.0":
version "2.2.0"
@@ -5090,7 +5090,7 @@
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.30.tgz#2103ca23b8007c59ec71718d34cdc97861c409e5"
integrity sha512-HwY2VvkQ8lNR6ks5NqQfAtg+4IZqz3KV1T8d2DlI8emIn9uMmaoFbIOg0nzjqAVKKnZSbMTRRtUoAh6mmjRvog==
"@uniswap/universal-router-sdk@1.3.4", "@uniswap/universal-router-sdk@^1.3.0":
"@uniswap/universal-router-sdk@1.3.4", "@uniswap/universal-router-sdk@^1.3.4":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-1.3.4.tgz#7b6b8e30d6faff812f224d32a832385378568160"
integrity sha512-RIWZm48N/fiAssMOj0nMLoeN5JATKOMfbFwyVnCaFHIVMJmKEZtZLKe3QOkl2LMVnQ/nP4LVCDwHU+mdP68jCQ==
@@ -5194,24 +5194,24 @@
"@uniswap/v3-core" "1.0.0"
"@uniswap/v3-periphery" "^1.0.1"
"@uniswap/widgets@2.25.1":
version "2.25.1"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.25.1.tgz#53d03da44587c0851d2f39516d7f9037d0c65b9d"
integrity sha512-Jth8qd7g58W1SVRpSFNiTKjs0HmcOacpxdSKfRSOIJOcYq8vgjZ0k4W3i/bzjf8PDRtLGh4Nn09VLugtIMUVIw==
"@uniswap/widgets@^2.27.0":
version "2.27.0"
resolved "https://registry.yarnpkg.com/@uniswap/widgets/-/widgets-2.27.0.tgz#debd946e6ff736a501a98321a2b8f98d7b055f22"
integrity sha512-CHOoIORI0nYWN29s0ZrjGRVPiOWGy3hy8ZFhKM+CSz+mDNPWnlVssmDNBYhzM+tl2g/PcJpR49ggY+QWPlyDEQ==
dependencies:
"@babel/runtime" ">=7.17.0"
"@fontsource/ibm-plex-mono" "^4.5.1"
"@fontsource/inter" "^4.5.1"
"@popperjs/core" "^2.4.4"
"@reduxjs/toolkit" "^1.6.1"
"@uniswap/conedison" "^1.1.1"
"@uniswap/conedison" "^1.2.1"
"@uniswap/permit2-sdk" "^1.2.0"
"@uniswap/redux-multicall" "^1.1.8"
"@uniswap/router-sdk" "^1.3.0"
"@uniswap/sdk-core" "^3.0.1"
"@uniswap/smart-order-router" "^2.10.0"
"@uniswap/token-lists" "^1.0.0-beta.30"
"@uniswap/universal-router-sdk" "^1.3.0"
"@uniswap/universal-router-sdk" "^1.3.4"
"@uniswap/v2-sdk" "^3.0.1"
"@uniswap/v3-sdk" "^3.8.2"
"@web3-react/core" "8.0.35-beta.0"
@@ -5743,10 +5743,10 @@
dependencies:
"@walletconnect/window-getters" "^1.0.0"
"@web3-react/coinbase-wallet@8.0.34-beta.0":
version "8.0.34-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/coinbase-wallet/-/coinbase-wallet-8.0.34-beta.0.tgz#43d51bb440fb4b98cc2c33782714da30ab8e1fb6"
integrity sha512-eGgtGtAqcRL64U1lcWeICB9CmpUycVl/mWD/b2Nd7yE0hXFUbPGLZHNvBTpnOpkANkjHI5ufFoyDGzzoxdw12A==
"@web3-react/coinbase-wallet@8.0.35-beta.0":
version "8.0.35-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/coinbase-wallet/-/coinbase-wallet-8.0.35-beta.0.tgz#903df113a0987f0b28c07941a82c05f227cbccf8"
integrity sha512-wSITb75xIfERw1rZA8t35mJ3Lq459Emt2ybraWvT0TAIVTzHGCKvEm3WglEVWj5CMy1X1lrJvWJ0ZfWUj1/0Lg==
dependencies:
"@web3-react/types" "^8.0.20-beta.0"
@@ -5768,6 +5768,13 @@
dependencies:
"@web3-react/types" "^8.0.20-beta.0"
"@web3-react/eip1193@8.0.27-beta.0":
version "8.0.27-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/eip1193/-/eip1193-8.0.27-beta.0.tgz#b768fc3ae6e234627e6a8ecd4d5c473c371a816b"
integrity sha512-8aca1NDP+qigh/LOxSiC5N44qvVo1z5V7QI4T5iUpNrzbXEXV3G2JXutoW5fNEm+oK5fkHK64p9eZ6lo0qyMbQ==
dependencies:
"@web3-react/types" "^8.0.20-beta.0"
"@web3-react/empty@8.0.20-beta.0":
version "8.0.20-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/empty/-/empty-8.0.20-beta.0.tgz#f8e2a6414ba49c7da3937776c213eb4c8ff6e2c7"
@@ -5792,10 +5799,10 @@
"@metamask/detect-provider" "^1.2.0"
"@web3-react/types" "^8.0.20-beta.0"
"@web3-react/metamask@8.0.29-beta.0":
version "8.0.29-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.29-beta.0.tgz#536536b8d4f22f21d3e109efaa8149939833f21b"
integrity sha512-UPaVmNum6cJ/CwW5WYFMrm6GwiuY1hnuCYB+bV1Bs0xghdag2Laj8/mSfpFCsCHcvg1ZWTcr4bH+WyuYAHgUxw==
"@web3-react/metamask@8.0.30-beta.0":
version "8.0.30-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/metamask/-/metamask-8.0.30-beta.0.tgz#afab5d7cf556e3ec77836ff2b4753f6f301cfbeb"
integrity sha512-SzL8/RUmLHEQCdd6KNjEIHXwuYiQrKX5e1Bgipvtm8MKBRk1ty17Aj7MKWNRcl/Qt06pLSxbcEJeT9kPIrc0mg==
dependencies:
"@metamask/detect-provider" "^1.2.0"
"@web3-react/types" "^8.0.20-beta.0"
@@ -5840,6 +5847,14 @@
"@web3-react/types" "^8.0.20-beta.0"
eventemitter3 "^4.0.7"
"@web3-react/walletconnect@8.0.37-beta.0":
version "8.0.37-beta.0"
resolved "https://registry.yarnpkg.com/@web3-react/walletconnect/-/walletconnect-8.0.37-beta.0.tgz#d375dc75f34e9bb225d26a88b16aa2a001b4e3bc"
integrity sha512-BW8tKE0dlFWSLBC/AWKULZbS1axToN7kMREKVY3YK1uj8bMRsoWcOQPceQl21WuEyVXax2V8My8SJgWuxcumvw==
dependencies:
"@web3-react/types" "^8.0.20-beta.0"
eventemitter3 "^4.0.7"
"@webassemblyjs/ast@1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"