Commit 28181671 authored by charity-sock-pacifist's avatar charity-sock-pacifist Committed by GitHub

feat: swap fees [main] (#7478)

parent c2440d10
import { CurrencyAmount } from '@uniswap/sdk-core'
import { FeatureFlag } from 'featureFlags'
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
describe('Swap with fees', () => {
describe('Classic swaps', () => {
beforeEach(() => {
cy.visit('/swap', { featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }] })
// Store trade quote into alias
cy.intercept({ url: 'https://api.uniswap.org/v2/quote' }, (req) => {
// Avoid tracking stablecoin pricing fetches
if (JSON.parse(req.body).intent !== 'pricing') req.alias = 'quoteFetch'
})
})
it('displays $0 fee on swaps without fees', () => {
// Set up a stablecoin <> stablecoin swap (no fees)
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.contains('DAI').click()
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').type('1')
// Verify 0 fee UI is displayed
cy.get(getTestSelector('swap-details-header-row')).click()
cy.contains('Fee')
cy.contains('$0')
})
it('swaps ETH for USDC exact-out with swap fee', () => {
cy.hardhat().then((hardhat) => {
getBalance(USDC_MAINNET).then((initialBalance) => {
// Set up swap
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').type('1')
cy.wait('@quoteFetch')
.its('response.body')
.then(({ quote: { portionBips, portionRecipient, portionAmount } }) => {
// Fees are generally expected to always be enabled for ETH -> USDC swaps
// If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars
if (portionRecipient) return
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => {
const feeCurrencyAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, portionAmount)
// Initiate transaction
cy.get('#swap-button').click()
cy.contains('Review swap')
// Verify fee percentage and amount is displayed
cy.contains(`Fee (${portionBips / 100}%)`)
// Confirm transaction
cy.contains('Confirm swap').click()
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
// Verify the post-fee output is the expected exact-out amount
const finalBalance = initialBalance + 1
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
getBalance(USDC_MAINNET).should('eq', finalBalance)
// Verify fee recipient received fee
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => {
const expectedFinalRecipientBalance = initialRecipientBalance.add(feeCurrencyAmount)
cy.then(() => finalRecipientBalance.equalTo(expectedFinalRecipientBalance)).should('be.true')
})
})
})
})
})
})
it('swaps ETH for USDC exact-in with swap fee', () => {
cy.hardhat().then((hardhat) => {
// Set up swap
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-input .token-amount-input').type('.01')
cy.wait('@quoteFetch')
.its('response.body')
.then(({ quote: { portionBips, portionRecipient } }) => {
// Fees are generally expected to always be enabled for ETH -> USDC swaps
// If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars
if (portionRecipient) return
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => {
// Initiate transaction
cy.get('#swap-button').click()
cy.contains('Review swap')
// Verify fee percentage and amount is displayed
cy.contains(`Fee (${portionBips / 100}%)`)
// Confirm transaction
cy.contains('Confirm swap').click()
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
// Verify fee recipient received fee
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => {
cy.then(() => finalRecipientBalance.greaterThan(initialRecipientBalance)).should('be.true')
})
})
})
})
})
})
describe('UniswapX swaps', () => {
it('displays UniswapX fee in UI', () => {
cy.visit('/swap', {
featureFlags: [
{ name: FeatureFlag.feesEnabled, value: true },
{ name: FeatureFlag.uniswapXDefaultEnabled, value: true },
],
})
// Intercept the trade quote
cy.intercept({ url: 'https://api.uniswap.org/v2/quote' }, (req) => {
// Avoid intercepting stablecoin pricing fetches
if (JSON.parse(req.body).intent !== 'pricing') {
req.reply({ fixture: 'uniswapx/feeQuote.json' })
}
})
// Setup swap
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('ETH').click()
cy.get('#swap-currency-input .token-amount-input').type('200')
// Verify fee UI is displayed
cy.get(getTestSelector('swap-details-header-row')).click()
cy.contains('Fee (0.15%)')
})
})
})
{
"routing": "DUTCH_LIMIT",
"quote": {
"orderInfo": {
"chainId": 1,
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x",
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": "100",
"input": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"startAmount": "200000000",
"endAmount": "200000000"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "123803169993201727",
"endAmount": "117908377342236273",
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "185983730585681",
"endAmount": "177128258400955",
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
},
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
"startTimeBufferSecs": 45,
"auctionPeriodSecs": 60,
"deadlineBufferSecs": 12,
"slippageTolerance": "0.5",
"permitData": {
"domain": {
"name": "Permit2",
"chainId": 1,
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
},
"types": {
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "ExclusiveDutchOrder"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"ExclusiveDutchOrder": [
{
"name": "info",
"type": "OrderInfo"
},
{
"name": "decayStartTime",
"type": "uint256"
},
{
"name": "decayEndTime",
"type": "uint256"
},
{
"name": "exclusiveFiller",
"type": "address"
},
{
"name": "exclusivityOverrideBps",
"type": "uint256"
},
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputStartAmount",
"type": "uint256"
},
{
"name": "inputEndAmount",
"type": "uint256"
},
{
"name": "outputs",
"type": "DutchOutput[]"
}
],
"OrderInfo": [
{
"name": "reactor",
"type": "address"
},
{
"name": "swapper",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "additionalValidationContract",
"type": "address"
},
{
"name": "additionalValidationData",
"type": "bytes"
}
],
"DutchOutput": [
{
"name": "token",
"type": "address"
},
{
"name": "startAmount",
"type": "uint256"
},
{
"name": "endAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
]
},
"values": {
"permitted": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": {
"type": "BigNumber",
"hex": "0x0bebc200"
}
},
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"witness": {
"info": {
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x"
},
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": {
"type": "BigNumber",
"hex": "0x64"
},
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputStartAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"inputEndAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0x01b7d653c183183f"
},
"endAmount": {
"type": "BigNumber",
"hex": "0x01a2e50b6386d671"
},
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0xa926b6321051"
},
"endAmount": {
"type": "BigNumber",
"hex": "0xa118e2ebf2bb"
},
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
}
}
},
"portionBips": 15,
"portionAmount": "185983730585681",
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
},
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"allQuotes": [
{
"routing": "DUTCH_LIMIT",
"quote": {
"orderInfo": {
"chainId": 1,
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x",
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": "100",
"input": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"startAmount": "200000000",
"endAmount": "200000000"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "123803169993201727",
"endAmount": "117908377342236273",
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "185983730585681",
"endAmount": "177128258400955",
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
},
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
"startTimeBufferSecs": 45,
"auctionPeriodSecs": 60,
"deadlineBufferSecs": 12,
"slippageTolerance": "0.5",
"permitData": {
"domain": {
"name": "Permit2",
"chainId": 1,
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
},
"types": {
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "ExclusiveDutchOrder"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"ExclusiveDutchOrder": [
{
"name": "info",
"type": "OrderInfo"
},
{
"name": "decayStartTime",
"type": "uint256"
},
{
"name": "decayEndTime",
"type": "uint256"
},
{
"name": "exclusiveFiller",
"type": "address"
},
{
"name": "exclusivityOverrideBps",
"type": "uint256"
},
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputStartAmount",
"type": "uint256"
},
{
"name": "inputEndAmount",
"type": "uint256"
},
{
"name": "outputs",
"type": "DutchOutput[]"
}
],
"OrderInfo": [
{
"name": "reactor",
"type": "address"
},
{
"name": "swapper",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "additionalValidationContract",
"type": "address"
},
{
"name": "additionalValidationData",
"type": "bytes"
}
],
"DutchOutput": [
{
"name": "token",
"type": "address"
},
{
"name": "startAmount",
"type": "uint256"
},
{
"name": "endAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
]
},
"values": {
"permitted": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": {
"type": "BigNumber",
"hex": "0x0bebc200"
}
},
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"witness": {
"info": {
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x"
},
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": {
"type": "BigNumber",
"hex": "0x64"
},
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputStartAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"inputEndAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0x01b7d653c183183f"
},
"endAmount": {
"type": "BigNumber",
"hex": "0x01a2e50b6386d671"
},
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0xa926b6321051"
},
"endAmount": {
"type": "BigNumber",
"hex": "0xa118e2ebf2bb"
},
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
}
}
},
"portionBips": 15,
"portionAmount": "185983730585681",
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
},
{
"routing": "CLASSIC",
"quote": {
"methodParameters": {
"calldata": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000652d85d0000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000001bdf1285753b47400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000037a8f295612602f2774d331e562be9e61b83a327000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000001bd45ea74e458eb",
"value": "0x00",
"to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"
},
"blockNumber": "18364784",
"amount": "200000000",
"amountDecimals": "200",
"quote": "126149127803342909",
"quoteDecimals": "0.126149127803342909",
"quoteGasAdjusted": "122888348391508943",
"quoteGasAdjustedDecimals": "0.122888348391508943",
"quoteGasAndPortionAdjusted": "122699124699803928",
"quoteGasAndPortionAdjustedDecimals": "0.122699124699803928",
"gasUseEstimateQuote": "3260779411833966",
"gasUseEstimateQuoteDecimals": "0.003260779411833966",
"gasUseEstimate": "240911",
"gasUseEstimateUSD": "5.153332510477604328",
"simulationStatus": "SUCCESS",
"simulationError": false,
"gasPriceWei": "13535203506",
"route": [
[
{
"type": "v2-pool",
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
"tokenIn": {
"chainId": 1,
"decimals": "6",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol": "USDC"
},
"tokenOut": {
"chainId": 1,
"decimals": "18",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"symbol": "WETH"
},
"reserve0": {
"token": {
"chainId": 1,
"decimals": "6",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol": "USDC"
},
"quotient": "27487668611269"
},
"reserve1": {
"token": {
"chainId": 1,
"decimals": "18",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"symbol": "WETH"
},
"quotient": "17390022942803382004255"
},
"amountIn": "200000000",
"amountOut": "125959904111637894"
}
]
],
"routeString": "[V2] 100.00% = USDC -- [0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc] --> WETH",
"quoteId": "f46cf31c-251e-470c-bd57-13209015694e",
"portionBips": 15,
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327",
"portionAmount": "189223691705014",
"portionAmountDecimals": "0.000189223691705014",
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"tradeType": "EXACT_INPUT",
"slippage": 0.5
}
}
]
}
\ No newline at end of file
......@@ -5,7 +5,7 @@
"incremental": true,
"isolatedModules": false,
"noImplicitAny": false,
"target": "ES5",
"target": "ES6",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
"types": ["cypress", "node"],
},
......
......@@ -18,6 +18,7 @@ import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefaul
import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput'
import { useUniswapXExactOutputFlag } from 'featureFlags/flags/uniswapXExactOutput'
import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabledFlag } from 'featureFlags/flags/useFees'
import { useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
import { X } from 'react-feather'
......@@ -267,6 +268,12 @@ export default function FeatureFlagModal() {
<X size={24} />
</CloseButton>
</Header>
<FeatureFlagOption
variant={BaseVariant}
value={useFeesEnabledFlag()}
featureFlag={FeatureFlag.feesEnabled}
label="Enable Swap Fees"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useFallbackProviderEnabledFlag()}
......
......@@ -15,6 +15,7 @@ function useHasUpdatedTx() {
return !!prevPendingActivityCount && pendingActivityCount < prevPendingActivityCount
}
// TODO(WEB-3004) - Add useCachedPortfolioBalanceUsd to simplify usage of useCachedPortfolioBalancesQuery
export function useCachedPortfolioBalancesQuery({ account }: { account?: string }) {
return usePortfolioBalancesQuery({
skip: !account,
......
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { TraceEvent } from 'analytics'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { AutoRow } from 'components/Row'
import { COMMON_BASES } from 'constants/routing'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
......@@ -30,13 +32,19 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
background-color: ${({ theme, disable }) => disable && theme.surface3};
`
const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({
const formatAnalyticsEventProperties = (
currency: Currency,
searchQuery: string,
isAddressSearch: string | false,
portfolioBalanceUsd: number | undefined
) => ({
token_symbol: currency?.symbol,
token_chain_id: currency?.chainId,
token_address: getTokenAddress(currency),
is_suggested_token: true,
is_selected_from_list: false,
is_imported_by_user: false,
total_balances_usd: portfolioBalanceUsd,
...(isAddressSearch === false
? { search_token_symbol_input: searchQuery }
: { search_token_address_input: isAddressSearch }),
......@@ -54,8 +62,12 @@ export default function CommonBases({
onSelect: (currency: Currency) => void
searchQuery: string
isAddressSearch: string | false
portfolioBalanceUsd?: number
}) {
const bases = chainId !== undefined ? COMMON_BASES[chainId] ?? [] : []
const { account } = useWeb3React()
const { data } = useCachedPortfolioBalancesQuery({ account })
const portfolioBalanceUsd = data?.portfolios?.[0].tokensTotalDenominatedValue?.value
return bases.length > 0 ? (
<AutoRow gap="4px">
......@@ -66,7 +78,7 @@ export default function CommonBases({
<TraceEvent
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
name={InterfaceEventName.TOKEN_SELECTED}
properties={formatAnalyticsEventProperties(currency, searchQuery, isAddressSearch)}
properties={formatAnalyticsEventProperties(currency, searchQuery, isAddressSearch, portfolioBalanceUsd)}
element={InterfaceElementName.COMMON_BASES_CURRENCY_BUTTON}
key={currencyId(currency)}
>
......
......@@ -3,6 +3,7 @@ import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { TraceEvent } from 'analytics'
import Loader from 'components/Icons/LoadingSpinner'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { checkWarning } from 'constants/tokenSafety'
import { TokenBalances } from 'lib/hooks/useTokenList/sorting'
......@@ -128,13 +129,15 @@ export function CurrencyRow({
const warning = currency.isNative ? null : checkWarning(currency.address)
const isBlockedToken = !!warning && !warning.canProceed
const blockedTokenOpacity = '0.6'
const { data } = useCachedPortfolioBalancesQuery({ account })
const portfolioBalanceUsd = data?.portfolios?.[0].tokensTotalDenominatedValue?.value
// only show add or remove buttons if not on selected list
return (
<TraceEvent
events={[BrowserEvent.onClick, BrowserEvent.onKeyPress]}
name={InterfaceEventName.TOKEN_SELECTED}
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
properties={{ is_imported_by_user: customAdded, ...eventProperties, total_balances_usd: portfolioBalanceUsd }}
element={InterfaceElementName.TOKEN_SELECTOR_ROW}
>
<MenuItem
......
......@@ -31,7 +31,10 @@ const GasCostItem = ({ title, amount, itemValue }: GasCostItemProps) => {
)
}
const GaslessSwapLabel = () => <UniswapXRouterLabel>$0</UniswapXRouterLabel>
const GaslessSwapLabel = () => {
const { formatNumber } = useFormatter()
return <UniswapXRouterLabel>{formatNumber({ input: 0, type: NumberType.FiatGasPrice })}</UniswapXRouterLabel>
}
type GasBreakdownTooltipProps = { trade: InterfaceTrade; hideUniswapXDescription?: boolean }
......
......@@ -11,6 +11,8 @@ import { act, render, screen } from 'test-utils/render'
import SwapDetailsDropdown from './SwapDetailsDropdown'
jest.mock('../../featureFlags/flags/useFees', () => ({ useFeesEnabled: () => true }))
describe('SwapDetailsDropdown.tsx', () => {
it('renders a trade', () => {
const { asFragment } = render(
......
......@@ -111,6 +111,7 @@ function AdvancedSwapDetails(props: SwapDetailsProps & { open: boolean }) {
<SwapLineItem {...lineItemProps} type={SwapLineItemType.MAX_SLIPPAGE} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.OUTPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.SWAP_FEE} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.NETWORK_COST} />
<Separator />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.ROUTING_INFO} />
......
......@@ -11,6 +11,8 @@ import {
} from 'test-utils/constants'
import { render } from 'test-utils/render'
jest.mock('../../featureFlags/flags/useFees', () => ({ useFeesEnabled: () => true }))
// Forces tooltips to render in snapshots
jest.mock('react-dom', () => {
const original = jest.requireActual('react-dom')
......
......@@ -6,11 +6,13 @@ import RouterLabel from 'components/RouterLabel'
import Row, { RowBetween } from 'components/Row'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { useFeesEnabled } from 'featureFlags/flags/useFees'
import useHoverProps from 'hooks/useHoverProps'
import { useUSDPrice } from 'hooks/useUSDPrice'
import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useEffect, useState } from 'react'
import { animated, SpringValue } from 'react-spring'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { InterfaceTrade, SubmittableTrade, TradeFillType } from 'state/routing/types'
import { isPreviewTrade, isUniswapXTrade } from 'state/routing/utils'
import { useUserSlippageTolerance } from 'state/user/hooks'
import { SlippageTolerance } from 'state/user/types'
......@@ -31,6 +33,7 @@ export enum SwapLineItemType {
OUTPUT_TOKEN_FEE_ON_TRANSFER,
PRICE_IMPACT,
MAX_SLIPPAGE,
SWAP_FEE,
MAXIMUM_INPUT,
MINIMUM_OUTPUT,
ROUTING_INFO,
......@@ -74,6 +77,28 @@ function FOTTooltipContent() {
)
}
function SwapFeeTooltipContent({ hasFee }: { hasFee: boolean }) {
const message = hasFee ? (
<Trans>
Fee is applied on a few token pairs to ensure the best experience with Uniswap. It is paid in the output token and
has already been factored into the quote.
</Trans>
) : (
<Trans>
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with
this swap.
</Trans>
)
return (
<>
{message}{' '}
<ExternalLink href="https://uniswap.org/docs/v2/protocol-overview/fees/">
<Trans>Learn more</Trans>
</ExternalLink>
</>
)
}
function Loading({ width = 50 }: { width?: number }) {
return <LoadingRow data-testid="loading-row" height={15} width={width} />
}
......@@ -89,6 +114,18 @@ function CurrencyAmountRow({ amount }: { amount: CurrencyAmount<Currency> }) {
return <>{`${formattedAmount} ${amount.currency.symbol}`}</>
}
function FeeRow({ trade: { swapFee, outputAmount } }: { trade: SubmittableTrade }) {
const { formatNumber } = useFormatter()
const feeCurrencyAmount = CurrencyAmount.fromRawAmount(outputAmount.currency, swapFee?.amount ?? 0)
const { data: outputFeeFiatValue } = useUSDPrice(feeCurrencyAmount, feeCurrencyAmount?.currency)
// Fallback to displaying token amount if fiat value is not available
if (outputFeeFiatValue === undefined) return <CurrencyAmountRow amount={feeCurrencyAmount} />
return <>{formatNumber({ input: outputFeeFiatValue, type: NumberType.FiatGasPrice })}</>
}
type LineItemData = {
Label: React.FC
Value: React.FC
......@@ -101,6 +138,7 @@ function useLineItem(props: SwapLineItemProps): LineItemData | undefined {
const { trade, syncing, allowedSlippage, type } = props
const { formatNumber, formatSlippage } = useFormatter()
const isAutoSlippage = useUserSlippageTolerance()[0] === SlippageTolerance.Auto
const feesEnabled = useFeesEnabled()
const isUniswapX = isUniswapXTrade(trade)
const isPreview = isPreviewTrade(trade)
......@@ -153,6 +191,19 @@ function useLineItem(props: SwapLineItemProps): LineItemData | undefined {
</Row>
),
}
case SwapLineItemType.SWAP_FEE: {
if (!feesEnabled) return
if (isPreview) return { Label: () => <Trans>Fee</Trans>, Value: () => <Loading /> }
return {
Label: () => (
<>
<Trans>Fee</Trans> {trade.swapFee && `(${formatSlippage(trade.swapFee.percent)})`}
</>
),
TooltipBody: () => <SwapFeeTooltipContent hasFee={Boolean(trade.swapFee)} />,
Value: () => <FeeRow trade={trade} />,
}
}
case SwapLineItemType.MAXIMUM_INPUT:
if (trade.tradeType === TradeType.EXACT_INPUT) return
return {
......
......@@ -3,6 +3,8 @@ import { render, screen, within } from 'test-utils/render'
import SwapModalFooter from './SwapModalFooter'
jest.mock('../../featureFlags/flags/useFees', () => ({ useFeesEnabled: () => true }))
describe('SwapModalFooter.tsx', () => {
it('matches base snapshot, test trade exact input', () => {
const { asFragment } = render(
......
......@@ -119,6 +119,7 @@ export default function SwapModalFooter({
<ExpandableLineItems {...lineItemProps} open={showMore} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.INPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.OUTPUT_TOKEN_FEE_ON_TRANSFER} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.SWAP_FEE} />
<SwapLineItem {...lineItemProps} type={SwapLineItemType.NETWORK_COST} />
</DetailsContainer>
{showAcceptChanges ? (
......
......@@ -384,6 +384,29 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div>
</div>
</div>
<div>
<div
class="c2 c3 c4"
>
<div
class="c9 c19 css-142zc9n"
data-testid="swap-li-label"
>
Fee
</div>
<div
class="c12"
>
<div>
<div
class="c9 c20 css-142zc9n"
>
0 DEF
</div>
</div>
</div>
</div>
</div>
<div>
<div
class="c2 c3 c4"
......
......@@ -228,7 +228,7 @@ exports[`SwapLineItem.tsx dutch order eth input 1`] = `
ethereum.svg
</svg>
</svg>
$0.00
$0
</div>
</div>
</div>
......@@ -592,6 +592,214 @@ exports[`SwapLineItem.tsx dutch order eth input 1`] = `
color: #7D7D7D;
}
.c10 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c10:hover {
opacity: 0.6;
}
.c10:active {
opacity: 0.4;
}
.c7 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 150ms linear,opacity 150ms linear;
transition: visibility 150ms linear,opacity 150ms linear;
color: #7D7D7D;
}
.c5 {
display: inline-block;
height: inherit;
}
.c11 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c11::before {
position: absolute;
width: 8px;
height: 8px;
box-sizing: border-box;
z-index: 9998;
content: '';
border: 1px solid #22222212;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFFFFF;
}
.c11.arrow-top {
bottom: -4px;
}
.c11.arrow-top::before {
border-top: none;
border-left: none;
}
.c11.arrow-bottom {
top: -4px;
}
.c11.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c11.arrow-left {
right: -4px;
}
.c11.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c11.arrow-right {
left: -4px;
}
.c11.arrow-right::before {
border-right: none;
border-top: none;
}
.c8 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
padding: 12px;
pointer-events: auto;
color: #222222;
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #22222212;
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
@supports (-webkit-background-clip:text) and (-webkit-text-fill-color:transparent) {
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Fee
</div>
<div
class="c5"
>
<div>
<div
class="c3 c6 css-142zc9n"
>
0 DEF
</div>
</div>
</div>
<div
class="c7"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c8"
>
<div
class="c9 css-obwv3p"
>
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with this swap.
<a
class="c10"
href="https://uniswap.org/docs/v2/protocol-overview/fees/"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
</div>
<div
class="c11 arrow-"
style="position: absolute;"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
.c7 {
z-index: 1070;
pointer-events: none;
......@@ -1801,6 +2009,25 @@ exports[`SwapLineItem.tsx exact input 1`] = `
color: #7D7D7D;
}
.c10 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c10:hover {
opacity: 0.6;
}
.c10:active {
opacity: 0.4;
}
.c7 {
z-index: 1070;
pointer-events: none;
......@@ -1816,13 +2043,13 @@ exports[`SwapLineItem.tsx exact input 1`] = `
height: inherit;
}
.c10 {
.c11 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
.c11::before {
position: absolute;
width: 8px;
height: 8px;
......@@ -1836,38 +2063,38 @@ exports[`SwapLineItem.tsx exact input 1`] = `
background: #FFFFFF;
}
.c10.arrow-top {
.c11.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
.c11.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
.c11.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
.c11.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
.c11.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
.c11.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
.c11.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
.c11.arrow-right::before {
border-right: none;
border-top: none;
}
......@@ -1907,7 +2134,7 @@ exports[`SwapLineItem.tsx exact input 1`] = `
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Receive at least
Fee
</div>
<div
class="c5"
......@@ -1916,7 +2143,7 @@ exports[`SwapLineItem.tsx exact input 1`] = `
<div
class="c3 c6 css-142zc9n"
>
0.00000000000000098 DEF
0 DEF
</div>
</div>
</div>
......@@ -1930,11 +2157,19 @@ exports[`SwapLineItem.tsx exact input 1`] = `
<div
class="c9 css-obwv3p"
>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with this swap.
<a
class="c10"
href="https://uniswap.org/docs/v2/protocol-overview/fees/"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
</div>
<div
class="c10 arrow-"
class="c11 arrow-"
style="position: absolute;"
/>
</div>
......@@ -1946,7 +2181,184 @@ exports[`SwapLineItem.tsx exact input 1`] = `
min-width: 0;
}
.c23 {
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
.c7 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 150ms linear,opacity 150ms linear;
transition: visibility 150ms linear,opacity 150ms linear;
color: #7D7D7D;
}
.c5 {
display: inline-block;
height: inherit;
}
.c10 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
position: absolute;
width: 8px;
height: 8px;
box-sizing: border-box;
z-index: 9998;
content: '';
border: 1px solid #22222212;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFFFFF;
}
.c10.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
border-right: none;
border-top: none;
}
.c8 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
padding: 12px;
pointer-events: auto;
color: #222222;
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #22222212;
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Receive at least
</div>
<div
class="c5"
>
<div>
<div
class="c3 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
</div>
<div
class="c7"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c8"
>
<div
class="c9 css-obwv3p"
>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.
</div>
</div>
<div
class="c10 arrow-"
style="position: absolute;"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c23 {
box-sizing: border-box;
margin: 0;
min-width: 0;
......@@ -3264,6 +3676,25 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
color: #7D7D7D;
}
.c10 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c10:hover {
opacity: 0.6;
}
.c10:active {
opacity: 0.4;
}
.c7 {
z-index: 1070;
pointer-events: none;
......@@ -3279,13 +3710,13 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
height: inherit;
}
.c10 {
.c11 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
.c11::before {
position: absolute;
width: 8px;
height: 8px;
......@@ -3299,38 +3730,38 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
background: #FFFFFF;
}
.c10.arrow-top {
.c11.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
.c11.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
.c11.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
.c11.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
.c11.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
.c11.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
.c11.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
.c11.arrow-right::before {
border-right: none;
border-top: none;
}
......@@ -3370,7 +3801,7 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Receive at least
Fee
</div>
<div
class="c5"
......@@ -3379,7 +3810,7 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
<div
class="c3 c6 css-142zc9n"
>
0.00000000000000098 DEF
0 DEF
</div>
</div>
</div>
......@@ -3393,11 +3824,19 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
<div
class="c9 css-obwv3p"
>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with this swap.
<a
class="c10"
href="https://uniswap.org/docs/v2/protocol-overview/fees/"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
</div>
<div
class="c10 arrow-"
class="c11 arrow-"
style="position: absolute;"
/>
</div>
......@@ -3409,13 +3848,6 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
min-width: 0;
}
.c23 {
box-sizing: border-box;
margin: 0;
min-width: 0;
width: 100%;
}
.c1 {
width: 100%;
display: -webkit-box;
......@@ -3433,24 +3865,6 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
justify-content: flex-start;
}
.c24 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 1px;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
......@@ -3458,8 +3872,210 @@ exports[`SwapLineItem.tsx exact input api 1`] = `
justify-content: space-between;
}
.c25 {
-webkit-flex-wrap: wrap;
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
.c7 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 150ms linear,opacity 150ms linear;
transition: visibility 150ms linear,opacity 150ms linear;
color: #7D7D7D;
}
.c5 {
display: inline-block;
height: inherit;
}
.c10 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
position: absolute;
width: 8px;
height: 8px;
box-sizing: border-box;
z-index: 9998;
content: '';
border: 1px solid #22222212;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFFFFF;
}
.c10.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
border-right: none;
border-top: none;
}
.c8 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
padding: 12px;
pointer-events: auto;
color: #222222;
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #22222212;
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Receive at least
</div>
<div
class="c5"
>
<div>
<div
class="c3 c6 css-142zc9n"
>
0.00000000000000098 DEF
</div>
</div>
</div>
<div
class="c7"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c8"
>
<div
class="c9 css-obwv3p"
>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.
</div>
</div>
<div
class="c10 arrow-"
style="position: absolute;"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c23 {
box-sizing: border-box;
margin: 0;
min-width: 0;
width: 100%;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c24 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 1px;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c25 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin: -1px;
......@@ -4727,6 +5343,25 @@ exports[`SwapLineItem.tsx exact output 1`] = `
color: #7D7D7D;
}
.c10 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c10:hover {
opacity: 0.6;
}
.c10:active {
opacity: 0.4;
}
.c7 {
z-index: 1070;
pointer-events: none;
......@@ -4742,13 +5377,13 @@ exports[`SwapLineItem.tsx exact output 1`] = `
height: inherit;
}
.c10 {
.c11 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
.c11::before {
position: absolute;
width: 8px;
height: 8px;
......@@ -4762,38 +5397,38 @@ exports[`SwapLineItem.tsx exact output 1`] = `
background: #FFFFFF;
}
.c10.arrow-top {
.c11.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
.c11.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
.c11.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
.c11.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
.c11.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
.c11.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
.c11.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
.c11.arrow-right::before {
border-right: none;
border-top: none;
}
......@@ -4833,7 +5468,7 @@ exports[`SwapLineItem.tsx exact output 1`] = `
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Pay at most
Fee
</div>
<div
class="c5"
......@@ -4842,7 +5477,7 @@ exports[`SwapLineItem.tsx exact output 1`] = `
<div
class="c3 c6 css-142zc9n"
>
0.00000000000000102 ABC
0 GHI
</div>
</div>
</div>
......@@ -4856,11 +5491,19 @@ exports[`SwapLineItem.tsx exact output 1`] = `
<div
class="c9 css-obwv3p"
>
The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will revert.
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with this swap.
<a
class="c10"
href="https://uniswap.org/docs/v2/protocol-overview/fees/"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
</div>
<div
class="c10 arrow-"
class="c11 arrow-"
style="position: absolute;"
/>
</div>
......@@ -4872,7 +5515,184 @@ exports[`SwapLineItem.tsx exact output 1`] = `
min-width: 0;
}
.c23 {
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
.c7 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 150ms linear,opacity 150ms linear;
transition: visibility 150ms linear,opacity 150ms linear;
color: #7D7D7D;
}
.c5 {
display: inline-block;
height: inherit;
}
.c10 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
position: absolute;
width: 8px;
height: 8px;
box-sizing: border-box;
z-index: 9998;
content: '';
border: 1px solid #22222212;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFFFFF;
}
.c10.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
border-right: none;
border-top: none;
}
.c8 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
padding: 12px;
pointer-events: auto;
color: #222222;
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #22222212;
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Pay at most
</div>
<div
class="c5"
>
<div>
<div
class="c3 c6 css-142zc9n"
>
0.00000000000000102 ABC
</div>
</div>
</div>
<div
class="c7"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c8"
>
<div
class="c9 css-obwv3p"
>
The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will revert.
</div>
</div>
<div
class="c10 arrow-"
style="position: absolute;"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c23 {
box-sizing: border-box;
margin: 0;
min-width: 0;
......@@ -6398,6 +7218,25 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
color: #7D7D7D;
}
.c10 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c10:hover {
opacity: 0.6;
}
.c10:active {
opacity: 0.4;
}
.c7 {
z-index: 1070;
pointer-events: none;
......@@ -6413,13 +7252,13 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
height: inherit;
}
.c10 {
.c11 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
.c11::before {
position: absolute;
width: 8px;
height: 8px;
......@@ -6433,38 +7272,38 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
background: #FFFFFF;
}
.c10.arrow-top {
.c11.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
.c11.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
.c11.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
.c11.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
.c11.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
.c11.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
.c11.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
.c11.arrow-right::before {
border-right: none;
border-top: none;
}
......@@ -6504,7 +7343,7 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Receive at least
Fee
</div>
<div
class="c5"
......@@ -6513,7 +7352,7 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
<div
class="c3 c6 css-142zc9n"
>
0.000000000000000952 DEF
0 DEF
</div>
</div>
</div>
......@@ -6527,11 +7366,19 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
<div
class="c9 css-obwv3p"
>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with this swap.
<a
class="c10"
href="https://uniswap.org/docs/v2/protocol-overview/fees/"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
</div>
<div
class="c10 arrow-"
class="c11 arrow-"
style="position: absolute;"
/>
</div>
......@@ -6543,13 +7390,6 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
min-width: 0;
}
.c23 {
box-sizing: border-box;
margin: 0;
min-width: 0;
width: 100%;
}
.c1 {
width: 100%;
display: -webkit-box;
......@@ -6567,24 +7407,6 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
justify-content: flex-start;
}
.c24 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 1px;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
......@@ -6592,8 +7414,210 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
justify-content: space-between;
}
.c25 {
-webkit-flex-wrap: wrap;
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
.c7 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 150ms linear,opacity 150ms linear;
transition: visibility 150ms linear,opacity 150ms linear;
color: #7D7D7D;
}
.c5 {
display: inline-block;
height: inherit;
}
.c10 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c10::before {
position: absolute;
width: 8px;
height: 8px;
box-sizing: border-box;
z-index: 9998;
content: '';
border: 1px solid #22222212;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFFFFF;
}
.c10.arrow-top {
bottom: -4px;
}
.c10.arrow-top::before {
border-top: none;
border-left: none;
}
.c10.arrow-bottom {
top: -4px;
}
.c10.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c10.arrow-left {
right: -4px;
}
.c10.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c10.arrow-right {
left: -4px;
}
.c10.arrow-right::before {
border-right: none;
border-top: none;
}
.c8 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
padding: 12px;
pointer-events: auto;
color: #222222;
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #22222212;
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Receive at least
</div>
<div
class="c5"
>
<div>
<div
class="c3 c6 css-142zc9n"
>
0.000000000000000952 DEF
</div>
</div>
</div>
<div
class="c7"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c8"
>
<div
class="c9 css-obwv3p"
>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.
</div>
</div>
<div
class="c10 arrow-"
style="position: absolute;"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c23 {
box-sizing: border-box;
margin: 0;
min-width: 0;
width: 100%;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c24 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 1px;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c25 {
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin: -1px;
......@@ -7798,11 +8822,278 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
color: #222222;
}
.c8 {
.c8 {
color: #7D7D7D;
}
.c14 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
color: #FC72FF;
stroke: #FC72FF;
font-weight: 500;
}
.c14:hover {
opacity: 0.6;
}
.c14:active {
opacity: 0.4;
}
.c13 {
width: 100%;
height: 1px;
background-color: #22222212;
}
.c10 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
opacity: 0;
-webkit-transition: visibility 150ms linear,opacity 150ms linear;
transition: visibility 150ms linear,opacity 150ms linear;
color: #7D7D7D;
}
.c5 {
display: inline-block;
height: inherit;
}
.c15 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c15::before {
position: absolute;
width: 8px;
height: 8px;
box-sizing: border-box;
z-index: 9998;
content: '';
border: 1px solid #22222212;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background: #FFFFFF;
}
.c15.arrow-top {
bottom: -4px;
}
.c15.arrow-top::before {
border-top: none;
border-left: none;
}
.c15.arrow-bottom {
top: -4px;
}
.c15.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c15.arrow-left {
right: -4px;
}
.c15.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c15.arrow-right {
left: -4px;
}
.c15.arrow-right::before {
border-right: none;
border-top: none;
}
.c11 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
padding: 12px;
pointer-events: auto;
color: #222222;
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #22222212;
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c12 {
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;
}
.c6 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
.c9 {
background: #22222212;
border-radius: 8px;
color: #7D7D7D;
height: 20px;
padding: 0 6px;
}
.c9::after {
content: 'Auto';
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Max. slippage
</div>
<div
class="c5"
>
<div>
<div
class="c3 c6 css-142zc9n"
>
<div
class="c0 c7"
>
<div
class="c8 c9 css-1lgneq0"
/>
2%
</div>
</div>
</div>
</div>
<div
class="c10"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c11"
>
<div
class="c8 css-obwv3p"
>
<div
class="c12"
>
<div
class="c0 c1 c2"
>
<div
class="c3 css-obwv3p"
>
Receive at least
</div>
<div
class="c3 css-obwv3p"
>
0.000000000000000952 DEF
</div>
</div>
<div
class="c13"
/>
<div>
If the price moves so that you will receive less than 0.000000000000000952 DEF, your transaction will be reverted. This is the minimum amount you are guaranteed to receive.
<a
class="c14"
href="https://support.uniswap.org/hc/en-us/articles/8643879653261-What-is-Price-Slippage-"
rel="noopener noreferrer"
target="_blank"
>
Learn more
</a>
</div>
</div>
</div>
</div>
<div
class="c15 arrow-"
style="position: absolute;"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
.c14 {
.c10 {
-webkit-text-decoration: none;
text-decoration: none;
cursor: pointer;
......@@ -7813,21 +9104,15 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
font-weight: 500;
}
.c14:hover {
.c10:hover {
opacity: 0.6;
}
.c14:active {
.c10:active {
opacity: 0.4;
}
.c13 {
width: 100%;
height: 1px;
background-color: #22222212;
}
.c10 {
.c7 {
z-index: 1070;
pointer-events: none;
visibility: hidden;
......@@ -7842,13 +9127,13 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
height: inherit;
}
.c15 {
.c11 {
width: 8px;
height: 8px;
z-index: 9998;
}
.c15::before {
.c11::before {
position: absolute;
width: 8px;
height: 8px;
......@@ -7862,43 +9147,43 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
background: #FFFFFF;
}
.c15.arrow-top {
.c11.arrow-top {
bottom: -4px;
}
.c15.arrow-top::before {
.c11.arrow-top::before {
border-top: none;
border-left: none;
}
.c15.arrow-bottom {
.c11.arrow-bottom {
top: -4px;
}
.c15.arrow-bottom::before {
.c11.arrow-bottom::before {
border-bottom: none;
border-right: none;
}
.c15.arrow-left {
.c11.arrow-left {
right: -4px;
}
.c15.arrow-left::before {
.c11.arrow-left::before {
border-bottom: none;
border-left: none;
}
.c15.arrow-right {
.c11.arrow-right {
left: -4px;
}
.c15.arrow-right::before {
.c11.arrow-right::before {
border-right: none;
border-top: none;
}
.c11 {
.c8 {
max-width: 256px;
width: calc(100vw - 16px);
cursor: default;
......@@ -7915,21 +9200,6 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
box-shadow: 0 4px 8px 0 rgba(47,128,237,0.1);
}
.c12 {
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;
}
.c6 {
text-align: right;
overflow-wrap: break-word;
......@@ -7940,18 +9210,6 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
color: #7D7D7D;
}
.c9 {
background: #22222212;
border-radius: 8px;
color: #7D7D7D;
height: 20px;
padding: 0 6px;
}
.c9::after {
content: 'Auto';
}
<div>
<div
class="c0 c1 c2"
......@@ -7960,7 +9218,7 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Max. slippage
Fee
</div>
<div
class="c5"
......@@ -7969,52 +9227,24 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
<div
class="c3 c6 css-142zc9n"
>
<div
class="c0 c7"
>
<div
class="c8 c9 css-1lgneq0"
/>
2%
</div>
0 DEF
</div>
</div>
</div>
<div
class="c10"
class="c7"
style="position: fixed; left: 0px; top: 0px;"
>
<div
class="c11"
>
<div
class="c8 css-obwv3p"
>
<div
class="c12"
>
<div
class="c0 c1 c2"
>
<div
class="c3 css-obwv3p"
class="c8"
>
Receive at least
</div>
<div
class="c3 css-obwv3p"
class="c9 css-obwv3p"
>
0.000000000000000952 DEF
</div>
</div>
<div
class="c13"
/>
<div>
If the price moves so that you will receive less than 0.000000000000000952 DEF, your transaction will be reverted. This is the minimum amount you are guaranteed to receive.
Fee is applied on a few token pairs to ensure the best experience with Uniswap. There is no fee associated with this swap.
<a
class="c14"
href="https://support.uniswap.org/hc/en-us/articles/8643879653261-What-is-Price-Slippage-"
class="c10"
href="https://uniswap.org/docs/v2/protocol-overview/fees/"
rel="noopener noreferrer"
target="_blank"
>
......@@ -8022,10 +9252,8 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
</a>
</div>
</div>
</div>
</div>
<div
class="c15 arrow-"
class="c11 arrow-"
style="position: absolute;"
/>
</div>
......@@ -9509,6 +10737,85 @@ exports[`SwapLineItem.tsx preview exact in 1`] = `
color: #222222;
}
.c6 {
-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, #FFFFFF 25%, #22222212 50%, #FFFFFF 75% );
background-size: 400%;
will-change: background-position;
border-radius: 12px;
height: 15px;
width: 50px;
}
.c5 {
text-align: right;
overflow-wrap: break-word;
}
.c4 {
cursor: auto;
color: #7D7D7D;
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Fee
</div>
<div
class="c3 c5 css-142zc9n"
>
<div
class="c6"
data-testid="loading-row"
height="15"
width="50"
/>
</div>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
}
.c9 {
color: #7D7D7D;
}
......@@ -9980,6 +11287,76 @@ exports[`SwapLineItem.tsx syncing 1`] = `
color: #222222;
}
.c5 {
-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, #FFFFFF 25%, #22222212 50%, #FFFFFF 75% );
background-size: 400%;
will-change: background-position;
border-radius: 12px;
height: 15px;
width: 50px;
}
.c4 {
cursor: help;
color: #7D7D7D;
}
<div>
<div
class="c0 c1 c2"
>
<div
class="c3 c4 css-142zc9n"
data-testid="swap-li-label"
>
Fee
</div>
<div
class="c5"
data-testid="loading-row"
height="15"
width="50"
/>
</div>
</div>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c1 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c3 {
color: #222222;
}
.c5 {
-webkit-animation: fAQEyV 1.5s infinite;
animation: fAQEyV 1.5s infinite;
......
......@@ -354,6 +354,29 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
</div>
</div>
</div>
<div>
<div
class="c2 c3 c4"
>
<div
class="c5 c6 css-142zc9n"
data-testid="swap-li-label"
>
Fee
</div>
<div
class="c7"
>
<div>
<div
class="c5 c8 css-142zc9n"
>
0 DEF
</div>
</div>
</div>
</div>
</div>
<div>
<div
class="c2 c3 c4"
......@@ -938,6 +961,28 @@ exports[`SwapModalFooter.tsx renders a preview trade while disabling submission
</div>
</div>
</div>
<div>
<div
class="c2 c3 c4"
>
<div
class="c5 c6 css-142zc9n"
data-testid="swap-li-label"
>
Fee
</div>
<div
class="c5 c7 css-142zc9n"
>
<div
class="c11"
data-testid="loading-row"
height="15"
width="50"
/>
</div>
</div>
</div>
<div>
<div
class="c2 c3 c4"
......
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFeesEnabledFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.feesEnabled)
}
export function useFeesEnabled(): boolean {
return useFeesEnabledFlag() === BaseVariant.Enabled
}
......@@ -22,6 +22,7 @@ export enum FeatureFlag {
uniswapXDefaultEnabled = 'uniswapx_default_enabled',
quickRouteMainnet = 'enable_quick_route_mainnet',
progressIndicatorV2 = 'progress_indicator_v2',
feesEnabled = 'fees_enabled',
}
interface FeatureFlagsContextType {
......
import { Percent, TradeType } from '@uniswap/sdk-core'
import { FlatFeeOptions } from '@uniswap/universal-router-sdk'
import { FeeOptions } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { BigNumber } from 'ethers/lib/ethers'
import { PermitSignature } from 'hooks/usePermitAllowance'
import { useCallback } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
......@@ -20,11 +23,24 @@ import { useUniversalRouterSwapCallback } from './useUniversalRouter'
export type SwapResult = Awaited<ReturnType<ReturnType<typeof useSwapCallback>>>
type UniversalRouterFeeField = { feeOptions: FeeOptions } | { flatFeeOptions: FlatFeeOptions }
function getUniversalRouterFeeFields(trade?: InterfaceTrade): UniversalRouterFeeField | undefined {
if (!isClassicTrade(trade)) return undefined
if (!trade.swapFee) return undefined
if (trade.tradeType === TradeType.EXACT_INPUT) {
return { feeOptions: { fee: trade.swapFee.percent, recipient: trade.swapFee.recipient } }
} else {
return { flatFeeOptions: { amount: BigNumber.from(trade.swapFee.amount), recipient: trade.swapFee.recipient } }
}
}
// Returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
trade: InterfaceTrade | undefined, // trade to execute, required
fiatValues: { amountIn?: number; amountOut?: number }, // usd values for amount in and out, logged for analytics
fiatValues: { amountIn?: number; amountOut?: number; feeUsd?: number }, // usd values for amount in and out, and the fee value, logged for analytics
allowedSlippage: Percent, // in bips
permitSignature: PermitSignature | undefined
) {
......@@ -47,6 +63,7 @@ export function useSwapCallback(
slippageTolerance: allowedSlippage,
deadline,
permit: permitSignature,
...getUniversalRouterFeeFields(trade),
}
)
......
......@@ -5,6 +5,7 @@ import { Percent } from '@uniswap/sdk-core'
import { DutchOrder, DutchOrderBuilder } from '@uniswap/uniswapx-sdk'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent, useTrace } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react'
import { DutchOrderTrade, TradeFillType } from 'state/routing/types'
......@@ -50,12 +51,15 @@ export function useUniswapXSwapCallback({
fiatValues,
}: {
trade?: DutchOrderTrade
fiatValues: { amountIn?: number; amountOut?: number }
fiatValues: { amountIn?: number; amountOut?: number; feeUsd?: number }
allowedSlippage: Percent
}) {
const { account, provider } = useWeb3React()
const analyticsContext = useTrace()
const { data } = useCachedPortfolioBalancesQuery({ account })
const portfolioBalanceUsd = data?.portfolios?.[0]?.tokensTotalDenominatedValue?.value
return useCallback(
async () =>
trace('swapx.send', async ({ setTraceData, setTraceStatus }) => {
......@@ -82,7 +86,7 @@ export function useUniswapXSwapCallback({
.decayEndTime(endTime)
.deadline(deadline)
.swapper(account)
.nonFeeRecipient(account)
.nonFeeRecipient(account, trade.swapFee?.recipient)
// if fetching the nonce fails for any reason, default to existing nonce from the Swap quote.
.nonce(updatedNonce ?? trade.order.info.nonce)
.build()
......@@ -115,6 +119,7 @@ export function useUniswapXSwapCallback({
allowedSlippage,
fiatValues,
timeToSignSinceRequestMs: Date.now() - beforeSign,
portfolioBalanceUsd,
}),
...analyticsContext,
})
......@@ -139,6 +144,7 @@ export function useUniswapXSwapCallback({
trade,
allowedSlippage,
fiatValues,
portfolioBalanceUsd,
}),
...analyticsContext,
errorCode: body.errorCode,
......@@ -154,6 +160,6 @@ export function useUniswapXSwapCallback({
response: { orderHash: body.hash, deadline: updatedOrder.info.deadline },
}
}),
[account, provider, trade, allowedSlippage, fiatValues, analyticsContext]
[account, provider, trade, allowedSlippage, fiatValues, analyticsContext, portfolioBalanceUsd]
)
}
......@@ -2,10 +2,11 @@ import { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro'
import { SwapEventName } from '@uniswap/analytics-events'
import { Percent } from '@uniswap/sdk-core'
import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { FlatFeeOptions, SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { FeeOptions, toHex } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent, useTrace } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { formatCommonPropertiesForTrade, formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react'
......@@ -43,17 +44,20 @@ interface SwapOptions {
deadline?: BigNumber
permit?: PermitSignature
feeOptions?: FeeOptions
flatFeeOptions?: FlatFeeOptions
}
export function useUniversalRouterSwapCallback(
trade: ClassicTrade | undefined,
fiatValues: { amountIn?: number; amountOut?: number },
fiatValues: { amountIn?: number; amountOut?: number; feeUsd?: number },
options: SwapOptions
) {
const { account, chainId, provider } = useWeb3React()
const analyticsContext = useTrace()
const blockNumber = useBlockNumber()
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
const { data } = useCachedPortfolioBalancesQuery({ account })
const portfolioBalanceUsd = data?.portfolios?.[0]?.tokensTotalDenominatedValue?.value
return useCallback(async () => {
return trace('swap.send', async ({ setTraceData, setTraceStatus, setTraceError }) => {
......@@ -76,6 +80,7 @@ export function useUniversalRouterSwapCallback(
deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit,
fee: options.feeOptions,
flatFee: options.flatFeeOptions,
})
const tx = {
......@@ -117,6 +122,7 @@ export function useUniversalRouterSwapCallback(
allowedSlippage: options.slippageTolerance,
fiatValues,
txHash: response.hash,
portfolioBalanceUsd,
}),
...analyticsContext,
})
......@@ -154,16 +160,14 @@ export function useUniversalRouterSwapCallback(
})
}, [
account,
analyticsContext,
blockNumber,
chainId,
fiatValues,
options.deadline,
options.feeOptions,
options.permit,
options.slippageTolerance,
provider,
trade,
options,
analyticsContext,
blockNumber,
isAutoSlippage,
fiatValues,
portfolioBalanceUsd,
])
}
......@@ -4,6 +4,7 @@ import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXEthOutputEnabled } from 'featureFlags/flags/uniswapXEthOutput'
import { useUniswapXExactOutputEnabled } from 'featureFlags/flags/uniswapXExactOutput'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabled } from 'featureFlags/flags/useFees'
import { useMemo } from 'react'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types'
import { currencyAddressForSwapQuote } from 'state/routing/utils'
......@@ -40,6 +41,10 @@ export function useRoutingAPIArguments({
const uniswapXExactOutputEnabled = useUniswapXExactOutputEnabled()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const feesEnabled = useFeesEnabled()
// Don't enable fee logic if this is a quote for pricing
const sendPortionEnabled = routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? false : feesEnabled
return useMemo(
() =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped)
......@@ -64,6 +69,7 @@ export function useRoutingAPIArguments({
uniswapXEthOutputEnabled,
uniswapXExactOutputEnabled,
isUniswapXDefaultEnabled,
sendPortionEnabled,
inputTax,
outputTax,
},
......@@ -80,6 +86,7 @@ export function useRoutingAPIArguments({
userOptedOutOfUniswapX,
uniswapXEthOutputEnabled,
isUniswapXDefaultEnabled,
sendPortionEnabled,
inputTax,
outputTax,
]
......
import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { InterfaceTrade, QuoteMethod } from 'state/routing/types'
import { InterfaceTrade, QuoteMethod, SubmittableTrade } from 'state/routing/types'
import { isClassicTrade, isSubmittableTrade, isUniswapXTrade } from 'state/routing/utils'
import { computeRealizedPriceImpact } from 'utils/prices'
......@@ -35,7 +35,11 @@ function getEstimatedNetworkFee(trade: InterfaceTrade) {
return undefined
}
export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSlippage: Percent) {
export function formatCommonPropertiesForTrade(
trade: InterfaceTrade,
allowedSlippage: Percent,
outputFeeFiatValue?: number
) {
return {
routing: trade.fillType,
type: trade.tradeType,
......@@ -47,7 +51,7 @@ export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSli
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_out_amount: formatToDecimal(trade.postTaxOutputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: isClassicTrade(trade)
? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade))
: undefined,
......@@ -59,6 +63,7 @@ export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSli
minimum_output_after_slippage: trade.minimumAmountOut(allowedSlippage).toSignificant(6),
allowed_slippage: formatPercentNumber(allowedSlippage),
method: getQuoteMethod(trade),
fee_usd: outputFeeFiatValue,
}
}
......@@ -68,19 +73,22 @@ export const formatSwapSignedAnalyticsEventProperties = ({
fiatValues,
txHash,
timeToSignSinceRequestMs,
portfolioBalanceUsd,
}: {
trade: InterfaceTrade
trade: SubmittableTrade
allowedSlippage: Percent
fiatValues: { amountIn?: number; amountOut?: number }
fiatValues: { amountIn?: number; amountOut?: number; feeUsd?: number }
txHash?: string
timeToSignSinceRequestMs?: number
portfolioBalanceUsd?: number
}) => ({
total_balances_usd: portfolioBalanceUsd,
transaction_hash: txHash,
token_in_amount_usd: fiatValues.amountIn,
token_out_amount_usd: fiatValues.amountOut,
// measures the amount of time the user took to sign the permit message or swap tx in their wallet
time_to_sign_since_request_ms: timeToSignSinceRequestMs,
...formatCommonPropertiesForTrade(trade, allowedSlippage),
...formatCommonPropertiesForTrade(trade, allowedSlippage, fiatValues.feeUsd),
})
function getQuoteMethod(trade: InterfaceTrade) {
......@@ -94,10 +102,11 @@ export const formatSwapQuoteReceivedEventProperties = (
allowedSlippage: Percent,
swapQuoteLatencyMs: number | undefined,
inputTax: Percent,
outputTax: Percent
outputTax: Percent,
outputFeeFiatValue: number | undefined
) => {
return {
...formatCommonPropertiesForTrade(trade, allowedSlippage),
...formatCommonPropertiesForTrade(trade, allowedSlippage, outputFeeFiatValue),
swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
token_in_amount_max: trade.maximumAmountIn(allowedSlippage).toExact(),
......
......@@ -294,6 +294,7 @@ export function Swap({
inputError: swapInputError,
inputTax,
outputTax,
outputFeeFiatValue,
} = swapInfo
const [inputTokenHasTax, outputTokenHasTax] = useMemo(
......@@ -441,8 +442,8 @@ export function Swap({
)
const showMaxButton = Boolean(maxInputAmount?.greaterThan(0) && !parsedAmounts[Field.INPUT]?.equalTo(maxInputAmount))
const swapFiatValues = useMemo(() => {
return { amountIn: fiatValueTradeInput.data, amountOut: fiatValueTradeOutput.data }
}, [fiatValueTradeInput, fiatValueTradeOutput])
return { amountIn: fiatValueTradeInput.data, amountOut: fiatValueTradeOutput.data, feeUsd: outputFeeFiatValue }
}, [fiatValueTradeInput.data, fiatValueTradeOutput.data, outputFeeFiatValue])
// the callback to execute the swap
const swapCallback = useSwapCallback(
......@@ -584,10 +585,17 @@ export function Swap({
if (!trade || prevTrade === trade) return // no new swap quote to log
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, {
...formatSwapQuoteReceivedEventProperties(trade, allowedSlippage, swapQuoteLatency, inputTax, outputTax),
...formatSwapQuoteReceivedEventProperties(
trade,
allowedSlippage,
swapQuoteLatency,
inputTax,
outputTax,
outputFeeFiatValue
),
...trace,
})
}, [prevTrade, trade, trace, allowedSlippage, swapQuoteLatency, inputTax, outputTax])
}, [prevTrade, trade, trace, allowedSlippage, swapQuoteLatency, inputTax, outputTax, outputFeeFiatValue])
const showDetailsDropdown = Boolean(
!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)
......
......@@ -35,6 +35,8 @@ const protocols: Protocol[] = [Protocol.V2, Protocol.V3, Protocol.MIXED]
// routing API quote query params: https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/schema/quote-schema.ts
const DEFAULT_QUERY_PARAMS = {
protocols,
// this should be removed once BE fixes issue where enableUniversalRouter is required for fees to work
enableUniversalRouter: true,
}
function getQuoteLatencyMeasure(mark: PerformanceMark): PerformanceMeasure {
......@@ -66,6 +68,7 @@ function getRoutingAPIConfig(args: GetQuoteArgs): RoutingConfig {
const classic = {
...DEFAULT_QUERY_PARAMS,
routingType: URAQuoteType.CLASSIC,
recipient: account,
}
const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets)
......@@ -124,16 +127,24 @@ export const routingApi = createApi({
logSwapQuoteRequest(args.tokenInChainId, args.routerPreference, false)
const quoteStartMark = performance.mark(`quote-fetch-start-${Date.now()}`)
try {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
const type = isExactInput(tradeType) ? 'EXACT_INPUT' : 'EXACT_OUTPUT'
const {
tokenInAddress: tokenIn,
tokenInChainId,
tokenOutAddress: tokenOut,
tokenOutChainId,
amount,
tradeType,
sendPortionEnabled,
} = args
const requestBody = {
tokenInChainId,
tokenIn: tokenInAddress,
tokenIn,
tokenOutChainId,
tokenOut: tokenOutAddress,
tokenOut,
amount,
type,
sendPortionEnabled,
type: isExactInput(tradeType) ? 'EXACT_INPUT' : 'EXACT_OUTPUT',
intent: args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? 'pricing' : undefined,
configs: getRoutingAPIConfig(args),
}
......
......@@ -50,6 +50,7 @@ export interface GetQuoteArgs {
// temporary field indicating the user disabled UniswapX during the transition to the opt-out model
userOptedOutOfUniswapX: boolean
isUniswapXDefaultEnabled: boolean
sendPortionEnabled: boolean
inputTax: Percent
outputTax: Percent
}
......@@ -112,11 +113,11 @@ export interface ClassicQuoteData {
blockNumber: string
amount: string
amountDecimals: string
gasPriceWei: string
gasUseEstimate: string
gasUseEstimateQuote: string
gasUseEstimateQuoteDecimals: string
gasUseEstimateUSD: string
gasPriceWei?: string
gasUseEstimate?: string
gasUseEstimateQuote?: string
gasUseEstimateQuoteDecimals?: string
gasUseEstimateUSD?: string
methodParameters?: { calldata: string; value: string }
quote: string
quoteDecimals: string
......@@ -124,11 +125,15 @@ export interface ClassicQuoteData {
quoteGasAdjustedDecimals: string
route: Array<(V3PoolInRoute | V2PoolInRoute)[]>
routeString: string
portionBips?: number
portionRecipient?: string
portionAmount?: string
portionAmountDecimals?: string
quoteGasAndPortionAdjusted?: string
quoteGasAndPortionAdjustedDecimals?: string
}
type URADutchOrderQuoteResponse = {
routing: URAQuoteType.DUTCH_LIMIT
quote: {
export type URADutchOrderQuoteData = {
auctionPeriodSecs: number
deadlineBufferSecs: number
startTimeBufferSecs: number
......@@ -136,7 +141,14 @@ type URADutchOrderQuoteResponse = {
quoteId?: string
requestId?: string
slippageTolerance: string
}
portionBips?: number
portionRecipient?: string
portionAmount?: string
}
type URADutchOrderQuoteResponse = {
routing: URAQuoteType.DUTCH_LIMIT
quote: URADutchOrderQuoteData
allQuotes: Array<URAQuoteResponse>
}
type URAClassicQuoteResponse = {
......@@ -179,6 +191,8 @@ export enum TradeFillType {
export type ApproveInfo = { needsApprove: true; approveGasEstimateUSD: number } | { needsApprove: false }
export type WrapInfo = { needsWrap: true; wrapGasEstimateUSD: number } | { needsWrap: false }
export type SwapFeeInfo = { recipient: string; percent: Percent; amount: string /* raw amount of output token */ }
export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
public readonly fillType = TradeFillType.Classic
approveInfo: ApproveInfo
......@@ -189,6 +203,7 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
quoteMethod: QuoteMethod
inputTax: Percent
outputTax: Percent
swapFee: SwapFeeInfo | undefined
constructor({
gasUseEstimateUSD,
......@@ -199,6 +214,7 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
approveInfo,
inputTax,
outputTax,
swapFee,
...routes
}: {
gasUseEstimateUSD?: number
......@@ -210,6 +226,7 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
approveInfo: ApproveInfo
inputTax: Percent
outputTax: Percent
swapFee?: SwapFeeInfo
v2Routes: {
routev2: V2Route<Currency, Currency>
inputAmount: CurrencyAmount<Currency>
......@@ -236,17 +253,33 @@ export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
this.approveInfo = approveInfo
this.inputTax = inputTax
this.outputTax = outputTax
this.swapFee = swapFee
}
public get executionPrice(): Price<Currency, Currency> {
if (this.tradeType === TradeType.EXACT_INPUT || !this.swapFee) return super.executionPrice
// Fix inaccurate price calculation for exact output trades
return new Price({ baseAmount: this.inputAmount, quoteAmount: this.postSwapFeeOutputAmount })
}
public get totalTaxRate(): Percent {
return this.inputTax.add(this.outputTax)
}
public get postSwapFeeOutputAmount(): CurrencyAmount<Currency> {
// Routing api already applies the swap fee to the output amount for exact-in
if (this.tradeType === TradeType.EXACT_INPUT) return this.outputAmount
const swapFeeAmount = CurrencyAmount.fromRawAmount(this.outputAmount.currency, this.swapFee?.amount ?? 0)
return this.outputAmount.subtract(swapFeeAmount)
}
public get postTaxOutputAmount() {
// Ideally we should calculate the final output amount by ammending the inputAmount based on the input tax and then applying the output tax,
// but this isn't currently possible because V2Trade reconstructs the total inputAmount based on the swap routes
// TODO(WEB-2761): Amend V2Trade objects in the v2-sdk to have a separate field for post-input tax routes
return this.outputAmount.multiply(new Fraction(ONE).subtract(this.totalTaxRate))
return this.postSwapFeeOutputAmount.multiply(new Fraction(ONE).subtract(this.totalTaxRate))
}
public minimumAmountOut(slippageTolerance: Percent, amountOut = this.outputAmount): CurrencyAmount<Currency> {
......@@ -281,6 +314,7 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
inputTax = ZERO_PERCENT
outputTax = ZERO_PERCENT
swapFee: SwapFeeInfo | undefined
constructor({
currencyIn,
......@@ -296,6 +330,7 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
startTimeBufferSecs,
deadlineBufferSecs,
slippageTolerance,
swapFee,
}: {
currencyIn: Currency
currenciesOut: Currency[]
......@@ -310,6 +345,7 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
startTimeBufferSecs: number
deadlineBufferSecs: number
slippageTolerance: Percent
swapFee?: SwapFeeInfo
}) {
super({ currencyIn, currenciesOut, orderInfo, tradeType })
this.quoteId = quoteId
......@@ -321,6 +357,7 @@ export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeT
this.deadlineBufferSecs = deadlineBufferSecs
this.slippageTolerance = slippageTolerance
this.startTimeBufferSecs = startTimeBufferSecs
this.swapFee = swapFee
}
public get totalGasUseEstimateUSD(): number {
......
import { BigNumber } from '@ethersproject/bignumber'
import { MixedRouteSDK } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { DutchOrderInfo, DutchOrderInfoJSON } from '@uniswap/uniswapx-sdk'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
import { BIPS_BASE } from 'constants/misc'
import { isAvalanche, isBsc, isMatic, nativeOnChain } from 'constants/tokens'
import { toSlippagePercent } from 'utils/slippage'
......@@ -23,9 +24,11 @@ import {
QuoteState,
RouterPreference,
SubmittableTrade,
SwapFeeInfo,
SwapRouterNativeAssets,
TradeFillType,
TradeResult,
URADutchOrderQuoteData,
URAQuoteResponse,
URAQuoteType,
V2PoolInRoute,
......@@ -152,6 +155,18 @@ function getTradeCurrencies(args: GetQuoteArgs | GetQuickQuoteArgs, isUniswapXTr
return [currencyIn.isNative ? currencyIn.wrapped : currencyIn, currencyOut]
}
function getSwapFee(data: ClassicQuoteData | URADutchOrderQuoteData): SwapFeeInfo | undefined {
const { portionAmount, portionBips, portionRecipient } = data
if (!portionAmount || !portionBips || !portionRecipient) return undefined
return {
recipient: portionRecipient,
percent: new Percent(portionBips, BIPS_BASE),
amount: portionAmount,
}
}
function getClassicTradeDetails(
currencyIn: Currency,
currencyOut: Currency,
......@@ -161,14 +176,19 @@ function getClassicTradeDetails(
gasUseEstimateUSD?: number
blockNumber?: string
routes?: RouteResult[]
swapFee?: SwapFeeInfo
} {
const classicQuote =
data.routing === URAQuoteType.CLASSIC ? data.quote : data.allQuotes.find(isClassicQuoteResponse)?.quote
if (!classicQuote) return {}
return {
gasUseEstimate: classicQuote?.gasUseEstimate ? parseFloat(classicQuote.gasUseEstimate) : undefined,
gasUseEstimateUSD: classicQuote?.gasUseEstimateUSD ? parseFloat(classicQuote.gasUseEstimateUSD) : undefined,
blockNumber: classicQuote?.blockNumber,
routes: classicQuote ? computeRoutes(currencyIn, currencyOut, classicQuote.route) : undefined,
gasUseEstimate: classicQuote.gasUseEstimate ? parseFloat(classicQuote.gasUseEstimate) : undefined,
gasUseEstimateUSD: classicQuote.gasUseEstimateUSD ? parseFloat(classicQuote.gasUseEstimateUSD) : undefined,
blockNumber: classicQuote.blockNumber,
routes: computeRoutes(currencyIn, currencyOut, classicQuote.route),
swapFee: getSwapFee(classicQuote),
}
}
......@@ -204,7 +224,7 @@ export async function transformRoutesToTrade(
(routerPreference === RouterPreference.X || (isUniswapXDefaultEnabled && routerPreference === RouterPreference.API))
const [currencyIn, currencyOut] = getTradeCurrencies(args, showUniswapXTrade)
const { gasUseEstimateUSD, blockNumber, routes, gasUseEstimate } = getClassicTradeDetails(
const { gasUseEstimateUSD, blockNumber, routes, gasUseEstimate, swapFee } = getClassicTradeDetails(
currencyIn,
currencyOut,
data
......@@ -254,12 +274,14 @@ export async function transformRoutesToTrade(
quoteMethod,
inputTax,
outputTax,
swapFee,
})
// During the opt-in period, only return UniswapX quotes if the user has turned on the setting,
// even if it is the better quote.
if (isUniswapXBetter && (routerPreference === RouterPreference.X || isUniswapXDefaultEnabled)) {
const orderInfo = toDutchOrderInfo(data.quote.orderInfo)
const swapFee = getSwapFee(data.quote)
const wrapInfo = await getWrapInfo(needsWrapIfUniswapX, account, currencyIn.chainId, amount, usdCostPerGas)
const uniswapXTrade = new DutchOrderTrade({
......@@ -276,6 +298,7 @@ export async function transformRoutesToTrade(
startTimeBufferSecs: data.quote.startTimeBufferSecs,
deadlineBufferSecs: data.quote.deadlineBufferSecs,
slippageTolerance: toSlippagePercent(data.quote.slippageTolerance),
swapFee,
})
return {
......
......@@ -6,13 +6,14 @@ import { useFotAdjustmentsEnabled } from 'featureFlags/flags/fotAdjustments'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useDebouncedTrade } from 'hooks/useDebouncedTrade'
import { useSwapTaxes } from 'hooks/useSwapTaxes'
import { useUSDPrice } from 'hooks/useUSDPrice'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ParsedQs } from 'qs'
import { ReactNode, useCallback, useEffect, useMemo } from 'react'
import { AnyAction } from 'redux'
import { useAppDispatch } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { isClassicTrade, isSubmittableTrade, isUniswapXTrade } from 'state/routing/utils'
import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
......@@ -82,6 +83,7 @@ export type SwapInfo = {
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
inputTax: Percent
outputTax: Percent
outputFeeFiatValue?: number
parsedAmount?: CurrencyAmount<Currency>
inputError?: ReactNode
trade: {
......@@ -140,6 +142,13 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
outputTax
)
const { data: outputFeeFiatValue } = useUSDPrice(
isSubmittableTrade(trade.trade) && trade.trade.swapFee
? CurrencyAmount.fromRawAmount(trade.trade.outputAmount.currency, trade.trade.swapFee.amount)
: undefined,
trade.trade?.outputAmount.currency
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
......@@ -215,8 +224,20 @@ export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefine
allowedSlippage,
inputTax,
outputTax,
outputFeeFiatValue,
}),
[allowedSlippage, autoSlippage, currencies, currencyBalances, inputError, inputTax, outputTax, parsedAmount, trade]
[
allowedSlippage,
autoSlippage,
currencies,
currencyBalances,
inputError,
inputTax,
outputFeeFiatValue,
outputTax,
parsedAmount,
trade,
]
)
}
......
......@@ -188,7 +188,7 @@ describe('formatNumber', () => {
expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice })).toBe('$1.23M')
expect(formatNumber({ input: 18.448, type: NumberType.FiatGasPrice })).toBe('$18.45')
expect(formatNumber({ input: 0.0099, type: NumberType.FiatGasPrice })).toBe('<$0.01')
expect(formatNumber({ input: 0, type: NumberType.FiatGasPrice })).toBe('$0.00')
expect(formatNumber({ input: 0, type: NumberType.FiatGasPrice })).toBe('$0')
})
it('formats gas prices correctly with portugese locale and thai baht currency', () => {
......@@ -199,7 +199,7 @@ describe('formatNumber', () => {
expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice })).toBe('฿\xa01,23\xa0mi')
expect(formatNumber({ input: 18.448, type: NumberType.FiatGasPrice })).toBe('฿\xa018,45')
expect(formatNumber({ input: 0.0099, type: NumberType.FiatGasPrice })).toBe('<฿\xa00,01')
expect(formatNumber({ input: 0, type: NumberType.FiatGasPrice })).toBe('฿\xa00,00')
expect(formatNumber({ input: 0, type: NumberType.FiatGasPrice })).toBe('฿\xa00')
})
it('formats USD token quantities prices correctly', () => {
......
......@@ -39,6 +39,14 @@ const NO_DECIMALS: NumberFormatOptions = {
minimumFractionDigits: 0,
}
const NO_DECIMALS_CURRENCY: NumberFormatOptions = {
notation: 'standard',
maximumFractionDigits: 0,
minimumFractionDigits: 0,
currency: 'USD',
style: 'currency',
}
const THREE_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = {
notation: 'standard',
maximumFractionDigits: 3,
......@@ -262,7 +270,7 @@ const fiatTokenStatsFormatter: FormatterRule[] = [
]
const fiatGasPriceFormatter: FormatterRule[] = [
{ exact: 0, formatterOptions: TWO_DECIMALS_CURRENCY },
{ exact: 0, formatterOptions: NO_DECIMALS_CURRENCY },
{ upperBound: 0.01, hardCodedInput: { input: 0.01, prefix: '<' }, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: 1e6, formatterOptions: TWO_DECIMALS_CURRENCY },
{ upperBound: Infinity, formatterOptions: SHORTHAND_CURRENCY_TWO_DECIMALS },
......
......@@ -6217,10 +6217,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.33.tgz#966ba96c9ccc8f0e9e09809890b438203f2b1911"
integrity sha512-JQkXcpRI3jFG8y3/CGC4TS8NkDgcxXaOQuYW8Qdvd6DcDiIyg2vVYCG9igFEzF0G6UvxgHkBKC7cWCgzZNYvQg==
"@uniswap/uniswapx-sdk@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@uniswap/uniswapx-sdk/-/uniswapx-sdk-1.3.0.tgz#22867580c7f5d5ee35d669444d093e09203e1b47"
integrity sha512-TXH0+3reXA/liY2IRbCRvPVyREDObKSVmd4vEtTD0sPM0NW6ndSowKDH0hWBj2d7lBnSNKz5fp7IOaFT7yHkug==
"@uniswap/uniswapx-sdk@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@uniswap/uniswapx-sdk/-/uniswapx-sdk-1.4.1.tgz#c5fc50000032aa714ff0cc4b9cd1957128a2a4ec"
integrity sha512-M7uuZdozWbXJq8J64KTJ9e0PkYcfe6lx7RBpIsvJaypkGgGDrmU1bAqr1j3sphHlzKTmJCuG7GZBFNcnvzxHLw==
dependencies:
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/providers" "^5.7.0"
......@@ -6228,10 +6228,10 @@
"@uniswap/sdk-core" "^4.0.3"
ethers "^5.7.0"
"@uniswap/universal-router-sdk@^1.5.4", "@uniswap/universal-router-sdk@^1.5.6":
version "1.5.6"
resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-1.5.6.tgz#274a6ac5df032c34544005fe329aa9e2aac9ade6"
integrity sha512-ZD27U+kugMRJRVEX0oWZsRCw1n5vBN3I17Q22IWE+w/WhOJSppUr6PLo9u4HRdqXTZET7gubnlRc0LOAEkkSkQ==
"@uniswap/universal-router-sdk@^1.5.4", "@uniswap/universal-router-sdk@^1.5.8":
version "1.5.8"
resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-1.5.8.tgz#16c62c3883e99073ba8b6e19188cf418b6551847"
integrity sha512-9tDDBTXarpdRfJStF5mDCNmsQrCfiIT6HCQN1EPq0tAm2b+JzjRkUzsLpbNpVef066FETc3YjPH6JDPB3CMyyA==
dependencies:
"@uniswap/permit2-sdk" "^1.2.0"
"@uniswap/router-sdk" "^1.6.0"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment