Compare commits

..

13 Commits

Author SHA1 Message Date
cartcrom
d0a10fcf8d refactor: activation hook w/ global state (#6413)
* feat: moved tryActivation to global hook/state

* test: activation hook

* fix: merge conflicts

* fix: update file path for render utils in activate.test.ts

* fix: add await for connector deactivation

* fix: pr comment fixes

* fix: update tests

* refactor: use stronger activation state type

* refactor: use global state instead of props in ConnectionErrorView

* fix: re-add uri availability check

* fix: lint

* fix: nits

* fix: css regression

* fix: update test enum usage

* fix: use native disabled attribute

* test: add snapshot tests for different Option states

* fix: zach's PR comments

* test: update snapshots/unit tests

* style: pending boolean names

* fix: updated test import

* docs: added comment explaining analytics difference in wallet connection

* test: assert console.debug calls and fix act() issues

* test: drawer close

* test: import specific drawer fn instead of whole module
2023-04-28 19:14:30 -04:00
Jordan Frankfurt
7a1a476e45 fix: spoof origin and referer (#6468)
* fix: spoof origin and referer

* comments, chaining, and an accurate replication of amplitude response bodies
2023-04-28 17:29:16 -05:00
Vignesh Mohankumar
b3bfc1003a build: upgrade sentry-release action (#6463) 2023-04-28 11:55:28 -04:00
Zach Pomerantz
3b1ef8033b test: upgrade cypress-hardhat (#6462) 2023-04-28 08:46:42 -07:00
Zach Pomerantz
803485b96a build: update default test settings (#6441)
build: coverage on CI by default
2023-04-27 14:34:37 -07:00
Jack Short
6df5d3a701 chore: moving nft test fixtures to their own file (#6456)
* chore: moving nft test fixtures to their own file

* refactoring structure

* renaming file
2023-04-27 17:10:06 -04:00
lynn
2d4eafc6b3 test: unsupported currency footer unit test (#6360)
* tests are complete

* update comment

* remove console log, and test close icon

* init

* lets try something else to avoid timeout

* try splitting up last test for timeout

* undo, it wasnt working

* revert order test

* add comment for sanity check

* test another way

* try userEvent

* increase timeout

* move timeout

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* merge constants and use mocked

* remove comments

* remove dual import

* Update src/components/swap/UnsupportedCurrencyFooter.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* respond comments

* update tests

* remove console log

* fixes

* add act

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-27 16:54:05 -04:00
lynn
9b52fea58a test: swap details dropdown unit test (#6349)
* init for swap details dropdown test

* more tests

* complete tests, ready for review

* add to dev deps

* init

* merge main

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* merge with constants

* move noop

* add eslint rule

* return null in noop

* merge

* update snapshot

* constant name

* snapshot change

* lint

* undo eslint change

* Update src/components/swap/SwapDetailsDropdown.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/components/swap/SwapDetailsDropdown.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/components/swap/SwapDetailsDropdown.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* respond comments

* update snapshot

* merge main

* user event instead

* add act

* import fix

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-27 16:53:45 -04:00
Jack Short
b92b286626 chore: updating listing marketplace icons (#6458)
* chore: updating listing marketplace icons

* removing svgs from public

* responding to comments
2023-04-27 16:51:53 -04:00
lynn
a8268728d3 test: advanced swap details component unit test (#6363)
* init

* init

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* move noop

* add eslint rule

* return null in noop

* merge

* checkpoint

* fixes

* add tooltip test

* remove unused file

* merge swap modal header

* add third test

* add test for syncing and loading case

* respond to comments

* more descriptive comments

* update snapshot

* update

* change to act
2023-04-27 16:36:36 -04:00
lynn
ab6debbf46 test: swap modal footer unit test (#6353)
* swap footer modal test

* remove empty test

* init

* merge main and jordan comment

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* merge constants

* fix errors

* remove dual import noop

* update snapshot

* Update src/components/swap/SwapModalFooter.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* update snapshot

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-27 15:07:49 -04:00
matteen
4c664645c6 feat: add L2 CG tokenlists (#6292)
* feat: add L2 CG tokenlists

* remove extra line
2023-04-27 12:50:05 -04:00
Jordan Frankfurt
4416a84fd7 fix: disable bnb network option on uniwallet (#6452)
* fix: disable bnb network option on uniwallet

* use supported chain set
2023-04-27 09:16:28 -05:00
64 changed files with 2521 additions and 555 deletions

View File

@@ -114,7 +114,7 @@ jobs:
githubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Upload source maps to Sentry
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

View File

@@ -59,7 +59,7 @@ jobs:
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn test --silent --maxWorkers=100%
- run: yarn test --coverage --maxWorkers=100%
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -1,10 +1,8 @@
import { USDC_MAINNET } from '../../src/constants/tokens'
import { WETH_GOERLI } from '../fixtures/constants'
import { HardhatProvider } from '../support/hardhat'
import { getTestSelector } from '../utils'
describe('Swap', () => {
let hardhat: HardhatProvider
const verifyAmount = (field: 'input' | 'output', amountText: string | null) => {
if (amountText === null) {
cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value')
@@ -46,9 +44,7 @@ describe('Swap', () => {
}
before(() => {
cy.visit('/swap', { ethereum: 'hardhat' }).then((window) => {
hardhat = window.hardhat
})
cy.visit('/swap', { ethereum: 'hardhat' })
})
it('starts with ETH selected by default', () => {
@@ -81,35 +77,33 @@ describe('Swap', () => {
it('can swap ETH for USDC', () => {
const TOKEN_ADDRESS = USDC_MAINNET.address
const BALANCE_INCREMENT = 1
cy.visit('/swap', { ethereum: 'hardhat' })
.then((window) => {
hardhat = window.hardhat
})
.then(() => hardhat.utils.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(TOKEN_ADDRESS)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.hardhat().then((hardhat) => {
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(TOKEN_ADDRESS)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.then(() => hardhat.send('hardhat_mine', ['0x1', '0xc'])).then(() => {
// ui check
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + BALANCE_INCREMENT}`
)
cy.then(() => hardhat.provider.send('hardhat_mine', ['0x1', '0xc'])).then(() => {
// ui check
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + BALANCE_INCREMENT}`
)
// chain state check
cy.then(() => hardhat.utils.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance + BALANCE_INCREMENT)
// chain state check
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance + BALANCE_INCREMENT)
})
})
})
})
})
it('should have the correct default input/output and token selection should work', () => {
@@ -162,21 +156,22 @@ describe('Swap', () => {
})
it('should render and dismiss the wallet rejection modal', () => {
cy.visit('/swap', { ethereum: 'hardhat' }).then((window) => {
hardhat = window.hardhat
cy.stub(hardhat.wallet, 'sendTransaction').rejects(new Error('user cancelled'))
cy.visit('/swap', { ethereum: 'hardhat' })
.hardhat()
.then((hardhat) => {
cy.stub(hardhat.wallet, 'sendTransaction').rejects(new Error('user cancelled'))
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1')
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
})
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1')
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
})
})
it('Opens and closes the settings menu', () => {

View File

@@ -6,23 +6,21 @@
// ***********************************************************
import '@cypress/code-coverage/support'
import 'cypress-hardhat/lib/browser'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import assert from 'assert'
import { Network } from 'cypress-hardhat/lib/browser'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { UserState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
import { injected } from './ethereum'
import { HardhatProvider } from './hardhat'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
ethereum: Eip1193Bridge
hardhat: HardhatProvider
}
interface VisitOptions {
serviceWorker?: true
@@ -39,10 +37,6 @@ declare global {
*/
userState?: Partial<UserState>
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
task(event: 'hardhat'): Chainable<Network>
}
}
}
@@ -59,8 +53,8 @@ Cypress.Commands.overwrite(
return cy
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
.task('hardhat')
.then((network) =>
.provider()
.then((provider) =>
original({
...options,
url: hashUrl,
@@ -84,9 +78,7 @@ Cypress.Commands.overwrite(
// Inject the mock ethereum provider.
if (options?.ethereum === 'hardhat') {
// The provider is exposed via hardhat to allow mocking / network manipulation.
win.hardhat = new HardhatProvider(network)
win.ethereum = win.hardhat
win.ethereum = provider
} else {
win.ethereum = injected
}
@@ -97,28 +89,31 @@ Cypress.Commands.overwrite(
)
beforeEach(() => {
// Infura security policies are based on Origin headers.
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
cy.intercept(/infura.io/, (res) => {
res.headers['origin'] = 'http://localhost:3000'
res.alias = res.body.method
res.continue()
})
// Graphql security policies are based on Origin headers.
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
cy.intercept('https://api.uniswap.org/v1/graphql', (res) => {
res.headers['origin'] = 'https://app.uniswap.org'
res.continue()
})
cy.intercept('https://beta.api.uniswap.org/v1/graphql', (res) => {
res.headers['origin'] = 'https://app.uniswap.org'
res.continue()
})
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (res) => {
res.reply(JSON.stringify({}))
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
cy.intercept('*', (req) => {
req.headers['referer'] = 'https://app.uniswap.org'
req.headers['origin'] = 'https://app.uniswap.org'
})
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
.intercept(/infura.io/, (req) => {
req.headers['referer'] = 'http://localhost:3000'
req.headers['origin'] = 'http://localhost:3000'
req.alias = req.body.method
req.continue()
})
// Mock Amplitude responses to avoid analytics from tests.
.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size
req.reply(
JSON.stringify({
code: 200,
server_upload_time: Date.now(),
payload_size_bytes: byteSize,
events_ingested: req.body.events.length,
})
)
})
})
Cypress.on('uncaught:exception', () => {

View File

@@ -1,81 +0,0 @@
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import { JsonRpcProvider } from '@ethersproject/providers'
import { Wallet } from '@ethersproject/wallet'
import { HardhatUtils, Network } from 'cypress-hardhat/lib/browser'
export class HardhatProvider extends Eip1193Bridge {
readonly utils: HardhatUtils
readonly chainId: string
readonly wallet: Wallet
isMetaMask = true
constructor(network: Network) {
const utils = new HardhatUtils(network)
const wallet = new Wallet(utils.account.privateKey, utils.provider)
super(wallet, utils.provider)
this.utils = utils
this.chainId = `0x${network.chainId.toString(16)}`
this.wallet = wallet
}
async sendAsync(...args: any[]) {
return this.send(...args)
}
async send(...args: any[]) {
console.debug('hardhat:send', ...args)
// Parse callback form.
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
let callback = <T>(error: Error | null, result?: { result: T }) => {
if (error) throw error
return result?.result
}
let method
let params
if (isCallbackForm) {
callback = args[1]
method = args[0].method
params = args[0].params
} else {
method = args[0]
params = args[1]
}
let result
try {
switch (method) {
case 'eth_requestAccounts':
case 'eth_accounts':
result = [this.wallet.address]
break
case 'eth_chainId':
result = this.chainId
break
case 'eth_sendTransaction': {
// Eip1193Bridge doesn't support .gas and .from directly, so we massage it to satisfy ethers' expectations.
// See https://github.com/ethers-io/ethers.js/issues/1683.
params[0].gasLimit = params[0].gas
delete params[0].gas
delete params[0].from
const req = JsonRpcProvider.hexlifyTransaction(params[0])
req.gasLimit = req.gas
delete req.gas
result = (await this.signer.sendTransaction(req)).hash
break
}
default:
result = await super.send(method, params)
}
console.debug('hardhat:receive', method, result)
return callback(null, { result })
} catch (error) {
console.debug('hardhat:error', method, error)
return callback(error as Error)
}
}
}

View File

@@ -24,7 +24,7 @@
"serve": "serve build -l 3000",
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
"typecheck": "tsc",
"test": "craco test --coverage",
"test": "craco test",
"test:size": "node scripts/test-size.js",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
@@ -103,7 +103,7 @@
"@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-istanbul": "^6.1.1",
"cypress": "10.3.1",
"cypress-hardhat": "^1.0.1",
"cypress-hardhat": "^2.0.0",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"eslint-plugin-import": "^2.27",
@@ -113,6 +113,7 @@
"ms.macro": "^2.0.0",
"prettier": "^2.7.1",
"react-scripts": "^4.0.3",
"resize-observer-polyfill": "^1.5.1",
"serve": "^11.3.2",
"source-map-explorer": "^2.5.3",
"ts-transform-graphql-tag": "^0.2.1",

View File

@@ -1,5 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#04CD58"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M100 112C87.3026 112 77 101.708 77 89C77 76.2923 87.3026 66 100 66C112.697 66 123 76.2923 123 89C123 101.708 112.697 112 100 112ZM90 89C90 94.5251 94.4794 99 100 99C105.521 99 110 94.5251 110 89C110 83.4749 105.521 79 100 79C94.4794 79 90 83.4749 90 89Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 89.0304L70 45H130L174 89.0304L100 163L26 89.0304ZM134 72.9998C115.305 54.2224 84.6953 54.2225 66 72.9999L50 89.0001L66 105C84.6953 123.778 115.305 123.777 134 105L150 89.0001L134 72.9998Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 731 B

View File

@@ -1,5 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill="#2081E2"/>
<path d="M24.6679 51.6802L24.8836 51.3411L37.8906 30.9933C38.0807 30.6954 38.5276 30.7262 38.6714 31.0498C40.8444 35.9197 42.7194 41.9763 41.841 45.7469C41.466 47.2983 40.4386 49.3993 39.2827 51.3411C39.1338 51.6237 38.9694 51.9011 38.7947 52.1682C38.7125 52.2915 38.5738 52.3634 38.4248 52.3634H25.048C24.6884 52.3634 24.4778 51.973 24.6679 51.6802Z" fill="white"/>
<path d="M82.6444 55.461V58.6819C82.6444 58.8668 82.5314 59.0312 82.367 59.1031C81.3602 59.5346 77.9132 61.1168 76.48 63.11C72.8224 68.2008 70.0279 75.48 63.7812 75.48H37.721C28.4847 75.48 21 67.9697 21 58.7024V58.4045C21 58.1579 21.2003 57.9576 21.4469 57.9576H35.9745C36.2621 57.9576 36.4727 58.2247 36.4471 58.5072C36.3443 59.4524 36.519 60.4182 36.9659 61.2966C37.8289 63.0484 39.6166 64.1426 41.5481 64.1426H48.74V58.5278H41.6303C41.2656 58.5278 41.0499 58.1065 41.2605 57.8086C41.3375 57.6904 41.4249 57.5672 41.5173 57.4285C42.1903 56.473 43.1509 54.9884 44.1064 53.2983C44.7588 52.1579 45.3906 50.9404 45.8992 49.7178C46.002 49.4969 46.0841 49.2708 46.1663 49.0499C46.305 48.6595 46.4489 48.2948 46.5516 47.9301C46.6544 47.6218 46.7365 47.2982 46.8187 46.9951C47.0602 45.9574 47.1629 44.8581 47.1629 43.7177C47.1629 43.2708 47.1424 42.8033 47.1013 42.3564C47.0807 41.8684 47.0191 41.3803 46.9574 40.8923C46.9163 40.4608 46.8393 40.0344 46.7571 39.5875C46.6544 38.9351 46.5105 38.2879 46.3461 37.6354L46.2896 37.3889C46.1663 36.9419 46.0636 36.5156 45.9198 36.0687C45.5139 34.6662 45.0465 33.2998 44.5533 32.0207C44.3735 31.5121 44.168 31.0241 43.9625 30.5361C43.6595 29.8015 43.3512 29.1337 43.0687 28.5018C42.9249 28.2141 42.8016 27.9521 42.6783 27.685C42.5396 27.3819 42.3958 27.0788 42.2519 26.7912C42.1492 26.5703 42.031 26.3648 41.9488 26.1593L41.0704 24.536C40.9471 24.3151 41.1526 24.0531 41.394 24.1199L46.8907 25.6096H46.9061C46.9163 25.6096 46.9215 25.6148 46.9266 25.6148L47.6509 25.8151L48.4472 26.0412L48.74 26.1233V22.8562C48.74 21.2791 50.0037 20 51.5654 20C52.3462 20 53.0551 20.3185 53.5637 20.8373C54.0722 21.3562 54.3907 22.0651 54.3907 22.8562V27.7056L54.9764 27.8699C55.0226 27.8854 55.0688 27.9059 55.1099 27.9367C55.2538 28.0446 55.4592 28.2038 55.7212 28.3991C55.9267 28.5634 56.1476 28.7638 56.4147 28.9693C56.9438 29.3956 57.5757 29.9453 58.2692 30.5772C58.4541 30.7364 58.6339 30.9008 58.7983 31.0652C59.6922 31.8974 60.6939 32.8734 61.6494 33.9522C61.9165 34.2553 62.1785 34.5635 62.4456 34.8871C62.7127 35.2159 62.9953 35.5395 63.2418 35.8632C63.5655 36.2947 63.9148 36.7416 64.2179 37.2091C64.3617 37.43 64.5261 37.656 64.6648 37.8769C65.0552 38.4676 65.3994 39.079 65.7282 39.6903C65.8669 39.9728 66.0107 40.281 66.134 40.5841C66.4987 41.4009 66.7864 42.2331 66.9713 43.0653C67.0278 43.2451 67.0689 43.4403 67.0895 43.615V43.6561C67.1511 43.9026 67.1717 44.1646 67.1922 44.4317C67.2744 45.2845 67.2333 46.1372 67.0484 46.9951C66.9713 47.3599 66.8686 47.704 66.7453 48.0688C66.622 48.4181 66.4987 48.7828 66.3395 49.127C66.0313 49.841 65.6665 50.5551 65.235 51.2229C65.0963 51.4695 64.9319 51.7315 64.7675 51.9781C64.5877 52.24 64.4028 52.4866 64.2384 52.7281C64.0124 53.0363 63.771 53.3599 63.5244 53.6476C63.3035 53.9507 63.0775 54.2538 62.8309 54.5209C62.4867 54.9267 62.1579 55.312 61.8137 55.6819C61.6083 55.9233 61.3874 56.1699 61.1613 56.3908C60.9405 56.6373 60.7144 56.8582 60.5089 57.0637C60.1648 57.4079 59.8771 57.675 59.6356 57.8959L59.0706 58.4148C58.9884 58.4867 58.8805 58.5278 58.7675 58.5278H54.3907V64.1426H59.8976C61.1305 64.1426 62.3018 63.7059 63.247 62.9045C63.5706 62.622 64.9833 61.3994 66.6528 59.5552C66.7093 59.4935 66.7813 59.4473 66.8635 59.4268L82.0742 55.0295C82.3568 54.9473 82.6444 55.163 82.6444 55.461Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,20 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" fill="#F9F9F9"/>
<path d="M21.94 7.92004C20.4077 6.42277 18.3116 5.5 16 5.5C11.3056 5.5 7.5 9.30559 7.5 14C7.5 18.6944 11.3056 22.5 16 22.5C18.3116 22.5 20.4077 21.5772 21.94 20.08C20.1123 22.4633 17.2356 24 14 24C8.47715 24 4 19.5229 4 14C4 8.47715 8.47715 4 14 4C17.2356 4 20.1123 5.53668 21.94 7.92004Z" fill="url(#paint0_linear_6993_17582)"/>
<path d="M9.64795 18.864C10.8738 20.0618 12.5507 20.8 14.4 20.8C18.1555 20.8 21.2 17.7555 21.2 14C21.2 10.2445 18.1555 7.2 14.4 7.2C12.5507 7.2 10.8738 7.9382 9.64795 9.13601C11.1102 7.22934 13.4115 6 16 6C20.4183 6 24 9.58172 24 14C24 18.4183 20.4183 22 16 22C13.4115 22 11.1102 20.7707 9.64795 18.864Z" fill="url(#paint1_linear_6993_17582)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 14C20 17.3137 17.3137 20 14 20C10.6863 20 8 17.3137 8 14C8 10.6863 10.6863 8 14 8C17.3137 8 20 10.6863 20 14ZM18 14C18 16.2091 16.2091 18 14 18C11.7909 18 10 16.2091 10 14C10 11.7909 11.7909 10 14 10C16.2091 10 18 11.7909 18 14Z" fill="url(#paint2_linear_6993_17582)"/>
<defs>
<linearGradient id="paint0_linear_6993_17582" x1="4" y1="13.6552" x2="24" y2="13.6552" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E0FF"/>
<stop offset="1" stop-color="#562EC8"/>
</linearGradient>
<linearGradient id="paint1_linear_6993_17582" x1="3.99998" y1="13.6552" x2="24" y2="13.6552" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E0FF"/>
<stop offset="1" stop-color="#562EC8"/>
</linearGradient>
<linearGradient id="paint2_linear_6993_17582" x1="4" y1="13.6552" x2="24" y2="13.6552" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E0FF"/>
<stop offset="1" stop-color="#562EC8"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,17 +1,16 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { WalletConnect } from '@web3-react/walletconnect'
import Column, { AutoColumn } from 'components/Column'
import Modal from 'components/Modal'
import { RowBetween } from 'components/Row'
import { uniwalletConnectConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate'
import { ConnectionType } from 'connection/types'
import { UniwalletConnect } from 'connection/WalletConnect'
import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useEffect, useState } from 'react'
import { useModalIsOpen, useToggleUniwalletModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
@@ -39,44 +38,37 @@ const Divider = styled.div`
`
export default function UniwalletModal() {
const open = useModalIsOpen(ApplicationModal.UNIWALLET_CONNECT)
const toggle = useToggleUniwalletModal()
const { activationState, cancelActivation } = useActivationState()
const [uri, setUri] = useState<string>()
// 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 &&
!!uri
useEffect(() => {
;(uniwalletConnectConnection.connector as WalletConnect).events.addListener(
UniwalletConnect.UNI_URI_AVAILABLE,
(uri) => {
uri && setUri(uri)
toggle()
}
)
}, [toggle])
}, [])
const { account } = useWeb3React()
useEffect(() => {
if (open) {
sendAnalyticsEvent('Uniswap wallet modal opened', { userConnected: !!account })
if (account) {
toggle()
}
}
}, [account, open, toggle])
const onClose = useCallback(() => {
uniwalletConnectConnection.connector.deactivate?.()
toggle()
}, [toggle])
if (open) sendAnalyticsEvent('Uniswap wallet modal opened')
}, [open])
const theme = useTheme()
return (
<Modal isOpen={open} onDismiss={onClose}>
<Modal isOpen={open} onDismiss={cancelActivation}>
<UniwalletConnectWrapper>
<HeaderRow>
<ThemedText.SubHeader>
<Trans>Scan with Uniswap Wallet</Trans>
</ThemedText.SubHeader>
<CloseIcon onClick={onClose} />
<CloseIcon onClick={cancelActivation} />
</HeaderRow>
<QRCodeWrapper>
{uri && (

View File

@@ -27,6 +27,11 @@ export function useToggleAccountDrawer() {
}, [updateAccountDrawerOpen])
}
export function useCloseAccountDrawer() {
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
return useCallback(() => updateAccountDrawerOpen(false), [updateAccountDrawerOpen])
}
export function useAccountDrawer(): [boolean, () => void] {
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
return [accountDrawerOpen, useToggleAccountDrawer()]

View File

@@ -1,6 +1,6 @@
import { useWeb3React } from '@web3-react/core'
import { Unicon } from 'components/Unicon'
import { Connection, ConnectionType } from 'connection'
import { Connection, ConnectionType } from 'connection/types'
import useENSAvatar from 'hooks/useENSAvatar'
import styled from 'styled-components/macro'
import { useIsDarkMode } from 'theme/components/ThemeToggle'

View File

@@ -1,10 +1,10 @@
import { t } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { MouseoverTooltip } from 'components/Tooltip'
import { ConnectionType } from 'connection'
import { useGetConnection } from 'connection'
import { ConnectionType } from 'connection/types'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { SupportedChainId, UniWalletSupportedChains } from 'constants/chains'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import useSelectChain from 'hooks/useSelectChain'
import useSyncChainQuery from 'hooks/useSyncChainQuery'
@@ -76,7 +76,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
<Column paddingX="8">
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) => (
<ChainSelectorRow
disabled={isUniWallet && chainId === SupportedChainId.CELO}
disabled={isUniWallet && !UniWalletSupportedChains.includes(chainId)}
onSelectChain={onSelectChain}
targetChain={chainId}
key={chainId}

View File

@@ -1,5 +1,7 @@
import { Trans } from '@lingui/macro'
import { useCloseAccountDrawer } from 'components/AccountDrawer'
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
import { ActivationStatus, useActivationState } from 'connection/activate'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
@@ -20,13 +22,15 @@ const AlertTriangleIcon = styled(AlertTriangle)`
color: ${({ theme }) => theme.accentCritical};
`
export default function ConnectionErrorView({
retryActivation,
openOptions,
}: {
retryActivation: () => void
openOptions: () => void
}) {
// TODO(cartcrom): move this to a top level modal, rather than inline in the drawer
export default function ConnectionErrorView() {
const { activationState, tryActivation, cancelActivation } = useActivationState()
const closeDrawer = useCloseAccountDrawer()
if (activationState.status !== ActivationStatus.ERROR) return null
const retry = () => tryActivation(activationState.connection, closeDrawer)
return (
<Wrapper>
<AlertTriangleIcon />
@@ -38,11 +42,11 @@ export default function ConnectionErrorView({
The connection attempt failed. Please click try again and follow the steps to connect in your wallet.
</Trans>
</ThemedText.BodyPrimary>
<ButtonPrimary $borderRadius="16px" onClick={retryActivation}>
<ButtonPrimary $borderRadius="16px" onClick={retry}>
<Trans>Try Again</Trans>
</ButtonPrimary>
<ButtonEmpty width="fit-content" padding="0" marginTop={20}>
<ThemedText.Link onClick={openOptions} marginBottom={12}>
<ThemedText.Link onClick={cancelActivation} marginBottom={12}>
<Trans>Back to wallet selection</Trans>
</ThemedText.Link>
</ButtonEmpty>

View File

@@ -0,0 +1,85 @@
import { Connector } from '@web3-react/types'
import UNIWALLET_ICON from 'assets/images/uniwallet.png'
import { useCloseAccountDrawer } from 'components/AccountDrawer'
import { Connection, ConnectionType } from 'connection/types'
import { mocked } from 'test-utils/mocked'
import { createDeferredPromise } from 'test-utils/promise'
import { act, render } from 'test-utils/render'
import Option from './Option'
const mockCloseDrawer = jest.fn()
jest.mock('components/AccountDrawer')
beforeEach(() => {
jest.spyOn(console, 'debug').mockReturnValue()
mocked(useCloseAccountDrawer).mockReturnValue(mockCloseDrawer)
})
const mockConnection1: Connection = {
getName: () => 'Mock Connection 1',
connector: {
activate: jest.fn(),
deactivate: jest.fn(),
} as unknown as Connector,
getIcon: () => UNIWALLET_ICON,
type: ConnectionType.UNIWALLET,
} as unknown as Connection
const mockConnection2: Connection = {
getName: () => 'Mock Connection 2',
connector: {
activate: jest.fn(),
deactivate: jest.fn(),
} as unknown as Connector,
getIcon: () => UNIWALLET_ICON,
type: ConnectionType.INJECTED,
} as unknown as Connection
describe('Wallet Option', () => {
it('renders default state', () => {
const component = render(<Option connection={mockConnection1} />)
const option = component.getByTestId('wallet-option-UNIWALLET')
expect(option).toBeEnabled()
expect(option).toHaveProperty('selected', false)
expect(option).toMatchSnapshot()
})
it('connect when clicked', async () => {
const activationResponse = createDeferredPromise()
mocked(mockConnection1.connector.activate).mockReturnValue(activationResponse.promise)
const component = render(
<>
<Option connection={mockConnection1} />
<Option connection={mockConnection2} />
</>
)
const option1 = component.getByTestId('wallet-option-UNIWALLET')
const option2 = component.getByTestId('wallet-option-INJECTED')
expect(option1).toBeEnabled()
expect(option1).toHaveProperty('selected', false)
expect(option2).toBeEnabled()
expect(option2).toHaveProperty('selected', false)
await act(() => option1.click())
expect(option1).toBeDisabled()
expect(option1).toHaveProperty('selected', true)
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()
expect(option2).toHaveProperty('selected', false)
})
})

View File

@@ -1,7 +1,9 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
import { useCloseAccountDrawer } from 'components/AccountDrawer'
import Loader from 'components/Icons/LoadingSpinner'
import { Connection, ConnectionType } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate'
import { Connection } from 'connection/types'
import styled from 'styled-components/macro'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
@@ -14,27 +16,26 @@ const OptionCardLeft = styled.div`
align-items: center;
`
const OptionCardClickable = styled.button<{ isActive?: boolean; clickable?: boolean }>`
const OptionCardClickable = styled.button<{ selected: boolean }>`
background-color: ${({ theme }) => theme.backgroundModule};
border: none;
width: 100% !important;
border-color: ${({ theme, isActive }) => (isActive ? theme.accentActive : 'transparent')};
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 2rem;
padding: 1rem;
padding: 18px;
margin-top: 0;
transition: ${({ theme }) => theme.transition.duration.fast};
opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
opacity: ${({ disabled, selected }) => (disabled && !selected ? '0.5' : '1')};
&:hover {
cursor: ${({ clickable }) => clickable && 'pointer'};
background-color: ${({ theme, clickable }) => clickable && theme.hoverState};
cursor: ${({ disabled }) => !disabled && 'pointer'};
background-color: ${({ theme, disabled }) => !disabled && theme.hoverState};
}
&:focus {
background-color: ${({ theme, clickable }) => clickable && theme.hoverState};
background-color: ${({ theme, disabled }) => !disabled && theme.hoverState};
}
`
@@ -62,15 +63,16 @@ const IconWrapper = styled.div`
`};
`
type OptionProps = {
connection: Connection
activate: () => void
pendingConnectionType?: ConnectionType
}
export default function Option({ connection, pendingConnectionType, activate }: OptionProps) {
const isPending = pendingConnectionType === connection.type
export default function Option({ connection }: { connection: Connection }) {
const { activationState, tryActivation } = useActivationState()
const closeDrawer = useCloseAccountDrawer()
const activate = () => tryActivation(connection, closeDrawer)
const isSomeOptionPending = activationState.status === ActivationStatus.PENDING
const isCurrentOptionPending = isSomeOptionPending && activationState.connection.type === connection.type
const isDarkMode = useIsDarkMode()
const content = (
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.WALLET_SELECTED}
@@ -78,10 +80,10 @@ export default function Option({ connection, pendingConnectionType, activate }:
element={InterfaceElementName.WALLET_TYPE_OPTION}
>
<OptionCardClickable
onClick={!pendingConnectionType ? activate : undefined}
clickable={!pendingConnectionType}
disabled={Boolean(!isPending && !!pendingConnectionType)}
data-testid="wallet-modal-option"
onClick={activate}
disabled={isSomeOptionPending}
selected={isCurrentOptionPending}
data-testid={`wallet-option-${connection.type}`}
>
<OptionCardLeft>
<IconWrapper>
@@ -90,10 +92,8 @@ export default function Option({ connection, pendingConnectionType, activate }:
<HeaderText>{connection.getName()}</HeaderText>
{connection.isNew && <NewBadge />}
</OptionCardLeft>
{isPending && <Loader />}
{isCurrentOptionPending && <Loader />}
</OptionCardClickable>
</TraceEvent>
)
return content
}

View File

@@ -0,0 +1,132 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Wallet Option renders default state 1`] = `
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-flow: column nowrap;
-ms-flex-flow: column nowrap;
flex-flow: column nowrap;
-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;
}
.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;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
padding: 18px;
-webkit-transition: 125ms;
transition: 125ms;
opacity: 1;
}
.c0:hover {
cursor: pointer;
background-color: #ADBCFF3d;
}
.c0:focus {
background-color: #ADBCFF3d;
}
.c3 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-flow: row nowrap;
-ms-flex-flow: row nowrap;
flex-flow: row nowrap;
-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;
color: #0D111C;
font-size: 16px;
font-weight: 600;
padding: 0 8px;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-flow: column nowrap;
-ms-flex-flow: column nowrap;
flex-flow: column nowrap;
-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;
}
.c2 > img,
.c2 span {
height: 40px;
width: 40px;
}
@media (max-width:960px) {
.c2 {
-webkit-align-items: flex-end;
-webkit-box-align: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
}
}
<button
class="c0"
data-testid="wallet-option-UNIWALLET"
>
<div
class="c1"
>
<div
class="c2"
>
<img
alt="Icon"
src="uniwallet.png"
/>
</div>
<div
class="c3"
>
Mock Connection 1
</div>
</div>
</button>
`;

View File

@@ -1,18 +1,12 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { useAccountDrawer } from 'components/AccountDrawer'
import IconButton from 'components/AccountDrawer/IconButton'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
import { ErrorCode } from 'connection/utils'
import { getConnections, networkConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate'
import { isSupportedChain } from 'constants/chains'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useEffect } from 'react'
import { Settings } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { flexColumnNoWrap } from 'theme/styles'
@@ -43,35 +37,12 @@ const PrivacyPolicyWrapper = styled.div`
padding: 0 4px;
`
function didUserReject(connection: Connection, error: any): boolean {
return (
error?.code === ErrorCode.USER_REJECTED_REQUEST ||
(connection.type === ConnectionType.WALLET_CONNECT && error?.toString?.() === ErrorCode.WC_MODAL_CLOSED) ||
(connection.type === ConnectionType.COINBASE_WALLET && error?.toString?.() === ErrorCode.CB_REJECTED_REQUEST)
)
}
export default function WalletModal({ openSettings }: { openSettings: () => void }) {
const dispatch = useAppDispatch()
const { connector, chainId } = useWeb3React()
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
const [pendingError, setPendingError] = useState<any>()
const connections = getConnections()
useEffect(() => {
// Clean up errors when the dropdown closes
return () => setPendingError(undefined)
}, [setPendingError])
const openOptions = useCallback(() => {
if (pendingConnection) {
setPendingError(undefined)
setPendingConnection(undefined)
}
}, [pendingConnection, setPendingError])
const { activationState } = useActivationState()
// Keep the network connector in sync with any active user connector to prevent chain-switching on wallet disconnection.
useEffect(() => {
@@ -80,71 +51,22 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
}
}, [chainId, connector])
// Used to track the state of the drawer in async function
const drawerOpenRef = useRef(drawerOpen)
drawerOpenRef.current = drawerOpen
const tryActivation = useCallback(
async (connection: Connection) => {
// Skips wallet connection if the connection should override the default behavior, i.e. install metamask or launch coinbase app
if (connection.overrideActivate?.()) return
// log selected wallet
sendEvent({
category: 'Wallet',
action: 'Change Wallet',
label: connection.type,
})
try {
setPendingConnection(connection)
setPendingError(undefined)
await connection.connector.activate()
console.debug(`connection activated: ${connection.getName()}`)
dispatch(updateSelectedWallet({ wallet: connection.type }))
if (drawerOpenRef.current) toggleWalletDrawer()
} catch (error) {
console.debug(`web3-react connection error: ${JSON.stringify(error)}`)
// TODO(WEB-3162): re-add special treatment for already-pending injected errors
if (didUserReject(connection, error)) {
setPendingConnection(undefined)
} else {
setPendingError(error)
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.FAILED,
wallet_type: connection.getName(),
})
}
}
},
[dispatch, setPendingError, toggleWalletDrawer]
)
return (
<Wrapper data-testid="wallet-modal">
<AutoRow justify="space-between" width="100%" marginBottom="16px">
<ThemedText.SubHeader>Connect a wallet</ThemedText.SubHeader>
<IconButton Icon={Settings} onClick={openSettings} data-testid="wallet-settings" />
</AutoRow>
{pendingError ? (
pendingConnection && (
<ConnectionErrorView openOptions={openOptions} retryActivation={() => tryActivation(pendingConnection)} />
)
{activationState.status === ActivationStatus.ERROR ? (
<ConnectionErrorView />
) : (
<AutoColumn gap="16px">
<OptionGrid data-testid="option-grid">
{connections.map((connection) =>
connection.shouldDisplay() ? (
<Option
key={connection.getName()}
connection={connection}
activate={() => tryActivation(connection)}
pendingConnectionType={pendingConnection?.type}
/>
) : null
)}
{connections
.filter((connection) => connection.shouldDisplay())
.map((connection) => (
<Option key={connection.getName()} connection={connection} />
))}
</OptionGrid>
<PrivacyPolicyWrapper>
<PrivacyPolicyNotice />

View File

@@ -4,7 +4,8 @@ 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 { Connection, ConnectionType, useGetConnection } from 'connection'
import { useGetConnection } from 'connection'
import { Connection, ConnectionType } from 'connection/types'
import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections'
import { Provider } from 'react-redux'

View File

@@ -0,0 +1,46 @@
import userEvent from '@testing-library/user-event'
import {
TEST_ALLOWED_SLIPPAGE,
TEST_TOKEN_1,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_OUTPUT,
toCurrencyAmount,
} from 'test-utils/constants'
import { act, render, screen } from 'test-utils/render'
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
describe('AdvancedSwapDetails.tsx', () => {
it('matches base snapshot', () => {
const { asFragment } = render(
<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
)
expect(asFragment()).toMatchSnapshot()
})
it('renders correct copy on mouseover', async () => {
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText('Price Impact')))
expect(await screen.getByText(/The impact your trade has on the market price of this pool./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText('Expected Output')))
expect(await screen.getByText(/The amount you expect to receive at the current market price./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText(/Minimum received/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
})
it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => {
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1)
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum sent/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
await act(() => userEvent.hover(screen.getByText('Network Fee')))
expect(await screen.getByText(/The fee paid to miners who process your transaction./i)).toBeVisible()
})
it('renders loading rows when syncing', async () => {
render(
<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} syncing={true} />
)
expect(screen.getAllByTestId('loading-rows').length).toBeGreaterThan(0)
})
})

View File

@@ -37,7 +37,7 @@ function TextWithLoadingPlaceholder({
children: JSX.Element
}) {
return syncing ? (
<LoadingRows>
<LoadingRows data-testid="loading-rows">
<div style={{ height: '15px', width: `${width}px` }} />
</LoadingRows>
) : (

View File

@@ -0,0 +1,43 @@
import userEvent from '@testing-library/user-event'
import { TEST_ALLOWED_SLIPPAGE, TEST_TOKEN_1, TEST_TRADE_EXACT_INPUT, toCurrencyAmount } from 'test-utils/constants'
import { act, render, screen } from 'test-utils/render'
import SwapDetailsDropdown from './SwapDetailsDropdown'
describe('SwapDetailsDropdown.tsx', () => {
it('renders a trade', () => {
const { asFragment } = render(
<SwapDetailsDropdown
trade={TEST_TRADE_EXACT_INPUT}
syncing={false}
loading={false}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
/>
)
expect(asFragment()).toMatchSnapshot()
})
it('renders loading state', () => {
render(
<SwapDetailsDropdown trade={undefined} syncing={true} loading={true} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
)
expect(screen.getByText('Fetching best price...')).toBeInTheDocument()
})
it('is interactive once loaded', async () => {
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1)
render(
<SwapDetailsDropdown
trade={TEST_TRADE_EXACT_INPUT}
syncing={false}
loading={false}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
/>
)
expect(screen.getByTestId('swap-details-header-row')).toBeInTheDocument()
expect(screen.getByTestId('trade-price-container')).toBeInTheDocument()
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
expect(screen.getByTestId('advanced-swap-details')).toBeInTheDocument()
expect(screen.getByTestId('swap-route-info')).toBeInTheDocument()
})
})

View File

@@ -118,7 +118,12 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
element={InterfaceElementName.SWAP_DETAILS_DROPDOWN}
shouldLogImpression={!showDetails}
>
<StyledHeaderRow onClick={() => setShowDetails(!showDetails)} disabled={!trade} open={showDetails}>
<StyledHeaderRow
data-testid="swap-details-header-row"
onClick={() => setShowDetails(!showDetails)}
disabled={!trade}
open={showDetails}
>
<RowFixed style={{ position: 'relative' }} align="center">
{Boolean(loading || syncing) && (
<StyledPolling>
@@ -128,7 +133,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
</StyledPolling>
)}
{trade ? (
<LoadingOpacityContainer $loading={syncing}>
<LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
<TradePrice price={trade.executionPrice} />
</LoadingOpacityContainer>
) : loading || syncing ? (
@@ -159,11 +164,11 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
<AnimatedDropdown open={showDetails}>
<AutoColumn gap="sm" style={{ padding: '0', paddingBottom: '8px' }}>
{trade ? (
<StyledCard>
<StyledCard data-testid="advanced-swap-details">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
</StyledCard>
) : null}
{trade ? <SwapRoute trade={trade} syncing={syncing} /> : null}
{trade ? <SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} /> : null}
</AutoColumn>
</AnimatedDropdown>
</AutoColumn>

View File

@@ -0,0 +1,27 @@
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { render, screen } from 'test-utils/render'
import SwapModalFooter from './SwapModalFooter'
const swapErrorMessage = 'swap error'
const fiatValue = { data: 123, isLoading: false }
describe('SwapModalFooter.tsx', () => {
it('renders with a disabled button with no account', () => {
const { asFragment } = render(
<SwapModalFooter
trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined}
onConfirm={() => null}
disabledConfirm
swapErrorMessage={swapErrorMessage}
swapQuoteReceivedDate={undefined}
fiatValueInput={fiatValue}
fiatValueOutput={fiatValue}
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByTestId('confirm-swap-button')).toBeDisabled()
})
})

View File

@@ -147,6 +147,7 @@ export default function SwapModalFooter({
})}
>
<ButtonError
data-testid="confirm-swap-button"
onClick={onConfirm}
disabled={disabledConfirm}
style={{ margin: '10px 0 0 0' }}
@@ -157,7 +158,6 @@ export default function SwapModalFooter({
</Text>
</ButtonError>
</TraceEvent>
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</AutoRow>
</>

View File

@@ -20,7 +20,7 @@ describe('SwapModalHeader.tsx', () => {
sendAnalyticsEventMock = jest.fn()
})
it('matches base snapshot, test trade exact input', () => {
it('matches base snapshot for test trade with exact input', () => {
const { asFragment } = render(
<SwapModalHeader
trade={TEST_TRADE_EXACT_INPUT}
@@ -33,28 +33,9 @@ describe('SwapModalHeader.tsx', () => {
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument()
expect(screen.getByTestId('input-symbol')).toHaveTextContent(
TEST_TRADE_EXACT_INPUT.inputAmount.currency.symbol ?? ''
)
expect(screen.getByTestId('output-symbol')).toHaveTextContent(
TEST_TRADE_EXACT_INPUT.outputAmount.currency.symbol ?? ''
)
expect(screen.getByTestId('input-amount')).toHaveTextContent(TEST_TRADE_EXACT_INPUT.inputAmount.toExact())
expect(screen.getByTestId('output-amount')).toHaveTextContent(TEST_TRADE_EXACT_INPUT.outputAmount.toExact())
const recipientInfo = screen.getByTestId('recipient-info')
expect(recipientInfo).toHaveTextContent(/Output will be sent to/i)
expect(within(recipientInfo).getByText('0x0000...0004')).toBeVisible()
expect(
screen.getByText(
'The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.'
)
).toBeInTheDocument()
expect(screen.getByText(/The amount you expect to receive at the current market price./i)).toBeInTheDocument()
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
})
it('shows accept changes section when available, and logs amplitude event when accept clicked', () => {
it('shows accept changes section and logs amplitude event', () => {
const setShouldLogModalCloseEventFn = jest.fn()
mockSendAnalyticsEvent.mockImplementation(sendAnalyticsEventMock)
render(
@@ -76,7 +57,7 @@ describe('SwapModalHeader.tsx', () => {
expect(sendAnalyticsEventMock).toHaveBeenCalledTimes(1)
})
it('test trade exact output, no recipient', () => {
it('renders correctly for test trade with exact output and no recipient', () => {
const rendered = render(
<SwapModalHeader
trade={TEST_TRADE_EXACT_OUTPUT}

View File

@@ -0,0 +1,73 @@
import userEvent from '@testing-library/user-event'
import { Token } from '@uniswap/sdk-core'
import { useUnsupportedTokens } from 'hooks/Tokens'
import { mocked } from 'test-utils/mocked'
import { act, render, screen, waitForElementToBeRemoved, within } from 'test-utils/render'
import { getExplorerLink } from 'utils/getExplorerLink'
import UnsupportedCurrencyFooter from './UnsupportedCurrencyFooter'
const unsupportedTokenAddress = '0x4e83b6287588a96321B2661c5E041845fF7814af'
const unsupportedTokenSymbol = 'ALTDOM-MAR2021'
const unsupportedToken = new Token(1, unsupportedTokenAddress, 18, 'ALTDOM-MAR2021')
const unsupportedTokenExplorerLink = 'www.blahblah.com'
jest.mock('../../hooks/Tokens')
jest.mock('../../utils/getExplorerLink')
describe('UnsupportedCurrencyFooter.tsx with unsupported tokens', () => {
beforeEach(() => {
mocked(useUnsupportedTokens).mockReturnValue({ [unsupportedTokenAddress]: unsupportedToken })
mocked(getExplorerLink).mockReturnValue(unsupportedTokenExplorerLink)
})
it('renders', () => {
const { asFragment } = render(<UnsupportedCurrencyFooter show={true} currencies={[unsupportedToken]} />)
expect(asFragment()).toMatchSnapshot()
})
it('works as expected when one unsupported token exists', async () => {
const rendered = render(<UnsupportedCurrencyFooter show={true} currencies={[unsupportedToken]} />)
await act(() => userEvent.click(screen.getByTestId('read-more-button')))
expect(screen.getByText('Unsupported Assets')).toBeInTheDocument()
expect(
screen.getByText((content) => content.startsWith('Some assets are not available through this interface'))
).toBeInTheDocument()
expect(screen.getAllByTestId('unsupported-token-card').length).toBe(1)
const unsupportedCard = screen.getByTestId('unsupported-token-card')
expect(within(unsupportedCard).getByText(unsupportedTokenSymbol)).toBeInTheDocument()
expect(within(unsupportedCard).getByText(unsupportedTokenAddress).closest('a')).toHaveAttribute(
'href',
unsupportedTokenExplorerLink
)
await act(() => userEvent.click(screen.getByTestId('close-icon')))
await waitForElementToBeRemoved(rendered.queryByTestId('unsupported-token-card'))
expect(rendered.queryByText('Unsupported Assets')).toBeNull()
expect(rendered.queryByTestId('unsupported-token-card')).toBeNull()
expect(
rendered.queryByText((content) => content.startsWith('Some assets are not available through this interface'))
).toBeNull()
})
})
describe('UnsupportedCurrencyFooter.tsx with no unsupported tokens', () => {
beforeEach(() => {
mocked(useUnsupportedTokens).mockReturnValue({})
})
it('works as expected when no unsupported tokens exist', async () => {
const rendered = render(<UnsupportedCurrencyFooter show={true} currencies={[unsupportedToken]} />)
await act(() => userEvent.click(screen.getByTestId('read-more-button')))
expect(screen.getByText('Unsupported Assets')).toBeInTheDocument()
expect(
screen.getByText((content) => content.startsWith('Some assets are not available through this interface'))
).toBeInTheDocument()
expect(rendered.queryByTestId('unsupported-token-card')).toBeNull()
await act(() => userEvent.click(screen.getByTestId('close-icon')))
await waitForElementToBeRemoved(screen.getByText('Unsupported Assets'))
expect(rendered.queryByText('Unsupported Assets')).toBeNull()
expect(
rendered.queryByText((content) => content.startsWith('Some assets are not available through this interface'))
).toBeNull()
})
})

View File

@@ -74,14 +74,14 @@ export default function UnsupportedCurrencyFooter({
<ThemedText.DeprecatedMediumHeader>
<Trans>Unsupported Assets</Trans>
</ThemedText.DeprecatedMediumHeader>
<CloseIcon onClick={() => setShowDetails(false)} />
<CloseIcon onClick={() => setShowDetails(false)} data-testid="close-icon" />
</RowBetween>
{tokens.map((token) => {
return (
token &&
unsupportedTokens &&
Object.keys(unsupportedTokens).includes(token.address) && (
<OutlineCard key={token.address?.concat('not-supported')}>
<OutlineCard key={token.address?.concat('not-supported')} data-testid="unsupported-token-card">
<AutoColumn gap="10px">
<AutoRow gap="5px" align="center">
<CurrencyLogo currency={token} size="24px" />
@@ -108,7 +108,7 @@ export default function UnsupportedCurrencyFooter({
</AutoColumn>
</Card>
</Modal>
<StyledButtonEmpty padding="0" onClick={() => setShowDetails(true)}>
<StyledButtonEmpty padding="0" onClick={() => setShowDetails(true)} data-testid="read-more-button">
<ThemedText.DeprecatedBlue>
<Trans>Read more about unsupported assets</Trans>
</ThemedText.DeprecatedBlue>

View File

@@ -0,0 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c5 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c6 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c8 {
color: #0D111C;
}
.c10 {
width: 100%;
height: 1px;
background-color: #D2D9EE;
}
.c1 {
width: 100%;
padding: 1rem;
border-radius: 16px;
}
.c3 {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 8px;
}
.c7 {
display: inline-block;
height: inherit;
}
.c9 {
color: #7780A0;
}
.c2 {
padding: 0;
}
<div
class="c0 c1 c2"
>
<div
class="c3"
>
<div
class="c0 c4 c5"
>
<div
class="c0 c4 c6"
>
<div
class="c7"
>
<div>
<div
class="css-zhpkf8"
>
Expected Output
</div>
</div>
</div>
</div>
<div
class="c8 css-q4yjm0"
>
0.000000000000001 DEF
</div>
</div>
<div
class="c0 c4 c5"
>
<div
class="c0 c4 c6"
>
<div
class="c7"
>
<div>
<div
class="css-zhpkf8"
>
Price Impact
</div>
</div>
</div>
</div>
<div
class="c8 css-q4yjm0"
>
<div
class="c9 css-1aekuku"
>
105567.37%
</div>
</div>
</div>
<div
class="c10"
/>
<div
class="c0 c4 c5"
>
<div
class="c0 c4 c6"
style="margin-right: 20px;"
>
<div
class="c7"
>
<div>
<div
class="css-zhpkf8"
>
Minimum received after slippage (2.00%)
</div>
</div>
</div>
</div>
<div
class="css-q4yjm0"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,807 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c20 {
box-sizing: border-box;
margin: 0;
min-width: 0;
width: auto;
}
.c37 {
box-sizing: border-box;
margin: 0;
min-width: 0;
width: 100%;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c21 {
width: auto;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c38 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 1px;
}
.c4 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c22 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin: -4px;
}
.c22 > * {
margin: 4px !important;
}
.c39 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin: -1px;
}
.c39 > * {
margin: 1px !important;
}
.c6 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c9 {
color: #0D111C;
}
.c44 {
color: #7780A0;
}
.c17 {
width: 100%;
height: 1px;
background-color: #D2D9EE;
}
.c11 {
width: 100%;
padding: 1rem;
border-radius: 16px;
}
.c12 {
border: 1px solid #B8C0DC;
}
.c3 {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 8px;
}
.c18 {
display: grid;
grid-auto-rows: auto;
}
.c7 {
-webkit-filter: none;
filter: none;
opacity: 1;
-webkit-transition: opacity 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out;
}
.c15 {
display: inline-block;
height: inherit;
}
.c16 {
color: #7780A0;
}
.c14 {
padding: 0;
}
.c33 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background: #E8ECFB;
border: unset;
border-radius: 0.5rem;
color: #000;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
padding: 4px 6px;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
font-weight: 500;
}
.c29 {
width: 20px;
height: 20px;
border-radius: 50%;
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
box-shadow: 0 0 1px white;
}
.c28 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c41 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
}
.c42 {
z-index: 1;
}
.c43 {
position: absolute;
left: -10px !important;
}
.c26 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
width: 100%;
}
.c27 {
display: grid;
grid-template-columns: 24px 1fr 24px;
}
.c30 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding: 0.1rem 0.5rem;
position: relative;
}
.c40 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 4px 4px;
}
.c31 {
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;
position: absolute;
width: calc(100%);
z-index: 1;
opacity: 0.5;
}
.c32 path {
stroke: #98A1C0;
}
.c34 {
background-color: #E8ECFB;
border-radius: 8px;
display: grid;
font-size: 12px;
grid-gap: 4px;
grid-auto-flow: column;
-webkit-box-pack: start;
-webkit-justify-content: start;
-ms-flex-pack: start;
justify-content: start;
padding: 4px 6px 4px 4px;
z-index: 1020;
}
.c35 {
background-color: #B8C0DC;
border-radius: 4px;
color: #7780A0;
font-size: 10px;
padding: 2px 4px;
z-index: 1021;
}
.c36 {
word-break: normal;
}
.c23 {
height: 16px;
width: 16px;
}
.c23:hover {
-webkit-filter: brightness(1.3);
filter: brightness(1.3);
}
.c24 {
line-height: 1rem;
color: #40B66B;
}
.c19 {
padding: 12px 8px 12px 12px;
border-radius: 16px;
border: 1px solid #D2D9EE;
cursor: pointer;
}
.c25 {
margin-left: 8px;
height: 20px;
stroke-width: 2px;
-webkit-transition: -webkit-transform 0.1s;
-webkit-transition: transform 0.1s;
transition: transform 0.1s;
-webkit-transform: none;
-ms-transform: none;
transform: none;
stroke: #98A1C0;
cursor: pointer;
}
.c25:hover {
opacity: 0.8;
}
.c8 {
background-color: transparent;
border: none;
cursor: pointer;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
padding: 0;
grid-template-columns: 1fr auto;
grid-gap: 0.25rem;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
text-align: left;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
padding: 8px 0;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.c2 {
width: 100%;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
border-radius: inherit;
padding: 8px 12px;
margin-top: 0;
min-height: 32px;
}
.c13 {
padding: 12px;
border: 1px solid #D2D9EE;
}
.c5 {
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
}
.c10 {
-webkit-transform: none;
-ms-transform: none;
transform: none;
-webkit-transition: -webkit-transform 0.1s linear;
-webkit-transition: transform 0.1s linear;
transition: transform 0.1s linear;
}
@supports (-webkit-background-clip:text) and (-webkit-text-fill-color:transparent) {
.c24 {
background-image: linear-gradient(90deg,#2172e5 0%,#54e521 163.16%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
<div
class="c0 c1 c2"
style="margin-top: 0px;"
>
<div
class="c3"
style="width: 100%; margin-bottom: -8px;"
>
<div
class="c0 c1 c4 c5"
data-testid="swap-details-header-row"
>
<div
class="c0 c1 c6"
style="position: relative;"
>
<div
class="c7"
data-testid="trade-price-container"
>
<button
class="c8"
title="1 DEF = 1.00 ABC "
>
<div
class="c9 css-zhpkf8"
>
1 DEF = 1.00 ABC
</div>
</button>
</div>
</div>
<div
class="c0 c1 c6"
>
<svg
class="c10"
fill="none"
height="24"
stroke="#98A1C0"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="6 9 12 15 18 9"
/>
</svg>
</div>
</div>
<div
style="height: 0px; overflow: hidden; width: 100%; will-change: height;"
>
<div>
<div
class="c3"
style="padding: 0px 0px 8px 0px;"
>
<div
class="c0 c11 c12 c13"
data-testid="advanced-swap-details"
>
<div
class="c0 c11 c14"
>
<div
class="c3"
>
<div
class="c0 c1 c4"
>
<div
class="c0 c1 c6"
>
<div
class="c15"
>
<div>
<div
class="css-zhpkf8"
>
Expected Output
</div>
</div>
</div>
</div>
<div
class="c9 css-q4yjm0"
>
0.000000000000001 DEF
</div>
</div>
<div
class="c0 c1 c4"
>
<div
class="c0 c1 c6"
>
<div
class="c15"
>
<div>
<div
class="css-zhpkf8"
>
Price Impact
</div>
</div>
</div>
</div>
<div
class="c9 css-q4yjm0"
>
<div
class="c16 css-1aekuku"
>
105567.37%
</div>
</div>
</div>
<div
class="c17"
/>
<div
class="c0 c1 c4"
>
<div
class="c0 c1 c6"
style="margin-right: 20px;"
>
<div
class="c15"
>
<div>
<div
class="css-zhpkf8"
>
Minimum received after slippage (2.00%)
</div>
</div>
</div>
</div>
<div
class="css-q4yjm0"
>
0.00000000000000098 DEF
</div>
</div>
</div>
</div>
</div>
<div
class="c18 c19"
data-testid="swap-route-info"
>
<div
class="c0 c1 c4"
>
<div
class="c20 c21 c22"
width="auto"
>
<svg
class="c23"
fill="none"
height="20"
viewBox="0 0 23 20"
width="23"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<lineargradient
gradientTransform="rotate(95)"
id="AutoRouterIconGradient0"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop
id="stop1"
offset="0"
stop-color="#2274E2"
/>
<stop
id="stop1"
offset="0.5"
stop-color="#2274E2"
/>
<stop
id="stop2"
offset="1"
stop-color="#3FB672"
/>
</lineargradient>
</defs>
<path
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
stroke="url(#AutoRouterIconGradient0)"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
<div
class="c9 c24 css-1aekuku"
>
Auto Router
</div>
</div>
<svg
class="c25"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
x2="12"
y1="5"
y2="19"
/>
<line
x1="5"
x2="19"
y1="12"
y2="12"
/>
</svg>
</div>
<div
style="height: 0px; overflow: hidden; width: 100%; will-change: height;"
>
<div>
<div
class="c20 c21 c22"
style="padding-top: 12px; margin: 0px;"
width="auto"
>
<div
class="c26 css-vurnku"
>
<div
class="c0 c1 c27"
>
<div
class="c28"
>
<img
alt="ABC logo"
class="c29"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
/>
</div>
<div
class="c0 c1 c30"
>
<div
class="c31"
>
<svg
class="c32"
>
dot_line.svg
</svg>
</div>
<div
class="c33 c34"
>
<div
class="c33 c35"
>
<div
class="c36 css-15li2d9"
>
V3
</div>
</div>
<div
class="c36 css-1aekuku"
style="min-width: auto;"
>
100%
</div>
</div>
<div
class="c37 c38 c39"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
<div
class="c15"
>
<div>
<div
class="c33 c40"
>
<div
class="css-mbnpt3"
>
<div
class="c41"
>
<div
class="c42"
>
<div
class="c28"
>
<img
alt="DEF logo"
class="c29"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
/>
</div>
</div>
<div
class="c43"
>
<div
class="c28"
>
<img
alt="ABC logo"
class="c29"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
/>
</div>
</div>
</div>
</div>
<div
class="css-1aekuku"
>
1%
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="c28"
>
<img
alt="DEF logo"
class="c29"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
/>
</div>
</div>
</div>
<div
class="c17"
/>
<div
class="c44 css-65u4ng"
>
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of each step.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,251 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c3 {
box-sizing: border-box;
margin: 0;
min-width: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
font-size: inherit;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: white;
background-color: primary;
border: 0;
border-radius: 4px;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.c2 > * {
margin: !important;
}
.c4 {
padding: 16px;
width: 100%;
font-weight: 500;
text-align: center;
border-radius: 20px;
outline: none;
border: 1px solid transparent;
color: #0D111C;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
will-change: transform;
-webkit-transition: -webkit-transform 450ms ease;
-webkit-transition: transform 450ms ease;
transition: transform 450ms ease;
-webkit-transform: perspective(1px) translateZ(0);
-ms-transform: perspective(1px) translateZ(0);
transform: perspective(1px) translateZ(0);
}
.c4:disabled {
opacity: 50%;
cursor: auto;
pointer-events: none;
}
.c4 > * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c4 > a {
-webkit-text-decoration: none;
text-decoration: none;
}
.c5 {
background-color: #FB118E;
font-size: 20px;
font-weight: 600;
padding: 16px;
color: #F5F6FC;
}
.c5:focus {
box-shadow: 0 0 0 1pt #ee0481;
background-color: #ee0481;
}
.c5:hover {
background-color: #ee0481;
}
.c5:active {
box-shadow: 0 0 0 1pt #d50474;
background-color: #d50474;
}
.c5:disabled {
background-color: #E8ECFB;
color: #7780A0;
cursor: auto;
box-shadow: none;
border: 1px solid transparent;
outline: none;
}
.c6 {
background-color: rgba(250,43,57,0.1);
border-radius: 1rem;
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;
font-size: 0.825rem;
width: 100%;
padding: 3rem 1.25rem 1rem 1rem;
margin-top: -2rem;
color: #FA2B39;
z-index: -1;
}
.c6 p {
padding: 0;
margin: 0;
font-weight: 500;
}
.c7 {
background-color: rgba(250,43,57,0.1);
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;
margin-right: 12px;
border-radius: 12px;
min-width: 48px;
height: 48px;
}
<div
class="c0 c1 c2"
>
<button
class="c3 c4 c5"
data-testid="confirm-swap-button"
disabled=""
id="confirm-swap-or-send"
style="margin: 10px 0px 0px 0px;"
>
<div
class="css-10ob8xa"
>
Confirm Swap
</div>
</button>
<div
class="c6"
>
<div
class="c7"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
<p
style="word-break: break-word;"
>
swap error
</p>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] = `
exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact input 1`] = `
<DocumentFragment>
.c1 {
box-sizing: border-box;

View File

@@ -0,0 +1,168 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UnsupportedCurrencyFooter.tsx with unsupported tokens renders 1`] = `
<DocumentFragment>
.c1 {
box-sizing: border-box;
margin: 0;
min-width: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
font-size: inherit;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: white;
background-color: primary;
border: 0;
border-radius: 4px;
padding: 0;
}
.c5 {
color: #FB118E;
}
.c2 {
padding: 0;
width: 100%;
font-weight: 500;
text-align: center;
border-radius: 20px;
outline: none;
border: 1px solid transparent;
color: #0D111C;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
will-change: transform;
-webkit-transition: -webkit-transform 450ms ease;
-webkit-transition: transform 450ms ease;
transition: transform 450ms ease;
-webkit-transform: perspective(1px) translateZ(0);
-ms-transform: perspective(1px) translateZ(0);
transform: perspective(1px) translateZ(0);
}
.c2:disabled {
opacity: 50%;
cursor: auto;
pointer-events: none;
}
.c2 > * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c2 > a {
-webkit-text-decoration: none;
text-decoration: none;
}
.c3 {
background-color: transparent;
color: #FB118E;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c3:focus {
-webkit-text-decoration: underline;
text-decoration: underline;
}
.c3:hover {
-webkit-text-decoration: none;
text-decoration: none;
}
.c3:active {
-webkit-text-decoration: none;
text-decoration: none;
}
.c3:disabled {
opacity: 50%;
cursor: auto;
}
.c0 {
padding-top: calc(16px + 2rem);
padding-bottom: 20px;
margin-left: auto;
margin-right: auto;
margin-top: -2rem;
width: 100%;
max-width: 400px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
color: #7780A0;
background-color: #FFFFFF99;
z-index: 0;
-webkit-transform: translateY(0%);
-ms-transform: translateY(0%);
transform: translateY(0%);
-webkit-transition: -webkit-transform 300ms ease-in-out;
-webkit-transition: transform 300ms ease-in-out;
transition: transform 300ms ease-in-out;
text-align: center;
}
.c4 {
-webkit-text-decoration: none;
text-decoration: none;
}
<div
class="c0"
>
<button
class="c1 c2 c3 c4"
data-testid="read-more-button"
>
<div
class="c5 css-8mokm4"
>
Read more about unsupported assets
</div>
</button>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,250 @@
import { Web3ReactHooks } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { createDeferredPromise } from 'test-utils/promise'
import { act, renderHook } from '../test-utils/render'
import { ActivationStatus, useActivationState } from './activate'
import { Connection, ConnectionType } from './types'
import { ErrorCode } from './utils'
class MockConnector extends Connector {
activate: () => void
deactivate: () => void
resetState = jest.fn()
constructor(activate: () => void, deactivate?: () => void) {
const actions = {
startActivation: jest.fn(),
update: jest.fn(),
resetState: jest.fn(),
}
super(actions)
this.activate = activate ?? jest.fn()
this.deactivate = deactivate ?? jest.fn()
}
}
function createMockConnection(
activate: () => void,
deactivate?: () => void,
type = ConnectionType.INJECTED
): Connection {
return {
getName: () => 'Test Connection',
hooks: {} as unknown as Web3ReactHooks,
type,
shouldDisplay: () => true,
connector: new MockConnector(activate, deactivate),
}
}
beforeEach(() => {
jest.spyOn(console, 'error').mockReturnValue()
jest.spyOn(console, 'debug').mockReturnValue()
})
it('Should initialize with proper IDLE state', async () => {
const result = renderHook(useActivationState).result
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
})
it('Should call activate function on a connection', async () => {
const activationResponse = createDeferredPromise()
const mockConnection = createMockConnection(jest.fn().mockImplementation(() => activationResponse.promise))
const result = renderHook(useActivationState).result
const onSuccess = jest.fn()
let activationCall: Promise<void> = new Promise(jest.fn())
act(() => {
activationCall = result.current.tryActivation(mockConnection, onSuccess)
})
expect(result.current.activationState).toEqual({ status: ActivationStatus.PENDING, connection: mockConnection })
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(1)
expect(console.debug).toHaveBeenLastCalledWith(`Connection activating: ${mockConnection.getName()}`)
expect(onSuccess).toHaveBeenCalledTimes(0)
await act(async () => {
activationResponse.resolve()
})
await activationCall
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(1)
expect(console.debug).toHaveBeenLastCalledWith(`Connection activated: ${mockConnection.getName()}`)
expect(console.debug).toHaveBeenCalledTimes(2)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
it('Should properly deactivate pending connection attempts', async () => {
const mockConnection = createMockConnection(
jest.fn().mockImplementation(() => new Promise(jest.fn())),
jest.fn().mockImplementation(() => Promise.resolve())
)
const result = renderHook(useActivationState).result
const onSuccess = jest.fn()
act(() => {
result.current.tryActivation(mockConnection, onSuccess)
})
expect(result.current.activationState).toEqual({ status: ActivationStatus.PENDING, connection: mockConnection })
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(1)
await act(() => result.current.cancelActivation())
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(mockConnection.connector.deactivate).toHaveBeenCalledTimes(1)
expect(console.debug).not.toHaveBeenLastCalledWith(`Connection activated: ${mockConnection.getName()}`)
expect(onSuccess).toHaveBeenCalledTimes(0)
})
it('Should properly display error state', async () => {
const activationResponse = createDeferredPromise()
const mockConnection = createMockConnection(
jest.fn().mockImplementation(() => activationResponse.promise),
jest.fn().mockImplementation(() => Promise.resolve())
)
const result = renderHook(useActivationState).result
const onSuccess = jest.fn()
act(() => {
result.current.tryActivation(mockConnection, onSuccess)
})
expect(result.current.activationState).toEqual({ status: ActivationStatus.PENDING, connection: mockConnection })
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(1)
await act(async () => {
activationResponse.reject('Failed to connect')
})
expect(result.current.activationState).toEqual({
status: ActivationStatus.ERROR,
connection: mockConnection,
error: 'Failed to connect',
})
expect(console.debug).toHaveBeenLastCalledWith(`Connection failed: ${mockConnection.getName()}`)
expect(console.debug).toHaveBeenCalledTimes(2)
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledTimes(0)
})
it('Should successfully retry a failed activation', async () => {
const mockConnection = createMockConnection(
jest
.fn()
.mockImplementationOnce(() => Promise.reject('Failed to connect'))
.mockImplementationOnce(() => Promise.resolve())
)
const result = renderHook(useActivationState).result
const onSuccess = jest.fn()
await act(() => result.current.tryActivation(mockConnection, onSuccess))
expect(result.current.activationState).toEqual({
status: ActivationStatus.ERROR,
connection: mockConnection,
error: 'Failed to connect',
})
expect(console.debug).toHaveBeenLastCalledWith(`Connection failed: ${mockConnection.getName()}`)
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledTimes(0)
await act(() => result.current.tryActivation(mockConnection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(mockConnection.connector.activate).toHaveBeenCalledTimes(2)
expect(console.debug).toHaveBeenLastCalledWith(`Connection activated: ${mockConnection.getName()}`)
expect(console.debug).toHaveBeenCalledTimes(4)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
describe('Should gracefully handle intentional user-rejection errors', () => {
it('handles Injected user-rejection error', async () => {
const result = renderHook(useActivationState).result
const injectedConection = createMockConnection(
jest
.fn()
.mockImplementationOnce(() => Promise.reject({ code: ErrorCode.USER_REJECTED_REQUEST }))
.mockImplementationOnce(() => Promise.resolve),
jest.fn(),
ConnectionType.INJECTED
)
const onSuccess = jest.fn()
await act(() => result.current.tryActivation(injectedConection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(injectedConection.connector.activate).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledTimes(0)
await act(() => result.current.tryActivation(injectedConection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(injectedConection.connector.activate).toHaveBeenCalledTimes(2)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
it('handles Coinbase user-rejection error', async () => {
const result = renderHook(useActivationState).result
const coinbaseConnection = createMockConnection(
jest
.fn()
.mockImplementationOnce(() => Promise.reject(ErrorCode.CB_REJECTED_REQUEST))
.mockImplementationOnce(() => Promise.resolve),
jest.fn(),
ConnectionType.COINBASE_WALLET
)
const onSuccess = jest.fn()
await act(() => result.current.tryActivation(coinbaseConnection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(coinbaseConnection.connector.activate).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledTimes(0)
await act(() => result.current.tryActivation(coinbaseConnection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(coinbaseConnection.connector.activate).toHaveBeenCalledTimes(2)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
it('handles WalletConect Modal close error', async () => {
const result = renderHook(useActivationState).result
const wcConnection = createMockConnection(
jest
.fn()
.mockImplementationOnce(() => Promise.reject(ErrorCode.WC_MODAL_CLOSED))
.mockImplementationOnce(() => Promise.resolve),
jest.fn(),
ConnectionType.WALLET_CONNECT
)
const onSuccess = jest.fn()
await act(() => result.current.tryActivation(wcConnection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(wcConnection.connector.activate).toHaveBeenCalledTimes(1)
expect(onSuccess).toHaveBeenCalledTimes(0)
await act(() => result.current.tryActivation(wcConnection, onSuccess))
expect(result.current.activationState).toEqual({ status: ActivationStatus.IDLE })
expect(wcConnection.connector.activate).toHaveBeenCalledTimes(2)
expect(onSuccess).toHaveBeenCalledTimes(1)
})
})

View File

@@ -0,0 +1,91 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { Connection } from 'connection/types'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback } from 'react'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import { didUserReject } from './utils'
export enum ActivationStatus {
PENDING,
ERROR,
IDLE,
}
type ActivationPendingState = { status: ActivationStatus.PENDING; connection: Connection }
type ActivationErrorState = { status: ActivationStatus.ERROR; connection: Connection; error: any }
const IDLE_ACTIVATION_STATE = { status: ActivationStatus.IDLE } as const
type ActivationState = ActivationPendingState | ActivationErrorState | typeof IDLE_ACTIVATION_STATE
const activationStateAtom = atom<ActivationState>(IDLE_ACTIVATION_STATE)
function useTryActivation() {
const dispatch = useAppDispatch()
const setActivationState = useUpdateAtom(activationStateAtom)
return useCallback(
async (connection: Connection, onSuccess: () => void) => {
/*
* Skips wallet connection if the connection should override the default
* behavior, i.e. install MetaMask or launch Coinbase app
*/
if (connection.overrideActivate?.()) return
try {
setActivationState({ status: ActivationStatus.PENDING, connection })
console.debug(`Connection activating: ${connection.getName()}`)
await connection.connector.activate()
console.debug(`Connection activated: ${connection.getName()}`)
dispatch(updateSelectedWallet({ wallet: connection.type }))
// Clears pending connection state
setActivationState(IDLE_ACTIVATION_STATE)
onSuccess()
} catch (error) {
// TODO(WEB-3162): re-add special treatment for already-pending injected errors & move debug to after didUserReject() check
console.debug(`Connection failed: ${connection.getName()}`)
console.error(error)
// Gracefully handles errors from the user rejecting a connection attempt
if (didUserReject(connection, error)) {
setActivationState(IDLE_ACTIVATION_STATE)
return
}
// Failed Connection events are logged here, while successful ones are logged by Web3Provider
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.FAILED,
wallet_type: connection.getName(),
})
setActivationState({ status: ActivationStatus.ERROR, connection, error })
}
},
[dispatch, setActivationState]
)
}
function useCancelActivation() {
const setActivationState = useUpdateAtom(activationStateAtom)
return useCallback(
() =>
setActivationState((activationState) => {
if (activationState.status !== ActivationStatus.IDLE) activationState.connection.connector.deactivate?.()
return IDLE_ACTIVATION_STATE
}),
[setActivationState]
)
}
export function useActivationState() {
const activationState = useAtomValue(activationStateAtom)
const tryActivation = useTryActivation()
const cancelActivation = useCancelActivation()
return { activationState, tryActivation, cancelActivation }
}

View File

@@ -1,8 +1,10 @@
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
import { ConnectionType, getConnections, useGetConnection } from 'connection'
import { getConnections, useGetConnection } from 'connection'
import { renderHook } from 'test-utils/render'
import { ConnectionType } from './types'
const UserAgentMock = jest.requireMock('utils/userAgent')
jest.mock('utils/userAgent', () => ({
isMobile: false,

View File

@@ -1,5 +1,5 @@
import { CoinbaseWallet } from '@web3-react/coinbase-wallet'
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
import { initializeConnector } from '@web3-react/core'
import { GnosisSafe } from '@web3-react/gnosis-safe'
import { MetaMask } from '@web3-react/metamask'
import { Network } from '@web3-react/network'
@@ -18,29 +18,10 @@ import { isMobile, isNonIOSPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks'
import { RPC_PROVIDERS } from '../constants/providers'
import { Connection, ConnectionType } from './types'
import { getIsCoinbaseWallet, getIsInjected, getIsMetaMaskWallet } from './utils'
import { UniwalletConnect, WalletConnectPopup } from './WalletConnect'
export enum ConnectionType {
UNIWALLET = 'UNIWALLET',
INJECTED = 'INJECTED',
COINBASE_WALLET = 'COINBASE_WALLET',
WALLET_CONNECT = 'WALLET_CONNECT',
NETWORK = 'NETWORK',
GNOSIS_SAFE = 'GNOSIS_SAFE',
}
export interface Connection {
getName(): string
connector: Connector
hooks: Web3ReactHooks
type: ConnectionType
getIcon?(isDarkMode: boolean): string
shouldDisplay(): boolean
overrideActivate?: () => boolean
isNew?: boolean
}
function onError(error: Error) {
console.debug(`web3-react error: ${error}`)
}

22
src/connection/types.ts Normal file
View File

@@ -0,0 +1,22 @@
import { Web3ReactHooks } from '@web3-react/core'
import { Connector } from '@web3-react/types'
export enum ConnectionType {
UNIWALLET = 'UNIWALLET',
INJECTED = 'INJECTED',
COINBASE_WALLET = 'COINBASE_WALLET',
WALLET_CONNECT = 'WALLET_CONNECT',
NETWORK = 'NETWORK',
GNOSIS_SAFE = 'GNOSIS_SAFE',
}
export interface Connection {
getName(): string
connector: Connector
hooks: Web3ReactHooks
type: ConnectionType
getIcon?(isDarkMode: boolean): string
shouldDisplay(): boolean
overrideActivate?: () => boolean
isNew?: boolean
}

View File

@@ -1,3 +1,5 @@
import { Connection, ConnectionType } from 'connection/types'
export const getIsInjected = () => Boolean(window.ethereum)
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
@@ -25,3 +27,12 @@ export enum ErrorCode {
WC_MODAL_CLOSED = 'Error: User closed modal',
CB_REJECTED_REQUEST = 'Error: User denied account authorization',
}
// TODO(WEB-3279): merge this function with existing didUserReject for Swap errors
export function didUserReject(connection: Connection, error: any): boolean {
return (
error?.code === ErrorCode.USER_REJECTED_REQUEST ||
(connection.type === ConnectionType.WALLET_CONNECT && error?.toString?.() === ErrorCode.WC_MODAL_CLOSED) ||
(connection.type === ConnectionType.COINBASE_WALLET && error?.toString?.() === ErrorCode.CB_REJECTED_REQUEST)
)
}

View File

@@ -25,6 +25,13 @@ export enum SupportedChainId {
BNB = 56,
}
export const UniWalletSupportedChains = [
SupportedChainId.MAINNET,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.OPTIMISM,
SupportedChainId.POLYGON,
]
export const CHAIN_IDS_TO_NAMES = {
[SupportedChainId.MAINNET]: 'mainnet',
[SupportedChainId.GOERLI]: 'goerli',

View File

@@ -6,6 +6,10 @@ const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/se
const CMC_ALL_LIST = 'https://s3.coinmarketcap.com/generated/dex/tokens/eth-tokens-all.json'
const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
const COINGECKO_BNB_LIST = 'https://tokens.coingecko.com/binance-smart-chain/all.json'
const COINGECKO_ARBITRUM_LIST = 'https://tokens.coingecko.com/arbitrum-one/all.json'
const COINGECKO_OPTIMISM_LIST = 'https://tokens.coingecko.com/optimistic-ethereum/all.json'
const COINGECKO_CELO_LIST = 'https://tokens.coingecko.com/celo/all.json'
const COINGECKO_POLYGON_LIST = 'https://tokens.coingecko.com/polygon-pos/all.json'
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
const KLEROS_LIST = 't2crtokens.eth'
@@ -28,6 +32,10 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
CMC_ALL_LIST,
COINGECKO_LIST,
COINGECKO_BNB_LIST,
COINGECKO_ARBITRUM_LIST,
COINGECKO_OPTIMISM_LIST,
COINGECKO_CELO_LIST,
COINGECKO_POLYGON_LIST,
KLEROS_LIST,
GEMINI_LIST,
WRAPPED_LIST,

View File

@@ -1,6 +1,7 @@
import { Connector } from '@web3-react/types'
import { Connection, gnosisSafeConnection, networkConnection } from 'connection'
import { gnosisSafeConnection, networkConnection } from 'connection'
import { useGetConnection } from 'connection'
import { Connection } from 'connection/types'
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'

View File

@@ -1,5 +1,5 @@
import { ConnectionType } from 'connection'
import { useGetConnection } from 'connection'
import { ConnectionType } from 'connection/types'
import { useMemo } from 'react'
import { useAppSelector } from 'state/hooks'

View File

@@ -1,4 +1,4 @@
import { TEST_NFT_ACTIVITY_EVENT } from 'test-utils/constants'
import { TEST_NFT_ACTIVITY_EVENT } from 'test-utils/nft/fixtures'
import { render, screen } from 'test-utils/render'
import { BuyCell } from './ActivityCells'

View File

@@ -1,5 +1,5 @@
import { UniformAspectRatios } from 'nft/types'
import { TEST_NFT_ASSET } from 'test-utils/constants'
import { TEST_NFT_ASSET } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { CollectionAsset } from './CollectionAsset'

View File

@@ -1,4 +1,4 @@
import { TEST_NFT_ASSET } from 'test-utils/constants'
import { TEST_NFT_ASSET } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { DataPage } from './DataPage'

View File

@@ -12,6 +12,7 @@ import {
} from 'nft/components/profile/list/utils'
import { useSellAsset } from 'nft/hooks'
import { ListingMarket, WalletAsset } from 'nft/types'
import { getMarketplaceIcon } from 'nft/utils'
import { formatEth, formatUsdPrice } from 'nft/utils/currency'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { Dispatch, DispatchWithoutAction, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
@@ -64,11 +65,10 @@ const MarketIconWrapper = styled(Column)`
cursor: pointer;
`
const MarketIcon = styled.img<{ index: number }>`
const MarketIcon = styled.div<{ index: number }>`
width: 20px;
height: 20px;
border-radius: 4px;
object-fit: cover;
z-index: ${({ index }) => 2 - index};
margin-left: ${({ index }) => `${index === 0 ? 0 : -8}px`};
outline: 1px solid ${({ theme }) => theme.backgroundInteractive};
@@ -214,7 +214,7 @@ export const MarketplaceRow = ({
removeMarket && removeMarket()
}}
>
<MarketIcon alt={market.name} src={market.icon} index={index} />
<MarketIcon index={index}>{getMarketplaceIcon(market.name, '20')}</MarketIcon>
<RemoveMarketplaceWrap hovered={marketIconHovered && (expandMarketplaceRows ?? false)}>
<img width="20px" src="/nft/svgs/minusCircle.svg" alt="Remove item" />
</RemoveMarketplaceWrap>

View File

@@ -4,6 +4,7 @@ import Loader from 'components/Icons/LoadingSpinner'
import Row from 'components/Row'
import { VerifiedIcon } from 'nft/components/icons'
import { AssetRow, CollectionRow, ListingStatus } from 'nft/types'
import { getMarketplaceIcon } from 'nft/utils'
import { useEffect, useRef } from 'react'
import { Check, XOctagon } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
@@ -37,7 +38,7 @@ const AssetIcon = styled.img`
z-index: 1;
`
const MarketplaceIcon = styled.img`
const MarketplaceIcon = styled.div`
border-radius: 4px;
height: 24px;
width: 24px;
@@ -137,8 +138,8 @@ export const ContentRow = ({
failed={failed}
ref={rowRef}
>
{isCollectionApprovalSection ? <CollectionIcon src={row.images[0]} /> : <AssetIcon src={row.images[0]} />}
<MarketplaceIcon src={row.images[1]} />
{isCollectionApprovalSection ? <CollectionIcon src={row.image} /> : <AssetIcon src={row.image} />}
<MarketplaceIcon>{getMarketplaceIcon(row.marketplace.name, '24')}</MarketplaceIcon>
<ContentName>{row.name}</ContentName>
{isCollectionApprovalSection && (row as CollectionRow).isVerified && <StyledVerifiedIcon />}
<IconWrapper>

View File

@@ -143,7 +143,7 @@ export const ListModalSection = ({ sectionType, active, content, toggleSection }
{content.map((row: AssetRow) => (
<ContentRow
row={row}
key={(row?.name ?? '') + row?.images[1]}
key={row?.name ?? row.marketplace.name}
removeRow={removeRow}
isCollectionApprovalSection={isCollectionApprovalSection}
/>

View File

@@ -3,8 +3,8 @@ import Column from 'components/Column'
import Row from 'components/Row'
import { getMarketplaceFee, getRoyalty } from 'nft/components/profile/list/utils'
import { ListingMarket, WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils'
import styled from 'styled-components/macro'
import { formatEth, getMarketplaceIcon } from 'nft/utils'
import styled, { css } from 'styled-components/macro'
import { ThemedText } from 'theme'
const FeeWrap = styled(Row)`
@@ -17,17 +17,22 @@ const RoyaltyContainer = styled(Column)`
padding: 4px 0px;
`
const MarketIcon = styled.img`
const iconStyles = css`
width: 16px;
height: 16px;
border-radius: 2px;
object-fit: cover;
outline: 1px solid ${({ theme }) => theme.backgroundInteractive};
margin-right: 8px;
`
const CollectionIcon = styled(MarketIcon)`
const MarketIcon = styled.div`
border-radius: 4px;
${iconStyles}
`
const CollectionIcon = styled.img`
object-fit: cover;
border-radius: 50%;
${iconStyles}
`
const FeePercent = styled(ThemedText.Caption)`
@@ -57,7 +62,7 @@ export const RoyaltyTooltip = ({
{selectedMarkets.map((market) => (
<FeeWrap key={asset.collection?.address ?? '' + asset.tokenId + market.name + 'fee'}>
<Row>
<MarketIcon src={market.icon} />
<MarketIcon>{getMarketplaceIcon(market.name, '16')}</MarketIcon>
<ThemedText.Caption lineHeight="16px" marginRight="12px">
{market.name}&nbsp;
<Trans>fee</Trans>

View File

@@ -6,6 +6,7 @@ import { Checkbox } from 'nft/components/layout/Checkbox'
import { buttonTextMedium, caption } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css'
import { ListingMarket } from 'nft/types'
import { getMarketplaceIcon } from 'nft/utils'
import { ListingMarkets } from 'nft/utils/listNfts'
import { Dispatch, FormEvent, useMemo, useReducer, useRef } from 'react'
import styled from 'styled-components/macro'
@@ -25,13 +26,6 @@ const MarketplaceRowWrapper = styled(Row)`
border-radius: 12px;
`
const MarketplaceDropdownIcon = styled.img`
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: cover;
`
const FeeText = styled.div`
color: ${({ theme }) => theme.textSecondary};
`
@@ -60,7 +54,7 @@ const MarketplaceRow = ({ market, setSelectedMarkets, selectedMarkets }: Marketp
return (
<MarketplaceRowWrapper onMouseEnter={toggleHovered} onMouseLeave={toggleHovered} onClick={toggleSelected}>
<Row gap="12" onClick={toggleSelected}>
<MarketplaceDropdownIcon alt={market.name} src={market.icon} />
{getMarketplaceIcon(market.name, '24')}
<Column>
<ThemedText.BodyPrimary>{market.name}</ThemedText.BodyPrimary>
<FeeText className={caption}>{market.fee}% fee</FeeText>
@@ -93,12 +87,11 @@ const HeaderButtonContentWrapper = styled.div`
display: flex;
`
const MarketIcon = styled.img<{ index: number; totalSelected: number }>`
const MarketIcon = styled.div<{ index: number; totalSelected: number }>`
height: 20px;
width: 20px;
margin-right: 8px;
border: 1px solid;
border-color: ${({ theme }) => theme.backgroundInteractive};
outline: 1px solid ${({ theme }) => theme.backgroundInteractive};
border-radius: 4px;
z-index: ${({ index, totalSelected }) => totalSelected - index};
margin-left: ${({ index }) => `${index === 0 ? 0 : -18}px`};
@@ -156,13 +149,9 @@ export const SelectMarketplacesDropdown = ({
<HeaderButtonContentWrapper>
{selectedMarkets.map((market, index) => {
return (
<MarketIcon
key={index}
alt={market.name}
src={market.icon}
totalSelected={selectedMarkets.length}
index={index}
/>
<MarketIcon key={index} totalSelected={selectedMarkets.length} index={index}>
{getMarketplaceIcon(market.name, '20')}
</MarketIcon>
)
})}
{dropdownDisplayText}

View File

@@ -93,7 +93,7 @@ const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]]
sellAssets.forEach((asset) => {
asset.marketplaces?.forEach((marketplace: ListingMarket) => {
const newListing = {
images: [asset.smallImageUrl, marketplace.icon],
image: asset.smallImageUrl,
name: asset.name || `#${asset.tokenId}`,
status: ListingStatus.DEFINED,
asset,
@@ -109,7 +109,7 @@ const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]]
)
) {
const newCollectionRow = {
images: [asset.asset_contract.image_url, marketplace.icon],
image: asset.asset_contract.image_url,
name: asset.asset_contract.name,
status: ListingStatus.DEFINED,
collectionAddress: asset.asset_contract.address,

View File

@@ -1,4 +1,4 @@
import { TEST_NFT_WALLET_ASSET } from 'test-utils/constants'
import { TEST_NFT_WALLET_ASSET } from 'test-utils/nft/fixtures'
import { render } from 'test-utils/render'
import { ViewMyNftsAsset } from './ViewMyNftsAsset'

View File

@@ -5,7 +5,6 @@ import { GenieCollection, PriceInfo } from '../common'
export interface ListingMarket {
name: string
fee: number
icon: string
}
export interface SellOrder {
@@ -89,22 +88,21 @@ export enum ListingStatus {
}
export interface AssetRow {
images: (string | undefined)[]
image: string | undefined
name?: string
status: ListingStatus
marketplace: ListingMarket
callback?: () => Promise<void>
}
export interface ListingRow extends AssetRow {
asset: WalletAsset
marketplace: ListingMarket
price?: number
}
export interface CollectionRow extends AssetRow {
collectionAddress?: string
isVerified?: boolean
marketplace: ListingMarket
nftStandard?: NftStandard
}

View File

@@ -35,12 +35,10 @@ export const ListingMarkets: ListingMarket[] = [
{
name: 'X2Y2',
fee: 0.5,
icon: '/nft/svgs/marketplaces/x2y2.svg',
},
{
name: 'OpenSea',
fee: 0,
icon: '/nft/svgs/marketplaces/opensea.svg',
},
]

View File

@@ -3,6 +3,7 @@ import 'jest-styled-components' // adds style diffs to snapshot tests
import type { createPopper } from '@popperjs/core'
import { useWeb3React } from '@web3-react/core'
import ResizeObserver from 'resize-observer-polyfill'
import { Readable } from 'stream'
import { mocked } from 'test-utils/mocked'
import { TextDecoder, TextEncoder } from 'util'
@@ -16,6 +17,8 @@ if (typeof global.TextEncoder === 'undefined') {
global.TextDecoder = TextDecoder as typeof global.TextDecoder
}
global.ResizeObserver = ResizeObserver
global.matchMedia =
global.matchMedia ||
function () {

View File

@@ -98,10 +98,6 @@ export function useOpenModal(modal: ApplicationModal): () => void {
return useCallback(() => dispatch(setOpenModal(modal)), [dispatch, modal])
}
export function useToggleUniwalletModal(): () => void {
return useToggleModal(ApplicationModal.UNIWALLET_CONNECT)
}
export function useToggleSettingsMenu(): () => void {
return useToggleModal(ApplicationModal.SETTINGS)
}

View File

@@ -13,7 +13,6 @@ export type PopupContent =
}
export enum ApplicationModal {
UNIWALLET_CONNECT,
ADDRESS_CLAIM,
BLOCKED_ACCOUNT,
CLAIM_POPUP,

View File

@@ -1,5 +1,5 @@
import { createSlice } from '@reduxjs/toolkit'
import { ConnectionType } from 'connection'
import { ConnectionType } from 'connection/types'
interface ConnectionState {
errorByConnectionType: Record<ConnectionType, string | undefined>

View File

@@ -1,5 +1,5 @@
import { createSlice } from '@reduxjs/toolkit'
import { ConnectionType } from 'connection'
import { ConnectionType } from 'connection/types'
import { SupportedLocale } from 'constants/locales'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'

View File

@@ -1,9 +1,7 @@
import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { V3Route } from '@uniswap/smart-order-router'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import { NftActivityType, NftStandard, OrderStatus } from 'graphql/data/__generated__/types-and-hooks'
import JSBI from 'jsbi'
import { ActivityEvent, GenieAsset, Markets, WalletAsset } from 'nft/types'
import { InterfaceTrade } from 'state/routing/types'
export const TEST_TOKEN_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc')
@@ -57,100 +55,3 @@ export const TEST_TRADE_EXACT_OUTPUT = new InterfaceTrade({
})
export const TEST_ALLOWED_SLIPPAGE = new Percent(2, 100)
export const TEST_NFT_ASSET: GenieAsset = {
id: 'TmZ0QXNzZXQ6MHhlZDVhZjM4ODY1MzU2N2FmMmYzODhlNjIyNGRjN2M0YjMyNDFjNTQ0XzMzMTg=',
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
notForSale: false,
collectionName: 'Azuki',
imageUrl:
'https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/3318/50ed67ad647d0aa0cad0b830d136a677efc2fb72a44587bc35f2a5fb334a7fdf.png',
marketplace: Markets.Opensea,
name: 'Azuki #3318',
priceInfo: {
ETHPrice: '15800000000000000000',
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: '15800000000000000000',
},
susFlag: false,
tokenId: '3318',
tokenType: NftStandard.Erc721,
totalCount: 10000,
collectionIsVerified: true,
rarity: {
primaryProvider: 'Rarity Sniper',
providers: [
{
rank: 7079,
provider: 'Rarity Sniper',
},
],
},
creator: {},
}
export const TEST_NFT_WALLET_ASSET: WalletAsset = {
id: 'TmZ0QXNzZXQ6RVRIRVJFVU1fMHgyOTY1MkMyZTlEMzY1NjQzNEJjODEzM2M2OTI1OEM4ZDA1MjkwZjQxXzIzNTk=',
imageUrl: 'https://c.neevacdn.net/image/upload/xyz/T96PksTnWGNh79CrzLn-zpYfqRWtD5wME0MBPL_Md6Q.png',
smallImageUrl:
'https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/T96PksTnWGNh79CrzLn-zpYfqRWtD5wME0MBPL_Md6Q.webp',
notForSale: true,
priceInfo: {
ETHPrice: '0',
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: '0',
},
name: 'Froggy Friend #2359',
tokenId: '2359',
asset_contract: {
address: '0x29652c2e9d3656434bc8133c69258c8d05290f41',
tokenType: NftStandard.Erc721,
name: 'Froggy Friends Official',
description: '4444 of the friendliest frogs in the metaverse.',
image_url: 'https://i.seadn.io/gcs/files/84483786d97b4d471cb48d224c4c5c91.png?w=500&auto=format',
},
collection: {
address: '0x29652c2e9d3656434bc8133c69258c8d05290f41',
name: 'Froggy Friends Official',
isVerified: true,
imageUrl: 'https://i.seadn.io/gcs/files/84483786d97b4d471cb48d224c4c5c91.png?w=500&auto=format',
twitterUrl: '@FroggyFriendNFT',
},
collectionIsVerified: true,
lastPrice: 0,
floorPrice: 0.0775,
basisPoints: 0,
date_acquired: '1682024661',
sellOrders: [],
}
export const TEST_NFT_ACTIVITY_EVENT: ActivityEvent = {
collectionAddress: '0xed5af388653567af2f388e6224dc7c4b3241c544',
tokenId: '5674',
tokenMetadata: {
name: 'Azuki #5674',
imageUrl:
'https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/5674/b2e5cb241d4a28bb3688ff6ae12f2d60c9850721f35f5104b5c42b31511e8a42.png',
smallImageUrl: 'https://i.seadn.io/gcs/files/e2dabe8f353ed6354f5a1927e3d8bd64.png?w=500&auto=format',
metadataUrl: 'ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/5674',
rarity: {
source: 'RARITY_SNIPER',
rank: 9412,
score: 2778,
},
suspiciousFlag: false,
standard: NftStandard.Erc721,
},
eventType: NftActivityType.Listing,
marketplace: 'OPENSEA',
fromAddress: '0xbf9fda32692b25c6083cbe48399ef019b62f0712',
toAddress: undefined,
transactionHash: undefined,
price: '15.2',
orderStatus: OrderStatus.Valid,
quantity: 1,
url: 'https://opensea.io/assets/0xed5af388653567af2f388e6224dc7c4b3241c544/5674',
eventTimestamp: 1682444662,
}

View File

@@ -0,0 +1,99 @@
import { NftActivityType, NftStandard, OrderStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ActivityEvent, GenieAsset, Markets, WalletAsset } from 'nft/types'
export const TEST_NFT_ASSET: GenieAsset = {
id: 'TmZ0QXNzZXQ6MHhlZDVhZjM4ODY1MzU2N2FmMmYzODhlNjIyNGRjN2M0YjMyNDFjNTQ0XzMzMTg=',
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
notForSale: false,
collectionName: 'Azuki',
imageUrl:
'https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/3318/50ed67ad647d0aa0cad0b830d136a677efc2fb72a44587bc35f2a5fb334a7fdf.png',
marketplace: Markets.Opensea,
name: 'Azuki #3318',
priceInfo: {
ETHPrice: '15800000000000000000',
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: '15800000000000000000',
},
susFlag: false,
tokenId: '3318',
tokenType: NftStandard.Erc721,
totalCount: 10000,
collectionIsVerified: true,
rarity: {
primaryProvider: 'Rarity Sniper',
providers: [
{
rank: 7079,
provider: 'Rarity Sniper',
},
],
},
creator: {},
}
export const TEST_NFT_WALLET_ASSET: WalletAsset = {
id: 'TmZ0QXNzZXQ6RVRIRVJFVU1fMHgyOTY1MkMyZTlEMzY1NjQzNEJjODEzM2M2OTI1OEM4ZDA1MjkwZjQxXzIzNTk=',
imageUrl: 'https://c.neevacdn.net/image/upload/xyz/T96PksTnWGNh79CrzLn-zpYfqRWtD5wME0MBPL_Md6Q.png',
smallImageUrl:
'https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/T96PksTnWGNh79CrzLn-zpYfqRWtD5wME0MBPL_Md6Q.webp',
notForSale: true,
priceInfo: {
ETHPrice: '0',
baseAsset: 'ETH',
baseDecimals: '18',
basePrice: '0',
},
name: 'Froggy Friend #2359',
tokenId: '2359',
asset_contract: {
address: '0x29652c2e9d3656434bc8133c69258c8d05290f41',
tokenType: NftStandard.Erc721,
name: 'Froggy Friends Official',
description: '4444 of the friendliest frogs in the metaverse.',
image_url: 'https://i.seadn.io/gcs/files/84483786d97b4d471cb48d224c4c5c91.png?w=500&auto=format',
},
collection: {
address: '0x29652c2e9d3656434bc8133c69258c8d05290f41',
name: 'Froggy Friends Official',
isVerified: true,
imageUrl: 'https://i.seadn.io/gcs/files/84483786d97b4d471cb48d224c4c5c91.png?w=500&auto=format',
twitterUrl: '@FroggyFriendNFT',
},
collectionIsVerified: true,
lastPrice: 0,
floorPrice: 0.0775,
basisPoints: 0,
date_acquired: '1682024661',
sellOrders: [],
}
export const TEST_NFT_ACTIVITY_EVENT: ActivityEvent = {
collectionAddress: '0xed5af388653567af2f388e6224dc7c4b3241c544',
tokenId: '5674',
tokenMetadata: {
name: 'Azuki #5674',
imageUrl:
'https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/5674/b2e5cb241d4a28bb3688ff6ae12f2d60c9850721f35f5104b5c42b31511e8a42.png',
smallImageUrl: 'https://i.seadn.io/gcs/files/e2dabe8f353ed6354f5a1927e3d8bd64.png?w=500&auto=format',
metadataUrl: 'ipfs://QmZcH4YvBVVRJtdn4RdbaqgspFU8gH6P9vomDpBVpAL3u4/5674',
rarity: {
source: 'RARITY_SNIPER',
rank: 9412,
score: 2778,
},
suspiciousFlag: false,
standard: NftStandard.Erc721,
},
eventType: NftActivityType.Listing,
marketplace: 'OPENSEA',
fromAddress: '0xbf9fda32692b25c6083cbe48399ef019b62f0712',
toAddress: undefined,
transactionHash: undefined,
price: '15.2',
orderStatus: OrderStatus.Valid,
quantity: 1,
url: 'https://opensea.io/assets/0xed5af388653567af2f388e6224dc7c4b3241c544/5674',
eventTimestamp: 1682444662,
}

17
src/test-utils/promise.ts Normal file
View File

@@ -0,0 +1,17 @@
type DeferredPromise<T> = {
promise: Promise<T>
resolve: (value: T) => void
reject: (reason: unknown) => void
}
export function createDeferredPromise<T = void>() {
const deferedPromise = {} as DeferredPromise<T>
const promise = new Promise<T>((resolve, reject) => {
deferedPromise.reject = reject
deferedPromise.resolve = resolve
})
deferedPromise.promise = promise
return deferedPromise
}

View File

@@ -9046,10 +9046,10 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress-hardhat@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cypress-hardhat/-/cypress-hardhat-1.0.1.tgz#11b86653282769dadc0bd0c65ca41011865b0762"
integrity sha512-eGD7fNM8BXShXEsDbO/m2jv9mx7jHs44bnuWKYxO29ySXX5Soz9+AFYelhzKDvh/T+MJy1YqApPbif9+PNA++g==
cypress-hardhat@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/cypress-hardhat/-/cypress-hardhat-2.0.0.tgz#98f07370270ec7c754d35f77c73b216d9f924abb"
integrity sha512-YLLVZa/15CBo7mmu5JuIGAPg4jLbSYTw/LR690tx90WEiIaPH9diHzWAE41wB+cvmgM7fGwhKnKEEeE1s1DWKg==
cypress@*, cypress@10.3.1:
version "10.3.1"