Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fca93af230 | ||
|
|
4235b57cd8 | ||
|
|
cb362f1b2c | ||
|
|
a4d61d8eaa | ||
|
|
310623b948 | ||
|
|
b7303fb9c0 | ||
|
|
4fbb8e9117 | ||
|
|
f0502bfc33 | ||
|
|
98f4af55c9 | ||
|
|
08b8bdd769 | ||
|
|
1283199d0d | ||
|
|
c1fff5ea49 | ||
|
|
94adc449a1 | ||
|
|
4b24e5f754 | ||
|
|
e0a531e538 | ||
|
|
8cef1ca0f7 | ||
|
|
088f1d9ae4 | ||
|
|
89a7d98b41 | ||
|
|
0076fdc65b | ||
|
|
48b4a533c3 | ||
|
|
0b66fde26c | ||
|
|
5788385951 | ||
|
|
0891e67528 | ||
|
|
b319acd9c4 | ||
|
|
05977f950b | ||
|
|
5ac36d4156 | ||
|
|
fb998706c2 | ||
|
|
c45492c890 | ||
|
|
41219b435f | ||
|
|
e1321843de | ||
|
|
0baa8a1fff | ||
|
|
f2a3b66357 | ||
|
|
f2af46037e | ||
|
|
20a06c9b5a | ||
|
|
8ef54d41b6 | ||
|
|
1cdddd1321 | ||
|
|
f834af69fe | ||
|
|
08cd4bec41 | ||
|
|
63ac64f470 | ||
|
|
72686f1e32 | ||
|
|
c07359362f | ||
|
|
ed58c39bdc | ||
|
|
5d2254be27 |
1
.env
1
.env
@@ -11,3 +11,4 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
|
||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
||||
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
|
||||
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
|
||||
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -8,7 +8,7 @@ runs:
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 18
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
|
||||
4
.github/workflows/2-deploy-to-staging.yml
vendored
4
.github/workflows/2-deploy-to-staging.yml
vendored
@@ -27,10 +27,6 @@ jobs:
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_STAGING: 1
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Update Cloudflare Pages deployment
|
||||
id: pages-deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
|
||||
5
.github/workflows/4-deploy-to-prod.yml
vendored
5
.github/workflows/4-deploy-to-prod.yml
vendored
@@ -74,11 +74,6 @@ jobs:
|
||||
|
||||
${{ steps.github-tag-action.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
id: pages-deployment
|
||||
|
||||
123
.github/workflows/release.yaml
vendored
123
.github/workflows/release.yaml
vendored
@@ -1,123 +0,0 @@
|
||||
name: Release
|
||||
on:
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
wait-on-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: unit-tests
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: unit-tests
|
||||
- id: cypress-tests
|
||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: cypress-tests
|
||||
- if: steps.unit-tests.outputs.conclusion != 'success' || steps.cypress-tests.outputs.conclusion != 'success'
|
||||
run: exit 1
|
||||
|
||||
tag:
|
||||
needs: wait-on-tests
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_tag: ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github-tag-action.outputs.changelog }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Bump and tag
|
||||
id: github-tag-action
|
||||
uses: mathieudutour/github-tag-action@v6.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: .*
|
||||
default_bump: patch
|
||||
|
||||
release:
|
||||
needs: tag
|
||||
if: ${{ needs.tag.outputs.new_tag != null }}
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: release
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: pinata
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||
with:
|
||||
pin-name: Uniswap ${{ needs.tag.outputs.new_tag }}
|
||||
path: './build'
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Pin to Crust
|
||||
uses: crustio/ipfs-crust-action@v2.0.3
|
||||
continue-on-error: true
|
||||
timeout-minutes: 2
|
||||
with:
|
||||
cid: ${{ steps.pinata.outputs.hash }}
|
||||
seeds: ${{ secrets.CRUST_SEEDS }}
|
||||
|
||||
- name: Convert CIDv0 to CIDv1
|
||||
id: convert-cidv0
|
||||
uses: uniswap/convert-cidv0-cidv1@v1.0.0
|
||||
with:
|
||||
cidv0: ${{ steps.pinata.outputs.hash }}
|
||||
|
||||
- name: Release
|
||||
uses: actions/create-release@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ needs.tag.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.tag.outputs.new_tag }}
|
||||
body: |
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.pinata.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert-cidv0.outputs.cidv1 }}`
|
||||
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
|
||||
**You should always use an IPFS gateway that enforces origin separation**, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
Your Uniswap settings are never remembered across different URLs.
|
||||
|
||||
IPFS gateways:
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.pinata.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
|
||||
|
||||
${{ needs.tag.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: production
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -78,7 +78,6 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: unit-tests
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
@@ -100,7 +99,7 @@ jobs:
|
||||
- run: yarn build:e2e
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-e2e
|
||||
path: build
|
||||
@@ -166,14 +165,11 @@ jobs:
|
||||
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
|
||||
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
|
||||
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: e2e-tests
|
||||
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
|
||||
21
codecov.yml
21
codecov.yml
@@ -9,17 +9,38 @@ ignore:
|
||||
- "**/constants/**/*"
|
||||
- "constants/**/*"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
# Omit merging unit/e2e reports into the defaults, as it is nonsensical.
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
flag_management:
|
||||
default_rules:
|
||||
statuses:
|
||||
- type: project
|
||||
target: auto
|
||||
threshold: 1%
|
||||
# Adjust the base when removing code to avoid penalizing tech debt payback / dead code removal.
|
||||
removed_code_behavior: adjust_base
|
||||
if_ci_failed: error
|
||||
- type: patch
|
||||
target: 80%
|
||||
individual_flags:
|
||||
- name: unit-tests
|
||||
- name: e2e-tests
|
||||
# Wait until all machines have reported coverage - e2e tests run across 4 machines.
|
||||
after_n_builds: 4
|
||||
statuses:
|
||||
- type: patch
|
||||
target: 0%
|
||||
|
||||
comment:
|
||||
layout: flags
|
||||
# Wait until all machines have reported coverage - e2e tests run across 4 machines + unit tests across 1.
|
||||
after_n_builds: 5
|
||||
hide_comment_details: false
|
||||
|
||||
github_checks:
|
||||
# Turn off GitHub Check annotations, as they make it more difficult to review code.
|
||||
annotations: false
|
||||
|
||||
@@ -9,6 +9,7 @@ export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
|
||||
retries: { runMode: 2 },
|
||||
videoCompression: false,
|
||||
e2e: {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
|
||||
202
cypress/README.md
Normal file
202
cypress/README.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# e2e testing with Cypress
|
||||
|
||||
End-to-end tests are run through [Cypress](https://docs.cypress.io/api/table-of-contents/), which runs tests in a real browser. Cypress is a little different than other testing frameworks, and e2e tests are a little different than unit tests, so this directory has its own set of patterns, idioms, and best practices. Not only that, but we're testing against a forked blockchain, not just against typical Web APIs, so we have unique flows that you may not have seen elsewhere.
|
||||
|
||||
## Running your first e2e tests
|
||||
|
||||
Cypress tests run against a local server, so you'll need to run the application locally at the same time. The fastest way to run e2e tests is to use your dev server: `yarn start`.
|
||||
|
||||
Open cypress at the same time with `yarn cypress:open`. You should do this from another window or tab, so that you can continue to see any typechecking/linting warnings from `yarn start`.
|
||||
|
||||
Cypress opens its own instance of Chrome, with a list of "E2E specs" for you to select. When you're developing locally, you usually only want to run one spec file at a time. Select your spec by clicking on the filename and it will run.
|
||||
|
||||
## Glossary
|
||||
|
||||
#### spec
|
||||
Cypress considers each file a separate spec, or collection of tests.
|
||||
Specs are always run as a whole through `yarn cypress:open` or on the same machine through CI.
|
||||
|
||||
#### Thenable
|
||||
Cypress queues commands to run in the browser using [Thenables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables), not Promises.
|
||||
For this reason, you should not use `async/await` syntax in Cypress unless it is wholly-contained in a `cy.then` function argument.
|
||||
|
||||
## Writing your first e2e test
|
||||
|
||||
_For an excellent treatment on tests, check out the [Cypress Fundamentals](https://learn.cypress.io/cypress-fundamentals/how-to-write-a-test) course._
|
||||
_While some of that will be paraphrased here, this should be sufficient to get you started:_
|
||||
|
||||
### What is a test?
|
||||
|
||||
Cypress tests are just like any other test: you should set up an initial state, execute an action, and verify the action's consequence. This is codified in the AAA (Arrange-Act-Assert) pattern, and you'll see this in most of our tests. In _our_ case, it plays out as:
|
||||
|
||||
1. Arrange: Visit a page, eg `cy.visit('/swap')`, and set up the state, on the blockchain and the page.
|
||||
2. Act: Initiate your action under test, eg `initiateSwap()`
|
||||
3. Assert: Verify that the action has occured, eg `// Verify swap has occured`
|
||||
|
||||
You'll usually see the setup, followed by a newline, followed by assertions with comments stating what they are asserting.
|
||||
Because Cypress tests are translated into user actions, it may be hard to follow the action being described. You should use comments liberally to describe what you are doing and what you intend to test, to make tests easier to read and maintain in the future.
|
||||
|
||||
### Thinking about tests: queuing up a sequence of commands
|
||||
|
||||
Cypress uses `Thenable`s to achieve "command chaining". A test is described as a series of commands, which are only executed once the previous command in the chain has executed.
|
||||
|
||||
```
|
||||
cy.visit('/swap')
|
||||
cy.contains('Select token').click()
|
||||
cy.contains('DAI').click()
|
||||
```
|
||||
|
||||
In this example, `cy.contains('Select token').click()` is queued up right away (all the code is synchronous), but it will not execute until `/swap` has loaded (all the commands are chained); and `click()` will not execute until `Select token` has been found.
|
||||
|
||||
This becomes more relevant as you work with data on the blockchain, as you'll need to load it at the correct time, _after_ it's been modified by the application:
|
||||
|
||||
```
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// wait for the transaction to be executed
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// BAD: This will get the balance _before_ the other queued actions have executed.
|
||||
const balance = await hardhat.getBalance(hardhat.wallet, USDC_MAINNET)
|
||||
cy.wrap(balance).should('deep.equal', expectedBalance)
|
||||
})
|
||||
```
|
||||
|
||||
```
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// wait for the transaction to be executed
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// GOOD: cy.then chains the command so that it runs _after_ executing the swap
|
||||
cy.hardhat()
|
||||
.then((hardhat) => hardhat.getBalance(hardhat.wallet, USDC_MAINNET))
|
||||
.should('deep.equal', expectedBalance)
|
||||
})
|
||||
```
|
||||
|
||||
### Working with the blockchain (ie hardhat)
|
||||
|
||||
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
|
||||
Currently, tests using hardhat must opt-in in when they load the page: `cy.visit('/swap', { ethereum: 'hardhat' })`. This will not be necessary once we've totally migrated to hardhat.
|
||||
|
||||
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.
|
||||
|
||||
The hardhat integration has built-in utilities to let you modify and assert on balances, approvals, and permits, and should be fully typed. Check it out at [Uniswap/cypress-hardhat](https://github.com/Uniswap/cypress-hardhat).
|
||||
|
||||
### Asserting on wallet methods
|
||||
|
||||
Wallet methods to hardhat are all aliased. If you'd like to assert that a method was sent to the wallet, you can do so using the method name, prefixed with `@`:
|
||||
|
||||
```
|
||||
// Asserts that `eth_sendRawTransaction` was sent to the wallet.
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
```
|
||||
|
||||
Sometimes, you may want a method to _fail_. In this case, you can stub it, but you should disable logging to avoid spamming the test:
|
||||
|
||||
```
|
||||
// Stub calls to eth_signTypedData_v4 and fail them
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Note the closure to keep signTypedDataStub in scope. Using closures instead of variables (eg let) helps prevent misuse of chaining.
|
||||
const signTypedDataStub = cy.stub(hardhat.provider, 'send').log(false)
|
||||
signTypedDataStub.withArgs('eth_signTypedData_v4).rejects(USER_REJECTION)
|
||||
signTypedDataStub.callThrough() // allws other methods to call through to hardhat
|
||||
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify the call occured
|
||||
// Note the call to cy.wrap to correctly queue the chained command. Without this, the test would occur before the stub is called.
|
||||
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||
|
||||
// Restore the stub
|
||||
// note the call to cy.then to correctly queue the chained command. Without this, the stub would be restored immediately.
|
||||
cy.then(() => permitApprovalStub.restore())
|
||||
})
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
<!-- Best practices should all be labeled using H3, with the rationale italicized at the end of the section. -->
|
||||
<!-- Best practice 🤣 is to also include an example before your rationale. -->
|
||||
|
||||
### Spec / test grouping
|
||||
|
||||
Each spec should be specific to one route, _not_ one functional behavior.
|
||||
For example, `token-details.test.ts` is separated from `swap.test.ts`.
|
||||
|
||||
If a route has different functional behaviors, that route should become a directory name, and its spec should be split.
|
||||
For example, `swap.test.ts` may be split into `swap/swap.test.ts`, `swap/wrap.test.ts`, `swap/permit2.test.ts`.
|
||||
|
||||
_This prevents specs from growing too large, which is important because they are always run as a whole locally and on the same machine through CI. If a spec grows too large, it will have a longer local feedback loop, and it will become the bottleneck for CI test runtime._
|
||||
|
||||
_Similarly, avoid actions outside the scope of your spec, as it will cause total testing time to increase._
|
||||
|
||||
### Use closures instead of variables
|
||||
|
||||
Avoid usage of `let`, instead assigning a constant. In practice, this means using closures for your variables:
|
||||
|
||||
```javascript
|
||||
let badVariable
|
||||
|
||||
cy.hardhat({ automine: false })
|
||||
.then((hardhat) => cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)))
|
||||
.then((initialBalance) => {
|
||||
// Do not assign to a variable outside of your closure!
|
||||
badVariable = initialBalance // <-- bad!
|
||||
|
||||
// Use initial balance here, within the closure.
|
||||
})
|
||||
|
||||
cy.get('.class-name').then((el) => {
|
||||
// Do not use badVariable here! It may have changed value due to the queued async nature of Cypress.
|
||||
expect(el).should('contain', badVariable) // <-- bad!
|
||||
})
|
||||
```
|
||||
|
||||
_This prevents misuse of a not-yet-initialized variable, or a variable that has changed as the test progresses._
|
||||
|
||||
### Prefer selecting elements using on-screen text over data-testid attributes
|
||||
|
||||
When selecting components (eg with `cy.get`), prefer defining your selector with visible UI. Sometimes this is not possible (eg if the text is duplicated on-screen), and you'll need to add a `data-testid` property.
|
||||
|
||||
_Defining tests using visual fields helps ensure that we don't break them. `data-testid` may select an element that is only selectable programmatically, and should be used only when necessary, as its use may cover up UI breakages._
|
||||
|
||||
_You'll still want to use `data-testid` in cases where the text is rendered in multiple containers and you need to select the correct one, or where the component doesn't render predictable text output._
|
||||
|
||||
### Avoid branching logic
|
||||
|
||||
Do not write tests that rely on if-statements or conditionals. Do not create helper methods which do more than one thing, and rely on branching logic to apply to different but similar situations.
|
||||
|
||||
_Tests should be readable and simple. Branching logic makes it harder to reason about tests, and may hide otherwise flaky or ill-defined behaviors._
|
||||
|
||||
_Similarly, you should avoid complicated for-loops. Sometimes, for simple repetition, for-loops are ok._
|
||||
|
||||
### Avoid spamming the console
|
||||
|
||||
It is ok to include logging while you are developing a test, but that logging should be removed if it is not needed to debug (potential) errors.
|
||||
|
||||
For example, stubbing a wallet method will result in dumping a hex string (the calldata) to the log. Instead, suppress logging from methods which you know will flood the log.
|
||||
|
||||
```javascript
|
||||
cy.stub(hardhat.wallet, 'sendTransaction')
|
||||
.log(false) // <-- suppresses logs from this stub
|
||||
.rejects(new Error('user cancelled'))
|
||||
```
|
||||
|
||||
_Unnecessary logs it makes it harder to reason about a test overall._
|
||||
|
||||
### Name helper methods using transitive verbs
|
||||
|
||||
Name helper methods using "action verbs": `expectsThisToHappen`, not `expectThisToHappen`; `selectsToken(token: string)`, not `selectAToken(token: string)`.
|
||||
|
||||
_This makes your tests read more naturally, and makes it easier to follow given existing `should` syntax._
|
||||
@@ -2,7 +2,7 @@
|
||||
describe('Link', () => {
|
||||
it('should update route', () => {
|
||||
cy.viewport(2000, 1600)
|
||||
cy.visit('/')
|
||||
cy.visit('/swap')
|
||||
cy.contains('Pool').click()
|
||||
cy.get('[data-cy="join-pool-button"]').should('exist')
|
||||
})
|
||||
|
||||
@@ -28,6 +28,27 @@ describe('Mini Portfolio account drawer', () => {
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('fetches account information', () => {
|
||||
// Open the mini portfolio
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/tokens.json' })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Verify that wallet state loads correctly
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Tokens')
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (201)')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/nfts.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty')
|
||||
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Contract Interaction')
|
||||
})
|
||||
|
||||
it('refetches balances when account changes', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const accountA = hardhat.wallets[0].address
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('mini-portfolio activity history', () => {
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
|
||||
@@ -4,11 +4,8 @@ const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('should load nft leaderboard', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('nft-nav')).first().click()
|
||||
cy.get(getTestSelector('nft-nav')).first().should('exist')
|
||||
cy.get(getTestSelector('nft-nav')).first().click()
|
||||
@@ -49,15 +46,11 @@ describe('Testing nfts', () => {
|
||||
cy.get(getTestSelector('nft-bag')).should('exist')
|
||||
})
|
||||
|
||||
it('should navigate to the owned nfts page', () => {
|
||||
it('should navigate to and from the owned nfts page', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('nft-view-self-nfts')).click()
|
||||
})
|
||||
|
||||
it('should close the sidebar when navigating to NFT details', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,212 +5,195 @@ import { getTestSelector } from '../utils'
|
||||
|
||||
/** Initiates a swap. */
|
||||
function initiateSwap() {
|
||||
// The swap button is re-rendered once enable, so we must wait until the original button is not disabled to re-select the appropriate button.
|
||||
// The swap button is re-rendered once enabled, so we must wait until the original button is not disabled to re-select the appropriate button.
|
||||
cy.get('#swap-button').should('not.be.disabled')
|
||||
// Completes the swap.
|
||||
cy.get('#swap-button').click()
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
cy.contains('Confirm swap').click()
|
||||
}
|
||||
|
||||
describe('Permit2', () => {
|
||||
// The same tokens & swap-amount combination is used for all permit2 tests.
|
||||
// The same tokens are used for all permit2 tests.
|
||||
const INPUT_TOKEN = DAI
|
||||
const OUTPUT_TOKEN = USDC_MAINNET
|
||||
const TEST_BALANCE_INCREMENT = 0.01
|
||||
|
||||
beforeEach(() => {
|
||||
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
|
||||
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
})
|
||||
cy.get('#swap-currency-input .token-amount-input').type(TEST_BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01')
|
||||
})
|
||||
|
||||
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
|
||||
function expectTokenAllowanceForPermit2ToBeMax() {
|
||||
// check token approval
|
||||
return cy
|
||||
.hardhat()
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }))
|
||||
.should('deep.equal', MaxUint256)
|
||||
}
|
||||
|
||||
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
|
||||
function expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime: number) {
|
||||
return cy
|
||||
.hardhat()
|
||||
function expectPermit2AllowanceForUniversalRouterToBeMax() {
|
||||
cy.hardhat()
|
||||
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
|
||||
.then((allowance) => {
|
||||
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
|
||||
const THIRTY_DAYS_SECONDS = 2_592_000
|
||||
const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
|
||||
})
|
||||
}
|
||||
|
||||
it('swaps when user has already approved token and permit2', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
||||
describe('approval process (with intermediate screens)', () => {
|
||||
// Turn off automine so that intermediate screens are available to assert on.
|
||||
beforeEach(() => cy.hardhat({ automine: false }))
|
||||
|
||||
it('swaps after completing full permit2 approval process', () => {
|
||||
initiateSwap()
|
||||
|
||||
// verify that the modal retains its state when the window loses focus
|
||||
cy.window().trigger('blur')
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending DAI on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.contains('Allow DAI to be used for swapping')
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax()
|
||||
})
|
||||
|
||||
it('swaps with existing permit approval and missing token approval', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN })
|
||||
await hardhat.mine()
|
||||
})
|
||||
initiateSwap()
|
||||
|
||||
// Verify token approval
|
||||
cy.contains('Enable spending DAI on Uniswap')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
initiateSwap()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
// Verifies that there is a successful swap notification.
|
||||
cy.contains('Swapped').should('exist')
|
||||
})
|
||||
|
||||
it('swaps after completing full permit2 approval process', () => {
|
||||
cy.hardhat().then(({ provider }) => {
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
it('swaps when user has already approved token and permit2', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.contains('Approved').should('exist')
|
||||
|
||||
cy.contains('Allow DAI to be used for swapping').should('exist')
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
// Verify transaction
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
it('swaps after handling user rejection of both approval and signature', () => {
|
||||
const USER_REJECTION = { code: 4001 }
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction')
|
||||
tokenApprovalStub.rejects(USER_REJECTION) // reject token approval
|
||||
const permitApprovalStub = cy.stub(hardhat.provider, 'send')
|
||||
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // reject permit approval
|
||||
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
||||
|
||||
// Reject token approval
|
||||
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(false)
|
||||
tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval
|
||||
initiateSwap()
|
||||
|
||||
// tokenApprovalStub should reject here, and the modal should revert to the review state.
|
||||
cy.contains('Review swap').should('be.visible')
|
||||
|
||||
cy.then(() => {
|
||||
// The user is now allowing approval, but the permit2 signature will be rejected by the user (permitApprovalStub).
|
||||
tokenApprovalStub.restore() // allow token approval
|
||||
})
|
||||
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.contains('Approved').should('exist')
|
||||
|
||||
// permitApprovalStub should reject here, and the modal should revert to the review state.
|
||||
// Verify token approval rejection
|
||||
cy.wrap(tokenApprovalStub).should('be.calledOnce')
|
||||
cy.contains('Review swap')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
permitApprovalStub.restore() // allow permit approval
|
||||
})
|
||||
|
||||
cy.get(getTestSelector('confirm-swap-button')).click()
|
||||
// Allow token approval
|
||||
cy.then(() => tokenApprovalStub.restore())
|
||||
|
||||
// The swap should now be able to proceed, as the permit2 signature will be accepted by the user.
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Reject permit2 approval
|
||||
const permitApprovalStub = cy.stub(hardhat.provider, 'send').log(false)
|
||||
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // rejects permit approval
|
||||
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify token approval
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectTokenAllowanceForPermit2ToBeMax()
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
})
|
||||
|
||||
it('swaps with existing token approval and missing permit approval', () => {
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
cy.then(() => initiateSwap())
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
// Verify permit2 approval rejection
|
||||
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
|
||||
cy.contains('Review swap')
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Allow permit2 approval
|
||||
cy.then(() => permitApprovalStub.restore())
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
})
|
||||
|
||||
it('swaps with existing permit approval and missing token approval', () => {
|
||||
cy.hardhat().then(({ approval, wallet }) => approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }))
|
||||
cy.then(() => {
|
||||
initiateSwap()
|
||||
})
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval is expired', () => {
|
||||
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
||||
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance)
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
})
|
||||
cy.then(() => {
|
||||
initiateSwap()
|
||||
})
|
||||
cy.then(() => {
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval amount is too low', () => {
|
||||
const smallAllowance = { amount: 1 }
|
||||
|
||||
cy.hardhat().then(({ approval, wallet, provider }) => {
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance)
|
||||
cy.spy(provider, 'send').as('permitApprovalSpy')
|
||||
initiateSwap()
|
||||
const approvalTime = Date.now()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
cy.get('@permitApprovalSpy').should('have.been.calledWith', 'eth_signTypedData_v4')
|
||||
// Verify permit2 approval
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax()
|
||||
})
|
||||
})
|
||||
|
||||
it('prompts token approval when existing approval amount is too low', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => {
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1)
|
||||
})
|
||||
.then(() => {
|
||||
initiateSwap()
|
||||
const approvalTime = Date.now()
|
||||
cy.contains('Enable spending limits for DAI on Uniswap').should('exist')
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }),
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.contains('Swapped').should('exist')
|
||||
// Verify token approval
|
||||
cy.get(getTestSelector('popups')).contains('Approved')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax()
|
||||
})
|
||||
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
|
||||
})
|
||||
it('prompts signature when existing permit approval is expired', () => {
|
||||
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax()
|
||||
})
|
||||
|
||||
it('prompts signature when existing permit approval amount is too low', () => {
|
||||
const smallAllowance = { amount: 1 }
|
||||
cy.hardhat().then(({ approval, wallet }) =>
|
||||
Promise.all([
|
||||
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }),
|
||||
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance),
|
||||
])
|
||||
)
|
||||
initiateSwap()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
@@ -13,15 +14,18 @@ describe('Swap errors', () => {
|
||||
// Stub the wallet to reject any transaction.
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
|
||||
cy.contains('Review swap').should('exist')
|
||||
cy.get('body').click('topRight')
|
||||
cy.contains('Review swap').should('not.exist')
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas')
|
||||
|
||||
// Verify rejection
|
||||
cy.contains('Review swap')
|
||||
cy.contains('Confirm swap')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,32 +33,33 @@ describe('Swap errors', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Set deadline to minimum. (1 minute)
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('transaction-deadline-settings')).click()
|
||||
cy.get(getTestSelector('deadline-input')).clear().type('1') // 1 minute
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('deadline-input')).should('not.exist')
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Transaction submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine(1, /* 10 minutes */ 1000 * 60 * 10)) // mines past the deadline
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
// Remove the transaction from the mempool, so that it doesn't fail but it is past the deadline.
|
||||
// This should result in it being removed from pending transactions, without a failure notificiation.
|
||||
const transactions = await hardhat.send('eth_pendingTransactions', [])
|
||||
await hardhat.send('hardhat_dropTransaction', [transactions[0].hash])
|
||||
// Mine past the deadline
|
||||
await hardhat.mine(1, DEFAULT_DEADLINE_FROM_NOW + 1)
|
||||
})
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction did not occur
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// Verify the balance is unchanged.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should('have.text', `Balance: ${initialBalance}`)
|
||||
cy.get(getTestSelector('popups')).should('not.contain', 'Swap failed')
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${initialBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
@@ -74,43 +79,29 @@ describe('Swap errors', () => {
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get('body').click('topRight') // close modal
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Swap 2 times.
|
||||
const AMOUNT_TO_SWAP = 200
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type(AMOUNT_TO_SWAP.toString())
|
||||
.should('have.value', AMOUNT_TO_SWAP.toString())
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type(AMOUNT_TO_SWAP.toString())
|
||||
.should('have.value', AMOUNT_TO_SWAP.toString())
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Confirm Swap').should('exist')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
// Submit 2 transactions
|
||||
for (let i = 0; i < 2; i++) {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('200').should('have.value', '200')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Transaction submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
}
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
|
||||
|
||||
// Mine transactions
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction did not occur
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// Assert that the transactions were unsuccessful by checking on-chain balance.
|
||||
getBalance(UNI_MAINNET).should('equal', initialBalance)
|
||||
cy.get(getTestSelector('popups')).contains('Swap failed')
|
||||
getBalance(UNI_MAINNET).should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,29 +41,35 @@ describe('Swap', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Select USDC
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
|
||||
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.contains('Review swap')
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Transaction submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.contains('Transaction submitted').should('not.exist')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swapped')
|
||||
|
||||
// Verify the balance is updated.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance + 1}`
|
||||
)
|
||||
getBalance(USDC_MAINNET).should('eq', initialBalance + 1)
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(USDC_MAINNET).should('eq', finalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('Swap wrap', () => {
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01').should('have.value', '0.01')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
||||
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.02').should('have.value', '0.02')
|
||||
@@ -20,31 +20,28 @@ describe('Swap wrap', () => {
|
||||
})
|
||||
|
||||
it('should be able to wrap ETH', () => {
|
||||
getBalance(WETH).then((initialWethBalance) => {
|
||||
getBalance(WETH).then((initialBalance) => {
|
||||
cy.contains('Enter ETH amount')
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
|
||||
// This also ensures we don't click "Wrap" before the UI has caught up.
|
||||
// Enter amount to wrap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||
|
||||
// Click the wrap button.
|
||||
// Submit transaction
|
||||
cy.contains('Wrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output').should('contain', `Balance: ${initialWethBalance + 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance + 1)
|
||||
cy.get(getTestSelector('popups')).contains('Wrapped')
|
||||
const finalBalance = initialBalance + 1
|
||||
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(WETH).should('equal', finalBalance)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,33 +51,30 @@ describe('Swap wrap', () => {
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(WETH).then((initialWethBalance) => {
|
||||
// Swap input/output to unwrap WETH.
|
||||
getBalance(WETH).then((initialBalance) => {
|
||||
// Swap input/output to unwrap WETH
|
||||
cy.get(getTestSelector('swap-currency-button')).click()
|
||||
cy.contains('Enter WETH amount')
|
||||
|
||||
// Enter the amount to unwrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
|
||||
// This also ensures we don't click "Wrap" before the UI has caught up.
|
||||
// Enter the amount to unwrap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
|
||||
|
||||
// Click the unwrap button.
|
||||
// Submit transaction
|
||||
cy.contains('Unwrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
|
||||
// Mine transaction
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-input').should('contain', `Balance: ${initialWethBalance - 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance - 1)
|
||||
cy.get(getTestSelector('popups')).contains('Unwrapped')
|
||||
const finalBalance = initialBalance - 1
|
||||
cy.get('#swap-currency-input').contains(`Balance: ${finalBalance}`)
|
||||
getBalance(WETH).should('equal', finalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('Token details', () => {
|
||||
cy.url().should('not.include', `${UNI_MAINNET.address}`)
|
||||
})
|
||||
|
||||
it.only('should not share swap state with the main swap page', () => {
|
||||
it('should not share swap state with the main swap page', () => {
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
cy.get(`#swap-currency-input .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
@@ -151,6 +151,7 @@ describe('Token details', () => {
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
cy.get(getTestSelector('token-table-row-ARB')).click()
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).should('be.disabled')
|
||||
cy.contains('Connect to Arbitrum').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,83 +1,24 @@
|
||||
describe.skip('Token explore filter', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('should filter correctly by uni search term', () => {
|
||||
describe('Token explore filter', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByUni = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('uni'))
|
||||
cy.wrap(filteredByUni).as('filteredByUni')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('uni')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByUni').then((filteredByUni) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByUni.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByUni).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function aliasFilteredTokens(filter: string) {
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens).filter((token) => token.innerText.toLowerCase().includes(filter))).as('filteredTokens')
|
||||
})
|
||||
}
|
||||
|
||||
function searchFor(filter: string) {
|
||||
cy.get('[data-cy="explore-tokens-search-input"]').clear().type(filter).type('{enter}')
|
||||
}
|
||||
|
||||
it('should filter correctly by dao search term', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByDao = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('dao'))
|
||||
cy.wrap(filteredByDao).as('filteredByDao')
|
||||
aliasFilteredTokens('dao')
|
||||
searchFor('dao')
|
||||
|
||||
cy.get('@filteredTokens').then((filteredTokens) => {
|
||||
cy.get('[data-cy="token-name"]').should('deep.equal', filteredTokens)
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('dao')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByDao').then((filteredByDao) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByDao.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByDao).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should filter correctly by ax search term', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
const filteredByAx = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('ax'))
|
||||
cy.wrap(filteredByAx).as('filteredByAx')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="explore-tokens-search-input"]')
|
||||
.clear()
|
||||
.type('ax')
|
||||
.type('{enter}')
|
||||
.then(() => {
|
||||
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
|
||||
cy.get('@filteredByAx').then((filteredByAx) => {
|
||||
cy.get('[data-cy="token-name"]').then(($els) => {
|
||||
const tokenNames = Array.from($els, (el) => el.innerText)
|
||||
expect(tokenNames.length).to.equal(filteredByAx.length)
|
||||
tokenNames.forEach((tokenName) => {
|
||||
expect(filteredByAx).to.include(tokenName)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
40
cypress/e2e/wallet-connection/connect.test.ts
Normal file
40
cypress/e2e/wallet-connection/connect.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('disconnect wallet', () => {
|
||||
it('should clear state', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
|
||||
// Verify wallet is connected
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)))
|
||||
cy.contains('Balance:')
|
||||
|
||||
// Disconnect the wallet
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)).click())
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).contains('Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
|
||||
// Verify wallet has disconnected
|
||||
cy.contains('Connect a wallet').should('exist')
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect')
|
||||
cy.contains('Connect Wallet')
|
||||
|
||||
// Verify swap input is cleared
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
})
|
||||
})
|
||||
|
||||
describe('connect wallet', () => {
|
||||
it('should load state', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat', userState: {} })
|
||||
|
||||
// Connect the wallet
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click()
|
||||
cy.contains('MetaMask').click()
|
||||
|
||||
// Verify wallet is connected
|
||||
cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)))
|
||||
cy.contains('Balance:')
|
||||
})
|
||||
})
|
||||
@@ -45,6 +45,9 @@ describe('Wallet Dropdown', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
// click twice, first time to show confirmation, second to confirm
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
|
||||
1
cypress/fixtures/mini-portfolio/activity.json
Normal file
1
cypress/fixtures/mini-portfolio/activity.json
Normal file
File diff suppressed because one or more lines are too long
1
cypress/fixtures/mini-portfolio/nfts.json
Normal file
1
cypress/fixtures/mini-portfolio/nfts.json
Normal file
File diff suppressed because one or more lines are too long
1
cypress/fixtures/mini-portfolio/tokens.json
Normal file
1
cypress/fixtures/mini-portfolio/tokens.json
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@ import 'cypress-hardhat/lib/browser'
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { FeatureFlag } from '../../src/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-ignore
|
||||
import TokenListJSON from '@uniswap/default-token-list'
|
||||
import { CyHttpMessages } from 'cypress/types/net-stubbing'
|
||||
|
||||
beforeEach(() => {
|
||||
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
|
||||
@@ -16,6 +17,9 @@ beforeEach(() => {
|
||||
req.continue()
|
||||
})
|
||||
|
||||
// Log requests to hardhat.
|
||||
cy.intercept(/:8545/, logJsonRpc)
|
||||
|
||||
// Mock analytics responses to avoid analytics in tests.
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
@@ -39,3 +43,21 @@ beforeEach(() => {
|
||||
// This resets the fork, as well as options like automine.
|
||||
cy.hardhat().then((hardhat) => hardhat.reset())
|
||||
})
|
||||
|
||||
function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
|
||||
req.alias = req.body.method
|
||||
const log = Cypress.log({
|
||||
autoEnd: false,
|
||||
name: req.body.method,
|
||||
message: req.body.params?.map((param: unknown) =>
|
||||
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
|
||||
),
|
||||
})
|
||||
req.on('after:response', (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
log.end()
|
||||
} else {
|
||||
log.error(new Error(`${res.statusCode}: ${res.statusMessage}`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"strict": true,
|
||||
"target": "ES5",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["cypress", "node"]
|
||||
"types": ["cypress", "node"],
|
||||
"jsx": "react"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
"@visx/react-spring": "^2.12.2",
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@walletconnect/ethereum-provider": "^1.8.0",
|
||||
"@web3-react/coinbase-wallet": "^8.2.0",
|
||||
"@web3-react/core": "^8.2.0",
|
||||
"@web3-react/eip1193": "^8.2.0",
|
||||
@@ -197,6 +196,7 @@
|
||||
"@web3-react/types": "^8.2.0",
|
||||
"@web3-react/url": "^8.2.0",
|
||||
"@web3-react/walletconnect": "^8.2.0",
|
||||
"@web3-react/walletconnect-v2": "8.3.2",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
@@ -248,7 +248,7 @@
|
||||
"styled-components": "^5.3.5",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"ua-parser-js": "^1.0.35",
|
||||
"use-resize-observer": "^9.0.2",
|
||||
"use-resize-observer": "^9.1.0",
|
||||
"uuid": "^8.3.2",
|
||||
"video-extensions": "^1.2.0",
|
||||
"wcag-contrast": "^3.0.0",
|
||||
@@ -261,7 +261,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"npm": "please-use-yarn",
|
||||
"node": "14",
|
||||
"node": "18.x",
|
||||
"yarn": ">=1.22"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const thegraphConfig = require('../graphql.thegraph.config')
|
||||
const exec = promisify(child_process.exec)
|
||||
|
||||
function fetchSchema(url, outputFile) {
|
||||
exec(`npx get-graphql-schema --h Origin=https://app.uniswap.org ${url}`)
|
||||
exec(`yarn --silent get-graphql-schema --h Origin=https://app.uniswap.org ${url}`)
|
||||
.then(({ stderr, stdout }) => {
|
||||
if (stderr) {
|
||||
throw new Error(stderr)
|
||||
|
||||
1
src/assets/svg/moonpay.svg
Normal file
1
src/assets/svg/moonpay.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="36" height="36" rx="18" fill="#7D00FF"/><path d="M24.933 14.14a3.07 3.07 0 0 0 0-6.14 3.07 3.07 0 0 0 0 6.14ZM15.5 28A7.495 7.495 0 0 1 8 20.493a7.495 7.495 0 0 1 7.5-7.506c4.149 0 7.5 3.354 7.5 7.506A7.495 7.495 0 0 1 15.5 28Z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -10,14 +10,14 @@ import { AutoRow } from 'components/Row'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { ArrowDownRight, ArrowUpRight, Copy, CreditCard, IconProps, Info, Power, Settings } from 'react-feather'
|
||||
import { ArrowDownRight, ArrowUpRight, Copy, CreditCard, IconProps, Info, LogOut, Settings } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
@@ -31,7 +31,7 @@ import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { useToggleAccountDrawer } from '.'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
import IconButton, { IconHoverText, IconWithConfirmTextButton } from './IconButton'
|
||||
import MiniPortfolio from './MiniPortfolio'
|
||||
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
|
||||
|
||||
@@ -103,7 +103,9 @@ const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
|
||||
const StatusWrapper = styled.div`
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
padding-right: 4px;
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
padding-right: 14px;
|
||||
display: inline-flex;
|
||||
`
|
||||
|
||||
@@ -158,6 +160,10 @@ export function PortfolioArrow({ change, ...rest }: { change: number } & IconPro
|
||||
)
|
||||
}
|
||||
|
||||
const LogOutCentered = styled(LogOut)`
|
||||
transform: translateX(2px);
|
||||
`
|
||||
|
||||
export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) {
|
||||
const { connector, ENSName } = useWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -172,7 +178,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
|
||||
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
|
||||
const isUnclaimed = useUserHasAvailableClaim(account)
|
||||
const getConnection = useGetConnection()
|
||||
const connection = getConnection(connector)
|
||||
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
|
||||
@@ -232,6 +237,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
const totalBalance = portfolio?.tokensTotalDenominatedValue?.value
|
||||
const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value
|
||||
const percentChange = portfolio?.tokensTotalDenominatedValueChange?.percentage?.value
|
||||
const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false)
|
||||
|
||||
return (
|
||||
<AuthenticatedHeaderWrapper>
|
||||
@@ -253,13 +259,21 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
)}
|
||||
</StatusWrapper>
|
||||
<IconContainer>
|
||||
<IconButton data-testid="wallet-settings" onClick={openSettings} Icon={Settings} />
|
||||
{!showDisconnectConfirm && (
|
||||
<IconButton data-testid="wallet-settings" onClick={openSettings} Icon={Settings} />
|
||||
)}
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.DISCONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power} />
|
||||
<IconWithConfirmTextButton
|
||||
data-testid="wallet-disconnect"
|
||||
onConfirm={disconnect}
|
||||
onShowConfirm={setShowDisconnectConfirm}
|
||||
Icon={LogOutCentered}
|
||||
text="Disconnect"
|
||||
/>
|
||||
</TraceEvent>
|
||||
</IconContainer>
|
||||
</HeaderWrapper>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Icon } from 'react-feather'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
|
||||
import Row from '../Row'
|
||||
|
||||
export const IconHoverText = styled.span`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
@@ -13,12 +17,17 @@ export const IconHoverText = styled.span`
|
||||
left: 10px;
|
||||
`
|
||||
|
||||
const widthTransition = `width ease-in 80ms`
|
||||
|
||||
const IconStyles = css`
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
transition: ${widthTransition};
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
@@ -28,7 +37,7 @@ const IconStyles = css`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast} background-color ${timing.in}`};
|
||||
}) => `${duration.fast} background-color ${timing.in}, ${widthTransition}`};
|
||||
|
||||
${IconHoverText} {
|
||||
opacity: 1;
|
||||
@@ -36,7 +45,7 @@ const IconStyles = css`
|
||||
}
|
||||
:active {
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
transition: background-color 50ms linear;
|
||||
transition: background-color 50ms linear, ${widthTransition};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -51,27 +60,29 @@ const IconBlockButton = styled.button`
|
||||
`
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
`
|
||||
interface BaseProps {
|
||||
Icon: Icon
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
interface IconLinkProps extends React.ComponentPropsWithoutRef<'a'>, BaseProps {}
|
||||
interface IconButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseProps {}
|
||||
|
||||
const IconBlock = (props: React.ComponentPropsWithoutRef<'a' | 'button'>) => {
|
||||
type IconBlockProps = React.ComponentPropsWithoutRef<'a' | 'button'>
|
||||
|
||||
const IconBlock = forwardRef<HTMLAnchorElement | HTMLDivElement, IconBlockProps>(function IconBlock(props, ref) {
|
||||
if ('href' in props) {
|
||||
return <IconBlockLink {...props} />
|
||||
return <IconBlockLink ref={ref as React.ForwardedRef<HTMLAnchorElement>} {...props} />
|
||||
}
|
||||
// ignoring 'button' 'type' conflict between React and styled-components
|
||||
// @ts-ignore
|
||||
return <IconBlockButton {...props} />
|
||||
}
|
||||
return <IconBlockButton ref={ref} {...props} />
|
||||
})
|
||||
|
||||
const IconButton = ({ Icon, ...rest }: IconButtonProps | IconLinkProps) => (
|
||||
<IconBlock {...rest}>
|
||||
@@ -81,4 +92,119 @@ const IconButton = ({ Icon, ...rest }: IconButtonProps | IconLinkProps) => (
|
||||
</IconBlock>
|
||||
)
|
||||
|
||||
type IconWithTextProps = (IconButtonProps | IconLinkProps) & {
|
||||
text: string
|
||||
onConfirm?: () => void
|
||||
onShowConfirm?: (on: boolean) => void
|
||||
}
|
||||
|
||||
const TextWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
min-width: min-content;
|
||||
`
|
||||
|
||||
const TextHide = styled.div`
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
/**
|
||||
* Allows for hiding and showing some text next to an IconButton
|
||||
* Note that for width transitions to animate in CSS we need to always specify the width (no auto)
|
||||
* so there's resize observing and measuring going on here.
|
||||
*/
|
||||
export const IconWithConfirmTextButton = ({
|
||||
Icon,
|
||||
text,
|
||||
onConfirm,
|
||||
onShowConfirm,
|
||||
onClick,
|
||||
...rest
|
||||
}: IconWithTextProps) => {
|
||||
const [showText, setShowTextWithoutCallback] = useState(false)
|
||||
const frameObserver = useResizeObserver<HTMLElement>()
|
||||
const hiddenObserver = useResizeObserver<HTMLElement>()
|
||||
|
||||
const setShowText = useCallback(
|
||||
(val: boolean) => {
|
||||
setShowTextWithoutCallback(val)
|
||||
onShowConfirm?.(val)
|
||||
},
|
||||
[onShowConfirm]
|
||||
)
|
||||
|
||||
const dimensionsRef = useRef({
|
||||
frame: 0,
|
||||
hidden: 0,
|
||||
})
|
||||
const dimensions = (() => {
|
||||
// once opened, we avoid updating it to prevent constant resize loop
|
||||
if (!showText) {
|
||||
dimensionsRef.current = { frame: frameObserver.width || 0, hidden: hiddenObserver.width || 0 }
|
||||
}
|
||||
return dimensionsRef.current
|
||||
})()
|
||||
|
||||
// keyboard action to cancel
|
||||
useEffect(() => {
|
||||
if (!showText) return
|
||||
const isClient = typeof window !== 'undefined'
|
||||
if (!isClient) return
|
||||
if (!showText) return
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowText(false)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', keyHandler, { capture: true })
|
||||
return () => {
|
||||
window.removeEventListener('keydown', keyHandler, { capture: true })
|
||||
}
|
||||
}, [setShowText, showText])
|
||||
|
||||
const xPad = showText ? 12 : 0
|
||||
const width = showText ? dimensions.frame + dimensions.hidden + xPad : 32
|
||||
|
||||
return (
|
||||
<IconBlock
|
||||
ref={frameObserver.ref}
|
||||
{...rest}
|
||||
style={{
|
||||
width,
|
||||
paddingLeft: xPad,
|
||||
paddingRight: xPad,
|
||||
}}
|
||||
// @ts-ignore MouseEvent is valid, its a subset of the two mouse events,
|
||||
// even manually typing this all out more specifically it still gets mad about any casting for some reason
|
||||
onClick={(e: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (showText) {
|
||||
onConfirm?.()
|
||||
} else {
|
||||
onClick?.(e)
|
||||
setShowText(!showText)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Row height="100%" gap="xs">
|
||||
<IconWrapper>
|
||||
<Icon strokeWidth={1.5} size={16} />
|
||||
</IconWrapper>
|
||||
|
||||
{/* this outer div is so we can cut it off but keep the inner text width full-width so we can measure it */}
|
||||
<TextHide
|
||||
style={{
|
||||
maxWidth: showText ? dimensions.hidden : 0,
|
||||
minWidth: showText ? dimensions.hidden : 0,
|
||||
}}
|
||||
>
|
||||
<TextWrapper ref={hiddenObserver.ref}>{text}</TextWrapper>
|
||||
</TextHide>
|
||||
</Row>
|
||||
</IconBlock>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconButton
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { formatNumberOrString, NumberType } from '@uniswap/conedison/format'
|
||||
import { formatFiatPrice, formatNumberOrString, NumberType } from '@uniswap/conedison/format'
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import moonpayLogoSrc from 'assets/svg/moonpay.svg'
|
||||
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESS } from 'constants/addresses'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import {
|
||||
ActivityType,
|
||||
AssetActivityPartsFragment,
|
||||
Currency,
|
||||
NftApprovalPartsFragment,
|
||||
NftApproveForAllPartsFragment,
|
||||
NftTransferPartsFragment,
|
||||
@@ -17,6 +19,7 @@ import ms from 'ms.macro'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isAddress } from 'utils'
|
||||
|
||||
import { MOONPAY_SENDER_ADDRESSES } from '../constants'
|
||||
import { Activity } from './types'
|
||||
|
||||
type TransactionChanges = {
|
||||
@@ -106,6 +109,17 @@ function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param transactedValue Transacted value amount from TokenTransfer API response
|
||||
* @returns parsed & formatted USD value as a string if currency is of type USD
|
||||
*/
|
||||
function formatTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): string {
|
||||
if (!transactedValue) return '-'
|
||||
const price = transactedValue?.currency === Currency.Usd ? transactedValue.value ?? undefined : undefined
|
||||
return formatFiatPrice(price)
|
||||
}
|
||||
|
||||
function parseSwap(changes: TransactionChanges) {
|
||||
if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) {
|
||||
const collectionCounts = getCollectionCounts(changes.NftTransfer)
|
||||
@@ -175,17 +189,27 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActiv
|
||||
}
|
||||
|
||||
if (transfer && assetName && amount) {
|
||||
return transfer.direction === 'IN'
|
||||
? {
|
||||
title: t`Received`,
|
||||
descriptor: `${amount} ${assetName} ${t`from`} `,
|
||||
otherAccount: isAddress(transfer.sender) || undefined,
|
||||
}
|
||||
: {
|
||||
title: t`Sent`,
|
||||
descriptor: `${amount} ${assetName} ${t`to`} `,
|
||||
otherAccount: isAddress(transfer.recipient) || undefined,
|
||||
}
|
||||
const isMoonpayPurchase = MOONPAY_SENDER_ADDRESSES.some((address) => isSameAddress(address, transfer?.sender))
|
||||
|
||||
if (transfer.direction === 'IN') {
|
||||
return isMoonpayPurchase && transfer.__typename === 'TokenTransfer'
|
||||
? {
|
||||
title: t`Purchased`,
|
||||
descriptor: `${amount} ${assetName} ${t`for`} ${formatTransactedValue(transfer.transactedValue)}`,
|
||||
logos: [moonpayLogoSrc],
|
||||
}
|
||||
: {
|
||||
title: t`Received`,
|
||||
descriptor: `${amount} ${assetName} ${t`from`} `,
|
||||
otherAccount: isAddress(transfer.sender) || undefined,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: t`Sent`,
|
||||
descriptor: `${amount} ${assetName} ${t`to`} `,
|
||||
otherAccount: isAddress(transfer.recipient) || undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { title: t`Unknown Send` }
|
||||
}
|
||||
|
||||
@@ -153,3 +153,11 @@ export function getActivityTitle(type: TransactionType, status: TransactionStatu
|
||||
}
|
||||
return TransactionTitleTable[type][status]
|
||||
}
|
||||
|
||||
// Non-exhaustive list of addresses Moonpay uses when sending purchased tokens
|
||||
export const MOONPAY_SENDER_ADDRESSES = [
|
||||
'0x8216874887415e2650d12d53ff53516f04a74fd7',
|
||||
'0x151b381058f91cf871e7ea1ee83c45326f61e96d',
|
||||
'0xb287eac48ab21c5fb1d3723830d60b4c797555b0',
|
||||
'0xd108fd0e8c8e71552a167e7a44ff1d345d233ba6',
|
||||
]
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function MiniPortfolio({ account }: { account: string }) {
|
||||
return (
|
||||
<Trace section={InterfaceSectionName.MINI_PORTFOLIO}>
|
||||
<Wrapper>
|
||||
<Nav>
|
||||
<Nav data-testid="mini-portfolio-navbar">
|
||||
{Pages.map(({ title, loggingElementName, key }, index) => {
|
||||
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
|
||||
return (
|
||||
@@ -105,19 +105,14 @@ export default function MiniPortfolio({ account }: { account: string }) {
|
||||
element={loggingElementName}
|
||||
key={index}
|
||||
>
|
||||
<NavItem
|
||||
data-testid={`mini-portfolio-nav-${key}`}
|
||||
onClick={() => setCurrentPage(index)}
|
||||
active={currentPage === index}
|
||||
key={`Mini Portfolio page ${index}`}
|
||||
>
|
||||
<NavItem onClick={() => setCurrentPage(index)} active={currentPage === index} key={key}>
|
||||
{title}
|
||||
</NavItem>
|
||||
</TraceEvent>
|
||||
)
|
||||
})}
|
||||
</Nav>
|
||||
<PageWrapper>
|
||||
<PageWrapper data-testid="mini-portfolio-page">
|
||||
<Page account={account} />
|
||||
</PageWrapper>
|
||||
</Wrapper>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function UniwalletModal() {
|
||||
// Displays the modal if a Uniswap Wallet Connection is pending & qrcode URI is available
|
||||
const open =
|
||||
activationState.status === ActivationStatus.PENDING &&
|
||||
activationState.connection.type === ConnectionType.UNIWALLET &&
|
||||
activationState.connection.type === ConnectionType.UNISWAP_WALLET &&
|
||||
!!uri
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { darken } from 'polished'
|
||||
import { forwardRef } from 'react'
|
||||
import { Check, ChevronDown } from 'react-feather'
|
||||
import { Button as RebassButton, ButtonProps as ButtonPropsOriginal } from 'rebass/styled-components'
|
||||
import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
|
||||
@@ -524,15 +525,19 @@ const BaseThemeButton = styled.button<BaseThemeButtonProps>`
|
||||
`
|
||||
|
||||
interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {}
|
||||
type ThemeButtonRef = HTMLButtonElement
|
||||
|
||||
export const ThemeButton = ({ children, ...rest }: ThemeButtonProps) => {
|
||||
export const ThemeButton = forwardRef<ThemeButtonRef, ThemeButtonProps>(function ThemeButton(
|
||||
{ children, ...rest },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<BaseThemeButton {...rest}>
|
||||
<BaseThemeButton {...rest} ref={ref}>
|
||||
<ButtonOverlay />
|
||||
{children}
|
||||
</BaseThemeButton>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export const ButtonLight = ({ children, ...rest }: BaseButtonProps) => {
|
||||
return (
|
||||
|
||||
8
src/components/Logo/AppleLogo.tsx
Normal file
8
src/components/Logo/AppleLogo.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export const AppleLogo = (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox="0 0 814 1000" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
@@ -1,7 +1,8 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { WalletConnect } from '@web3-react/walletconnect-v2'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { ConnectionType } from 'connection/types'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId, UniWalletSupportedChains } from 'constants/chains'
|
||||
@@ -15,6 +16,7 @@ import { useIsMobile } from 'nft/hooks'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import { isProductionEnv } from 'utils/env'
|
||||
|
||||
import * as styles from './ChainSelector.css'
|
||||
import ChainSelectorRow from './ChainSelectorRow'
|
||||
@@ -29,12 +31,42 @@ const NETWORK_SELECTOR_CHAINS = [
|
||||
SupportedChainId.BNB,
|
||||
]
|
||||
|
||||
if (!isProductionEnv()) {
|
||||
NETWORK_SELECTOR_CHAINS.push(SupportedChainId.SEPOLIA)
|
||||
}
|
||||
|
||||
interface ChainSelectorProps {
|
||||
leftAlign?: boolean
|
||||
}
|
||||
|
||||
// accounts is an array of strings in the format of "eip155:<chainId>:<address>"
|
||||
function getChainsFromEIP155Accounts(accounts?: string[]): SupportedChainId[] {
|
||||
if (!accounts) return []
|
||||
return accounts
|
||||
.map((account) => {
|
||||
const splitAccount = account.split(':')
|
||||
return splitAccount[1] ? parseInt(splitAccount[1]) : undefined
|
||||
})
|
||||
.filter((x) => x !== undefined) as SupportedChainId[]
|
||||
}
|
||||
|
||||
function useWalletSupportedChains() {
|
||||
const { connector } = useWeb3React()
|
||||
|
||||
const connectionType = getConnection(connector).type
|
||||
|
||||
switch (connectionType) {
|
||||
case ConnectionType.WALLET_CONNECT_V2:
|
||||
return getChainsFromEIP155Accounts((connector as WalletConnect).provider?.session?.namespaces.eip155.accounts)
|
||||
case ConnectionType.UNISWAP_WALLET:
|
||||
return UniWalletSupportedChains
|
||||
default:
|
||||
return NETWORK_SELECTOR_CHAINS
|
||||
}
|
||||
}
|
||||
|
||||
export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
||||
const { chainId, connector } = useWeb3React()
|
||||
const { chainId } = useWeb3React()
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
@@ -61,9 +93,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
||||
[selectChain, setIsOpen]
|
||||
)
|
||||
|
||||
const getConnection = useGetConnection()
|
||||
const connectionType = getConnection(connector).type
|
||||
const isUniWallet = connectionType === ConnectionType.UNIWALLET
|
||||
const walletSupportsChain = useWalletSupportedChains()
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
@@ -74,13 +104,13 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
||||
const dropdown = (
|
||||
<NavDropdown top="56" left={leftAlign ? '0' : 'auto'} right={leftAlign ? 'auto' : '0'} ref={modalRef}>
|
||||
<Column paddingX="8">
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
|
||||
{NETWORK_SELECTOR_CHAINS.map((selectorChain: SupportedChainId) => (
|
||||
<ChainSelectorRow
|
||||
disabled={isUniWallet && !UniWalletSupportedChains.includes(chainId)}
|
||||
disabled={!walletSupportsChain.includes(selectorChain)}
|
||||
onSelectChain={onSelectChain}
|
||||
targetChain={chainId}
|
||||
key={chainId}
|
||||
isPending={chainId === pendingChainId}
|
||||
targetChain={selectorChain}
|
||||
key={selectorChain}
|
||||
isPending={selectorChain === pendingChainId}
|
||||
/>
|
||||
))}
|
||||
</Column>
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function ChainSelectorRow({ disabled, targetChain, onSelectChain,
|
||||
)}
|
||||
<Status>
|
||||
{active && <CheckMarkIcon width={LOGO_SIZE} height={LOGO_SIZE} color={theme.accentActive} />}
|
||||
{isPending && <Loader width={LOGO_SIZE} height={LOGO_SIZE} />}
|
||||
{!active && isPending && <Loader width={LOGO_SIZE} height={LOGO_SIZE} />}
|
||||
</Status>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -131,7 +131,7 @@ export const MenuDropdown = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative" ref={ref}>
|
||||
<Box position="relative" ref={ref} marginRight="4">
|
||||
<NavIcon isActive={isOpen} onClick={toggleOpen} label={isOpen ? t`Show resources` : t`Hide resources`}>
|
||||
<EllipsisIcon viewBox="0 0 20 20" width={24} height={24} />
|
||||
</NavIcon>
|
||||
|
||||
@@ -65,6 +65,10 @@ export const searchBarContainerNft = style([
|
||||
},
|
||||
])
|
||||
|
||||
export const searchBarContainerDisableBlur = style({
|
||||
backdropFilter: 'none',
|
||||
})
|
||||
|
||||
export const searchBar = style([
|
||||
baseSearchStyle,
|
||||
sprinkles({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -102,7 +102,7 @@ export const SearchBar = () => {
|
||||
...trace,
|
||||
}
|
||||
const placeholderText = useMemo(() => {
|
||||
return isMobileOrTablet ? t`Search` : t`Search tokens and NFT collections`
|
||||
return isMobileOrTablet ? `Search` : `Search tokens and NFT collections`
|
||||
}, [isMobileOrTablet])
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
@@ -134,11 +134,17 @@ export const SearchBar = () => {
|
||||
<Trace section={InterfaceSectionName.NAVBAR_SEARCH}>
|
||||
<Column
|
||||
data-cy="search-bar"
|
||||
position={{ sm: 'fixed', md: 'absolute', xl: 'relative' }}
|
||||
position={{ sm: 'fixed', md: 'absolute' }}
|
||||
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
|
||||
ref={searchRef}
|
||||
className={styles.searchBarContainerNft}
|
||||
className={clsx(styles.searchBarContainerNft, {
|
||||
searchBarContainerDisableBlur: isNavSearchInputVisible,
|
||||
})}
|
||||
display={{ sm: isOpen ? 'flex' : 'none', xl: 'flex' }}
|
||||
{...(isNavSearchInputVisible && {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
})}
|
||||
>
|
||||
<Row
|
||||
className={clsx(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { chainIdToBackendName } from 'graphql/data/util'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
@@ -10,11 +11,12 @@ import { Row } from 'nft/components/Flex'
|
||||
import { UniIcon } from 'nft/components/icons'
|
||||
import { useProfilePageState } from 'nft/hooks'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useCallback } from 'react'
|
||||
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useIsNavSearchInputVisible } from '../../nft/hooks/useIsNavSearchInputVisible'
|
||||
import { Bag } from './Bag'
|
||||
import Blur from './Blur'
|
||||
import { ChainSelector } from './ChainSelector'
|
||||
@@ -90,6 +92,19 @@ const Navbar = ({ blur }: { blur: boolean }) => {
|
||||
const isNftPage = useIsNftPage()
|
||||
const sellPageState = useProfilePageState((state) => state.state)
|
||||
const navigate = useNavigate()
|
||||
const isNavSearchInputVisible = useIsNavSearchInputVisible()
|
||||
|
||||
const [accountDrawerOpen, toggleAccountDrawer] = useAccountDrawer()
|
||||
|
||||
const handleUniIconClick = useCallback(() => {
|
||||
if (accountDrawerOpen) {
|
||||
toggleAccountDrawer()
|
||||
}
|
||||
navigate({
|
||||
pathname: '/',
|
||||
search: '?intro=true',
|
||||
})
|
||||
}, [accountDrawerOpen, navigate, toggleAccountDrawer])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -103,12 +118,7 @@ const Navbar = ({ blur }: { blur: boolean }) => {
|
||||
height="48"
|
||||
data-testid="uniswap-logo"
|
||||
className={styles.logo}
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: '/',
|
||||
search: '?intro=true',
|
||||
})
|
||||
}}
|
||||
onClick={handleUniIconClick}
|
||||
/>
|
||||
</Box>
|
||||
{!isNftPage && (
|
||||
@@ -120,12 +130,17 @@ const Navbar = ({ blur }: { blur: boolean }) => {
|
||||
<PageTabs />
|
||||
</Row>
|
||||
</Box>
|
||||
<Box className={styles.searchContainer}>
|
||||
<Box
|
||||
className={styles.searchContainer}
|
||||
{...(isNavSearchInputVisible && {
|
||||
display: 'flex',
|
||||
})}
|
||||
>
|
||||
<SearchBar />
|
||||
</Box>
|
||||
<Box className={styles.rightSideContainer}>
|
||||
<Row gap="12">
|
||||
<Box position="relative" display={{ sm: 'flex', navSearchInputVisible: 'none' }}>
|
||||
<Box position="relative" display={isNavSearchInputVisible ? 'none' : { sm: 'flex' }}>
|
||||
<SearchBar />
|
||||
</Box>
|
||||
{isNftPage && sellPageState !== ProfilePageStateType.LISTING && <Bag />}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const searchContainer = style([
|
||||
flex: '1',
|
||||
flexShrink: '1',
|
||||
justifyContent: { lg: 'flex-end', xl: 'center' },
|
||||
display: { sm: 'none', navSearchInputVisible: 'flex' },
|
||||
display: { sm: 'none' },
|
||||
alignSelf: 'center',
|
||||
height: '48',
|
||||
alignItems: 'flex-start',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ReactNode } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { Link as HistoryLink, useLocation } from 'react-router-dom'
|
||||
@@ -68,6 +69,7 @@ export function AddRemoveTabs({
|
||||
showBackLink?: boolean
|
||||
children?: ReactNode
|
||||
}) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
// reset states on back
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -108,7 +110,7 @@ export function AddRemoveTabs({
|
||||
)}
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<Box style={{ marginRight: '.5rem' }}>{children}</Box>
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import styled from 'styled-components/macro'
|
||||
import { ExternalLink, HideSmall } from 'theme'
|
||||
import { colors } from 'theme/colors'
|
||||
import { useDarkModeManager } from 'theme/components/ThemeToggle'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
@@ -122,9 +123,9 @@ const LinkOutToBridge = styled(ExternalLink)`
|
||||
font-size: 16px;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
margin-right: 12px;
|
||||
text-decoration: none !important;
|
||||
width: 100%;
|
||||
z-index: ${Z_INDEX.hover};
|
||||
`
|
||||
|
||||
const StyledArrowUpRight = styled(ArrowUpRight)`
|
||||
|
||||
@@ -29,7 +29,6 @@ function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chai
|
||||
|
||||
return (
|
||||
<PortfolioRow
|
||||
data-testid="transaction-popup"
|
||||
left={
|
||||
success ? (
|
||||
<Column>
|
||||
|
||||
@@ -66,14 +66,14 @@ export default function Popups() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
|
||||
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet} data-testid="popups">
|
||||
<ClaimPopup />
|
||||
{activePopups.map((item) => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
{activePopups?.length > 0 && (
|
||||
<MobilePopupWrapper>
|
||||
<MobilePopupWrapper data-testid="popups">
|
||||
<MobilePopupInner>
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
|
||||
@@ -41,8 +41,8 @@ const MenuFlyout = styled(AutoColumn)`
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
const { chainId } = useWeb3React()
|
||||
export default function SettingsTab({ autoSlippage, chainId }: { autoSlippage: Percent; chainId?: number }) {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
|
||||
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
@@ -55,7 +55,7 @@ export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent })
|
||||
|
||||
return (
|
||||
<Menu ref={node}>
|
||||
<MenuButton disabled={!isSupportedChain} isActive={isOpen} onClick={toggleMenu} />
|
||||
<MenuButton disabled={!isSupportedChain || chainId !== connectedChainId} isActive={isOpen} onClick={toggleMenu} />
|
||||
{isOpen && (
|
||||
<MenuFlyout>
|
||||
<RouterPreferenceSettings />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
||||
import { displayWarningLabel, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety'
|
||||
import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor'
|
||||
import { AlertTriangle, Slash } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -11,6 +11,7 @@ const Label = styled.div<{ color: string; backgroundColor: string }>`
|
||||
padding: 12px 20px 16px;
|
||||
background-color: ${({ backgroundColor }) => backgroundColor};
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
color: ${({ color }) => color};
|
||||
`
|
||||
|
||||
@@ -35,7 +36,8 @@ const DetailsRow = styled.div`
|
||||
`
|
||||
|
||||
const StyledLink = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
|
||||
font-weight: 700;
|
||||
`
|
||||
|
||||
@@ -51,10 +53,12 @@ export default function TokenSafetyMessage({ warning, tokenAddress }: TokenSafet
|
||||
|
||||
return (
|
||||
<Label data-cy="token-safety-message" color={textColor} backgroundColor={backgroundColor}>
|
||||
<TitleRow>
|
||||
{warning.canProceed ? <AlertTriangle size="16px" /> : <Slash size="16px" />}
|
||||
<Title marginLeft="7px">{warning.message}</Title>
|
||||
</TitleRow>
|
||||
{displayWarningLabel(warning) && (
|
||||
<TitleRow>
|
||||
{warning.canProceed ? <AlertTriangle size="16px" /> : <Slash size="16px" />}
|
||||
<Title marginLeft="7px">{warning.message}</Title>
|
||||
</TitleRow>
|
||||
)}
|
||||
|
||||
<DetailsRow>
|
||||
{heading}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import TokenSafetyLabel from 'components/TokenSafety/TokenSafetyLabel'
|
||||
import { checkWarning, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning, WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import {
|
||||
checkWarning,
|
||||
displayWarningLabel,
|
||||
getWarningCopy,
|
||||
NotFoundWarning,
|
||||
TOKEN_SAFETY_ARTICLE,
|
||||
Warning,
|
||||
} from 'constants/tokenSafety'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { ExternalLink as LinkIconFeather } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -23,7 +30,7 @@ const Wrapper = styled.div`
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 32px 50px;
|
||||
padding: 32px 40px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
@@ -85,7 +92,7 @@ const Buttons = ({
|
||||
return warning.canProceed ? (
|
||||
<>
|
||||
<StyledButton onClick={onContinue}>
|
||||
<Trans>I understand</Trans>
|
||||
{!displayWarningLabel(warning) ? <Trans>Continue</Trans> : <Trans>I understand</Trans>}
|
||||
</StyledButton>
|
||||
{showCancel && <StyledCancelButton onClick={onCancel}>Cancel</StyledCancelButton>}
|
||||
</>
|
||||
@@ -183,7 +190,7 @@ function ExplorerView({ token }: { token: Token }) {
|
||||
}
|
||||
|
||||
const StyledExternalLink = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
stroke: currentColor;
|
||||
font-weight: 600;
|
||||
`
|
||||
@@ -251,11 +258,6 @@ export default function TokenSafety({
|
||||
<Trans>Learn more</Trans>
|
||||
</StyledExternalLink>
|
||||
)
|
||||
const tokenNotFoundWarning = {
|
||||
level: WARNING_LEVEL.UNKNOWN,
|
||||
message: <Trans>Token not found</Trans>,
|
||||
canProceed: false,
|
||||
}
|
||||
|
||||
return displayWarning ? (
|
||||
<Wrapper data-testid="TokenSafetyWrapper">
|
||||
@@ -263,9 +265,11 @@ export default function TokenSafety({
|
||||
<AutoColumn>
|
||||
<LogoContainer>{logos}</LogoContainer>
|
||||
</AutoColumn>
|
||||
<ShortColumn>
|
||||
<SafetyLabel warning={displayWarning} />
|
||||
</ShortColumn>
|
||||
{displayWarningLabel(displayWarning) && (
|
||||
<ShortColumn>
|
||||
<SafetyLabel warning={displayWarning} />
|
||||
</ShortColumn>
|
||||
)}
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
{heading} {description} {learnMoreUrl}
|
||||
@@ -285,14 +289,14 @@ export default function TokenSafety({
|
||||
<Wrapper>
|
||||
<Container>
|
||||
<ShortColumn>
|
||||
<SafetyLabel warning={tokenNotFoundWarning} />
|
||||
<SafetyLabel warning={NotFoundWarning} />
|
||||
</ShortColumn>
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
{heading} {description} {learnMoreUrl}
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<Buttons warning={tokenNotFoundWarning} onCancel={onCancel} showCancel={true} />
|
||||
<Buttons warning={NotFoundWarning} onCancel={onCancel} showCancel={true} />
|
||||
</Container>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { AxisBottom, TickFormatter } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
monthYearDayFormatter,
|
||||
weekFormatter,
|
||||
} from 'utils/formatChartTimes'
|
||||
import { formatDollar } from 'utils/formatNumbers'
|
||||
|
||||
const DATA_EMPTY = { value: 0, timestamp: 0 }
|
||||
|
||||
@@ -186,14 +186,17 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
|
||||
const startDateWithOffset = new Date((startingPrice.timestamp.valueOf() + offsetTime) * 1000)
|
||||
const endDateWithOffset = new Date((endingPrice.timestamp.valueOf() - offsetTime) * 1000)
|
||||
switch (timePeriod) {
|
||||
case TimePeriod.HOUR:
|
||||
case TimePeriod.HOUR: {
|
||||
const interval = timeMinute.every(5)
|
||||
|
||||
return [
|
||||
hourFormatter(locale),
|
||||
dayHourFormatter(locale),
|
||||
(timeMinute.every(5) ?? timeMinute)
|
||||
.range(startDateWithOffset, endDateWithOffset, 2)
|
||||
(interval ?? timeMinute)
|
||||
.range(startDateWithOffset, endDateWithOffset, interval ? 2 : 10)
|
||||
.map((x) => x.valueOf() / 1000),
|
||||
]
|
||||
}
|
||||
case TimePeriod.DAY:
|
||||
return [
|
||||
hourFormatter(locale),
|
||||
@@ -276,7 +279,7 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
|
||||
<ChartHeader data-cy="chart-header">
|
||||
{displayPrice.value ? (
|
||||
<>
|
||||
<TokenPrice>{formatDollar({ num: displayPrice.value, isPrice: true })}</TokenPrice>
|
||||
<TokenPrice>{formatUSDPrice(displayPrice.value)}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
{formattedDelta}
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
|
||||
112
src/components/Tokens/TokenTable/TokenRow.test.tsx
Normal file
112
src/components/Tokens/TokenTable/TokenRow.test.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { Currency, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import { LoadedRow } from './TokenRow'
|
||||
|
||||
const sparklineData = [
|
||||
{
|
||||
__typename: 'TimestampedAmount',
|
||||
id: 'VGltZXN0YW1wZWRBbW91bnQ6Ni41MjM1MTI1NzI1Nzc0MDZfMTY4NTk3NjkzNV9VU0Q=',
|
||||
timestamp: 1685976935,
|
||||
value: 6.523512572577406,
|
||||
},
|
||||
{
|
||||
__typename: 'TimestampedAmount',
|
||||
id: 'VGltZXN0YW1wZWRBbW91bnQ6Ni41MDQ2NTU5Njk1ODg3NzJfMTY4NTk3NzUzNV9VU0Q=',
|
||||
timestamp: 1685977535,
|
||||
value: 6.504655969588772,
|
||||
},
|
||||
{
|
||||
__typename: 'TimestampedAmount',
|
||||
id: 'VGltZXN0YW1wZWRBbW91bnQ6Ni40NzU2MTY0ODczODQ0NDlfMTY4NTk3ODEzNV9VU0Q=',
|
||||
timestamp: 1685978135,
|
||||
value: 6.475616487384449,
|
||||
},
|
||||
]
|
||||
|
||||
const market = {
|
||||
__typename: 'TokenMarket' as const,
|
||||
id: 'VG9rZW5NYXJrZXQ6RVRIRVJFVU1fMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4X1VTRA==',
|
||||
totalValueLocked: {
|
||||
__typename: 'Amount' as const,
|
||||
id: 'QW1vdW50OjY3NDY2MDI0OC43Njk5ODdfVVNE',
|
||||
value: 674660248.769987,
|
||||
currency: Currency.Usd,
|
||||
},
|
||||
price: {
|
||||
__typename: 'Amount' as const,
|
||||
id: 'QW1vdW50OjAuOTk5OTk5OTk5OTk5OTk5OV9VU0Q=',
|
||||
value: 0.9999999999999999,
|
||||
currency: Currency.Usd,
|
||||
},
|
||||
pricePercentChange: {
|
||||
__typename: 'Amount' as const,
|
||||
id: 'QW1vdW50OjBfVVNE',
|
||||
currency: Currency.Usd,
|
||||
value: 0,
|
||||
},
|
||||
volume: {
|
||||
__typename: 'Amount' as const,
|
||||
id: 'QW1vdW50OjQ0NDc0NDcwMy4yNTQ2Mzg3X1VTRA==',
|
||||
value: 444744703.2546387,
|
||||
currency: Currency.Usd,
|
||||
},
|
||||
}
|
||||
|
||||
const project = {
|
||||
__typename: 'TokenProject' as const,
|
||||
id: 'VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OA==',
|
||||
logoUrl:
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
|
||||
}
|
||||
|
||||
describe('LoadedRow.tsx', () => {
|
||||
it('renders a row', () => {
|
||||
const { asFragment } = render(
|
||||
<LoadedRow
|
||||
tokenListIndex={0}
|
||||
tokenListLength={1}
|
||||
token={{
|
||||
__typename: 'Token',
|
||||
id: 'VG9rZW46RVRIRVJFVU1fMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4',
|
||||
name: 'USD Coin',
|
||||
chain: CHAIN_ID_TO_BACKEND_NAME[SupportedChainId.MAINNET],
|
||||
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
symbol: 'USDC',
|
||||
standard: TokenStandard.Erc20,
|
||||
market,
|
||||
project,
|
||||
}}
|
||||
sparklineMap={{ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': sparklineData }}
|
||||
sortRank={2}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should render "-" as the price when it receives a 0 value', () => {
|
||||
const newMarket = { ...market, price: { ...market.price, value: 0 } }
|
||||
render(
|
||||
<LoadedRow
|
||||
tokenListIndex={0}
|
||||
tokenListLength={1}
|
||||
token={{
|
||||
__typename: 'Token',
|
||||
id: 'VG9rZW46RVRIRVJFVU1fMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4',
|
||||
name: 'USD Coin',
|
||||
chain: CHAIN_ID_TO_BACKEND_NAME[SupportedChainId.MAINNET],
|
||||
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
symbol: 'USDC',
|
||||
standard: TokenStandard.Erc20,
|
||||
market: newMarket,
|
||||
project,
|
||||
}}
|
||||
sparklineMap={{ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': sparklineData }}
|
||||
sortRank={2}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('-')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -41,7 +41,7 @@ const Cell = styled.div`
|
||||
const StyledTokenRow = styled.div<{
|
||||
first?: boolean
|
||||
last?: boolean
|
||||
loading?: boolean
|
||||
$loading?: boolean
|
||||
}>`
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
@@ -66,8 +66,8 @@ const StyledTokenRow = styled.div<{
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
|
||||
&:hover {
|
||||
${({ loading, theme }) =>
|
||||
!loading &&
|
||||
${({ $loading, theme }) =>
|
||||
!$loading &&
|
||||
css`
|
||||
background-color: ${theme.hoverDefault};
|
||||
`}
|
||||
@@ -349,7 +349,7 @@ function TokenRow({
|
||||
first?: boolean
|
||||
header: boolean
|
||||
listNumber: ReactNode
|
||||
loading?: boolean
|
||||
$loading?: boolean
|
||||
tvl: ReactNode
|
||||
price: ReactNode
|
||||
percentChange: ReactNode
|
||||
@@ -404,7 +404,7 @@ export function LoadingRow(props: { first?: boolean; last?: boolean }) {
|
||||
<TokenRow
|
||||
header={false}
|
||||
listNumber={<SmallLoadingBubble />}
|
||||
loading
|
||||
$loading
|
||||
tokenInfo={
|
||||
<>
|
||||
<IconLoadingBubble />
|
||||
@@ -453,6 +453,9 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
search_token_address_input: filterString,
|
||||
}
|
||||
|
||||
// A simple 0 price indicates the price is not currently available from the api
|
||||
const price = token.market?.price?.value === 0 ? '-' : formatUSDPrice(token.market?.price?.value)
|
||||
|
||||
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
|
||||
@@ -477,7 +480,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
price={
|
||||
<ClickableContent>
|
||||
<PriceInfoCell>
|
||||
{formatUSDPrice(token.market?.price?.value)}
|
||||
{price}
|
||||
<PercentChangeInfoCell>
|
||||
<ArrowCell>{smallArrow}</ArrowCell>
|
||||
<DeltaText delta={delta}>{formattedDelta}</DeltaText>
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LoadedRow.tsx renders a row 1`] = `
|
||||
<DocumentFragment>
|
||||
.c18 {
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
padding-right: 3px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
--size: 24px;
|
||||
border-radius: 100px;
|
||||
color: #0D111C;
|
||||
background-color: #E8ECFB;
|
||||
font-size: calc(var(--size) / 3);
|
||||
font-weight: 500;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
--size: calc(24px / 2);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
background: url();
|
||||
background-repeat: no-repeat;
|
||||
background-size: calc(24px / 2) calc(24px / 2);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
font-size: 16px;
|
||||
grid-template-columns: 1fr 7fr 4fr 4fr 4fr 4fr 5fr;
|
||||
line-height: 24px;
|
||||
max-width: 1200px;
|
||||
min-width: 390px;
|
||||
height: 72px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
-webkit-transition: background-color 250ms ease;
|
||||
transition: background-color 250ms ease;
|
||||
width: 100%;
|
||||
-webkit-transition-duration: 125ms;
|
||||
transition-duration: 125ms;
|
||||
}
|
||||
|
||||
.c1:hover {
|
||||
background-color: #98A1C014;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
color: #0D111C;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
color: #7780A0;
|
||||
min-width: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: flex-end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
min-width: 80px;
|
||||
-webkit-user-select: unset;
|
||||
-moz-user-select: unset;
|
||||
-ms-user-select: unset;
|
||||
user-select: unset;
|
||||
-webkit-transition: background-color 250ms ease;
|
||||
transition: background-color 250ms ease;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
padding: 0px 8px;
|
||||
min-width: 240px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: flex-end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
padding: 0px 24px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
width: 124px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
gap: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
max-width: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
color: #98A1C0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width:1200px) {
|
||||
.c1 {
|
||||
grid-template-columns: 1fr 6.5fr 4.5fr 4.5fr 4.5fr 4.5fr 1.7fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:840px) {
|
||||
.c1 {
|
||||
grid-template-columns: 1fr 7.5fr 4.5fr 4.5fr 4.5fr 1.7fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:720px) {
|
||||
.c1 {
|
||||
grid-template-columns: 1fr 10fr 5fr 5fr 1.2fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c1 {
|
||||
grid-template-columns: 2fr 3fr;
|
||||
min-width: unset;
|
||||
border-bottom: 0.5px solid #F5F6FC;
|
||||
}
|
||||
|
||||
.c1:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c3 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:720px) {
|
||||
.c21 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c20 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c16 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: flex-end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
color: #7780A0;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c15 {
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:1200px) {
|
||||
.c23 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c10 {
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:540px) {
|
||||
.c12 {
|
||||
font-size: 12px;
|
||||
height: 16px;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width:840px) {
|
||||
.c22 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
data-testid="token-table-row-USDC"
|
||||
>
|
||||
<a
|
||||
class="c0"
|
||||
href="#/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<div
|
||||
class="c2 c3"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<div
|
||||
class="c2 c4"
|
||||
data-testid="name-cell"
|
||||
>
|
||||
<div
|
||||
class="c5 c6"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
USD
|
||||
</div>
|
||||
<div
|
||||
class="c9"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="7 c2 c10"
|
||||
>
|
||||
<div
|
||||
class="8 c11"
|
||||
data-cy="token-name"
|
||||
>
|
||||
USD Coin
|
||||
</div>
|
||||
<div
|
||||
class="9 c2 c12"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c13 c14"
|
||||
data-testid="price-cell"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="2 c2 c15"
|
||||
>
|
||||
$1.00
|
||||
<div
|
||||
class="1 c2 c16"
|
||||
>
|
||||
<div
|
||||
class="c17"
|
||||
>
|
||||
<svg
|
||||
aria-label="up"
|
||||
class="c18"
|
||||
fill="none"
|
||||
height="14"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="7"
|
||||
x2="17"
|
||||
y1="17"
|
||||
y2="7"
|
||||
/>
|
||||
<polyline
|
||||
points="7 7 17 7 17 17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="c19"
|
||||
>
|
||||
0.00%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="0 c2 c13 c20"
|
||||
data-testid="percent-change-cell"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c17"
|
||||
>
|
||||
<svg
|
||||
aria-label="up"
|
||||
class="c18"
|
||||
fill="none"
|
||||
height="20"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="7"
|
||||
x2="17"
|
||||
y1="17"
|
||||
y2="7"
|
||||
/>
|
||||
<polyline
|
||||
points="7 7 17 7 17 17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
class="c19"
|
||||
>
|
||||
0.00%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c13 c21"
|
||||
data-testid="tvl-cell"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
$674.7M
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="0 c2 c13 c22"
|
||||
data-testid="volume-cell"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
$444.7M
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="4 c2 c23"
|
||||
>
|
||||
<div
|
||||
class="5 c2 c24"
|
||||
>
|
||||
<div
|
||||
style="width: 100%; height: 100%;"
|
||||
>
|
||||
<svg
|
||||
height="0"
|
||||
width="0"
|
||||
>
|
||||
<g
|
||||
class="visx-group"
|
||||
transform="translate(0, 5)"
|
||||
>
|
||||
<path
|
||||
class="visx-linepath"
|
||||
d="M0,0C0,0,53.166666666666664,11.310946288825667,55,11.810946288825667C56.833333333333336,12.310946288825667,110,30,110,30"
|
||||
fill="transparent"
|
||||
stroke="#40B66B"
|
||||
stroke-linecap="round"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import UNIWALLET_ICON from 'assets/images/uniwallet.png'
|
||||
import { useCloseAccountDrawer } from 'components/AccountDrawer'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { Connection, ConnectionType } from 'connection/types'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { createDeferredPromise } from 'test-utils/promise'
|
||||
@@ -8,12 +8,12 @@ import { act, render } from 'test-utils/render'
|
||||
|
||||
import Option from './Option'
|
||||
|
||||
const mockCloseDrawer = jest.fn()
|
||||
const mockToggleDrawer = jest.fn()
|
||||
jest.mock('components/AccountDrawer')
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'debug').mockReturnValue()
|
||||
mocked(useCloseAccountDrawer).mockReturnValue(mockCloseDrawer)
|
||||
mocked(useAccountDrawer).mockReturnValue([true, mockToggleDrawer])
|
||||
})
|
||||
|
||||
const mockConnection1: Connection = {
|
||||
@@ -23,7 +23,7 @@ const mockConnection1: Connection = {
|
||||
deactivate: jest.fn(),
|
||||
} as unknown as Connector,
|
||||
getIcon: () => UNIWALLET_ICON,
|
||||
type: ConnectionType.UNIWALLET,
|
||||
type: ConnectionType.UNISWAP_WALLET,
|
||||
} as unknown as Connection
|
||||
|
||||
const mockConnection2: Connection = {
|
||||
@@ -39,7 +39,7 @@ const mockConnection2: Connection = {
|
||||
describe('Wallet Option', () => {
|
||||
it('renders default state', () => {
|
||||
const component = render(<Option connection={mockConnection1} />)
|
||||
const option = component.getByTestId('wallet-option-UNIWALLET')
|
||||
const option = component.getByTestId('wallet-option-UNISWAP_WALLET')
|
||||
expect(option).toBeEnabled()
|
||||
expect(option).toHaveProperty('selected', false)
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('Wallet Option', () => {
|
||||
<Option connection={mockConnection2} />
|
||||
</>
|
||||
)
|
||||
const option1 = component.getByTestId('wallet-option-UNIWALLET')
|
||||
const option1 = component.getByTestId('wallet-option-UNISWAP_WALLET')
|
||||
const option2 = component.getByTestId('wallet-option-INJECTED')
|
||||
|
||||
expect(option1).toBeEnabled()
|
||||
@@ -71,12 +71,8 @@ describe('Wallet Option', () => {
|
||||
expect(option2).toBeDisabled()
|
||||
expect(option2).toHaveProperty('selected', false)
|
||||
|
||||
expect(mockCloseDrawer).toHaveBeenCalledTimes(0)
|
||||
|
||||
await act(async () => activationResponse.resolve())
|
||||
|
||||
expect(mockCloseDrawer).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(option1).toBeEnabled()
|
||||
expect(option1).toHaveProperty('selected', false)
|
||||
expect(option2).toBeEnabled()
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useCloseAccountDrawer } from 'components/AccountDrawer'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { walletConnectV2Connection } from 'connection'
|
||||
import { ActivationStatus, useActivationState } from 'connection/activate'
|
||||
import { Connection } from 'connection/types'
|
||||
import { Connection, ConnectionType } from 'connection/types'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { MouseEvent, useEffect, useRef, useState } from 'react'
|
||||
import { MoreHorizontal } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import NewBadge from './NewBadge'
|
||||
|
||||
@@ -17,26 +24,17 @@ const OptionCardLeft = styled.div`
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled.button<{ selected: boolean }>`
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border: none;
|
||||
width: 100% !important;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 18px;
|
||||
|
||||
transition: ${({ theme }) => theme.transition.duration.fast};
|
||||
opacity: ${({ disabled, selected }) => (disabled && !selected ? '0.5' : '1')};
|
||||
|
||||
&:hover {
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.hoverState};
|
||||
}
|
||||
&:focus {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.hoverState};
|
||||
}
|
||||
padding: 18px;
|
||||
transition: ${({ theme }) => theme.transition.duration.fast};
|
||||
`
|
||||
|
||||
const HeaderText = styled.div`
|
||||
@@ -48,7 +46,6 @@ const HeaderText = styled.div`
|
||||
font-weight: 600;
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
${flexColumnNoWrap};
|
||||
align-items: center;
|
||||
@@ -62,38 +59,164 @@ const IconWrapper = styled.div`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
const WCv2PopoverContent = styled(ThemeButton)`
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
max-width: 240px;
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 52px;
|
||||
z-index: ${Z_INDEX.popover};
|
||||
`
|
||||
const TOGGLE_SIZE = 24
|
||||
const WCv2PopoverToggle = styled.button`
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: ${TOGGLE_SIZE}px;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
max-width: 48px;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: calc(50% - ${TOGGLE_SIZE / 2}px);
|
||||
width: ${TOGGLE_SIZE}px;
|
||||
|
||||
export default function Option({ connection }: { connection: Connection }) {
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
const Wrapper = styled.div<{ disabled: boolean }>`
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
|
||||
&:hover {
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.hoverState};
|
||||
}
|
||||
&:focus {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.hoverState};
|
||||
}
|
||||
`
|
||||
|
||||
const WCv2Icon = styled.img`
|
||||
height: 20px !important;
|
||||
width: 20px !important;
|
||||
`
|
||||
const WCv2BodyText = styled(ThemedText.BodyPrimary)`
|
||||
margin-bottom: 4px;
|
||||
text-align: left;
|
||||
`
|
||||
const WCv2Caption = styled(ThemedText.Caption)`
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
interface PopupButtonContentProps {
|
||||
connection: Connection
|
||||
isDarkMode: boolean
|
||||
show: boolean
|
||||
onClick: (e: MouseEvent<HTMLButtonElement>) => void
|
||||
onClose: () => void
|
||||
}
|
||||
function PopupButtonContent({ connection, isDarkMode, show, onClick, onClose }: PopupButtonContentProps) {
|
||||
const popoverElement = useRef<HTMLButtonElement>(null)
|
||||
useOnClickOutside(popoverElement, onClose)
|
||||
if (!show) return null
|
||||
return (
|
||||
<WCv2PopoverContent onClick={onClick} ref={popoverElement} size={ButtonSize.small} emphasis={ButtonEmphasis.medium}>
|
||||
<IconWrapper>
|
||||
<WCv2Icon src={connection.getIcon?.(isDarkMode)} alt={connection.getName()} />
|
||||
</IconWrapper>
|
||||
<div>
|
||||
<WCv2BodyText>Connect with v2</WCv2BodyText>
|
||||
<WCv2Caption color="textSecondary">Under development and unsupported by most wallets</WCv2Caption>
|
||||
</div>
|
||||
</WCv2PopoverContent>
|
||||
)
|
||||
}
|
||||
|
||||
interface OptionProps {
|
||||
connection: Connection
|
||||
}
|
||||
export default function Option({ connection }: OptionProps) {
|
||||
const { activationState, tryActivation } = useActivationState()
|
||||
const closeDrawer = useCloseAccountDrawer()
|
||||
const activate = () => tryActivation(connection, closeDrawer)
|
||||
const [WC2PromptOpen, setWC2PromptOpen] = useState(false)
|
||||
const [accountDrawerOpen, toggleAccountDrawerOpen] = useAccountDrawer()
|
||||
const activate = () => tryActivation(connection, toggleAccountDrawerOpen)
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountDrawerOpen) setWC2PromptOpen(false)
|
||||
}, [accountDrawerOpen])
|
||||
|
||||
const isSomeOptionPending = activationState.status === ActivationStatus.PENDING
|
||||
const isCurrentOptionPending = isSomeOptionPending && activationState.connection.type === connection.type
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
const handleClickConnectViaWCv2 = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
tryActivation(walletConnectV2Connection, () => {
|
||||
setWC2PromptOpen(false)
|
||||
toggleAccountDrawerOpen()
|
||||
})
|
||||
}
|
||||
const handleClickOpenWCv2Tooltip = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
setWC2PromptOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.WALLET_SELECTED}
|
||||
properties={{ wallet_type: connection.getName() }}
|
||||
element={InterfaceElementName.WALLET_TYPE_OPTION}
|
||||
>
|
||||
<OptionCardClickable
|
||||
onClick={activate}
|
||||
disabled={isSomeOptionPending}
|
||||
selected={isCurrentOptionPending}
|
||||
data-testid={`wallet-option-${connection.type}`}
|
||||
<Wrapper disabled={isSomeOptionPending}>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.WALLET_SELECTED}
|
||||
properties={{ wallet_type: connection.getName() }}
|
||||
element={InterfaceElementName.WALLET_TYPE_OPTION}
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<IconWrapper>
|
||||
<img src={connection.getIcon?.(isDarkMode)} alt="Icon" />
|
||||
</IconWrapper>
|
||||
<HeaderText>{connection.getName()}</HeaderText>
|
||||
{connection.isNew && <NewBadge />}
|
||||
</OptionCardLeft>
|
||||
{isCurrentOptionPending && <Loader />}
|
||||
</OptionCardClickable>
|
||||
</TraceEvent>
|
||||
<OptionCardClickable
|
||||
disabled={isSomeOptionPending}
|
||||
onClick={activate}
|
||||
selected={isCurrentOptionPending}
|
||||
data-testid={`wallet-option-${connection.type}`}
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<IconWrapper>
|
||||
<img src={connection.getIcon?.(isDarkMode)} alt={connection.getName()} />
|
||||
</IconWrapper>
|
||||
<HeaderText>{connection.getName()}</HeaderText>
|
||||
{connection.isNew && <NewBadge />}
|
||||
</OptionCardLeft>
|
||||
{isCurrentOptionPending && <Loader />}
|
||||
</OptionCardClickable>
|
||||
</TraceEvent>
|
||||
|
||||
{connection.type === ConnectionType.WALLET_CONNECT && (
|
||||
<>
|
||||
<WCv2PopoverToggle onClick={handleClickOpenWCv2Tooltip} onMouseDown={handleClickOpenWCv2Tooltip}>
|
||||
<MoreHorizontal />
|
||||
</WCv2PopoverToggle>
|
||||
<PopupButtonContent
|
||||
connection={connection}
|
||||
isDarkMode={isDarkMode}
|
||||
show={WC2PromptOpen}
|
||||
onClick={handleClickConnectViaWCv2}
|
||||
onClose={() => setWC2PromptOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,37 +19,31 @@ exports[`Wallet Option renders default state 1`] = `
|
||||
}
|
||||
|
||||
.c0 {
|
||||
background-color: #F5F6FC;
|
||||
border: none;
|
||||
width: 100% !important;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: unset;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex: 1 1 auto;
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
opacity: 1;
|
||||
padding: 18px;
|
||||
-webkit-transition: 125ms;
|
||||
transition: 125ms;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.c0:hover {
|
||||
cursor: pointer;
|
||||
background-color: #ADBCFF3d;
|
||||
}
|
||||
|
||||
.c0:focus {
|
||||
background-color: #ADBCFF3d;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
@@ -109,7 +103,7 @@ exports[`Wallet Option renders default state 1`] = `
|
||||
|
||||
<button
|
||||
class="c0"
|
||||
data-testid="wallet-option-UNIWALLET"
|
||||
data-testid="wallet-option-UNISWAP_WALLET"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
@@ -118,7 +112,7 @@ exports[`Wallet Option renders default state 1`] = `
|
||||
class="c2"
|
||||
>
|
||||
<img
|
||||
alt="Icon"
|
||||
alt="Mock Connection 1"
|
||||
src="uniwallet.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-e
|
||||
import { initializeConnector, MockEIP1193Provider } from '@web3-react/core'
|
||||
import { EIP1193 } from '@web3-react/eip1193'
|
||||
import { Provider as EIP1193Provider } from '@web3-react/types'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { Connection, ConnectionType } from 'connection/types'
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
@@ -21,7 +21,7 @@ jest.mock('@uniswap/analytics', () => ({
|
||||
}))
|
||||
jest.mock('connection', () => {
|
||||
const { ConnectionType } = jest.requireActual('connection')
|
||||
return { ConnectionType, useGetConnection: jest.fn() }
|
||||
return { ConnectionType, getConnection: jest.fn() }
|
||||
})
|
||||
jest.mock('hooks/useEagerlyConnect', () => jest.fn())
|
||||
jest.mock('hooks/useOrderedConnections', () => jest.fn())
|
||||
@@ -72,7 +72,7 @@ describe('Web3Provider', () => {
|
||||
|
||||
describe('analytics', () => {
|
||||
beforeEach(() => {
|
||||
mocked(useGetConnection).mockReturnValue(jest.fn().mockReturnValue(connection))
|
||||
mocked(getConnection).mockReturnValue(connection)
|
||||
})
|
||||
|
||||
it('sends event when the active account changes', async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from
|
||||
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
|
||||
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
@@ -54,7 +54,6 @@ function Updater() {
|
||||
|
||||
// Send analytics events when the active account changes.
|
||||
const previousAccount = usePrevious(account)
|
||||
const getConnection = useGetConnection()
|
||||
const [connectedWallets, addConnectedWallet] = useConnectedWallets()
|
||||
useEffect(() => {
|
||||
if (account && account !== previousAccount) {
|
||||
@@ -85,17 +84,7 @@ function Updater() {
|
||||
|
||||
addConnectedWallet({ account, walletType })
|
||||
}
|
||||
}, [
|
||||
account,
|
||||
addConnectedWallet,
|
||||
currentPage,
|
||||
chainId,
|
||||
connectedWallets,
|
||||
connector,
|
||||
getConnection,
|
||||
previousAccount,
|
||||
provider,
|
||||
])
|
||||
}, [account, addConnectedWallet, currentPage, chainId, connectedWallets, connector, previousAccount, provider])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -6,13 +6,12 @@ import PortfolioDrawer, { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { navSearchInputVisibleSize } from 'hooks/useScreenSize'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { colors } from 'theme/colors'
|
||||
import { flexRowNoWrap } from 'theme/styles'
|
||||
@@ -42,16 +41,6 @@ const Web3StatusGeneric = styled(ButtonSecondary)`
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
const Web3StatusError = styled(Web3StatusGeneric)`
|
||||
background-color: ${({ theme }) => theme.accentFailure};
|
||||
border: 1px solid ${({ theme }) => theme.accentFailure};
|
||||
color: ${({ theme }) => theme.white};
|
||||
font-weight: 500;
|
||||
:hover,
|
||||
:focus {
|
||||
background-color: ${({ theme }) => darken(0.1, theme.accentFailure)};
|
||||
}
|
||||
`
|
||||
|
||||
const Web3StatusConnectWrapper = styled.div<{ faded?: boolean }>`
|
||||
${flexRowNoWrap};
|
||||
@@ -107,7 +96,7 @@ const Web3StatusConnected = styled(Web3StatusGeneric)<{
|
||||
const AddressAndChevronContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.navSearchInputVisible}px`}) {
|
||||
@media only screen and (max-width: ${navSearchInputVisibleSize}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
@@ -123,13 +112,6 @@ const Text = styled.p`
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const NetworkIcon = styled(AlertTriangle)`
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`
|
||||
|
||||
// we want the latest one to come first, so return negative if a is after b
|
||||
function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) {
|
||||
return b.addedTime - a.addedTime
|
||||
@@ -149,7 +131,6 @@ const StyledConnectButton = styled.button`
|
||||
|
||||
function Web3StatusInner() {
|
||||
const { account, connector, chainId, ENSName } = useWeb3React()
|
||||
const getConnection = useGetConnection()
|
||||
const connection = getConnection(connector)
|
||||
const [, toggleAccountDrawer] = useAccountDrawer()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
@@ -158,8 +139,6 @@ function Web3StatusInner() {
|
||||
}, [toggleAccountDrawer])
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const sortedRecentTransactions = useMemo(() => {
|
||||
@@ -173,15 +152,6 @@ function Web3StatusInner() {
|
||||
|
||||
if (!chainId) {
|
||||
return null
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={handleWalletDropdownClick}>
|
||||
<NetworkIcon />
|
||||
<Text>
|
||||
<Trans>Error</Trans>
|
||||
</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else if (account) {
|
||||
return (
|
||||
<TraceEvent
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('PendingModalContent', () => {
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Enable spending limits for ABC on Uniswap')).toBeInTheDocument()
|
||||
expect(screen.getByText('Enable spending ABC on Uniswap')).toBeInTheDocument()
|
||||
expect(screen.getByText('Proceed in your wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('Why is this required?')).toBeInTheDocument()
|
||||
})
|
||||
@@ -46,7 +46,7 @@ describe('PendingModalContent', () => {
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('Enable spending limits for ABC on Uniswap')).toBeInTheDocument()
|
||||
expect(screen.getByText('Enable spending ABC on Uniswap')).toBeInTheDocument()
|
||||
expect(screen.getByText('Proceed in your wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('Why is this required?')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Allow ABC to be used for swapping')).not.toBeInTheDocument()
|
||||
@@ -67,7 +67,7 @@ describe('PendingModalContent', () => {
|
||||
expect(screen.getByText('Allow ABC to be used for swapping')).toBeInTheDocument()
|
||||
expect(screen.getByText('Proceed in your wallet')).toBeInTheDocument()
|
||||
expect(screen.getByText('Why is this required?')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Enable spending limits for ABC on Uniswap')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Enable spending ABC on Uniswap')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { TradeSummary } from './TradeSummary'
|
||||
|
||||
export const PendingModalContainer = styled(ColumnCenter)`
|
||||
margin: 48px 0 28px;
|
||||
margin: 48px 0 8px;
|
||||
`
|
||||
|
||||
const HeaderContainer = styled(ColumnCenter)<{ $disabled?: boolean }>`
|
||||
@@ -123,7 +123,7 @@ function getContent(args: ContentArgs): PendingModalStep {
|
||||
switch (step) {
|
||||
case ConfirmModalState.APPROVING_TOKEN:
|
||||
return {
|
||||
title: t`Enable spending limits for ${approvalCurrency?.symbol ?? 'this token'} on Uniswap`,
|
||||
title: t`Enable spending ${approvalCurrency?.symbol ?? 'this token'} on Uniswap`,
|
||||
subtitle: (
|
||||
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/8120520483085">
|
||||
<Trans>Why is this required?</Trans>
|
||||
@@ -249,7 +249,7 @@ export function PendingModalContent({
|
||||
</Row>
|
||||
</HeaderContainer>
|
||||
{button && <Row justify="center">{button}</Row>}
|
||||
{!hideStepIndicators && (
|
||||
{!hideStepIndicators && !showSuccess && (
|
||||
<Row gap="14px" justify="center">
|
||||
{steps.map((_, i) => {
|
||||
return <StepCircle key={i} active={steps.indexOf(currentStep) === i} />
|
||||
|
||||
@@ -4,7 +4,8 @@ import { act, render, screen } from 'test-utils/render'
|
||||
|
||||
import SwapDetailsDropdown from './SwapDetailsDropdown'
|
||||
|
||||
describe('SwapDetailsDropdown.tsx', () => {
|
||||
// TODO(WEB-2120): Reenable tests after mocking trade fetch in this file
|
||||
describe.skip('SwapDetailsDropdown.tsx', () => {
|
||||
it('renders a trade', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapDetailsDropdown
|
||||
|
||||
@@ -18,7 +18,7 @@ const HeaderButtonContainer = styled(RowFixed)`
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
export default function SwapHeader({ autoSlippage, chainId }: { autoSlippage: Percent; chainId?: number }) {
|
||||
const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
|
||||
|
||||
return (
|
||||
@@ -30,7 +30,7 @@ export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent })
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</HeaderButtonContainer>
|
||||
<RowFixed>
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} />
|
||||
</RowFixed>
|
||||
</StyledSwapHeader>
|
||||
)
|
||||
|
||||
51
src/connection/WalletConnectV2.ts
Normal file
51
src/connection/WalletConnectV2.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { WalletConnect, WalletConnectConstructorArgs } from '@web3-react/walletconnect-v2'
|
||||
import { L1_CHAIN_IDS, L2_CHAIN_IDS, SupportedChainId } from 'constants/chains'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { RPC_URLS } from '../constants/networks'
|
||||
|
||||
// Avoid testing for the best URL by only passing a single URL per chain.
|
||||
// Otherwise, WC will not initialize until all URLs have been tested (see getBestUrl in web3-react).
|
||||
const RPC_URLS_WITHOUT_FALLBACKS = Object.entries(RPC_URLS).reduce(
|
||||
(map, [chainId, urls]) => ({
|
||||
...map,
|
||||
[chainId]: urls[0],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
const optionalChains = [...L1_CHAIN_IDS, ...L2_CHAIN_IDS].filter((x) => x !== SupportedChainId.MAINNET)
|
||||
|
||||
export class WalletConnectV2Popup extends WalletConnect {
|
||||
ANALYTICS_EVENT = 'Wallet Connect QR Scan'
|
||||
constructor({
|
||||
actions,
|
||||
onError,
|
||||
qrcode = true,
|
||||
}: Omit<WalletConnectConstructorArgs, 'options'> & { qrcode?: boolean }) {
|
||||
const darkmode = Boolean(window.matchMedia('(prefers-color-scheme: dark)'))
|
||||
super({
|
||||
actions,
|
||||
options: {
|
||||
projectId: process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID as string,
|
||||
optionalChains,
|
||||
chains: [SupportedChainId.MAINNET],
|
||||
showQrModal: qrcode,
|
||||
rpcMap: RPC_URLS_WITHOUT_FALLBACKS,
|
||||
qrModalOptions: {
|
||||
themeMode: darkmode ? 'dark' : 'light',
|
||||
themeVariables: {
|
||||
'--w3m-font-family': '"Inter custom", sans-serif',
|
||||
'--w3m-z-index': Z_INDEX.modal.toString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
onError,
|
||||
})
|
||||
}
|
||||
|
||||
activate(chainId?: number) {
|
||||
sendAnalyticsEvent(this.ANALYTICS_EVENT)
|
||||
return super.activate(chainId)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
|
||||
import { getConnections, useGetConnection } from 'connection'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
import { getConnection, getConnections } from 'connection'
|
||||
|
||||
import { ConnectionType } from './types'
|
||||
|
||||
@@ -16,10 +15,9 @@ describe('connection utility/metadata tests', () => {
|
||||
global.window.ethereum = ethereum
|
||||
|
||||
const displayed = getConnections().filter((c) => c.shouldDisplay())
|
||||
const getConnection = renderHook(() => useGetConnection()).result.current
|
||||
const injected = getConnection(ConnectionType.INJECTED)
|
||||
const coinbase = getConnection(ConnectionType.COINBASE_WALLET)
|
||||
const uniswap = getConnection(ConnectionType.UNIWALLET)
|
||||
const uniswap = getConnection(ConnectionType.UNISWAP_WALLET)
|
||||
const walletconnect = getConnection(ConnectionType.WALLET_CONNECT)
|
||||
|
||||
return { displayed, injected, coinbase, uniswap, walletconnect }
|
||||
|
||||
@@ -13,7 +13,6 @@ import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
|
||||
import UNISWAP_LOGO from 'assets/svg/logo.svg'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useCallback } from 'react'
|
||||
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
|
||||
import { RPC_URLS } from '../constants/networks'
|
||||
@@ -21,6 +20,7 @@ import { RPC_PROVIDERS } from '../constants/providers'
|
||||
import { Connection, ConnectionType } from './types'
|
||||
import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
|
||||
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
|
||||
import { WalletConnectV2Popup } from './WalletConnectV2'
|
||||
|
||||
function onError(error: Error) {
|
||||
console.debug(`web3-react error: ${error}`)
|
||||
@@ -87,6 +87,18 @@ export const walletConnectConnection: Connection = {
|
||||
shouldDisplay: () => !getIsInjectedMobileBrowser(),
|
||||
}
|
||||
|
||||
const [web3WalletConnectV2, web3WalletConnectV2Hooks] = initializeConnector<WalletConnectV2Popup>(
|
||||
(actions) => new WalletConnectV2Popup({ actions, onError })
|
||||
)
|
||||
export const walletConnectV2Connection: Connection = {
|
||||
getName: () => 'WalletConnectV2',
|
||||
connector: web3WalletConnectV2,
|
||||
hooks: web3WalletConnectV2Hooks,
|
||||
type: ConnectionType.WALLET_CONNECT_V2,
|
||||
getIcon: () => WALLET_CONNECT_ICON,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
const [web3UniwalletConnect, web3UniwalletConnectHooks] = initializeConnector<UniwalletConnect>(
|
||||
(actions) => new UniwalletConnect({ actions, onError })
|
||||
)
|
||||
@@ -94,7 +106,7 @@ export const uniwalletConnectConnection: Connection = {
|
||||
getName: () => 'Uniswap Wallet',
|
||||
connector: web3UniwalletConnect,
|
||||
hooks: web3UniwalletConnectHooks,
|
||||
type: ConnectionType.UNIWALLET,
|
||||
type: ConnectionType.UNISWAP_WALLET,
|
||||
getIcon: () => UNIWALLET_ICON,
|
||||
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
|
||||
isNew: true,
|
||||
@@ -137,35 +149,36 @@ export function getConnections() {
|
||||
uniwalletConnectConnection,
|
||||
injectedConnection,
|
||||
walletConnectConnection,
|
||||
walletConnectV2Connection,
|
||||
coinbaseWalletConnection,
|
||||
gnosisSafeConnection,
|
||||
networkConnection,
|
||||
]
|
||||
}
|
||||
|
||||
export function useGetConnection() {
|
||||
return useCallback((c: Connector | ConnectionType) => {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections().find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
return connection
|
||||
} else {
|
||||
switch (c) {
|
||||
case ConnectionType.INJECTED:
|
||||
return injectedConnection
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
return coinbaseWalletConnection
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
return walletConnectConnection
|
||||
case ConnectionType.UNIWALLET:
|
||||
return uniwalletConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
export function getConnection(c: Connector | ConnectionType) {
|
||||
if (c instanceof Connector) {
|
||||
const connection = getConnections().find((connection) => connection.connector === c)
|
||||
if (!connection) {
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
}, [])
|
||||
return connection
|
||||
} else {
|
||||
switch (c) {
|
||||
case ConnectionType.INJECTED:
|
||||
return injectedConnection
|
||||
case ConnectionType.COINBASE_WALLET:
|
||||
return coinbaseWalletConnection
|
||||
case ConnectionType.WALLET_CONNECT:
|
||||
return walletConnectConnection
|
||||
case ConnectionType.WALLET_CONNECT_V2:
|
||||
return walletConnectV2Connection
|
||||
case ConnectionType.UNISWAP_WALLET:
|
||||
return uniwalletConnectConnection
|
||||
case ConnectionType.NETWORK:
|
||||
return networkConnection
|
||||
case ConnectionType.GNOSIS_SAFE:
|
||||
return gnosisSafeConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import { Web3ReactHooks } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
|
||||
export enum ConnectionType {
|
||||
UNIWALLET = 'UNIWALLET',
|
||||
UNISWAP_WALLET = 'UNISWAP_WALLET',
|
||||
INJECTED = 'INJECTED',
|
||||
COINBASE_WALLET = 'COINBASE_WALLET',
|
||||
WALLET_CONNECT = 'WALLET_CONNECT',
|
||||
WALLET_CONNECT_V2 = 'WALLET_CONNECT_V2',
|
||||
NETWORK = 'NETWORK',
|
||||
GNOSIS_SAFE = 'GNOSIS_SAFE',
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export enum ErrorCode {
|
||||
CHAIN_NOT_ADDED = 4902,
|
||||
MM_ALREADY_PENDING = -32002,
|
||||
|
||||
WC_V2_MODAL_CLOSED = 'Error: Connection request reset. Please try again.',
|
||||
WC_MODAL_CLOSED = 'Error: User closed modal',
|
||||
CB_REJECTED_REQUEST = 'Error: User denied account authorization',
|
||||
}
|
||||
@@ -32,6 +33,7 @@ export enum ErrorCode {
|
||||
export function didUserReject(connection: Connection, error: any): boolean {
|
||||
return (
|
||||
error?.code === ErrorCode.USER_REJECTED_REQUEST ||
|
||||
(connection.type === ConnectionType.WALLET_CONNECT_V2 && error?.toString?.() === ErrorCode.WC_V2_MODAL_CLOSED) ||
|
||||
(connection.type === ConnectionType.WALLET_CONNECT && error?.toString?.() === ErrorCode.WC_MODAL_CLOSED) ||
|
||||
(connection.type === ConnectionType.COINBASE_WALLET && error?.toString?.() === ErrorCode.CB_REJECTED_REQUEST)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import bnbCircleLogoUrl from 'assets/images/bnbCircle.svg'
|
||||
import celoCircleLogoUrl from 'assets/images/celoCircle.png'
|
||||
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
|
||||
import polygonCircleLogoUrl from 'assets/images/polygonCircle.png'
|
||||
import { default as arbitrumCircleLogoUrl, default as arbitrumLogoUrl } from 'assets/svg/arbitrum_logo.svg'
|
||||
@@ -190,7 +189,7 @@ const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/celo/',
|
||||
label: 'Celo',
|
||||
logoUrl: celoLogo,
|
||||
circleLogoUrl: celoCircleLogoUrl,
|
||||
circleLogoUrl: celoLogo,
|
||||
squareLogoUrl: celoSquareLogoUrl,
|
||||
nativeCurrency: { name: 'Celo', symbol: 'CELO', decimals: 18 },
|
||||
defaultListUrl: CELO_LIST,
|
||||
|
||||
@@ -78,6 +78,12 @@ const BlockedWarning: Warning = {
|
||||
canProceed: false,
|
||||
}
|
||||
|
||||
export const NotFoundWarning: Warning = {
|
||||
level: WARNING_LEVEL.UNKNOWN,
|
||||
message: <Trans>Token not found</Trans>,
|
||||
canProceed: false,
|
||||
}
|
||||
|
||||
export function checkWarning(tokenAddress: string) {
|
||||
if (tokenAddress === NATIVE_CHAIN_ID || tokenAddress === ZERO_ADDRESS) {
|
||||
return null
|
||||
@@ -103,3 +109,7 @@ export function checkSearchTokenWarning(token: SearchToken) {
|
||||
}
|
||||
return checkWarning(token.address)
|
||||
}
|
||||
|
||||
export function displayWarningLabel(warning: Warning | null) {
|
||||
return warning && warning.level !== WARNING_LEVEL.MEDIUM
|
||||
}
|
||||
|
||||
@@ -489,7 +489,7 @@ function getCeloNativeCurrency(chainId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function isMatic(chainId: number): chainId is SupportedChainId.POLYGON | SupportedChainId.POLYGON_MUMBAI {
|
||||
export function isMatic(chainId: number): chainId is SupportedChainId.POLYGON | SupportedChainId.POLYGON_MUMBAI {
|
||||
return chainId === SupportedChainId.POLYGON_MUMBAI || chainId === SupportedChainId.POLYGON
|
||||
}
|
||||
|
||||
@@ -511,7 +511,7 @@ class MaticNativeCurrency extends NativeCurrency {
|
||||
}
|
||||
}
|
||||
|
||||
function isBsc(chainId: number): chainId is SupportedChainId.BNB {
|
||||
export function isBsc(chainId: number): chainId is SupportedChainId.BNB {
|
||||
return chainId === SupportedChainId.BNB
|
||||
}
|
||||
|
||||
|
||||
38
src/graphql/data/nft/Details.test.ts
Normal file
38
src/graphql/data/nft/Details.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
import { useNftAssetDetails } from './Details'
|
||||
|
||||
describe('useNftAssetDetails', () => {
|
||||
it('should handle listing.price.value of 1e-18 without crashing', () => {
|
||||
// Mock the useDetailsQuery hook
|
||||
const mockUseDetailsQuery = jest.fn(() => ({
|
||||
data: {
|
||||
nftAssets: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
listings: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
price: {
|
||||
value: 1e-18,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
}))
|
||||
jest.mock('../__generated__/types-and-hooks', () => ({
|
||||
useDetailsQuery: mockUseDetailsQuery,
|
||||
}))
|
||||
const { result } = renderHook(() => useNftAssetDetails('address', 'tokenId'))
|
||||
expect(result.current.data[0].priceInfo.ETHPrice).toBe('0')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import gql from 'graphql-tag'
|
||||
import { CollectionInfoForAsset, GenieAsset, Markets, SellOrder } from 'nft/types'
|
||||
import { wrapScientificNotation } from 'nft/utils'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { NftAsset, useDetailsQuery } from '../__generated__/types-and-hooks'
|
||||
@@ -105,7 +106,7 @@ export function useNftAssetDetails(
|
||||
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()
|
||||
const ethPrice = parseEther(wrapScientificNotation(listing?.price?.value?.toString() ?? '0')).toString()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -123,6 +123,7 @@ export function validateUrlChainParam(chainName: string | undefined) {
|
||||
export const CHAIN_NAME_TO_CHAIN_ID: { [key in Chain]: SupportedChainId } = {
|
||||
[Chain.Ethereum]: SupportedChainId.MAINNET,
|
||||
[Chain.EthereumGoerli]: SupportedChainId.GOERLI,
|
||||
[Chain.EthereumSepolia]: SupportedChainId.SEPOLIA,
|
||||
[Chain.Polygon]: SupportedChainId.POLYGON,
|
||||
[Chain.Celo]: SupportedChainId.CELO,
|
||||
[Chain.Optimism]: SupportedChainId.OPTIMISM,
|
||||
|
||||
16
src/hooks/__snapshots__/useScreenSize.test.ts.snap
Normal file
16
src/hooks/__snapshots__/useScreenSize.test.ts.snap
Normal file
@@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`useScreenSize returns the right initial value based on breakpoints 1`] = `
|
||||
Object {
|
||||
"current": Object {
|
||||
"lg": true,
|
||||
"md": true,
|
||||
"navSearchInputVisible": false,
|
||||
"sm": true,
|
||||
"xl": false,
|
||||
"xs": true,
|
||||
"xxl": false,
|
||||
"xxxl": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -1,9 +1,11 @@
|
||||
import TokenLogoLookupTable from 'constants/TokenLogoLookupTable'
|
||||
import { isCelo, nativeOnChain } from 'constants/tokens'
|
||||
import { chainIdToNetworkName, getNativeLogoURI } from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { isAddress } from 'utils'
|
||||
|
||||
import celoLogo from '../assets/svg/celo_logo.svg'
|
||||
import { checkWarning } from '../constants/tokenSafety'
|
||||
|
||||
const BAD_SRCS: { [tokenAddress: string]: true } = {}
|
||||
@@ -41,6 +43,11 @@ function getInitialUrl(address?: string | null, chainId?: number | null, isNativ
|
||||
|
||||
const networkName = chainId ? chainIdToNetworkName(chainId) : 'ethereum'
|
||||
const checksummedAddress = isAddress(address)
|
||||
|
||||
if (chainId && isCelo(chainId) && address === nativeOnChain(chainId).wrapped.address) {
|
||||
return celoLogo
|
||||
}
|
||||
|
||||
if (checksummedAddress) {
|
||||
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${checksummedAddress}/logo.png`
|
||||
} else {
|
||||
|
||||
@@ -36,9 +36,10 @@ export function useBestTrade(
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
|
||||
const debouncedSwapQuoteFlagEnabled = useDebounceSwapQuoteFlag() === DebounceSwapQuoteVariant.Enabled
|
||||
const [debouncedAmount, debouncedOtherCurrency] = useDebounce(
|
||||
useMemo(() => [amountSpecified, otherCurrency], [amountSpecified, otherCurrency]),
|
||||
useDebounceSwapQuoteFlag() === DebounceSwapQuoteVariant.Enabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME
|
||||
debouncedSwapQuoteFlagEnabled ? DEBOUNCE_TIME_INCREASED : DEBOUNCE_TIME
|
||||
)
|
||||
|
||||
const isAWrapTransaction = useMemo(() => {
|
||||
@@ -60,7 +61,9 @@ export function useBestTrade(
|
||||
routerPreference
|
||||
)
|
||||
|
||||
const isLoading = routingAPITrade.state === TradeState.LOADING
|
||||
const inDebounce =
|
||||
(!debouncedAmount && Boolean(amountSpecified)) || (!debouncedOtherCurrency && Boolean(otherCurrency))
|
||||
const isLoading = routingAPITrade.state === TradeState.LOADING || inDebounce
|
||||
const useFallback = (!autoRouterSupported || routingAPITrade.state === TradeState.NO_ROUTE_FOUND) && shouldGetTrade
|
||||
|
||||
// only use client side router if routing api trade failed or is not supported
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { gnosisSafeConnection, networkConnection } from 'connection'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { Connection } from 'connection/types'
|
||||
import { useEffect } from 'react'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
@@ -22,7 +22,6 @@ export default function useEagerlyConnect() {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const selectedWallet = useAppSelector((state) => state.user.selectedWallet)
|
||||
const getConnection = useGetConnection()
|
||||
|
||||
let selectedConnection: Connection | undefined
|
||||
if (selectedWallet) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { ConnectionType } from 'connection/types'
|
||||
import { useMemo } from 'react'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
|
||||
const SELECTABLE_WALLETS = [
|
||||
ConnectionType.UNIWALLET,
|
||||
ConnectionType.UNISWAP_WALLET,
|
||||
ConnectionType.INJECTED,
|
||||
ConnectionType.WALLET_CONNECT,
|
||||
ConnectionType.COINBASE_WALLET,
|
||||
@@ -12,7 +12,7 @@ const SELECTABLE_WALLETS = [
|
||||
|
||||
export default function useOrderedConnections() {
|
||||
const selectedWallet = useAppSelector((state) => state.user.selectedWallet)
|
||||
const getConnection = useGetConnection()
|
||||
|
||||
return useMemo(() => {
|
||||
const orderedConnectionTypes: ConnectionType[] = []
|
||||
|
||||
@@ -29,5 +29,5 @@ export default function useOrderedConnections() {
|
||||
orderedConnectionTypes.push(ConnectionType.NETWORK)
|
||||
|
||||
return orderedConnectionTypes.map((connectionType) => getConnection(connectionType))
|
||||
}, [getConnection, selectedWallet])
|
||||
}, [selectedWallet])
|
||||
}
|
||||
|
||||
10
src/hooks/useScreenSize.test.ts
Normal file
10
src/hooks/useScreenSize.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
import { useScreenSize } from './useScreenSize'
|
||||
|
||||
describe('useScreenSize', () => {
|
||||
it('returns the right initial value based on breakpoints', () => {
|
||||
const { result } = renderHook(() => useScreenSize())
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -9,17 +9,28 @@ import { BREAKPOINTS } from 'theme'
|
||||
|
||||
const isClient = typeof window !== 'undefined'
|
||||
|
||||
function getScreenSize(): Record<keyof typeof BREAKPOINTS, boolean> {
|
||||
return Object.keys(BREAKPOINTS).reduce(
|
||||
export const navSearchInputVisibleSize = 1100
|
||||
|
||||
// for breakpoints that are not meant to be used except for in marginal areas of the app
|
||||
// we don't want to expose the types everywhere, just make them available via this hook
|
||||
const BREAKPOINTS_ADDITIONAL = {
|
||||
...BREAKPOINTS,
|
||||
navSearchInputVisible: navSearchInputVisibleSize,
|
||||
}
|
||||
|
||||
function getScreenSize(): Record<keyof typeof BREAKPOINTS_ADDITIONAL, boolean> {
|
||||
return Object.keys(BREAKPOINTS_ADDITIONAL).reduce(
|
||||
(obj, key) =>
|
||||
Object.assign(obj, {
|
||||
[key]: isClient ? window.innerWidth >= BREAKPOINTS[key as keyof typeof BREAKPOINTS] : false,
|
||||
[key]: isClient
|
||||
? window.innerWidth >= BREAKPOINTS_ADDITIONAL[key as keyof typeof BREAKPOINTS_ADDITIONAL]
|
||||
: false,
|
||||
}),
|
||||
{} as Record<keyof typeof BREAKPOINTS, boolean>
|
||||
{} as Record<keyof typeof BREAKPOINTS_ADDITIONAL, boolean>
|
||||
)
|
||||
}
|
||||
|
||||
export function useScreenSize(): Record<keyof typeof BREAKPOINTS, boolean> {
|
||||
export function useScreenSize(): Record<keyof typeof BREAKPOINTS_ADDITIONAL, boolean> {
|
||||
const [screenSize, setScreenSize] = useState(getScreenSize())
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { getConnection } from 'connection'
|
||||
import { didUserReject } from 'connection/utils'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useCallback } from 'react'
|
||||
import { addPopup } from 'state/application/reducer'
|
||||
import { updateConnectionError } from 'state/connection/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
export default function useSelectChain() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector } = useWeb3React()
|
||||
const getConnection = useGetConnection()
|
||||
|
||||
return useCallback(
|
||||
async (targetChain: SupportedChainId) => {
|
||||
if (!connector) return
|
||||
|
||||
const connectionType = getConnection(connector).type
|
||||
const connection = getConnection(connector)
|
||||
|
||||
try {
|
||||
dispatch(updateConnectionError({ connectionType, error: undefined }))
|
||||
await switchChain(connector, targetChain)
|
||||
} catch (error) {
|
||||
if (didUserReject(connection, error)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error('Failed to switch networks', error)
|
||||
|
||||
dispatch(updateConnectionError({ connectionType, error: error.message }))
|
||||
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: 'failed-network-switch' }))
|
||||
}
|
||||
},
|
||||
[connector, dispatch, getConnection]
|
||||
[connector, dispatch]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PermitSignature } from 'hooks/usePermitAllowance'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { TransactionType } from '../state/transactions/types'
|
||||
import { TransactionInfo, TransactionType } from '../state/transactions/types'
|
||||
import { currencyId } from '../utils/currencyId'
|
||||
import useTransactionDeadline from './useTransactionDeadline'
|
||||
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
|
||||
@@ -30,33 +30,30 @@ export function useSwapCallback(
|
||||
|
||||
const callback = useMemo(() => {
|
||||
if (!trade || !swapCallback) return null
|
||||
const info: TransactionInfo = {
|
||||
type: TransactionType.SWAP,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
...(trade.tradeType === TradeType.EXACT_INPUT
|
||||
? {
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
|
||||
}
|
||||
: {
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
|
||||
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
}),
|
||||
}
|
||||
return () =>
|
||||
swapCallback().then((response) => {
|
||||
addTransaction(
|
||||
response,
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
|
||||
}
|
||||
: {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
}
|
||||
)
|
||||
addTransaction(response, info, deadline?.toNumber())
|
||||
return response.hash
|
||||
})
|
||||
}, [addTransaction, allowedSlippage, swapCallback, trade])
|
||||
}, [addTransaction, allowedSlippage, deadline, swapCallback, trade])
|
||||
|
||||
return {
|
||||
callback,
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('Token Warning Colors', () => {
|
||||
describe('useTokenWarningColor', () => {
|
||||
it('medium', () => {
|
||||
const { result } = renderHook(() => useTokenWarningColor(WARNING_LEVEL.MEDIUM))
|
||||
expect(result.current).toEqual(lightTheme.accentWarningSoft)
|
||||
expect(result.current).toEqual(lightTheme.backgroundFloating)
|
||||
})
|
||||
|
||||
it('strong', () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useTokenWarningColor = (level: WARNING_LEVEL) => {
|
||||
|
||||
switch (level) {
|
||||
case WARNING_LEVEL.MEDIUM:
|
||||
return theme.accentWarningSoft
|
||||
return theme.backgroundFloating
|
||||
case WARNING_LEVEL.UNKNOWN:
|
||||
return theme.accentFailureSoft
|
||||
case WARNING_LEVEL.BLOCKED:
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import { RefObject, useEffect } from 'react'
|
||||
|
||||
function isAnimating(node?: Animatable | Document) {
|
||||
/**
|
||||
* Checks whether a given node is currently animating.
|
||||
*
|
||||
* @param node - The node to check for ongoing animations.
|
||||
* @returns - true if the node is animating; false otherwise.
|
||||
*/
|
||||
function isAnimating(node?: Animatable | Document): boolean {
|
||||
return (node?.getAnimations?.().length ?? 0) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook runs an unmounting animation on a specified node.
|
||||
*
|
||||
* The hook will also run the animation on any additional elements specified in
|
||||
* the `animatedElements` parameter. If no additional elements are specified,
|
||||
* the animation will only run on the provided node.
|
||||
*
|
||||
* After any of the animated elements have completed their animation, `node` is removed from its parent.
|
||||
*
|
||||
* @param node - The node to animate and remove.
|
||||
* @param getAnimatingClass - A function that returns the CSS class to add to the animating elements.
|
||||
* @param animatedElements - Additional elements to animate.
|
||||
* @param skip - Whether to skip the animation and remove the node immediately.
|
||||
*/
|
||||
export function useUnmountingAnimation(
|
||||
node: RefObject<HTMLElement>,
|
||||
getAnimatingClass: () => string,
|
||||
@@ -12,16 +32,26 @@ export function useUnmountingAnimation(
|
||||
) {
|
||||
useEffect(() => {
|
||||
const current = node.current
|
||||
|
||||
// Gather all elements to animate, defaulting to the current node if none are specified.
|
||||
const animated = animatedElements?.map((element) => element.current) ?? [current]
|
||||
const parent = current?.parentElement
|
||||
const removeChild = parent?.removeChild
|
||||
|
||||
// If we can't remove the child or skipping is requested, stop here.
|
||||
if (!(parent && removeChild) || skip) return
|
||||
|
||||
// Override the parent's removeChild function to add our animation logic
|
||||
parent.removeChild = function <T extends Node>(child: T) {
|
||||
// If the current child is the one being removed and it's supposed to animate
|
||||
if ((child as Node) === current && animated) {
|
||||
// Add animation class to all elements
|
||||
animated.forEach((element) => element?.classList.add(getAnimatingClass()))
|
||||
|
||||
// Check if any of the animated elements is animating
|
||||
const animating = animated.find((element) => isAnimating(element ?? undefined))
|
||||
if (animating) {
|
||||
// If an element is animating, we wait for the animation to end before removing the child
|
||||
animating?.addEventListener('animationend', (x) => {
|
||||
// This check is needed because the animationend event will fire for all animations on the
|
||||
// element or its children.
|
||||
@@ -30,13 +60,18 @@ export function useUnmountingAnimation(
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// If no element is animating, we remove the child immediately
|
||||
removeChild.call(parent, child)
|
||||
}
|
||||
// We've handled the removal, so we return the child
|
||||
return child
|
||||
} else {
|
||||
// If the child isn't the one we're supposed to animate, remove it normally
|
||||
return removeChild.call(parent, child) as T
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the removeChild function to its original value when the component is unmounted
|
||||
return () => {
|
||||
parent.removeChild = removeChild
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { retry, RetryableError } from './retry'
|
||||
import { CanceledError, retry, RetryableError } from './retry'
|
||||
|
||||
describe('retry', () => {
|
||||
function makeFn<T>(fails: number, result: T, retryable = true): () => Promise<T> {
|
||||
@@ -32,7 +32,7 @@ describe('retry', () => {
|
||||
it('cancel causes promise to reject', async () => {
|
||||
const { promise, cancel } = retry(makeFn(2, 'abc'), { n: 3, minWait: 100, maxWait: 100 })
|
||||
cancel()
|
||||
await expect(promise).rejects.toThrow('Cancelled')
|
||||
await expect(promise).rejects.toThrow(expect.any(CanceledError))
|
||||
})
|
||||
|
||||
it('cancel no-op after complete', async () => {
|
||||
|
||||
@@ -6,21 +6,15 @@ function waitRandom(min: number, max: number): Promise<void> {
|
||||
return wait(min + Math.round(Math.random() * Math.max(0, max - min)))
|
||||
}
|
||||
|
||||
/**
|
||||
* This error is thrown if the function is cancelled before completing
|
||||
*/
|
||||
class CancelledError extends Error {
|
||||
public isCancelledError = true as const
|
||||
constructor() {
|
||||
super('Cancelled')
|
||||
}
|
||||
/** Thrown if the function is canceled before resolving. */
|
||||
export class CanceledError extends Error {
|
||||
name = 'CanceledError'
|
||||
message = 'Retryable was canceled'
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw this error if the function should retry
|
||||
*/
|
||||
/** May be thrown to force a retry. */
|
||||
export class RetryableError extends Error {
|
||||
public isRetryableError = true as const
|
||||
name = 'RetryableError'
|
||||
}
|
||||
|
||||
export interface RetryOptions {
|
||||
@@ -30,7 +24,7 @@ export interface RetryOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries the function that returns the promise until the promise successfully resolves up to n retries
|
||||
* Retries a function until its returned promise successfully resolves, up to n times.
|
||||
* @param fn function to retry
|
||||
* @param n how many times to retry
|
||||
* @param minWait min wait between retries in ms
|
||||
@@ -59,7 +53,7 @@ export function retry<T>(
|
||||
if (completed) {
|
||||
break
|
||||
}
|
||||
if (n <= 0 || !error.isRetryableError) {
|
||||
if (n <= 0 || !(error instanceof RetryableError)) {
|
||||
reject(error)
|
||||
completed = true
|
||||
break
|
||||
@@ -74,7 +68,7 @@ export function retry<T>(
|
||||
cancel: () => {
|
||||
if (completed) return
|
||||
completed = true
|
||||
rejectCancelled(new CancelledError())
|
||||
rejectCancelled(new CanceledError())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { TransactionReceipt } from '@ethersproject/abstract-provider'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTransactionRemover } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
|
||||
import { retry, RetryableError, RetryOptions } from './retry'
|
||||
import { CanceledError, retry, RetryableError, RetryOptions } from './retry'
|
||||
|
||||
interface Transaction {
|
||||
addedTime: number
|
||||
@@ -53,6 +54,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
const lastBlockNumber = useBlockNumber()
|
||||
const fastForwardBlockNumber = useFastForwardBlockNumber()
|
||||
const removeTransaction = useTransactionRemover()
|
||||
const blockTimestamp = useCurrentBlockTimestamp()
|
||||
|
||||
const getReceipt = useCallback(
|
||||
(hash: string) => {
|
||||
@@ -63,16 +65,17 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
provider.getTransactionReceipt(hash).then(async (receipt) => {
|
||||
if (receipt === null) {
|
||||
if (account) {
|
||||
const transactionCount = await provider.getTransactionCount(account)
|
||||
const tx = pendingTransactions[hash]
|
||||
// We check for the presence of a nonce because we haven't always saved them,
|
||||
// so this code may run against old store state where nonce is undefined.
|
||||
if (tx.nonce && tx.nonce < transactionCount) {
|
||||
// We remove pending transactions from redux if they are no longer the latest nonce.
|
||||
// Remove transactions past their deadline or - if there is no deadline - older than 6 hours.
|
||||
if (tx.deadline) {
|
||||
// Deadlines are expressed as seconds since epoch, as they are used on-chain.
|
||||
if (blockTimestamp && tx.deadline < blockTimestamp.toNumber()) {
|
||||
removeTransaction(hash)
|
||||
}
|
||||
} else if (tx.addedTime + ms`6h` < Date.now()) {
|
||||
removeTransaction(hash)
|
||||
}
|
||||
}
|
||||
console.debug(`Retrying tranasaction receipt for ${hash}`)
|
||||
throw new RetryableError()
|
||||
}
|
||||
return receipt
|
||||
@@ -80,7 +83,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
retryOptions
|
||||
)
|
||||
},
|
||||
[account, chainId, pendingTransactions, provider, removeTransaction]
|
||||
[account, blockTimestamp, chainId, pendingTransactions, provider, removeTransaction]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,17 +95,12 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
const { promise, cancel } = getReceipt(hash)
|
||||
promise
|
||||
.then((receipt) => {
|
||||
if (receipt) {
|
||||
fastForwardBlockNumber(receipt.blockNumber)
|
||||
onReceipt({ chainId, hash, receipt })
|
||||
} else {
|
||||
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
|
||||
}
|
||||
fastForwardBlockNumber(receipt.blockNumber)
|
||||
onReceipt({ chainId, hash, receipt })
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!error.isCancelledError) {
|
||||
console.warn(`Failed to get transaction receipt for ${hash}`, error)
|
||||
}
|
||||
if (error instanceof CanceledError) return
|
||||
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
|
||||
})
|
||||
return cancel
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import CeloLogo from '../../assets/svg/celo_logo.svg'
|
||||
import MaticLogo from '../../assets/svg/matic-token-icon.svg'
|
||||
import { isCelo, NATIVE_CHAIN_ID, nativeOnChain } from '../../constants/tokens'
|
||||
|
||||
type Network = 'ethereum' | 'arbitrum' | 'optimism' | 'polygon' | 'smartchain'
|
||||
type Network = 'ethereum' | 'arbitrum' | 'optimism' | 'polygon' | 'smartchain' | 'celo'
|
||||
|
||||
export function chainIdToNetworkName(networkId: SupportedChainId): Network {
|
||||
switch (networkId) {
|
||||
@@ -23,6 +23,8 @@ export function chainIdToNetworkName(networkId: SupportedChainId): Network {
|
||||
return 'polygon'
|
||||
case SupportedChainId.BNB:
|
||||
return 'smartchain'
|
||||
case SupportedChainId.CELO:
|
||||
return 'celo'
|
||||
default:
|
||||
return 'ethereum'
|
||||
}
|
||||
@@ -51,15 +53,12 @@ function getTokenLogoURI(address: string, chainId: SupportedChainId = SupportedC
|
||||
SupportedChainId.OPTIMISM,
|
||||
SupportedChainId.BNB,
|
||||
]
|
||||
if (networksWithUrls.includes(chainId)) {
|
||||
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${address}/logo.png`
|
||||
if (isCelo(chainId) && address === nativeOnChain(chainId).wrapped.address) {
|
||||
return CeloLogo
|
||||
}
|
||||
|
||||
// Celo logo logo is hosted elsewhere.
|
||||
if (isCelo(chainId)) {
|
||||
if (address === nativeOnChain(chainId).wrapped.address) {
|
||||
return 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_CELO.png'
|
||||
}
|
||||
if (networksWithUrls.includes(chainId)) {
|
||||
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${address}/logo.png`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -558,7 +558,7 @@ exports[`data page loads with header showing 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="reset_base__1klryar0 sprinkles_display_flex_sm__rgw6ez4t4 sprinkles_flexDirection_row_sm__rgw6ez4vn sprinkles_alignItems_center_sm__rgw6ez3k c27"
|
||||
class="reset_base__1klryar0 sprinkles_display_flex_sm__rgw6ez44v sprinkles_flexDirection_row_sm__rgw6ez471 sprinkles_alignItems_center_sm__rgw6ez3j c27"
|
||||
>
|
||||
<span>
|
||||
Activity Content
|
||||
@@ -1159,7 +1159,7 @@ exports[`data page loads without header showing 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="reset_base__1klryar0 sprinkles_display_flex_sm__rgw6ez4t4 sprinkles_flexDirection_row_sm__rgw6ez4vn sprinkles_alignItems_center_sm__rgw6ez3k c27"
|
||||
class="reset_base__1klryar0 sprinkles_display_flex_sm__rgw6ez44v sprinkles_flexDirection_row_sm__rgw6ez471 sprinkles_alignItems_center_sm__rgw6ez3j c27"
|
||||
>
|
||||
<span>
|
||||
Activity Content
|
||||
|
||||
@@ -88,7 +88,7 @@ exports[`EmptyWalletContent.tsx matches base snapshot 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezen sprinkles_fontSize_28_sm__rgw6ezcw sprinkles_lineHeight_36_sm__rgw6ezgl css-rjqmed"
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezd1 sprinkles_fontSize_28_sm__rgw6ezbj sprinkles_lineHeight_36_sm__rgw6ezep css-rjqmed"
|
||||
>
|
||||
No NFTs yet
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@ exports[`EmptyWalletContent.tsx matches base snapshot 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezen sprinkles_fontSize_28_sm__rgw6ezcw sprinkles_lineHeight_36_sm__rgw6ezgl css-rjqmed"
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezd1 sprinkles_fontSize_28_sm__rgw6ezbj sprinkles_lineHeight_36_sm__rgw6ezep css-rjqmed"
|
||||
>
|
||||
No tokens yet
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@ exports[`EmptyWalletContent.tsx matches base snapshot 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezen sprinkles_fontSize_28_sm__rgw6ezcw sprinkles_lineHeight_36_sm__rgw6ezgl css-rjqmed"
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezd1 sprinkles_fontSize_28_sm__rgw6ezbj sprinkles_lineHeight_36_sm__rgw6ezep css-rjqmed"
|
||||
>
|
||||
No activity yet
|
||||
</div>
|
||||
@@ -209,7 +209,7 @@ exports[`EmptyWalletContent.tsx matches base snapshot 1`] = `
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezen sprinkles_fontSize_28_sm__rgw6ezcw sprinkles_lineHeight_36_sm__rgw6ezgl css-rjqmed"
|
||||
class="c1 c2 common__127l8hd1 sprinkles_fontWeight_semibold_sm__rgw6ezd1 sprinkles_fontSize_28_sm__rgw6ezbj sprinkles_lineHeight_36_sm__rgw6ezep css-rjqmed"
|
||||
>
|
||||
No pools yet
|
||||
</div>
|
||||
|
||||
@@ -296,7 +296,6 @@ export const breakpoints = {
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
navSearchInputVisible: 1100,
|
||||
xl: 1280,
|
||||
xxl: 1536,
|
||||
xxxl: 1920,
|
||||
@@ -308,7 +307,6 @@ const layoutStyles = defineProperties({
|
||||
md: { '@media': `screen and (min-width: ${breakpoints.sm}px)` },
|
||||
lg: { '@media': `screen and (min-width: ${breakpoints.md}px)` },
|
||||
xl: { '@media': `screen and (min-width: ${breakpoints.lg}px)` },
|
||||
navSearchInputVisible: { '@media': `screen and (min-width: ${breakpoints.navSearchInputVisible}px)` },
|
||||
xxl: { '@media': `screen and (min-width: ${breakpoints.xl}px)` },
|
||||
xxxl: { '@media': `screen and (min-width: ${breakpoints.xxl}px)` },
|
||||
},
|
||||
|
||||
@@ -3,5 +3,5 @@ import { useScreenSize } from 'hooks/useScreenSize'
|
||||
|
||||
export function useIsNavSearchInputVisible(): boolean {
|
||||
const isScreenSize = useScreenSize()
|
||||
return isScreenSize['navSearchInputVisible']
|
||||
return isScreenSize.navSearchInputVisible
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { AboutFooter } from 'components/About/AboutFooter'
|
||||
import Card, { CardType } from 'components/About/Card'
|
||||
import { MAIN_CARDS, MORE_CARDS } from 'components/About/constants'
|
||||
import ProtocolBanner from 'components/About/ProtocolBanner'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import { AppleLogo } from 'components/Logo/AppleLogo'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import Swap from 'pages/Swap'
|
||||
import { parse } from 'qs'
|
||||
@@ -18,9 +20,10 @@ import { useAppSelector } from 'state/hooks'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { TRANSITION_DURATIONS } from 'theme/styles'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const PageContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
const PageContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: ${({ theme }) => theme.navHeight}px 0px 0px 0px;
|
||||
@@ -30,11 +33,6 @@ const PageContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
align-items: center;
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
|
||||
background: ${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%)'
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%)'};
|
||||
`
|
||||
|
||||
const Gradient = styled.div<{ isDarkMode: boolean }>`
|
||||
@@ -46,10 +44,14 @@ const Gradient = styled.div<{ isDarkMode: boolean }>`
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
min-height: 550px;
|
||||
background: ${({ isDarkMode }) =>
|
||||
${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%)'
|
||||
: 'linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%)'};
|
||||
? css`
|
||||
background: linear-gradient(rgba(8, 10, 24, 0) 0%, rgb(8 10 24 / 100%) 45%);
|
||||
`
|
||||
: css`
|
||||
background: linear-gradient(rgba(255, 255, 255, 0) 0%, rgb(255 255 255 /100%) 45%);
|
||||
`};
|
||||
z-index: ${Z_INDEX.under_dropdown};
|
||||
pointer-events: none;
|
||||
height: ${({ theme }) => `calc(100vh - ${theme.mobileBottomBarHeight}px)`};
|
||||
@@ -111,10 +113,14 @@ const TitleText = styled.h1<{ isDarkMode: boolean }>`
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0 0 24px;
|
||||
background: ${({ isDarkMode }) =>
|
||||
${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(20deg, rgba(255, 244, 207, 1) 10%, rgba(255, 87, 218, 1) 100%)'
|
||||
: 'linear-gradient(10deg, rgba(255,79,184,1) 0%, rgba(255,159,251,1) 100%)'};
|
||||
? css`
|
||||
background: linear-gradient(20deg, rgba(255, 244, 207, 1) 10%, rgba(255, 87, 218, 1) 100%);
|
||||
`
|
||||
: css`
|
||||
background: linear-gradient(10deg, rgba(255, 79, 184, 1) 0%, rgba(255, 159, 251, 1) 100%);
|
||||
`};
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
|
||||
@@ -188,7 +194,7 @@ const LearnMoreContainer = styled.div`
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 36px 0 0;
|
||||
margin: 36px 0;
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
pointer-events: auto;
|
||||
@@ -214,10 +220,14 @@ const AboutContentContainer = styled.div<{ isDarkMode: boolean }>`
|
||||
align-items: center;
|
||||
padding: 0 24px 5rem;
|
||||
width: 100%;
|
||||
background: ${({ isDarkMode }) =>
|
||||
${({ isDarkMode }) =>
|
||||
isDarkMode
|
||||
? 'linear-gradient(179.82deg, rgba(0, 0, 0, 0) 0.16%, #050026 99.85%)'
|
||||
: 'linear-gradient(179.82deg, rgba(255, 255, 255, 0) 0.16%, #eaeaea 99.85%)'};
|
||||
? css`
|
||||
background: linear-gradient(179.82deg, rgba(0, 0, 0, 0) 0.16%, #050026 99.85%);
|
||||
`
|
||||
: css`
|
||||
background: linear-gradient(179.82deg, rgba(255, 255, 255, 0) 0.16%, #eaeaea 99.85%);
|
||||
`};
|
||||
@media screen and (min-width: ${BREAKPOINTS.md}px) {
|
||||
padding: 0 96px 5rem;
|
||||
}
|
||||
@@ -301,95 +311,120 @@ export default function Landing() {
|
||||
ignoreQueryPrefix: true,
|
||||
})
|
||||
|
||||
// This can be simplified significantly once the flag is removed! For now being explicit is clearer.
|
||||
const [accountDrawerOpen] = useAccountDrawer()
|
||||
useEffect(() => {
|
||||
if (queryParams.intro || !selectedWallet) {
|
||||
if ((queryParams.intro || !selectedWallet) && !accountDrawerOpen) {
|
||||
setShowContent(true)
|
||||
} else {
|
||||
navigate('/swap')
|
||||
setShowContent(false)
|
||||
setTimeout(() => {
|
||||
navigate('/swap')
|
||||
}, TRANSITION_DURATIONS.medium)
|
||||
}
|
||||
}, [navigate, selectedWallet, queryParams.intro])
|
||||
}, [navigate, selectedWallet, queryParams.intro, accountDrawerOpen])
|
||||
|
||||
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
|
||||
|
||||
return (
|
||||
<Trace page={InterfacePageName.LANDING_PAGE} shouldLogImpression>
|
||||
{showContent && (
|
||||
<PageContainer isDarkMode={isDarkMode} data-testid="landing-page">
|
||||
<LandingSwapContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.LANDING_PAGE_SWAP_ELEMENT}
|
||||
>
|
||||
<Link to="/swap">
|
||||
<LandingSwap />
|
||||
</Link>
|
||||
</TraceEvent>
|
||||
</LandingSwapContainer>
|
||||
<Gradient isDarkMode={isDarkMode} />
|
||||
<GlowContainer>
|
||||
<Glow />
|
||||
</GlowContainer>
|
||||
<ContentContainer isDarkMode={isDarkMode}>
|
||||
<TitleText isDarkMode={isDarkMode}>
|
||||
{shouldDisableNFTRoutes ? (
|
||||
<Trans>Trade crypto with confidence</Trans>
|
||||
) : (
|
||||
<Trans>Trade crypto and NFTs with confidence</Trans>
|
||||
)}
|
||||
</TitleText>
|
||||
<SubTextContainer>
|
||||
<SubText>
|
||||
<PageContainer data-testid="landing-page">
|
||||
<LandingSwapContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.LANDING_PAGE_SWAP_ELEMENT}
|
||||
>
|
||||
<Link to="/swap">
|
||||
<LandingSwap />
|
||||
</Link>
|
||||
</TraceEvent>
|
||||
</LandingSwapContainer>
|
||||
{showContent && (
|
||||
<>
|
||||
<Gradient isDarkMode={isDarkMode} />
|
||||
<GlowContainer>
|
||||
<Glow />
|
||||
</GlowContainer>
|
||||
<ContentContainer isDarkMode={isDarkMode}>
|
||||
<TitleText isDarkMode={isDarkMode}>
|
||||
{shouldDisableNFTRoutes ? (
|
||||
<Trans>Buy, sell, and explore tokens</Trans>
|
||||
<Trans>Trade crypto with confidence</Trans>
|
||||
) : (
|
||||
<Trans>Buy, sell, and explore tokens and NFTs</Trans>
|
||||
<Trans>Trade crypto and NFTs with confidence</Trans>
|
||||
)}
|
||||
</SubText>
|
||||
</SubTextContainer>
|
||||
<ActionsContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.CONTINUE_BUTTON}
|
||||
</TitleText>
|
||||
<SubTextContainer>
|
||||
<SubText>
|
||||
{shouldDisableNFTRoutes ? (
|
||||
<Trans>Buy, sell, and explore tokens</Trans>
|
||||
) : (
|
||||
<Trans>Buy, sell, and explore tokens and NFTs</Trans>
|
||||
)}
|
||||
</SubText>
|
||||
</SubTextContainer>
|
||||
<ActionsContainer>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.CONTINUE_BUTTON}
|
||||
>
|
||||
<ButtonCTA as={Link} to="/swap">
|
||||
<ButtonCTAText>
|
||||
<Trans>Get started</Trans>
|
||||
</ButtonCTAText>
|
||||
</ButtonCTA>
|
||||
</TraceEvent>
|
||||
</ActionsContainer>
|
||||
<LearnMoreContainer
|
||||
onClick={() => {
|
||||
cardsRef?.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
<ButtonCTA as={Link} to="/swap">
|
||||
<ButtonCTAText>
|
||||
<Trans>Get started</Trans>
|
||||
</ButtonCTAText>
|
||||
</ButtonCTA>
|
||||
</TraceEvent>
|
||||
</ActionsContainer>
|
||||
<LearnMoreContainer
|
||||
onClick={() => {
|
||||
cardsRef?.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
<Trans>Learn more</Trans>
|
||||
<LearnMoreArrow />
|
||||
</LearnMoreContainer>
|
||||
</ContentContainer>
|
||||
<AboutContentContainer isDarkMode={isDarkMode}>
|
||||
<CardGrid cols={2} ref={cardsRef}>
|
||||
{MAIN_CARDS.map(({ darkBackgroundImgSrc, lightBackgroundImgSrc, ...card }) => (
|
||||
<Card
|
||||
{...card}
|
||||
backgroundImgSrc={isDarkMode ? darkBackgroundImgSrc : lightBackgroundImgSrc}
|
||||
key={card.title}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
<CardGrid cols={3}>
|
||||
{MORE_CARDS.map(({ darkIcon, lightIcon, ...card }) => (
|
||||
<Card {...card} icon={isDarkMode ? darkIcon : lightIcon} key={card.title} type={CardType.Secondary} />
|
||||
))}
|
||||
</CardGrid>
|
||||
<ProtocolBanner />
|
||||
<AboutFooter />
|
||||
</AboutContentContainer>
|
||||
</PageContainer>
|
||||
)}
|
||||
<Trans>Learn more</Trans>
|
||||
<LearnMoreArrow />
|
||||
</LearnMoreContainer>
|
||||
|
||||
<DownloadWalletLink href="https://wallet.uniswap.org/">
|
||||
<AppleLogo width="20" height="20" />
|
||||
Download the Uniswap Wallet for iOS
|
||||
</DownloadWalletLink>
|
||||
</ContentContainer>
|
||||
<AboutContentContainer isDarkMode={isDarkMode}>
|
||||
<CardGrid cols={2} ref={cardsRef}>
|
||||
{MAIN_CARDS.map(({ darkBackgroundImgSrc, lightBackgroundImgSrc, ...card }) => (
|
||||
<Card
|
||||
{...card}
|
||||
backgroundImgSrc={isDarkMode ? darkBackgroundImgSrc : lightBackgroundImgSrc}
|
||||
key={card.title}
|
||||
/>
|
||||
))}
|
||||
</CardGrid>
|
||||
<CardGrid cols={3}>
|
||||
{MORE_CARDS.map(({ darkIcon, lightIcon, ...card }) => (
|
||||
<Card {...card} icon={isDarkMode ? darkIcon : lightIcon} key={card.title} type={CardType.Secondary} />
|
||||
))}
|
||||
</CardGrid>
|
||||
<ProtocolBanner />
|
||||
<AboutFooter />
|
||||
</AboutContentContainer>
|
||||
</>
|
||||
)}
|
||||
</PageContainer>
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
|
||||
const DownloadWalletLink = styled.a`
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -729,7 +729,7 @@ export default function MigrateV2Pair() {
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Migrate V2 Liquidity</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<SettingsTab autoSlippage={DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE} />
|
||||
<SettingsTab autoSlippage={DEFAULT_MIGRATE_SLIPPAGE_TOLERANCE} chainId={chainId} />
|
||||
</AutoRow>
|
||||
|
||||
{!account ? (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user