Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b25da9de2d | ||
|
|
ae1fb4367f | ||
|
|
1410edda32 | ||
|
|
3bc7f015ee | ||
|
|
802714377c | ||
|
|
352daf959e | ||
|
|
92c21c2811 | ||
|
|
e70723aaf3 | ||
|
|
1802f50163 | ||
|
|
2aa1b18d14 | ||
|
|
a286e5b114 | ||
|
|
62361647e0 | ||
|
|
deee278439 | ||
|
|
6340deb201 | ||
|
|
28b154ebe8 | ||
|
|
3bde2165f4 | ||
|
|
78c8fd2359 | ||
|
|
c378752910 | ||
|
|
0bf7b92013 | ||
|
|
283479f76e | ||
|
|
d3c30e2f6b | ||
|
|
32d226f78e | ||
|
|
96744505c0 | ||
|
|
97236033d4 | ||
|
|
86e62dc4b9 | ||
|
|
e584a5fa36 | ||
|
|
332ef6e6c8 | ||
|
|
8cbd111e65 | ||
|
|
55ffcbd465 | ||
|
|
404775e86d | ||
|
|
0ae9fe28a2 | ||
|
|
89c0caae43 | ||
|
|
c8086e3c76 | ||
|
|
1c2842e5a0 | ||
|
|
a2c6d3f475 | ||
|
|
841ea7f8a1 | ||
|
|
804692b114 | ||
|
|
6282298d13 | ||
|
|
7a5b855097 | ||
|
|
c9908748cf | ||
|
|
79b77deee1 | ||
|
|
a554af6670 | ||
|
|
1843f214b1 | ||
|
|
3e0788092e | ||
|
|
d14c49df0d | ||
|
|
c098ad1ffe | ||
|
|
48114ef51d | ||
|
|
cb7132ee17 | ||
|
|
0fa4859a09 | ||
|
|
f8bb5046f0 | ||
|
|
7d1589d1df | ||
|
|
26b603cc2e | ||
|
|
ece68a0ec7 | ||
|
|
fd212477ce | ||
|
|
a16d2387cc | ||
|
|
cae56ec385 | ||
|
|
d16b3473e0 | ||
|
|
f66f249dba | ||
|
|
08afd888d0 | ||
|
|
b427be2673 | ||
|
|
f753a5e325 | ||
|
|
46d9d8e3df | ||
|
|
680d3a3f26 | ||
|
|
e4c625ee71 |
7
.env
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -0,0 +1,7 @@
|
||||
/* eslint-env node */
|
||||
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
}
|
||||
109
.eslintrc.json
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
7
.github/workflows/release.yaml
vendored
@@ -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
@@ -9,7 +9,6 @@
|
||||
/src/locales/**/pseudo.po
|
||||
|
||||
# generated graphql types
|
||||
__generated__/
|
||||
schema.graphql
|
||||
|
||||
# dependencies
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/src/schema/schema.graphql
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
7
.vscode/settings.json
vendored
@@ -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
@@ -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
|
||||
25
apollo-codegen_thegraph.ts
Normal 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
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
src: './src',
|
||||
language: 'typescript',
|
||||
@@ -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,
|
||||
35
package.json
@@ -8,10 +8,10 @@
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"relay": "relay-compiler relay.config.js",
|
||||
"relay-thegraph": "relay-compiler relay_thegraph.config.js",
|
||||
"graphql:fetch": "node fetch-schema.js",
|
||||
"graphql:generate": "yarn relay && yarn relay-thegraph",
|
||||
"graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
|
||||
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
|
||||
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
|
||||
"prei18n:extract": "node prei18n-extract.js",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
21
src/assets/svg/fiat_mask.svg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
89
src/components/About/AboutFooter.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
150
src/components/About/Card.tsx
Normal 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
|
||||
106
src/components/About/ProtocolBanner.tsx
Normal 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
|
||||
71
src/components/About/constants.tsx
Normal 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,
|
||||
},
|
||||
]
|
||||
|
Before Width: | Height: | Size: 327 KiB After Width: | Height: | Size: 327 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 379 KiB |
BIN
src/components/About/images/Mesh.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/components/About/images/aboutArrowDark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/components/About/images/aboutArrowLight.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
src/components/About/images/aboutDollarDark.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src/components/About/images/aboutTerminalDark.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/components/About/images/nftCard.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
src/components/About/images/swapCard.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -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 />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
13
src/components/Button/LoadingButtonSpinner.tsx
Normal 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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
149
src/components/FiatOnrampAnnouncement/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
143
src/components/FiatOnrampModal/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const baseNavDropdown = style([
|
||||
borderWidth: '1px',
|
||||
paddingBottom: '8',
|
||||
paddingTop: '8',
|
||||
zIndex: '2',
|
||||
}),
|
||||
{
|
||||
boxShadow: '0px 4px 12px 0px #00000026',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,21 +27,20 @@ import Widget from 'components/Widget'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { TokenPriceQuery } from 'graphql/data/__generated__/TokenPriceQuery.graphql'
|
||||
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
||||
import { QueryToken, tokenQuery } from 'graphql/data/Token'
|
||||
import { QueryToken } from 'graphql/data/Token'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
|
||||
import { useCallback, useMemo, useState, useTransition } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { PreloadedQuery, usePreloadedQuery } from 'react-relay'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isAddress } from 'utils'
|
||||
|
||||
import { RefetchPricesFunction } from './ChartSection'
|
||||
import { OnChangeTimePeriod } from './ChartSection'
|
||||
import InvalidTokenDetails from './InvalidTokenDetails'
|
||||
|
||||
const TokenSymbol = styled.span`
|
||||
@@ -75,7 +74,7 @@ function useRelevantToken(
|
||||
const queryToken = useMemo(() => {
|
||||
if (!address) return undefined
|
||||
if (address === NATIVE_CHAIN_ID) return nativeOnChain(pageChainId)
|
||||
if (tokenQueryData) return new QueryToken(tokenQueryData)
|
||||
if (tokenQueryData) return new QueryToken(address, tokenQueryData)
|
||||
return undefined
|
||||
}, [pageChainId, address, tokenQueryData])
|
||||
// fetches on-chain token if query data is missing and page chain matches global chain (else fetch won't work)
|
||||
@@ -91,16 +90,16 @@ function useRelevantToken(
|
||||
type TokenDetailsProps = {
|
||||
urlAddress: string | undefined
|
||||
chain: Chain
|
||||
tokenQueryReference: PreloadedQuery<TokenQuery>
|
||||
priceQueryReference: PreloadedQuery<TokenPriceQuery> | null | undefined
|
||||
refetchTokenPrices: RefetchPricesFunction
|
||||
tokenQuery: TokenQuery
|
||||
tokenPriceQuery: TokenPriceQuery | undefined
|
||||
onChangeTimePeriod: OnChangeTimePeriod
|
||||
}
|
||||
export default function TokenDetails({
|
||||
urlAddress,
|
||||
chain,
|
||||
tokenQueryReference,
|
||||
priceQueryReference,
|
||||
refetchTokenPrices,
|
||||
tokenQuery,
|
||||
tokenPriceQuery,
|
||||
onChangeTimePeriod,
|
||||
}: TokenDetailsProps) {
|
||||
if (!urlAddress) {
|
||||
throw new Error('Invalid token details route: tokenAddress param is undefined')
|
||||
@@ -112,7 +111,7 @@ export default function TokenDetails({
|
||||
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
|
||||
const tokenQueryData = usePreloadedQuery(tokenQuery, tokenQueryReference).tokens?.[0]
|
||||
const tokenQueryData = tokenQuery.tokens?.[0]
|
||||
const crossChainMap = useMemo(
|
||||
() =>
|
||||
tokenQueryData?.project?.tokens.reduce((map, current) => {
|
||||
@@ -200,7 +199,7 @@ export default function TokenDetails({
|
||||
<ShareButton currency={token} />
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartSection priceQueryReference={priceQueryReference} refetchTokenPrices={refetchTokenPrices} />
|
||||
<ChartSection tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
|
||||
<StatsSection
|
||||
TVL={tokenQueryData?.market?.totalValueLocked?.value}
|
||||
volume24H={tokenQueryData?.market?.volume24H?.value}
|
||||
|
||||
@@ -459,7 +459,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${tokenName}`}>
|
||||
<StyledLink
|
||||
to={getTokenDetailsURL(token.address, token.chain)}
|
||||
to={getTokenDetailsURL(token.address ?? '', token.chain)}
|
||||
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
|
||||
>
|
||||
<TokenRow
|
||||
@@ -512,7 +512,6 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
height={height}
|
||||
tokenData={token}
|
||||
pricePercentChange={token.market?.pricePercentChange?.value}
|
||||
timePeriod={timePeriod}
|
||||
sparklineMap={props.sparklineMap}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ const LoadingRows = ({ rowCount }: { rowCount: number }) => (
|
||||
</>
|
||||
)
|
||||
|
||||
export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
@@ -75,14 +75,15 @@ export function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable({ setRowCount }: { setRowCount: (c: number) => void }) {
|
||||
export default function TokenTable() {
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { tokens, sparklines } = useTopTokens(chainName)
|
||||
setRowCount(tokens?.length ?? PAGE_SIZE)
|
||||
const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
|
||||
/* loading and error state */
|
||||
if (!tokens) {
|
||||
if (loadingTokens) {
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
} else if (!tokens) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
97
src/components/Web3Status/MetamaskConnectionError.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum FeatureFlag {
|
||||
fiatOnramp = 'fiatOnramp',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
}
|
||||
|
||||
6
src/featureFlags/flags/fiatOnramp.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useFiatOnrampFlag(): BaseVariant {
|
||||
return BaseVariant.Enabled
|
||||
// return useBaseFlag(FeatureFlag.fiatOnramp)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import ms from 'ms.macro'
|
||||
import {
|
||||
RelayNetworkLayer,
|
||||
RelayNetworkLayerResponse,
|
||||
retryMiddleware,
|
||||
urlMiddleware,
|
||||
} from 'react-relay-network-modern'
|
||||
import { Environment, RecordSource, Store } from 'relay-runtime'
|
||||
|
||||
// This makes it possible (and more likely) to be able to reuse data when navigating back to a page,
|
||||
// tab or piece of content that has been visited before. These settings together configure the cache
|
||||
// to serve the last 250 records, so long as they are less than 5 minutes old:
|
||||
const gcReleaseBufferSize = 250
|
||||
const queryCacheExpirationTime = ms`5m`
|
||||
|
||||
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
|
||||
if (!GRAPHQL_URL) {
|
||||
throw new Error('AWS URL MISSING FROM ENVIRONMENT')
|
||||
}
|
||||
|
||||
const RETRY_TIME_MS = [3200, 6400, 12800]
|
||||
|
||||
// This network layer must not cache, or it will break cache-evicting network policies
|
||||
const network = new RelayNetworkLayer(
|
||||
[
|
||||
urlMiddleware({
|
||||
url: GRAPHQL_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}),
|
||||
function logAndIgnoreErrors(next) {
|
||||
return async (req) => {
|
||||
try {
|
||||
const res = await next(req)
|
||||
if (!res || !res.data) throw new Error('Missing response data')
|
||||
return res
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return RelayNetworkLayerResponse.createFromGraphQL({ data: [] })
|
||||
}
|
||||
}
|
||||
},
|
||||
retryMiddleware({
|
||||
fetchTimeout: ms`30s`, // mirrors backend's timeout in case that fails
|
||||
retryDelays: RETRY_TIME_MS,
|
||||
statusCodes: (statusCode) => statusCode >= 500 && statusCode < 600,
|
||||
}),
|
||||
],
|
||||
{ noThrow: true }
|
||||
)
|
||||
|
||||
const CachingEnvironment = new Environment({
|
||||
network,
|
||||
store: new Store(new RecordSource(), { gcReleaseBufferSize, queryCacheExpirationTime }),
|
||||
})
|
||||
export default CachingEnvironment
|
||||
@@ -1,8 +1,8 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
|
||||
import gql from 'graphql-tag'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
|
||||
import { TokenQuery$data } from './__generated__/TokenQuery.graphql'
|
||||
import { TokenQuery } from './__generated__/types-and-hooks'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from './util'
|
||||
|
||||
/*
|
||||
@@ -13,14 +13,14 @@ The difference between Token and TokenProject:
|
||||
TokenMarket is per-chain market data for contracts pulled from the graph.
|
||||
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
|
||||
*/
|
||||
export const tokenQuery = graphql`
|
||||
query TokenQuery($contract: ContractInput!) {
|
||||
gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
id @required(action: LOG)
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain @required(action: LOG)
|
||||
address @required(action: LOG)
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
totalValueLocked {
|
||||
@@ -48,23 +48,24 @@ export const tokenQuery = graphql`
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
chain @required(action: LOG)
|
||||
address @required(action: LOG)
|
||||
chain
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export type { Chain, TokenQuery } from './__generated__/TokenQuery.graphql'
|
||||
|
||||
export type TokenQueryData = NonNullable<TokenQuery$data['tokens']>[number]
|
||||
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
||||
|
||||
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
|
||||
|
||||
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
|
||||
export class QueryToken extends WrappedTokenInfo {
|
||||
constructor(data: NonNullable<TokenQueryData>, logoSrc?: string) {
|
||||
constructor(address: string, data: NonNullable<TokenQueryData>, logoSrc?: string) {
|
||||
super({
|
||||
chainId: CHAIN_NAME_TO_CHAIN_ID[data.chain],
|
||||
address: data.address,
|
||||
address,
|
||||
decimals: data.decimals ?? DEFAULT_ERC20_DECIMALS,
|
||||
symbol: data.symbol ?? '',
|
||||
name: data.name ?? '',
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
|
||||
export const tokenPriceQuery = graphql`
|
||||
query TokenPriceQuery($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
gql`
|
||||
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
market(currency: USD) @required(action: LOG) {
|
||||
market(currency: USD) {
|
||||
price {
|
||||
value @required(action: LOG)
|
||||
value
|
||||
}
|
||||
priceHistory(duration: $duration) {
|
||||
timestamp @required(action: LOG)
|
||||
value @required(action: LOG)
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
export type { TokenPriceQuery } from './__generated__/TokenPriceQuery.graphql'
|
||||
export type { TokenPriceQuery } from './__generated__/types-and-hooks'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import {
|
||||
filterStringAtom,
|
||||
filterTimeAtom,
|
||||
@@ -6,22 +5,25 @@ import {
|
||||
sortMethodAtom,
|
||||
TokenSortMethod,
|
||||
} from 'components/Tokens/state'
|
||||
import gql from 'graphql-tag'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { fetchQuery, useLazyLoadQuery, useRelayEnvironment } from 'react-relay'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import type { Chain, TopTokens100Query } from './__generated__/TopTokens100Query.graphql'
|
||||
import { TopTokensSparklineQuery } from './__generated__/TopTokensSparklineQuery.graphql'
|
||||
import { isPricePoint, PricePoint } from './util'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, toHistoryDuration, unwrapToken } from './util'
|
||||
import {
|
||||
Chain,
|
||||
TopTokens100Query,
|
||||
useTopTokens100Query,
|
||||
useTopTokensSparklineQuery,
|
||||
} from './__generated__/types-and-hooks'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
|
||||
|
||||
const topTokens100Query = graphql`
|
||||
query TopTokens100Query($duration: HistoryDuration!, $chain: Chain!) {
|
||||
gql`
|
||||
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id @required(action: LOG)
|
||||
id
|
||||
name
|
||||
chain @required(action: LOG)
|
||||
address @required(action: LOG)
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
totalValueLocked {
|
||||
@@ -48,21 +50,21 @@ const topTokens100Query = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
const tokenSparklineQuery = graphql`
|
||||
query TopTokensSparklineQuery($duration: HistoryDuration!, $chain: Chain!) {
|
||||
gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
address
|
||||
market(currency: USD) {
|
||||
priceHistory(duration: $duration) {
|
||||
timestamp @required(action: LOG)
|
||||
value @required(action: LOG)
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
function useSortedTokens(tokens: NonNullable<TopTokens100Query['response']['topTokens']>) {
|
||||
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
const sortMethod = useAtomValue(sortMethodAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
@@ -91,7 +93,7 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['response']['topT
|
||||
}, [tokens, sortMethod, sortAscending])
|
||||
}
|
||||
|
||||
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']['topTokens']>) {
|
||||
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
|
||||
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
|
||||
@@ -112,11 +114,12 @@ function useFilteredTokens(tokens: NonNullable<TopTokens100Query['response']['to
|
||||
|
||||
// Number of items to render in each fetch in infinite scroll.
|
||||
export const PAGE_SIZE = 20
|
||||
|
||||
export type TopToken = NonNullable<NonNullable<TopTokens100Query['response']>['topTokens']>[number]
|
||||
export type SparklineMap = { [key: string]: PricePoint[] | undefined }
|
||||
export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[number]
|
||||
|
||||
interface UseTopTokensReturnValue {
|
||||
tokens: TopToken[] | undefined
|
||||
loadingTokens: boolean
|
||||
sparklines: SparklineMap
|
||||
}
|
||||
|
||||
@@ -124,33 +127,27 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
||||
|
||||
const environment = useRelayEnvironment()
|
||||
const [sparklines, setSparklines] = useState<SparklineMap>({})
|
||||
useEffect(() => {
|
||||
const subscription = fetchQuery<TopTokensSparklineQuery>(environment, tokenSparklineQuery, { duration, chain })
|
||||
.map((data) => ({
|
||||
topTokens: data.topTokens?.map((token) => unwrapToken(chainId, token)),
|
||||
}))
|
||||
.subscribe({
|
||||
next(data) {
|
||||
const map: SparklineMap = {}
|
||||
data.topTokens?.forEach(
|
||||
(current) =>
|
||||
current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint))
|
||||
)
|
||||
setSparklines(map)
|
||||
},
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}, [chain, chainId, duration, environment])
|
||||
const { data: sparklineQuery } = useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setSparklines({})
|
||||
}, [duration])
|
||||
const sparklines = useMemo(() => {
|
||||
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
|
||||
const map: SparklineMap = {}
|
||||
unwrappedTokens?.forEach(
|
||||
(current) => current?.address && (map[current.address] = current?.market?.priceHistory?.filter(isPricePoint))
|
||||
)
|
||||
return map
|
||||
}, [chainId, sparklineQuery?.topTokens])
|
||||
|
||||
const { topTokens } = useLazyLoadQuery<TopTokens100Query>(topTokens100Query, { duration, chain })
|
||||
const mappedTokens = useMemo(() => topTokens?.map((token) => unwrapToken(chainId, token)) ?? [], [chainId, topTokens])
|
||||
const { data, loading: loadingTokens } = useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const mappedTokens = useMemo(
|
||||
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
|
||||
[chainId, data]
|
||||
)
|
||||
const filteredTokens = useFilteredTokens(mappedTokens)
|
||||
const sortedTokens = useSortedTokens(filteredTokens)
|
||||
return useMemo(() => ({ tokens: sortedTokens, sparklines }), [sortedTokens, sparklines])
|
||||
return useMemo(() => ({ tokens: sortedTokens, loadingTokens, sparklines }), [loadingTokens, sortedTokens, sparklines])
|
||||
}
|
||||
|
||||
1595
src/graphql/data/__generated__/types-and-hooks.ts
generated
Normal file
30
src/graphql/data/apollo.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
import { relayStylePagination } from '@apollo/client/utilities'
|
||||
|
||||
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
|
||||
if (!GRAPHQL_URL) {
|
||||
throw new Error('AWS URL MISSING FROM ENVIRONMENT')
|
||||
}
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
uri: GRAPHQL_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Origin: 'https://app.uniswap.org',
|
||||
},
|
||||
cache: new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
nftBalances: relayStylePagination(),
|
||||
nftAssets: relayStylePagination(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,25 +1,30 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import ms from 'ms.macro'
|
||||
import { GenieAsset, Trait } from 'nft/types'
|
||||
import gql from 'graphql-tag'
|
||||
import { GenieAsset, Markets, Trait } from 'nft/types'
|
||||
import { wrapScientificNotation } from 'nft/utils'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { fetchQuery, useLazyLoadQuery, usePaginationFragment, useQueryLoader, useRelayEnvironment } from 'react-relay'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { AssetPaginationQuery } from './__generated__/AssetPaginationQuery.graphql'
|
||||
import {
|
||||
AssetQuery,
|
||||
AssetQuery$variables,
|
||||
AssetQueryVariables,
|
||||
NftAssetEdge,
|
||||
NftAssetsFilterInput,
|
||||
NftAssetSortableField,
|
||||
NftAssetTraitInput,
|
||||
NftMarketplace,
|
||||
} from './__generated__/AssetQuery.graphql'
|
||||
import { AssetQuery_nftAssets$data } from './__generated__/AssetQuery_nftAssets.graphql'
|
||||
useAssetQuery,
|
||||
} from '../__generated__/types-and-hooks'
|
||||
|
||||
const assetPaginationQuery = graphql`
|
||||
fragment AssetQuery_nftAssets on Query @refetchable(queryName: "AssetPaginationQuery") {
|
||||
gql`
|
||||
query Asset(
|
||||
$address: String!
|
||||
$orderBy: NftAssetSortableField
|
||||
$asc: Boolean
|
||||
$filter: NftAssetsFilterInput
|
||||
$first: Int
|
||||
$after: String
|
||||
$last: Int
|
||||
$before: String
|
||||
) {
|
||||
nftAssets(
|
||||
address: $address
|
||||
orderBy: $orderBy
|
||||
@@ -29,7 +34,7 @@ const assetPaginationQuery = graphql`
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
) @connection(key: "AssetQuery_nftAssets") {
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
@@ -99,52 +104,38 @@ const assetPaginationQuery = graphql`
|
||||
}
|
||||
metadataUrl
|
||||
}
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
pageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const assetQuery = graphql`
|
||||
query AssetQuery(
|
||||
$address: String!
|
||||
$orderBy: NftAssetSortableField
|
||||
$asc: Boolean
|
||||
$filter: NftAssetsFilterInput
|
||||
$first: Int
|
||||
$after: String
|
||||
$last: Int
|
||||
$before: String
|
||||
) {
|
||||
...AssetQuery_nftAssets
|
||||
}
|
||||
`
|
||||
|
||||
type NftAssetsQueryAsset = NonNullable<
|
||||
NonNullable<NonNullable<AssetQuery_nftAssets$data['nftAssets']>['edges']>[number]
|
||||
>
|
||||
|
||||
function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: number) {
|
||||
function formatAssetQueryData(queryAsset: NftAssetEdge, totalCount?: number) {
|
||||
const asset = queryAsset.node
|
||||
const ethPrice = parseEther(wrapScientificNotation(asset.listings?.edges[0]?.node.price.value ?? 0)).toString()
|
||||
return {
|
||||
id: asset.id,
|
||||
address: asset?.collection?.nftContracts?.[0]?.address,
|
||||
address: asset?.collection?.nftContracts?.[0]?.address ?? '',
|
||||
notForSale: asset.listings?.edges?.length === 0,
|
||||
collectionName: asset.collection?.name,
|
||||
collectionSymbol: asset.collection?.image?.url,
|
||||
imageUrl: asset.image?.url,
|
||||
animationUrl: asset.animationUrl,
|
||||
marketplace: asset.listings?.edges[0]?.node?.marketplace?.toLowerCase(),
|
||||
marketplace: asset.listings?.edges[0]?.node?.marketplace?.toLowerCase() as unknown as Markets,
|
||||
name: asset.name,
|
||||
priceInfo: asset.listings
|
||||
? {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
}
|
||||
: undefined,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
susFlag: asset.suspiciousFlag,
|
||||
sellorders: asset.listings?.edges.map((listingNode) => {
|
||||
return {
|
||||
@@ -155,7 +146,7 @@ function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: numb
|
||||
}
|
||||
}),
|
||||
smallImageUrl: asset.smallImage?.url,
|
||||
tokenId: asset.tokenId,
|
||||
tokenId: asset.tokenId ?? '',
|
||||
tokenType: asset.collection?.nftContracts?.[0]?.standard,
|
||||
totalCount,
|
||||
collectionIsVerified: asset.collection?.isVerified,
|
||||
@@ -168,7 +159,7 @@ function formatAssetQueryData(queryAsset: NftAssetsQueryAsset, totalCount?: numb
|
||||
}
|
||||
}),
|
||||
},
|
||||
owner: asset.ownerAddress,
|
||||
ownerAddress: asset.ownerAddress,
|
||||
creator: {
|
||||
profile_img_url: asset.collection?.creator?.profileImage?.url,
|
||||
address: asset.collection?.creator?.address,
|
||||
@@ -190,57 +181,50 @@ export interface AssetFetcherParams {
|
||||
before?: string
|
||||
}
|
||||
|
||||
const defaultAssetFetcherParams: Omit<AssetQuery$variables, 'address'> = {
|
||||
orderBy: 'PRICE',
|
||||
const defaultAssetFetcherParams: Omit<AssetQueryVariables, 'address'> = {
|
||||
orderBy: NftAssetSortableField.Price,
|
||||
asc: true,
|
||||
// tokenSearchQuery must be specified so that this exactly matches the initial query.
|
||||
filter: { listed: false, tokenSearchQuery: '' },
|
||||
first: ASSET_PAGE_SIZE,
|
||||
}
|
||||
|
||||
export function useLoadAssetsQuery(address?: string) {
|
||||
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery)
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
loadQuery({ ...defaultAssetFetcherParams, address })
|
||||
}
|
||||
}, [address, loadQuery])
|
||||
}
|
||||
export function useNftAssets(params: AssetFetcherParams) {
|
||||
const variables = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params])
|
||||
|
||||
export function useLazyLoadAssetsQuery(params: AssetFetcherParams) {
|
||||
const vars = useMemo(() => ({ ...defaultAssetFetcherParams, ...params }), [params])
|
||||
const [fetchKey, setFetchKey] = useState(0)
|
||||
// Use the store if it is available (eg from polling), or the network if it is not (eg from an incorrect preload).
|
||||
const fetchPolicy = 'store-or-network'
|
||||
const queryData = useLazyLoadQuery<AssetQuery>(assetQuery, vars, { fetchKey, fetchPolicy }) // this will suspend if not yet loaded
|
||||
|
||||
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<AssetPaginationQuery, any>(
|
||||
assetPaginationQuery,
|
||||
queryData
|
||||
const { data, loading, fetchMore } = useAssetQuery({
|
||||
variables,
|
||||
})
|
||||
const hasNext = data?.nftAssets?.pageInfo?.hasNextPage
|
||||
const loadMore = useCallback(
|
||||
() =>
|
||||
fetchMore({
|
||||
variables: {
|
||||
after: data?.nftAssets?.pageInfo?.endCursor,
|
||||
},
|
||||
}),
|
||||
[data, fetchMore]
|
||||
)
|
||||
|
||||
// Poll for updates.
|
||||
const POLLING_INTERVAL = ms`5s`
|
||||
const environment = useRelayEnvironment()
|
||||
const poll = useCallback(async () => {
|
||||
if (data.nftAssets?.edges?.length > ASSET_PAGE_SIZE) return
|
||||
// Initiate a network request. When it resolves, refresh the UI from store (to avoid re-triggering Suspense);
|
||||
// see: https://relay.dev/docs/guided-tour/refetching/refreshing-queries/#if-you-need-to-avoid-suspense-1.
|
||||
await fetchQuery<AssetQuery>(environment, assetQuery, { ...vars }).toPromise()
|
||||
setFetchKey((fetchKey) => fetchKey + 1)
|
||||
}, [data.nftAssets?.edges?.length, environment, vars])
|
||||
useInterval(poll, isLoadingNext ? null : POLLING_INTERVAL, /* leading= */ false)
|
||||
// TODO: setup polling while handling pagination
|
||||
|
||||
// It is especially important for this to be memoized to avoid re-rendering from polling if data is unchanged.
|
||||
const assets: GenieAsset[] = useMemo(
|
||||
const assets: GenieAsset[] | undefined = useMemo(
|
||||
() =>
|
||||
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => {
|
||||
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount)
|
||||
data?.nftAssets?.edges?.map((queryAsset) => {
|
||||
return formatAssetQueryData(queryAsset as NonNullable<NftAssetEdge>, data.nftAssets?.totalCount)
|
||||
}),
|
||||
[data.nftAssets?.edges, data.nftAssets?.totalCount]
|
||||
[data?.nftAssets?.edges, data?.nftAssets?.totalCount]
|
||||
)
|
||||
|
||||
return { assets, hasNext, isLoadingNext, loadNext }
|
||||
return useMemo(() => {
|
||||
return {
|
||||
data: assets,
|
||||
hasNext,
|
||||
loading,
|
||||
loadMore,
|
||||
}
|
||||
}, [assets, hasNext, loadMore, loading])
|
||||
}
|
||||
|
||||
const DEFAULT_SWEEP_AMOUNT = 50
|
||||
@@ -252,7 +236,7 @@ export interface SweepFetcherParams {
|
||||
traits?: Trait[]
|
||||
}
|
||||
|
||||
function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQuery$variables {
|
||||
function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepFetcherParams): AssetQueryVariables {
|
||||
const filter: NftAssetsFilterInput = useMemo(
|
||||
() => ({
|
||||
listed: true,
|
||||
@@ -272,7 +256,7 @@ function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepF
|
||||
return useMemo(
|
||||
() => ({
|
||||
address: contractAddress,
|
||||
orderBy: 'PRICE',
|
||||
orderBy: NftAssetSortableField.Price,
|
||||
asc: true,
|
||||
first: DEFAULT_SWEEP_AMOUNT,
|
||||
filter,
|
||||
@@ -281,28 +265,19 @@ function useSweepFetcherVars({ contractAddress, markets, price, traits }: SweepF
|
||||
)
|
||||
}
|
||||
|
||||
export function useLoadSweepAssetsQuery(params: SweepFetcherParams, enabled = true) {
|
||||
const [, loadQuery] = useQueryLoader<AssetQuery>(assetQuery)
|
||||
const vars = useSweepFetcherVars(params)
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
loadQuery(vars)
|
||||
}
|
||||
}, [loadQuery, enabled, vars])
|
||||
}
|
||||
|
||||
// Lazy-loads an already loaded AssetsQuery.
|
||||
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to
|
||||
// prevent waterfalling. Use useLoadSweepAssetsQuery to trigger the query.
|
||||
export function useLazyLoadSweepAssetsQuery(params: SweepFetcherParams): GenieAsset[] {
|
||||
const vars = useSweepFetcherVars(params)
|
||||
const queryData = useLazyLoadQuery(assetQuery, vars, { fetchPolicy: 'store-only' }) // this will suspend if not yet loaded
|
||||
const { data } = usePaginationFragment<AssetPaginationQuery, any>(assetPaginationQuery, queryData)
|
||||
return useMemo<GenieAsset[]>(
|
||||
export function useSweepNftAssets(params: SweepFetcherParams) {
|
||||
const variables = useSweepFetcherVars(params)
|
||||
const { data, loading } = useAssetQuery({
|
||||
variables,
|
||||
// This prevents overwriting the page's call to assets for cards shown
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
const assets = useMemo<GenieAsset[] | undefined>(
|
||||
() =>
|
||||
data.nftAssets?.edges?.map((queryAsset: NftAssetsQueryAsset) => {
|
||||
return formatAssetQueryData(queryAsset, data.nftAssets?.totalCount)
|
||||
data?.nftAssets?.edges?.map((queryAsset) => {
|
||||
return formatAssetQueryData(queryAsset as NonNullable<NftAssetEdge>, data.nftAssets?.totalCount)
|
||||
}),
|
||||
[data.nftAssets?.edges, data.nftAssets?.totalCount]
|
||||
[data?.nftAssets?.edges, data?.nftAssets?.totalCount]
|
||||
)
|
||||
return useMemo(() => ({ data: assets, loading }), [assets, loading])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import gql from 'graphql-tag'
|
||||
import { GenieCollection, Trait } from 'nft/types'
|
||||
import { useEffect } from 'react'
|
||||
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { CollectionQuery } from './__generated__/CollectionQuery.graphql'
|
||||
import { NftCollection, useCollectionQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
const collectionQuery = graphql`
|
||||
query CollectionQuery($addresses: [String!]!) {
|
||||
gql`
|
||||
query Collection($addresses: [String!]!) {
|
||||
nftCollections(filter: { addresses: $addresses }) {
|
||||
edges {
|
||||
cursor
|
||||
@@ -87,28 +86,23 @@ const collectionQuery = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
export function useLoadCollectionQuery(address?: string | string[]): void {
|
||||
const [, loadQuery] = useQueryLoader(collectionQuery)
|
||||
useEffect(() => {
|
||||
if (address) {
|
||||
loadQuery({ addresses: Array.isArray(address) ? address : [address] })
|
||||
}
|
||||
}, [address, loadQuery])
|
||||
interface useCollectionReturnProps {
|
||||
data: GenieCollection
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
// Lazy-loads an already loaded CollectionQuery.
|
||||
// This will *not* trigger a query - that must be done from a parent component to ensure proper query coalescing and to
|
||||
// prevent waterfalling. Use useLoadCollectionQuery to trigger the query.
|
||||
export function useCollectionQuery(address: string): GenieCollection {
|
||||
const queryData = useLazyLoadQuery<CollectionQuery>( // this will suspend if not yet loaded
|
||||
collectionQuery,
|
||||
{ addresses: [address] },
|
||||
{ fetchPolicy: 'store-or-network' }
|
||||
)
|
||||
export function useCollection(address: string): useCollectionReturnProps {
|
||||
const { data: queryData, loading } = useCollectionQuery({
|
||||
variables: {
|
||||
addresses: address,
|
||||
},
|
||||
})
|
||||
|
||||
const queryCollection = queryData.nftCollections?.edges[0]?.node
|
||||
const market = queryCollection?.markets && queryCollection?.markets[0]
|
||||
const traits = {} as Record<string, Trait[]>
|
||||
const queryCollection = queryData?.nftCollections?.edges?.[0]?.node as NonNullable<NftCollection>
|
||||
const market = queryCollection?.markets?.[0]
|
||||
const traits = useMemo(() => {
|
||||
return {} as Record<string, Trait[]>
|
||||
}, [])
|
||||
if (queryCollection?.traits) {
|
||||
queryCollection?.traits.forEach((trait) => {
|
||||
if (trait.name && trait.stats) {
|
||||
@@ -122,42 +116,43 @@ export function useCollectionQuery(address: string): GenieCollection {
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
address,
|
||||
isVerified: queryCollection?.isVerified ?? undefined,
|
||||
name: queryCollection?.name ?? undefined,
|
||||
description: queryCollection?.description ?? undefined,
|
||||
standard: queryCollection?.nftContracts ? queryCollection?.nftContracts[0]?.standard ?? undefined : undefined,
|
||||
bannerImageUrl: queryCollection?.bannerImage?.url ?? undefined,
|
||||
stats: queryCollection?.markets
|
||||
? {
|
||||
num_owners: market?.owners ?? undefined,
|
||||
floor_price: market?.floorPrice?.value ?? undefined,
|
||||
one_day_volume: market?.volume?.value ?? undefined,
|
||||
one_day_change: market?.volumePercentChange?.value ?? undefined,
|
||||
one_day_floor_change: market?.floorPricePercentChange?.value ?? undefined,
|
||||
banner_image_url: queryCollection?.bannerImage?.url ?? undefined,
|
||||
total_supply: queryCollection?.numAssets ?? undefined,
|
||||
total_listings: market?.listings?.value ?? undefined,
|
||||
total_volume: market?.totalVolume?.value ?? undefined,
|
||||
}
|
||||
: {},
|
||||
traits,
|
||||
marketplaceCount: queryCollection?.markets
|
||||
? market?.marketplaces?.map((market) => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
data: {
|
||||
address,
|
||||
isVerified: queryCollection?.isVerified,
|
||||
name: queryCollection?.name,
|
||||
description: queryCollection?.description,
|
||||
standard: queryCollection?.nftContracts?.[0]?.standard,
|
||||
bannerImageUrl: queryCollection?.bannerImage?.url,
|
||||
stats: {
|
||||
num_owners: market?.owners,
|
||||
floor_price: market?.floorPrice?.value,
|
||||
one_day_volume: market?.volume?.value,
|
||||
one_day_change: market?.volumePercentChange?.value,
|
||||
one_day_floor_change: market?.floorPricePercentChange?.value,
|
||||
banner_image_url: queryCollection?.bannerImage?.url,
|
||||
total_supply: queryCollection?.numAssets,
|
||||
total_listings: market?.listings?.value,
|
||||
total_volume: market?.totalVolume?.value,
|
||||
},
|
||||
traits,
|
||||
marketplaceCount: market?.marketplaces?.map((market) => {
|
||||
return {
|
||||
marketplace: market.marketplace?.toLowerCase() ?? '',
|
||||
count: market.listings ?? 0,
|
||||
floorPrice: market.floorPrice ?? 0,
|
||||
}
|
||||
})
|
||||
: undefined,
|
||||
imageUrl: queryCollection?.image?.url ?? '',
|
||||
twitterUrl: queryCollection?.twitterName ?? '',
|
||||
instagram: queryCollection?.instagramName ?? undefined,
|
||||
discordUrl: queryCollection?.discordUrl ?? undefined,
|
||||
externalUrl: queryCollection?.homepageUrl ?? undefined,
|
||||
rarityVerified: false, // TODO update when backend supports
|
||||
// isFoundation: boolean, // TODO ask backend to add
|
||||
}
|
||||
}),
|
||||
imageUrl: queryCollection?.image?.url ?? '',
|
||||
twitterUrl: queryCollection?.twitterName,
|
||||
instagram: queryCollection?.instagramName,
|
||||
discordUrl: queryCollection?.discordUrl,
|
||||
externalUrl: queryCollection?.homepageUrl,
|
||||
rarityVerified: false, // TODO update when backend supports
|
||||
// isFoundation: boolean, // TODO ask backend to add
|
||||
},
|
||||
loading,
|
||||
}
|
||||
}, [address, loading, market, queryCollection, traits])
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { CollectionInfoForAsset, GenieAsset, SellOrder, TokenType } from 'nft/types'
|
||||
import { useEffect } from 'react'
|
||||
import { useLazyLoadQuery, useQueryLoader } from 'react-relay'
|
||||
import gql from 'graphql-tag'
|
||||
import { CollectionInfoForAsset, GenieAsset, Markets, SellOrder } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { DetailsQuery } from './__generated__/DetailsQuery.graphql'
|
||||
import { NftAsset, useDetailsQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
const detailsQuery = graphql`
|
||||
query DetailsQuery($address: String!, $tokenId: String!) {
|
||||
gql`
|
||||
query Details($address: String!, $tokenId: String!) {
|
||||
nftAssets(address: $address, filter: { listed: false, tokenIds: [$tokenId] }) {
|
||||
edges {
|
||||
node {
|
||||
@@ -92,92 +91,87 @@ const detailsQuery = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
export function useLoadDetailsQuery(address?: string, tokenId?: string): void {
|
||||
const [, loadQuery] = useQueryLoader(detailsQuery)
|
||||
useEffect(() => {
|
||||
if (address && tokenId) {
|
||||
loadQuery({ address, tokenId })
|
||||
}
|
||||
}, [address, tokenId, loadQuery])
|
||||
}
|
||||
|
||||
export function useDetailsQuery(address: string, tokenId: string): [GenieAsset, CollectionInfoForAsset] | undefined {
|
||||
const queryData = useLazyLoadQuery<DetailsQuery>(
|
||||
detailsQuery,
|
||||
{
|
||||
export function useNftAssetDetails(
|
||||
address: string,
|
||||
tokenId: string
|
||||
): { data: [GenieAsset, CollectionInfoForAsset]; loading: boolean } {
|
||||
const { data: queryData, loading } = useDetailsQuery({
|
||||
variables: {
|
||||
address,
|
||||
tokenId,
|
||||
},
|
||||
{ fetchPolicy: 'store-or-network' }
|
||||
)
|
||||
})
|
||||
|
||||
const asset = queryData.nftAssets?.edges[0]?.node
|
||||
const asset = queryData?.nftAssets?.edges[0]?.node as NonNullable<NftAsset> | undefined
|
||||
const collection = asset?.collection
|
||||
const listing = asset?.listings?.edges[0]?.node
|
||||
const ethPrice = parseEther(listing?.price?.value?.toString() ?? '0').toString()
|
||||
|
||||
return [
|
||||
{
|
||||
id: asset?.id,
|
||||
address,
|
||||
notForSale: asset?.listings === null,
|
||||
collectionName: asset?.collection?.name ?? undefined,
|
||||
collectionSymbol: asset?.collection?.image?.url ?? undefined,
|
||||
imageUrl: asset?.image?.url ?? undefined,
|
||||
animationUrl: asset?.animationUrl ?? undefined,
|
||||
// todo: fix the back/frontend discrepency here and drop the any
|
||||
marketplace: listing?.marketplace.toLowerCase() as any,
|
||||
name: asset?.name ?? undefined,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
susFlag: asset?.suspiciousFlag ?? undefined,
|
||||
sellorders: asset?.listings?.edges.map((listingNode) => {
|
||||
return {
|
||||
...listingNode.node,
|
||||
protocolParameters: listingNode.node.protocolParameters
|
||||
? JSON.parse(listingNode.node.protocolParameters.toString())
|
||||
: undefined,
|
||||
} as SellOrder
|
||||
}),
|
||||
smallImageUrl: asset?.smallImage?.url ?? undefined,
|
||||
tokenId,
|
||||
tokenType: (asset?.collection?.nftContracts && asset?.collection.nftContracts[0]?.standard) as TokenType,
|
||||
collectionIsVerified: asset?.collection?.isVerified ?? undefined,
|
||||
rarity: {
|
||||
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
|
||||
providers: asset?.rarities
|
||||
? asset?.rarities?.map((rarity) => {
|
||||
return useMemo(
|
||||
() => ({
|
||||
data: [
|
||||
{
|
||||
id: asset?.id,
|
||||
address,
|
||||
notForSale: asset?.listings === null,
|
||||
collectionName: asset?.collection?.name,
|
||||
collectionSymbol: asset?.collection?.image?.url,
|
||||
imageUrl: asset?.image?.url,
|
||||
animationUrl: asset?.animationUrl,
|
||||
marketplace: listing?.marketplace.toLowerCase() as unknown as Markets,
|
||||
name: asset?.name,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
susFlag: asset?.suspiciousFlag,
|
||||
sellorders: asset?.listings?.edges.map((listingNode) => {
|
||||
return {
|
||||
...listingNode.node,
|
||||
protocolParameters: listingNode.node.protocolParameters
|
||||
? JSON.parse(listingNode.node.protocolParameters.toString())
|
||||
: undefined,
|
||||
} as SellOrder
|
||||
}),
|
||||
smallImageUrl: asset?.smallImage?.url,
|
||||
tokenId,
|
||||
tokenType: asset?.collection?.nftContracts?.[0]?.standard,
|
||||
collectionIsVerified: asset?.collection?.isVerified,
|
||||
rarity: {
|
||||
primaryProvider: 'Rarity Sniper', // TODO update when backend adds more providers
|
||||
providers: asset?.rarities?.map((rarity) => {
|
||||
return {
|
||||
rank: rarity.rank ?? undefined,
|
||||
score: rarity.score ?? undefined,
|
||||
rank: rarity.rank,
|
||||
score: rarity.score,
|
||||
provider: 'Rarity Sniper',
|
||||
}
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
owner: { address: asset?.ownerAddress ?? '' },
|
||||
creator: {
|
||||
profile_img_url: asset?.creator?.profileImage?.url ?? '',
|
||||
address: asset?.creator?.address ?? '',
|
||||
},
|
||||
metadataUrl: asset?.metadataUrl ?? '',
|
||||
traits: asset?.traits?.map((trait) => {
|
||||
return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' }
|
||||
}),
|
||||
},
|
||||
{
|
||||
collectionDescription: collection?.description ?? undefined,
|
||||
collectionImageUrl: collection?.image?.url ?? undefined,
|
||||
collectionName: collection?.name ?? undefined,
|
||||
isVerified: collection?.isVerified ?? undefined,
|
||||
totalSupply: collection?.numAssets ?? undefined,
|
||||
twitterUrl: collection?.twitterName ?? undefined,
|
||||
discordUrl: collection?.discordUrl ?? undefined,
|
||||
externalUrl: collection?.homepageUrl ?? undefined,
|
||||
},
|
||||
]
|
||||
}),
|
||||
},
|
||||
ownerAddress: asset?.ownerAddress,
|
||||
creator: {
|
||||
profile_img_url: asset?.creator?.profileImage?.url ?? '',
|
||||
address: asset?.creator?.address ?? '',
|
||||
},
|
||||
metadataUrl: asset?.metadataUrl ?? '',
|
||||
traits: asset?.traits?.map((trait) => {
|
||||
return { trait_type: trait.name ?? '', trait_value: trait.value ?? '' }
|
||||
}),
|
||||
},
|
||||
{
|
||||
collectionDescription: collection?.description,
|
||||
collectionImageUrl: collection?.image?.url,
|
||||
collectionName: collection?.name,
|
||||
isVerified: collection?.isVerified,
|
||||
totalSupply: collection?.numAssets,
|
||||
twitterUrl: collection?.twitterName,
|
||||
discordUrl: collection?.discordUrl,
|
||||
externalUrl: collection?.homepageUrl,
|
||||
},
|
||||
],
|
||||
loading,
|
||||
}),
|
||||
[address, asset, collection, ethPrice, listing?.marketplace, loading, tokenId]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import { parseEther } from 'ethers/lib/utils'
|
||||
import { DEFAULT_WALLET_ASSET_QUERY_AMOUNT } from 'nft/components/profile/view/ProfilePage'
|
||||
import { WalletAsset } from 'nft/types'
|
||||
import gql from 'graphql-tag'
|
||||
import { GenieCollection, WalletAsset } from 'nft/types'
|
||||
import { wrapScientificNotation } from 'nft/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useLazyLoadQuery, usePaginationFragment, useQueryLoader } from 'react-relay'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { NftBalancePaginationQuery } from './__generated__/NftBalancePaginationQuery.graphql'
|
||||
import { NftBalanceQuery } from './__generated__/NftBalanceQuery.graphql'
|
||||
import { NftBalanceQuery_nftBalances$data } from './__generated__/NftBalanceQuery_nftBalances.graphql'
|
||||
import { NftAsset, useNftBalanceQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
const nftBalancePaginationQuery = graphql`
|
||||
fragment NftBalanceQuery_nftBalances on Query @refetchable(queryName: "NftBalancePaginationQuery") {
|
||||
gql`
|
||||
query NftBalance(
|
||||
$ownerAddress: String!
|
||||
$filter: NftBalancesFilterInput
|
||||
$first: Int
|
||||
$after: String
|
||||
$last: Int
|
||||
$before: String
|
||||
) {
|
||||
nftBalances(
|
||||
ownerAddress: $ownerAddress
|
||||
filter: $filter
|
||||
@@ -19,7 +22,7 @@ const nftBalancePaginationQuery = graphql`
|
||||
after: $after
|
||||
last: $last
|
||||
before: $before
|
||||
) @connection(key: "NftBalanceQuery_nftBalances") {
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
ownedAsset {
|
||||
@@ -99,43 +102,7 @@ const nftBalancePaginationQuery = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
const nftBalanceQuery = graphql`
|
||||
query NftBalanceQuery(
|
||||
$ownerAddress: String!
|
||||
$filter: NftBalancesFilterInput
|
||||
$first: Int
|
||||
$after: String
|
||||
$last: Int
|
||||
$before: String
|
||||
) {
|
||||
...NftBalanceQuery_nftBalances
|
||||
}
|
||||
`
|
||||
|
||||
type NftBalanceQueryAsset = NonNullable<
|
||||
NonNullable<NonNullable<NftBalanceQuery_nftBalances$data['nftBalances']>['edges']>[number]
|
||||
>
|
||||
|
||||
export function useLoadNftBalanceQuery(
|
||||
ownerAddress?: string,
|
||||
collectionAddress?: string | string[],
|
||||
tokenId?: string
|
||||
): void {
|
||||
const [, loadQuery] = useQueryLoader(nftBalanceQuery)
|
||||
useEffect(() => {
|
||||
if (ownerAddress) {
|
||||
loadQuery({
|
||||
ownerAddress,
|
||||
filter: tokenId
|
||||
? { assets: [{ address: collectionAddress, tokenId }] }
|
||||
: { addresses: Array.isArray(collectionAddress) ? collectionAddress : [collectionAddress] },
|
||||
first: tokenId ? 1 : DEFAULT_WALLET_ASSET_QUERY_AMOUNT,
|
||||
})
|
||||
}
|
||||
}, [ownerAddress, loadQuery, collectionAddress, tokenId])
|
||||
}
|
||||
|
||||
export function useNftBalanceQuery(
|
||||
export function useNftBalance(
|
||||
ownerAddress: string,
|
||||
collectionFilters?: string[],
|
||||
assetsFilter?: { address: string; tokenId: string }[],
|
||||
@@ -144,9 +111,8 @@ export function useNftBalanceQuery(
|
||||
last?: number,
|
||||
before?: string
|
||||
) {
|
||||
const queryData = useLazyLoadQuery<NftBalanceQuery>(
|
||||
nftBalanceQuery,
|
||||
{
|
||||
const { data, loading, fetchMore } = useNftBalanceQuery({
|
||||
variables: {
|
||||
ownerAddress,
|
||||
filter:
|
||||
assetsFilter && assetsFilter.length > 0
|
||||
@@ -161,14 +127,21 @@ export function useNftBalanceQuery(
|
||||
last,
|
||||
before,
|
||||
},
|
||||
{ fetchPolicy: 'store-or-network' }
|
||||
})
|
||||
|
||||
const hasNext = data?.nftBalances?.pageInfo?.hasNextPage
|
||||
const loadMore = useCallback(
|
||||
() =>
|
||||
fetchMore({
|
||||
variables: {
|
||||
after: data?.nftBalances?.pageInfo?.endCursor,
|
||||
},
|
||||
}),
|
||||
[data?.nftBalances?.pageInfo?.endCursor, fetchMore]
|
||||
)
|
||||
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<NftBalancePaginationQuery, any>(
|
||||
nftBalancePaginationQuery,
|
||||
queryData
|
||||
)
|
||||
const walletAssets: WalletAsset[] = data.nftBalances?.edges?.map((queryAsset: NftBalanceQueryAsset) => {
|
||||
const asset = queryAsset.node.ownedAsset
|
||||
|
||||
const walletAssets: WalletAsset[] | undefined = data?.nftBalances?.edges?.map((queryAsset) => {
|
||||
const asset = queryAsset?.node.ownedAsset as NonNullable<NftAsset>
|
||||
const ethPrice = parseEther(wrapScientificNotation(asset?.listings?.edges[0]?.node.price.value ?? 0)).toString()
|
||||
return {
|
||||
id: asset?.id,
|
||||
@@ -177,35 +150,32 @@ export function useNftBalanceQuery(
|
||||
notForSale: asset?.listings?.edges?.length === 0,
|
||||
animationUrl: asset?.animationUrl,
|
||||
susFlag: asset?.suspiciousFlag,
|
||||
priceInfo: asset?.listings
|
||||
? {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
}
|
||||
: undefined,
|
||||
priceInfo: {
|
||||
ETHPrice: ethPrice,
|
||||
baseAsset: 'ETH',
|
||||
baseDecimals: '18',
|
||||
basePrice: ethPrice,
|
||||
},
|
||||
name: asset?.name,
|
||||
tokenId: asset?.tokenId,
|
||||
asset_contract: {
|
||||
address: asset?.collection?.nftContracts?.[0]?.address,
|
||||
schema_name: asset?.collection?.nftContracts?.[0]?.standard,
|
||||
tokenType: asset?.collection?.nftContracts?.[0]?.standard,
|
||||
name: asset?.collection?.name,
|
||||
description: asset?.description,
|
||||
image_url: asset?.collection?.image?.url,
|
||||
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
|
||||
tokenType: asset?.collection?.nftContracts?.[0].standard,
|
||||
},
|
||||
collection: asset?.collection,
|
||||
collection: asset?.collection as unknown as GenieCollection,
|
||||
collectionIsVerified: asset?.collection?.isVerified,
|
||||
lastPrice: queryAsset.node.lastPrice?.value,
|
||||
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,
|
||||
basisPoints: queryAsset?.node?.listingFees?.[0]?.basisPoints ?? 0 / 10000,
|
||||
listing_date: asset?.listings?.edges?.[0]?.node?.createdAt,
|
||||
date_acquired: queryAsset.node.lastPrice?.timestamp,
|
||||
listing_date: asset?.listings?.edges?.[0]?.node?.createdAt?.toString(),
|
||||
date_acquired: queryAsset.node.lastPrice?.timestamp?.toString(),
|
||||
sellOrders: asset?.listings?.edges.map((edge: any) => edge.node),
|
||||
floor_sell_order_price: asset?.listings?.edges?.[0]?.node?.price?.value,
|
||||
}
|
||||
})
|
||||
return { walletAssets, hasNext, isLoadingNext, loadNext }
|
||||
return useMemo(() => ({ walletAssets, hasNext, loadMore, loading }), [hasNext, loadMore, loading, walletAssets])
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
|
||||
import { Chain, HistoryDuration } from './__generated__/TopTokens100Query.graphql'
|
||||
import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
|
||||
|
||||
export enum TimePeriod {
|
||||
HOUR,
|
||||
@@ -15,15 +15,15 @@ export enum TimePeriod {
|
||||
export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
return 'HOUR'
|
||||
return HistoryDuration.Hour
|
||||
case TimePeriod.DAY:
|
||||
return 'DAY'
|
||||
return HistoryDuration.Day
|
||||
case TimePeriod.WEEK:
|
||||
return 'WEEK'
|
||||
return HistoryDuration.Week
|
||||
case TimePeriod.MONTH:
|
||||
return 'MONTH'
|
||||
return HistoryDuration.Month
|
||||
case TimePeriod.YEAR:
|
||||
return 'YEAR'
|
||||
return HistoryDuration.Year
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,16 @@ export function isPricePoint(p: PricePoint | null): p is PricePoint {
|
||||
}
|
||||
|
||||
export const CHAIN_ID_TO_BACKEND_NAME: { [key: number]: Chain } = {
|
||||
[SupportedChainId.MAINNET]: 'ETHEREUM',
|
||||
[SupportedChainId.GOERLI]: 'ETHEREUM_GOERLI',
|
||||
[SupportedChainId.POLYGON]: 'POLYGON',
|
||||
[SupportedChainId.POLYGON_MUMBAI]: 'POLYGON',
|
||||
[SupportedChainId.CELO]: 'CELO',
|
||||
[SupportedChainId.CELO_ALFAJORES]: 'CELO',
|
||||
[SupportedChainId.ARBITRUM_ONE]: 'ARBITRUM',
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: 'ARBITRUM',
|
||||
[SupportedChainId.OPTIMISM]: 'OPTIMISM',
|
||||
[SupportedChainId.OPTIMISM_GOERLI]: 'OPTIMISM',
|
||||
[SupportedChainId.MAINNET]: Chain.Ethereum,
|
||||
[SupportedChainId.GOERLI]: Chain.EthereumGoerli,
|
||||
[SupportedChainId.POLYGON]: Chain.Polygon,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: Chain.Polygon,
|
||||
[SupportedChainId.CELO]: Chain.Celo,
|
||||
[SupportedChainId.CELO_ALFAJORES]: Chain.Celo,
|
||||
[SupportedChainId.ARBITRUM_ONE]: Chain.Arbitrum,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: Chain.Arbitrum,
|
||||
[SupportedChainId.OPTIMISM]: Chain.Optimism,
|
||||
[SupportedChainId.OPTIMISM_GOERLI]: Chain.Optimism,
|
||||
}
|
||||
|
||||
export function chainIdToBackendName(chainId: number | undefined) {
|
||||
@@ -53,15 +53,15 @@ export function chainIdToBackendName(chainId: number | undefined) {
|
||||
}
|
||||
|
||||
const URL_CHAIN_PARAM_TO_BACKEND: { [key: string]: Chain } = {
|
||||
ethereum: 'ETHEREUM',
|
||||
polygon: 'POLYGON',
|
||||
celo: 'CELO',
|
||||
arbitrum: 'ARBITRUM',
|
||||
optimism: 'OPTIMISM',
|
||||
ethereum: Chain.Ethereum,
|
||||
polygon: Chain.Polygon,
|
||||
celo: Chain.Celo,
|
||||
arbitrum: Chain.Arbitrum,
|
||||
optimism: Chain.Optimism,
|
||||
}
|
||||
|
||||
export function validateUrlChainParam(chainName: string | undefined) {
|
||||
return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : 'ETHEREUM'
|
||||
return chainName && URL_CHAIN_PARAM_TO_BACKEND[chainName] ? URL_CHAIN_PARAM_TO_BACKEND[chainName] : Chain.Ethereum
|
||||
}
|
||||
|
||||
export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
|
||||
@@ -72,7 +72,7 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
|
||||
OPTIMISM: SupportedChainId.OPTIMISM,
|
||||
}
|
||||
|
||||
export const BACKEND_CHAIN_NAMES: Chain[] = ['ETHEREUM', 'POLYGON', 'OPTIMISM', 'ARBITRUM', 'CELO']
|
||||
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
|
||||
|
||||
export function isValidBackendChainName(chainName: string | undefined): chainName is Chain {
|
||||
if (!chainName) return false
|
||||
@@ -95,7 +95,11 @@ export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?:
|
||||
}
|
||||
}
|
||||
|
||||
export function unwrapToken<T extends { address: string | null } | null>(chainId: number, token: T): T {
|
||||
export function unwrapToken<
|
||||
T extends {
|
||||
address?: string | null | undefined
|
||||
} | null
|
||||
>(chainId: number, token: T): T {
|
||||
if (!token?.address) return token
|
||||
|
||||
const address = token.address.toLowerCase()
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchQuery } from 'react-relay'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import gql from 'graphql-tag'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import type {
|
||||
AllV3TicksQuery as AllV3TicksQueryType,
|
||||
AllV3TicksQuery$data,
|
||||
} from './__generated__/AllV3TicksQuery.graphql'
|
||||
import environment from './RelayEnvironment'
|
||||
import { AllV3TicksQuery } from './__generated__/types-and-hooks'
|
||||
import { apolloClient } from './apollo'
|
||||
|
||||
const query = graphql`
|
||||
query AllV3TicksQuery($poolAddress: String!, $skip: Int!) {
|
||||
const query = gql`
|
||||
query AllV3Ticks($poolAddress: String!, $skip: Int!) {
|
||||
ticks(first: 1000, skip: $skip, where: { poolAddress: $poolAddress }, orderBy: tickIdx) {
|
||||
tick: tickIdx
|
||||
liquidityNet
|
||||
@@ -21,33 +16,29 @@ const query = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
export type Ticks = AllV3TicksQuery$data['ticks']
|
||||
export type Ticks = AllV3TicksQuery['ticks']
|
||||
export type TickData = Ticks[number]
|
||||
|
||||
export default function useAllV3TicksQuery(poolAddress: string | undefined, skip: number, interval: number) {
|
||||
const [data, setData] = useState<AllV3TicksQuery$data | null>(null)
|
||||
const [error, setError] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const chainId = useAppSelector((state) => state.application.chainId)
|
||||
const {
|
||||
data,
|
||||
loading: isLoading,
|
||||
error,
|
||||
} = useQuery(query, {
|
||||
variables: {
|
||||
poolAddress: poolAddress?.toLowerCase(),
|
||||
skip,
|
||||
},
|
||||
pollInterval: interval,
|
||||
client: apolloClient,
|
||||
})
|
||||
|
||||
const refreshData = useCallback(() => {
|
||||
if (poolAddress && chainId) {
|
||||
fetchQuery<AllV3TicksQueryType>(environment, query, {
|
||||
poolAddress: poolAddress.toLowerCase(),
|
||||
skip,
|
||||
}).subscribe({
|
||||
next: setData,
|
||||
error: setError,
|
||||
complete: () => setIsLoading(false),
|
||||
})
|
||||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [poolAddress, skip, chainId])
|
||||
|
||||
// Trigger fetch on first load
|
||||
useEffect(refreshData, [refreshData, poolAddress, skip])
|
||||
|
||||
useInterval(refreshData, interval, true)
|
||||
return { error, isLoading, data }
|
||||
return useMemo(
|
||||
() => ({
|
||||
error,
|
||||
isLoading,
|
||||
data,
|
||||
}),
|
||||
[data, error, isLoading]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import graphql from 'babel-plugin-relay/macro'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchQuery } from 'react-relay'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { ApolloError, useQuery } from '@apollo/client'
|
||||
import gql from 'graphql-tag'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import type {
|
||||
FeeTierDistributionQuery as FeeTierDistributionQueryType,
|
||||
FeeTierDistributionQuery$data,
|
||||
} from './__generated__/FeeTierDistributionQuery.graphql'
|
||||
import environment from './RelayEnvironment'
|
||||
import { FeeTierDistributionQuery } from './__generated__/types-and-hooks'
|
||||
import { apolloClient } from './apollo'
|
||||
|
||||
const query = graphql`
|
||||
query FeeTierDistributionQuery($token0: String!, $token1: String!) {
|
||||
const query = gql`
|
||||
query FeeTierDistribution($token0: String!, $token1: String!) {
|
||||
_meta {
|
||||
block {
|
||||
number
|
||||
@@ -42,28 +37,26 @@ export default function useFeeTierDistributionQuery(
|
||||
token0: string | undefined,
|
||||
token1: string | undefined,
|
||||
interval: number
|
||||
) {
|
||||
const [data, setData] = useState<FeeTierDistributionQuery$data | null>(null)
|
||||
const [error, setError] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const chainId = useAppSelector((state) => state.application.chainId)
|
||||
): { error: ApolloError | undefined; isLoading: boolean; data: FeeTierDistributionQuery } {
|
||||
const {
|
||||
data,
|
||||
loading: isLoading,
|
||||
error,
|
||||
} = useQuery(query, {
|
||||
variables: {
|
||||
token0: token0?.toLowerCase(),
|
||||
token1: token1?.toLowerCase(),
|
||||
},
|
||||
pollInterval: interval,
|
||||
client: apolloClient,
|
||||
})
|
||||
|
||||
const refreshData = useCallback(() => {
|
||||
if (token0 && token1 && chainId) {
|
||||
fetchQuery<FeeTierDistributionQueryType>(environment, query, {
|
||||
token0: token0.toLowerCase(),
|
||||
token1: token1.toLowerCase(),
|
||||
}).subscribe({
|
||||
next: setData,
|
||||
error: setError,
|
||||
complete: () => setIsLoading(false),
|
||||
})
|
||||
}
|
||||
}, [token0, token1, chainId])
|
||||
|
||||
// Trigger fetch on first load
|
||||
useEffect(refreshData, [refreshData, token0, token1])
|
||||
|
||||
useInterval(refreshData, interval, true)
|
||||
return { error, isLoading, data }
|
||||
return useMemo(
|
||||
() => ({
|
||||
error,
|
||||
isLoading,
|
||||
data,
|
||||
}),
|
||||
[data, error, isLoading]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Environment, Network, RecordSource, Store } from 'relay-runtime'
|
||||
|
||||
import fetchGraphQL from './fetchGraphQL'
|
||||
|
||||
// Export a singleton instance of Relay Environment configured with our network function:
|
||||
export default new Environment({
|
||||
network: Network.create(fetchGraphQL),
|
||||
store: new Store(new RecordSource()),
|
||||
})
|
||||
4750
src/graphql/thegraph/__generated__/types-and-hooks.ts
generated
Normal file
40
src/graphql/thegraph/apollo.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApolloClient, ApolloLink, concat, HttpLink, InMemoryCache } from '@apollo/client'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
|
||||
import store, { AppState } from '../../state/index'
|
||||
|
||||
const CHAIN_SUBGRAPH_URL: Record<number, string> = {
|
||||
[SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
|
||||
[SupportedChainId.RINKEBY]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
|
||||
|
||||
[SupportedChainId.ARBITRUM_ONE]: 'https://api.thegraph.com/subgraphs/name/ianlapham/arbitrum-minimal',
|
||||
|
||||
[SupportedChainId.OPTIMISM]: 'https://api.thegraph.com/subgraphs/name/ianlapham/optimism-post-regenesis',
|
||||
|
||||
[SupportedChainId.POLYGON]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-polygon',
|
||||
|
||||
[SupportedChainId.CELO]: 'https://api.thegraph.com/subgraphs/name/jesse-sawa/uniswap-celo',
|
||||
}
|
||||
|
||||
const httpLink = new HttpLink({ uri: CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET] })
|
||||
|
||||
// This middleware will allow us to dynamically update the uri for the requests based off chainId
|
||||
// For more information: https://www.apollographql.com/docs/react/networking/advanced-http-networking/
|
||||
const authMiddleware = new ApolloLink((operation, forward) => {
|
||||
// add the authorization to the headers
|
||||
const chainId = (store.getState() as AppState).application.chainId
|
||||
|
||||
operation.setContext(() => ({
|
||||
uri:
|
||||
chainId && CHAIN_SUBGRAPH_URL[chainId]
|
||||
? CHAIN_SUBGRAPH_URL[chainId]
|
||||
: CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET],
|
||||
}))
|
||||
|
||||
return forward(operation)
|
||||
})
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: concat(authMiddleware, httpLink),
|
||||
})
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Helpful Resources
|
||||
* https://github.com/sibelius/create-react-app-relay-modern/blob/master/src/relay/fetchQuery.js
|
||||
* https://github.com/relay-tools/relay-compiler-language-typescript/blob/master/example/ts/app.tsx
|
||||
*/
|
||||
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { Variables } from 'react-relay'
|
||||
import { GraphQLResponse, ObservableFromValue, RequestParameters } from 'relay-runtime'
|
||||
|
||||
import store, { AppState } from '../../state/index'
|
||||
|
||||
const CHAIN_SUBGRAPH_URL: Record<number, string> = {
|
||||
[SupportedChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
|
||||
[SupportedChainId.RINKEBY]: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3',
|
||||
|
||||
[SupportedChainId.ARBITRUM_ONE]: 'https://api.thegraph.com/subgraphs/name/ianlapham/arbitrum-minimal',
|
||||
|
||||
[SupportedChainId.OPTIMISM]: 'https://api.thegraph.com/subgraphs/name/ianlapham/optimism-post-regenesis',
|
||||
|
||||
[SupportedChainId.POLYGON]: 'https://api.thegraph.com/subgraphs/name/ianlapham/uniswap-v3-polygon',
|
||||
|
||||
[SupportedChainId.CELO]: 'https://api.thegraph.com/subgraphs/name/jesse-sawa/uniswap-celo',
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-type': 'application/json',
|
||||
}
|
||||
|
||||
// Define a function that fetches the results of a request (query/mutation/etc)
|
||||
// and returns its results as a Promise:
|
||||
const fetchQuery = (params: RequestParameters, variables: Variables): ObservableFromValue<GraphQLResponse> => {
|
||||
const chainId = (store.getState() as AppState).application.chainId
|
||||
|
||||
const subgraphUrl =
|
||||
chainId && CHAIN_SUBGRAPH_URL[chainId] ? CHAIN_SUBGRAPH_URL[chainId] : CHAIN_SUBGRAPH_URL[SupportedChainId.MAINNET]
|
||||
|
||||
const body = JSON.stringify({
|
||||
query: params.text, // GraphQL text from input
|
||||
variables,
|
||||
})
|
||||
|
||||
const response = fetch(subgraphUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body,
|
||||
}).then((res) => res.json())
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export default fetchQuery
|
||||
@@ -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) {
|
||||
|
||||