Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a3475bf6 | ||
|
|
f6c393b016 | ||
|
|
15f8d34320 | ||
|
|
504e09d3dc | ||
|
|
1f755e8b0d | ||
|
|
f45a7f921b | ||
|
|
29db61ff90 | ||
|
|
8431ad9161 | ||
|
|
fd1aded517 | ||
|
|
27ad7cbd41 | ||
|
|
01e5de436a | ||
|
|
fd5aa1b51e | ||
|
|
a6e1a7e6d9 | ||
|
|
629fe2c144 | ||
|
|
d73763ce75 | ||
|
|
fe6df38997 | ||
|
|
719ee0f5b5 | ||
|
|
75bdf9a8d4 | ||
|
|
efbe3994bb | ||
|
|
93fe8e4349 | ||
|
|
6062f615a0 | ||
|
|
42e3af7b5c | ||
|
|
57274a800d |
@@ -2,13 +2,18 @@
|
||||
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir')
|
||||
rulesDirPlugin.RULES_DIR = 'eslint_rules'
|
||||
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
extends: ['@uniswap/eslint-config/react'],
|
||||
plugins: ['rulesdir'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*'],
|
||||
rules: {
|
||||
'multiline-comment-style': ['error', 'separate-lines'],
|
||||
'rulesdir/enforce-retry-on-import': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Release
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * 1-4' # every day 12:00 UTC Monday-Thursday
|
||||
- cron: '0 16 * * 1-4' # every day 16:00 UTC Monday-Thursday
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
118
craco.config.cjs
118
craco.config.cjs
@@ -1,9 +1,11 @@
|
||||
/* eslint-env node */
|
||||
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
|
||||
const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
|
||||
const { DefinePlugin, IgnorePlugin } = require('webpack')
|
||||
const path = require('path')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
@@ -12,6 +14,11 @@ const isProduction = process.env.NODE_ENV === 'production'
|
||||
// Omit them from production builds, as they slow down the feedback loop.
|
||||
const shouldLintOrTypeCheck = !isProduction
|
||||
|
||||
function getCacheDirectory(cacheName) {
|
||||
// Include the trailing slash to denote that this is a directory.
|
||||
return `${path.join(__dirname, 'node_modules/.cache/', cacheName)}/`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
babel: {
|
||||
plugins: [
|
||||
@@ -44,8 +51,13 @@ module.exports = {
|
||||
pluginOptions(eslintConfig) {
|
||||
return Object.assign(eslintConfig, {
|
||||
cache: true,
|
||||
cacheLocation: 'node_modules/.cache/eslint/',
|
||||
cacheLocation: getCacheDirectory('eslint'),
|
||||
ignorePath: '.gitignore',
|
||||
// Use our own eslint/plugins/config, as overrides interfere with caching.
|
||||
// This ensures that `yarn start` and `yarn lint` share one cache.
|
||||
eslintPath: require.resolve('eslint'),
|
||||
resolvePluginsRelativeTo: null,
|
||||
baseConfig: null,
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -55,11 +67,13 @@ module.exports = {
|
||||
jest: {
|
||||
configure(jestConfig) {
|
||||
return Object.assign(jestConfig, {
|
||||
transform: {
|
||||
'\\.css\\.ts$': './vanilla.transform.cjs',
|
||||
...jestConfig.transform,
|
||||
},
|
||||
cacheDirectory: 'node_modules/.cache/jest',
|
||||
cacheDirectory: getCacheDirectory('jest'),
|
||||
transform: Object.assign(jestConfig.transform, {
|
||||
// Transform vanilla-extract using its own transformer.
|
||||
// See https://sandroroth.com/blog/vanilla-extract-cra#jest-transform.
|
||||
'\\.css\\.ts$': '@vanilla-extract/jest-transform',
|
||||
}),
|
||||
// Use @uniswap/conedison's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
@@ -69,8 +83,19 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
webpack: {
|
||||
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
|
||||
plugins: [
|
||||
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
|
||||
new ProvidePlugin({
|
||||
// - react-markdown requires process.cwd
|
||||
process: 'process/browser',
|
||||
}),
|
||||
// vanilla-extract has poor performance on M1 machines with 'debug' identifiers, so we use 'short' instead.
|
||||
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
|
||||
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
|
||||
new VanillaExtractPlugin({ identifiers: 'short' }),
|
||||
],
|
||||
configure: (webpackConfig) => {
|
||||
// Configure webpack plugins:
|
||||
webpackConfig.plugins = webpackConfig.plugins
|
||||
.map((plugin) => {
|
||||
// Extend process.env with dynamic values (eg commit hash).
|
||||
@@ -87,10 +112,16 @@ module.exports = {
|
||||
plugin.options.ignoreOrder = true
|
||||
}
|
||||
|
||||
// Disable TypeScript's config overwrite, as it interferes with incremental build caching.
|
||||
// This ensures that `yarn start` and `yarn typecheck` share one cache.
|
||||
if (plugin.constructor.name == 'ForkTsCheckerWebpackPlugin') {
|
||||
delete plugin.options.typescript.configOverwrite
|
||||
}
|
||||
|
||||
return plugin
|
||||
})
|
||||
.filter((plugin) => {
|
||||
// Case sensitive paths are enforced by TypeScript.
|
||||
// Case sensitive paths are already enforced by TypeScript.
|
||||
// See https://www.typescriptlang.org/tsconfig#forceConsistentCasingInFileNames.
|
||||
if (plugin instanceof CaseSensitivePathsPlugin) return false
|
||||
|
||||
@@ -100,20 +131,67 @@ module.exports = {
|
||||
return true
|
||||
})
|
||||
|
||||
// We're currently on Webpack 4.x which doesn't support the `exports` field in package.json.
|
||||
// Instead, we need to manually map the import path to the correct exports path (eg dist or build folder).
|
||||
// See https://github.com/webpack/webpack/issues/9509.
|
||||
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
|
||||
// Configure webpack resolution:
|
||||
webpackConfig.resolve = Object.assign(webpackConfig.resolve, {
|
||||
plugins: webpackConfig.resolve.plugins.map((plugin) => {
|
||||
// Allow vanilla-extract in production builds.
|
||||
// This is necessary because create-react-app guards against external imports.
|
||||
// See https://sandroroth.com/blog/vanilla-extract-cra#production-build.
|
||||
if (plugin instanceof ModuleScopePlugin) {
|
||||
plugin.allowedPaths.push(path.join(__dirname, 'node_modules/@vanilla-extract/webpack-plugin'))
|
||||
}
|
||||
|
||||
return plugin
|
||||
}),
|
||||
// Webpack 5 does not resolve node modules, so we do so for those necessary:
|
||||
fallback: {
|
||||
// - react-markdown requires path
|
||||
path: require.resolve('path-browserify'),
|
||||
},
|
||||
})
|
||||
|
||||
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
|
||||
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
|
||||
// The fallback rule (eg for dependencies).
|
||||
if (rule.loader && rule.loader.match(/babel-loader/) && !rule.include) {
|
||||
// Allow not-fully-specified modules so that legacy packages are still able to build.
|
||||
rule.resolve = { fullySpecified: false }
|
||||
|
||||
// The class properties transform is required for @uniswap/analytics to build.
|
||||
rule.options.plugins.push('@babel/plugin-proposal-class-properties')
|
||||
}
|
||||
return rule
|
||||
})
|
||||
|
||||
// Configure webpack optimization:
|
||||
webpackConfig.optimization.splitChunks = Object.assign(webpackConfig.optimization.splitChunks, {
|
||||
// Cap the chunk size to 5MB.
|
||||
// react-scripts suggests a chunk size under 1MB after gzip, but we can only measure maxSize before gzip.
|
||||
// react-scripts also caps cacheable chunks at 5MB, which gzips to below 1MB, so we cap chunk size there.
|
||||
// See https://github.com/facebook/create-react-app/blob/d960b9e/packages/react-scripts/config/webpack.config.js#L713-L716.
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
webpackConfig.optimization = Object.assign(
|
||||
webpackConfig.optimization,
|
||||
isProduction
|
||||
? {
|
||||
splitChunks: {
|
||||
// Cap the chunk size to 5MB.
|
||||
// react-scripts suggests a chunk size under 1MB after gzip, but we can only measure maxSize before gzip.
|
||||
// react-scripts also caps cacheable chunks at 5MB, which gzips to below 1MB, so we cap chunk size there.
|
||||
// See https://github.com/facebook/create-react-app/blob/d960b9e/packages/react-scripts/config/webpack.config.js#L713-L716.
|
||||
maxSize: 5 * 1024 * 1024,
|
||||
// Optimize over all chunks, instead of async chunks (the default), so that initial chunks are also optimized.
|
||||
chunks: 'all',
|
||||
},
|
||||
}
|
||||
: {}
|
||||
)
|
||||
|
||||
// Configure webpack caching:
|
||||
webpackConfig.cache = Object.assign(webpackConfig.cache, {
|
||||
cacheDirectory: getCacheDirectory('webpack'),
|
||||
})
|
||||
|
||||
// Ignore failed source mappings to avoid spamming the console.
|
||||
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
|
||||
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
|
||||
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
|
||||
webpackConfig.ignoreWarnings = [/Failed to parse source map/]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('mini-portfolio activity history', () => {
|
||||
afterEach(() => {
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'https://beta.api.uniswap.org/v1/graphql',
|
||||
},
|
||||
// Pass an empty object to allow the original behavior
|
||||
{}
|
||||
).as('restoreOriginalBehavior')
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat({ automine: false })
|
||||
beforeEach(() => {
|
||||
cy.hardhat()
|
||||
.then((hardhat) => hardhat.wallet.getTransactionCount())
|
||||
.then((currentNonce) => {
|
||||
const nextNonce = currentNonce + 1
|
||||
// Mock graphql response to include a specific nonce.
|
||||
.then((nonce) => {
|
||||
// Mock graphql response to include specific nonces.
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -43,7 +30,7 @@ describe('mini-portfolio activity history', () => {
|
||||
status: 'CONFIRMED',
|
||||
to: '0x034a40764485f7e08ca16366e503491a6af7850d',
|
||||
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
nonce: currentNonce,
|
||||
nonce,
|
||||
__typename: 'Transaction',
|
||||
},
|
||||
assetChanges: [],
|
||||
@@ -61,7 +48,7 @@ describe('mini-portfolio activity history', () => {
|
||||
status: 'CONFIRMED',
|
||||
to: '0x1b5154aa4b8f027b9fd19341132fc9dae10f7359',
|
||||
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
nonce: nextNonce,
|
||||
nonce: nonce + 1,
|
||||
__typename: 'Transaction',
|
||||
},
|
||||
assetChanges: [
|
||||
@@ -101,33 +88,37 @@ describe('mini-portfolio activity history', () => {
|
||||
},
|
||||
},
|
||||
}
|
||||
).as('graphqlMock')
|
||||
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Set slippage to a high value.
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('5')
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Click swap button.
|
||||
cy.contains('1 USDC = ').should('exist')
|
||||
cy.get('#swap-button').should('not.be', 'disabled').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
).as('graphql')
|
||||
})
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' }).hardhat({ automine: false })
|
||||
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('USDC').click()
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Set slippage to a high value.
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('5')
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Click swap button.
|
||||
cy.contains('1 USDC = ').should('exist')
|
||||
cy.get('#swap-button').should('not.be', 'disabled').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,9 +51,9 @@ describe('Permit2', () => {
|
||||
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
|
||||
.then((allowance) => {
|
||||
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 20 seconds.
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 20)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
describe('Swap on main page', () => {
|
||||
before(() => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
})
|
||||
|
||||
it('starts with ETH selected by default', () => {
|
||||
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
})
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('should render an error when a transaction fails due to a passed deadline', () => {
|
||||
const DEADLINE_MINUTES = 1
|
||||
const TEN_MINUTES_MS = 1000 * 60 * DEADLINE_MINUTES * 10
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat({ automine: false })
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.then((initialBalance) => {
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
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', '')
|
||||
|
||||
// Set deadline to minimum. (1 minute)
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('transaction-deadline-settings')).click()
|
||||
cy.get(getTestSelector('deadline-input')).clear().type(DEADLINE_MINUTES.toString())
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('deadline-input')).should('not.exist')
|
||||
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
|
||||
// Dismiss the modal that appears when a transaction is broadcast to the network.
|
||||
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
|
||||
|
||||
// The UI should show the transaction as pending.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
|
||||
// Mine a block past the deadline.
|
||||
cy.then(() => hardhat.mine(1, TEN_MINUTES_MS)).then(() => {
|
||||
// The UI should no longer show the transaction as pending.
|
||||
cy.contains('1 Pending').should('not.exist')
|
||||
|
||||
// Check that the user is informed of the failure
|
||||
cy.contains('Swap failed').should('exist')
|
||||
|
||||
// Check that the balance is unchanged in the UI
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance}`
|
||||
)
|
||||
|
||||
// Check that the balance is unchanged on chain
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should default inputs from URL params ', () => {
|
||||
cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'UNI')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
|
||||
cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
cy.visit('/swap')
|
||||
cy.get(`#swap-currency-output .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
||||
})
|
||||
|
||||
it('Opens and closes the settings menu', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('swap-settings-button')).click()
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('Auto Router API').should('exist')
|
||||
cy.get(getTestSelector('swap-settings-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
|
||||
it('inputs reset when navigating between pages', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.visit('/pool')
|
||||
cy.visit('/swap')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
})
|
||||
|
||||
it('can swap ETH for USDC', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
const TOKEN_ADDRESS = USDC_MAINNET.address
|
||||
const BALANCE_INCREMENT = 1
|
||||
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()
|
||||
|
||||
// ui check
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${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 be able to wrap ETH', () => {
|
||||
const BALANCE_INCREMENT = 1
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.then((initialWethBalance) => {
|
||||
// Select WETH for the token output.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('WETH').click()
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Click the wrap button.
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('have.descendants', ':contains("1 Pending")')
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be gone.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.have.descendants', ':contains("1 Pending")')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialWethBalance + BALANCE_INCREMENT}`
|
||||
)
|
||||
|
||||
// There should be a successful wrap notification.
|
||||
cy.contains('Wrapped').should('exist')
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.should('eq', initialWethBalance + BALANCE_INCREMENT)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to unwrap WETH', () => {
|
||||
const BALANCE_INCREMENT = 1
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET])).then(
|
||||
(initialBalance) => {
|
||||
// Select WETH for the token output.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('WETH').click()
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Click the wrap button.
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
// The user should see a notification telling them they successfully wrapped their ETH.
|
||||
cy.contains('Wrapped').should('exist')
|
||||
|
||||
// Switch to unwrapping the ETH we just wrapped.
|
||||
cy.get(getTestSelector('swap-currency-button')).click()
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
|
||||
// Click the Unwrap button.
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be gone.
|
||||
cy.contains('1 Pending').should('not.exist')
|
||||
// The user should see a notification telling them they successfully unwrapped their ETH.
|
||||
cy.contains('Unwrapped').should('exist')
|
||||
|
||||
// The UI balance should have decreased.
|
||||
cy.get('#swap-currency-input [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance.toFixed(0)}`
|
||||
)
|
||||
|
||||
// There should be a successful unwrap notification.
|
||||
cy.contains('Unwrapped').should('exist')
|
||||
|
||||
// The user's WETH account balance should not have changed from the initial balance
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => balance.toFixed(0))
|
||||
.should('eq', initialBalance.toFixed(0))
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should render and dismiss the wallet rejection modal', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).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')
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should render an error for slippage failure', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat({ automine: false })
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)).then((initialBalance) => {
|
||||
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
|
||||
const send = cy.stub(hardhat.provider, 'send').log(false)
|
||||
send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000))
|
||||
send.callThrough()
|
||||
|
||||
// Set slippage to a very low value.
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Open the currency select modal.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
|
||||
// Select UNI as output token
|
||||
cy.get(getTestSelector('token-search-input')).clear().type('Uniswap')
|
||||
cy.get(getTestSelector('currency-list-wrapper'))
|
||||
.contains(/^Uniswap$/)
|
||||
.first()
|
||||
// Our scrolling library (react-window) seems to freeze when acted on by cypress, with this element set to
|
||||
// `pointer-events: none`. This can be ignored using `{force: true}`.
|
||||
.click({ force: true })
|
||||
|
||||
// Swap 2 times.
|
||||
const AMOUNT_TO_SWAP = 400
|
||||
const NUMBER_OF_SWAPS = 2
|
||||
const INDIVIDUAL_SWAP_INPUT = AMOUNT_TO_SWAP / NUMBER_OF_SWAPS
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type(INDIVIDUAL_SWAP_INPUT.toString())
|
||||
cy.get('#swap-currency-output .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.get('#swap-currency-input .token-amount-input').clear().type(INDIVIDUAL_SWAP_INPUT.toString())
|
||||
cy.get('#swap-currency-output .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()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('Pending').should('exist')
|
||||
|
||||
cy.then(() => hardhat.mine()).then(() => {
|
||||
// The pending transaction indicator should not be visible.
|
||||
cy.contains('Pending').should('not.exist')
|
||||
|
||||
// Check for a failed transaction notification.
|
||||
cy.contains('Swap failed').should('exist')
|
||||
|
||||
// Assert that at least one of the swaps failed due to slippage.
|
||||
cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)).then((finalBalance) => {
|
||||
expect(finalBalance.gt(initialBalance.sub(parseEther(AMOUNT_TO_SWAP.toString())))).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
89
cypress/e2e/swap/errors.test.ts
Normal file
89
cypress/e2e/swap/errors.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
|
||||
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Swap', () => {
|
||||
it('should render and dismiss the wallet rejection modal', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).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')
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should render an error for slippage failure', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat({ automine: false })
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)).then((initialBalance) => {
|
||||
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
|
||||
const send = cy.stub(hardhat.provider, 'send').log(false)
|
||||
send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000))
|
||||
send.callThrough()
|
||||
|
||||
// Set slippage to a very low value.
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('max-slippage-settings')).click()
|
||||
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('slippage-input')).should('not.exist')
|
||||
|
||||
// Open the currency select modal.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
|
||||
// Select UNI as output token
|
||||
cy.get(getTestSelector('token-search-input')).clear().type('Uniswap')
|
||||
cy.get(getTestSelector('currency-list-wrapper'))
|
||||
.contains(/^Uniswap$/)
|
||||
.first()
|
||||
// Our scrolling library (react-window) seems to freeze when acted on by cypress, with this element set to
|
||||
// `pointer-events: none`. This can be ignored using `{force: true}`.
|
||||
.click({ force: true })
|
||||
|
||||
// Swap 2 times.
|
||||
const AMOUNT_TO_SWAP = 400
|
||||
const NUMBER_OF_SWAPS = 2
|
||||
const INDIVIDUAL_SWAP_INPUT = AMOUNT_TO_SWAP / NUMBER_OF_SWAPS
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type(INDIVIDUAL_SWAP_INPUT.toString())
|
||||
cy.get('#swap-currency-output .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.get('#swap-currency-input .token-amount-input').clear().type(INDIVIDUAL_SWAP_INPUT.toString())
|
||||
cy.get('#swap-currency-output .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()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('Pending').should('exist')
|
||||
|
||||
cy.then(() => hardhat.mine()).then(() => {
|
||||
// The pending transaction indicator should not be visible.
|
||||
cy.contains('Pending').should('not.exist')
|
||||
|
||||
// Check for a failed transaction notification.
|
||||
cy.contains('Swap failed').should('exist')
|
||||
|
||||
// Assert that at least one of the swaps failed due to slippage.
|
||||
cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)).then((finalBalance) => {
|
||||
expect(finalBalance.gt(initialBalance.sub(parseEther(AMOUNT_TO_SWAP.toString())))).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
168
cypress/e2e/swap/swap.test.ts
Normal file
168
cypress/e2e/swap/swap.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
describe('Swap on main page', () => {
|
||||
before(() => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
})
|
||||
|
||||
it('starts with ETH selected by default', () => {
|
||||
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
})
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('should render an error when a transaction fails due to a passed deadline', () => {
|
||||
const DEADLINE_MINUTES = 1
|
||||
const TEN_MINUTES_MS = 1000 * 60 * DEADLINE_MINUTES * 10
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat({ automine: false })
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.then((initialBalance) => {
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
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', '')
|
||||
|
||||
// Set deadline to minimum. (1 minute)
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('transaction-deadline-settings')).click()
|
||||
cy.get(getTestSelector('deadline-input')).clear().type(DEADLINE_MINUTES.toString())
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('deadline-input')).should('not.exist')
|
||||
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').click()
|
||||
|
||||
// Dismiss the modal that appears when a transaction is broadcast to the network.
|
||||
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
|
||||
|
||||
// The UI should show the transaction as pending.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
|
||||
// Mine a block past the deadline.
|
||||
cy.then(() => hardhat.mine(1, TEN_MINUTES_MS)).then(() => {
|
||||
// The UI should no longer show the transaction as pending.
|
||||
cy.contains('1 Pending').should('not.exist')
|
||||
|
||||
// Check that the user is informed of the failure
|
||||
cy.contains('Swap failed').should('exist')
|
||||
|
||||
// Check that the balance is unchanged in the UI
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance}`
|
||||
)
|
||||
|
||||
// Check that the balance is unchanged on chain
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should default inputs from URL params ', () => {
|
||||
cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'UNI')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
|
||||
|
||||
cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
cy.visit('/swap')
|
||||
cy.get(`#swap-currency-output .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
||||
})
|
||||
|
||||
it('Opens and closes the settings menu', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('Auto Router API').should('exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
|
||||
it('inputs reset when navigating between pages', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.visit('/pool')
|
||||
cy.visit('/swap')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
})
|
||||
|
||||
it('can swap ETH for USDC', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
const TOKEN_ADDRESS = USDC_MAINNET.address
|
||||
const BALANCE_INCREMENT = 1
|
||||
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()
|
||||
|
||||
// ui check
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
111
cypress/e2e/swap/wrap.test.ts
Normal file
111
cypress/e2e/swap/wrap.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Swap', () => {
|
||||
it('should be able to wrap ETH', () => {
|
||||
const BALANCE_INCREMENT = 1
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.then((initialWethBalance) => {
|
||||
// Select WETH for the token output.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('WETH').click()
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Click the wrap button.
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('have.descendants', ':contains("1 Pending")')
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be gone.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.have.descendants', ':contains("1 Pending")')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialWethBalance + BALANCE_INCREMENT}`
|
||||
)
|
||||
|
||||
// There should be a successful wrap notification.
|
||||
cy.contains('Wrapped').should('exist')
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.should('eq', initialWethBalance + BALANCE_INCREMENT)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to unwrap WETH', () => {
|
||||
const BALANCE_INCREMENT = 1
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET])).then(
|
||||
(initialBalance) => {
|
||||
// Select WETH for the token output.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('WETH').click()
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Click the wrap button.
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
// The user should see a notification telling them they successfully wrapped their ETH.
|
||||
cy.contains('Wrapped').should('exist')
|
||||
|
||||
// Switch to unwrapping the ETH we just wrapped.
|
||||
cy.get(getTestSelector('swap-currency-button')).click()
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
|
||||
// Click the Unwrap button.
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be gone.
|
||||
cy.contains('1 Pending').should('not.exist')
|
||||
// The user should see a notification telling them they successfully unwrapped their ETH.
|
||||
cy.contains('Unwrapped').should('exist')
|
||||
|
||||
// The UI balance should have decreased.
|
||||
cy.get('#swap-currency-input [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance.toFixed(0)}`
|
||||
)
|
||||
|
||||
// There should be a successful unwrap notification.
|
||||
cy.contains('Unwrapped').should('exist')
|
||||
|
||||
// The user's WETH account balance should not have changed from the initial balance
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => balance.toFixed(0))
|
||||
.should('eq', initialBalance.toFixed(0))
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
36
eslint_rules/enforce-retry-on-import.js
Normal file
36
eslint_rules/enforce-retry-on-import.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'enforce use of retry() for dynamic imports',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportExpression(node) {
|
||||
const grandParent = node.parent.parent
|
||||
if (
|
||||
!(
|
||||
grandParent &&
|
||||
grandParent.type === 'CallExpression' &&
|
||||
// Technically, we are only checking that a function named `retry` wraps the dynamic import.
|
||||
// We do not go as far as enforcing that it is import('utils/retry').retry
|
||||
grandParent.callee.name === 'retry' &&
|
||||
grandParent.arguments.length === 1 &&
|
||||
grandParent.arguments[0].type === 'ArrowFunctionExpression'
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -67,7 +67,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "6.4.3",
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
@@ -104,18 +104,22 @@
|
||||
"@vanilla-extract/jest-transform": "^1.1.1",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cypress": "10.3.1",
|
||||
"cypress-hardhat": "^2.3.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-plugin-import": "^2.27",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"hardhat": "^2.14.0",
|
||||
"jest-fail-on-console": "^3.1.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"react-scripts": "^4.0.3",
|
||||
"process": "^0.11.10",
|
||||
"react-scripts": "^5.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"serve": "^11.3.2",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
@@ -136,6 +140,7 @@
|
||||
"@graphql-codegen/typescript-operations": "^2.5.8",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^2.7.8",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lingui/core": "^3.14.0",
|
||||
"@lingui/macro": "^3.14.0",
|
||||
"@lingui/react": "^3.14.0",
|
||||
|
||||
@@ -82,7 +82,24 @@ const ActivityGroupWrapper = styled(Column)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array<Activity> {
|
||||
/* Detects transactions from same account with the same nonce and different hash */
|
||||
function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account: string): boolean {
|
||||
// handles locally cached tx's that were stored before we started tracking nonces
|
||||
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return false
|
||||
|
||||
return Object.values(remoteMap).some((remoteTx) => {
|
||||
if (!remoteTx) return false
|
||||
|
||||
// Cancellations are only possible when both nonce and tx.from are the same
|
||||
if (remoteTx.nonce === localActivity.nonce && remoteTx.receipt?.from.toLowerCase() === account.toLowerCase()) {
|
||||
// If the remote tx has a different hash than the local tx, the local tx was cancelled
|
||||
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}, account: string): Array<Activity> {
|
||||
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
|
||||
|
||||
// Merges local and remote activities w/ same hash, preferring remote data
|
||||
@@ -90,16 +107,12 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
const localActivity = (localMap?.[hash] ?? {}) as Activity
|
||||
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
|
||||
|
||||
// Check for nonce collision
|
||||
const isNonceCollision =
|
||||
localActivity.nonce !== undefined &&
|
||||
Object.keys(remoteMap).some((remoteHash) => remoteMap[remoteHash]?.nonce === localActivity.nonce)
|
||||
// TODO(WEB-2064): Display cancelled status in UI rather than completely hiding cancelled TXs
|
||||
if (wasTxCancelled(localActivity, remoteMap, account)) return acc
|
||||
|
||||
if (!isNonceCollision) {
|
||||
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
|
||||
// i.e.prefer remote exact swap output instead of local estimated output
|
||||
acc.push({ ...localActivity, ...remoteActivity } as Activity)
|
||||
}
|
||||
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
|
||||
// i.e.prefer remote exact swap output instead of local estimated output
|
||||
acc.push({ ...localActivity, ...remoteActivity } as Activity)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
@@ -132,9 +145,9 @@ export function ActivityTab({ account }: { account: string }) {
|
||||
|
||||
const activityGroups = useMemo(() => {
|
||||
const remoteMap = parseRemoteActivities(data?.portfolios?.[0].assetActivities)
|
||||
const allActivities = combineActivities(localMap, remoteMap)
|
||||
const allActivities = combineActivities(localMap, remoteMap, account)
|
||||
return createGroups(allActivities)
|
||||
}, [data?.portfolios, localMap])
|
||||
}, [data?.portfolios, localMap, account])
|
||||
|
||||
if (!data && loading)
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { AddressMap } from '@uniswap/smart-order-router'
|
||||
import { abi as MulticallABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
|
||||
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||
import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
|
||||
import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { MULTICALL_ADDRESS, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES } from 'constants/addresses'
|
||||
import { isSupportedChain, SupportedChainId } from 'constants/chains'
|
||||
@@ -43,11 +43,11 @@ function useContractMultichain<T extends BaseContract>(
|
||||
}
|
||||
|
||||
export function useV3ManagerContracts(chainIds: SupportedChainId[]): ContractMap<NonfungiblePositionManager> {
|
||||
return useContractMultichain<NonfungiblePositionManager>(V3NFT_ADDRESSES, NFTPositionManagerABI, chainIds)
|
||||
return useContractMultichain<NonfungiblePositionManager>(V3NFT_ADDRESSES, NFTPositionManagerJSON.abi, chainIds)
|
||||
}
|
||||
|
||||
export function useInterfaceMulticallContracts(chainIds: SupportedChainId[]): ContractMap<UniswapInterfaceMulticall> {
|
||||
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallABI, chainIds)
|
||||
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallJSON.abi, chainIds)
|
||||
}
|
||||
|
||||
type PriceMap = { [key: CurrencyKey]: number | undefined }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import IUniswapV3PoolStateJSON from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import { computePoolAddress, Pool, Position } from '@uniswap/v3-sdk'
|
||||
import { V3_CORE_FACTORY_ADDRESSES } from 'constants/addresses'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
@@ -118,7 +118,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT
|
||||
// Combines PositionDetails with Pool data to build our return type
|
||||
const fetchPositionInfo = useCallback(
|
||||
async (positionDetails: PositionDetails[], chainId: SupportedChainId, multicall: UniswapInterfaceMulticall) => {
|
||||
const poolInterface = new Interface(IUniswapV3PoolStateABI) as UniswapV3PoolInterface
|
||||
const poolInterface = new Interface(IUniswapV3PoolStateJSON.abi) as UniswapV3PoolInterface
|
||||
const tokens = await getTokens(
|
||||
positionDetails.flatMap((details) => [details.token0, details.token1]),
|
||||
chainId
|
||||
|
||||
@@ -296,7 +296,7 @@ export function ButtonConfirmed({
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProps) {
|
||||
export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButtonProps) {
|
||||
if (error) {
|
||||
return <ButtonErrorStyle {...rest} />
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
|
||||
type Gap = keyof DefaultTheme['grids']
|
||||
import styled from 'styled-components/macro'
|
||||
import { Gap } from 'theme'
|
||||
|
||||
export const Column = styled.div<{
|
||||
gap?: Gap
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box } from 'rebass/styled-components'
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
|
||||
type Gap = keyof DefaultTheme['grids']
|
||||
import styled from 'styled-components/macro'
|
||||
import { Gap } from 'theme'
|
||||
|
||||
// TODO(WEB-3289):
|
||||
// Setting `width: 100%` by default prevents composability in complex flex layouts.
|
||||
@@ -14,7 +13,7 @@ const Row = styled(Box)<{
|
||||
padding?: string
|
||||
border?: string
|
||||
borderRadius?: string
|
||||
gap?: string
|
||||
gap?: Gap | string
|
||||
}>`
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
display: flex;
|
||||
|
||||
36
src/components/Settings/MenuButton/index.test.tsx
Normal file
36
src/components/Settings/MenuButton/index.test.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import { SlippageTolerance } from 'state/user/types'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
import { lightTheme } from 'theme/colors'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import MenuButton from '.'
|
||||
|
||||
jest.mock('state/user/hooks')
|
||||
|
||||
const renderButton = () => {
|
||||
render(<MenuButton disabled={false} onClick={noop} isActive={false} />)
|
||||
}
|
||||
|
||||
describe('MenuButton', () => {
|
||||
it('should render an icon when slippage is Auto', () => {
|
||||
mocked(useUserSlippageTolerance).mockReturnValue([SlippageTolerance.Auto, noop])
|
||||
renderButton()
|
||||
expect(screen.queryByText('slippage')).not.toBeInTheDocument()
|
||||
})
|
||||
it('should render an icon with a custom slippage value', () => {
|
||||
mocked(useUserSlippageTolerance).mockReturnValue([new Percent(5, 10_000), noop])
|
||||
renderButton()
|
||||
expect(screen.queryByText('0.05% slippage')).toBeInTheDocument()
|
||||
})
|
||||
it('should render an icon with a custom slippage and a warning when value is out of bounds', () => {
|
||||
mocked(useUserSlippageTolerance).mockReturnValue([new Percent(1, 10_000), noop])
|
||||
renderButton()
|
||||
expect(screen.getByTestId('settings-icon-with-slippage')).toHaveStyleRule(
|
||||
'background-color',
|
||||
lightTheme.accentWarningSoft
|
||||
)
|
||||
})
|
||||
})
|
||||
91
src/components/Settings/MenuButton/index.tsx
Normal file
91
src/components/Settings/MenuButton/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import Row from 'components/Row'
|
||||
import { Settings } from 'react-feather'
|
||||
import { useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import { SlippageTolerance } from 'state/user/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import validateUserSlippageTolerance, { SlippageValidationResult } from 'utils/validateUserSlippageTolerance'
|
||||
|
||||
const Icon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.textSecondary};
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled.button<{ isActive: boolean }>`
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
:not([disabled]):hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
${({ isActive }) => isActive && `opacity: 0.7`}
|
||||
`
|
||||
|
||||
const IconContainer = styled(Row)`
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
`
|
||||
|
||||
const IconContainerWithSlippage = styled(IconContainer)<{ displayWarning?: boolean }>`
|
||||
div {
|
||||
color: ${({ theme, displayWarning }) => (displayWarning ? theme.accentWarning : theme.textSecondary)};
|
||||
}
|
||||
|
||||
background-color: ${({ theme, displayWarning }) =>
|
||||
displayWarning ? theme.accentWarningSoft : theme.backgroundModule};
|
||||
`
|
||||
|
||||
const ButtonContent = () => {
|
||||
const [userSlippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
if (userSlippageTolerance === SlippageTolerance.Auto) {
|
||||
return (
|
||||
<IconContainer>
|
||||
<Icon />
|
||||
</IconContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const isInvalidSlippage = validateUserSlippageTolerance(userSlippageTolerance) !== SlippageValidationResult.Valid
|
||||
|
||||
return (
|
||||
<IconContainerWithSlippage data-testid="settings-icon-with-slippage" gap="sm" displayWarning={isInvalidSlippage}>
|
||||
<ThemedText.Caption>
|
||||
<Trans>{userSlippageTolerance.toFixed(2)}% slippage</Trans>
|
||||
</ThemedText.Caption>
|
||||
<Icon />
|
||||
</IconContainerWithSlippage>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MenuButton({
|
||||
disabled,
|
||||
onClick,
|
||||
isActive,
|
||||
}: {
|
||||
disabled: boolean
|
||||
onClick: () => void
|
||||
isActive: boolean
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
isActive={isActive}
|
||||
id="open-settings-dialog-button"
|
||||
data-testid="open-settings-dialog-button"
|
||||
aria-label={t`Transaction Settings`}
|
||||
>
|
||||
<ButtonContent />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
@@ -7,82 +6,39 @@ import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { isSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
import { useRef } from 'react'
|
||||
import { Settings } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Divider } from 'theme'
|
||||
|
||||
import MaxSlippageSettings from './MaxSlippageSettings'
|
||||
import MenuButton from './MenuButton'
|
||||
import RouterPreferenceSettings from './RouterPreferenceSettings'
|
||||
import TransactionDeadlineSettings from './TransactionDeadlineSettings'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.textSecondary};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button<{ disabled: boolean }>`
|
||||
const Menu = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
height: 20px;
|
||||
${({ disabled }) =>
|
||||
!disabled &&
|
||||
`
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
const MenuFlyout = styled(AutoColumn)`
|
||||
min-width: 20.125rem;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 0rem;
|
||||
top: 100%;
|
||||
margin-top: 10px;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
min-width: 18.125rem;
|
||||
`};
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: ${({ theme }) => theme.backgroundOutline};
|
||||
gap: 16px;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
@@ -90,37 +46,29 @@ export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent })
|
||||
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
|
||||
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
const open = useModalIsOpen(ApplicationModal.SETTINGS)
|
||||
const isOpen = useModalIsOpen(ApplicationModal.SETTINGS)
|
||||
|
||||
const toggle = useToggleSettingsMenu()
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
const toggleMenu = useToggleSettingsMenu()
|
||||
useOnClickOutside(node, isOpen ? toggleMenu : undefined)
|
||||
|
||||
const isSupportedChain = isSupportedChainId(chainId)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton
|
||||
disabled={!isSupportedChainId(chainId)}
|
||||
onClick={toggle}
|
||||
id="open-settings-dialog-button"
|
||||
data-testid="open-settings-dialog-button"
|
||||
aria-label={t`Transaction Settings`}
|
||||
>
|
||||
<StyledMenuIcon data-testid="swap-settings-button" />
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<Menu ref={node}>
|
||||
<MenuButton disabled={!isSupportedChain} isActive={isOpen} onClick={toggleMenu} />
|
||||
{isOpen && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="16px" style={{ padding: '1rem' }}>
|
||||
{isSupportedChainId(chainId) && <RouterPreferenceSettings />}
|
||||
<Divider />
|
||||
<MaxSlippageSettings autoSlippage={autoSlippage} />
|
||||
{showDeadlineSettings && (
|
||||
<>
|
||||
<Divider />
|
||||
<TransactionDeadlineSettings />
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
<RouterPreferenceSettings />
|
||||
<Divider />
|
||||
<MaxSlippageSettings autoSlippage={autoSlippage} />
|
||||
{showDeadlineSettings && (
|
||||
<>
|
||||
<Divider />
|
||||
<TransactionDeadlineSettings />
|
||||
</>
|
||||
)}
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import { retry } from 'utils/retry'
|
||||
|
||||
const Bag = lazy(() => import('nft/components/bag/Bag'))
|
||||
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
|
||||
const AirdropModal = lazy(() => import('components/AirdropModal'))
|
||||
const Bag = lazy(() => retry(() => import('nft/components/bag/Bag')))
|
||||
const TransactionCompleteModal = lazy(() => retry(() => import('nft/components/collection/TransactionCompleteModal')))
|
||||
const AirdropModal = lazy(() => retry(() => import('components/AirdropModal')))
|
||||
|
||||
export default function TopLevelModals() {
|
||||
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TransactionSummary } from '../AccountDetails/TransactionSummary'
|
||||
import { ButtonLight, ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import AnimatedConfirmation from './AnimatedConfirmation'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@@ -28,16 +28,12 @@ const Wrapper = styled.div`
|
||||
border-radius: 20px;
|
||||
outline: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`
|
||||
const Section = styled(AutoColumn)<{ inline?: boolean }>`
|
||||
padding: ${({ inline }) => (inline ? '0' : '0')};
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const BottomSection = styled(Section)`
|
||||
const BottomSection = styled(AutoColumn)`
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
padding-bottom: 10px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)<{ inline?: boolean }>`
|
||||
@@ -50,6 +46,10 @@ const StyledLogo = styled.img`
|
||||
margin-left: 6px;
|
||||
`
|
||||
|
||||
const ConfirmationModalContentWrapper = styled(AutoColumn)`
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
function ConfirmationPendingContent({
|
||||
onDismiss,
|
||||
pendingText,
|
||||
@@ -59,8 +59,6 @@ function ConfirmationPendingContent({
|
||||
pendingText: ReactNode
|
||||
inline?: boolean // not in modal
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<AutoColumn gap="md">
|
||||
@@ -74,15 +72,15 @@ function ConfirmationPendingContent({
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={inline ? '40px' : '90px'} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<Text fontWeight={500} fontSize={20} color={theme.textPrimary} textAlign="center">
|
||||
<ThemedText.SubHeaderLarge color="textPrimary" textAlign="center">
|
||||
<Trans>Waiting for confirmation</Trans>
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={16} color={theme.textPrimary} textAlign="center">
|
||||
</ThemedText.SubHeaderLarge>
|
||||
<ThemedText.SubHeader color="textPrimary" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
<Text fontWeight={400} fontSize={12} color={theme.textSecondary} textAlign="center" marginBottom="12px">
|
||||
</ThemedText.SubHeader>
|
||||
<ThemedText.SubHeaderSmall color="textSecondary" textAlign="center" marginBottom="12px">
|
||||
<Trans>Confirm this transaction in your wallet</Trans>
|
||||
</Text>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
@@ -125,7 +123,7 @@ function TransactionSubmittedContent({
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section inline={inline}>
|
||||
<AutoColumn>
|
||||
{!inline && (
|
||||
<RowBetween>
|
||||
<div />
|
||||
@@ -135,7 +133,7 @@ function TransactionSubmittedContent({
|
||||
<ConfirmedIcon inline={inline}>
|
||||
<ArrowUpCircle strokeWidth={1} size={inline ? '40px' : '75px'} color={theme.accentActive} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="md" justify="center" style={{ paddingBottom: '12px' }}>
|
||||
<ConfirmationModalContentWrapper gap="md" justify="center">
|
||||
<ThemedText.MediumHeader textAlign="center">
|
||||
<Trans>Transaction submitted</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
@@ -154,19 +152,19 @@ function TransactionSubmittedContent({
|
||||
</ButtonLight>
|
||||
)}
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }} data-testid="dismiss-tx-confirmation">
|
||||
<Text fontWeight={600} fontSize={20} color={theme.accentTextLightPrimary}>
|
||||
<ThemedText.HeadlineSmall color={theme.accentTextLightPrimary}>
|
||||
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
|
||||
</Text>
|
||||
</ThemedText.HeadlineSmall>
|
||||
</ButtonPrimary>
|
||||
{chainId && hash && (
|
||||
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
|
||||
<Text fontWeight={600} fontSize={14} color={theme.accentAction}>
|
||||
<ThemedText.Link color={theme.accentAction}>
|
||||
<Trans>View on {chainId === SupportedChainId.MAINNET ? 'Etherscan' : 'Block Explorer'}</Trans>
|
||||
</Text>
|
||||
</ThemedText.Link>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</ConfirmationModalContentWrapper>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -184,15 +182,15 @@ export function ConfirmationModalContent({
|
||||
}) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{title}
|
||||
</Text>
|
||||
<AutoColumn gap="sm">
|
||||
<Row>
|
||||
<Row justify="center" marginLeft="24px">
|
||||
<ThemedText.SubHeader>{title}</ThemedText.SubHeader>
|
||||
</Row>
|
||||
<CloseIcon onClick={onDismiss} data-cy="confirmation-close-icon" />
|
||||
</RowBetween>
|
||||
</Row>
|
||||
{topContent()}
|
||||
</Section>
|
||||
</AutoColumn>
|
||||
{bottomContent && <BottomSection gap="12px">{bottomContent()}</BottomSection>}
|
||||
</Wrapper>
|
||||
)
|
||||
@@ -202,7 +200,7 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<AutoColumn>
|
||||
<RowBetween>
|
||||
<Text fontWeight={600} fontSize={16}>
|
||||
<Trans>Error</Trans>
|
||||
@@ -213,7 +211,7 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React
|
||||
<AlertTriangle color={theme.accentCritical} style={{ strokeWidth: 1 }} size={90} />
|
||||
<ThemedText.MediumHeader textAlign="center">{message}</ThemedText.MediumHeader>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</AutoColumn>
|
||||
<BottomSection gap="12px">
|
||||
<ButtonPrimary onClick={onDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
@@ -252,7 +250,7 @@ function L2Content({
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section inline={inline}>
|
||||
<AutoColumn>
|
||||
{!inline && (
|
||||
<RowBetween mb="16px">
|
||||
<Badge>
|
||||
@@ -277,7 +275,7 @@ function L2Content({
|
||||
)}
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<Text fontWeight={500} fontSize={20} textAlign="center">
|
||||
<ThemedText.SubHeaderLarge textAlign="center">
|
||||
{!hash ? (
|
||||
<Trans>Confirm transaction in wallet</Trans>
|
||||
) : !confirmed ? (
|
||||
@@ -287,20 +285,20 @@ function L2Content({
|
||||
) : (
|
||||
<Trans>Error</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text fontWeight={400} fontSize={16} textAlign="center">
|
||||
</ThemedText.SubHeaderLarge>
|
||||
<ThemedText.BodySecondary textAlign="center">
|
||||
{transaction ? <TransactionSummary info={transaction.info} /> : pendingText}
|
||||
</Text>
|
||||
</ThemedText.BodySecondary>
|
||||
{chainId && hash ? (
|
||||
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.accentAction}>
|
||||
<ThemedText.SubHeaderSmall color={theme.accentAction}>
|
||||
<Trans>View on Explorer</Trans>
|
||||
</Text>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<div style={{ height: '17px' }} />
|
||||
)}
|
||||
<Text color={theme.textTertiary} style={{ margin: '20px 0 0 0' }} fontSize="14px">
|
||||
<ThemedText.SubHeaderSmall color={theme.textTertiary} marginTop="20px">
|
||||
{!secondsToConfirm ? (
|
||||
<div style={{ height: '24px' }} />
|
||||
) : (
|
||||
@@ -311,14 +309,14 @@ function L2Content({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Text>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '4px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
<ThemedText.SubHeaderLarge>
|
||||
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
|
||||
</Text>
|
||||
</ThemedText.SubHeaderLarge>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
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 { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
|
||||
import { act, render, screen } from 'test-utils/render'
|
||||
|
||||
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
@@ -27,9 +21,9 @@ describe('AdvancedSwapDetails.tsx', () => {
|
||||
})
|
||||
|
||||
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)
|
||||
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = '1.00'
|
||||
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
|
||||
await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
|
||||
await act(() => userEvent.hover(screen.getByText(/Maximum input/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()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
@@ -16,7 +16,7 @@ import RouterLabel from './RouterLabel'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
interface AdvancedSwapDetailsProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade
|
||||
allowedSlippage: Percent
|
||||
syncing?: boolean
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD}</ThemedText.BodySmall>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
)}
|
||||
@@ -75,7 +75,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
}
|
||||
>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Minimum output</Trans>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum output</Trans> : <Trans>Maximum input</Trans>}
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName } from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { sendAnalyticsEvent, Trace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { formatSwapPriceUpdatedEventProperties } from 'utils/loggingFormatters'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import TransactionConfirmationModal, {
|
||||
@@ -21,21 +22,17 @@ export default function ConfirmSwapModal({
|
||||
allowedSlippage,
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
recipient,
|
||||
swapErrorMessage,
|
||||
isOpen,
|
||||
attemptingTxn,
|
||||
txHash,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
originalTrade: Trade<Currency, Currency, TradeType> | undefined
|
||||
trade: InterfaceTrade
|
||||
originalTrade: InterfaceTrade | undefined
|
||||
attemptingTxn: boolean
|
||||
txHash: string | undefined
|
||||
recipient: string | null
|
||||
allowedSlippage: Percent
|
||||
onAcceptChanges: () => void
|
||||
onConfirm: () => void
|
||||
@@ -45,35 +42,34 @@ export default function ConfirmSwapModal({
|
||||
fiatValueInput: { data?: number; isLoading: boolean }
|
||||
fiatValueOutput: { data?: number; isLoading: boolean }
|
||||
}) {
|
||||
// shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed
|
||||
// and an event triggered by modal closing should be logged.
|
||||
const [shouldLogModalCloseEvent, setShouldLogModalCloseEvent] = useState(false)
|
||||
const showAcceptChanges = useMemo(
|
||||
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
() => Boolean(originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
[originalTrade, trade]
|
||||
)
|
||||
|
||||
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade?.executionPrice)
|
||||
const [priceUpdate, setPriceUpdate] = useState<number>()
|
||||
useEffect(() => {
|
||||
if (lastExecutionPrice && !trade.executionPrice.equalTo(lastExecutionPrice)) {
|
||||
setPriceUpdate(getPriceUpdateBasisPoints(lastExecutionPrice, trade.executionPrice))
|
||||
setLastExecutionPrice(trade.executionPrice)
|
||||
}
|
||||
}, [lastExecutionPrice, setLastExecutionPrice, trade])
|
||||
|
||||
const onModalDismiss = useCallback(() => {
|
||||
if (isOpen) setShouldLogModalCloseEvent(true)
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED,
|
||||
formatSwapPriceUpdatedEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED)
|
||||
)
|
||||
onDismiss()
|
||||
}, [isOpen, onDismiss])
|
||||
}, [onDismiss, priceUpdate, trade])
|
||||
|
||||
const modalHeader = useCallback(() => {
|
||||
return trade ? (
|
||||
<SwapModalHeader
|
||||
trade={trade}
|
||||
shouldLogModalCloseEvent={shouldLogModalCloseEvent}
|
||||
setShouldLogModalCloseEvent={setShouldLogModalCloseEvent}
|
||||
allowedSlippage={allowedSlippage}
|
||||
recipient={recipient}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
onAcceptChanges={onAcceptChanges}
|
||||
/>
|
||||
) : null
|
||||
}, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade, shouldLogModalCloseEvent])
|
||||
return <SwapModalHeader trade={trade} allowedSlippage={allowedSlippage} />
|
||||
}, [allowedSlippage, trade])
|
||||
|
||||
const modalBottom = useCallback(() => {
|
||||
return trade ? (
|
||||
return (
|
||||
<SwapModalFooter
|
||||
onConfirm={onConfirm}
|
||||
trade={trade}
|
||||
@@ -84,25 +80,28 @@ export default function ConfirmSwapModal({
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueInput}
|
||||
fiatValueOutput={fiatValueOutput}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
onAcceptChanges={onAcceptChanges}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
}, [
|
||||
trade,
|
||||
onConfirm,
|
||||
txHash,
|
||||
allowedSlippage,
|
||||
showAcceptChanges,
|
||||
swapErrorMessage,
|
||||
trade,
|
||||
allowedSlippage,
|
||||
txHash,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
onAcceptChanges,
|
||||
])
|
||||
|
||||
// text to show while loading
|
||||
const pendingText = (
|
||||
<Trans>
|
||||
Swapping {trade?.inputAmount?.toSignificant(6)} {trade?.inputAmount?.currency?.symbol} for{' '}
|
||||
{trade?.outputAmount?.toSignificant(6)} {trade?.outputAmount?.currency?.symbol}
|
||||
Swapping {trade.inputAmount.toSignificant(6)} {trade.inputAmount.currency?.symbol} for{' '}
|
||||
{trade.outputAmount.toSignificant(6)} {trade.outputAmount.currency?.symbol}
|
||||
</Trans>
|
||||
)
|
||||
|
||||
@@ -112,7 +111,7 @@ export default function ConfirmSwapModal({
|
||||
<TransactionErrorContent onDismiss={onModalDismiss} message={swapErrorMessage} />
|
||||
) : (
|
||||
<ConfirmationModalContent
|
||||
title={<Trans>Confirm Swap</Trans>}
|
||||
title={<Trans>Review Swap</Trans>}
|
||||
onDismiss={onModalDismiss}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
@@ -124,13 +123,13 @@ export default function ConfirmSwapModal({
|
||||
return (
|
||||
<Trace modal={InterfaceModalName.CONFIRM_SWAP}>
|
||||
<TransactionConfirmationModal
|
||||
isOpen={isOpen}
|
||||
isOpen
|
||||
onDismiss={onModalDismiss}
|
||||
attemptingTxn={attemptingTxn}
|
||||
hash={txHash}
|
||||
content={confirmationContent}
|
||||
pendingText={pendingText}
|
||||
currencyToAdd={trade?.outputAmount.currency}
|
||||
currencyToAdd={trade.outputAmount.currency}
|
||||
/>
|
||||
</Trace>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
|
||||
@@ -26,14 +25,14 @@ export default function GasEstimateTooltip({
|
||||
loading,
|
||||
disabled,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> // dollar amount in active chain's stablecoin
|
||||
trade: InterfaceTrade // dollar amount in active chain's stablecoin
|
||||
loading: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const formattedGasPriceString = trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? trade.gasUseEstimateUSD === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: '$' + trade.gasUseEstimateUSD
|
||||
: undefined
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TOKEN_1, TEST_TRADE_EXACT_INPUT, toCurrencyAmount } from 'test-utils/constants'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
|
||||
import { act, render, screen } from 'test-utils/render'
|
||||
|
||||
import SwapDetailsDropdown from './SwapDetailsDropdown'
|
||||
@@ -25,7 +25,7 @@ describe('SwapDetailsDropdown.tsx', () => {
|
||||
})
|
||||
|
||||
it('is interactive once loaded', async () => {
|
||||
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1)
|
||||
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = '1.00'
|
||||
render(
|
||||
<SwapDetailsDropdown
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import Column from 'components/Column'
|
||||
@@ -92,7 +92,7 @@ const Wrapper = styled(Column)`
|
||||
`
|
||||
|
||||
interface SwapDetailsInlineProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade: InterfaceTrade | undefined
|
||||
syncing: boolean
|
||||
loading: boolean
|
||||
allowedSlippage: Percent
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
import SwapBuyFiatButton from './SwapBuyFiatButton'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
const StyledSwapHeader = styled(RowBetween)`
|
||||
margin-bottom: 10px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
const HeaderButtonContainer = styled(RowFixed)`
|
||||
padding: 0 12px;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
@@ -30,17 +23,15 @@ export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent })
|
||||
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<RowFixed style={{ gap: '8px' }}>
|
||||
<TextHeader className={subhead}>
|
||||
<Trans>Swap</Trans>
|
||||
</TextHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<HeaderButtonContainer>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</HeaderButtonContainer>
|
||||
<RowFixed>
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
</RowFixed>
|
||||
</StyledSwapHeader>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,103 @@
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
|
||||
import { render, screen, within } 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', () => {
|
||||
it('matches base snapshot, test trade exact input', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalFooter
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
hash={undefined}
|
||||
onConfirm={() => null}
|
||||
disabledConfirm
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
onConfirm={jest.fn()}
|
||||
swapErrorMessage={undefined}
|
||||
disabledConfirm={false}
|
||||
swapQuoteReceivedDate={undefined}
|
||||
fiatValueInput={fiatValue}
|
||||
fiatValueOutput={fiatValue}
|
||||
fiatValueInput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
fiatValueOutput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
showAcceptChanges={false}
|
||||
onAcceptChanges={jest.fn()}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByTestId('confirm-swap-button')).toBeDisabled()
|
||||
|
||||
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 fee paid to miners who process your transaction. This must be paid in $ETH.')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows accept changes section when available', () => {
|
||||
const mockAcceptChanges = jest.fn()
|
||||
render(
|
||||
<SwapModalFooter
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
hash={undefined}
|
||||
onConfirm={jest.fn()}
|
||||
swapErrorMessage={undefined}
|
||||
disabledConfirm={false}
|
||||
swapQuoteReceivedDate={undefined}
|
||||
fiatValueInput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
fiatValueOutput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
showAcceptChanges={true}
|
||||
onAcceptChanges={mockAcceptChanges}
|
||||
/>
|
||||
)
|
||||
const showAcceptChanges = screen.getByTestId('show-accept-changes')
|
||||
expect(showAcceptChanges).toBeInTheDocument()
|
||||
expect(within(showAcceptChanges).getByText('Price updated')).toBeVisible()
|
||||
expect(within(showAcceptChanges).getByText('Accept')).toBeVisible()
|
||||
})
|
||||
|
||||
it('test trade exact output, no recipient', () => {
|
||||
render(
|
||||
<SwapModalFooter
|
||||
trade={TEST_TRADE_EXACT_OUTPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
hash={undefined}
|
||||
onConfirm={jest.fn()}
|
||||
swapErrorMessage={undefined}
|
||||
disabledConfirm={false}
|
||||
swapQuoteReceivedDate={undefined}
|
||||
fiatValueInput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
fiatValueOutput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
showAcceptChanges={true}
|
||||
onAcceptChanges={jest.fn()}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
screen.getByText(
|
||||
'The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will revert.'
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,105 +1,48 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { formatPriceImpact } from '@uniswap/conedison/format'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatPercentNumber,
|
||||
formatToDecimal,
|
||||
getDurationFromDateMilliseconds,
|
||||
getDurationUntilTimestampSeconds,
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { ReactNode } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import getRoutingDiagramEntries, { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers'
|
||||
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
|
||||
import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoRow } from '../Row'
|
||||
import { SwapCallbackError } from './styleds'
|
||||
import { ButtonError, SmallButtonPrimary } from '../Button'
|
||||
import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import { SwapCallbackError, SwapShowAcceptChanges } from './styleds'
|
||||
import { Label } from './SwapModalHeaderAmount'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
transactionDeadlineSecondsSinceEpoch: number | undefined
|
||||
isAutoSlippage: boolean
|
||||
isAutoRouterApi: boolean
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
routes: RoutingDiagramEntry[]
|
||||
fiatValueInput?: number
|
||||
fiatValueOutput?: number
|
||||
}
|
||||
const DetailsContainer = styled(Column)`
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => {
|
||||
const routesEventProperties: Record<string, any[]> = {
|
||||
routes_percentages: [],
|
||||
routes_protocols: [],
|
||||
}
|
||||
const StyledAlertTriangle = styled(AlertTriangle)`
|
||||
margin-right: 8px;
|
||||
min-width: 24px;
|
||||
`
|
||||
|
||||
routes.forEach((route, index) => {
|
||||
routesEventProperties['routes_percentages'].push(formatPercentNumber(route.percent))
|
||||
routesEventProperties['routes_protocols'].push(route.protocol)
|
||||
routesEventProperties[`route_${index}_input_currency_symbols`] = route.path.map(
|
||||
(pathStep) => pathStep[0].symbol ?? ''
|
||||
)
|
||||
routesEventProperties[`route_${index}_output_currency_symbols`] = route.path.map(
|
||||
(pathStep) => pathStep[1].symbol ?? ''
|
||||
)
|
||||
routesEventProperties[`route_${index}_input_currency_addresses`] = route.path.map((pathStep) =>
|
||||
getTokenAddress(pathStep[0])
|
||||
)
|
||||
routesEventProperties[`route_${index}_output_currency_addresses`] = route.path.map((pathStep) =>
|
||||
getTokenAddress(pathStep[1])
|
||||
)
|
||||
routesEventProperties[`route_${index}_fee_amounts_hundredths_of_bps`] = route.path.map((pathStep) => pathStep[2])
|
||||
})
|
||||
const ConfirmButton = styled(ButtonError)`
|
||||
height: 56px;
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
return routesEventProperties
|
||||
}
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
|
||||
transaction_hash: hash,
|
||||
transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch),
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
token_in_amount_usd: fiatValueInput,
|
||||
token_out_amount_usd: fiatValueOutput,
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
is_auto_router_api: isAutoRouterApi,
|
||||
is_auto_slippage: isAutoSlippage,
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate
|
||||
? getDurationFromDateMilliseconds(swapQuoteReceivedDate)
|
||||
: undefined,
|
||||
swap_quote_block_number: trade.blockNumber,
|
||||
...formatRoutesEventProperties(routes),
|
||||
})
|
||||
const DetailRowValue = styled(ThemedText.BodySmall)`
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
`
|
||||
|
||||
export default function SwapModalFooter({
|
||||
trade,
|
||||
@@ -111,8 +54,10 @@ export default function SwapModalFooter({
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
onConfirm: () => void
|
||||
@@ -121,46 +66,142 @@ export default function SwapModalFooter({
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
fiatValueInput: { data?: number; isLoading: boolean }
|
||||
fiatValueOutput: { data?: number; isLoading: boolean }
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
|
||||
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
|
||||
const [routerPreference] = useRouterPreference()
|
||||
const routes = getRoutingDiagramEntries(trade)
|
||||
const theme = useTheme()
|
||||
const { chainId } = useWeb3React()
|
||||
const nativeCurrency = useNativeCurrency(chainId)
|
||||
|
||||
const label = `${trade.executionPrice.baseCurrency?.symbol} `
|
||||
const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}`
|
||||
const formattedPrice = formatTransactionAmount(priceToPreciseFloat(trade.executionPrice))
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoRow>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
element={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED}
|
||||
properties={formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput: fiatValueInput.data,
|
||||
fiatValueOutput: fiatValueOutput.data,
|
||||
})}
|
||||
>
|
||||
<ButtonError
|
||||
data-testid="confirm-swap-button"
|
||||
onClick={onConfirm}
|
||||
disabled={disabledConfirm}
|
||||
style={{ margin: '10px 0 0 0' }}
|
||||
id={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
<DetailsContainer gap="md">
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<Label>
|
||||
<Trans>Exchange rate</Trans>
|
||||
</Label>
|
||||
<DetailRowValue>{`1 ${labelInverted} = ${formattedPrice ?? '-'} ${label}`}</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The fee paid to miners who process your transaction. This must be paid in ${nativeCurrency.symbol}.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<Label cursor="help">
|
||||
<Trans>Network fee</Trans>
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue>{trade.gasUseEstimateUSD ? `~$${trade.gasUseEstimateUSD}` : '-'}</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
|
||||
<Label cursor="help">
|
||||
<Trans>Price impact</Trans>
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue color={getPriceImpactWarning(trade.priceImpact)}>
|
||||
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
|
||||
</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>
|
||||
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
|
||||
will revert.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
The maximum amount you are guaranteed to spend. If the price slips any further, your transaction
|
||||
will revert.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Label cursor="help">
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>Minimum received</Trans>
|
||||
) : (
|
||||
<Trans>Maximum sent</Trans>
|
||||
)}
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
|
||||
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
|
||||
</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
</DetailsContainer>
|
||||
{showAcceptChanges ? (
|
||||
<SwapShowAcceptChanges data-testid="show-accept-changes">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<StyledAlertTriangle size={20} />
|
||||
<ThemedText.DeprecatedMain color={theme.accentAction}>
|
||||
<Trans>Price updated</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</RowFixed>
|
||||
<SmallButtonPrimary onClick={onAcceptChanges}>
|
||||
<Trans>Accept</Trans>
|
||||
</SmallButtonPrimary>
|
||||
</RowBetween>
|
||||
</SwapShowAcceptChanges>
|
||||
) : (
|
||||
<AutoRow>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
element={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED}
|
||||
properties={formatSwapButtonClickEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput: fiatValueInput.data,
|
||||
fiatValueOutput: fiatValueOutput.data,
|
||||
})}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
<Trans>Confirm Swap</Trans>
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</TraceEvent>
|
||||
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</AutoRow>
|
||||
<ConfirmButton
|
||||
data-testid="confirm-swap-button"
|
||||
onClick={onConfirm}
|
||||
disabled={disabledConfirm}
|
||||
$borderRadius="12px"
|
||||
id={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
>
|
||||
<ThemedText.HeadlineSmall color="accentTextLightPrimary">
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
</ConfirmButton>
|
||||
</TraceEvent>
|
||||
|
||||
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</AutoRow>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,83 +1,44 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import {
|
||||
TEST_ALLOWED_SLIPPAGE,
|
||||
TEST_RECIPIENT_ADDRESS,
|
||||
TEST_TRADE_EXACT_INPUT,
|
||||
TEST_TRADE_EXACT_OUTPUT,
|
||||
} from 'test-utils/constants'
|
||||
import { render, screen, within } from 'test-utils/render'
|
||||
import noop from 'utils/noop'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
jest.mock('@uniswap/analytics')
|
||||
const mockSendAnalyticsEvent = sendAnalyticsEvent as jest.MockedFunction<typeof sendAnalyticsEvent>
|
||||
|
||||
describe('SwapModalHeader.tsx', () => {
|
||||
let sendAnalyticsEventMock: jest.Mock<any, any>
|
||||
|
||||
beforeAll(() => {
|
||||
sendAnalyticsEventMock = jest.fn()
|
||||
})
|
||||
|
||||
it('matches base snapshot for test trade with exact input', () => {
|
||||
it('matches base snapshot, test trade exact input', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalHeader
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
shouldLogModalCloseEvent={false}
|
||||
showAcceptChanges={false}
|
||||
setShouldLogModalCloseEvent={noop}
|
||||
onAcceptChanges={noop}
|
||||
recipient={TEST_RECIPIENT_ADDRESS}
|
||||
/>
|
||||
<SwapModalHeader trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_INPUT.inputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_INPUT.inputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_INPUT.outputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_INPUT.outputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
it('shows accept changes section and logs amplitude event', () => {
|
||||
const setShouldLogModalCloseEventFn = jest.fn()
|
||||
mockSendAnalyticsEvent.mockImplementation(sendAnalyticsEventMock)
|
||||
render(
|
||||
<SwapModalHeader
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
shouldLogModalCloseEvent
|
||||
showAcceptChanges
|
||||
setShouldLogModalCloseEvent={setShouldLogModalCloseEventFn}
|
||||
onAcceptChanges={noop}
|
||||
recipient={TEST_RECIPIENT_ADDRESS}
|
||||
/>
|
||||
it('test trade exact output, no recipient', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalHeader trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
|
||||
)
|
||||
expect(setShouldLogModalCloseEventFn).toHaveBeenCalledWith(false)
|
||||
const showAcceptChanges = screen.getByTestId('show-accept-changes')
|
||||
expect(showAcceptChanges).toBeInTheDocument()
|
||||
expect(within(showAcceptChanges).getByText('Price Updated')).toBeVisible()
|
||||
expect(within(showAcceptChanges).getByText('Accept')).toBeVisible()
|
||||
expect(sendAnalyticsEventMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders correctly for test trade with exact output and no recipient', () => {
|
||||
const rendered = render(
|
||||
<SwapModalHeader
|
||||
trade={TEST_TRADE_EXACT_OUTPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
shouldLogModalCloseEvent={false}
|
||||
showAcceptChanges={false}
|
||||
setShouldLogModalCloseEvent={noop}
|
||||
onAcceptChanges={noop}
|
||||
recipient={null}
|
||||
/>
|
||||
)
|
||||
expect(rendered.queryByTestId('recipient-info')).toBeNull()
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText(/Input is estimated. You will sell at most/i)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-symbol')).toHaveTextContent(
|
||||
TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? ''
|
||||
|
||||
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_OUTPUT.inputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
expect(screen.getByTestId('output-symbol')).toHaveTextContent(
|
||||
TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? ''
|
||||
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_OUTPUT.outputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
expect(screen.getByTestId('input-amount')).toHaveTextContent(TEST_TRADE_EXACT_OUTPUT.inputAmount.toExact())
|
||||
expect(screen.getByTestId('output-amount')).toHaveTextContent(TEST_TRADE_EXACT_OUTPUT.outputAmount.toExact())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,216 +1,72 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import Column, { AutoColumn } from 'components/Column'
|
||||
import { useUSDPrice } from 'hooks/useUSDPrice'
|
||||
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle, ArrowDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { Field } from 'state/swap/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Divider, ThemedText } from 'theme'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { isAddress, shortenAddress } from '../../utils'
|
||||
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { LightCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { FiatValue } from '../CurrencyInputPanel/FiatValue'
|
||||
import CurrencyLogo from '../Logo/CurrencyLogo'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import TradePrice from '../swap/TradePrice'
|
||||
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
import { SwapShowAcceptChanges, TruncatedText } from './styleds'
|
||||
import { SwapModalHeaderAmount } from './SwapModalHeaderAmount'
|
||||
|
||||
const ArrowWrapper = styled.div`
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
left: calc(50% - 16px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 4px solid;
|
||||
border-color: ${({ theme }) => theme.backgroundModule};
|
||||
z-index: 2;
|
||||
const Rule = styled(Divider)`
|
||||
margin: 16px 2px 24px 2px;
|
||||
`
|
||||
|
||||
const formatAnalyticsEventProperties = (
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>,
|
||||
priceUpdate: number | undefined,
|
||||
response: SwapPriceUpdateUserResponse
|
||||
) => ({
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
response,
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
price_update_basis_points: priceUpdate,
|
||||
})
|
||||
const HeaderContainer = styled(AutoColumn)`
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
export default function SwapModalHeader({
|
||||
trade,
|
||||
shouldLogModalCloseEvent,
|
||||
setShouldLogModalCloseEvent,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
shouldLogModalCloseEvent: boolean
|
||||
setShouldLogModalCloseEvent: (shouldLog: boolean) => void
|
||||
trade: InterfaceTrade
|
||||
allowedSlippage: Percent
|
||||
recipient: string | null
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade.executionPrice)
|
||||
const [priceUpdate, setPriceUpdate] = useState<number | undefined>()
|
||||
|
||||
const fiatValueInput = useUSDPrice(trade.inputAmount)
|
||||
const fiatValueOutput = useUSDPrice(trade.outputAmount)
|
||||
|
||||
useEffect(() => {
|
||||
if (!trade.executionPrice.equalTo(lastExecutionPrice)) {
|
||||
setPriceUpdate(getPriceUpdateBasisPoints(lastExecutionPrice, trade.executionPrice))
|
||||
setLastExecutionPrice(trade.executionPrice)
|
||||
}
|
||||
}, [lastExecutionPrice, setLastExecutionPrice, trade.executionPrice])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldLogModalCloseEvent && showAcceptChanges) {
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED,
|
||||
formatAnalyticsEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED)
|
||||
)
|
||||
}
|
||||
setShouldLogModalCloseEvent(false)
|
||||
}, [shouldLogModalCloseEvent, showAcceptChanges, setShouldLogModalCloseEvent, trade, priceUpdate])
|
||||
|
||||
return (
|
||||
<AutoColumn gap="4px" style={{ marginTop: '1rem' }}>
|
||||
<LightCard padding="0.75rem 1rem">
|
||||
<AutoColumn gap="sm">
|
||||
<RowBetween align="center">
|
||||
<RowFixed gap="0px">
|
||||
<TruncatedText
|
||||
fontSize={24}
|
||||
fontWeight={500}
|
||||
color={showAcceptChanges && trade.tradeType === TradeType.EXACT_OUTPUT ? theme.accentAction : ''}
|
||||
data-testid="input-amount"
|
||||
>
|
||||
{trade.inputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap="0px">
|
||||
<CurrencyLogo currency={trade.inputAmount.currency} size="20px" style={{ marginRight: '12px' }} />
|
||||
<Text fontSize={20} fontWeight={500} data-testid="input-symbol">
|
||||
{trade.inputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<FiatValue fiatValue={fiatValueInput} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
<ArrowWrapper>
|
||||
<ArrowDown size="16" color={theme.textPrimary} />
|
||||
</ArrowWrapper>
|
||||
<LightCard padding="0.75rem 1rem" style={{ marginBottom: '0.25rem' }}>
|
||||
<AutoColumn gap="sm">
|
||||
<RowBetween align="flex-end">
|
||||
<RowFixed gap="0px">
|
||||
<TruncatedText fontSize={24} fontWeight={500} data-testid="output-amount">
|
||||
{trade.outputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap="0px">
|
||||
<CurrencyLogo currency={trade.outputAmount.currency} size="20px" style={{ marginRight: '12px' }} />
|
||||
<Text fontSize={20} fontWeight={500} data-testid="output-symbol">
|
||||
{trade.outputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedBody fontSize={14} color={theme.textTertiary}>
|
||||
<FiatValue
|
||||
fiatValue={fiatValueOutput}
|
||||
priceImpact={computeFiatValuePriceImpact(fiatValueInput.data, fiatValueOutput.data)}
|
||||
/>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
<RowBetween style={{ marginTop: '0.25rem', padding: '0 1rem' }}>
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
</RowBetween>
|
||||
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</LightCard>
|
||||
{showAcceptChanges ? (
|
||||
<SwapShowAcceptChanges justify="flex-start" gap="0px" data-testid="show-accept-changes">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<AlertTriangle size={20} style={{ marginRight: '8px', minWidth: 24 }} />
|
||||
<ThemedText.DeprecatedMain color={theme.accentAction}>
|
||||
<Trans>Price Updated</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</RowFixed>
|
||||
<ButtonPrimary
|
||||
style={{ padding: '.5rem', width: 'fit-content', fontSize: '0.825rem', borderRadius: '12px' }}
|
||||
onClick={onAcceptChanges}
|
||||
>
|
||||
<Trans>Accept</Trans>
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
</SwapShowAcceptChanges>
|
||||
) : null}
|
||||
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '.75rem 1rem' }}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<ThemedText.DeprecatedItalic fontWeight={400} textAlign="left" style={{ width: '100%' }}>
|
||||
<Trans>
|
||||
Output is estimated. You will receive at least{' '}
|
||||
<b>
|
||||
{trade.minimumAmountOut(allowedSlippage).toSignificant(6)} {trade.outputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedItalic>
|
||||
) : (
|
||||
<ThemedText.DeprecatedItalic fontWeight={400} textAlign="left" style={{ width: '100%' }}>
|
||||
<Trans>
|
||||
Input is estimated. You will sell at most{' '}
|
||||
<b>
|
||||
{trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {trade.inputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedItalic>
|
||||
)}
|
||||
</AutoColumn>
|
||||
{recipient !== null ? (
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }} data-testid="recipient-info">
|
||||
<ThemedText.DeprecatedMain>
|
||||
<Trans>
|
||||
Output will be sent to{' '}
|
||||
<b title={recipient}>{isAddress(recipient) ? shortenAddress(recipient) : recipient}</b>
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
<HeaderContainer gap="sm">
|
||||
<Column gap="lg">
|
||||
<SwapModalHeaderAmount
|
||||
field={Field.INPUT}
|
||||
label={<Trans>You pay</Trans>}
|
||||
amount={trade.inputAmount}
|
||||
usdAmount={fiatValueInput.data}
|
||||
/>
|
||||
<SwapModalHeaderAmount
|
||||
field={Field.OUTPUT}
|
||||
label={<Trans>You receive</Trans>}
|
||||
amount={trade.outputAmount}
|
||||
usdAmount={fiatValueOutput.data}
|
||||
tooltipText={
|
||||
trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<ThemedText.Caption>
|
||||
<Trans>
|
||||
Output is estimated. You will receive at least{' '}
|
||||
<b>
|
||||
{trade.minimumAmountOut(allowedSlippage).toSignificant(6)} {trade.outputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
) : (
|
||||
<ThemedText.Caption>
|
||||
<Trans>
|
||||
Input is estimated. You will sell at most{' '}
|
||||
<b>
|
||||
{trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {trade.inputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
<Rule />
|
||||
</HeaderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
68
src/components/swap/SwapModalHeaderAmount.tsx
Normal file
68
src/components/swap/SwapModalHeaderAmount.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { formatCurrencyAmount, formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import Column from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
import { PropsWithChildren, ReactNode } from 'react'
|
||||
import { TextProps } from 'rebass'
|
||||
import { Field } from 'state/swap/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
const MAX_AMOUNT_STR_LENGTH = 9
|
||||
|
||||
export const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>`
|
||||
cursor: ${({ cursor }) => cursor};
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const ResponsiveHeadline = ({ children, ...textProps }: PropsWithChildren<TextProps>) => {
|
||||
const { width } = useWindowSize()
|
||||
|
||||
if (width && width < BREAKPOINTS.xs) {
|
||||
return <ThemedText.HeadlineMedium {...textProps}>{children}</ThemedText.HeadlineMedium>
|
||||
}
|
||||
|
||||
return <ThemedText.HeadlineLarge {...textProps}>{children}</ThemedText.HeadlineLarge>
|
||||
}
|
||||
|
||||
interface AmountProps {
|
||||
field: Field
|
||||
tooltipText?: ReactNode
|
||||
label: ReactNode
|
||||
amount: CurrencyAmount<Currency> | undefined
|
||||
usdAmount?: number
|
||||
}
|
||||
|
||||
export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, field }: AmountProps) {
|
||||
let formattedAmount = formatCurrencyAmount(amount, NumberType.TokenTx)
|
||||
if (formattedAmount.length > MAX_AMOUNT_STR_LENGTH) {
|
||||
formattedAmount = formatCurrencyAmount(amount, NumberType.SwapTradeAmount)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row align="center" justify="space-between" gap="md">
|
||||
<Column gap="xs">
|
||||
<ThemedText.BodySecondary>
|
||||
<MouseoverTooltip text={tooltipText} disabled={!tooltipText}>
|
||||
<Label cursor="help">{label}</Label>
|
||||
</MouseoverTooltip>
|
||||
</ThemedText.BodySecondary>
|
||||
<Column gap="xs">
|
||||
<ResponsiveHeadline data-testid={`${field}-amount`}>
|
||||
{formattedAmount} {amount?.currency.symbol}
|
||||
</ResponsiveHeadline>
|
||||
{usdAmount && (
|
||||
<ThemedText.BodySmall color="textTertiary">
|
||||
{formatNumber(usdAmount, NumberType.FiatTokenQuantity)}
|
||||
</ThemedText.BodySmall>
|
||||
)}
|
||||
</Column>
|
||||
</Column>
|
||||
{amount?.currency && <CurrencyLogo currency={amount.currency} size="36px" />}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
@@ -12,13 +11,7 @@ import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
|
||||
|
||||
import RouterLabel from './RouterLabel'
|
||||
|
||||
export default function SwapRoute({
|
||||
trade,
|
||||
syncing,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
syncing: boolean
|
||||
}) {
|
||||
export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; syncing: boolean }) {
|
||||
const { chainId } = useWeb3React()
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
@@ -28,9 +21,9 @@ export default function SwapRoute({
|
||||
// TODO(WEB-3303)
|
||||
// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`?
|
||||
trade.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? trade.gasUseEstimateUSD === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: '$' + trade.gasUseEstimateUSD
|
||||
: undefined
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,17 +32,17 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c8 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
.c6 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
.c7 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c5 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
@@ -82,14 +82,34 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c5"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Network fee
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
~$1.00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c8"
|
||||
>
|
||||
<div
|
||||
class="c6"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Minimum output
|
||||
</div>
|
||||
@@ -97,7 +117,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
@@ -106,14 +126,14 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c5"
|
||||
class="c2 c3 c8"
|
||||
>
|
||||
<div
|
||||
class="c6"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
@@ -121,7 +141,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
@@ -133,16 +153,16 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c6"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
Uniswap API
|
||||
</div>
|
||||
|
||||
@@ -42,11 +42,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
.c12 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
.c16 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #D2D9EE;
|
||||
@@ -66,7 +66,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
.c15 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
@@ -89,11 +89,20 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
.c10 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
margin-right: 4px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.c11 > * {
|
||||
stroke: #98A1C0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
@@ -135,7 +144,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
.c13 {
|
||||
-webkit-transform: none;
|
||||
-ms-transform: none;
|
||||
transform: none;
|
||||
@@ -144,7 +153,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
.c14 {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
@@ -184,8 +193,32 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
<div
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<svg
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<svg
|
||||
class="c11"
|
||||
>
|
||||
gas-icon.svg
|
||||
</svg>
|
||||
<div
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
$1.00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="c13"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="#98A1C0"
|
||||
@@ -207,15 +240,35 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c11"
|
||||
class="c14"
|
||||
data-testid="advanced-swap-details"
|
||||
>
|
||||
<div
|
||||
class="c12"
|
||||
class="c15"
|
||||
>
|
||||
<div
|
||||
class="c13"
|
||||
class="c16"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Network fee
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
~$1.00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
@@ -223,11 +276,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Minimum output
|
||||
</div>
|
||||
@@ -247,11 +300,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
@@ -265,18 +318,18 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c13"
|
||||
class="c16"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c14"
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,172 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`] = `
|
||||
exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = `
|
||||
<DocumentFragment>
|
||||
.c3 {
|
||||
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: flex-start;
|
||||
-webkit-box-align: flex-start;
|
||||
-ms-flex-align: flex-start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
cursor: help;
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c5 css-zhpkf8"
|
||||
>
|
||||
Exchange rate
|
||||
</div>
|
||||
<div
|
||||
class="c2 c6 css-zhpkf8"
|
||||
>
|
||||
1 DEF = 1.00 ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c2 c8 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Network fee
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c6 css-zhpkf8"
|
||||
>
|
||||
~$1.00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c2 c8 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Price impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
105566.373%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c2 c8 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Minimum received
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c6 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -58,6 +223,10 @@ exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`]
|
||||
margin: !important;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
color: #F5F6FC;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
@@ -146,106 +315,24 @@ exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`]
|
||||
}
|
||||
|
||||
.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;
|
||||
height: 56px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1 c2"
|
||||
>
|
||||
<button
|
||||
class="c3 c4 c5"
|
||||
class="c3 c4 c5 c6"
|
||||
data-testid="confirm-swap-button"
|
||||
disabled=""
|
||||
id="confirm-swap-or-send"
|
||||
style="margin: 10px 0px 0px 0px;"
|
||||
>
|
||||
<div
|
||||
class="css-10ob8xa"
|
||||
class="c7 css-iapcxi"
|
||||
>
|
||||
Confirm Swap
|
||||
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>
|
||||
`;
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact input 1`] = `
|
||||
exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c3 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c4 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -26,99 +19,30 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
-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;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
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: 0px;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
margin: -0px;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
.c6 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
.c8 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: #D2D9EE;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
border: 1px solid #E8ECFB;
|
||||
background-color: #F5F6FC;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
@@ -130,63 +54,39 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 4px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
justify-items: flex-start;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
border-radius: 12px;
|
||||
border-radius: 12px;
|
||||
height: 24px;
|
||||
width: 50%;
|
||||
width: 50%;
|
||||
-webkit-animation: fAQEyV 1.5s infinite;
|
||||
animation: fAQEyV 1.5s infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient( to left,#E8ECFB 25%,#fff 50%,#E8ECFB 75% );
|
||||
will-change: background-position;
|
||||
background-size: 400%;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
border-radius: 4px;
|
||||
width: 4rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
.c11 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
|
||||
box-shadow: 0 0 1px white;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
.c10 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -194,342 +94,334 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
.c7 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
cursor: help;
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
margin: 16px 2px 24px 2px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c2"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
You pay
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
You receive
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<0.00001 DEF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<img
|
||||
alt="DEF logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c12 c13"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
|
||||
<DocumentFragment>
|
||||
.c3 {
|
||||
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: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: #D2D9EE;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-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;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
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;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
|
||||
box-shadow: 0 0 1px white;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
left: calc(50% - 16px);
|
||||
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;
|
||||
background-color: #FFFFFF;
|
||||
border: 4px solid;
|
||||
border-color: #F5F6FC;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
cursor: help;
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
margin: 16px 2px 24px 2px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
style="margin-top: 1rem;"
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c1 c2 c3"
|
||||
class="c2"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c10 css-13xjr5l"
|
||||
data-testid="input-amount"
|
||||
>
|
||||
0.000000000000001
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
>
|
||||
<div
|
||||
class="c11"
|
||||
style="margin-right: 12px;"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c12"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-10ob8xa"
|
||||
data-testid="input-symbol"
|
||||
>
|
||||
ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c13 c14"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="#0D111C"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="5"
|
||||
y2="19"
|
||||
/>
|
||||
<polyline
|
||||
points="19 12 12 19 5 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="c1 c2 c3"
|
||||
style="margin-bottom: 0.25rem;"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c16 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
>
|
||||
<div
|
||||
class="c10 css-1kwqs79"
|
||||
data-testid="output-amount"
|
||||
>
|
||||
0.000000000000001
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
>
|
||||
<div
|
||||
class="c11"
|
||||
style="margin-right: 12px;"
|
||||
>
|
||||
<img
|
||||
alt="DEF logo"
|
||||
class="c12"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-10ob8xa"
|
||||
data-testid="output-symbol"
|
||||
>
|
||||
DEF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c13 c14"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
style="margin-top: 0.25rem; padding: 0px 1rem;"
|
||||
>
|
||||
<button
|
||||
class="c17"
|
||||
title="1 DEF = 1.00 ABC "
|
||||
>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
>
|
||||
1 DEF = 1.00 ABC
|
||||
</div>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c19 c3"
|
||||
style="padding: .75rem; margin-top: 0.5rem;"
|
||||
>
|
||||
<div
|
||||
class="c20"
|
||||
>
|
||||
<div
|
||||
class="c21"
|
||||
/>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c22"
|
||||
>
|
||||
<div
|
||||
class="c23"
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Minimum output
|
||||
You pay
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
class="c5"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
class="c10"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c22"
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c23"
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Expected output
|
||||
You receive
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
class="c5"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c21"
|
||||
/>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c23"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
>
|
||||
Uniswap API
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<0.00001 GHI
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c25"
|
||||
style="padding: .75rem 1rem;"
|
||||
>
|
||||
<div
|
||||
class="c24 css-k51stg"
|
||||
style="width: 100%;"
|
||||
>
|
||||
Output is estimated. You will receive at least
|
||||
<b>
|
||||
0.00000000000000098 DEF
|
||||
</b>
|
||||
or the transaction will revert.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c25"
|
||||
data-testid="recipient-info"
|
||||
style="padding: 12px 0px 0px 0px;"
|
||||
>
|
||||
<div
|
||||
class="c24 css-8mokm4"
|
||||
>
|
||||
Output will be sent to
|
||||
<b
|
||||
title="0x0000000000000000000000000000000000000004"
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
0x0000...0004
|
||||
</b>
|
||||
<img
|
||||
alt="GHI logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000003/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c12 c13"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
@@ -29,6 +28,7 @@ export const SwapWrapper = styled.main<{ chainId: number | undefined }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 8px;
|
||||
padding-top: 12px;
|
||||
box-shadow: ${({ chainId }) => !!chainId && chainId === SupportedChainId.BNB && '0px 40px 120px 0px #f0b90b29'};
|
||||
z-index: ${Z_INDEX.default};
|
||||
transition: transform 250ms ease;
|
||||
@@ -63,13 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
: null}
|
||||
`
|
||||
|
||||
export const TruncatedText = styled(Text)`
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
// styles
|
||||
export const Dots = styled.span`
|
||||
&::after {
|
||||
@@ -135,7 +128,7 @@ export function SwapCallbackError({ error }: { error: ReactNode }) {
|
||||
export const SwapShowAcceptChanges = styled(AutoColumn)`
|
||||
background-color: ${({ theme }) => transparentize(0.95, theme.deprecated_primary3)};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
padding: 0.5rem;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
@@ -46,16 +46,16 @@ function useTryActivation() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Failed Connection events are logged here, while successful ones are logged by Web3Provider
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.FAILED,
|
||||
|
||||
@@ -113,3 +113,7 @@ export const L2_CHAIN_IDS = [
|
||||
] as const
|
||||
|
||||
export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number]
|
||||
|
||||
export function isPolygonChain(chainId: number): chainId is SupportedChainId.POLYGON | SupportedChainId.POLYGON_MUMBAI {
|
||||
return chainId === SupportedChainId.POLYGON || chainId === SupportedChainId.POLYGON_MUMBAI
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ gql`
|
||||
}
|
||||
description
|
||||
}
|
||||
listings(first: 1) {
|
||||
listings(first: 25) {
|
||||
edges {
|
||||
node {
|
||||
address
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import useGasPrice from './useGasPrice'
|
||||
import useStablecoinPrice, { useStablecoinValue } from './useStablecoinPrice'
|
||||
import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice'
|
||||
|
||||
const DEFAULT_AUTO_SLIPPAGE = new Percent(1, 1000) // .10%
|
||||
|
||||
@@ -72,15 +72,14 @@ const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 100) // 5%
|
||||
/**
|
||||
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
|
||||
*/
|
||||
export default function useAutoSlippageTolerance(
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
): Percent {
|
||||
export default function useAutoSlippageTolerance(trade?: InterfaceTrade): Percent {
|
||||
const { chainId } = useWeb3React()
|
||||
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
|
||||
const outputDollarValue = useStablecoinValue(trade?.outputAmount)
|
||||
const nativeGasPrice = useGasPrice()
|
||||
|
||||
const gasEstimate = guesstimateGas(trade)
|
||||
const gasEstimateUSD = useStablecoinAmountFromFiatValue(trade?.gasUseEstimateUSD) ?? null
|
||||
const nativeCurrency = useNativeCurrency(chainId)
|
||||
const nativeCurrencyPrice = useStablecoinPrice((trade && nativeCurrency) ?? undefined)
|
||||
|
||||
@@ -100,9 +99,7 @@ export default function useAutoSlippageTolerance(
|
||||
// NOTE - dont use gas estimate for L2s yet - need to verify accuracy
|
||||
// if not, use local heuristic
|
||||
const dollarCostToUse =
|
||||
chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD
|
||||
: dollarGasCost
|
||||
chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && gasEstimateUSD ? gasEstimateUSD : dollarGasCost
|
||||
|
||||
if (outputDollarValue && dollarCostToUse) {
|
||||
// optimize for highest possible slippage without getting MEV'd
|
||||
@@ -121,5 +118,15 @@ export default function useAutoSlippageTolerance(
|
||||
}
|
||||
|
||||
return DEFAULT_AUTO_SLIPPAGE
|
||||
}, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue])
|
||||
}, [
|
||||
trade,
|
||||
onL2,
|
||||
nativeGasPrice,
|
||||
gasEstimate,
|
||||
nativeCurrency,
|
||||
nativeCurrencyPrice,
|
||||
chainId,
|
||||
gasEstimateUSD,
|
||||
outputDollarValue,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -83,15 +83,6 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute client side v3 trade if routing api is SYNCING', () => {
|
||||
expectRouterMock(TradeState.SYNCING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
describe('when routing api is in error state', () => {
|
||||
@@ -167,15 +158,6 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute client side v3 trade if routing api is SYNCING', () => {
|
||||
expectRouterMock(TradeState.SYNCING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
describe('when routing api is in error state', () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useBestTrade(
|
||||
otherCurrency?: Currency
|
||||
): {
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade?: InterfaceTrade
|
||||
} {
|
||||
const { chainId } = useWeb3React()
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import JSBI from 'jsbi'
|
||||
import { useSingleContractWithCallData } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import { ClassicTrade, InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
|
||||
import { isCelo } from '../constants/tokens'
|
||||
import { useAllV3Routes } from './useAllV3Routes'
|
||||
@@ -33,7 +33,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
|
||||
tradeType: TTradeType,
|
||||
amountSpecified?: CurrencyAmount<Currency>,
|
||||
otherCurrency?: Currency
|
||||
): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } {
|
||||
): { state: TradeState; trade: InterfaceTrade | undefined } {
|
||||
const [currencyIn, currencyOut] =
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
@@ -135,7 +135,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
|
||||
|
||||
return {
|
||||
state: TradeState.VALID,
|
||||
trade: new InterfaceTrade({
|
||||
trade: new ClassicTrade({
|
||||
v2Routes: [],
|
||||
v3Routes: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { BigintIsh, Currency, Token } from '@uniswap/sdk-core'
|
||||
import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import IUniswapV3PoolStateJSON from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import { computePoolAddress } from '@uniswap/v3-sdk'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -11,7 +11,7 @@ import { useMemo } from 'react'
|
||||
import { V3_CORE_FACTORY_ADDRESSES } from '../constants/addresses'
|
||||
import { IUniswapV3PoolStateInterface } from '../types/v3/IUniswapV3PoolState'
|
||||
|
||||
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
|
||||
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateJSON.abi) as IUniswapV3PoolStateInterface
|
||||
|
||||
// Classes are expensive to instantiate, so this caches the recently instantiated pools.
|
||||
// This avoids re-instantiating pools as the other pools in the same request are loaded.
|
||||
|
||||
@@ -41,8 +41,8 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
|
||||
}
|
||||
}
|
||||
|
||||
if (!trade || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
|
||||
return { data: undefined, isLoading: state === TradeState.LOADING || state === TradeState.SYNCING }
|
||||
if (!trade || state === TradeState.LOADING || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
|
||||
return { data: undefined, isLoading: state === TradeState.LOADING }
|
||||
}
|
||||
|
||||
const { numerator, denominator } = trade.routes[0].midPrice
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import IUniswapV2PairJSON from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { computePairAddress, Pair } from '@uniswap/v2-sdk'
|
||||
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { V2_FACTORY_ADDRESSES } from '../constants/addresses'
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairJSON.abi)
|
||||
|
||||
export enum PairState {
|
||||
LOADING,
|
||||
|
||||
@@ -3,8 +3,10 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import JSBI from 'jsbi'
|
||||
import { GetQuoteResult } from 'state/routing/types'
|
||||
import { GetQuoteArgs } from 'state/routing/slice'
|
||||
import { QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types'
|
||||
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
|
||||
|
||||
export function toSupportedChainId(chainId: ChainId): SupportedChainId | undefined {
|
||||
@@ -19,50 +21,41 @@ export function isSupportedChainId(chainId: ChainId | undefined): boolean {
|
||||
|
||||
async function getQuote(
|
||||
{
|
||||
type,
|
||||
tradeType,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amount: amountRaw,
|
||||
}: {
|
||||
type: 'exactIn' | 'exactOut'
|
||||
tradeType: TradeType
|
||||
tokenIn: { address: string; chainId: number; decimals: number; symbol?: string }
|
||||
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
|
||||
amount: BigintIsh
|
||||
},
|
||||
router: AlphaRouter,
|
||||
config: Partial<AlphaRouterConfig>
|
||||
): Promise<{ data: GetQuoteResult; error?: unknown }> {
|
||||
const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
|
||||
const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
|
||||
routerConfig: Partial<AlphaRouterConfig>
|
||||
): Promise<QuoteResult> {
|
||||
const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenIn.address as SwapRouterNativeAssets)
|
||||
const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOut.address as SwapRouterNativeAssets)
|
||||
|
||||
const currencyIn = tokenInIsNative
|
||||
? nativeOnChain(tokenIn.chainId)
|
||||
: new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
|
||||
const currencyOut = tokenOutIsNative
|
||||
? nativeOnChain(tokenOut.chainId)
|
||||
: new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
|
||||
|
||||
const baseCurrency = tradeType === TradeType.EXACT_INPUT ? currencyIn : currencyOut
|
||||
const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? currencyOut : currencyIn
|
||||
|
||||
const baseCurrency = type === 'exactIn' ? currencyIn : currencyOut
|
||||
const quoteCurrency = type === 'exactIn' ? currencyOut : currencyIn
|
||||
const amount = CurrencyAmount.fromRawAmount(baseCurrency, JSBI.BigInt(amountRaw))
|
||||
// TODO (WEB-2055): explore initializing client side routing on first load (when amountRaw is null) if there are enough users using client-side router preference.
|
||||
const swapRoute = await router.route(amount, quoteCurrency, tradeType, /*swapConfig=*/ undefined, routerConfig)
|
||||
|
||||
const swapRoute = await router.route(
|
||||
amount,
|
||||
quoteCurrency,
|
||||
type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
|
||||
/*swapConfig=*/ undefined,
|
||||
config
|
||||
)
|
||||
if (!swapRoute) {
|
||||
return { state: QuoteState.NOT_FOUND }
|
||||
}
|
||||
|
||||
if (!swapRoute) throw new Error('Failed to generate client side quote')
|
||||
|
||||
return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) }
|
||||
}
|
||||
|
||||
interface QuoteArguments {
|
||||
tokenInAddress: string
|
||||
tokenInChainId: ChainId
|
||||
tokenInDecimals: number
|
||||
tokenInSymbol?: string
|
||||
tokenOutAddress: string
|
||||
tokenOutChainId: ChainId
|
||||
tokenOutDecimals: number
|
||||
tokenOutSymbol?: string
|
||||
amount: string
|
||||
type: 'exactIn' | 'exactOut'
|
||||
return transformSwapRouteToGetQuoteResult(tradeType, amount, swapRoute)
|
||||
}
|
||||
|
||||
export async function getClientSideQuote(
|
||||
@@ -76,14 +69,14 @@ export async function getClientSideQuote(
|
||||
tokenOutDecimals,
|
||||
tokenOutSymbol,
|
||||
amount,
|
||||
type,
|
||||
}: QuoteArguments,
|
||||
tradeType,
|
||||
}: GetQuoteArgs,
|
||||
router: AlphaRouter,
|
||||
config: Partial<AlphaRouterConfig>
|
||||
) {
|
||||
return getQuote(
|
||||
{
|
||||
type,
|
||||
tradeType,
|
||||
tokenIn: {
|
||||
address: tokenInAddress,
|
||||
chainId: tokenInChainId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { useMemo } from 'react'
|
||||
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
|
||||
import { currencyAddressForSwapQuote } from 'state/routing/utils'
|
||||
|
||||
/**
|
||||
* Returns query arguments for the Routing API query or undefined if the
|
||||
@@ -26,16 +27,16 @@ export function useRoutingAPIArguments({
|
||||
? undefined
|
||||
: {
|
||||
amount: amount.quotient.toString(),
|
||||
tokenInAddress: tokenIn.wrapped.address,
|
||||
tokenInAddress: currencyAddressForSwapQuote(tokenIn),
|
||||
tokenInChainId: tokenIn.wrapped.chainId,
|
||||
tokenInDecimals: tokenIn.wrapped.decimals,
|
||||
tokenInSymbol: tokenIn.wrapped.symbol,
|
||||
tokenOutAddress: tokenOut.wrapped.address,
|
||||
tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
|
||||
tokenOutChainId: tokenOut.wrapped.chainId,
|
||||
tokenOutDecimals: tokenOut.wrapped.decimals,
|
||||
tokenOutSymbol: tokenOut.wrapped.symbol,
|
||||
routerPreference,
|
||||
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut',
|
||||
tradeType,
|
||||
},
|
||||
[amount, routerPreference, tokenIn, tokenOut, tradeType]
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from 'make-plural/plurals'
|
||||
import { PluralCategory } from 'make-plural/plurals'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { retry } from 'utils/retry'
|
||||
|
||||
type LocalePlural = {
|
||||
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
|
||||
@@ -79,7 +80,7 @@ const plurals: LocalePlural = {
|
||||
export async function dynamicActivate(locale: SupportedLocale) {
|
||||
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
|
||||
try {
|
||||
const catalog = await import(`locales/${locale}.js`)
|
||||
const catalog = await retry(() => import(`locales/${locale}.js`))
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
|
||||
fiatValues,
|
||||
txHash,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade | Trade<Currency, Currency, TradeType>
|
||||
fiatValues: { amountIn: number | undefined; amountOut: number | undefined }
|
||||
txHash: string
|
||||
}) => ({
|
||||
@@ -61,7 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
|
||||
|
||||
export const formatSwapQuoteReceivedEventProperties = (
|
||||
trade: Trade<Currency, Currency, TradeType>,
|
||||
gasUseEstimateUSD?: CurrencyAmount<Token>,
|
||||
gasUseEstimateUSD?: string,
|
||||
fetchingSwapQuoteStartTime?: Date
|
||||
) => {
|
||||
return {
|
||||
@@ -70,7 +70,7 @@ export const formatSwapQuoteReceivedEventProperties = (
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined,
|
||||
estimated_network_fee_usd: gasUseEstimateUSD ? formatToDecimal(gasUseEstimateUSD, 2) : undefined,
|
||||
estimated_network_fee_usd: gasUseEstimateUSD,
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
|
||||
@@ -3,7 +3,7 @@ import { formatEther, parseEther } from '@ethersproject/units'
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
@@ -208,7 +208,7 @@ const InputCurrencyValue = ({
|
||||
totalEthPrice: BigNumber
|
||||
activeCurrency: Currency | undefined | null
|
||||
tradeState: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade: InterfaceTrade | undefined
|
||||
}) => {
|
||||
if (!usingPayWithAnyToken) {
|
||||
return (
|
||||
@@ -219,7 +219,7 @@ const InputCurrencyValue = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (tradeState === TradeState.LOADING) {
|
||||
if (tradeState === TradeState.LOADING && !trade) {
|
||||
return (
|
||||
<ThemedText.BodyPrimary color="textTertiary" lineHeight="20px" fontWeight="500">
|
||||
<Trans>Fetching price...</Trans>
|
||||
@@ -228,7 +228,7 @@ const InputCurrencyValue = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<ValueText color={tradeState === TradeState.SYNCING ? 'textTertiary' : 'textPrimary'}>
|
||||
<ValueText color={tradeState === TradeState.LOADING ? 'textTertiary' : 'textPrimary'}>
|
||||
{ethNumberStandardFormatter(trade?.inputAmount.toExact())}
|
||||
</ValueText>
|
||||
)
|
||||
|
||||
@@ -9,13 +9,14 @@ import { DataPageHeader } from './DataPageHeader'
|
||||
import { DataPageTable } from './DataPageTable'
|
||||
import { DataPageTraits } from './DataPageTraits'
|
||||
|
||||
const DataPageContainer = styled(Column)`
|
||||
const DataPagePaddingContainer = styled.div`
|
||||
padding: 24px 64px;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
gap: 36px;
|
||||
max-width: ${({ theme }) => theme.maxWidth};
|
||||
margin: 0 auto;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.md}px) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
padding: 24px 48px;
|
||||
@@ -26,6 +27,14 @@ const DataPageContainer = styled(Column)`
|
||||
}
|
||||
`
|
||||
|
||||
const DataPageContainer = styled(Column)`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 36px;
|
||||
max-width: ${({ theme }) => theme.maxWidth};
|
||||
margin: 0 auto;
|
||||
`
|
||||
|
||||
const ContentContainer = styled(Row)`
|
||||
gap: 24px;
|
||||
padding-bottom: 45px;
|
||||
@@ -43,15 +52,17 @@ const LeftColumn = styled(Column)`
|
||||
|
||||
export const DataPage = ({ asset }: { asset: GenieAsset }) => {
|
||||
return (
|
||||
<DataPageContainer>
|
||||
<DataPageHeader asset={asset} />
|
||||
<ContentContainer>
|
||||
<LeftColumn>
|
||||
{!!asset.traits?.length && <DataPageTraits asset={asset} />}
|
||||
<DataPageDescription />
|
||||
</LeftColumn>
|
||||
<DataPageTable />
|
||||
</ContentContainer>
|
||||
</DataPageContainer>
|
||||
<DataPagePaddingContainer>
|
||||
<DataPageContainer>
|
||||
<DataPageHeader asset={asset} />
|
||||
<ContentContainer>
|
||||
<LeftColumn>
|
||||
{!!asset.traits?.length && <DataPageTraits asset={asset} />}
|
||||
<DataPageDescription />
|
||||
</LeftColumn>
|
||||
<DataPageTable asset={asset} />
|
||||
</ContentContainer>
|
||||
</DataPageContainer>
|
||||
</DataPagePaddingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
23
src/nft/components/details/detailsV2/DataPageTable.test.tsx
Normal file
23
src/nft/components/details/detailsV2/DataPageTable.test.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { TEST_NFT_ASSET, TEST_OFFER, TEST_SELL_ORDER } from 'test-utils/nft/fixtures'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { ListingsTableContent } from './ListingsTableContent'
|
||||
import { OffersTableContent } from './OffersTableContent'
|
||||
|
||||
it('data page offers table content loads with a given asset', () => {
|
||||
const assetWithOffer = {
|
||||
...TEST_NFT_ASSET,
|
||||
offers: [TEST_OFFER],
|
||||
}
|
||||
const { asFragment } = render(<OffersTableContent asset={assetWithOffer} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('data page listings table content loads with a given asset', () => {
|
||||
const assetWithOrder = {
|
||||
...TEST_NFT_ASSET,
|
||||
sellorders: [TEST_SELL_ORDER],
|
||||
}
|
||||
const { asFragment } = render(<ListingsTableContent asset={assetWithOrder} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
@@ -1,45 +1,50 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { GenieAsset } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { ActivityTableContent } from './ActivityTableContent'
|
||||
import { ListingsTableContent } from './ListingsTableContent'
|
||||
import { OffersTableContent } from './OffersTableContent'
|
||||
import { Tab, TabbedComponent } from './TabbedComponent'
|
||||
|
||||
enum TableTabsKeys {
|
||||
export enum TableTabsKeys {
|
||||
Activity = 'activity',
|
||||
Offers = 'offers',
|
||||
Listings = 'listings',
|
||||
}
|
||||
|
||||
const TableTabs: Map<string, Tab> = new Map([
|
||||
[
|
||||
TableTabsKeys.Activity,
|
||||
{
|
||||
title: <Trans>Activity</Trans>,
|
||||
key: TableTabsKeys.Activity,
|
||||
content: <ActivityTableContent />,
|
||||
},
|
||||
],
|
||||
[
|
||||
TableTabsKeys.Offers,
|
||||
{
|
||||
title: <Trans>Offers</Trans>,
|
||||
key: TableTabsKeys.Offers,
|
||||
content: <OffersTableContent />,
|
||||
count: 11, // TODO Replace Placeholder with real data
|
||||
},
|
||||
],
|
||||
[
|
||||
TableTabsKeys.Listings,
|
||||
{
|
||||
title: <Trans>Listings</Trans>,
|
||||
key: TableTabsKeys.Listings,
|
||||
content: <ListingsTableContent />,
|
||||
count: 11, // TODO Replace Placeholder with real data
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
export const DataPageTable = () => {
|
||||
export const DataPageTable = ({ asset }: { asset: GenieAsset }) => {
|
||||
const TableTabs: Map<string, Tab> = useMemo(
|
||||
() =>
|
||||
new Map([
|
||||
[
|
||||
TableTabsKeys.Activity,
|
||||
{
|
||||
title: <Trans>Activity</Trans>,
|
||||
key: TableTabsKeys.Activity,
|
||||
content: <ActivityTableContent />,
|
||||
},
|
||||
],
|
||||
[
|
||||
TableTabsKeys.Offers,
|
||||
{
|
||||
title: <Trans>Offers</Trans>,
|
||||
key: TableTabsKeys.Offers,
|
||||
content: <OffersTableContent asset={asset} />,
|
||||
count: 11, // TODO Replace Placeholder with real data
|
||||
},
|
||||
],
|
||||
[
|
||||
TableTabsKeys.Listings,
|
||||
{
|
||||
title: <Trans>Listings</Trans>,
|
||||
key: TableTabsKeys.Listings,
|
||||
content: <ListingsTableContent asset={asset} />,
|
||||
count: asset.sellorders?.length,
|
||||
},
|
||||
],
|
||||
]),
|
||||
[asset]
|
||||
)
|
||||
return <TabbedComponent tabs={TableTabs} />
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import { GenieAsset } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
import { Scrim } from './shared'
|
||||
import { Tab, TabbedComponent } from './TabbedComponent'
|
||||
import { TraitRow } from './TraitRow'
|
||||
|
||||
const TraitsHeaderContainer = styled(Row)`
|
||||
padding-right: 12px;
|
||||
padding: 0px 12px;
|
||||
`
|
||||
|
||||
const TraitsHeader = styled(ThemedText.SubHeaderSmall)<{
|
||||
@@ -45,26 +45,6 @@ const TraitRowScrollableContainer = styled.div`
|
||||
${ScrollBarStyles}
|
||||
`
|
||||
|
||||
// Scrim that fades out the top and bottom of the scrollable container, isBottom changes the direction and placement of the fade
|
||||
const Scrim = styled.div<{ isBottom?: boolean }>`
|
||||
position: absolute;
|
||||
height: 88px;
|
||||
left: 0px;
|
||||
right: 6px;
|
||||
|
||||
${({ isBottom }) =>
|
||||
isBottom
|
||||
? 'bottom: 0px'
|
||||
: `
|
||||
top: 0px;
|
||||
transform: matrix(1, 0, 0, -1, 0, 0);
|
||||
`};
|
||||
|
||||
background: ${({ theme }) =>
|
||||
`linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`};
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const TraitsContent = ({ asset }: { asset: GenieAsset }) => {
|
||||
const { userCanScroll, scrollRef, scrollProgress, scrollHandler } = useSubscribeScrollState()
|
||||
|
||||
|
||||
201
src/nft/components/details/detailsV2/InfoChips.tsx
Normal file
201
src/nft/components/details/detailsV2/InfoChips.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { Unicon } from 'components/Unicon'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset } from 'nft/types'
|
||||
import { getLinkForTrait } from 'nft/utils'
|
||||
import { ReactNode, useReducer } from 'react'
|
||||
import { ChevronDown, DollarSign } from 'react-feather'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ClickableStyle, EllipsisStyle, ExternalLink, LinkStyle, ThemedText } from 'theme'
|
||||
import { isAddress, shortenAddress } from 'utils'
|
||||
import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
import { getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
const StyledBubble = styled(Row)`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
padding: 10px 12px 10px 8px;
|
||||
border-radius: 20px;
|
||||
max-width: 144px;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
max-width: 169px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLabelMedium = styled.div`
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
${EllipsisStyle}
|
||||
`
|
||||
|
||||
const StyledIcon = styled(Row)`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
${ClickableStyle}
|
||||
${LinkStyle}
|
||||
`
|
||||
|
||||
const ConditionalLinkWrapper = ({
|
||||
isExternal,
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
isExternal?: boolean
|
||||
href: string
|
||||
children: ReactNode
|
||||
}) => {
|
||||
return isExternal ? (
|
||||
<ExternalLink href={href}>{children}</ExternalLink>
|
||||
) : (
|
||||
<StyledLink to={href}>{children}</StyledLink>
|
||||
)
|
||||
}
|
||||
|
||||
const InfoBubble = ({
|
||||
title,
|
||||
info,
|
||||
icon,
|
||||
href,
|
||||
isExternal,
|
||||
}: {
|
||||
title: ReactNode
|
||||
info: string
|
||||
icon: ReactNode
|
||||
href: string
|
||||
isExternal?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<Column gap="sm">
|
||||
<ThemedText.Caption color="textSecondary">{title}</ThemedText.Caption>
|
||||
<ConditionalLinkWrapper isExternal={isExternal} href={href}>
|
||||
<StyledBubble gap="sm">
|
||||
<StyledIcon>{icon}</StyledIcon>
|
||||
<StyledLabelMedium>{info}</StyledLabelMedium>
|
||||
</StyledBubble>
|
||||
</ConditionalLinkWrapper>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
const InfoChipDropdown = styled.button`
|
||||
padding: 10px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const InfoChipDropdownContainer = styled(Column)`
|
||||
height: 100%;
|
||||
margin-top: auto;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Break = styled(Column)`
|
||||
flex-basis: 100%;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const InfoChipsContainer = styled(Row)`
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledChevron = styled(ChevronDown)<{ isOpen: boolean }>`
|
||||
transform: ${({ isOpen }) => (isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
|
||||
will-change: transform;
|
||||
transition: transform ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
export const InfoChips = ({ asset }: { asset: GenieAsset }) => {
|
||||
const { chainId } = useWeb3React()
|
||||
const isMobile = useIsMobile()
|
||||
const [showExtraInfoChips, toggleShowExtraInfoChips] = useReducer((s) => !s, false)
|
||||
const shouldShowExtraInfoChips = !isMobile || showExtraInfoChips
|
||||
|
||||
const topTrait = asset?.traits?.[0]
|
||||
const traitCollectionAddress = topTrait && getLinkForTrait(topTrait, asset.address)
|
||||
|
||||
const isChecksummedAddress = isAddress(asset.ownerAddress)
|
||||
const checksummedAddress = isChecksummedAddress ? isChecksummedAddress : undefined
|
||||
const { ENSName } = useENSName(checksummedAddress)
|
||||
const { avatar } = useENSAvatar(checksummedAddress)
|
||||
const shortenedAddress = asset.ownerAddress ? shortenAddress(asset.ownerAddress) : ''
|
||||
const addressToDisplay = ENSName ?? shortenedAddress
|
||||
const avatarToDisplay = avatar ? (
|
||||
<img src={avatar} width={24} height={24} />
|
||||
) : (
|
||||
<Unicon size={24} address={asset.ownerAddress ?? ''} />
|
||||
)
|
||||
|
||||
return (
|
||||
<Column gap="sm">
|
||||
<InfoChipsContainer justify="center">
|
||||
<InfoBubble
|
||||
title={<Trans>Owner</Trans>}
|
||||
info={addressToDisplay}
|
||||
icon={avatarToDisplay}
|
||||
href={getExplorerLink(chainId ?? 1, asset.ownerAddress ?? '', ExplorerDataType.ADDRESS)}
|
||||
isExternal={true}
|
||||
/>
|
||||
{traitCollectionAddress && (
|
||||
<>
|
||||
<InfoBubble
|
||||
title={<Trans>Trait Floor</Trans>}
|
||||
info="5.3 ETH"
|
||||
icon={<DollarSign size={20} />}
|
||||
href={traitCollectionAddress}
|
||||
/>
|
||||
<InfoChipDropdownContainer>
|
||||
<InfoChipDropdown onClick={toggleShowExtraInfoChips}>
|
||||
<StyledChevron isOpen={showExtraInfoChips} size={20} display="block" />
|
||||
</InfoChipDropdown>
|
||||
</InfoChipDropdownContainer>
|
||||
{shouldShowExtraInfoChips && (
|
||||
<>
|
||||
<Break />
|
||||
<InfoBubble
|
||||
title={<Trans>Top Trait</Trans>}
|
||||
info={topTrait.trait_value}
|
||||
icon=""
|
||||
href={traitCollectionAddress}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</InfoChipsContainer>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
import { InfoChips } from './InfoChips'
|
||||
import { MediaRenderer } from './MediaRenderer'
|
||||
|
||||
const MAX_WIDTH = 560
|
||||
@@ -117,6 +118,7 @@ export const LandingPage = ({ asset, collection }: LandingPageProps) => {
|
||||
</Row>
|
||||
<StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText>
|
||||
</InfoDetailsContainer>
|
||||
<InfoChips asset={asset} />
|
||||
</InfoContainer>
|
||||
</LandingPageContainer>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { TableContentContainer } from './shared'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { AddToBagIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset } from 'nft/types'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
|
||||
export const ListingsTableContent = () => {
|
||||
return <TableContentContainer>Listings Content</TableContentContainer>
|
||||
import { TableTabsKeys } from './DataPageTable'
|
||||
import { TableContentComponent } from './TableContentComponent'
|
||||
import { ContentRow, HeaderRow } from './TableRowComponent'
|
||||
|
||||
export const ListingsTableContent = ({ asset }: { asset: GenieAsset }) => {
|
||||
const isMobile = useIsMobile()
|
||||
const theme = useTheme()
|
||||
const headers = <HeaderRow type={TableTabsKeys.Listings} is1155={asset.tokenType === NftStandard.Erc1155} />
|
||||
const contentRows = (asset.sellorders || []).map((offer, index) => (
|
||||
<ContentRow
|
||||
key={'offer_' + index}
|
||||
content={offer}
|
||||
buttonCTA={isMobile ? <AddToBagIcon color={theme.textSecondary} /> : <Trans>Add to Bag</Trans>}
|
||||
is1155={asset.tokenType === NftStandard.Erc1155}
|
||||
/>
|
||||
))
|
||||
return <TableContentComponent headerRow={headers} contentRows={contentRows} type={TableTabsKeys.Offers} />
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ const DetailsBackground = styled.div<{ backgroundImage: string }>`
|
||||
|
||||
const DetailsContentContainer = styled.div`
|
||||
z-index: ${Z_INDEX.hover};
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
export const NftDetails = ({ asset, collection }: NftDetailsProps) => {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
import { TableContentContainer } from './shared'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import { GenieAsset } from 'nft/types'
|
||||
import { Check } from 'react-feather'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import { TEST_OFFER } from 'test-utils/nft/fixtures'
|
||||
|
||||
export const OffersTableContent = () => {
|
||||
return <TableContentContainer>Offers Content</TableContentContainer>
|
||||
import { TableTabsKeys } from './DataPageTable'
|
||||
import { TableContentComponent } from './TableContentComponent'
|
||||
import { ContentRow, HeaderRow } from './TableRowComponent'
|
||||
|
||||
export const OffersTableContent = ({ asset }: { asset: GenieAsset }) => {
|
||||
// TODO(NFT-1189) Replace with real offer data when BE supports
|
||||
const mockOffers = new Array(11).fill(TEST_OFFER)
|
||||
const isMobile = useIsMobile()
|
||||
const theme = useTheme()
|
||||
const headers = <HeaderRow type={TableTabsKeys.Offers} is1155={asset.tokenType === NftStandard.Erc1155} />
|
||||
const contentRows = mockOffers.map((offer, index) => (
|
||||
<ContentRow
|
||||
key={'offer_' + index}
|
||||
content={offer}
|
||||
buttonCTA={isMobile ? <Check color={theme.textSecondary} height="20px" width="20px" /> : <Trans>Accept</Trans>}
|
||||
is1155={asset.tokenType === NftStandard.Erc1155}
|
||||
/>
|
||||
))
|
||||
return <TableContentComponent headerRow={headers} contentRows={contentRows} type={TableTabsKeys.Offers} />
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ScrollBarStyles } from 'components/Common'
|
||||
import { useSubscribeScrollState } from 'nft/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { TableTabsKeys } from './DataPageTable'
|
||||
import { Scrim } from './shared'
|
||||
|
||||
const TableRowsContainer = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const TableRowScrollableContainer = styled.div`
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 264px;
|
||||
|
||||
${ScrollBarStyles}
|
||||
`
|
||||
|
||||
const TableHeaderRowContainer = styled.div<{ userCanScroll: boolean }>`
|
||||
margin-right: ${({ userCanScroll }) => (userCanScroll ? '11px' : '0')};
|
||||
`
|
||||
|
||||
const TableRowContainer = styled.div`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`
|
||||
|
||||
interface TableContentComponentProps {
|
||||
headerRow: React.ReactNode
|
||||
contentRows: React.ReactNode[]
|
||||
type: TableTabsKeys
|
||||
}
|
||||
|
||||
export const TableContentComponent = ({ headerRow, contentRows, type }: TableContentComponentProps) => {
|
||||
const { userCanScroll, scrollRef, scrollProgress, scrollHandler } = useSubscribeScrollState()
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeaderRowContainer userCanScroll={userCanScroll}>{headerRow}</TableHeaderRowContainer>
|
||||
<TableRowsContainer>
|
||||
{scrollProgress > 0 && <Scrim />}
|
||||
<TableRowScrollableContainer ref={scrollRef} onScroll={scrollHandler}>
|
||||
{contentRows.map((row, index) => (
|
||||
<TableRowContainer key={type + '_row_' + index}>{row}</TableRowContainer>
|
||||
))}
|
||||
</TableRowScrollableContainer>
|
||||
{userCanScroll && scrollProgress !== 100 && <Scrim isBottom={true} />}
|
||||
</TableRowsContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
156
src/nft/components/details/detailsV2/TableRowComponent.tsx
Normal file
156
src/nft/components/details/detailsV2/TableRowComponent.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
import Row from 'components/Row'
|
||||
import { OrderType } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useScreenSize } from 'hooks/useScreenSize'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { HomeSearchIcon } from 'nft/components/icons'
|
||||
import { Offer, SellOrder } from 'nft/types'
|
||||
import { formatEth, getMarketplaceIcon, timeUntil } from 'nft/utils'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ExternalLink, ThemedText } from 'theme'
|
||||
import { shortenAddress } from 'utils'
|
||||
|
||||
import { TableTabsKeys } from './DataPageTable'
|
||||
|
||||
const TableCell = styled.div<{ $flex?: number; $justifyContent?: string; $color?: string; hideOnSmall?: boolean }>`
|
||||
display: flex;
|
||||
flex: ${({ $flex }) => $flex ?? 1};
|
||||
justify-content: ${({ $justifyContent }) => $justifyContent};
|
||||
color: ${({ $color }) => $color};
|
||||
flex-shrink: 0;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
display: ${({ hideOnSmall }) => (hideOnSmall ? 'none' : 'flex')};
|
||||
}
|
||||
`
|
||||
|
||||
const ActionButton = styled.div`
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
${OpacityHoverState}
|
||||
`
|
||||
|
||||
const USDPrice = styled(ThemedText.BodySmall)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
line-height: 20px;
|
||||
|
||||
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: ${BREAKPOINTS.lg}px) and (max-width: ${BREAKPOINTS.xl - 1}px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Link = styled(ExternalLink)`
|
||||
height: 20px;
|
||||
`
|
||||
|
||||
const PriceCell = ({ price }: { price: number }) => {
|
||||
const { chainId } = useWeb3React()
|
||||
const nativeCurrency = useNativeCurrency(chainId)
|
||||
const parsedAmount = tryParseCurrencyAmount(price.toString(), nativeCurrency)
|
||||
const usdValue = useStablecoinValue(parsedAmount)
|
||||
return (
|
||||
<Row gap="8px">
|
||||
<ThemedText.LabelSmall color="textPrimary" lineHeight="16px">
|
||||
{formatEth(price)}
|
||||
</ThemedText.LabelSmall>
|
||||
<USDPrice>{formatCurrencyAmount(usdValue, NumberType.FiatTokenPrice)}</USDPrice>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export const HeaderRow = ({ type, is1155 }: { type: TableTabsKeys; is1155?: boolean }) => {
|
||||
const screenSize = useScreenSize()
|
||||
const isMobile = !screenSize['sm']
|
||||
const isLargeScreen = screenSize['lg'] && !screenSize['xl']
|
||||
const reducedPriceWidth = isMobile || isLargeScreen
|
||||
|
||||
return (
|
||||
<Row gap="12px" padding="6px 6px 6px 0px">
|
||||
<HomeSearchIcon />
|
||||
<TableCell $flex={reducedPriceWidth ? 1 : 1.75}>
|
||||
<ThemedText.SubHeaderSmall color="textSecondary">
|
||||
<Trans>Price</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</TableCell>
|
||||
{is1155 && (
|
||||
<TableCell $flex={0.5}>
|
||||
<ThemedText.SubHeaderSmall color="textSecondary">
|
||||
<Trans>Quantity</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</TableCell>
|
||||
)}
|
||||
{(type === TableTabsKeys.Offers || is1155) && (
|
||||
<TableCell hideOnSmall={true}>
|
||||
<ThemedText.SubHeaderSmall color="textSecondary">
|
||||
{type === TableTabsKeys.Offers ? <Trans>From</Trans> : <Trans>Seller</Trans>}
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell $justifyContent="flex-end">
|
||||
<ThemedText.SubHeaderSmall color="textSecondary">
|
||||
<Trans>Expires in</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</TableCell>
|
||||
{/* An empty cell is needed in the headers for proper vertical alignment with the action buttons */}
|
||||
<TableCell $flex={isMobile ? 0.25 : 1}> </TableCell>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export const ContentRow = ({
|
||||
content,
|
||||
buttonCTA,
|
||||
is1155,
|
||||
}: {
|
||||
content: Offer | SellOrder
|
||||
buttonCTA: React.ReactNode
|
||||
is1155?: boolean
|
||||
}) => {
|
||||
const screenSize = useScreenSize()
|
||||
const isMobile = !screenSize['sm']
|
||||
const date = content.endAt && new Date(content.endAt)
|
||||
const isSellOrder = 'type' in content && content.type === OrderType.Listing
|
||||
const reducedPriceWidth = isMobile || (screenSize['lg'] && !screenSize['xl'])
|
||||
|
||||
return (
|
||||
<Row gap="12px" padding="16px 6px 16px 0px">
|
||||
<Link href={content.marketplaceUrl}>{getMarketplaceIcon(content.marketplace, '20')}</Link>
|
||||
{content.price && (
|
||||
<TableCell $flex={reducedPriceWidth ? 1 : 1.75}>
|
||||
<PriceCell price={content.price.value} />
|
||||
</TableCell>
|
||||
)}
|
||||
{is1155 && (
|
||||
<TableCell $flex={0.5} $justifyContent="center">
|
||||
<ThemedText.SubHeaderSmall color="textPrimary">{content.quantity}</ThemedText.SubHeaderSmall>
|
||||
</TableCell>
|
||||
)}
|
||||
{(!isSellOrder || is1155) && (
|
||||
<TableCell hideOnSmall={true}>
|
||||
<Link href={`https://etherscan.io/address/${content.maker}`}>
|
||||
<ThemedText.LabelSmall color="textPrimary">{shortenAddress(content.maker)}</ThemedText.LabelSmall>
|
||||
</Link>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell $justifyContent="flex-end">
|
||||
<ThemedText.LabelSmall color="textPrimary">
|
||||
{date ? timeUntil(date) : <Trans>Never</Trans>}
|
||||
</ThemedText.LabelSmall>
|
||||
</TableCell>
|
||||
<TableCell $flex={isMobile ? 0.25 : 1} $justifyContent="center">
|
||||
<ActionButton>
|
||||
<ThemedText.LabelSmall color="textSecondary">{buttonCTA}</ThemedText.LabelSmall>
|
||||
</ActionButton>
|
||||
</TableCell>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { Trait } from 'nft/types'
|
||||
import { formatEth } from 'nft/utils'
|
||||
import qs from 'qs'
|
||||
import { formatEth, getLinkForTrait } from 'nft/utils'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
@@ -25,7 +24,7 @@ const SubheaderTinyHidden = styled(SubheaderTiny)`
|
||||
`
|
||||
|
||||
const TraitRowContainer = styled(Row)`
|
||||
padding: 12px 18px 12px 0px;
|
||||
padding: 12px 18px 12px 12px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
@@ -60,14 +59,9 @@ export const TraitRow = ({ trait, collectionAddress }: { trait: Trait; collectio
|
||||
// rarity eventually should be number of items with this trait / total number of items, smaller rarity means more rare
|
||||
const randomRarity = Math.random()
|
||||
const rarityLevel = getRarityLevel(randomRarity)
|
||||
const params = qs.stringify(
|
||||
{ traits: [`("${trait.trait_type}","${trait.trait_value}")`] },
|
||||
{
|
||||
arrayFormat: 'comma',
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<TraitRowLink to={`/nfts/collection/${collectionAddress}?${params}`}>
|
||||
<TraitRowLink to={getLinkForTrait(trait, collectionAddress)}>
|
||||
<TraitRowContainer>
|
||||
<TraitColumnValue>
|
||||
<SubheaderTiny>{trait.trait_type}</SubheaderTiny>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
exports[`placeholder containers load 1`] = `
|
||||
<DocumentFragment>
|
||||
.c2 {
|
||||
.c3 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
.c11 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
@@ -18,7 +18,7 @@ exports[`placeholder containers load 1`] = `
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
.c13 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
@@ -41,7 +41,7 @@ exports[`placeholder containers load 1`] = `
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
.c4 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -58,7 +58,7 @@ exports[`placeholder containers load 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
.c8 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -76,7 +76,7 @@ exports[`placeholder containers load 1`] = `
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
.c12 {
|
||||
width: -webkit-min-content;
|
||||
width: -moz-min-content;
|
||||
width: min-content;
|
||||
@@ -96,7 +96,7 @@ exports[`placeholder containers load 1`] = `
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
.c22 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -114,15 +114,15 @@ exports[`placeholder containers load 1`] = `
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
.c9 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
.c10 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
@@ -136,12 +136,13 @@ exports[`placeholder containers load 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
.c26 {
|
||||
height: 568px;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
.c19 {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D2D9EE;
|
||||
border-radius: 16px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
@@ -150,33 +151,33 @@ exports[`placeholder containers load 1`] = `
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
.c20 {
|
||||
gap: 32px;
|
||||
margin-bottom: 12px;
|
||||
width: 100;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
.c21 {
|
||||
color: #0D111C;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c20:hover {
|
||||
.c21:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
.c23 {
|
||||
color: #98A1C0;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c22:hover {
|
||||
.c23:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
.c25 {
|
||||
background: #D2D9EE;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
@@ -184,11 +185,11 @@ exports[`placeholder containers load 1`] = `
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
.c24 {
|
||||
height: 252px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
.c14 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
@@ -226,56 +227,56 @@ exports[`placeholder containers load 1`] = `
|
||||
transform: perspective(1px) translateZ(0);
|
||||
}
|
||||
|
||||
.c13:disabled {
|
||||
.c14:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c13 > * {
|
||||
.c14 > * {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c13 > a {
|
||||
.c14 > a {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
.c15 {
|
||||
background-color: #F5F6FC;
|
||||
color: #7780A0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.c14:hover {
|
||||
.c15:hover {
|
||||
background-color: #d2daf7;
|
||||
}
|
||||
|
||||
.c14:active {
|
||||
.c15:active {
|
||||
background-color: #bdc8f3;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
.c5 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c6 {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 20px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c7 {
|
||||
gap: 4px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
.c16 {
|
||||
white-space: nowrap;
|
||||
width: -webkit-min-content;
|
||||
width: -moz-min-content;
|
||||
@@ -286,21 +287,26 @@ exports[`placeholder containers load 1`] = `
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
.c0 {
|
||||
padding: 24px 64px;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
gap: 36px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
.c17 {
|
||||
gap: 24px;
|
||||
padding-bottom: 45px;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
.c18 {
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
-webkit-align-self: flex-start;
|
||||
@@ -309,31 +315,37 @@ exports[`placeholder containers load 1`] = `
|
||||
}
|
||||
|
||||
@media screen and (max-width:768px) {
|
||||
.c4 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:1024px) {
|
||||
.c5 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:1024px) {
|
||||
.c6 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:768px) {
|
||||
.c0 {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:640px) {
|
||||
.c1 {
|
||||
.c0 {
|
||||
padding: 24px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:396px) {
|
||||
.c1 {
|
||||
.c0 {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:1024px) {
|
||||
.c16 {
|
||||
.c17 {
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
@@ -341,146 +353,145 @@ exports[`placeholder containers load 1`] = `
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1"
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<img
|
||||
class="c5"
|
||||
src="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/3318/50ed67ad647d0aa0cad0b830d136a677efc2fb72a44587bc35f2a5fb334a7fdf.png"
|
||||
/>
|
||||
<div
|
||||
class="c0 c6"
|
||||
>
|
||||
<div
|
||||
class="c2 c7"
|
||||
>
|
||||
<div
|
||||
class="c8 css-1aekuku"
|
||||
>
|
||||
Azuki
|
||||
</div>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16px"
|
||||
viewBox="0 0 20 20"
|
||||
width="16px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.52795 13.8056C4.52719 14.4043 4.6712 14.8474 4.95997 15.135C5.24798 15.4233 5.68496 15.5651 6.27091 15.5605H7.57497C7.62945 15.5585 7.68379 15.5676 7.73463 15.5873C7.78547 15.607 7.83176 15.6369 7.87062 15.6752L8.79884 16.5928C9.22054 17.0142 9.63382 17.2237 10.0387 17.2214C10.4436 17.2191 10.8569 17.0096 11.2786 16.5928L12.1954 15.6752C12.2356 15.6365 12.2832 15.6063 12.3354 15.5866C12.3876 15.5669 12.4433 15.558 12.499 15.5605H13.7951C14.3871 15.5613 14.8283 15.4171 15.1186 15.1281C15.4089 14.839 15.5541 14.3959 15.5541 13.7987V12.5014C15.5511 12.389 15.5923 12.2799 15.6687 12.1974L16.5854 11.2798C17.0125 10.86 17.2245 10.4467 17.2214 10.0399C17.2184 9.63305 17.0064 9.21935 16.5854 8.79878L15.6687 7.88115C15.592 7.79886 15.5509 7.68965 15.5541 7.57719V6.2799C15.5533 5.68191 15.4093 5.23878 15.1221 4.95049C14.8348 4.66221 14.3925 4.51806 13.7951 4.51806H12.499C12.4433 4.52036 12.3877 4.51138 12.3355 4.49168C12.2834 4.47197 12.2357 4.44193 12.1954 4.40336L11.2786 3.48574C10.8569 3.06439 10.4436 2.85487 10.0387 2.85717C9.63382 2.85946 9.22054 3.06898 8.79884 3.48574L7.87062 4.40336C7.83164 4.44148 7.78536 4.4713 7.73454 4.49101C7.68373 4.51072 7.62943 4.51993 7.57497 4.51806H6.27091C5.67961 4.51883 5.23995 4.66182 4.95194 4.94705C4.66393 5.23228 4.51992 5.67656 4.51992 6.2799V7.58063C4.52314 7.69309 4.48197 7.80229 4.40533 7.88459L3.48859 8.80222C3.06765 9.22203 2.85718 9.63572 2.85718 10.0433C2.85718 10.4509 3.07033 10.8653 3.49662 11.2867L4.41336 12.2043C4.48979 12.2867 4.53092 12.3958 4.52795 12.5083V13.8056Z"
|
||||
fill="#FB118E"
|
||||
/>
|
||||
<path
|
||||
d="M9.99737 12.4943C9.86205 12.7005 9.6623 12.8164 9.43032 12.8164C9.19191 12.8164 9.00504 12.7198 8.83106 12.4943L7.31036 10.6385C7.20082 10.5032 7.14282 10.3614 7.14282 10.2068C7.14282 9.88458 7.38768 9.63327 7.70342 9.63327C7.89673 9.63327 8.05138 9.70415 8.20603 9.90391L9.40455 11.4311L11.9498 7.34577C12.0851 7.12669 12.2591 7.02359 12.4524 7.02359C12.7553 7.02359 13.0388 7.23623 13.0388 7.55197C13.0388 7.70017 12.9615 7.85482 12.8777 7.99014L9.99737 12.4943Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-1tiu9da"
|
||||
>
|
||||
Azuki #3318
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 c11"
|
||||
width="min-content"
|
||||
>
|
||||
<button
|
||||
class="c12 c13 c14 c15"
|
||||
>
|
||||
Make an offer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c16"
|
||||
class="c1 c2"
|
||||
>
|
||||
<div
|
||||
class="c0 c17"
|
||||
class="c3 c4 c5"
|
||||
>
|
||||
<img
|
||||
class="c6"
|
||||
src="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/3318/50ed67ad647d0aa0cad0b830d136a677efc2fb72a44587bc35f2a5fb334a7fdf.png"
|
||||
/>
|
||||
<div
|
||||
class="c18"
|
||||
class="c1 c7"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c19"
|
||||
class="c3 c8"
|
||||
>
|
||||
<div
|
||||
class="c9 c20 css-rjqmed"
|
||||
class="c9 css-1aekuku"
|
||||
>
|
||||
<div
|
||||
class="c2 c21"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
Azuki
|
||||
</div>
|
||||
<div
|
||||
class="c9 c22 css-rjqmed"
|
||||
<svg
|
||||
fill="none"
|
||||
height="16px"
|
||||
viewBox="0 0 20 20"
|
||||
width="16px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<div
|
||||
class="c2 c21"
|
||||
>
|
||||
Details
|
||||
</div>
|
||||
</div>
|
||||
<path
|
||||
d="M4.52795 13.8056C4.52719 14.4043 4.6712 14.8474 4.95997 15.135C5.24798 15.4233 5.68496 15.5651 6.27091 15.5605H7.57497C7.62945 15.5585 7.68379 15.5676 7.73463 15.5873C7.78547 15.607 7.83176 15.6369 7.87062 15.6752L8.79884 16.5928C9.22054 17.0142 9.63382 17.2237 10.0387 17.2214C10.4436 17.2191 10.8569 17.0096 11.2786 16.5928L12.1954 15.6752C12.2356 15.6365 12.2832 15.6063 12.3354 15.5866C12.3876 15.5669 12.4433 15.558 12.499 15.5605H13.7951C14.3871 15.5613 14.8283 15.4171 15.1186 15.1281C15.4089 14.839 15.5541 14.3959 15.5541 13.7987V12.5014C15.5511 12.389 15.5923 12.2799 15.6687 12.1974L16.5854 11.2798C17.0125 10.86 17.2245 10.4467 17.2214 10.0399C17.2184 9.63305 17.0064 9.21935 16.5854 8.79878L15.6687 7.88115C15.592 7.79886 15.5509 7.68965 15.5541 7.57719V6.2799C15.5533 5.68191 15.4093 5.23878 15.1221 4.95049C14.8348 4.66221 14.3925 4.51806 13.7951 4.51806H12.499C12.4433 4.52036 12.3877 4.51138 12.3355 4.49168C12.2834 4.47197 12.2357 4.44193 12.1954 4.40336L11.2786 3.48574C10.8569 3.06439 10.4436 2.85487 10.0387 2.85717C9.63382 2.85946 9.22054 3.06898 8.79884 3.48574L7.87062 4.40336C7.83164 4.44148 7.78536 4.4713 7.73454 4.49101C7.68373 4.51072 7.62943 4.51993 7.57497 4.51806H6.27091C5.67961 4.51883 5.23995 4.66182 4.95194 4.94705C4.66393 5.23228 4.51992 5.67656 4.51992 6.2799V7.58063C4.52314 7.69309 4.48197 7.80229 4.40533 7.88459L3.48859 8.80222C3.06765 9.22203 2.85718 9.63572 2.85718 10.0433C2.85718 10.4509 3.07033 10.8653 3.49662 11.2867L4.41336 12.2043C4.48979 12.2867 4.53092 12.3958 4.52795 12.5083V13.8056Z"
|
||||
fill="#FB118E"
|
||||
/>
|
||||
<path
|
||||
d="M9.99737 12.4943C9.86205 12.7005 9.6623 12.8164 9.43032 12.8164C9.19191 12.8164 9.00504 12.7198 8.83106 12.4943L7.31036 10.6385C7.20082 10.5032 7.14282 10.3614 7.14282 10.2068C7.14282 9.88458 7.38768 9.63327 7.70342 9.63327C7.89673 9.63327 8.05138 9.70415 8.20603 9.90391L9.40455 11.4311L11.9498 7.34577C12.0851 7.12669 12.2591 7.02359 12.4524 7.02359C12.7553 7.02359 13.0388 7.23623 13.0388 7.55197C13.0388 7.70017 12.9615 7.85482 12.8777 7.99014L9.99737 12.4943Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="c23"
|
||||
class="c10 css-1tiu9da"
|
||||
>
|
||||
Description Content
|
||||
Azuki #3318
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c11 c12"
|
||||
width="min-content"
|
||||
>
|
||||
<button
|
||||
class="c13 c14 c15 c16"
|
||||
>
|
||||
Make an offer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18"
|
||||
class="c3 c4 c17"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c19"
|
||||
class="c1 c18"
|
||||
>
|
||||
<div
|
||||
class="c9 c20 css-rjqmed"
|
||||
class="c19"
|
||||
>
|
||||
<div
|
||||
class="c2 c21"
|
||||
class="c3 c4 c20"
|
||||
>
|
||||
Activity
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 c22 css-rjqmed"
|
||||
>
|
||||
<div
|
||||
class="c2 c21"
|
||||
>
|
||||
Offers
|
||||
<div
|
||||
class="c24 css-f8aq60"
|
||||
class="c10 c21 css-rjqmed"
|
||||
>
|
||||
10+
|
||||
<div
|
||||
class="c3 c22"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 c23 css-rjqmed"
|
||||
>
|
||||
<div
|
||||
class="c3 c22"
|
||||
>
|
||||
Details
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 c22 css-rjqmed"
|
||||
>
|
||||
<div
|
||||
class="c2 c21"
|
||||
class="c24"
|
||||
>
|
||||
Listings
|
||||
<div
|
||||
class="c24 css-f8aq60"
|
||||
>
|
||||
10+
|
||||
</div>
|
||||
Description Content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c25"
|
||||
class="c19"
|
||||
>
|
||||
Activity Content
|
||||
<div
|
||||
class="c3 c4 c20"
|
||||
>
|
||||
<div
|
||||
class="c10 c21 css-rjqmed"
|
||||
>
|
||||
<div
|
||||
class="c3 c22"
|
||||
>
|
||||
Activity
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 c23 css-rjqmed"
|
||||
>
|
||||
<div
|
||||
class="c3 c22"
|
||||
>
|
||||
Offers
|
||||
<div
|
||||
class="c25 css-f8aq60"
|
||||
>
|
||||
10+
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 c23 css-rjqmed"
|
||||
>
|
||||
<div
|
||||
class="c3 c22"
|
||||
>
|
||||
Listings
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c26"
|
||||
>
|
||||
Activity Content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@ exports[`data page trait component does not load with asset with no traits 1`] =
|
||||
|
||||
.c0 {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D2D9EE;
|
||||
border-radius: 16px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
@@ -88,7 +89,7 @@ exports[`data page trait component does not load with asset with no traits 1`] =
|
||||
}
|
||||
|
||||
.c8 {
|
||||
padding-right: 12px;
|
||||
padding: 0px 12px;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
|
||||
@@ -26,6 +26,81 @@ exports[`LandingPage renders it correctly 1`] = `
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
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: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
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: 8px;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
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;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
-webkit-transition-duration: 125ms;
|
||||
transition-duration: 125ms;
|
||||
color: #FB118E;
|
||||
stroke: #FB118E;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.c19:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.c19:active {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -40,6 +115,21 @@ exports[`LandingPage renders it correctly 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
width: 100%;
|
||||
-webkit-align-items: center;
|
||||
@@ -48,6 +138,50 @@ exports[`LandingPage renders it correctly 1`] = `
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
background-color: #FFFFFF;
|
||||
padding: 10px 12px 10px 8px;
|
||||
border-radius: 20px;
|
||||
max-width: 144px;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: #0D111C;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
-webkit-flex-shrink: 0;
|
||||
-ms-flex-negative: 0;
|
||||
flex-shrink: 0;
|
||||
color: #FB118E;
|
||||
border-radius: 100%;
|
||||
overflow: hidden;
|
||||
-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;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
position: relative;
|
||||
object-fit: contain;
|
||||
@@ -128,6 +262,21 @@ exports[`LandingPage renders it correctly 1`] = `
|
||||
filter: drop-shadow(0px 12px 20px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
@media screen and (min-width:640px) {
|
||||
.c21 {
|
||||
max-width: 169px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:640px) {
|
||||
.c17 {
|
||||
gap: 12px;
|
||||
-webkit-flex-wrap: nowrap;
|
||||
-ms-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width:1280px) {
|
||||
.c4 {
|
||||
-webkit-filter: blur(50px);
|
||||
@@ -259,6 +408,40 @@ exports[`LandingPage renders it correctly 1`] = `
|
||||
Azuki #3318
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<div
|
||||
class="c9 c16 c17"
|
||||
>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<div
|
||||
class="c18 css-4u0e4f"
|
||||
>
|
||||
Owner
|
||||
</div>
|
||||
<a
|
||||
class="c19"
|
||||
href="https://etherscan.io/address/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="c9 c20 c21"
|
||||
>
|
||||
<div
|
||||
class="c9 c22 c23"
|
||||
/>
|
||||
<div
|
||||
class="c24"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { opacify } from 'theme/utils'
|
||||
|
||||
export const containerStyles = css`
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
padding: 16px 20px;
|
||||
width: 100%;
|
||||
@@ -11,3 +13,24 @@ export const containerStyles = css`
|
||||
export const TableContentContainer = styled.div`
|
||||
height: 568px;
|
||||
`
|
||||
|
||||
// Scrim that fades out the top and bottom of the scrollable container, isBottom changes the direction and placement of the fade
|
||||
export const Scrim = styled.div<{ isBottom?: boolean }>`
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
height: 88px;
|
||||
left: 0px;
|
||||
right: 6px;
|
||||
|
||||
${({ isBottom }) =>
|
||||
isBottom
|
||||
? 'bottom: 0px'
|
||||
: `
|
||||
top: 0px;
|
||||
transform: matrix(1, 0, 0, -1, 0, 0);
|
||||
`};
|
||||
|
||||
background: ${({ theme }) =>
|
||||
`linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`};
|
||||
display: flex;
|
||||
`
|
||||
|
||||
@@ -1351,6 +1351,35 @@ export const UniswapMagentaIcon = (props: SVGProps) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const HomeSearchIcon = (props: SVGProps) => (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M17.898 7.57097L11.7212 2.49102C10.7237 1.67268 9.27795 1.67185 8.28045 2.49102L2.10379 7.57016C1.83796 7.78932 1.79877 8.18268 2.01794 8.45018C2.2371 8.71768 2.63213 8.75437 2.89796 8.53604L3.54209 8.00605V15.0002C3.54209 17.0152 4.65209 18.1252 6.66709 18.1252H13.3338C15.3488 18.1252 16.4588 17.0152 16.4588 15.0002V8.00605L17.1029 8.53604C17.2195 8.63187 17.3604 8.67845 17.5004 8.67845C17.6804 8.67845 17.8596 8.601 17.9829 8.451C18.2029 8.1835 18.1638 7.79014 17.898 7.57097ZM15.2088 15.0002C15.2088 16.3143 14.6479 16.8752 13.3338 16.8752H6.66709C5.35292 16.8752 4.79209 16.3143 4.79209 15.0002V6.97852L9.07462 3.45771C9.61045 3.01688 10.3913 3.01688 10.9271 3.45771L15.2096 6.97934V15.0002H15.2088ZM6.45875 10.7643C6.45875 12.4493 7.82958 13.8202 9.51458 13.8202C10.1312 13.8202 10.7038 13.6335 11.1838 13.3176L12.4746 14.6085C12.5962 14.7302 12.7563 14.7918 12.9163 14.7918C13.0763 14.7918 13.2363 14.731 13.358 14.6085C13.6021 14.3644 13.6021 13.9685 13.358 13.7243L12.0663 12.4326C12.3813 11.9518 12.568 11.3794 12.568 10.7627C12.568 9.07854 11.1971 7.70688 9.51295 7.70688C7.82962 7.70854 6.45875 9.07933 6.45875 10.7643ZM11.3196 10.7643C11.3196 11.7602 10.5096 12.5702 9.51458 12.5702C8.51875 12.5702 7.70875 11.7602 7.70875 10.7643C7.70875 9.7685 8.51875 8.9585 9.51458 8.9585C10.5096 8.9585 11.3196 9.7685 11.3196 10.7643Z"
|
||||
fill="#7780A0"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const AddToBagIcon = (props: SVGProps) => (
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M8.51389 18.25H5.44444C4.6467 18.25 4 17.653 4 16.9167V7.58333C4 6.84695 4.6467 6.25 5.44444 6.25H14.5556C15.3533 6.25 16 6.84695 16 7.58333V10.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7 6.25L7 5.45C7 4.60131 7.31607 3.78737 7.87868 3.18726C8.44129 2.58714 9.20435 2.25 10 2.25C10.7956 2.25 11.5587 2.58714 12.1213 3.18726C12.6839 3.78737 13 4.60131 13 5.45L13 6.25"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M11 15.25H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M14 12.25L14 18.25" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
export const HandHoldingDollarIcon = (props: SVGProps) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function useDerivedPayWithAnyTokenSwapInfo(
|
||||
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
|
||||
): {
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade: InterfaceTrade | undefined
|
||||
maximumAmountIn: CurrencyAmount<Token> | undefined
|
||||
allowedSlippage: Percent
|
||||
} {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { PermitInput, TokenTradeRoutesInput, TokenTradeType } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Allowance } from 'hooks/usePermit2Allowance'
|
||||
import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
|
||||
@@ -8,7 +8,7 @@ import { InterfaceTrade } from 'state/routing/types'
|
||||
import { useTokenInput } from './useTokenInput'
|
||||
|
||||
export default function usePayWithAnyTokenSwap(
|
||||
trade?: InterfaceTrade<Currency, Currency, TradeType> | undefined,
|
||||
trade?: InterfaceTrade | undefined,
|
||||
allowance?: Allowance,
|
||||
allowedSlippage?: Percent
|
||||
) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
@@ -14,7 +14,7 @@ interface PriceImpactSeverity {
|
||||
color: string
|
||||
}
|
||||
|
||||
export function usePriceImpact(trade?: InterfaceTrade<Currency, Currency, TradeType>): PriceImpact | undefined {
|
||||
export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined {
|
||||
const theme = useTheme()
|
||||
|
||||
return useMemo(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MediaType, NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { MediaType, NftMarketplace, NftStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SortBy } from 'nft/hooks'
|
||||
|
||||
import { SellOrder } from '../sell'
|
||||
@@ -79,6 +79,21 @@ export interface Trait {
|
||||
trait_count?: number
|
||||
order?: any
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
createdAt: number
|
||||
endAt?: number
|
||||
id: string
|
||||
maker: string
|
||||
marketplace: NftMarketplace
|
||||
marketplaceUrl: string
|
||||
price: {
|
||||
currency?: string
|
||||
value: number
|
||||
}
|
||||
quantity?: number
|
||||
}
|
||||
|
||||
export interface GenieAsset {
|
||||
id?: string // This would be a random id created and assigned by front end
|
||||
address: string
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
SquareSudoSwapMarketplaceIcon,
|
||||
SquareZoraMarketplaceIcon,
|
||||
} from 'nft/components/icons'
|
||||
import { DetailsOrigin, GenieAsset, Listing, Markets, UpdatedGenieAsset, WalletAsset } from 'nft/types'
|
||||
import { DetailsOrigin, GenieAsset, Listing, Markets, Trait, UpdatedGenieAsset, WalletAsset } from 'nft/types'
|
||||
import qs from 'qs'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export function getRarityStatus(
|
||||
@@ -125,3 +126,14 @@ export const generateTweetForList = (assets: WalletAsset[]): string => {
|
||||
.join(', ')} \n\nMarketplaces: ${assets[0].marketplaces?.map((market) => market.name).join(', ')}`
|
||||
return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}`
|
||||
}
|
||||
|
||||
export function getLinkForTrait(trait: Trait, collectionAddress: string): string {
|
||||
const params = qs.stringify(
|
||||
{ traits: [`("${trait.trait_type}","${trait.trait_value}")`] },
|
||||
{
|
||||
arrayFormat: 'comma',
|
||||
}
|
||||
)
|
||||
|
||||
return `/nfts/collection/${collectionAddress}?${params}`
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ export * from './numbers'
|
||||
export * from './pooledAssets'
|
||||
export * from './putCommas'
|
||||
export * from './roundAndPluralize'
|
||||
export * from './timeSince'
|
||||
export * from './time'
|
||||
export * from './transactionResponse'
|
||||
export * from './updatedAssets'
|
||||
|
||||
58
src/nft/utils/time.test.ts
Normal file
58
src/nft/utils/time.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { i18n } from '@lingui/core'
|
||||
import { DEFAULT_LOCALE } from 'constants/locales'
|
||||
import catalog from 'locales/en-US'
|
||||
import { en } from 'make-plural'
|
||||
|
||||
import { timeUntil } from './time'
|
||||
|
||||
describe('timeUntil', () => {
|
||||
const originalDate = new Date('2023-06-01T00:00:00.000Z')
|
||||
i18n.load({
|
||||
[DEFAULT_LOCALE]: catalog.messages,
|
||||
})
|
||||
i18n.loadLocaleData({
|
||||
[DEFAULT_LOCALE]: { plurals: en },
|
||||
})
|
||||
i18n.activate(DEFAULT_LOCALE)
|
||||
|
||||
test('returns undefined when date is in the past', () => {
|
||||
const pastDate = new Date('2022-01-01T00:00:00.000Z')
|
||||
expect(timeUntil(pastDate, originalDate)).toBeUndefined()
|
||||
})
|
||||
|
||||
test('returns the correct time until in months', () => {
|
||||
const futureDate = new Date('2023-09-01T00:00:00.000Z')
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('3 months')
|
||||
})
|
||||
|
||||
test('returns the correct time until in weeks', () => {
|
||||
const futureDate = new Date('2023-06-20T00:00:00.000Z')
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('2 weeks')
|
||||
})
|
||||
|
||||
test('returns the correct time until in days', () => {
|
||||
const futureDate = new Date('2023-06-03T12:00:00.000Z')
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('2 days')
|
||||
})
|
||||
|
||||
test('returns the correct time untwil in hours', () => {
|
||||
const futureDate = new Date('2023-06-01T05:00:00.000Z')
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('5 hours')
|
||||
})
|
||||
|
||||
test('returns the correct time until in minutes', () => {
|
||||
const futureDate = new Date('2023-06-01T00:05:00.000Z')
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('5 minutes')
|
||||
})
|
||||
|
||||
test('returns the correct time until in seconds', () => {
|
||||
const futureDate = new Date('2023-06-01T00:00:05.000Z')
|
||||
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('5 seconds')
|
||||
})
|
||||
|
||||
test('returns 99+ months for large intervals', () => {
|
||||
const futureDate = new Date('2123-01-01T00:00:00.000Z')
|
||||
expect(timeUntil(futureDate, originalDate)).toEqual('99+ months')
|
||||
})
|
||||
})
|
||||
98
src/nft/utils/time.ts
Normal file
98
src/nft/utils/time.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { plural, t } from '@lingui/macro'
|
||||
import ms from 'ms.macro'
|
||||
|
||||
import { roundAndPluralize } from './roundAndPluralize'
|
||||
|
||||
const SECOND = ms`1s`
|
||||
const MINUTE = ms`1m`
|
||||
const HOUR = ms`1h`
|
||||
const DAY = ms`1d`
|
||||
const WEEK = ms`7d`
|
||||
const MONTH = ms`30d`
|
||||
|
||||
interface TimePeriod {
|
||||
milliseconds: number
|
||||
pluralLabel: (i: number) => string
|
||||
}
|
||||
|
||||
const timePeriods: TimePeriod[] = [
|
||||
{
|
||||
milliseconds: MONTH,
|
||||
pluralLabel: (i: number) =>
|
||||
plural(i, {
|
||||
one: 'month',
|
||||
other: 'months',
|
||||
}),
|
||||
},
|
||||
{
|
||||
milliseconds: WEEK,
|
||||
pluralLabel: (i: number) =>
|
||||
plural(i, {
|
||||
one: 'week',
|
||||
other: 'weeks',
|
||||
}),
|
||||
},
|
||||
{
|
||||
milliseconds: DAY,
|
||||
pluralLabel: (i: number) =>
|
||||
plural(i, {
|
||||
one: 'day',
|
||||
other: 'days',
|
||||
}),
|
||||
},
|
||||
{
|
||||
milliseconds: HOUR,
|
||||
pluralLabel: (i: number) =>
|
||||
plural(i, {
|
||||
one: 'hour',
|
||||
other: 'hours',
|
||||
}),
|
||||
},
|
||||
{
|
||||
milliseconds: MINUTE,
|
||||
pluralLabel: (i: number) =>
|
||||
plural(i, {
|
||||
one: 'minute',
|
||||
other: 'minutes',
|
||||
}),
|
||||
},
|
||||
{
|
||||
milliseconds: SECOND,
|
||||
pluralLabel: (i: number) =>
|
||||
plural(i, {
|
||||
one: 'second',
|
||||
other: 'seconds',
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
export function timeUntil(date: Date, originalDate?: Date): string | undefined {
|
||||
const referenceDate = originalDate ?? new Date()
|
||||
|
||||
const milliseconds = date.getTime() - referenceDate.getTime()
|
||||
|
||||
if (milliseconds < 0) return undefined
|
||||
|
||||
const monthInterval = milliseconds / MONTH
|
||||
if (monthInterval >= 100) return `99+ ${t`months`}`
|
||||
|
||||
for (const period of timePeriods) {
|
||||
const interval = milliseconds / period.milliseconds
|
||||
|
||||
if (interval >= 1) {
|
||||
return `${Math.floor(interval)} ${period.pluralLabel(interval)}`
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const timeLeft = (targetDate: Date): string => {
|
||||
const countDown = new Date(targetDate).getTime() - new Date().getTime()
|
||||
const days = Math.floor(countDown / DAY)
|
||||
const hours = Math.floor((countDown % DAY) / HOUR)
|
||||
const minutes = Math.floor((countDown % HOUR) / MINUTE)
|
||||
|
||||
return `${days !== 0 ? roundAndPluralize(days, 'day') : ''} ${
|
||||
hours !== 0 ? roundAndPluralize(hours, 'hour') : ''
|
||||
} ${roundAndPluralize(minutes, 'minute')}`
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { roundAndPluralize } from './roundAndPluralize'
|
||||
|
||||
export function timeSince(date: Date, min?: boolean) {
|
||||
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000)
|
||||
|
||||
let interval = seconds / 31536000
|
||||
|
||||
if (interval > 1) return roundAndPluralize(interval, min ? 'yr' : 'year')
|
||||
|
||||
interval = seconds / 2592000
|
||||
if (interval > 1) return roundAndPluralize(interval, min ? 'mth' : 'month')
|
||||
|
||||
interval = seconds / 86400
|
||||
if (interval > 1) return roundAndPluralize(interval, 'day')
|
||||
|
||||
interval = seconds / 3600
|
||||
|
||||
if (interval > 1) return roundAndPluralize(interval, min ? 'hr' : 'hour')
|
||||
|
||||
interval = seconds / 60
|
||||
if (interval > 1) return roundAndPluralize(interval, 'min')
|
||||
|
||||
return roundAndPluralize(interval, 'sec')
|
||||
}
|
||||
|
||||
const MINUTE = 1000 * 60
|
||||
const HOUR = MINUTE * 60
|
||||
const DAY = 24 * HOUR
|
||||
|
||||
export const timeLeft = (targetDate: Date): string => {
|
||||
const countDown = new Date(targetDate).getTime() - new Date().getTime()
|
||||
const days = Math.floor(countDown / DAY)
|
||||
const hours = Math.floor((countDown % DAY) / HOUR)
|
||||
const minutes = Math.floor((countDown % HOUR) / MINUTE)
|
||||
|
||||
return `${days !== 0 ? roundAndPluralize(days, 'day') : ''} ${
|
||||
hours !== 0 ? roundAndPluralize(hours, 'hour') : ''
|
||||
} ${roundAndPluralize(minutes, 'minute')}`
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IRoute, Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { Pool } from '@uniswap/v3-sdk'
|
||||
import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks'
|
||||
@@ -108,7 +108,7 @@ function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAllTradeRouteInputs(trade: InterfaceTrade<Currency, Currency, TradeType>): {
|
||||
export function buildAllTradeRouteInputs(trade: InterfaceTrade): {
|
||||
mixedTokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
|
||||
v2TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
|
||||
v3TokenTradeRouteInputs: TokenTradeRouteInput[] | undefined
|
||||
|
||||
@@ -18,6 +18,7 @@ import { flexRowNoWrap } from 'theme/styles'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { STATSIG_DUMMY_KEY } from 'tracing'
|
||||
import { getEnvName } from 'utils/env'
|
||||
import { retry } from 'utils/retry'
|
||||
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
|
||||
|
||||
import { useAnalyticsReporter } from '../components/analytics'
|
||||
@@ -45,12 +46,12 @@ import Swap from './Swap'
|
||||
import { RedirectPathToSwapOnly } from './Swap/redirects'
|
||||
import Tokens from './Tokens'
|
||||
|
||||
const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
const Vote = lazy(() => import('./Vote'))
|
||||
const NftExplore = lazy(() => import('nft/pages/explore'))
|
||||
const Collection = lazy(() => import('nft/pages/collection'))
|
||||
const Profile = lazy(() => import('nft/pages/profile/profile'))
|
||||
const Asset = lazy(() => import('nft/pages/asset/Asset'))
|
||||
const TokenDetails = lazy(() => retry(() => import('./TokenDetails')))
|
||||
const Vote = lazy(() => retry(() => import('./Vote')))
|
||||
const NftExplore = lazy(() => retry(() => import('nft/pages/explore')))
|
||||
const Collection = lazy(() => retry(() => import('nft/pages/collection')))
|
||||
const Profile = lazy(() => retry(() => import('nft/pages/profile/profile')))
|
||||
const Asset = lazy(() => retry(() => import('nft/pages/asset/Asset')))
|
||||
|
||||
const BodyWrapper = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -46,7 +46,7 @@ import useIsArgentWallet from '../../hooks/useIsArgentWallet'
|
||||
import { useTotalSupply } from '../../hooks/useTotalSupply'
|
||||
import { useTokenBalance } from '../../state/connection/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { BackArrow, ExternalLink, ThemedText } from '../../theme'
|
||||
import { BackArrowLink, ExternalLink, ThemedText } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { calculateGasMargin } from '../../utils/calculateGasMargin'
|
||||
import { currencyId } from '../../utils/currencyId'
|
||||
@@ -725,7 +725,7 @@ export default function MigrateV2Pair() {
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<BackArrow to="/migrate/v2" />
|
||||
<BackArrowLink to="/migrate/v2" />
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Migrate V2 Liquidity</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Dots } from '../../components/swap/styleds'
|
||||
import { V2_FACTORY_ADDRESSES } from '../../constants/addresses'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/connection/hooks'
|
||||
import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks'
|
||||
import { BackArrow, StyledInternalLink, ThemedText } from '../../theme'
|
||||
import { BackArrowLink, StyledInternalLink, ThemedText } from '../../theme'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
|
||||
function EmptyState({ message }: { message: ReactNode }) {
|
||||
@@ -116,7 +116,7 @@ export default function MigrateV2() {
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<BackArrow to="/pools" />
|
||||
<BackArrowLink to="/pools" />
|
||||
<ThemedText.DeprecatedMediumHeader>
|
||||
<Trans>Migrate V2 Liquidity</Trans>
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
InterfaceSectionName,
|
||||
SwapEventName,
|
||||
} from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
@@ -39,6 +38,7 @@ import { TradeState } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
|
||||
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
@@ -115,11 +115,11 @@ const OutputSwapSection = styled(SwapSection)`
|
||||
`
|
||||
|
||||
function getIsValidSwapQuote(
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined,
|
||||
trade: InterfaceTrade | undefined,
|
||||
tradeState: TradeState,
|
||||
swapInputError?: ReactNode
|
||||
): boolean {
|
||||
return !!swapInputError && !!trade && (tradeState === TradeState.VALID || tradeState === TradeState.SYNCING)
|
||||
return Boolean(swapInputError && trade && tradeState === TradeState.VALID)
|
||||
}
|
||||
|
||||
function largerPercentValue(a?: Percent, b?: Percent) {
|
||||
@@ -293,7 +293,7 @@ export function Swap({
|
||||
const fiatValueOutput = useUSDPrice(parsedAmounts[Field.OUTPUT])
|
||||
|
||||
const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo(
|
||||
() => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.SYNCING === tradeState],
|
||||
() => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.LOADING === tradeState && Boolean(trade)],
|
||||
[trade, tradeState]
|
||||
)
|
||||
|
||||
@@ -336,7 +336,7 @@ export function Swap({
|
||||
// modal and loading
|
||||
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
|
||||
showConfirm: boolean
|
||||
tradeToConfirm: Trade<Currency, Currency, TradeType> | undefined
|
||||
tradeToConfirm: InterfaceTrade | undefined
|
||||
attemptingTxn: boolean
|
||||
swapErrorMessage: string | undefined
|
||||
txHash: string | undefined
|
||||
@@ -571,22 +571,22 @@ export function Swap({
|
||||
showCancel={true}
|
||||
/>
|
||||
<SwapHeader autoSlippage={autoSlippage} />
|
||||
<ConfirmSwapModal
|
||||
isOpen={showConfirm}
|
||||
trade={trade}
|
||||
originalTrade={tradeToConfirm}
|
||||
onAcceptChanges={handleAcceptChanges}
|
||||
attemptingTxn={attemptingTxn}
|
||||
txHash={txHash}
|
||||
recipient={recipient}
|
||||
allowedSlippage={allowedSlippage}
|
||||
onConfirm={handleSwap}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
onDismiss={handleConfirmDismiss}
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueTradeInput}
|
||||
fiatValueOutput={fiatValueTradeOutput}
|
||||
/>
|
||||
{trade && showConfirm && (
|
||||
<ConfirmSwapModal
|
||||
trade={trade}
|
||||
originalTrade={tradeToConfirm}
|
||||
onAcceptChanges={handleAcceptChanges}
|
||||
attemptingTxn={attemptingTxn}
|
||||
txHash={txHash}
|
||||
allowedSlippage={allowedSlippage}
|
||||
onConfirm={handleSwap}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
onDismiss={handleConfirmDismiss}
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueTradeInput}
|
||||
fiatValueOutput={fiatValueTradeOutput}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'relative' }}>
|
||||
<SwapSection>
|
||||
@@ -696,8 +696,17 @@ export function Swap({
|
||||
</TraceEvent>
|
||||
) : chainId && chainId !== connectedChainId ? (
|
||||
<ButtonPrimary
|
||||
onClick={() => {
|
||||
switchChain(connector, chainId)
|
||||
onClick={async () => {
|
||||
try {
|
||||
await switchChain(connector, chainId)
|
||||
} catch (error) {
|
||||
if (didUserReject(error)) {
|
||||
// Ignore error, which keeps the user on the previous chain.
|
||||
} else {
|
||||
// TODO(WEB-3306): This UX could be improved to show an error state.
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Connect to {getChainInfo(chainId)?.label}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import 'polyfill-object.fromentries'
|
||||
|
||||
import { ResizeObserver } from '@juggle/resize-observer'
|
||||
import flat from 'array.prototype.flat'
|
||||
import flatMap from 'array.prototype.flatmap'
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
flat.shim()
|
||||
flatMap.shim()
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Buffer: typeof Buffer
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.Buffer) {
|
||||
window.Buffer = Buffer
|
||||
}
|
||||
|
||||
if (!window.ResizeObserver) {
|
||||
window.ResizeObserver = ResizeObserver
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import '@testing-library/jest-dom' // jest custom assertions
|
||||
import 'polyfills'
|
||||
import 'jest-styled-components' // adds style diffs to snapshot tests
|
||||
import 'polyfills'
|
||||
|
||||
import type { createPopper } from '@popperjs/core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import failOnConsole from 'jest-fail-on-console'
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
import { Readable } from 'stream'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { TextDecoder, TextEncoder } from 'util'
|
||||
@@ -18,21 +19,19 @@ if (typeof global.TextEncoder === 'undefined') {
|
||||
global.TextDecoder = TextDecoder as typeof global.TextDecoder
|
||||
}
|
||||
|
||||
global.ResizeObserver = ResizeObserver
|
||||
|
||||
// Sets origin to the production origin, because some tests depend on this.
|
||||
// This prevents each test file from needing to set this manually.
|
||||
global.origin = 'https://app.uniswap.org'
|
||||
|
||||
global.matchMedia =
|
||||
global.matchMedia ||
|
||||
function () {
|
||||
(() => {
|
||||
return {
|
||||
matches: false,
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@popperjs/core', () => {
|
||||
const core = jest.requireActual('@popperjs/core')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build/MerkleDistributor.json'
|
||||
import MerkleDistributorJSON from '@uniswap/merkle-distributor/build/MerkleDistributor.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { MERKLE_DISTRIBUTOR_ADDRESS } from 'constants/addresses'
|
||||
@@ -15,7 +15,7 @@ import { useTransactionAdder } from '../transactions/hooks'
|
||||
import { TransactionType } from '../transactions/types'
|
||||
|
||||
function useMerkleDistributorContract() {
|
||||
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
|
||||
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MerkleDistributorJSON.abi, true)
|
||||
}
|
||||
|
||||
interface UserClaimData {
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { TransactionResponse } from '@ethersproject/providers'
|
||||
import { toUtf8String, Utf8ErrorFuncs, Utf8ErrorReason } from '@ethersproject/strings'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import { abi as GOVERNANCE_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
|
||||
import { abi as UNI_ABI } from '@uniswap/governance/build/Uni.json'
|
||||
import GovernorAlphaJSON from '@uniswap/governance/build/GovernorAlpha.json'
|
||||
import UniJSON from '@uniswap/governance/build/Uni.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import GOVERNOR_BRAVO_ABI from 'abis/governor-bravo.json'
|
||||
@@ -39,11 +39,11 @@ import { TransactionType } from '../transactions/types'
|
||||
import { VoteOption } from './types'
|
||||
|
||||
function useGovernanceV0Contract(): Contract | null {
|
||||
return useContract(GOVERNANCE_ALPHA_V0_ADDRESSES, GOVERNANCE_ABI, false)
|
||||
return useContract(GOVERNANCE_ALPHA_V0_ADDRESSES, GovernorAlphaJSON.abi, false)
|
||||
}
|
||||
|
||||
function useGovernanceV1Contract(): Contract | null {
|
||||
return useContract(GOVERNANCE_ALPHA_V1_ADDRESSES, GOVERNANCE_ABI, false)
|
||||
return useContract(GOVERNANCE_ALPHA_V1_ADDRESSES, GovernorAlphaJSON.abi, false)
|
||||
}
|
||||
|
||||
function useGovernanceBravoContract(): Contract | null {
|
||||
@@ -55,7 +55,7 @@ const useLatestGovernanceContract = useGovernanceBravoContract
|
||||
function useUniContract() {
|
||||
const { chainId } = useWeb3React()
|
||||
const uniAddress = useMemo(() => (chainId ? UNI[chainId]?.address : undefined), [chainId])
|
||||
return useContract(uniAddress, UNI_ABI, true)
|
||||
return useContract(uniAddress, UniJSON.abi, true)
|
||||
}
|
||||
|
||||
interface ProposalDetail {
|
||||
@@ -99,7 +99,7 @@ export enum ProposalState {
|
||||
EXECUTED,
|
||||
}
|
||||
|
||||
const GovernanceInterface = new Interface(GOVERNANCE_ABI)
|
||||
const GovernanceInterface = new Interface(GovernorAlphaJSON.abi)
|
||||
|
||||
// get count of all proposals made in the latest governor contract
|
||||
function useProposalCount(contract: Contract | null): number | undefined {
|
||||
|
||||
@@ -14,7 +14,15 @@ const store = configureStore({
|
||||
reducer,
|
||||
enhancers: (defaultEnhancers) => defaultEnhancers.concat(sentryEnhancer),
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({ thunk: true })
|
||||
getDefaultMiddleware({
|
||||
thunk: true,
|
||||
serializableCheck: {
|
||||
// meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok
|
||||
// because we are not adding it into any persisted store that requires serialization (e.g. localStorage)
|
||||
ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'],
|
||||
ignoredPaths: [routingApi.reducerPath],
|
||||
},
|
||||
})
|
||||
.concat(routingApi.middleware)
|
||||
.concat(save({ states: PERSISTED_KEYS, debounce: 1000 })),
|
||||
preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { TradeType } from '@uniswap/sdk-core'
|
||||
import { AlphaRouter, ChainId } from '@uniswap/smart-order-router'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
@@ -7,7 +8,8 @@ import ms from 'ms.macro'
|
||||
import qs from 'qs'
|
||||
import { trace } from 'tracing/trace'
|
||||
|
||||
import { GetQuoteResult } from './types'
|
||||
import { QuoteData, TradeResult } from './types'
|
||||
import { isExactInput, transformRoutesToTrade } from './utils'
|
||||
|
||||
export enum RouterPreference {
|
||||
AUTO = 'auto',
|
||||
@@ -69,7 +71,7 @@ const PRICE_PARAMS = {
|
||||
distributionPercent: 100,
|
||||
}
|
||||
|
||||
interface GetQuoteArgs {
|
||||
export interface GetQuoteArgs {
|
||||
tokenInAddress: string
|
||||
tokenInChainId: ChainId
|
||||
tokenInDecimals: number
|
||||
@@ -80,7 +82,12 @@ interface GetQuoteArgs {
|
||||
tokenOutSymbol?: string
|
||||
amount: string
|
||||
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
type: 'exactIn' | 'exactOut'
|
||||
tradeType: TradeType
|
||||
}
|
||||
|
||||
enum QuoteState {
|
||||
SUCCESS = 'Success',
|
||||
NOT_FOUND = 'Not found',
|
||||
}
|
||||
|
||||
export const routingApi = createApi({
|
||||
@@ -89,7 +96,7 @@ export const routingApi = createApi({
|
||||
baseUrl: 'https://api.uniswap.org/v1/',
|
||||
}),
|
||||
endpoints: (build) => ({
|
||||
getQuote: build.query<GetQuoteResult, GetQuoteArgs>({
|
||||
getQuote: build.query<TradeResult, GetQuoteArgs>({
|
||||
async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) {
|
||||
trace(
|
||||
'quote',
|
||||
@@ -119,11 +126,14 @@ export const routingApi = createApi({
|
||||
)
|
||||
},
|
||||
async queryFn(args, _api, _extraOptions, fetch) {
|
||||
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, routerPreference, type } =
|
||||
args
|
||||
|
||||
try {
|
||||
if (routerPreference === RouterPreference.API) {
|
||||
if (
|
||||
args.routerPreference === RouterPreference.API ||
|
||||
args.routerPreference === RouterPreference.AUTO ||
|
||||
args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
) {
|
||||
try {
|
||||
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
|
||||
const type = isExactInput(tradeType) ? 'exactIn' : 'exactOut'
|
||||
const query = qs.stringify({
|
||||
...API_QUERY_PARAMS,
|
||||
tokenInAddress,
|
||||
@@ -133,21 +143,40 @@ export const routingApi = createApi({
|
||||
amount,
|
||||
type,
|
||||
})
|
||||
return (await fetch(`quote?${query}`)) as { data: GetQuoteResult } | { error: FetchBaseQueryError }
|
||||
} else {
|
||||
const router = getRouter(args.tokenInChainId)
|
||||
return await getClientSideQuote(
|
||||
args,
|
||||
router,
|
||||
// TODO(zzmp): Use PRICE_PARAMS for RouterPreference.PRICE.
|
||||
// This change is intentionally being deferred to first see what effect router caching has.
|
||||
CLIENT_PARAMS
|
||||
const response = await fetch(`quote?${query}`)
|
||||
if (response.error) {
|
||||
try {
|
||||
// cast as any here because we do a runtime check on it being an object before indexing into .errorCode
|
||||
const errorData = response.error.data as any
|
||||
// NO_ROUTE should be treated as a valid response to prevent retries.
|
||||
if (typeof errorData === 'object' && errorData?.errorCode === 'NO_ROUTE') {
|
||||
return { data: { state: QuoteState.NOT_FOUND } }
|
||||
}
|
||||
} catch {
|
||||
throw response.error
|
||||
}
|
||||
}
|
||||
|
||||
const quoteData = response.data as QuoteData
|
||||
const tradeResult = transformRoutesToTrade(args, quoteData)
|
||||
return { data: tradeResult }
|
||||
} catch (error: any) {
|
||||
console.warn(
|
||||
`GetQuote failed on routing API, falling back to client: ${error?.message ?? error?.detail ?? error}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: fall back to client-side quoter when auto router fails.
|
||||
// deprecate 'legacy' v2/v3 routers first.
|
||||
return { error: { status: 'CUSTOM_ERROR', error: error.toString() } }
|
||||
}
|
||||
try {
|
||||
const router = getRouter(args.tokenInChainId)
|
||||
const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS)
|
||||
if (quoteResult.state === QuoteState.SUCCESS) {
|
||||
return { data: transformRoutesToTrade(args, quoteResult.data) }
|
||||
} else {
|
||||
return { data: quoteResult }
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`GetQuote failed on client: ${error}`)
|
||||
return { error: { status: 'CUSTOM_ERROR', error: error?.detail ?? error?.message ?? error } }
|
||||
}
|
||||
},
|
||||
keepUnusedDataFor: ms`10s`,
|
||||
|
||||
@@ -8,7 +8,6 @@ export enum TradeState {
|
||||
INVALID,
|
||||
NO_ROUTE_FOUND,
|
||||
VALID,
|
||||
SYNCING,
|
||||
}
|
||||
|
||||
// from https://github.com/Uniswap/routing-api/blob/main/lib/handlers/schema.ts
|
||||
@@ -49,7 +48,7 @@ export type V2PoolInRoute = {
|
||||
address?: string
|
||||
}
|
||||
|
||||
export interface GetQuoteResult {
|
||||
export interface QuoteData {
|
||||
quoteId?: string
|
||||
blockNumber: string
|
||||
amount: string
|
||||
@@ -68,12 +67,12 @@ export interface GetQuoteResult {
|
||||
routeString: string
|
||||
}
|
||||
|
||||
export class InterfaceTrade<
|
||||
export class ClassicTrade<
|
||||
TInput extends Currency,
|
||||
TOutput extends Currency,
|
||||
TTradeType extends TradeType
|
||||
> extends Trade<TInput, TOutput, TTradeType> {
|
||||
gasUseEstimateUSD: CurrencyAmount<Token> | null | undefined
|
||||
gasUseEstimateUSD: string | null | undefined
|
||||
blockNumber: string | null | undefined
|
||||
|
||||
constructor({
|
||||
@@ -81,8 +80,8 @@ export class InterfaceTrade<
|
||||
blockNumber,
|
||||
...routes
|
||||
}: {
|
||||
gasUseEstimateUSD?: CurrencyAmount<Token> | undefined | null
|
||||
blockNumber?: string | null | undefined
|
||||
gasUseEstimateUSD?: string | null
|
||||
blockNumber?: string | null
|
||||
v2Routes: {
|
||||
routev2: V2Route<TInput, TOutput>
|
||||
inputAmount: CurrencyAmount<TInput>
|
||||
@@ -105,3 +104,42 @@ export class InterfaceTrade<
|
||||
this.gasUseEstimateUSD = gasUseEstimateUSD
|
||||
}
|
||||
}
|
||||
|
||||
export type InterfaceTrade = ClassicTrade<Currency, Currency, TradeType>
|
||||
|
||||
export enum QuoteState {
|
||||
SUCCESS = 'Success',
|
||||
NOT_FOUND = 'Not found',
|
||||
}
|
||||
|
||||
export type QuoteResult =
|
||||
| {
|
||||
state: QuoteState.NOT_FOUND
|
||||
data?: undefined
|
||||
}
|
||||
| {
|
||||
state: QuoteState.SUCCESS
|
||||
data: QuoteData
|
||||
}
|
||||
|
||||
export type TradeResult =
|
||||
| {
|
||||
state: QuoteState.NOT_FOUND
|
||||
trade?: undefined
|
||||
}
|
||||
| {
|
||||
state: QuoteState.SUCCESS
|
||||
trade: InterfaceTrade
|
||||
}
|
||||
|
||||
export enum PoolType {
|
||||
V2Pool = 'v2-pool',
|
||||
V3Pool = 'v3-pool',
|
||||
}
|
||||
|
||||
// swap router API special cases these strings to represent native currencies
|
||||
// all chains have "ETH" as native currency symbol except for polygon
|
||||
export enum SwapRouterNativeAssets {
|
||||
MATIC = 'MATIC',
|
||||
ETH = 'ETH',
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { IMetric, MetricLoggerUnit, setGlobalMetric } from '@uniswap/smart-order-router'
|
||||
import { sendTiming } from 'components/analytics'
|
||||
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
|
||||
import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice'
|
||||
import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments'
|
||||
import ms from 'ms.macro'
|
||||
import { useMemo } from 'react'
|
||||
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference, useGetQuoteQuery } from 'state/routing/slice'
|
||||
|
||||
import { InterfaceTrade, TradeState } from './types'
|
||||
import { computeRoutes, transformRoutesToTrade } from './utils'
|
||||
import { InterfaceTrade, QuoteState, TradeState } from './types'
|
||||
|
||||
const TRADE_INVALID = { state: TradeState.INVALID, trade: undefined } as const
|
||||
const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const
|
||||
const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const
|
||||
|
||||
/**
|
||||
* Returns the best trade by invoking the routing api or the smart order router on the client
|
||||
@@ -25,7 +27,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
): {
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
|
||||
trade?: InterfaceTrade
|
||||
} {
|
||||
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
|
||||
() =>
|
||||
@@ -44,10 +46,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
})
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: quoteResult,
|
||||
currentData,
|
||||
data: tradeResult,
|
||||
currentData: currentTradeResult,
|
||||
} = useGetQuoteQuery(queryArgs ?? skipToken, {
|
||||
// Price-fetching is informational and costly, so it's done less frequently.
|
||||
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
|
||||
@@ -55,72 +56,23 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
|
||||
refetchOnMountOrArgChange: 2 * 60,
|
||||
})
|
||||
|
||||
const route = useMemo(
|
||||
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
|
||||
[currencyIn, currencyOut, quoteResult, tradeType]
|
||||
)
|
||||
|
||||
// get USD gas cost of trade in active chains stablecoin amount
|
||||
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
|
||||
|
||||
const isSyncing = currentData !== quoteResult
|
||||
const isCurrent = currentTradeResult === tradeResult
|
||||
|
||||
return useMemo(() => {
|
||||
if (!currencyIn || !currencyOut || currencyIn.equals(currencyOut)) {
|
||||
if (!amountSpecified || isError || !queryArgs) {
|
||||
return TRADE_INVALID
|
||||
} else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) {
|
||||
return TRADE_NOT_FOUND
|
||||
} else if (!tradeResult?.trade) {
|
||||
// TODO(WEB-3307): use `isLoading` returned by rtk-query hook instead of checking for `trade` status
|
||||
return TRADE_LOADING
|
||||
} else {
|
||||
return {
|
||||
state: TradeState.INVALID,
|
||||
trade: undefined,
|
||||
state: isCurrent ? TradeState.VALID : TradeState.LOADING,
|
||||
trade: tradeResult.trade,
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading && !quoteResult) {
|
||||
// only on first hook render
|
||||
return {
|
||||
state: TradeState.LOADING,
|
||||
trade: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let otherAmount = undefined
|
||||
if (quoteResult) {
|
||||
if (tradeType === TradeType.EXACT_INPUT && currencyOut) {
|
||||
otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote)
|
||||
}
|
||||
|
||||
if (tradeType === TradeType.EXACT_OUTPUT && currencyIn) {
|
||||
otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote)
|
||||
}
|
||||
}
|
||||
|
||||
if (isError || !otherAmount || !route || route.length === 0 || !queryArgs) {
|
||||
return {
|
||||
state: TradeState.NO_ROUTE_FOUND,
|
||||
trade: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const trade = transformRoutesToTrade(route, tradeType, quoteResult?.blockNumber, gasUseEstimateUSD)
|
||||
return {
|
||||
// always return VALID regardless of isFetching status
|
||||
state: isSyncing ? TradeState.SYNCING : TradeState.VALID,
|
||||
trade,
|
||||
}
|
||||
} catch (e) {
|
||||
return { state: TradeState.INVALID, trade: undefined }
|
||||
}
|
||||
}, [
|
||||
currencyIn,
|
||||
currencyOut,
|
||||
quoteResult,
|
||||
isLoading,
|
||||
tradeType,
|
||||
isError,
|
||||
route,
|
||||
queryArgs,
|
||||
gasUseEstimateUSD,
|
||||
isSyncing,
|
||||
])
|
||||
}, [amountSpecified, isCurrent, isError, queryArgs, tradeResult])
|
||||
}
|
||||
|
||||
// only want to enable this when app hook called
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
import { Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
|
||||
import { nativeOnChain } from '../../constants/tokens'
|
||||
import { PoolType } from './types'
|
||||
import { computeRoutes } from './utils'
|
||||
|
||||
const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC')
|
||||
const DAI = new Token(1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 6, 'DAI')
|
||||
const MKR = new Token(1, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 6, 'MKR')
|
||||
|
||||
const ETH = nativeOnChain(1)
|
||||
const ETH = nativeOnChain(SupportedChainId.MAINNET)
|
||||
|
||||
// helper function to make amounts more readable
|
||||
const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString()
|
||||
|
||||
describe('#useRoute', () => {
|
||||
it('handles an undefined payload', () => {
|
||||
const result = computeRoutes(undefined, undefined, TradeType.EXACT_INPUT, undefined)
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles empty edges and nodes', () => {
|
||||
const result = computeRoutes(USDC, DAI, TradeType.EXACT_INPUT, {
|
||||
route: [],
|
||||
})
|
||||
|
||||
const result = computeRoutes(false, false, [])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('handles a single route trade from DAI to USDC from v3', () => {
|
||||
const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
},
|
||||
],
|
||||
const result = computeRoutes(false, false, [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
},
|
||||
],
|
||||
})
|
||||
])
|
||||
|
||||
const r = result?.[0]
|
||||
|
||||
@@ -60,28 +51,26 @@ describe('#useRoute', () => {
|
||||
})
|
||||
|
||||
it('handles a single route trade from DAI to USDC from v2', () => {
|
||||
const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
reserve0: {
|
||||
token: DAI,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
const result = computeRoutes(false, false, [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
reserve0: {
|
||||
token: DAI,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
],
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
])
|
||||
|
||||
const r = result?.[0]
|
||||
|
||||
@@ -96,54 +85,52 @@ describe('#useRoute', () => {
|
||||
})
|
||||
|
||||
it('handles a multi-route trade from DAI to USDC', () => {
|
||||
const result = computeRoutes(DAI, USDC, TradeType.EXACT_OUTPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`5`,
|
||||
amountOut: amount`6`,
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
reserve0: {
|
||||
token: DAI,
|
||||
quotient: amount`1000`,
|
||||
},
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`500`,
|
||||
},
|
||||
const result = computeRoutes(false, false, [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`5`,
|
||||
amountOut: amount`6`,
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
reserve0: {
|
||||
token: DAI,
|
||||
quotient: amount`1000`,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`10`,
|
||||
amountOut: amount`1`,
|
||||
fee: '3000',
|
||||
tokenIn: DAI,
|
||||
tokenOut: MKR,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`500`,
|
||||
},
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`200`,
|
||||
fee: '10000',
|
||||
tokenIn: MKR,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`10`,
|
||||
amountOut: amount`1`,
|
||||
fee: '3000',
|
||||
tokenIn: DAI,
|
||||
tokenOut: MKR,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x3f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`200`,
|
||||
fee: '10000',
|
||||
tokenIn: MKR,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.length).toBe(2)
|
||||
@@ -165,38 +152,36 @@ describe('#useRoute', () => {
|
||||
})
|
||||
|
||||
it('handles a single route trade with same token pair, different fee tiers', () => {
|
||||
const result = computeRoutes(DAI, USDC, TradeType.EXACT_INPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`10`,
|
||||
amountOut: amount`50`,
|
||||
fee: '3000',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
],
|
||||
const result = computeRoutes(false, false, [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
],
|
||||
})
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`10`,
|
||||
amountOut: amount`50`,
|
||||
fee: '3000',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.length).toBe(2)
|
||||
@@ -206,28 +191,68 @@ describe('#useRoute', () => {
|
||||
expect(result?.[0].inputAmount.toSignificant()).toBe('1')
|
||||
})
|
||||
|
||||
it('computes mixed routes correctly', () => {
|
||||
const result = computeRoutes(false, false, [
|
||||
[
|
||||
{
|
||||
type: PoolType.V3Pool,
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`1`,
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
tokenIn: DAI,
|
||||
tokenOut: USDC,
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
},
|
||||
{
|
||||
type: PoolType.V2Pool,
|
||||
address: 'x2f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`10`,
|
||||
amountOut: amount`50`,
|
||||
tokenIn: USDC,
|
||||
tokenOut: MKR,
|
||||
reserve0: {
|
||||
token: USDC,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
reserve1: {
|
||||
token: MKR,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.length).toBe(1)
|
||||
expect(result?.[0].routev3).toBeNull()
|
||||
expect(result?.[0].routev2).toBeNull()
|
||||
expect(result?.[0].mixedRoute?.output).toStrictEqual(MKR)
|
||||
expect(result?.[0].inputAmount.toSignificant()).toBe('1')
|
||||
})
|
||||
|
||||
describe('with ETH', () => {
|
||||
it('outputs native ETH as input currency', () => {
|
||||
const WETH = ETH.wrapped
|
||||
|
||||
const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: (1e18).toString(),
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
tokenIn: WETH,
|
||||
tokenOut: USDC,
|
||||
},
|
||||
],
|
||||
const result = computeRoutes(true, false, [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: (1e18).toString(),
|
||||
amountOut: amount`5`,
|
||||
fee: '500',
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
tokenIn: WETH,
|
||||
tokenOut: USDC,
|
||||
},
|
||||
],
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.length).toBe(1)
|
||||
@@ -239,24 +264,22 @@ describe('#useRoute', () => {
|
||||
|
||||
it('outputs native ETH as output currency', () => {
|
||||
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
|
||||
const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`5`,
|
||||
amountOut: (1e18).toString(),
|
||||
fee: '500',
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
tokenIn: USDC,
|
||||
tokenOut: WETH,
|
||||
},
|
||||
],
|
||||
const result = computeRoutes(false, true, [
|
||||
[
|
||||
{
|
||||
type: 'v3-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`5`,
|
||||
amountOut: (1e18).toString(),
|
||||
fee: '500',
|
||||
sqrtRatioX96: '2437312313659959819381354528',
|
||||
liquidity: '10272714736694327408',
|
||||
tickCurrent: '-69633',
|
||||
tokenIn: USDC,
|
||||
tokenOut: WETH,
|
||||
},
|
||||
],
|
||||
})
|
||||
])
|
||||
|
||||
expect(result?.length).toBe(1)
|
||||
expect(result?.[0].routev3?.input).toStrictEqual(USDC)
|
||||
@@ -268,28 +291,26 @@ describe('#useRoute', () => {
|
||||
it('outputs native ETH as input currency for v2 routes', () => {
|
||||
const WETH = ETH.wrapped
|
||||
|
||||
const result = computeRoutes(ETH, USDC, TradeType.EXACT_OUTPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: (1e18).toString(),
|
||||
amountOut: amount`5`,
|
||||
tokenIn: WETH,
|
||||
tokenOut: USDC,
|
||||
reserve0: {
|
||||
token: WETH,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
const result = computeRoutes(true, false, [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: (1e18).toString(),
|
||||
amountOut: amount`5`,
|
||||
tokenIn: WETH,
|
||||
tokenOut: USDC,
|
||||
reserve0: {
|
||||
token: WETH,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
],
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.length).toBe(1)
|
||||
@@ -301,28 +322,26 @@ describe('#useRoute', () => {
|
||||
|
||||
it('outputs native ETH as output currency for v2 routes', () => {
|
||||
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
|
||||
const result = computeRoutes(USDC, ETH, TradeType.EXACT_OUTPUT, {
|
||||
route: [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`5`,
|
||||
amountOut: (1e18).toString(),
|
||||
tokenIn: USDC,
|
||||
tokenOut: WETH,
|
||||
reserve0: {
|
||||
token: WETH,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
const result = computeRoutes(false, true, [
|
||||
[
|
||||
{
|
||||
type: 'v2-pool',
|
||||
address: '0x1f8F72aA9304c8B593d555F12eF6589cC3A579A2',
|
||||
amountIn: amount`5`,
|
||||
amountOut: (1e18).toString(),
|
||||
tokenIn: USDC,
|
||||
tokenOut: WETH,
|
||||
reserve0: {
|
||||
token: WETH,
|
||||
quotient: amount`100`,
|
||||
},
|
||||
],
|
||||
reserve1: {
|
||||
token: USDC,
|
||||
quotient: amount`200`,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
])
|
||||
|
||||
expect(result?.length).toBe(1)
|
||||
expect(result?.[0].routev2?.input).toStrictEqual(USDC)
|
||||
|
||||
@@ -1,32 +1,50 @@
|
||||
import { MixedRouteSDK, Protocol } from '@uniswap/router-sdk'
|
||||
import { MixedRouteSDK } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
|
||||
import { isPolygonChain } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
|
||||
import { GetQuoteResult, InterfaceTrade, V2PoolInRoute, V3PoolInRoute } from './types'
|
||||
import { GetQuoteArgs } from './slice'
|
||||
import {
|
||||
ClassicTrade,
|
||||
PoolType,
|
||||
QuoteData,
|
||||
QuoteState,
|
||||
SwapRouterNativeAssets,
|
||||
TradeResult,
|
||||
V2PoolInRoute,
|
||||
V3PoolInRoute,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* Transforms a Routing API quote into an array of routes that can be used to create
|
||||
* a `Trade`.
|
||||
* Transforms a Routing API quote into an array of routes that can be used to
|
||||
* create a `Trade`.
|
||||
*/
|
||||
export function computeRoutes(
|
||||
currencyIn: Currency | undefined,
|
||||
currencyOut: Currency | undefined,
|
||||
tradeType: TradeType,
|
||||
quoteResult: Pick<GetQuoteResult, 'route'> | undefined
|
||||
) {
|
||||
if (!quoteResult || !quoteResult.route || !currencyIn || !currencyOut) return undefined
|
||||
tokenInIsNative: boolean,
|
||||
tokenOutIsNative: boolean,
|
||||
routes: QuoteData['route']
|
||||
):
|
||||
| {
|
||||
routev3: V3Route<Currency, Currency> | null
|
||||
routev2: V2Route<Currency, Currency> | null
|
||||
mixedRoute: MixedRouteSDK<Currency, Currency> | null
|
||||
inputAmount: CurrencyAmount<Currency>
|
||||
outputAmount: CurrencyAmount<Currency>
|
||||
}[]
|
||||
| undefined {
|
||||
if (routes.length === 0) return []
|
||||
|
||||
if (quoteResult.route.length === 0) return []
|
||||
const tokenIn = routes[0]?.[0]?.tokenIn
|
||||
const tokenOut = routes[0]?.[routes[0]?.length - 1]?.tokenOut
|
||||
if (!tokenIn || !tokenOut) throw new Error('Expected both tokenIn and tokenOut to be present')
|
||||
|
||||
const parsedTokenIn = parseToken(quoteResult.route[0][0].tokenIn)
|
||||
const parsedTokenOut = parseToken(quoteResult.route[0][quoteResult.route[0].length - 1].tokenOut)
|
||||
if (parsedTokenIn.address !== currencyIn.wrapped.address) return undefined
|
||||
if (parsedTokenOut.address !== currencyOut.wrapped.address) return undefined
|
||||
if (parsedTokenIn.wrapped.equals(parsedTokenOut.wrapped)) return undefined
|
||||
const parsedCurrencyIn = tokenInIsNative ? nativeOnChain(tokenIn.chainId) : parseToken(tokenIn)
|
||||
const parsedCurrencyOut = tokenOutIsNative ? nativeOnChain(tokenOut.chainId) : parseToken(tokenOut)
|
||||
|
||||
try {
|
||||
return quoteResult.route.map((route) => {
|
||||
return routes.map((route) => {
|
||||
if (route.length === 0) {
|
||||
throw new Error('Expected route to have at least one pair or pool')
|
||||
}
|
||||
@@ -37,68 +55,90 @@ export function computeRoutes(
|
||||
throw new Error('Expected both amountIn and amountOut to be present')
|
||||
}
|
||||
|
||||
const routeProtocol = getRouteProtocol(route)
|
||||
const isOnlyV2 = isVersionedRoute<V2PoolInRoute>(PoolType.V2Pool, route)
|
||||
const isOnlyV3 = isVersionedRoute<V3PoolInRoute>(PoolType.V3Pool, route)
|
||||
|
||||
return {
|
||||
routev3:
|
||||
routeProtocol === Protocol.V3
|
||||
? new V3Route(route.map(genericPoolPairParser) as Pool[], currencyIn, currencyOut)
|
||||
: null,
|
||||
routev2:
|
||||
routeProtocol === Protocol.V2
|
||||
? new V2Route(route.map(genericPoolPairParser) as Pair[], currencyIn, currencyOut)
|
||||
: null,
|
||||
routev3: isOnlyV3 ? new V3Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut) : null,
|
||||
routev2: isOnlyV2 ? new V2Route(route.map(parsePair), parsedCurrencyIn, parsedCurrencyOut) : null,
|
||||
mixedRoute:
|
||||
routeProtocol === Protocol.MIXED
|
||||
? new MixedRouteSDK(route.map(genericPoolPairParser), currencyIn, currencyOut)
|
||||
!isOnlyV3 && !isOnlyV2
|
||||
? new MixedRouteSDK(route.map(parsePoolOrPair), parsedCurrencyIn, parsedCurrencyOut)
|
||||
: null,
|
||||
inputAmount: CurrencyAmount.fromRawAmount(currencyIn, rawAmountIn),
|
||||
outputAmount: CurrencyAmount.fromRawAmount(currencyOut, rawAmountOut),
|
||||
inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn),
|
||||
outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut),
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
// `Route` constructor may throw if inputs/outputs are temporarily out of sync
|
||||
// (RTK-Query always returns the latest data which may not be the right inputs/outputs)
|
||||
// This is not fatal and will fix itself in future render cycles
|
||||
console.error(e)
|
||||
return undefined
|
||||
console.error('Error computing routes', e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export function transformRoutesToTrade<TTradeType extends TradeType>(
|
||||
route: ReturnType<typeof computeRoutes>,
|
||||
tradeType: TTradeType,
|
||||
blockNumber?: string | null,
|
||||
gasUseEstimateUSD?: CurrencyAmount<Token> | null
|
||||
): InterfaceTrade<Currency, Currency, TTradeType> {
|
||||
return new InterfaceTrade({
|
||||
v2Routes:
|
||||
route
|
||||
?.filter((r): r is typeof route[0] & { routev2: NonNullable<typeof route[0]['routev2']> } => r.routev2 !== null)
|
||||
.map(({ routev2, inputAmount, outputAmount }) => ({ routev2, inputAmount, outputAmount })) ?? [],
|
||||
v3Routes:
|
||||
route
|
||||
?.filter((r): r is typeof route[0] & { routev3: NonNullable<typeof route[0]['routev3']> } => r.routev3 !== null)
|
||||
.map(({ routev3, inputAmount, outputAmount }) => ({ routev3, inputAmount, outputAmount })) ?? [],
|
||||
mixedRoutes:
|
||||
route
|
||||
?.filter(
|
||||
(r): r is typeof route[0] & { mixedRoute: NonNullable<typeof route[0]['mixedRoute']> } =>
|
||||
r.mixedRoute !== null
|
||||
)
|
||||
.map(({ mixedRoute, inputAmount, outputAmount }) => ({ mixedRoute, inputAmount, outputAmount })) ?? [],
|
||||
tradeType,
|
||||
gasUseEstimateUSD,
|
||||
blockNumber,
|
||||
})
|
||||
const parsePoolOrPair = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => {
|
||||
return pool.type === PoolType.V3Pool ? parsePool(pool) : parsePair(pool)
|
||||
}
|
||||
|
||||
const parseToken = ({ address, chainId, decimals, symbol }: GetQuoteResult['route'][0][0]['tokenIn']): Token => {
|
||||
function isVersionedRoute<T extends V2PoolInRoute | V3PoolInRoute>(
|
||||
type: T['type'],
|
||||
route: (V3PoolInRoute | V2PoolInRoute)[]
|
||||
): route is T[] {
|
||||
return route.every((pool) => pool.type === type)
|
||||
}
|
||||
|
||||
export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): TradeResult {
|
||||
const { tokenInAddress, tokenOutAddress, tradeType } = args
|
||||
const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenInAddress as SwapRouterNativeAssets)
|
||||
const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets)
|
||||
const { gasUseEstimateUSD, blockNumber } = data
|
||||
const routes = computeRoutes(tokenInIsNative, tokenOutIsNative, data.route)
|
||||
|
||||
const trade = new ClassicTrade({
|
||||
v2Routes:
|
||||
routes
|
||||
?.filter(
|
||||
(r): r is typeof routes[0] & { routev2: NonNullable<typeof routes[0]['routev2']> } => r.routev2 !== null
|
||||
)
|
||||
.map(({ routev2, inputAmount, outputAmount }) => ({
|
||||
routev2,
|
||||
inputAmount,
|
||||
outputAmount,
|
||||
})) ?? [],
|
||||
v3Routes:
|
||||
routes
|
||||
?.filter(
|
||||
(r): r is typeof routes[0] & { routev3: NonNullable<typeof routes[0]['routev3']> } => r.routev3 !== null
|
||||
)
|
||||
.map(({ routev3, inputAmount, outputAmount }) => ({
|
||||
routev3,
|
||||
inputAmount,
|
||||
outputAmount,
|
||||
})) ?? [],
|
||||
mixedRoutes:
|
||||
routes
|
||||
?.filter(
|
||||
(r): r is typeof routes[0] & { mixedRoute: NonNullable<typeof routes[0]['mixedRoute']> } =>
|
||||
r.mixedRoute !== null
|
||||
)
|
||||
.map(({ mixedRoute, inputAmount, outputAmount }) => ({
|
||||
mixedRoute,
|
||||
inputAmount,
|
||||
outputAmount,
|
||||
})) ?? [],
|
||||
tradeType,
|
||||
gasUseEstimateUSD: parseFloat(gasUseEstimateUSD).toFixed(2).toString(),
|
||||
blockNumber,
|
||||
})
|
||||
|
||||
return { state: QuoteState.SUCCESS, trade }
|
||||
}
|
||||
|
||||
function parseToken({ address, chainId, decimals, symbol }: QuoteData['route'][0][0]['tokenIn']): Token {
|
||||
return new Token(chainId, address, parseInt(decimals.toString()), symbol)
|
||||
}
|
||||
|
||||
const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool =>
|
||||
new Pool(
|
||||
function parsePool({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOut }: V3PoolInRoute): Pool {
|
||||
return new Pool(
|
||||
parseToken(tokenIn),
|
||||
parseToken(tokenOut),
|
||||
parseInt(fee) as FeeAmount,
|
||||
@@ -106,6 +146,7 @@ const parsePool = ({ fee, sqrtRatioX96, liquidity, tickCurrent, tokenIn, tokenOu
|
||||
liquidity,
|
||||
parseInt(tickCurrent)
|
||||
)
|
||||
}
|
||||
|
||||
const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair =>
|
||||
new Pair(
|
||||
@@ -113,12 +154,15 @@ const parsePair = ({ reserve0, reserve1 }: V2PoolInRoute): Pair =>
|
||||
CurrencyAmount.fromRawAmount(parseToken(reserve1.token), reserve1.quotient)
|
||||
)
|
||||
|
||||
const genericPoolPairParser = (pool: V3PoolInRoute | V2PoolInRoute): Pool | Pair => {
|
||||
return pool.type === 'v3-pool' ? parsePool(pool) : parsePair(pool)
|
||||
// TODO(WEB-2050): Convert other instances of tradeType comparison to use this utility function
|
||||
export function isExactInput(tradeType: TradeType): boolean {
|
||||
return tradeType === TradeType.EXACT_INPUT
|
||||
}
|
||||
|
||||
function getRouteProtocol(route: (V3PoolInRoute | V2PoolInRoute)[]): Protocol {
|
||||
if (route.every((pool) => pool.type === 'v2-pool')) return Protocol.V2
|
||||
if (route.every((pool) => pool.type === 'v3-pool')) return Protocol.V3
|
||||
return Protocol.MIXED
|
||||
export function currencyAddressForSwapQuote(currency: Currency): string {
|
||||
if (currency.isNative) {
|
||||
return isPolygonChain(currency.chainId) ? SwapRouterNativeAssets.MATIC : SwapRouterNativeAssets.ETH
|
||||
}
|
||||
|
||||
return currency.address
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import StakingRewardsJSON from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
@@ -11,7 +11,7 @@ import { useMemo } from 'react'
|
||||
|
||||
import { DAI, UNI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
|
||||
const STAKING_REWARDS_INTERFACE = new Interface(STAKING_REWARDS_ABI)
|
||||
const STAKING_REWARDS_INTERFACE = new Interface(StakingRewardsJSON.abi)
|
||||
|
||||
export const STAKING_GENESIS = 1600387200
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export function useDerivedSwapInfo(
|
||||
parsedAmount: CurrencyAmount<Currency> | undefined
|
||||
inputError?: ReactNode
|
||||
trade: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade?: InterfaceTrade
|
||||
state: TradeState
|
||||
}
|
||||
allowedSlippage: Percent
|
||||
|
||||
@@ -149,10 +149,16 @@ export function useUserSlippageTolerance(): [
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => [userSlippageTolerance, setUserSlippageTolerance],
|
||||
[setUserSlippageTolerance, userSlippageTolerance]
|
||||
)
|
||||
return [userSlippageTolerance, setUserSlippageTolerance]
|
||||
}
|
||||
|
||||
/**
|
||||
*Returns user slippage tolerance, replacing the auto with a default value
|
||||
* @param defaultSlippageTolerance the value to replace auto with
|
||||
*/
|
||||
export function useUserSlippageToleranceWithDefault(defaultSlippageTolerance: Percent): Percent {
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
return allowedSlippage === SlippageTolerance.Auto ? defaultSlippageTolerance : allowedSlippage
|
||||
}
|
||||
|
||||
export function useUserHideClosedPositions(): [boolean, (newHideClosedPositions: boolean) => void] {
|
||||
@@ -170,18 +176,6 @@ export function useUserHideClosedPositions(): [boolean, (newHideClosedPositions:
|
||||
return [hideClosedPositions, setHideClosedPositions]
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as above but replaces the auto with a default value
|
||||
* @param defaultSlippageTolerance the default value to replace auto with
|
||||
*/
|
||||
export function useUserSlippageToleranceWithDefault(defaultSlippageTolerance: Percent): Percent {
|
||||
const allowedSlippage = useUserSlippageTolerance()[0]
|
||||
return useMemo(
|
||||
() => (allowedSlippage === SlippageTolerance.Auto ? defaultSlippageTolerance : allowedSlippage),
|
||||
[allowedSlippage, defaultSlippageTolerance]
|
||||
)
|
||||
}
|
||||
|
||||
export function useUserTransactionTTL(): [number, (slippage: number) => void] {
|
||||
const { chainId } = useWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user