Compare commits

...

64 Commits

Author SHA1 Message Date
Vignesh Mohankumar
b25da9de2d fix: end -> flex-end (#5774) 2022-12-29 21:55:42 -05:00
Zach Pomerantz
ae1fb4367f build: pause scheduled releases (#5765) 2022-12-22 10:32:38 -08:00
Mike Grabowski
1410edda32 fix: Sentry source maps URL (#5764)
fix
2022-12-22 22:14:03 +04:00
Jordan Frankfurt
3bc7f015ee fix: add two more analytics events for m parity (#5748)
* fix: add two more analytics events for m parity

* pr feedback
2022-12-22 11:26:34 -06:00
lynn
802714377c fix: display warning / reload prompt modal when metamask erroneously disconnects (#5722)
* init

* hook up modal to error detection

* zzmp comments

* zzmp fixes
2022-12-21 18:32:34 -05:00
Mike Grabowski
352daf959e feat: enable Sentry (#5758) 2022-12-22 02:44:04 +04:00
Mike Grabowski
92c21c2811 feat: better UX when Sentry disabled (#5695)
* feat: disable sentry

* chore: update error page

* chore: use continue on error instead

* chore: wrap in trans

* flip check
2022-12-22 00:39:29 +04:00
lynn
e70723aaf3 fix: add missing amplitude sell events (#5530)
* in progress

* add events

* remove debug

* update analytics pkg

* removed signatures requested

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-12-21 11:19:38 -08:00
Mike Grabowski
1802f50163 chore: use @uniswap eslint preset (#5556)
* feat: replace eslint with preset

* chore: update

* add empty line

* Revert changes

* chore: replace colors

* chore: tweaks

* Revert "chore: replace colors"

This reverts commit 3462420ecb.

* bring lint back

* chore: tweaks

* chore: add note

* chore: fix yarn lock

* chore: fix yarn.lock 2

* chore: use ESLint from npm

* Chore: update lockfile

* tweaks

* chore: initial take fixing some lint issues

* tweaks

* chore: another take

* chore: further tweaks

* chore: fix further

* feat: ignore Jest for cypress

* revert change

* chore: update to latest preset

* tmp lets see if this works

* chore: turn error into warning

* chore: remove warnings

* chore: deduplicate yarn lock

* feat: add recommended ESLint extension in case someone has Prettier instead

* upgrade to latest uniswap config

* chore: update todo

* remove patch

* find to some

* name

* chore: tweak yarn lock

* update

* cleanup

* update name for filter

* nl

* no unsafe finally

* chore: update doc

* fix

* fix

* one more fix

* one more file

* chore: Fix two last build issues

* add generated back

* fix lint after merge

* chore: fix tests

* remove

* one more
2022-12-21 03:08:20 +04:00
Charles Bachmeier
2aa1b18d14 chore: Migrate from Relay to Apollo (#5754)
* feat: initial apollo configutation (#5565)

* initial apollo configutation

* add new files

* check in types-and-hooks

* config unused export

* deduplicate

* ignore checked in schema for linting

* remove prettier ignore

* test unchecking types and hooks file

* undo

* rename codegen, respond to comments

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

* Remove maybe value from codegen

* add babel gql codegen

* correct ts graphql-tag

* remove plugin from craco

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

* chore: migrate Assets Query to Apollo

* delete comment

* move length check back to collectionAssets

* remove uneeded check

* respond to comments

* working switching and filters

* change sweep fetch policy

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

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

* migrate collection query to apollo

* remove page level suspense

* undo removing page level suspense

* rename query and hook

* guard returns

* add return type prop

* cleanup nullables

* memoizing

* use gql from apollo

* use babel gql and move empty trait

* add fetch policy

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

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

* chore: migrate NFT details query to apollo

* update todo

* update imports

* remove no longer used hook

* rename query

* use babel gql and nonnullable type

* working page

* add fetchpolicy

* respond to comments

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

* chore: migrate NftBalanceQuery (#5653)

* chore: migrate NftBalanceQuery

* cleanup

* update pagination

* better undefined handling

* move brake listing for invalid asset higher

* better handle loading

* memoize and cleanup

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

* remove named gql query consts

* set default fetchPolicy

* null suspense

* chore: Migrate The Graph queries (#5727)

* migrate TheGraph queries to Apollo

* add new files

* ignore thegraph generated types

* use standard fetchPolicy

* update apollo codegen commands

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

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

* migrate utils to types-and-hooks

* too many TokenTable re-renders

* working token queries

* fixed sparkline for native asset

* onChangeTimePeriod

* define inline

* use query instead of data in naming

* sparklineQuery instead of sparklineData

* rename to usePriceHistory

* multiline if else

* remove optional

* remove unneeded eslint ignore

* rename tokenQueryLoading

* rename OnChangeTimePeriod

* token address fallback

* just address

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

* chore: deprecate Relay (#5747)

* chore: deprecate Relay

* remove graph:ql generate step

* add new files

* apollo to graphql centric naming

* add new files

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

* remove no longer needed config exclusions

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-12-20 13:42:52 -08:00
Vignesh Mohankumar
a286e5b114 test: disable fiat announcement on cypress tests (#5751)
* test: disable fiat announcement on cypress tests

* Revert "test: skip tests due to fiat on-ramp change (#5742)"

This reverts commit 283479f76e.

* dismissed
2022-12-20 15:36:57 -05:00
Vignesh Mohankumar
62361647e0 fix: use localStorage for Fiat announcement (#5750)
* fix: use localStorage for Fiat announcement

* move to redux

* rm locally

* lint

* add change to hook

* fixes
2022-12-20 15:21:52 -05:00
Vignesh Mohankumar
deee278439 chore: remove unused isOpen state in Landing (#5732)
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2022-12-20 14:55:18 -05:00
Vignesh Mohankumar
6340deb201 chore: remove unused NFT redux user reducer state (#5753) 2022-12-20 14:55:08 -05:00
Vignesh Mohankumar
28b154ebe8 chore: remove landing redirect flag (#5736)
* test: add tests for landing redirect

* test: add tests for landing page (#5737)

* test: add tests for landing page

* lint

* intro=true

* rm downloads?

* fix: use second config option for no wallet in cypress

Co-authored-by: Eddie Dugan <eddie.dugan@uniswap.org>

Co-authored-by: Eddie Dugan <eddie.dugan@uniswap.org>
2022-12-20 14:54:56 -05:00
aballerr
3bde2165f4 fix: modal was removed, updating test to accommodate (#5749)
nft modal was removed, updating cypress to accommodate
2022-12-20 14:47:15 -05:00
Jack Short
78c8fd2359 style: updating cards to use appropriate aspect ratio (#5681)
* style: updating cards to use appropriate aspect ratio

* loading card height

* audio and video cards

* change loading height rendering

* respond to self serving comments

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2022-12-20 11:10:54 -08:00
eddie
c378752910 feat: fiat on ramp tile for about page (#5746)
* feat: move about content into landing page

* fix: delete unused image files

* fix: remove unused components

* fix: pointer events on title and subtitle

* feat: add about footer to landing page

* fix: simplify css in Landing Page

* feat(moonpay): moonpay ip checks to determine if the user can access the fiat onramp (#10)

* feat(moonpay): useFiatOnrampAvailable

* feat(moonpay): ip check with moonpay for buy crypto availability

* add error state and clear up some of the sequence of logic

* add button-specific spinner, put the ... menu button behind the feature flag

* hide ... menu option if onramp is unavailable

* add live publishable moonpay key

* add initial FoR hype border flash to announcement acknowledgment

* remove ... menu access to FoR feature

* add tooltip and external link to info icon

* nicer error display

* add stale market to ack

* pr feedback from zzmp

* fix really weird react bug

* ts fix and clear timeout

* pairing staleness handler w/ zzmp

* add back feature flag

* feat: fiat on ramp tile for about page

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
2022-12-20 13:23:29 -05:00
eddie
0bf7b92013 feat: move about content into landing page (#5734)
* feat: move about content into landing page

* fix: delete unused image files

* fix: remove unused components

* fix: pointer events on title and subtitle

* feat: add about footer to landing page

* fix: simplify css in Landing Page

* feat(moonpay): moonpay ip checks to determine if the user can access the fiat onramp (#10)

* feat(moonpay): useFiatOnrampAvailable

* feat(moonpay): ip check with moonpay for buy crypto availability

* add error state and clear up some of the sequence of logic

* add button-specific spinner, put the ... menu button behind the feature flag

* hide ... menu option if onramp is unavailable

* add live publishable moonpay key

* add initial FoR hype border flash to announcement acknowledgment

* remove ... menu access to FoR feature

* add tooltip and external link to info icon

* nicer error display

* add stale market to ack

* pr feedback from zzmp

* fix really weird react bug

* ts fix and clear timeout

* pairing staleness handler w/ zzmp

* add back feature flag

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
2022-12-20 13:07:46 -05:00
Jordan Frankfurt
283479f76e test: skip tests due to fiat on-ramp change (#5742)
skip tests
2022-12-20 12:31:11 -05:00
Vignesh Mohankumar
d3c30e2f6b docs: update buying crypto helpcenter link (#5744)
* docs: update buying crypto helpcenter link

* new link

* new link
2022-12-20 12:30:32 -05:00
Jordan Frankfurt
32d226f78e feat: enable FoR flag (#5741) 2022-12-20 11:50:00 -05:00
Jordan Frankfurt
96744505c0 Merge pull request #5740 from Uniswap/FoR-publish
feat: FoR publish
2022-12-20 10:40:36 -06:00
Jordan Frankfurt
97236033d4 conditional access of .renderCount 2022-12-20 10:27:52 -06:00
Jordan Frankfurt
86e62dc4b9 add back feature flag 2022-12-20 09:56:15 -06:00
Vignesh Mohankumar
e584a5fa36 chore: removes unused Liquidity Mining code (#5451)
* feat: remove unused liquidity mining pages

* some changes

* rm more
2022-12-19 20:06:59 -05:00
Vignesh Mohankumar
332ef6e6c8 chore: remove /#/claim (#5445)
rm claim
2022-12-19 19:46:36 -05:00
Jordan Frankfurt
8cbd111e65 Merge pull request #8 from Uniswap/merge-upstream
chore: Merge upstream
2022-12-19 15:25:10 -06:00
Jordan Frankfurt
55ffcbd465 Merge remote-tracking branch 'upstream/main' into merge-upstream 2022-12-19 15:12:16 -06:00
Jordan Frankfurt
404775e86d Merge pull request #7 from Uniswap/analytics-account-dropdown-click
chore: analytics account dropdown click
2022-12-19 15:10:15 -06:00
Vignesh Mohankumar
0ae9fe28a2 feat: force landing page to show based on landing query param (#5730)
* feat: force landing page to show based on `landing` query param

* flag

* use intro

* lint
2022-12-19 15:49:42 -05:00
Vignesh Mohankumar
89c0caae43 feat: push nav icon to /?intro=true (#5731)
* feat: push nav icon to /?intro=true

* search
2022-12-19 15:48:53 -05:00
Vignesh Mohankumar
c8086e3c76 fix: wait to render Landing content until checking wallet cache (#5729)
* fix: wait to render Landing content until checking wallet cache

* use state
2022-12-19 15:22:13 -05:00
Jordan Frankfurt
1c2842e5a0 chore: analytics account dropdown click 2022-12-19 14:21:50 -06:00
Vignesh Mohankumar
a2c6d3f475 feat: navigate to /swap if user has a wallet cached (#5728)
* feat: navigate to /swap if user has a wallet cached

* flag it
2022-12-19 14:55:51 -05:00
Jordan Frankfurt
841ea7f8a1 Merge pull request #6 from Uniswap/fix-menu-z-index
fix: landing page overlay occlusion of Z_INDEX.dropdown
2022-12-19 13:49:10 -06:00
aballerr
804692b114 fix: adding fixed header (#5712)
*  adding fixed header
2022-12-19 14:26:07 -05:00
Vignesh Mohankumar
6282298d13 chore: add landing page redirect flag (#5724)
* chore: add landing page redirect flag

* unused

* fix
2022-12-19 13:38:03 -05:00
eddie
7a5b855097 chore: bump version of @uniswap/analytics-events (#5726) 2022-12-19 13:36:42 -05:00
Jordan Frankfurt
c9908748cf make under-dropdown 990 2022-12-19 12:24:43 -06:00
Jordan Frankfurt
79b77deee1 fix: bump conedison (#5725) 2022-12-19 12:15:16 -06:00
Jordan Frankfurt
a554af6670 pr feedback 2022-12-19 11:39:29 -06:00
lynn
1843f214b1 fix: hover button states (#5553)
* init but this looks wrong based on figma

* init

* fix props

* add back children
2022-12-19 12:18:25 -05:00
Jordan Frankfurt
3e0788092e Merge pull request #5 from Uniswap/geocheck-analytics
feat: add analytics for moonpay ip check
2022-12-19 11:13:30 -06:00
Jordan Frankfurt
d14c49df0d fix: landing page overlay occlusion of Z_INDEX.dropdown 2022-12-19 11:03:03 -06:00
eddie
c098ad1ffe fix: correct font size for the trade rate to on safari (#5714)
* fix: correct font size for the trade rate to on safari

* fix: use themedText.BodySmall for this label
2022-12-19 09:27:43 -05:00
Jordan Frankfurt
48114ef51d feat: add analytics for moonpay ip check 2022-12-18 09:48:38 -06:00
Jordan Frankfurt
cb7132ee17 Merge pull request #3 from Uniswap/FoR-main
feat: FoR commits from mgtm repo
2022-12-16 11:10:30 -06:00
Jordan Frankfurt
0fa4859a09 6a47ac3c231a42a00ffee40677c46bf612e14187 2022-12-15 17:39:38 -06:00
Jordan Frankfurt
f8bb5046f0 73ad9987e4b337987f8e3cb2ef861bf03c42cc67 2022-12-15 17:37:58 -06:00
Jordan Frankfurt
7d1589d1df fixing from cherry-pick process 2022-12-15 17:05:06 -06:00
Jordan Frankfurt
26b603cc2e fix: update moonpay supported currencies list (#57)
lint
2022-12-15 16:34:05 -06:00
Jordan Frankfurt
ece68a0ec7 fix: close wallet modal on connection (#53)
fix: hide celo on uniswap wallet connections (#48)

* fix: hide celo on uniswap wallet connections

* add disabled UI instead of removing network from list

Revert "fix: hide celo on uniswap wallet connections (#48)"

This reverts commit b22d82545fef6812b22fe1adc3f281588642c9cd.
2022-12-15 15:32:21 -06:00
Jordan Frankfurt
fd212477ce fix: json.stringify addresses (#52) 2022-12-15 15:32:20 -06:00
Jordan Frankfurt
a16d2387cc fix: FoR polish (#47)
* fix: remove currencyCode config property from moonpay url

* add 8px padding to moonpay iframe

* don't switch button text when loading

* update tooltip text

* update announcement text

* remove FoR announcement on iOS mobile

* improve fiat quote format in account dropdown

fix: typo (#51)
2022-12-15 15:32:20 -06:00
Jordan Frankfurt
cae56ec385 feat: add supported currency list to moonpay config (#45) 2022-12-15 15:32:20 -06:00
Jordan Frankfurt
d16b3473e0 fix: remove currencyCode config property from moonpay url (#40) 2022-12-15 15:32:20 -06:00
Jordan Frankfurt
f66f249dba fix: fiat onramp polish (#36)
* fix: fiat onramp polish

* add 3 session render cap on FoR announcement
2022-12-15 15:32:20 -06:00
Jordan Frankfurt
08afd888d0 fix(moonpay): add ?platform=web to moonpay env vars (#35) 2022-12-15 15:32:20 -06:00
Jordan Frankfurt
b427be2673 feat(moonpay): moonpay ip checks to determine if the user can access the fiat onramp (#10)
* feat(moonpay): useFiatOnrampAvailable

* feat(moonpay): ip check with moonpay for buy crypto availability

* add error state and clear up some of the sequence of logic

* add button-specific spinner, put the ... menu button behind the feature flag

* hide ... menu option if onramp is unavailable

* add live publishable moonpay key

* add initial FoR hype border flash to announcement acknowledgment

* remove ... menu access to FoR feature

* add tooltip and external link to info icon

* nicer error display

* add stale market to ack

* pr feedback from zzmp

* fix really weird react bug

* ts fix and clear timeout

* pairing staleness handler w/ zzmp
2022-12-15 15:32:20 -06:00
Jordan Frankfurt
f753a5e325 feat(moonpay): add iframe (#9)
* feat(moonpay): add iframe

* add prod url

* polish iframe styles and supply the firebase fn with the new platform query param

* pr feedback

* pr feedback - .production env key distinction

feat(moonpay): add iframe (#9)

* feat(moonpay): add iframe

* add prod url

* polish iframe styles and supply the firebase fn with the new platform query param

* pr feedback

* pr feedback - .production env key distinction
2022-12-15 15:32:19 -06:00
Jordan Frankfurt
46d9d8e3df feat(fiat): announcement
cleanup

cleanup

fix close button

cleanup
2022-12-15 15:32:19 -06:00
Jordan Frankfurt
680d3a3f26 feat(fiat): add overflow menu cta 2022-12-15 15:32:19 -06:00
Jordan Frankfurt
e4c625ee71 feat(onramp): cta 2022-12-15 15:32:19 -06:00
199 changed files with 11175 additions and 4309 deletions

7
.env
View File

@@ -1,7 +1,12 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_SENTRY_ENABLED=false
ESLINT_NO_DEV_ERRORS=true
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"

View File

@@ -1,7 +1,11 @@
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
REACT_APP_SENTRY_ENABLED=true

View File

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

7
.eslintrc.js Normal file
View File

@@ -0,0 +1,7 @@
/* eslint-env node */
require('@uniswap/eslint-config/load')
module.exports = {
extends: '@uniswap/eslint-config/react',
}

View File

@@ -1,109 +0,0 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
// Allows for the parsing of JSX
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
},
"ignorePatterns": [
"src/types/v3",
"src/abis/types",
"src/locales/**/*.js",
"src/locales/**/en-US.po",
"node_modules",
"coverage",
"build",
"dist",
".DS_Store",
".env.local",
".env.development.local",
".env.test.local",
".env.production.local",
".idea/",
".vscode/",
"package-lock.json",
"yarn.lock"
],
"extends": [
"react-app",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
"plugin:import/typescript"
],
"plugins": ["import", "simple-import-sort", "unused-imports"],
"rules": {
"import/no-unused-modules": [2, { "unusedExports": true }],
"unused-imports/no-unused-imports": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"@typescript-eslint/explicit-function-return-type": "off",
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-curly-brace-presence": ["error", { "props": "never", "children": "never" }],
"object-shorthand": ["error", "always"],
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "ethers",
"message": "Please import from '@ethersproject/module' directly to support tree-shaking."
},
{
"name": "styled-components",
"message": "Please import from styled-components/macro."
},
{
"name": "@lingui/macro",
"importNames": ["t"],
"message": "Please use <Trans> instead of t."
}
],
"patterns": [
{
"group": ["**/dist"],
"message": "Do not import from dist/ - this is an implementation detail, and breaks tree-shaking."
},
{
"group": ["!styled-components/macro"]
}
]
}
],
"@typescript-eslint/no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@ethersproject/providers",
"message": "Please only use Providers instantiated in constants/providers to improve traceability.",
"allowTypeImports": true
}
]
}
]
}
}

View File

@@ -1,7 +1,5 @@
name: Release
on:
schedule:
- cron: '0 12 * * 1-4' # every day 12:00 UTC Monday-Thursday
# manual trigger
workflow_dispatch:
@@ -112,7 +110,8 @@ jobs:
githubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Upload source maps to Sentry
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
@@ -120,4 +119,4 @@ jobs:
with:
environment: production
sourcemaps: './build/static/js'
url_prefix: '/static/js'
url_prefix: '~/static/js'

1
.gitignore vendored
View File

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

View File

@@ -1 +0,0 @@
/src/schema/schema.graphql

View File

@@ -1,5 +0,0 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 120
}

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
],
"unwantedRecommendations": []
}

View File

@@ -5,15 +5,12 @@
"editor.formatOnSaveMode": "file",
"editor.tabCompletion": "on",
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.inlineSuggest.enabled": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"files.eol": "\n",
"eslint.enable": true,
"eslint.debug": true,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"eslint.debug": true
}

25
apollo-codegen.ts Normal file
View File

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

View File

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

View File

@@ -1,3 +1,5 @@
/* eslint-env node */
const isDev = process.env.NODE_ENV === 'development'
module.exports = {

View File

@@ -1,10 +1,29 @@
import { getTestSelector } from '../utils'
describe('Landing Page', () => {
beforeEach(() => cy.visit('/'))
it('loads swap page', () => {
cy.get('#swap-page')
it('shows landing page when no selectedWallet', () => {
cy.visit('/', { noWallet: true })
cy.get(getTestSelector('landing-page'))
cy.screenshot()
})
it('redirects to swap page when selectedWallet is INJECTED', () => {
cy.visit('/', { selectedWallet: 'INJECTED' })
cy.get('#swap-page')
cy.url().should('include', '/swap')
cy.screenshot()
})
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
cy.get(getTestSelector('landing-page'))
})
it('shows landing page when the unicorn icon in nav is selected', () => {
cy.get(getTestSelector('uniswap-logo')).click()
cy.get(getTestSelector('landing-page'))
})
it('allows navigation to pool', () => {
cy.get('#pool-nav-link').click()
cy.url().should('include', '/pool')

View File

@@ -59,6 +59,5 @@ describe('Testing nfts', () => {
cy.get(getTestSelector('nft-no-nfts-selected')).should('exist')
cy.get(getTestSelector('nft-bag-close-icon')).click()
cy.get(getTestSelector('nft-explore-nfts-button')).click()
cy.get(getTestSelector('nft-welcome-modal')).should('exist')
})
})

View File

@@ -8,7 +8,8 @@ describe(
},
() => {
it('loads swap page', () => {
// We *must* wait in order to space out the retry attempts.
// TODO: We *must* wait in order to space out the retry attempts. Find a better way to do this.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(ONE_MINUTE)
.visit('/', {
retryOnStatusCodeFailure: true,

View File

@@ -20,6 +20,8 @@ declare global {
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
selectedWallet?: string
noWallet?: boolean
}
}
}
@@ -38,7 +40,12 @@ Cypress.Commands.overwrite(
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
win.localStorage.clear()
win.localStorage.setItem('redux_localstorage_simple_user', '{"selectedWallet":"INJECTED"}')
const userState = {
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
fiatOnrampDismissed: true,
}
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce(
@@ -78,8 +85,7 @@ beforeEach(() => {
})
})
Cypress.on('uncaught:exception', (_err, _runnable) => {
// returning false here prevents Cypress from
// failing the test
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from failing the test
return false
})

View File

@@ -1,7 +1,7 @@
// Utility to match GraphQL mutation based on the query name
export const hasQuery = (req: any, queryName: string) => {
const { body } = req
return body.hasOwnProperty('query') && body.query.includes(queryName)
return Object.prototype.hasOwnProperty.call(body, 'query') && body.query.includes(queryName)
}
// Alias query if queryName matches

View File

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

View File

@@ -1,3 +1,5 @@
/* eslint-env node */
module.exports = {
src: './src',
language: 'typescript',

View File

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

View File

@@ -8,10 +8,10 @@
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
"relay": "relay-compiler relay.config.js",
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
"graphql:fetch": "node fetch-schema.js",
"graphql:generate": "yarn relay && yarn relay-thegraph",
"graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
"prei18n:extract": "node prei18n-extract.js",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
@@ -90,39 +90,35 @@
"@types/ua-parser-js": "^0.7.35",
"@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4",
"@typescript-eslint/parser": "^4",
"@uniswap/eslint-config": "^1.1.1",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-relay": "^14.1.0",
"cypress": "^10.3.1",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.11.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-better-styled-components": "^1.1.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"jest-styled-components": "^7.0.8",
"ms.macro": "^2.0.0",
"patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.7.1",
"react-scripts": "^4.0.3",
"relay-compiler": "^14.1.0",
"serve": "^11.3.2",
"ts-transform-graphql-tag": "^0.2.1",
"typechain": "^5.0.0",
"typescript": "^4.4.3",
"yarn-deduplicate": "^6.0.0"
},
"dependencies": {
"@apollo/client": "^3.7.2",
"@coinbase/wallet-sdk": "^3.3.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@graphql-codegen/cli": "^2.15.0",
"@graphql-codegen/client-preset": "^1.2.1",
"@graphql-codegen/typescript": "^2.8.3",
"@graphql-codegen/typescript-operations": "^2.5.8",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@graphql-codegen/typescript-resolvers": "^2.7.8",
"@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
@@ -135,11 +131,10 @@
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "7.20.1",
"@types/react-relay": "^13.0.2",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "1.2.0",
"@uniswap/analytics-events": "1.3.1",
"@uniswap/conedison": "^1.1.0",
"@uniswap/analytics-events": "^1.5.0",
"@uniswap/conedison": "^1.1.1",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
@@ -215,8 +210,6 @@
"react-popper": "^2.2.3",
"react-query": "^3.39.1",
"react-redux": "^8.0.2",
"react-relay": "^14.1.0",
"react-relay-network-modern": "^6.2.1",
"react-router-dom": "^6.3.0",
"react-spring": "^9.5.5",
"react-table": "^7.8.0",

View File

@@ -1,4 +1,5 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
/* eslint-env node */
const { exec } = require('child_process')
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 3.5 MiB

View File

@@ -0,0 +1,89 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { BookOpen, Globe, Heart, Twitter } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
const Footer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
gap: 48px;
max-width: 1440px;
`
const FooterLinks = styled.div`
display: grid;
grid-template-columns: 1fr;
gap: 12px;
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
`
const FooterLink = styled.a`
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
color: ${({ theme }) => theme.textPrimary};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
font-size: 16px;
line-height: 20px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
text-decoration: none;
svg {
color: ${({ theme }) => theme.textSecondary};
stroke-width: 1.5;
}
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
&:hover {
border: 1px solid ${({ theme }) => theme.textTertiary};
}
@media screen and (min-width: ${BREAKPOINTS.md}px) {
font-size: 20px;
line-height: 24px;
}
`
const Copyright = styled.span`
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.textTertiary};
`
export const AboutFooter = () => {
return (
<Footer>
<FooterLinks>
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.SUPPORT_LINK}>
<FooterLink rel="noopener noreferrer" target="_blank" href="https://support.uniswap.org">
<Globe /> Support
</FooterLink>
</TraceEvent>
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.TWITTER_LINK}>
<FooterLink rel="noopener noreferrer" target="_blank" href="https://twitter.com/uniswap">
<Twitter /> Twitter
</FooterLink>
</TraceEvent>
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.BLOG_LINK}>
<FooterLink rel="noopener noreferrer" target="_blank" href="https://uniswap.org/blog">
<BookOpen /> Blog
</FooterLink>
</TraceEvent>
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={ElementName.CAREERS_LINK}>
<FooterLink rel="noopener noreferrer" target="_blank" href="https://boards.greenhouse.io/uniswaplabs">
<Heart /> Careers
</FooterLink>
</TraceEvent>
</FooterLinks>
<Copyright>© {new Date().getFullYear()} Uniswap Labs</Copyright>
</Footer>
)
}

View File

@@ -0,0 +1,150 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, EventName } from '@uniswap/analytics-events'
import { Link } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled, { DefaultTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
export enum CardType {
Primary = 'Primary',
Secondary = 'Secondary',
}
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string; type: CardType }>`
display: flex;
background: ${({ isDarkMode, backgroundImgSrc, type, theme }) =>
isDarkMode
? `${type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
backgroundImgSrc ? ` url(${backgroundImgSrc})` : ''
}`
: `${type === CardType.Primary ? 'white' : theme.backgroundModule} url(${backgroundImgSrc})`};
background-size: auto 100%;
background-position: right;
background-repeat: no-repeat;
background-origin: border-box;
flex-direction: column;
justify-content: space-between;
text-decoration: none;
color: ${({ theme }) => theme.textPrimary};
padding: 24px;
height: 212px;
border-radius: 24px;
border: 1px solid ${({ theme, type }) => (type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
box-shadow: 0px 10px 24px 0px rgba(51, 53, 72, 0.04);
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
&:hover {
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
}
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 260)}px;
}
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
padding: 32px;
}
`
const TitleRow = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`
const CardTitle = styled.div`
font-size: 20px;
line-height: 28px;
font-weight: 600;
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
font-size: 28px;
line-height: 36px;
}
`
const getCardDescriptionColor = (type: CardType, theme: DefaultTheme) => {
switch (type) {
case CardType.Secondary:
return theme.textSecondary
default:
return theme.textPrimary
}
}
const CardDescription = styled.div<{ type: CardType }>`
display: flex;
flex-direction: column;
font-size: 16px;
line-height: 20px;
color: ${({ theme, type }) => getCardDescriptionColor(type, theme)};
padding: 0 40px 0 0;
max-width: 480px;
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
font-size: 20px;
line-height: 28px;
max-width: 480px;
}
`
const CardCTA = styled(CardDescription)`
color: ${({ theme }) => theme.accentAction};
font-weight: 500;
margin: 24px 0 0;
cursor: pointer;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
&:hover {
opacity: 0.6;
}
`
const Card = ({
type = CardType.Primary,
title,
description,
cta,
to,
external,
backgroundImgSrc,
icon,
elementName,
}: {
type?: CardType
title: string
description: string
cta?: string
to: string
external?: boolean
backgroundImgSrc?: string
icon?: React.ReactNode
elementName?: string
}) => {
const isDarkMode = useIsDarkMode()
return (
<TraceEvent events={[BrowserEvent.onClick]} name={EventName.ELEMENT_CLICKED} element={elementName}>
<StyledCard
type={type}
as={external ? 'a' : Link}
to={external ? undefined : to}
href={external ? to : undefined}
target={external ? '_blank' : undefined}
rel={external ? 'noopenener noreferrer' : undefined}
isDarkMode={isDarkMode}
backgroundImgSrc={backgroundImgSrc}
>
<TitleRow>
<CardTitle>{title}</CardTitle>
{icon}
</TitleRow>
<CardDescription type={type}>
{description}
<CardCTA type={type}>{cta}</CardCTA>
</CardDescription>
</StyledCard>
</TraceEvent>
)
}
export default Card

View File

@@ -0,0 +1,106 @@
import { ButtonEmpty } from 'components/Button'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import meshSrc from './images/Mesh.png'
const DARK_MODE_GRADIENT = 'radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%)'
const Banner = styled.div<{ isDarkMode: boolean }>`
height: 340px;
width: 100%;
border-radius: 32px;
max-width: 1440px;
margin: 80px 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 32px 48px;
box-shadow: 0px 10px 24px rgba(51, 53, 72, 0.04);
background: ${({ isDarkMode }) =>
isDarkMode
? `url(${meshSrc}), ${DARK_MODE_GRADIENT}`
: `url(${meshSrc}), linear-gradient(93.06deg, #FF00C7 2.66%, #FF9FFB 98.99%);`};
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
height: 140px;
flex-direction: row;
}
`
const TextContainer = styled.div`
color: white;
display: flex;
flex: 1;
flex-direction: column;
`
const HeaderText = styled.div`
font-weight: 700;
font-size: 28px;
line-height: 36px;
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
font-size: 28px;
line-height: 36px;
}
`
const DescriptionText = styled.div`
margin: 10px 10px 0 0;
font-weight: 500;
font-size: 16px;
line-height: 20px;
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
font-size: 20px;
line-height: 28px;
}
`
const BannerButtonContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} opacity`};
&:hover {
opacity: 0.6;
}
@media screen and (min-width: ${BREAKPOINTS.lg}px) {
width: auto;
}
`
const BannerButton = styled(ButtonEmpty)`
color: white;
border: 1px solid white;
`
const ProtocolBanner = () => {
const isDarkMode = useIsDarkMode()
return (
<Banner isDarkMode={isDarkMode}>
<TextContainer>
<HeaderText>Powered by the Uniswap Protocol</HeaderText>
<DescriptionText>
The leading decentralized crypto trading protocol, governed by a global community.
</DescriptionText>
</TextContainer>
<BannerButtonContainer>
<BannerButton width="200px" as="a" href="https://uniswap.org" rel="noopener noreferrer" target="_blank">
Learn more
</BannerButton>
</BannerButtonContainer>
</Banner>
)
}
export default ProtocolBanner

View File

@@ -0,0 +1,71 @@
import { ElementName } from '@uniswap/analytics-events'
import { DollarSign, Terminal } from 'react-feather'
import styled from 'styled-components/macro'
import { lightTheme } from 'theme/colors'
import darkArrowImgSrc from './images/aboutArrowDark.png'
import lightArrowImgSrc from './images/aboutArrowLight.png'
import darkDollarImgSrc from './images/aboutDollarDark.png'
import darkTerminalImgSrc from './images/aboutTerminalDark.png'
import nftCardImgSrc from './images/nftCard.png'
import swapCardImgSrc from './images/swapCard.png'
export const MAIN_CARDS = [
{
to: '/swap',
title: 'Swap tokens',
description: 'Buy, sell, and explore tokens on Ethereum, Polygon, Optimism, and more.',
cta: 'Trade Tokens',
darkBackgroundImgSrc: swapCardImgSrc,
lightBackgroundImgSrc: swapCardImgSrc,
elementName: ElementName.ABOUT_PAGE_SWAP_CARD,
},
{
to: '/nfts',
title: 'Trade NFTs',
description: 'Buy and sell NFTs across marketplaces to find more listings at better prices.',
cta: 'Explore NFTs',
darkBackgroundImgSrc: nftCardImgSrc,
lightBackgroundImgSrc: nftCardImgSrc,
elementName: ElementName.ABOUT_PAGE_NFTS_CARD,
},
]
const StyledCardLogo = styled.img`
min-width: 20px;
min-height: 20px;
max-height: 48px;
max-width: 48px;
`
export const MORE_CARDS = [
{
to: 'https://support.uniswap.org/hc/en-us/articles/11306574799117-How-to-use-Moon-Pay-on-the-Uniswap-web-app-',
external: true,
title: 'Buy crypto',
description: 'Buy crypto with your credit card or bank account at the best rates.',
lightIcon: <DollarSign color={lightTheme.textTertiary} size={48} />,
darkIcon: <StyledCardLogo src={darkDollarImgSrc} alt="Earn" />,
cta: 'Buy now',
elementName: ElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
},
{
to: '/pool',
title: 'Earn',
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,
darkIcon: <StyledCardLogo src={darkArrowImgSrc} alt="Analytics" />,
cta: 'Provide liquidity',
elementName: ElementName.ABOUT_PAGE_EARN_CARD,
},
{
to: 'https://docs.uniswap.org',
external: true,
title: 'Build dApps',
description: 'Build apps and tools on the largest DeFi protocol on Ethereum.',
lightIcon: <Terminal color={lightTheme.textTertiary} size={48} />,
darkIcon: <StyledCardLogo src={darkTerminalImgSrc} alt="Developers" />,
cta: 'Developer docs',
elementName: ElementName.ABOUT_PAGE_DEV_DOCS_CARD,
},
]

View File

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 327 KiB

View File

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -14,18 +14,15 @@ import {
CollectFeesTransactionInfo,
CreateV3PoolTransactionInfo,
DelegateTransactionInfo,
DepositLiquidityStakingTransactionInfo,
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
ExecuteTransactionInfo,
MigrateV2LiquidityToV3TransactionInfo,
QueueTransactionInfo,
RemoveLiquidityV3TransactionInfo,
SubmitProposalTransactionInfo,
TransactionInfo,
TransactionType,
VoteTransactionInfo,
WithdrawLiquidityStakingTransactionInfo,
WrapTransactionInfo,
} from '../../state/transactions/types'
@@ -83,7 +80,7 @@ function ClaimSummary({ info: { recipient, uniAmountRaw } }: { info: ClaimTransa
)
}
function SubmitProposalTransactionSummary(_: { info: SubmitProposalTransactionInfo }) {
function SubmitProposalTransactionSummary() {
return <Trans>Submit new proposal</Trans>
}
@@ -175,13 +172,13 @@ function WrapSummary({ info: { chainId, currencyAmountRaw, unwrapped } }: { info
}
}
function DepositLiquidityStakingSummary(_: { info: DepositLiquidityStakingTransactionInfo }) {
function DepositLiquidityStakingSummary() {
// not worth rendering the tokens since you can should no longer deposit liquidity in the staking contracts
// todo: deprecate and delete the code paths that allow this, show user more information
return <Trans>Deposit liquidity</Trans>
}
function WithdrawLiquidityStakingSummary(_: { info: WithdrawLiquidityStakingTransactionInfo }) {
function WithdrawLiquidityStakingSummary() {
return <Trans>Withdraw deposited liquidity</Trans>
}
@@ -319,10 +316,10 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
return <ClaimSummary info={info} />
case TransactionType.DEPOSIT_LIQUIDITY_STAKING:
return <DepositLiquidityStakingSummary info={info} />
return <DepositLiquidityStakingSummary />
case TransactionType.WITHDRAW_LIQUIDITY_STAKING:
return <WithdrawLiquidityStakingSummary info={info} />
return <WithdrawLiquidityStakingSummary />
case TransactionType.SWAP:
return <SwapSummary info={info} />
@@ -358,6 +355,6 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
return <ExecuteSummary info={info} />
case TransactionType.SUBMIT_PROPOSAL:
return <SubmitProposalTransactionSummary info={info} />
return <SubmitProposalTransactionSummary />
}
}

View File

@@ -39,26 +39,32 @@ const getCurrency = ({ info, chainId }: { info: TransactionInfo; chainId: number
switch (info.type) {
case TransactionType.ADD_LIQUIDITY_V3_POOL:
case TransactionType.REMOVE_LIQUIDITY_V3:
case TransactionType.CREATE_V3_POOL:
case TransactionType.CREATE_V3_POOL: {
const { baseCurrencyId, quoteCurrencyId } = info
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
case TransactionType.SWAP:
}
case TransactionType.SWAP: {
const { inputCurrencyId, outputCurrencyId } = info
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
case TransactionType.WRAP:
}
case TransactionType.WRAP: {
const { unwrapped } = info
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
const base = 'ETH'
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
case TransactionType.COLLECT_FEES:
}
case TransactionType.COLLECT_FEES: {
const { currencyId0, currencyId1 } = info
return { currencyId0, currencyId1 }
case TransactionType.APPROVAL:
}
case TransactionType.APPROVAL: {
return { currencyId0: info.tokenAddress, currencyId1: undefined }
case TransactionType.CLAIM:
}
case TransactionType.CLAIM: {
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
return { currencyId0: uniAddress, currencyId1: undefined }
}
default:
return { currencyId0: undefined, currencyId1: undefined }
}

View File

@@ -0,0 +1,13 @@
import { SpinnerSVG } from 'theme'
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
opacity="0.1"
d="M18.8334 10.0003C18.8334 14.6027 15.1025 18.3337 10.5001 18.3337C5.89771 18.3337 2.16675 14.6027 2.16675 10.0003C2.16675 5.39795 5.89771 1.66699 10.5001 1.66699C15.1025 1.66699 18.8334 5.39795 18.8334 10.0003ZM4.66675 10.0003C4.66675 13.222 7.27842 15.8337 10.5001 15.8337C13.7217 15.8337 16.3334 13.222 16.3334 10.0003C16.3334 6.77867 13.7217 4.16699 10.5001 4.16699C7.27842 4.16699 4.66675 6.77867 4.66675 10.0003Z"
/>
<path d="M17.5834 10.0003C18.2738 10.0003 18.843 9.4376 18.7398 8.755C18.6392 8.0891 18.458 7.43633 18.1991 6.8113C17.7803 5.80025 17.1665 4.88159 16.3926 4.10777C15.6188 3.33395 14.7002 2.72012 13.6891 2.30133C13.0641 2.04243 12.4113 1.86121 11.7454 1.76057C11.0628 1.6574 10.5001 2.22664 10.5001 2.91699C10.5001 3.60735 11.066 4.15361 11.7405 4.30041C12.0789 4.37406 12.4109 4.47786 12.7324 4.61103C13.4401 4.90418 14.0832 5.33386 14.6249 5.87554C15.1665 6.41721 15.5962 7.06027 15.8894 7.76801C16.0225 8.08949 16.1264 8.42147 16.2 8.75986C16.3468 9.43443 16.8931 10.0003 17.5834 10.0003Z" />
</SpinnerSVG>
)
export default ButtonLoadingSpinner

View File

@@ -5,16 +5,31 @@ import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
import { RowBetween } from '../Row'
export { default as LoadingButtonSpinner } from './LoadingButtonSpinner'
type ButtonProps = Omit<ButtonPropsOriginal, 'css'>
export const BaseButton = styled(RebassButton)<
{
padding?: string
width?: string
$borderRadius?: string
altDisabledStyle?: boolean
} & ButtonProps
>`
const ButtonOverlay = styled.div`
background-color: transparent;
bottom: 0;
border-radius: inherit;
height: 100%;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: 150ms ease background-color;
width: 100%;
`
type BaseButtonProps = {
padding?: string
width?: string
$borderRadius?: string
altDisabledStyle?: boolean
} & ButtonProps
export const BaseButton = styled(RebassButton)<BaseButtonProps>`
padding: ${({ padding }) => padding ?? '16px'};
width: ${({ width }) => width ?? '100%'};
font-weight: 500;
@@ -86,7 +101,7 @@ export const SmallButtonPrimary = styled(ButtonPrimary)`
border-radius: 12px;
`
export const ButtonLight = styled(BaseButton)`
const BaseButtonLight = styled(BaseButton)`
background-color: ${({ theme }) => theme.accentActionSoft};
color: ${({ theme }) => theme.accentAction};
font-size: 20px;
@@ -103,6 +118,19 @@ export const ButtonLight = styled(BaseButton)`
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
background-color: ${({ theme, disabled }) => !disabled && theme.accentActionSoft};
}
:hover {
${ButtonOverlay} {
background-color: ${({ theme }) => theme.stateOverlayHover};
}
}
:active {
${ButtonOverlay} {
background-color: ${({ theme }) => theme.stateOverlayPressed};
}
}
:disabled {
opacity: 0.4;
:hover {
@@ -369,18 +397,6 @@ export function ButtonRadioChecked({ active = false, children, ...rest }: { acti
}
}
const ButtonOverlay = styled.div`
background-color: transparent;
bottom: 0;
border-radius: 16px;
height: 100%;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: 150ms ease background-color;
width: 100%;
`
export enum ButtonSize {
small,
medium,
@@ -395,7 +411,7 @@ export enum ButtonEmphasis {
warning,
destructive,
}
interface BaseButtonProps {
interface BaseThemeButtonProps {
size: ButtonSize
emphasis: ButtonEmphasis
}
@@ -474,7 +490,7 @@ function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; em
}
}
const BaseThemeButton = styled.button<BaseButtonProps>`
const BaseThemeButton = styled.button<BaseThemeButtonProps>`
align-items: center;
background-color: ${pickThemeButtonBackgroundColor};
border-radius: 16px;
@@ -491,16 +507,13 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
padding: ${pickThemeButtonPadding};
position: relative;
transition: 150ms ease opacity;
user-select: none;
:active {
${ButtonOverlay} {
background-color: ${({ theme }) => theme.stateOverlayPressed};
}
}
:disabled {
cursor: default;
opacity: 0.6;
}
:focus {
${ButtonOverlay} {
background-color: ${({ theme }) => theme.stateOverlayPressed};
@@ -511,9 +524,20 @@ const BaseThemeButton = styled.button<BaseButtonProps>`
background-color: ${({ theme }) => theme.stateOverlayHover};
}
}
:disabled {
cursor: default;
opacity: 0.6;
}
:disabled:active,
:disabled:focus,
:disabled:hover {
${ButtonOverlay} {
background-color: transparent;
}
}
`
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseButtonProps {}
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {}
export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
return (
@@ -523,3 +547,12 @@ export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
</BaseThemeButton>
)
}
export const ButtonLight = ({ children, ...rest }: BaseButtonProps) => {
return (
<BaseButtonLight {...rest}>
<ButtonOverlay />
{children}
</BaseButtonLight>
)
}

View File

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

View File

@@ -6,6 +6,7 @@ import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useState } from 'react'
import { Copy } from 'react-feather'
import styled from 'styled-components/macro'
import { isSentryEnabled } from 'utils/env'
import { CopyToClipboard, ExternalLink, ThemedText } from '../../theme'
import { Column } from '../Column'
@@ -85,6 +86,7 @@ const CodeTitle = styled.div`
display: flex;
gap: 14px;
align-items: center;
justify-content: space-between;
word-break: break-word;
`
@@ -92,49 +94,87 @@ const Fallback = ({ error, eventId }: { error: Error; eventId: string | null })
const [isExpanded, setExpanded] = useState(false)
const isMobile = useIsMobile()
const errorId = eventId || 'unknown-error'
// @todo: ThemedText components should be responsive by default
const [Title, Description] = isMobile
? [ThemedText.HeadlineSmall, ThemedText.BodySmall]
: [ThemedText.HeadlineLarge, ThemedText.BodySecondary]
const showErrorId = isSentryEnabled() && eventId
const showMoreButton = (
<ShowMoreButton onClick={() => setExpanded((s) => !s)}>
<ThemedText.Link color="textSecondary">
<Trans>{isExpanded ? 'Show less' : 'Show more'}</Trans>
</ThemedText.Link>
<ShowMoreIcon $isExpanded={isExpanded} secondaryWidth="20" secondaryHeight="20" />
</ShowMoreButton>
)
const errorDetails = error.stack || error.message
return (
<FallbackWrapper>
<BodyWrapper>
<Column gap="xl">
<Column gap="sm">
<Title textAlign="center">
<Trans>Something went wrong</Trans>
</Title>
<Description textAlign="center" color="textSecondary">
<Trans>
Sorry, an error occured while processing your request. If you request support, be sure to provide your
error ID.
</Trans>
</Description>
</Column>
<CodeBlockWrapper>
<CodeTitle>
<ThemedText.SubHeader fontWeight={500}>Error ID: {errorId}</ThemedText.SubHeader>
<CopyToClipboard toCopy={errorId}>
<CopyIcon />
</CopyToClipboard>
</CodeTitle>
<Separator />
{isExpanded && (
<>
<Code>{error.stack}</Code>
{showErrorId ? (
<>
<Column gap="sm">
<Title textAlign="center">
<Trans>Something went wrong</Trans>
</Title>
<Description textAlign="center" color="textSecondary">
<Trans>
Sorry, an error occured while processing your request. If you request support, be sure to provide
your error ID.
</Trans>
</Description>
</Column>
<CodeBlockWrapper>
<CodeTitle>
<ThemedText.SubHeader fontWeight={500}>
<Trans>Error ID: {eventId}</Trans>
</ThemedText.SubHeader>
<CopyToClipboard toCopy={eventId}>
<CopyIcon />
</CopyToClipboard>
</CodeTitle>
<Separator />
</>
)}
<ShowMoreButton onClick={() => setExpanded((s) => !s)}>
<ThemedText.Link color="textSecondary">
<Trans>{isExpanded ? 'Show less' : 'Show more'}</Trans>
</ThemedText.Link>
<ShowMoreIcon $isExpanded={isExpanded} secondaryWidth="20" secondaryHeight="20" />
</ShowMoreButton>
</CodeBlockWrapper>
{isExpanded && (
<>
<Code>{errorDetails}</Code>
<Separator />
</>
)}
{showMoreButton}
</CodeBlockWrapper>
</>
) : (
<>
<Column gap="sm">
<Title textAlign="center">
<Trans>Something went wrong</Trans>
</Title>
<Description textAlign="center" color="textSecondary">
<Trans>
Sorry, an error occured while processing your request. If you request support, be sure to copy the
details of this error.
</Trans>
</Description>
</Column>
<CodeBlockWrapper>
<CodeTitle>
<ThemedText.SubHeader fontWeight={500}>Error details</ThemedText.SubHeader>
<CopyToClipboard toCopy={errorDetails}>
<CopyIcon />
</CopyToClipboard>
</CodeTitle>
<Separator />
<Code>{errorDetails.split('\n').slice(0, isExpanded ? undefined : 4)}</Code>
<Separator />
{showMoreButton}
</CodeBlockWrapper>
</>
)}
<StretchedRow>
<SmallButtonPrimary onClick={() => window.location.reload()}>
<Trans>Reload the app</Trans>

View File

@@ -1,4 +1,5 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
@@ -163,7 +164,7 @@ function FeatureFlagGroup({ name, children }: PropsWithChildren<{ name: string }
)
}
function FeatureFlagOption({ variant, featureFlag, value, label }: FeatureFlagProps) {
function FeatureFlagOption({ variant, featureFlag, label }: FeatureFlagProps) {
const updateFlag = useUpdateFlag()
const [count, setCount] = useState(0)
const featureFlags = useAtomValue(featureFlagSettings)
@@ -208,6 +209,12 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.permit2}
label="Permit 2 / Universal Router"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useFiatOnrampFlag()}
featureFlag={FeatureFlag.fiatOnramp}
label="Fiat on-ramp"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}

View File

@@ -0,0 +1,149 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { useWeb3React } from '@web3-react/core'
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { useCallback, useEffect } from 'react'
import { X } from 'react-feather'
import { useDispatch } from 'react-redux'
import { useToggleWalletDropdown } from 'state/application/hooks'
import { useAppSelector } from 'state/hooks'
import { useFiatOnrampAck } from 'state/user/hooks'
import { dismissFiatOnramp } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { isMobile } from 'utils/userAgent'
const Arrow = styled.div`
top: -4px;
height: 16px;
position: absolute;
right: 16px;
width: 16px;
::before {
background: hsl(315.75, 93%, 83%);
border-top: none;
border-left: none;
box-sizing: border-box;
content: '';
height: 16px;
position: absolute;
transform: rotate(45deg);
width: 16px;
}
`
const ArrowWrapper = styled.div`
position: absolute;
right: 16px;
top: 90%;
width: 100%;
max-width: 320px;
min-height: 92px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
right: 36px;
}
`
const CloseIcon = styled(X)`
color: white;
cursor: pointer;
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
`
const Wrapper = styled.button`
background: radial-gradient(105% 250% at 100% 5%, hsla(318, 95%, 85%) 1%, hsla(331, 80%, 75%, 0.1) 84%),
linear-gradient(180deg, hsla(296, 92%, 67%, 0.5) 0%, hsla(313, 96%, 60%, 0.5) 130%);
background-color: hsla(297, 93%, 68%, 1);
border-radius: 12px;
border: none;
cursor: pointer;
outline: none;
overflow: hidden;
position: relative;
text-align: start;
max-width: 320px;
min-height: 92px;
width: 100%;
:before {
background-image: url(${fiatMaskUrl});
background-repeat: no-repeat;
content: '';
height: 100%;
position: absolute;
right: -154px; // roughly width of fiat mask image
top: 0;
width: 100%;
}
`
const Header = styled(ThemedText.SubHeader)`
color: white;
margin: 0;
padding: 12px 12px 4px;
position: relative;
`
const Body = styled(ThemedText.BodySmall)`
color: white;
margin: 0 12px 12px 12px !important;
position: relative;
`
const MAX_RENDER_COUNT = 3
export function FiatOnrampAnnouncement() {
const { account } = useWeb3React()
const [acks, acknowledge] = useFiatOnrampAck()
const fiatOnrampDismissed = useAppSelector((state) => state.user.fiatOnrampDismissed)
useEffect(() => {
acknowledge({ renderCount: acks?.renderCount + 1 })
// The dependency list is empty so this is only run once on mount
}, []) // eslint-disable-line react-hooks/exhaustive-deps
const dispatch = useDispatch()
const handleClose = useCallback(() => {
dispatch(dismissFiatOnramp())
}, [dispatch])
const toggleWalletDropdown = useToggleWalletDropdown()
const handleClick = useCallback(() => {
sendAnalyticsEvent('FOR Banner Click')
toggleWalletDropdown()
acknowledge({ user: true })
}, [acknowledge, toggleWalletDropdown])
const fiatOnrampFlag = useFiatOnrampFlag()
const openModal = useAppSelector((state) => state.application.openModal)
if (
!account ||
acks?.user ||
fiatOnrampFlag === BaseVariant.Control ||
fiatOnrampDismissed ||
acks?.renderCount >= MAX_RENDER_COUNT ||
isMobile ||
openModal !== null
) {
return null
}
return (
<ArrowWrapper>
<Arrow />
<CloseIcon onClick={handleClose} />
<Wrapper onClick={handleClick}>
<Header>
<Trans>Buy crypto</Trans>
</Header>
<Body>
<Trans>Get tokens at the best prices in web3 on Uniswap, powered by Moonpay.</Trans>
</Body>
</Wrapper>
</ArrowWrapper>
)
}

View File

@@ -0,0 +1,143 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { useCallback, useEffect, useState } from 'react'
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro'
import { CustomLightSpinner, ThemedText } from 'theme'
import Circle from '../../assets/images/blue-loader.svg'
import Modal from '../Modal'
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.white};
border-radius: 20px;
box-shadow: ${({ theme }) => theme.deepShadow};
display: flex;
flex-flow: column nowrap;
margin: 0;
min-height: 720px;
min-width: 375px;
position: relative;
width: 100%;
`
const ErrorText = styled(ThemedText.BodyPrimary)`
color: ${({ theme }) => theme.accentFailure};
margin: auto !important;
text-align: center;
width: 90%;
`
const StyledIframe = styled.iframe`
background-color: ${({ theme }) => theme.white};
border-radius: 12px;
bottom: 0;
left: 0;
height: calc(100% - 16px);
margin: 8px;
padding: 0;
position: absolute;
right: 0;
top: 0;
width: calc(100% - 16px);
`
const StyledSpinner = styled(CustomLightSpinner)`
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
`
const MOONPAY_SUPPORTED_CURRENCY_CODES = [
'eth',
'eth_arbitrum',
'eth_optimism',
'eth_polygon',
'weth',
'wbtc',
'matic_polygon',
'polygon',
'usdc_arbitrum',
'usdc_optimism',
'usdc_polygon',
]
export default function FiatOnrampModal() {
const { account } = useWeb3React()
const theme = useTheme()
const closeModal = useCloseModal()
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
const [signedIframeUrl, setSignedIframeUrl] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const fetchSignedIframeUrl = useCallback(async () => {
if (!account) {
setError('Please connect an account before making a purchase.')
return
}
setLoading(true)
setError(null)
try {
const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string
const res = await fetch(signedIframeUrlFetchEndpoint, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
colorCode: theme.accentAction,
defaultCurrencyCode: 'eth',
redirectUrl: 'https://app.uniswap.org/#/swap',
walletAddresses: JSON.stringify(
MOONPAY_SUPPORTED_CURRENCY_CODES.reduce(
(acc, currencyCode) => ({
...acc,
[currencyCode]: account,
}),
{}
)
),
}),
})
const { url } = await res.json()
setSignedIframeUrl(url)
} catch (e) {
console.log('there was an error fetching the link', e)
setError(e.toString())
} finally {
setLoading(false)
}
}, [account, theme.accentAction])
useEffect(() => {
fetchSignedIframeUrl()
}, [fetchSignedIframeUrl])
return (
<Modal isOpen={fiatOnrampModalOpen} onDismiss={closeModal} maxHeight={720}>
<Wrapper data-testid="fiat-onramp-modal">
{error ? (
<>
<ThemedText.MediumHeader>
<Trans>Moonpay Fiat On-ramp iframe</Trans>
</ThemedText.MediumHeader>
<ErrorText>
<Trans>something went wrong!</Trans>
<br />
{error}
</ErrorText>
</>
) : loading ? (
<StyledSpinner src={Circle} alt="loading spinner" size="90px" />
) : (
<StyledIframe src={signedIframeUrl ?? ''} frameBorder="0" title="fiat-onramp-iframe" />
)}
</Wrapper>
</Modal>
)
}

View File

@@ -9,7 +9,7 @@ import { isMobile } from '../../utils/userAgent'
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: boolean }>`
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
&[data-reach-dialog-overlay] {
z-index: ${Z_INDEX.modalBackdrop};
background-color: transparent;
@@ -17,7 +17,7 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ scrollOverlay?: bool
display: flex;
align-items: center;
overflow-y: ${({ scrollOverlay }) => scrollOverlay && 'scroll'};
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
justify-content: center;
background-color: ${({ theme }) => theme.backgroundScrim};
@@ -89,7 +89,7 @@ interface ModalProps {
maxWidth?: number
initialFocusRef?: React.RefObject<any>
children?: React.ReactNode
scrollOverlay?: boolean
$scrollOverlay?: boolean
hideBorder?: boolean
isBottomSheet?: boolean
}
@@ -103,7 +103,7 @@ export default function Modal({
initialFocusRef,
children,
onSwipe = onDismiss,
scrollOverlay,
$scrollOverlay,
isBottomSheet = isMobile,
hideBorder = false,
}: ModalProps) {
@@ -136,7 +136,7 @@ export default function Modal({
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
unstable_lockFocusAcrossFrames={false}
scrollOverlay={scrollOverlay}
$scrollOverlay={$scrollOverlay}
>
<StyledDialogContent
{...(isMobile
@@ -149,7 +149,7 @@ export default function Modal({
$minHeight={minHeight}
$maxHeight={maxHeight}
$isBottomSheet={isBottomSheet}
$scrollOverlay={scrollOverlay}
$scrollOverlay={$scrollOverlay}
$hideBorder={hideBorder}
$maxWidth={maxWidth}
>

View File

@@ -16,6 +16,7 @@ import { body, bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { ReactNode, useReducer, useRef } from 'react'
import { NavLink, NavLinkProps } from 'react-router-dom'
import styled from 'styled-components/macro'
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
import { useToggleModal } from '../../state/application/hooks'
@@ -50,8 +51,13 @@ const PrimaryMenuRow = ({
)
}
const StyledBox = styled(Box)`
align-items: center;
display: flex;
justify-content: center;
`
const PrimaryMenuRowText = ({ children }: { children: ReactNode }) => {
return <Box className={`${styles.PrimaryText} ${body}`}>{children}</Box>
return <StyledBox className={`${styles.PrimaryText} ${body}`}>{children}</StyledBox>
}
PrimaryMenuRow.Text = PrimaryMenuRowText
@@ -115,7 +121,6 @@ export const MenuDropdown = () => {
const [isOpen, toggleOpen] = useReducer((s) => !s, false)
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)

View File

@@ -10,7 +10,6 @@ const baseNavDropdown = style([
borderWidth: '1px',
paddingBottom: '8',
paddingTop: '8',
zIndex: '2',
}),
{
boxShadow: '0px 4px 12px 0px #00000026',

View File

@@ -1,12 +1,20 @@
import { Box, BoxProps } from 'nft/components/Box'
import { useIsMobile } from 'nft/hooks'
import { ForwardedRef, forwardRef } from 'react'
import { Z_INDEX } from 'theme/zIndex'
import * as styles from './NavDropdown.css'
export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef<HTMLElement>) => {
const isMobile = useIsMobile()
return <Box ref={ref} className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown} {...props} />
return (
<Box
ref={ref}
style={{ zIndex: Z_INDEX.modal }}
className={isMobile ? styles.mobileNavDropdown : styles.NavDropdown}
{...props}
/>
)
})
NavDropdown.displayName = 'NavDropdown'

View File

@@ -90,9 +90,13 @@ const Navbar = () => {
<UniIcon
width="48"
height="48"
data-testid="uniswap-logo"
className={styles.logo}
onClick={() => {
navigate('/')
navigate({
pathname: '/',
search: '?intro=true',
})
}}
/>
</Box>

View File

@@ -76,7 +76,7 @@ export function AddRemoveTabs({
// detect if back should redirect to v3 or v2 pool page
const poolLink = location.pathname.includes('add/v2')
? '/pool/v2'
: '/pool' + (!!positionID ? `/${positionID.toString()}` : '')
: '/pool' + (positionID ? `/${positionID.toString()}` : '')
return (
<Tabs>

View File

@@ -4,6 +4,7 @@ import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import useGasPrice from 'hooks/useGasPrice'
import { useIsLandingPage } from 'hooks/useIsLandingPage'
import { useIsNftPage } from 'hooks/useIsNftPage'
import useMachineTimeMs from 'hooks/useMachineTime'
import JSBI from 'jsbi'
@@ -120,6 +121,7 @@ export default function Polling() {
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
const blockTime = useCurrentBlockTimestamp()
const isNftPage = useIsNftPage()
const isLandingPage = useIsLandingPage()
const ethGasPrice = useGasPrice()
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
@@ -154,7 +156,7 @@ export default function Polling() {
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
}, [blockNumber, chainId])
if (isNftPage) {
if (isNftPage || isLandingPage) {
return null
}

View File

@@ -1,74 +0,0 @@
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from '../../theme'
import { AutoColumn } from '../Column'
const Wrapper = styled(AutoColumn)`
margin-right: 8px;
height: 100%;
`
const Grouping = styled(AutoColumn)`
width: fit-content;
padding: 4px;
/* background-color: ${({ theme }) => theme.backgroundInteractive}; */
border-radius: 16px;
`
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
width: 48px;
height: 48px;
background-color: ${({ theme, confirmed, disabled }) =>
disabled ? theme.deprecated_bg3 : confirmed ? theme.accentSuccess : theme.accentAction};
border-radius: 50%;
color: ${({ theme, disabled }) => (disabled ? theme.textTertiary : theme.textPrimary)};
display: flex;
align-items: center;
justify-content: center;
line-height: 8px;
font-size: 16px;
padding: 1rem;
`
const CircleRow = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`
interface ProgressCirclesProps {
steps: boolean[]
disabled?: boolean
}
/**
* Based on array of steps, create a step counter of circles.
* A circle can be enabled, disabled, or confirmed. States are derived
* from previous step.
*
* An extra circle is added to represent the ability to swap, add, or remove.
* This step will never be marked as complete (because no 'txn done' state in body ui).
*
* @param steps array of booleans where true means step is complete
*/
export default function ProgressCircles({ steps, disabled = false, ...rest }: ProgressCirclesProps) {
const theme = useTheme()
return (
<Wrapper justify="center" {...rest}>
<Grouping>
{steps.map((step, i) => {
return (
<CircleRow key={i}>
<Circle confirmed={step} disabled={disabled || (!steps[i - 1] && i !== 0)}>
{step ? '✓' : i + 1 + '.'}
</Circle>
<ThemedText.DeprecatedMain color={theme.deprecated_text4}>|</ThemedText.DeprecatedMain>
</CircleRow>
)
})}
<Circle disabled={disabled || !steps[steps.length - 1]}>{steps.length + 1 + '.'}</Circle>
</Grouping>
</Wrapper>
)
}

View File

@@ -51,7 +51,7 @@ const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInit
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
ease-in;
background: ${({ theme, bgColor, isActive }) =>
isActive ? bgColor ?? theme.accentAction : !!bgColor ? theme.deprecated_bg4 : theme.textTertiary};
isActive ? bgColor ?? theme.accentAction : bgColor ? theme.deprecated_bg4 : theme.textTertiary};
border-radius: 50%;
height: 24px;
:hover {

View File

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

View File

@@ -222,7 +222,7 @@ export default function TokenDetailsSkeleton() {
const { chainName } = useParams<{ chainName?: string }>()
return (
<LeftPanel>
<BreadcrumbNavLink to={{ chainName } ? `/tokens/${chainName}` : `/explore`}>
<BreadcrumbNavLink to={chainName ? `/tokens/${chainName}` : `/explore`}>
<ArrowLeft size={14} /> Tokens
</BreadcrumbNavLink>
<TokenInfoContainer>

View File

@@ -51,24 +51,13 @@ export const StatsWrapper = styled.div`
type NumericStat = number | undefined | null
function Stat({
value,
title,
description,
isPrice = false,
}: {
value: NumericStat
title: ReactNode
description?: ReactNode
isPrice?: boolean
}) {
function Stat({ value, title, description }: { value: NumericStat; title: ReactNode; description?: ReactNode }) {
return (
<StatWrapper>
<StatTitle>
{title}
{description && <InfoTip text={description}></InfoTip>}
</StatTitle>
<StatPrice>{formatNumber(value, NumberType.FiatTokenStats)}</StatPrice>
</StatWrapper>
)
@@ -106,8 +95,8 @@ export default function StatsSection(props: StatsSectionProps) {
/>
</StatPair>
<StatPair>
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} isPrice={true} />
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} isPrice={true} />
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} />
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} />
</StatPair>
</TokenStatsSection>
</StatsWrapper>

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,4 @@ export const LARGE_MEDIA_BREAKPOINT = '840px'
export const MEDIUM_MEDIA_BREAKPOINT = '720px'
export const SMALL_MEDIA_BREAKPOINT = '540px'
export const MOBILE_MEDIA_BREAKPOINT = '420px'
export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'
// export const SMALL_MOBILE_MEDIA_BREAKPOINT = '390px'

View File

@@ -1,10 +1,11 @@
import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import NftExploreBanner from 'nft/components/nftExploreBanner/NftExploreBanner'
import { lazy } from 'react'
import { useLocation } from 'react-router-dom'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
@@ -17,21 +18,18 @@ export default function TopLevelModals() {
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useWeb3React()
const location = useLocation()
const pageShowsNftPromoBanner =
location.pathname.startsWith('/swap') ||
location.pathname.startsWith('/tokens') ||
location.pathname.startsWith('/pool')
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
const accountBlocked = Boolean(blockedAccountModalOpen && account)
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
return (
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
<ConnectedAccountBlocked account={account} isOpen={accountBlocked} />
<Bag />
<TransactionCompleteModal />
<AirdropModal />
{pageShowsNftPromoBanner && <NftExploreBanner />}
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
</>
)
}

View File

@@ -348,7 +348,7 @@ export default function TransactionConfirmationModal({
// confirmation screen
return (
<Modal isOpen={isOpen} scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
<Modal isOpen={isOpen} $scrollOverlay={true} onDismiss={onDismiss} maxHeight={90}>
{isL2ChainId(chainId) && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? (

View File

@@ -1,33 +1,70 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import ms from 'ms.macro'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
import { useCallback, useMemo } from 'react'
import { Copy, ExternalLink, Power } from 'react-feather'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { Text } from 'rebass'
import { useCurrencyBalanceString } from 'state/connection/hooks'
import { useAppDispatch } from 'state/hooks'
import { useFiatOnrampAck } from 'state/user/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled, { css } from 'styled-components/macro'
import { ThemedText } from 'theme'
import styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from '../../nft/utils/address'
import { useCloseModal, useToggleModal } from '../../state/application/hooks'
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import { ButtonEmphasis, ButtonSize, ThemeButton } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'
import IconButton, { IconHoverText } from './IconButton'
const BuyCryptoButtonBorderKeyframes = keyframes`
0% {
border-color: transparent;
}
33% {
border-color: hsla(225, 95%, 63%, 1);
}
66% {
border-color: hsla(267, 95%, 63%, 1);
}
100% {
border-color: transparent;
}
`
const BuyCryptoButton = styled(ThemeButton)<{ $animateBorder: boolean }>`
border-color: transparent;
border-radius: 12px;
border-style: solid;
border-width: 1px;
height: 40px;
margin-top: 12px;
animation-direction: alternate;
animation-duration: ${({ theme }) => theme.transition.duration.slow};
animation-fill-mode: none;
animation-iteration-count: 2;
animation-name: ${BuyCryptoButtonBorderKeyframes};
animation-play-state: ${({ $animateBorder }) => ($animateBorder ? 'running' : 'paused')};
animation-timing-function: ${({ theme }) => theme.transition.timing.inOut};
`
const WalletButton = styled(ThemeButton)`
border-radius: 12px;
padding-top: 10px;
@@ -75,7 +112,20 @@ const USDText = styled.div`
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const FiatOnrampNotAvailableText = styled(ThemedText.Caption)`
align-items: center;
color: ${({ theme }) => theme.textSecondary};
display: flex;
justify-content: center;
`
const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
align-items: center;
display: flex;
height: 14px;
justify-content: center;
margin-left: 6px;
width: 14px;
`
const FlexContainer = styled.div`
display: flex;
`
@@ -108,7 +158,14 @@ const AccountContainer = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
margin-top: 2.5px;
`
const StyledInfoIcon = styled(Info)`
height: 12px;
width: 12px;
flex: 1 1 auto;
`
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
fill: ${({ theme }) => theme.accentAction};
`
const BalanceWrapper = styled.div`
padding: 16px 0;
`
@@ -136,8 +193,7 @@ const AuthenticatedHeader = () => {
explorer,
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const navigate = useNavigate()
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
const closeModal = useCloseModal()
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
const resetSellAssets = useSellAsset((state) => state.reset)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
@@ -147,7 +203,7 @@ const AuthenticatedHeader = () => {
const isUnclaimed = useUserHasAvailableClaim(account)
const connectionType = getConnection(connector).type
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined) || 0
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
const disconnect = useCallback(() => {
@@ -159,18 +215,68 @@ const AuthenticatedHeader = () => {
}, [connector, dispatch])
const amountUSD = useMemo(() => {
if (!nativeCurrencyPrice || !balanceString) return undefined
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
const balance = parseFloat(balanceString || '0')
const balance = parseFloat(balanceString)
return price * balance
}, [balanceString, nativeCurrencyPrice])
const navigateToProfile = () => {
const navigateToProfile = useCallback(() => {
resetSellAssets()
setSellPageState(ProfilePageStateType.VIEWING)
clearCollectionFilters()
navigate('/nfts/profile')
closeModal()
}
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
const fiatOnrampFlag = useFiatOnrampFlag()
// animate the border of the buy crypto button when a user navigates here from the feature announcement
// can be removed when components/FiatOnrampAnnouncment.tsx is no longer used
const [acknowledgements, acknowledge] = useFiatOnrampAck()
const animateBuyCryptoButtonBorder = acknowledgements?.user && !acknowledgements.system
useEffect(() => {
let stale = false
let timeoutId = 0
if (animateBuyCryptoButtonBorder) {
timeoutId = setTimeout(() => {
if (stale) return
acknowledge({ system: true })
}, ms`2 seconds`) as unknown as number
// as unknown as number is necessary so it's not incorrectly typed as a NodeJS.Timeout
}
return () => {
stale = true
clearTimeout(timeoutId)
}
}, [acknowledge, animateBuyCryptoButtonBorder])
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
const openFoRModalWithAnalytics = useCallback(() => {
sendAnalyticsEvent('Fiat OnRamp Widget Opened')
openFiatOnrampModal()
}, [openFiatOnrampModal])
const [shouldCheck, setShouldCheck] = useState(false)
const {
available: fiatOnrampAvailable,
availabilityChecked: fiatOnrampAvailabilityChecked,
error,
loading: fiatOnrampAvailabilityLoading,
} = useFiatOnrampAvailability(shouldCheck, openFoRModalWithAnalytics)
const handleBuyCryptoClick = useCallback(() => {
if (!fiatOnrampAvailabilityChecked) {
setShouldCheck(true)
} else if (fiatOnrampAvailable) {
openFoRModalWithAnalytics()
}
}, [fiatOnrampAvailabilityChecked, fiatOnrampAvailable, openFoRModalWithAnalytics])
const disableBuyCryptoButton = Boolean(
error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading
)
const [showFiatOnrampUnavailableTooltip, setShow] = useState<boolean>(false)
const openFiatOnrampUnavailableTooltip = useCallback(() => setShow(true), [setShow])
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
return (
<AuthenticatedHeaderWrapper>
@@ -192,7 +298,7 @@ const AuthenticatedHeader = () => {
<IconButton onClick={copy} Icon={Copy}>
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
</IconButton>
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLink}>
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
<Trans>Explore</Trans>
</IconButton>
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
@@ -205,7 +311,7 @@ const AuthenticatedHeader = () => {
<Text fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol}
</Text>
<USDText>${amountUSD.toFixed(2)} USD</USDText>
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
</BalanceWrapper>
<ProfileButton
data-testid="nft-view-self-nfts"
@@ -215,6 +321,44 @@ const AuthenticatedHeader = () => {
>
<Trans>View and sell NFTs</Trans>
</ProfileButton>
{fiatOnrampFlag === BaseVariant.Enabled && (
<>
<BuyCryptoButton
$animateBorder={animateBuyCryptoButtonBorder}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
) : (
<>
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
<Trans>Buy crypto</Trans>
</>
)}
</BuyCryptoButton>
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>
<Tooltip
show={showFiatOnrampUnavailableTooltip}
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
>
<FiatOnrampAvailabilityExternalLink
onMouseEnter={openFiatOnrampUnavailableTooltip}
onMouseLeave={closeFiatOnrampUnavailableTooltip}
style={{ color: 'inherit' }}
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
>
<StyledInfoIcon />
</FiatOnrampAvailabilityExternalLink>
</Tooltip>
</FiatOnrampNotAvailableText>
)}
</>
)}
{isUnclaimed && (
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>

View File

@@ -1,6 +1,4 @@
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import * as connectionUtils from 'connection/utils'
import { ApplicationModal } from 'state/application/reducer'
import { nativeOnChain } from '../../constants/tokens'
import { render, screen } from '../../test-utils'
@@ -20,7 +18,7 @@ jest.mock('utils/userAgent', () => ({
jest.mock('.../../state/application/hooks', () => {
return {
useModalIsOpen: (_modal: ApplicationModal) => true,
useModalIsOpen: () => true,
useToggleWalletModal: () => {
return
},
@@ -29,7 +27,7 @@ jest.mock('.../../state/application/hooks', () => {
jest.mock('hooks/useStablecoinPrice', () => {
return {
useStablecoinValue: (_currencyAmount: CurrencyAmount<Currency> | undefined | null) => {
useStablecoinValue: () => {
return
},
}
@@ -38,10 +36,10 @@ jest.mock('hooks/useStablecoinPrice', () => {
jest.mock('lib/hooks/useCurrencyBalance', () => {
return {
__esModule: true,
default: (account?: string, currency?: Currency) => {
default: () => {
return
},
useTokenBalance: (account?: string, token?: Token) => {
useTokenBalance: () => {
return
},
}

View File

@@ -1,6 +1,7 @@
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { Connection } from 'connection'
import { ConnectionType, setMetMaskErrorHandler } from 'connection'
import { getConnectionName } from 'connection/utils'
import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
@@ -8,8 +9,19 @@ import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/tra
import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections'
import { ReactNode, useEffect, useMemo } from 'react'
import { updateConnectionError } from 'state/connection/reducer'
import { useAppDispatch } from 'state/hooks'
export default function Web3Provider({ children }: { children: ReactNode }) {
const dispatch = useAppDispatch()
// Set metamask error handler for metamask disconnection warning modal.
useEffect(() => {
setMetMaskErrorHandler((error: Error) =>
dispatch(updateConnectionError({ connectionType: ConnectionType.INJECTED, error: error.message }))
)
}, [dispatch])
useEagerlyConnect()
const connections = useOrderedConnections()
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])

View File

@@ -0,0 +1,97 @@
import { Trans } from '@lingui/macro'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import Modal from 'components/Modal'
import { RowBetween } from 'components/Row'
import { AlertTriangle } from 'react-feather'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import { useModalIsOpen, useToggleMetamaskConnectionErrorModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
const Wrapper = styled.div`
width: 100%;
position: relative;
display: flex;
flex-flow: column;
align-items: center;
`
const Container = styled.div`
width: 100%;
padding: 32px 32px;
display: flex;
flex-flow: column;
align-items: center;
`
const LogoContainer = styled.div`
display: flex;
gap: 16px;
`
const ShortColumn = styled(AutoColumn)`
margin-top: 10px;
`
const InfoText = styled(Text)`
padding: 0 12px 0 12px;
font-size: 14px;
line-height: 20px;
text-align: center;
`
const StyledButton = styled(ButtonPrimary)`
margin-top: 24px;
width: 100%;
font-weight: 600;
`
const WarningIcon = styled(AlertTriangle)`
width: 76px;
height: 76px;
margin-top: 4px;
margin-bottom: 28px;
stroke-width: 1px;
margin-right: 4px;
color: ${({ theme }) => theme.accentCritical};
`
const onReconnect = () => window.location.reload()
const header = 'Wallet disconnected'
const description = 'A Metamask error caused your wallet to disconnect. Reload the page to reconnect.'
export default function MetamaskConnectionError() {
const modalOpen = useModalIsOpen(ApplicationModal.METAMASK_CONNECTION_ERROR)
const toggleModal = useToggleMetamaskConnectionErrorModal()
return (
<Modal isOpen={modalOpen} onDismiss={toggleModal} minHeight={false} maxHeight={90}>
<Wrapper>
<RowBetween style={{ padding: '1rem' }}>
<div />
<CloseIcon onClick={toggleModal} />
</RowBetween>
<Container>
<AutoColumn>
<LogoContainer>
<WarningIcon />
</LogoContainer>
</AutoColumn>
<ShortColumn>
<InfoText>
<ThemedText.HeadlineSmall marginBottom="8px">{header}</ThemedText.HeadlineSmall>
<ThemedText.BodySmall>{description}</ThemedText.BodySmall>
</InfoText>
</ShortColumn>
<StyledButton onClick={onReconnect}>
<Trans>Reload</Trans>
</StyledButton>
</Container>
</Wrapper>
</Modal>
)
}

View File

@@ -1,15 +1,16 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, ElementName, EventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
import { IconWrapper } from 'components/Identicon/StatusIcon'
import WalletDropdown from 'components/WalletDropdown'
import { getConnection } from 'connection/utils'
import { getConnection, getIsMetaMask } from 'connection/utils'
import { Portal } from 'nft/components/common/Portal'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished'
import { useMemo, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
import { useAppSelector } from 'state/hooks'
import { useDerivedSwapInfo } from 'state/swap/hooks'
@@ -21,6 +22,7 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import {
useCloseModal,
useModalIsOpen,
useToggleMetamaskConnectionErrorModal,
useToggleWalletDropdown,
useToggleWalletModal,
} from '../../state/application/hooks'
@@ -33,6 +35,7 @@ import StatusIcon from '../Identicon/StatusIcon'
import Loader from '../Loader'
import { RowBetween } from '../Row'
import WalletModal from '../WalletModal'
import MetamaskConnectionError from './MetamaskConnectionError'
// https://stackoverflow.com/a/31617326
const FULL_BORDER_RADIUS = 9999
@@ -205,11 +208,21 @@ function Web3StatusInner() {
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
const theme = useTheme()
const toggleWalletDropdown = useToggleWalletDropdown()
const handleWalletDropdownClick = useCallback(() => {
sendAnalyticsEvent('FOR Account Dropdown Button Clicks')
toggleWalletDropdown()
}, [toggleWalletDropdown])
const toggleWalletModal = useToggleWalletModal()
const toggleMetamaskConnectionErrorModal = useToggleMetamaskConnectionErrorModal()
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
useEffect(() => {
if (getIsMetaMask() && error) {
toggleMetamaskConnectionErrorModal()
}
}, [error, toggleMetamaskConnectionErrorModal])
const allTransactions = useAllTransactions()
@@ -221,13 +234,12 @@ function Web3StatusInner() {
const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
const hasPendingTransactions = !!pending.length
const toggleWallet = toggleWalletDropdown
if (!chainId) {
return null
} else if (error) {
return (
<Web3StatusError onClick={toggleWallet}>
<Web3StatusError onClick={handleWalletDropdownClick}>
<NetworkIcon />
<Text>
<Trans>Error</Trans>
@@ -243,7 +255,7 @@ function Web3StatusInner() {
return (
<Web3StatusConnected
data-testid="web3-status-connected"
onClick={toggleWallet}
onClick={handleWalletDropdownClick}
pending={hasPendingTransactions}
isClaimAvailable={isClaimAvailable}
>
@@ -281,7 +293,7 @@ function Web3StatusInner() {
<Trans>Connect</Trans>
</StyledConnectButton>
<VerticalDivider />
<ChevronWrapper onClick={toggleWalletDropdown} data-testid="navbar-toggle-dropdown">
<ChevronWrapper onClick={handleWalletDropdownClick} data-testid="navbar-toggle-dropdown">
{walletIsOpen ? <ChevronUp {...chevronProps} /> : <ChevronDown {...chevronProps} />}
</ChevronWrapper>
</Web3StatusConnectWrapper>
@@ -296,7 +308,7 @@ export default function Web3Status() {
const allTransactions = useAllTransactions()
const ref = useRef<HTMLDivElement>(null)
const walletRef = useRef<HTMLDivElement>(null)
const closeModal = useCloseModal(ApplicationModal.WALLET_DROPDOWN)
const closeModal = useCloseModal()
const isOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
useOnClickOutside(ref, isOpen ? closeModal : undefined, [walletRef])
@@ -312,7 +324,9 @@ export default function Web3Status() {
return (
<span ref={ref}>
<Web3StatusInner />
<FiatOnrampAnnouncement />
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
<MetamaskConnectionError />
<Portal>
<span ref={walletRef}>
<WalletDropdown />

View File

@@ -1,128 +0,0 @@
import type { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { useWeb3React } from '@web3-react/core'
import { ReactNode, useState } from 'react'
import styled from 'styled-components/macro'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
import Modal from '../Modal'
import { LoadingView, SubmittedView } from '../ModalViews'
import { RowBetween } from '../Row'
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 1rem;
`
interface StakingModalProps {
isOpen: boolean
onDismiss: () => void
stakingInfo: StakingInfo
}
export default function ClaimRewardModal({ isOpen, onDismiss, stakingInfo }: StakingModalProps) {
const { account } = useWeb3React()
// monitor call to help UI loading state
const addTransaction = useTransactionAdder()
const [hash, setHash] = useState<string | undefined>()
const [attempting, setAttempting] = useState(false)
function wrappedOnDismiss() {
setHash(undefined)
setAttempting(false)
onDismiss()
}
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
async function onClaimReward() {
if (stakingContract && stakingInfo?.stakedAmount && account) {
setAttempting(true)
await stakingContract
.getReward({ gasLimit: 350000 })
.then((response: TransactionResponse) => {
addTransaction(response, {
type: TransactionType.CLAIM,
recipient: account,
})
setHash(response.hash)
})
.catch((error: any) => {
setAttempting(false)
console.log(error)
})
}
}
let error: ReactNode | undefined
if (!account) {
error = <Trans>Connect Wallet</Trans>
}
if (!stakingInfo?.stakedAmount) {
error = error ?? <Trans>Enter an amount</Trans>
}
return (
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && (
<ContentWrapper gap="lg">
<RowBetween>
<ThemedText.DeprecatedMediumHeader>
<Trans>Claim</Trans>
</ThemedText.DeprecatedMediumHeader>
<CloseIcon onClick={wrappedOnDismiss} />
</RowBetween>
{stakingInfo?.earnedAmount && (
<AutoColumn justify="center" gap="md">
<ThemedText.HeadlineLarge>{stakingInfo?.earnedAmount?.toSignificant(6)}</ThemedText.HeadlineLarge>
<ThemedText.DeprecatedBody>
<Trans>Unclaimed UNI</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
)}
<ThemedText.DeprecatedSubHeader style={{ textAlign: 'center' }}>
<Trans>When you claim without withdrawing your liquidity remains in the mining pool.</Trans>
</ThemedText.DeprecatedSubHeader>
<ButtonError disabled={!!error} error={!!error && !!stakingInfo?.stakedAmount} onClick={onClaimReward}>
{error ?? <Trans>Claim</Trans>}
</ButtonError>
</ContentWrapper>
)}
{attempting && !hash && (
<LoadingView onDismiss={wrappedOnDismiss}>
<AutoColumn gap="md" justify="center">
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Claiming {stakingInfo?.earnedAmount?.toSignificant(6)} UNI</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
</LoadingView>
)}
{hash && (
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
<AutoColumn gap="md" justify="center">
<ThemedText.DeprecatedLargeHeader>
<Trans>Transaction Submitted</Trans>
</ThemedText.DeprecatedLargeHeader>
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Claimed UNI!</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
</SubmittedView>
)}
</Modal>
)
}

View File

@@ -1,199 +0,0 @@
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
import styled from 'styled-components/macro'
import { BIG_INT_SECONDS_IN_WEEK } from '../../constants/misc'
import { useColor } from '../../hooks/useColor'
import useStablecoinPrice from '../../hooks/useStablecoinPrice'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useV2Pair } from '../../hooks/useV2Pairs'
import { StakingInfo } from '../../state/stake/hooks'
import { StyledInternalLink, ThemedText } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import { unwrappedToken } from '../../utils/unwrappedToken'
import { ButtonPrimary } from '../Button'
import { AutoColumn } from '../Column'
import DoubleCurrencyLogo from '../DoubleLogo'
import { RowBetween } from '../Row'
import { Break, CardBGImage, CardNoise } from './styled'
const StatContainer = styled.div`
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 12px;
margin-bottom: 1rem;
margin-right: 1rem;
margin-left: 1rem;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
display: none;
`};
`
const Wrapper = styled(AutoColumn)<{ showBackground: boolean; bgColor: any }>`
border-radius: 12px;
width: 100%;
overflow: hidden;
position: relative;
opacity: ${({ showBackground }) => (showBackground ? '1' : '1')};
background: ${({ theme, bgColor, showBackground }) =>
`radial-gradient(91.85% 100% at 1.84% 0%, ${bgColor} 0%, ${
showBackground ? theme.black : theme.deprecated_bg5
} 100%) `};
color: ${({ theme, showBackground }) => (showBackground ? theme.white : theme.textPrimary)} !important;
${({ showBackground }) =>
showBackground &&
` box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01);`}
`
const TopSection = styled.div`
display: grid;
grid-template-columns: 48px 1fr 120px;
grid-gap: 0px;
align-items: center;
padding: 1rem;
z-index: 1;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
grid-template-columns: 48px 1fr 96px;
`};
`
const BottomSection = styled.div<{ showBackground: boolean }>`
padding: 12px 16px;
opacity: ${({ showBackground }) => (showBackground ? '1' : '0.4')};
border-radius: 0 0 12px 12px;
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
z-index: 1;
`
export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo }) {
const token0 = stakingInfo.tokens[0]
const token1 = stakingInfo.tokens[1]
const currency0 = unwrappedToken(token0)
const currency1 = unwrappedToken(token1)
const isStaking = Boolean(stakingInfo.stakedAmount.greaterThan('0'))
// get the color of the token
const token = currency0.isNative ? token1 : token0
const WETH = currency0.isNative ? token0 : token1
const backgroundColor = useColor(token)
const totalSupplyOfStakingToken = useTotalSupply(stakingInfo.stakedAmount.currency)
const [, stakingTokenPair] = useV2Pair(...stakingInfo.tokens)
// let returnOverMonth: Percent = new Percent('0')
let valueOfTotalStakedAmountInWETH: CurrencyAmount<Token> | undefined
if (totalSupplyOfStakingToken && stakingTokenPair) {
// take the total amount of LP tokens staked, multiply by ETH value of all LP tokens, divide by all LP tokens
valueOfTotalStakedAmountInWETH = CurrencyAmount.fromRawAmount(
WETH,
JSBI.divide(
JSBI.multiply(
JSBI.multiply(stakingInfo.totalStakedAmount.quotient, stakingTokenPair.reserveOf(WETH).quotient),
JSBI.BigInt(2) // this is b/c the value of LP shares are ~double the value of the WETH they entitle owner to
),
totalSupplyOfStakingToken.quotient
)
)
}
// get the USD value of staked WETH
const USDPrice = useStablecoinPrice(WETH)
const valueOfTotalStakedAmountInUSDC =
valueOfTotalStakedAmountInWETH && USDPrice?.quote(valueOfTotalStakedAmountInWETH)
return (
<Wrapper showBackground={isStaking} bgColor={backgroundColor}>
<CardBGImage desaturate />
<CardNoise />
<TopSection>
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} size={24} />
<ThemedText.DeprecatedWhite fontWeight={600} fontSize={24} style={{ marginLeft: '8px' }}>
{currency0.symbol}-{currency1.symbol}
</ThemedText.DeprecatedWhite>
<StyledInternalLink to={`/uni/${currencyId(currency0)}/${currencyId(currency1)}`} style={{ width: '100%' }}>
<ButtonPrimary padding="8px" $borderRadius="8px">
{isStaking ? <Trans>Manage</Trans> : <Trans>Deposit</Trans>}
</ButtonPrimary>
</StyledInternalLink>
</TopSection>
<StatContainer>
<RowBetween>
<ThemedText.DeprecatedWhite>
<Trans>Total deposited</Trans>
</ThemedText.DeprecatedWhite>
<ThemedText.DeprecatedWhite>
{valueOfTotalStakedAmountInUSDC ? (
<Trans>${valueOfTotalStakedAmountInUSDC.toFixed(0, { groupSeparator: ',' })}</Trans>
) : (
<Trans>{valueOfTotalStakedAmountInWETH?.toSignificant(4, { groupSeparator: ',' }) ?? '-'} ETH</Trans>
)}
</ThemedText.DeprecatedWhite>
</RowBetween>
<RowBetween>
<ThemedText.DeprecatedWhite>
<Trans>Pool rate</Trans>
</ThemedText.DeprecatedWhite>
<ThemedText.DeprecatedWhite>
{stakingInfo ? (
stakingInfo.active ? (
<Trans>
{stakingInfo.totalRewardRate?.multiply(BIG_INT_SECONDS_IN_WEEK)?.toFixed(0, { groupSeparator: ',' })}{' '}
UNI / week
</Trans>
) : (
<Trans>0 UNI / week</Trans>
)
) : (
'-'
)}
</ThemedText.DeprecatedWhite>
</RowBetween>
</StatContainer>
{isStaking && (
<>
<Break />
<BottomSection showBackground={true}>
<ThemedText.DeprecatedBlack color="white" fontWeight={500}>
<span>
<Trans>Your rate</Trans>
</span>
</ThemedText.DeprecatedBlack>
<ThemedText.DeprecatedBlack style={{ textAlign: 'right' }} color="white" fontWeight={500}>
<span role="img" aria-label="wizard-icon" style={{ marginRight: '0.5rem' }}>
</span>
{stakingInfo ? (
stakingInfo.active ? (
<Trans>
{stakingInfo.rewardRate
?.multiply(BIG_INT_SECONDS_IN_WEEK)
?.toSignificant(4, { groupSeparator: ',' })}{' '}
UNI / week
</Trans>
) : (
<Trans>0 UNI / week</Trans>
)
) : (
'-'
)}
</ThemedText.DeprecatedBlack>
</BottomSection>
</>
)}
</Wrapper>
)
}

View File

@@ -1,249 +0,0 @@
import type { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { useV2LiquidityTokenPermit } from 'hooks/useV2LiquidityTokenPermit'
import { useCallback, useState } from 'react'
import styled from 'styled-components/macro'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useContract, usePairContract, useV2RouterContract } from '../../hooks/useContract'
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { formatCurrencyAmount } from '../../utils/formatCurrencyAmount'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { ButtonConfirmed, ButtonError } from '../Button'
import { AutoColumn } from '../Column'
import CurrencyInputPanel from '../CurrencyInputPanel'
import Modal from '../Modal'
import { LoadingView, SubmittedView } from '../ModalViews'
import ProgressCircles from '../ProgressSteps'
import { RowBetween } from '../Row'
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
const HypotheticalRewardRate = styled.div<{ dim: boolean }>`
display: flex;
justify-content: space-between;
padding-right: 20px;
padding-left: 20px;
opacity: ${({ dim }) => (dim ? 0.5 : 1)};
`
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 1rem;
`
interface StakingModalProps {
isOpen: boolean
onDismiss: () => void
stakingInfo: StakingInfo
userLiquidityUnstaked: CurrencyAmount<Token> | undefined
}
export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiquidityUnstaked }: StakingModalProps) {
const { provider } = useWeb3React()
// track and parse user input
const [typedValue, setTypedValue] = useState('')
const { parsedAmount, error } = useDerivedStakeInfo(
typedValue,
stakingInfo.stakedAmount.currency,
userLiquidityUnstaked
)
const parsedAmountWrapped = parsedAmount?.wrapped
let hypotheticalRewardRate: CurrencyAmount<Token> = CurrencyAmount.fromRawAmount(stakingInfo.rewardRate.currency, '0')
if (parsedAmountWrapped?.greaterThan('0')) {
hypotheticalRewardRate = stakingInfo.getHypotheticalRewardRate(
stakingInfo.stakedAmount.add(parsedAmountWrapped),
stakingInfo.totalStakedAmount.add(parsedAmountWrapped),
stakingInfo.totalRewardRate
)
}
// state for pending and submitted txn views
const addTransaction = useTransactionAdder()
const [attempting, setAttempting] = useState<boolean>(false)
const [hash, setHash] = useState<string | undefined>()
const wrappedOnDismiss = useCallback(() => {
setHash(undefined)
setAttempting(false)
onDismiss()
}, [onDismiss])
// pair contract for this token to be staked
const dummyPair = new Pair(
CurrencyAmount.fromRawAmount(stakingInfo.tokens[0], '0'),
CurrencyAmount.fromRawAmount(stakingInfo.tokens[1], '0')
)
const pairContract = usePairContract(dummyPair.liquidityToken.address)
// approval data for stake
const deadline = useTransactionDeadline()
const router = useV2RouterContract()
const { signatureData, gatherPermitSignature } = useV2LiquidityTokenPermit(parsedAmountWrapped, router?.address)
const [approval, approveCallback] = useApproveCallback(parsedAmount, stakingInfo.stakingRewardAddress)
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
async function onStake() {
setAttempting(true)
if (stakingContract && parsedAmount && deadline) {
if (approval === ApprovalState.APPROVED) {
await stakingContract.stake(`0x${parsedAmount.quotient.toString(16)}`, { gasLimit: 350000 })
} else if (signatureData) {
stakingContract
.stakeWithPermit(
`0x${parsedAmount.quotient.toString(16)}`,
signatureData.deadline,
signatureData.v,
signatureData.r,
signatureData.s,
{ gasLimit: 350000 }
)
.then((response: TransactionResponse) => {
addTransaction(response, {
type: TransactionType.DEPOSIT_LIQUIDITY_STAKING,
token0Address: stakingInfo.tokens[0].address,
token1Address: stakingInfo.tokens[1].address,
})
setHash(response.hash)
})
.catch((error: any) => {
setAttempting(false)
console.log(error)
})
} else {
setAttempting(false)
throw new Error('Attempting to stake without approval or a signature. Please contact support.')
}
}
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback((typedValue: string) => {
setTypedValue(typedValue)
}, [])
// used for max input button
const maxAmountInput = maxAmountSpend(userLiquidityUnstaked)
const atMaxAmount = Boolean(maxAmountInput && parsedAmount?.equalTo(maxAmountInput))
const handleMax = useCallback(() => {
maxAmountInput && onUserInput(maxAmountInput.toExact())
}, [maxAmountInput, onUserInput])
async function onAttemptToApprove() {
if (!pairContract || !provider || !deadline) throw new Error('missing dependencies')
if (!parsedAmount) throw new Error('missing liquidity amount')
if (gatherPermitSignature) {
try {
await gatherPermitSignature()
} catch (error) {
// try to approve if gatherPermitSignature failed for any reason other than the user rejecting it
if (error?.code !== 4001) {
await approveCallback()
}
}
} else {
await approveCallback()
}
}
return (
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && (
<ContentWrapper gap="lg">
<RowBetween>
<ThemedText.DeprecatedMediumHeader>
<Trans>Deposit</Trans>
</ThemedText.DeprecatedMediumHeader>
<CloseIcon onClick={wrappedOnDismiss} />
</RowBetween>
<CurrencyInputPanel
value={typedValue}
onUserInput={onUserInput}
onMax={handleMax}
showMaxButton={!atMaxAmount}
currency={stakingInfo.stakedAmount.currency}
pair={dummyPair}
label=""
renderBalance={(amount) => <Trans>Available to deposit: {formatCurrencyAmount(amount, 4)}</Trans>}
id="stake-liquidity-token"
/>
<HypotheticalRewardRate dim={!hypotheticalRewardRate.greaterThan('0')}>
<div>
<ThemedText.DeprecatedBlack fontWeight={600}>
<Trans>Weekly Rewards</Trans>
</ThemedText.DeprecatedBlack>
</div>
<ThemedText.DeprecatedBlack>
<Trans>
{hypotheticalRewardRate
.multiply((60 * 60 * 24 * 7).toString())
.toSignificant(4, { groupSeparator: ',' })}{' '}
UNI / week
</Trans>
</ThemedText.DeprecatedBlack>
</HypotheticalRewardRate>
<RowBetween>
<ButtonConfirmed
mr="0.5rem"
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
>
<Trans>Approve</Trans>
</ButtonConfirmed>
<ButtonError
disabled={!!error || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!!error && !!parsedAmount}
onClick={onStake}
>
{error ?? <Trans>Deposit</Trans>}
</ButtonError>
</RowBetween>
<ProgressCircles steps={[approval === ApprovalState.APPROVED || signatureData !== null]} disabled={true} />
</ContentWrapper>
)}
{attempting && !hash && (
<LoadingView onDismiss={wrappedOnDismiss}>
<AutoColumn gap="md" justify="center">
<ThemedText.DeprecatedLargeHeader>
<Trans>Depositing Liquidity</Trans>
</ThemedText.DeprecatedLargeHeader>
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>{parsedAmount?.toSignificant(4)} UNI-V2</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
</LoadingView>
)}
{attempting && hash && (
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
<AutoColumn gap="md" justify="center">
<ThemedText.DeprecatedLargeHeader>
<Trans>Transaction Submitted</Trans>
</ThemedText.DeprecatedLargeHeader>
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Deposited {parsedAmount?.toSignificant(4)} UNI-V2</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
</SubmittedView>
)}
</Modal>
)
}

View File

@@ -1,148 +0,0 @@
import type { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { useWeb3React } from '@web3-react/core'
import { ReactNode, useState } from 'react'
import styled from 'styled-components/macro'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
import FormattedCurrencyAmount from '../FormattedCurrencyAmount'
import Modal from '../Modal'
import { LoadingView, SubmittedView } from '../ModalViews'
import { RowBetween } from '../Row'
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 1rem;
`
interface StakingModalProps {
isOpen: boolean
onDismiss: () => void
stakingInfo: StakingInfo
}
export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: StakingModalProps) {
const { account } = useWeb3React()
// monitor call to help UI loading state
const addTransaction = useTransactionAdder()
const [hash, setHash] = useState<string | undefined>()
const [attempting, setAttempting] = useState(false)
function wrappedOnDismiss() {
setHash(undefined)
setAttempting(false)
onDismiss()
}
const stakingContract = useStakingContract(stakingInfo.stakingRewardAddress)
async function onWithdraw() {
if (stakingContract && stakingInfo?.stakedAmount) {
setAttempting(true)
await stakingContract
.exit({ gasLimit: 300000 })
.then((response: TransactionResponse) => {
addTransaction(response, {
type: TransactionType.WITHDRAW_LIQUIDITY_STAKING,
token0Address: stakingInfo.tokens[0].address,
token1Address: stakingInfo.tokens[1].address,
})
setHash(response.hash)
})
.catch((error: any) => {
setAttempting(false)
console.log(error)
})
}
}
let error: ReactNode | undefined
if (!account) {
error = <Trans>Connect a wallet</Trans>
}
if (!stakingInfo?.stakedAmount) {
error = error ?? <Trans>Enter an amount</Trans>
}
return (
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
{!attempting && !hash && (
<ContentWrapper gap="lg">
<RowBetween>
<ThemedText.DeprecatedMediumHeader>
<Trans>Withdraw</Trans>
</ThemedText.DeprecatedMediumHeader>
<CloseIcon onClick={wrappedOnDismiss} />
</RowBetween>
{stakingInfo?.stakedAmount && (
<AutoColumn justify="center" gap="md">
<ThemedText.HeadlineLarge>
<FormattedCurrencyAmount currencyAmount={stakingInfo.stakedAmount} />
</ThemedText.HeadlineLarge>
<ThemedText.DeprecatedBody>
<Trans>Deposited liquidity:</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
)}
{stakingInfo?.earnedAmount && (
<AutoColumn justify="center" gap="md">
<ThemedText.HeadlineLarge>
<FormattedCurrencyAmount currencyAmount={stakingInfo?.earnedAmount} />
</ThemedText.HeadlineLarge>
<ThemedText.DeprecatedBody>
<Trans>Unclaimed UNI</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
)}
<ThemedText.DeprecatedSubHeader style={{ textAlign: 'center' }}>
<Trans>When you withdraw, your UNI is claimed and your liquidity is removed from the mining pool.</Trans>
</ThemedText.DeprecatedSubHeader>
<ButtonError disabled={!!error} error={!!error && !!stakingInfo?.stakedAmount} onClick={onWithdraw}>
{error ?? <Trans>Withdraw & Claim</Trans>}
</ButtonError>
</ContentWrapper>
)}
{attempting && !hash && (
<LoadingView onDismiss={wrappedOnDismiss}>
<AutoColumn gap="md" justify="center">
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Withdrawing {stakingInfo?.stakedAmount?.toSignificant(4)} UNI-V2</Trans>
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Claiming {stakingInfo?.earnedAmount?.toSignificant(4)} UNI</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
</LoadingView>
)}
{hash && (
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
<AutoColumn gap="md" justify="center">
<ThemedText.DeprecatedLargeHeader>
<Trans>Transaction Submitted</Trans>
</ThemedText.DeprecatedLargeHeader>
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Withdrew UNI-V2!</Trans>
</ThemedText.DeprecatedBody>
<ThemedText.DeprecatedBody fontSize={20}>
<Trans>Claimed UNI!</Trans>
</ThemedText.DeprecatedBody>
</AutoColumn>
</SubmittedView>
)}
</Modal>
)
}

View File

@@ -110,19 +110,10 @@ interface SwapDetailsInlineProps {
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
syncing: boolean
loading: boolean
showInverted: boolean
setShowInverted: React.Dispatch<React.SetStateAction<boolean>>
allowedSlippage: Percent
}
export default function SwapDetailsDropdown({
trade,
syncing,
loading,
showInverted,
setShowInverted,
allowedSlippage,
}: SwapDetailsInlineProps) {
export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSlippage }: SwapDetailsInlineProps) {
const theme = useTheme()
const { chainId } = useWeb3React()
const [showDetails, setShowDetails] = useState(false)
@@ -169,11 +160,7 @@ export default function SwapDetailsDropdown({
)}
{trade ? (
<LoadingOpacityContainer $loading={syncing}>
<TradePrice
price={trade.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
<TradePrice price={trade.executionPrice} />
</LoadingOpacityContainer>
) : loading || syncing ? (
<ThemedText.DeprecatedMain fontSize={14}>

View File

@@ -75,7 +75,6 @@ export default function SwapModalHeader({
}) {
const theme = useTheme()
const [showInverted, setShowInverted] = useState<boolean>(false)
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade.executionPrice)
const [priceUpdate, setPriceUpdate] = useState<number | undefined>()
@@ -153,7 +152,7 @@ export default function SwapModalHeader({
</AutoColumn>
</LightCard>
<RowBetween style={{ marginTop: '0.25rem', padding: '0 1rem' }}>
<TradePrice price={trade.executionPrice} showInverted={showInverted} setShowInverted={setShowInverted} />
<TradePrice price={trade.executionPrice} />
</RowBetween>
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />

View File

@@ -1,16 +1,13 @@
import { Trans } from '@lingui/macro'
import { Currency, Price } from '@uniswap/sdk-core'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import { useCallback } from 'react'
import { Text } from 'rebass'
import styled, { useTheme } from 'styled-components/macro'
import { useCallback, useState } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { formatDollar, formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers'
interface TradePriceProps {
price: Price<Currency, Currency>
showInverted: boolean
setShowInverted: (showInverted: boolean) => void
}
const StyledPriceContainer = styled.button`
@@ -30,8 +27,8 @@ const StyledPriceContainer = styled.button`
user-select: text;
`
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
const theme = useTheme()
export default function TradePrice({ price }: TradePriceProps) {
const [showInverted, setShowInverted] = useState<boolean>(false)
const usdcPrice = useStablecoinPrice(showInverted ? price.baseCurrency : price.quoteCurrency)
@@ -58,9 +55,7 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
}}
title={text}
>
<Text fontWeight={500} color={theme.textPrimary}>
{text}
</Text>{' '}
<ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '}
{usdcPrice && (
<ThemedText.DeprecatedDarkGray>
<Trans>({formatDollar({ num: priceToPreciseFloat(usdcPrice), isPrice: true })})</Trans>

View File

@@ -25,10 +25,21 @@ export interface Connection {
type: ConnectionType
}
let metaMaskErrorHandler: (error: Error) => void | undefined
export function setMetMaskErrorHandler(errorHandler: (error: Error) => void) {
metaMaskErrorHandler = errorHandler
}
function onError(error: Error) {
console.debug(`web3-react error: ${error}`)
}
function onMetamaskError(error: Error) {
onError(error)
metaMaskErrorHandler?.(error)
}
const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
)
@@ -38,7 +49,9 @@ export const networkConnection: Connection = {
type: ConnectionType.NETWORK,
}
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>(
(actions) => new MetaMask({ actions, onError: onMetamaskError })
)
export const injectedConnection: Connection = {
connector: web3Injected,
hooks: web3InjectedHooks,

View File

@@ -3,9 +3,11 @@ import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from './chains'
describe('chains', () => {
describe('ALL_SUPPORTED_CHAIN_IDS', () => {
it('contains all the values in the SupportedChainId enum', () => {
Object.values(SupportedChainId).forEach((chainId) => {
if (typeof chainId === 'number') expect(ALL_SUPPORTED_CHAIN_IDS.includes(chainId as number)).toBeTruthy()
})
Object.values(SupportedChainId)
.filter((chainId) => typeof chainId === 'number')
.forEach((chainId) => {
expect(ALL_SUPPORTED_CHAIN_IDS.includes(chainId as number)).toBeTruthy()
})
})
it('contains no duplicates', () => {

View File

@@ -11,9 +11,6 @@ export const L2_DEADLINE_FROM_NOW = 60 * 5
export const DEFAULT_TXN_DISMISS_MS = 25000
export const L2_TXN_DISMISS_MS = 5000
// used for rewards deadlines
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)
export const BIG_INT_ZERO = JSBI.BigInt(0)
// one basis JSBI.BigInt

View File

@@ -1,4 +1,5 @@
export enum FeatureFlag {
fiatOnramp = 'fiatOnramp',
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
}

View File

@@ -0,0 +1,6 @@
import { BaseVariant } from '../index'
export function useFiatOnrampFlag(): BaseVariant {
return BaseVariant.Enabled
// return useBaseFlag(FeatureFlag.fiatOnramp)
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ const V2_SWAP_HOP_GAS_ESTIMATE = 50_000
* https://github.com/Uniswap/smart-order-router/blob/main/src/routers/alpha-router/gas-models/v2/v2-heuristic-gas-model.ts
*/
function guesstimateGas(trade: Trade<Currency, Currency, TradeType> | undefined): number | undefined {
if (!!trade) {
if (trade) {
let gas = 0
for (const { route } of trade.swaps) {
if (route.protocol === Protocol.V2) {

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