Commit 4c58258f authored by Tina's avatar Tina Committed by GitHub

feat: additional routing option prototype (#6934)

* add npm secret and modify github actions

* inject npm secret for tests as well

* revert changes to staging and prod actions because we arent going to use themmm

* remove unused github actions

* minor copy change for convenience lol

* feat: add DutchOrderTrade type to Swap components (#8)

* feat: add flag for gouda (#5)

* feat: add new signature details type (#4)

* feat: local gouda activity (#9)

* feat: Unified Routing API classic and dutch limit quote requests (#10)

* chore: Rebase 5/26 (#13)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>

* feat: add UniswapX to Settings (#7)

* feat: merge upstream 5/31 (#16)

* feat: Upgrade unified-routing-api URL (#15)

* chore: merge upstream 6/2 (#19)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarTina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>

* feat: uniswapx gas tooltip (#12)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarTina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>

* feat: swap callback (#17)

* feat: gouda gating (#14)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarTina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>

* fix: settings e2e test (#22)

* feat: update swap callback to add orders to redux state (#18)

* chore: Fix types for useBestTrade return result (#21)

* feat: gql gouda orders (#20)

* feat: show $0 for gas fee for now (#25)

* chore: Rebase 06/08 (#26)
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarTina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarBrendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: default avatarcartcrom <cartergcromer@gmail.com>
Co-authored-by: default avatarclrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: default avatarclrdo <clrdo@github.com>
Co-authored-by: default avatarEddie Dugan <eddie.dugan@uniswap.org>

* feat: poll on order submit (#23)

* feat: update gouda-sdk to 1.0.0-alpha.3 (#31)

* feat: rename gasUseEstimateUSD for dutch orders (#30)
Co-authored-by: default avatarTina Zheng <tina.s.zheng+github@gmail.com>

* chore: Fix response types (#36)

* feat: Gouda ETH input flow (#29)
Co-authored-by: default avatarEddie Dugan <eddie.dugan@uniswap.org>

* fix: use trade to determine what router label to show (#41)

* feat: open uniswapx modal on click (#32)

* feat: gouda logging new params in swap quote received (#33)

* fix: wrap step ui fixes (#40)

* feat: use BE deadline padding (#46)

* chore: merge 6/23 (#50)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarBrendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: default avatarcartcrom <cartergcromer@gmail.com>
Co-authored-by: default avatarclrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: default avatarclrdo <clrdo@github.com>
Co-authored-by: default avatarShubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: default avatarSaro Vindigni <sarovindigni@bolket.com>
Co-authored-by: default avatarJordan Frankfurt <&lt;jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJohn Short <john.short@CORN-Jack-899.local>

* feat: Gouda opt-in flow request logic (#37)
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>

* feat: hide slippage and deadline settings when the current trade is gouda (#44)

* feat: use settled order amounts (#45)

* feat: fetch receipt before dispatch (#49)

* fix: updated order popups to launch modal (#48)

* feat: Use slippage value from URA response for UniswapX trades (#51)

* fix: Bump gouda-sdk to match backend response for quotes (#58)

* feat: Change gouda order status URL param from offerer -> swapper (#59)

* feat: disable opt in flow (#57)

* feat: Dont show USD value change for uniswapx trades (#55)

* fix: Don't use WETH as input currency for Classic ETH-in trades (#54)

* feat: Reset to WETH after wrap is complete (#52)

* fix: correct descriptor in UniswapX activity row items (#61)

* fix: align review modal and gouda activity modal (#60)
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>

* feat: Get wrap and approve gas info (#53)
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>

* fix: restore summary view when wrap is rejected (#66)

* fix: serialize tx receipts before storing (#64)

* fix: Insufficient balance check should read from the right currency (#65)

* feat: update designs for gas tooltips (#67)

* fix: UniswapX gas descriptor boolean (#69)

* chore: Bump conedison for better gas price formatting (#70)

* chore: Switch from gouda-sdk to uniswapx-sdk (#71)

* chore: Rename all variables `gouda` to UniswapX (#72)

* chore: Merge 7/8/23 (#73)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarBrendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: default avatarcartcrom <cartergcromer@gmail.com>
Co-authored-by: default avatarclrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: default avatarclrdo <clrdo@github.com>
Co-authored-by: default avatarShubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: default avatarSaro Vindigni <sarovindigni@bolket.com>
Co-authored-by: default avatarJordan Frankfurt <&lt;jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJohn Short <john.short@CORN-Jack-899.local>
Co-authored-by: default avatarCharlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: default avatarUL Service Account <hello-happy-puppy@users.noreply.github.com>

* chore(conedison): update package (#77)

* feat: add opt-in UI (#68)

* chore: address some todos (#79)

* chore: Rename feature flag from gouda_enabled to uniswapx_enabled (#81)

* feat: Copy changes (#82)

* fix: improve timings on animations for gouda opt-in (#80)

* chore: Use updated URLs (#84)

* chore: Merge 7/14 (#85)
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarBrendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: default avatarcartcrom <cartergcromer@gmail.com>
Co-authored-by: default avatarclrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: default avatarclrdo <clrdo@github.com>
Co-authored-by: default avatarShubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: default avatarSaro Vindigni <sarovindigni@bolket.com>
Co-authored-by: default avatarJordan Frankfurt <&lt;jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJohn Short <john.short@CORN-Jack-899.local>
Co-authored-by: default avatarCharlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: default avatarUL Service Account <hello-happy-puppy@users.noreply.github.com>

* remove changes to github actions files

* fix import

* actually revert changes to yml

* remove method export

* feat: Add feature flag for synthetic quotes (#6938)

* fix: use Lingui Trans macro (#6943)

* fix: use trans macro

* add comments

* fix: update updater.tsx (#6942)

* fix: reformat variable to use ms

* move interval definition above getOrderStatus

* lint :)

* revert

* chore: bunch of nits (#6944)

bunch of nits

* fix: translations etc (#6945)

* chore: Remove placeholder signature types (#6937)

remove placeholder

* chore: merge main into branch (#6948)

* fix: Handle Scientific Notation for NFT Collection Activity Prices (#6936)

wrap nft activity price in

* fix: e2e tests (#6941)

* fix: e2e test

* fix: set flag for buy-crypto-modal test

* fix: fund DAI

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

---------
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>

* feat: make inputCurrency optional for swapheader (#6947)

* make inputCurrency optional for swapheader

* optional pass in

* fix: function defined twice (#6950)

fix lint

* test: add signatureToActivity undefined tests (#6949)

* fix: update token lists schema (#6951)

fix: update token list schema

* chore: some last nits (#6953)

* refactor: base type

* test: useUserDisabledUniswapX

* chore: simplify useAllSignatures usage

* chore: standard check order

* lint

---------
Co-authored-by: default avatareddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: default avatarMike Grabowski <grabbou@gmail.com>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarVignesh Mohankumar <me@vig.xyz>
Co-authored-by: default avatarJack Short <john.short.tj@gmail.com>
Co-authored-by: default avatarJordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: default avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: default avatarCrowdin Bot <support+bot@crowdin.com>
Co-authored-by: default avatarNate Wienert <natewienert@gmail.com>
Co-authored-by: default avatarCharles Bachmeier <charles@bachmeier.io>
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarBrendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: default avatarcartcrom <cartergcromer@gmail.com>
Co-authored-by: default avatarclrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: default avatarclrdo <clrdo@github.com>
Co-authored-by: default avatarEddie Dugan <eddie.dugan@uniswap.org>
Co-authored-by: default avatarmarktoda <toda.mark@gmail.com>
Co-authored-by: default avatarShubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: default avatarSaro Vindigni <sarovindigni@bolket.com>
Co-authored-by: default avatarJordan Frankfurt <&lt;jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: default avatarJohn Short <john.short@CORN-Jack-899.local>
Co-authored-by: default avatarCharlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: default avatarUL Service Account <hello-happy-puppy@users.noreply.github.com>
parent f6e6db64
......@@ -11,4 +11,5 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
\ No newline at end of file
REACT_APP_UNISWAP_API_URL="https://api.uniswap.org/v2"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
......@@ -41,7 +41,7 @@ describe('Swap errors', () => {
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.contains('Swap submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
......@@ -89,7 +89,7 @@ describe('Swap errors', () => {
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.contains('Swap submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
}
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
......
import { FeatureFlag } from '../../../src/featureFlags'
import { getTestSelector } from '../../utils'
describe('Swap settings', () => {
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled], ethereum: 'hardhat' })
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Max slippage').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.contains('UniswapX').should('exist')
cy.contains('Local routing').should('exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist')
})
......
......@@ -69,9 +69,9 @@ describe('Swap', () => {
cy.contains('Review swap')
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.contains('Swap submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.contains('Transaction submitted').should('not.exist')
cy.contains('Swap submitted').should('not.exist')
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
// Mine transaction
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.97119 6.19815C9.91786 6.07749 9.79854 6.00016 9.66654 6.00016H6.66654V1.00016C6.66654 0.862156 6.58189 0.738159 6.45255 0.688826C6.32255 0.638826 6.17787 0.674818 6.0852 0.776818L0.0852016 7.44349C-0.00279838 7.54149 -0.025439 7.68149 0.028561 7.80216C0.0818943 7.92283 0.201208 8.00016 0.333208 8.00016H3.33321V13.0002C3.33321 13.1382 3.41786 13.2622 3.5472 13.3115C3.58653 13.3262 3.62654 13.3335 3.66654 13.3335C3.75921 13.3335 3.84988 13.2948 3.91455 13.2228L9.91455 6.55616C10.0025 6.45882 10.0245 6.31815 9.97119 6.19815Z" fill="url(#paint0_linear_1816_1801)"/>
<defs>
<linearGradient id="paint0_linear_1816_1801" x1="-10.1808" y1="-12.0005" x2="10.6572" y2="-11.6015" gradientUnits="userSpaceOnUse">
<stop stop-color="#4673FA"/>
<stop offset="1" stop-color="#9646FA"/>
</linearGradient>
</defs>
</svg>
This diff is collapsed.
......@@ -283,7 +283,7 @@ function SwapSummary({ info }: { info: ExactInputSwapTransactionInfo | ExactOutp
/>{' '}
for{' '}
<FormattedCurrencyAmountManaged
rawAmount={info.expectedOutputCurrencyAmountRaw}
rawAmount={info.settledOutputCurrencyAmountRaw ?? info.expectedOutputCurrencyAmountRaw}
currencyId={info.outputCurrencyId}
sigFigs={6}
/>
......
......@@ -3,8 +3,10 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import Row from 'components/Row'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName'
import { useCallback } from 'react'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { shortenAddress } from 'utils'
......@@ -12,6 +14,7 @@ import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow from '../PortfolioRow'
import { useOpenOffchainActivityModal } from './OffchainActivityModal'
import { useTimeSince } from './parseRemote'
import { Activity } from './types'
......@@ -26,16 +29,36 @@ const StyledTimestamp = styled(ThemedText.Caption)`
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
`
export function ActivityRow({
activity: { chainId, status, title, descriptor, logos, otherAccount, currencies, timestamp, hash },
}: {
activity: Activity
}) {
const { ENSName } = useENSName(otherAccount)
function StatusIndicator({ activity: { status, timestamp } }: { activity: Activity }) {
const timeSince = useTimeSince(timestamp)
switch (status) {
case TransactionStatus.Pending:
return <LoaderV2 />
case TransactionStatus.Confirmed:
return <StyledTimestamp>{timeSince}</StyledTimestamp>
case TransactionStatus.Failed:
return <AlertTriangleFilled />
}
}
export function ActivityRow({ activity }: { activity: Activity }) {
const { chainId, title, descriptor, logos, otherAccount, currencies, hash, prefixIconSrc, offchainOrderStatus } =
activity
const openOffchainActivityModal = useOpenOffchainActivityModal()
const { ENSName } = useENSName(otherAccount)
const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)
const onClick = useCallback(() => {
if (offchainOrderStatus) {
openOffchainActivityModal({ orderHash: hash, status: offchainOrderStatus })
return
}
window.open(getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION), '_blank')
}, [offchainOrderStatus, chainId, hash, openOffchainActivityModal])
return (
<TraceEvent
events={[BrowserEvent.onClick]}
......@@ -49,23 +72,20 @@ export function ActivityRow({
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader>{title}</ThemedText.SubHeader>}
title={
<Row gap="4px">
{prefixIconSrc && <img height="14px" width="14px" src={prefixIconSrc} alt="" />}
<ThemedText.SubHeader>{title}</ThemedText.SubHeader>
</Row>
}
descriptor={
<ActivityRowDescriptor color="textSecondary">
{descriptor}
{ENSName ?? shortenAddress(otherAccount)}
</ActivityRowDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
right={<StatusIndicator activity={activity} />}
onClick={onClick}
/>
</TraceEvent>
)
......
import { t, Trans } from '@lingui/macro'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ReactComponent as ErrorContent } from 'assets/svg/uniswapx_error.svg'
import Column, { AutoColumn } from 'components/Column'
import { OpacityHoverState } from 'components/Common'
import { LoaderV3 } from 'components/Icons/LoadingSpinner'
import Modal from 'components/Modal'
import { AnimatedEntranceConfirmationIcon, FadePresence } from 'components/swap/PendingModalContent/Logos'
import { TradeSummary } from 'components/swap/PendingModalContent/TradeSummary'
import { useCurrency } from 'hooks/Tokens'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { useCallback, useMemo } from 'react'
import { X } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
import { useOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
type SelectedOrderInfo = {
modalOpen?: boolean
orderHash: string
status: UniswapXOrderStatus
details?: UniswapXOrderDetails
}
const selectedOrderAtom = atom<SelectedOrderInfo | undefined>(undefined)
export function useOpenOffchainActivityModal() {
const setSelectedOrder = useUpdateAtom(selectedOrderAtom)
return useCallback(
(order: { orderHash: string; status: UniswapXOrderStatus }) => setSelectedOrder({ ...order, modalOpen: true }),
[setSelectedOrder]
)
}
const Wrapper = styled(AutoColumn).attrs({ gap: 'md', grow: true })`
padding: 16px;
`
const ContentContainer = styled(AutoColumn).attrs({ justify: 'center', gap: 'md' })`
padding: 28px 44px 24px 44px;
`
const StyledXButton = styled(X)`
cursor: pointer;
justify-self: flex-end;
color: ${({ theme }) => theme.textPrimary};
${OpacityHoverState};
`
const LoadingWrapper = styled.div`
width: 52px;
height: 52px;
position: relative;
margin-bottom: 8px;
`
const LoadingIndicator = styled(LoaderV3)`
width: 100%;
height: 100%;
position: absolute;
`
function Loader() {
return (
<LoadingWrapper>
<FadePresence>
<LoadingIndicator />
</FadePresence>
</LoadingWrapper>
)
}
const Success = styled(AnimatedEntranceConfirmationIcon)`
margin-bottom: 10px;
`
const LearnMoreLink = styled(ExternalLink)`
font-weight: 600;
`
const DescriptionText = styled(ThemedText.LabelMicro)`
text-align: center;
`
function useOrderAmounts(
orderDetails?: UniswapXOrderDetails
): Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> | undefined {
const inputCurrency = useCurrency(orderDetails?.swapInfo?.inputCurrencyId, orderDetails?.chainId)
const outputCurrency = useCurrency(orderDetails?.swapInfo?.outputCurrencyId, orderDetails?.chainId)
if (!orderDetails) return undefined
if (!inputCurrency || !outputCurrency) {
console.error(`Could not find token(s) for order ${orderDetails.orderHash}`)
return undefined
}
const { swapInfo } = orderDetails
if (swapInfo.tradeType === TradeType.EXACT_INPUT) {
return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(
outputCurrency,
swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw
),
}
} else {
return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.expectedInputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw),
}
}
}
export function OrderContent({ order }: { order: SelectedOrderInfo }) {
const amounts = useOrderAmounts(order.details)
const explorerLink = order?.details?.txHash
? getExplorerLink(order.details.chainId, order.details.txHash, ExplorerDataType.TRANSACTION)
: undefined
switch (order.status) {
case UniswapXOrderStatus.OPEN: {
return (
<ContentContainer>
<Loader />
<ThemedText.SubHeaderLarge>
<Trans>Swapping</Trans>
</ThemedText.SubHeaderLarge>
<Column>
{amounts && <TradeSummary trade={amounts} />}
<ThemedText.Caption paddingTop="48px" textAlign="center">
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/17515415311501">
<Trans>Learn more about swapping with UniswapX</Trans>
</ExternalLink>
</ThemedText.Caption>
</Column>
</ContentContainer>
)
}
case UniswapXOrderStatus.FILLED:
return (
<ContentContainer>
<Success />
<ThemedText.SubHeaderLarge>
<Trans>Swapped</Trans>
</ThemedText.SubHeaderLarge>
<Column>
{amounts && <TradeSummary trade={amounts} />}
<ThemedText.Caption paddingTop="48px" textAlign="center">
{explorerLink && (
<ExternalLink href={explorerLink}>
<Trans>View on Explorer</Trans>
</ExternalLink>
)}
</ThemedText.Caption>
</Column>
</ContentContainer>
)
case UniswapXOrderStatus.CANCELLED:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Cancelled</Trans>
</ThemedText.SubHeaderLarge>
<ThemedText.LabelSmall textAlign="center">
<Trans>This order was cancelled</Trans>
</ThemedText.LabelSmall>
</ContentContainer>
)
case UniswapXOrderStatus.EXPIRED:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Swap expired</Trans>
</ThemedText.SubHeaderLarge>
<DescriptionText>
{/* TODO: Improve translation grammar by not having to break up the string */}
<Trans>Your swap expired before it could be filled. Try again or</Trans>{' '}
<LearnMoreLink href="https://support.uniswap.org/hc/en-us/articles/17515426867213">
<Trans>learn more.</Trans>
</LearnMoreLink>
</DescriptionText>
</ContentContainer>
)
case UniswapXOrderStatus.ERROR:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Error</Trans>
</ThemedText.SubHeaderLarge>
<ThemedText.LabelSmall textAlign="center">
{/* TODO: Improve translation grammar by not having to break up the string */}
<Trans>Your swap couldn&apos;t be filled at this time. Try again or </Trans>{' '}
<LearnMoreLink href="https://support.uniswap.org/hc/en-us/articles/17515489874189">
<Trans>learn more.</Trans>
</LearnMoreLink>
</ThemedText.LabelSmall>
</ContentContainer>
)
case UniswapXOrderStatus.INSUFFICIENT_FUNDS:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Insufficient funds for swap</Trans>
</ThemedText.SubHeaderLarge>
<ThemedText.LabelSmall textAlign="center">{t`You didn't have enough ${
amounts?.inputAmount.currency.symbol ?? amounts?.inputAmount.currency.name ?? t`of the input token`
} to complete this swap.`}</ThemedText.LabelSmall>
</ContentContainer>
)
}
}
/* Returns the order currently selected in the UI synced with updates from order status polling */
function useSyncedSelectedOrder(): SelectedOrderInfo | undefined {
const selectedOrder = useAtomValue(selectedOrderAtom)
const localPendingOrder = useOrder(selectedOrder?.orderHash ?? '')
return useMemo(() => {
if (!selectedOrder) return undefined
return {
...selectedOrder,
status: localPendingOrder?.status ?? selectedOrder.status,
details: localPendingOrder,
}
}, [localPendingOrder, selectedOrder])
}
export function OffchainActivityModal() {
const syncedSelectedOrder = useSyncedSelectedOrder()
const setSelectedOrder = useUpdateAtom(selectedOrderAtom)
const reset = useCallback(() => {
setSelectedOrder((order) => order && { ...order, modalOpen: false })
}, [setSelectedOrder])
return (
<Modal isOpen={!!syncedSelectedOrder?.modalOpen} onDismiss={reset}>
<Wrapper>
<StyledXButton onClick={reset} />
{syncedSelectedOrder && <OrderContent order={syncedSelectedOrder} />}
</Wrapper>
</Modal>
)
}
......@@ -3,7 +3,7 @@ import { useAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import { LoadingBubble } from 'components/Tokens/loading'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/types-and-hooks'
import { PollingInterval } from 'graphql/data/util'
import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
......@@ -91,7 +91,7 @@ function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account
if (!remoteTx) return false
// Cancellations are only possible when both nonce and tx.from are the same
if (remoteTx.nonce === localActivity.nonce && remoteTx.receipt?.from.toLowerCase() === account.toLowerCase()) {
if (remoteTx.nonce === localActivity.nonce && remoteTx.from.toLowerCase() === account.toLowerCase()) {
// If the remote tx has a different hash than the local tx, the local tx was cancelled
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
}
......@@ -126,11 +126,7 @@ export function ActivityTab({ account }: { account: string }) {
const localMap = useLocalActivities(account)
const { data, loading, refetch } = useTransactionListQuery({
variables: { account },
errorPolicy: 'all',
fetchPolicy: 'cache-first',
})
const { data, loading, refetch } = useActivityQuery({ variables: { account } })
// We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer
useEffect(() => {
......
......@@ -12,7 +12,9 @@ import {
} from 'state/transactions/types'
import { renderHook } from 'test-utils/render'
import { parseLocalActivity, useLocalActivities } from './parseLocal'
import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types'
import { SignatureDetails, SignatureType } from '../../../../state/signatures/types'
import { signatureToActivity, transactionToActivity, useLocalActivities } from './parseLocal'
function mockSwapInfo(
type: MockTradeType,
......@@ -30,6 +32,7 @@ function mockSwapInfo(
outputCurrencyId: outputCurrency.address,
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
isUniswapXOrder: false,
}
} else {
return {
......@@ -40,6 +43,7 @@ function mockSwapInfo(
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address,
outputCurrencyAmountRaw,
isUniswapXOrder: false,
}
}
}
......@@ -247,26 +251,12 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = ChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toEqual({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
hash: undefined,
receipt: {
id: '0x123',
info: {
type: 1,
tradeType: MockTradeType.EXACT_INPUT,
inputCurrencyId: MockUSDC_MAINNET.address,
inputCurrencyAmountRaw: mockCurrencyAmountRawUSDC,
outputCurrencyId: MockDAI.address,
expectedOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
},
receipt: { status: 1, transactionHash: '0x123' },
status: 'CONFIRMED',
transactionHash: '0x123',
},
from: undefined,
status: 'CONFIRMED',
timestamp: NaN,
title: 'Swapped',
......@@ -288,7 +278,7 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = ChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
......@@ -313,7 +303,7 @@ describe('parseLocalActivity', () => {
} as TransactionDetails
const chainId = ChainId.MAINNET
const tokens = {} as ChainTokenMap
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
expect(transactionToActivity(details, chainId, tokens)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown',
......@@ -349,10 +339,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -367,10 +354,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -385,10 +369,7 @@ describe('parseLocalActivity', () => {
descriptor: MockDAI.symbol,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -402,10 +383,6 @@ describe('parseLocalActivity', () => {
descriptor: MockUSDT.symbol,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
......@@ -422,10 +399,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -442,10 +416,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -460,10 +431,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -478,10 +446,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -496,10 +461,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -514,10 +476,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
......@@ -532,10 +491,29 @@ describe('parseLocalActivity', () => {
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
it('Signature to activity - returns undefined if is on chain order', () => {
expect(
signatureToActivity(
{
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.FILLED,
} as SignatureDetails,
{}
)
).toBeUndefined()
expect(
signatureToActivity(
{
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.CANCELLED,
} as SignatureDetails,
{}
)
).toBeUndefined()
})
})
......@@ -3,9 +3,12 @@ import { t } from '@lingui/macro'
import { formatCurrencyAmount } from '@uniswap/conedison/format'
import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { nativeOnChain } from '@uniswap/smart-order-router'
import UniswapXBolt from 'assets/svg/bolt.svg'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
import { useMemo } from 'react'
import { isOnChainOrder, useAllSignatures } from 'state/signatures/hooks'
import { SignatureDetails, SignatureType } from 'state/signatures/types'
import { useMultichainTransactions } from 'state/transactions/hooks'
import {
AddLiquidityV2PoolTransactionInfo,
......@@ -22,7 +25,7 @@ import {
WrapTransactionInfo,
} from 'state/transactions/types'
import { getActivityTitle } from '../constants'
import { getActivityTitle, OrderTextTable } from '../constants'
import { Activity, ActivityMap } from './types'
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
......@@ -52,12 +55,13 @@ function parseSwap(
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
const [inputRaw, outputRaw] =
swap.tradeType === TradeType.EXACT_INPUT
? [swap.inputCurrencyAmountRaw, swap.expectedOutputCurrencyAmountRaw]
? [swap.inputCurrencyAmountRaw, swap.settledOutputCurrencyAmountRaw ?? swap.expectedOutputCurrencyAmountRaw]
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
return {
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw),
currencies: [tokenIn, tokenOut],
prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined,
}
}
......@@ -134,7 +138,7 @@ function parseMigrateCreateV3(
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
export function parseLocalActivity(
export function transactionToActivity(
details: TransactionDetails,
chainId: ChainId,
tokens: ChainTokenMap
......@@ -146,22 +150,13 @@ export function parseLocalActivity(
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const receipt = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
...details,
status,
}
: undefined
const defaultFields = {
hash: details.hash,
chainId,
title: getActivityTitle(details.info.type, status),
status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt,
from: details.from,
nonce: details.nonce,
}
......@@ -192,17 +187,53 @@ export function parseLocalActivity(
}
}
export function signatureToActivity(signature: SignatureDetails, tokens: ChainTokenMap): Activity | undefined {
switch (signature.type) {
case SignatureType.SIGN_UNISWAPX_ORDER: {
// Only returns Activity items for orders that don't have an on-chain counterpart
if (isOnChainOrder(signature.status)) return undefined
const { title, statusMessage, status } = OrderTextTable[signature.status]
return {
hash: signature.orderHash,
chainId: signature.chainId,
title,
status,
offchainOrderStatus: signature.status,
timestamp: signature.addedTime / 1000,
from: signature.offerer,
statusMessage,
prefixIconSrc: UniswapXBolt,
...parseSwap(signature.swapInfo, signature.chainId, tokens),
}
}
default:
return undefined
}
}
export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const allSignatures = useAllSignatures()
const tokens = useAllTokensMultichain()
return useMemo(() => {
const activityByHash: ActivityMap = {}
const activityMap: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
const activity = transactionToActivity(transaction, chainId, tokens)
if (activity) activityMap[transaction.hash] = activity
}
return activityByHash
}, [account, allTransactions, tokens])
for (const signature of Object.values(allSignatures)) {
if (signature.offerer !== account) continue
const activity = signatureToActivity(signature, tokens)
if (activity) activityMap[signature.id] = activity
}
return activityMap
}, [account, allSignatures, allTransactions, tokens])
}
import { t } from '@lingui/macro'
import { formatFiatPrice, formatNumberOrString, NumberType } from '@uniswap/conedison/format'
import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESSES } from '@uniswap/sdk-core'
import UniswapXBolt from 'assets/svg/bolt.svg'
import moonpayLogoSrc from 'assets/svg/moonpay.svg'
import { nativeOnChain } from 'constants/tokens'
import {
......@@ -10,15 +11,18 @@ import {
NftApprovalPartsFragment,
NftApproveForAllPartsFragment,
NftTransferPartsFragment,
SwapOrderDetailsPartsFragment,
TokenApprovalPartsFragment,
TokenAssetPartsFragment,
TokenTransferPartsFragment,
TransactionDetailsPartsFragment,
} from 'graphql/data/__generated__/types-and-hooks'
import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import ms from 'ms.macro'
import { useEffect, useState } from 'react'
import { isAddress } from 'utils'
import { MOONPAY_SENDER_ADDRESSES } from '../constants'
import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
import { Activity } from './types'
type TransactionChanges = {
......@@ -74,10 +78,10 @@ function isSameAddress(a?: string, b?: string) {
return a === b || a?.toLowerCase() === b?.toLowerCase() // Lazy-lowercases the addresses
}
function callsPositionManagerContract(assetActivity: AssetActivityPartsFragment) {
function callsPositionManagerContract(assetActivity: TransactionActivity) {
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
if (!supportedChain) return false
return isSameAddress(assetActivity.transaction.to, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[supportedChain])
return isSameAddress(assetActivity.details.to, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[supportedChain])
}
// Gets counts for number of NFTs in each collection present
......@@ -116,6 +120,20 @@ function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferP
}
}
function getSwapDescriptor({
tokenIn,
inputAmount,
tokenOut,
outputAmount,
}: {
tokenIn: TokenAssetPartsFragment
outputAmount: string
tokenOut: TokenAssetPartsFragment
inputAmount: string
}) {
return `${inputAmount} ${tokenIn.symbol} for ${outputAmount} ${tokenOut.symbol}`
}
/**
*
* @param transactedValue Transacted value amount from TokenTransfer API response
......@@ -145,13 +163,17 @@ function parseSwap(changes: TransactionChanges) {
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
return {
title: getSwapTitle(sent, received),
descriptor: `${inputAmount} ${sent.asset.symbol} for ${outputAmount} ${received.asset.symbol}`,
descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }),
}
}
}
return { title: t`Unknown Swap` }
}
function parseSwapOrder(changes: TransactionChanges) {
return { ...parseSwap(changes), prefixIconSrc: UniswapXBolt }
}
function parseApprove(changes: TransactionChanges) {
if (changes.TokenApproval.length === 1) {
const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved`
......@@ -174,7 +196,10 @@ function parseLPTransfers(changes: TransactionChanges) {
}
}
function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment }
function parseSendReceive(changes: TransactionChanges, assetActivity: TransactionActivity) {
// TODO(cartcrom): remove edge cases after backend implements
// Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
......@@ -221,7 +246,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActiv
return { title: t`Unknown Send` }
}
function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
function parseMint(changes: TransactionChanges, assetActivity: TransactionActivity) {
const collectionMap = getCollectionCounts(changes.NftTransfer)
if (Object.keys(collectionMap).length === 1) {
const collectionName = Object.keys(collectionMap)[0]
......@@ -235,13 +260,14 @@ function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPart
return { title: t`Unknown Mint` }
}
function parseUnknown(_changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.transaction.to.toLowerCase()] }
function parseUnknown(_changes: TransactionChanges, assetActivity: TransactionActivity) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] }
}
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) => Partial<Activity>
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: TransactionActivity) => Partial<Activity>
const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
[ActivityType.Swap]: parseSwap,
[ActivityType.SwapOrder]: parseSwapOrder,
[ActivityType.Approve]: parseApprove,
[ActivityType.Send]: parseSendReceive,
[ActivityType.Receive]: parseSendReceive,
......@@ -262,8 +288,47 @@ function getLogoSrcs(changes: TransactionChanges): string[] {
return Array.from(logoSet).filter(Boolean) as string[]
}
function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activity | undefined {
const { inputToken, inputTokenQuantity, outputToken, outputTokenQuantity, orderStatus } = details
const uniswapXOrderStatus = OrderStatusTable[orderStatus]
const { status, statusMessage, title } = OrderTextTable[uniswapXOrderStatus]
const descriptor = getSwapDescriptor({
tokenIn: inputToken,
inputAmount: inputTokenQuantity,
tokenOut: outputToken,
outputAmount: outputTokenQuantity,
})
const supportedChain = supportedChainIdFromGQLChain(chain)
if (!supportedChain) {
logSentryErrorForUnsupportedChain({
extras: { details },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
return {
hash: details.hash,
chainId: supportedChain,
status,
statusMessage,
offchainOrderStatus: uniswapXOrderStatus,
timestamp,
logos: [inputToken.project?.logo?.url, outputToken.project?.logo?.url],
title,
descriptor,
from: details.offerer,
prefixIconSrc: UniswapXBolt,
}
}
function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
try {
if (assetActivity.details.__typename === 'SwapOrderDetails') {
return parseUniswapXOrder(assetActivity as OrderActivity)
}
const changes = assetActivity.assetChanges.reduce(
(acc: TransactionChanges, assetChange) => {
if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange)
......@@ -285,18 +350,18 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
return undefined
}
const defaultFields = {
hash: assetActivity.transaction.hash,
hash: assetActivity.details.hash,
chainId: supportedChain,
status: assetActivity.transaction.status,
status: assetActivity.details.status,
timestamp: assetActivity.timestamp,
logos: getLogoSrcs(changes),
title: assetActivity.type,
descriptor: assetActivity.transaction.to,
receipt: assetActivity.transaction,
nonce: assetActivity.transaction.nonce,
descriptor: assetActivity.details.to,
from: assetActivity.details.from,
nonce: assetActivity.details.nonce,
}
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity)
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity as TransactionActivity)
return { ...defaultFields, ...parsedFields }
} catch (e) {
console.error('Failed to parse activity', e, assetActivity)
......
import { ChainId, Currency } from '@uniswap/sdk-core'
import { AssetActivityPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
type Receipt = AssetActivityPartsFragment['transaction']
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
export type Activity = {
hash: string
chainId: ChainId
status: TransactionStatus
// TODO (UniswapX): decouple Activity from UniswapXOrderStatus once we can link UniswapXScan instead of needing data for modal
offchainOrderStatus?: UniswapXOrderStatus
statusMessage?: string
timestamp: number
title: string
descriptor?: string
logos?: Array<string | undefined>
currencies?: Array<Currency | undefined>
otherAccount?: string
receipt?: Omit<Receipt, 'nonce'>
from: string
nonce?: number | null
prefixIconSrc?: string
}
export type ActivityMap = { [hash: string]: Activity | undefined }
export type ActivityMap = { [id: string]: Activity | undefined }
import { t } from '@lingui/macro'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { SwapOrderStatus, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { TransactionType } from 'state/transactions/types'
// use even number because rows are in groups of 2
......@@ -159,6 +160,38 @@ export function getActivityTitle(type: TransactionType, status: TransactionStatu
return TransactionTitleTable[type][status]
}
const SwapTitleTable = TransactionTitleTable[TransactionType.SWAP]
export const OrderTextTable: {
[status in UniswapXOrderStatus]: { title: string; status: TransactionStatus; statusMessage?: string }
} = {
[UniswapXOrderStatus.OPEN]: {
title: SwapTitleTable.PENDING,
status: TransactionStatus.Pending,
},
[UniswapXOrderStatus.FILLED]: {
title: SwapTitleTable.CONFIRMED,
status: TransactionStatus.Confirmed,
},
[UniswapXOrderStatus.EXPIRED]: {
title: t`Swap expired`,
statusMessage: t`Your swap could not be fulfilled at this time. Please try again.`,
status: TransactionStatus.Failed,
},
[UniswapXOrderStatus.ERROR]: {
title: SwapTitleTable.FAILED,
status: TransactionStatus.Failed,
},
[UniswapXOrderStatus.INSUFFICIENT_FUNDS]: {
title: SwapTitleTable.FAILED,
statusMessage: t`Your account had insufficent funds to complete this swap.`,
status: TransactionStatus.Failed,
},
[UniswapXOrderStatus.CANCELLED]: {
title: t`Swap cancelled`,
status: TransactionStatus.Failed,
},
}
// Non-exhaustive list of addresses Moonpay uses when sending purchased tokens
export const MOONPAY_SENDER_ADDRESSES = [
'0x8216874887415e2650d12d53ff53516f04a74fd7',
......@@ -166,3 +199,11 @@ export const MOONPAY_SENDER_ADDRESSES = [
'0xb287eac48ab21c5fb1d3723830d60b4c797555b0',
'0xd108fd0e8c8e71552a167e7a44ff1d345d233ba6',
]
// Converts GQL backend orderStatus enum to the enum used by the frontend and UniswapX backend
export const OrderStatusTable: { [key in SwapOrderStatus]: UniswapXOrderStatus } = {
[SwapOrderStatus.Open]: UniswapXOrderStatus.OPEN,
[SwapOrderStatus.Expired]: UniswapXOrderStatus.EXPIRED,
[SwapOrderStatus.Error]: UniswapXOrderStatus.ERROR,
[SwapOrderStatus.InsufficientFunds]: UniswapXOrderStatus.INSUFFICIENT_FUNDS,
}
......@@ -21,7 +21,6 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
velocity: 0.01,
},
})
return (
<animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}>
<div ref={ref}>{children}</div>
......
......@@ -2,6 +2,8 @@ import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'fe
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
import { useRoutingAPIForPriceFlag } from 'featureFlags/flags/priceRoutingApi'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { UniswapXVariant, useUniswapXFlag } from 'featureFlags/flags/uniswapx'
import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
import { X } from 'react-feather'
......@@ -208,11 +210,23 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.detailsV2}
label="Use the new details page for nfts"
/>
<FeatureFlagOption
variant={UniswapXVariant}
value={useUniswapXFlag()}
featureFlag={FeatureFlag.uniswapXEnabled}
label="Enable UniswapX"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useUniswapXSyntheticQuoteFlag()}
featureFlag={FeatureFlag.uniswapXSyntheticQuote}
label="Force synthetic quotes for UniswapX"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useRoutingAPIForPriceFlag()}
featureFlag={FeatureFlag.routingAPIPrice}
label="Use the URA or routing-api for price fetches"
label="Use the routing-api v2 for price fetches"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
......
import { Trans } from '@lingui/macro'
import { ThemedText } from 'theme'
import UniswapXRouterLabel, { UnswapXRouterLabelProps } from '../RouterLabel/UniswapXRouterLabel'
type UniswapXBrandMarkProps = Omit<UnswapXRouterLabelProps, 'children' | 'fontWeight'> & {
fontWeight?: 'bold'
}
export default function UniswapXBrandMark({ fontWeight, ...props }: UniswapXBrandMarkProps): JSX.Element {
return (
<UniswapXRouterLabel {...props}>
<ThemedText.BodySecondary
fontSize="inherit"
{...(fontWeight === 'bold' && {
fontWeight: '500',
})}
>
<Trans>UniswapX</Trans>
</ThemedText.BodySecondary>
</UniswapXRouterLabel>
)
}
......@@ -20,7 +20,7 @@ const ReferenceElement = styled.div`
height: inherit;
`
const Arrow = styled.div`
export const Arrow = styled.div`
width: 8px;
height: 8px;
z-index: 9998;
......
import { Trans } from '@lingui/macro'
import { ChainId } from '@uniswap/sdk-core'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { getChainInfo } from 'constants/chainInfo'
import styled from 'styled-components/macro'
import { ThemedText } from '../../theme'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
`
const ColumnContainer = styled(AutoColumn)`
margin: 0 12px;
`
export const PopupAlertTriangle = styled(AlertTriangleFilled)`
flex-shrink: 0;
width: 32px;
height: 32px;
`
export default function FailedNetworkSwitchPopup({ chainId }: { chainId: ChainId }) {
const chainInfo = getChainInfo(chainId)
return (
<RowNoFlex gap="12px">
<PopupAlertTriangle />
<ColumnContainer gap="sm">
<ThemedText.SubHeader color="textSecondary">
<Trans>Failed to switch networks</Trans>
</ThemedText.SubHeader>
<ThemedText.BodySmall color="textSecondary">
<Trans>To use Uniswap on {chainInfo.label}, switch the network in your wallet’s settings.</Trans>
</ThemedText.BodySmall>
</ColumnContainer>
</RowNoFlex>
)
}
import { Trans } from '@lingui/macro'
import { ChainId } from '@uniswap/sdk-core'
import { useOpenOffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import { signatureToActivity, transactionToActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow'
import Column, { AutoColumn } from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { AutoRow } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { useAllTokensMultichain } from 'hooks/Tokens'
import useENSName from 'hooks/useENSName'
import { X } from 'react-feather'
import { useOrder } from 'state/signatures/hooks'
import { useTransaction } from 'state/transactions/hooks'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
const StyledClose = styled(X)<{ $padding: number }>`
position: absolute;
right: ${({ $padding }) => `${$padding}px`};
top: ${({ $padding }) => `${$padding}px`};
color: ${({ theme }) => theme.textSecondary};
:hover {
cursor: pointer;
}
`
const PopupContainer = styled.div<{ padded?: boolean }>`
display: inline-block;
width: 100%;
background-color: ${({ theme }) => theme.backgroundSurface};
position: relative;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
overflow: hidden;
box-shadow: ${({ theme }) => theme.deepShadow};
transition: ${({ theme }) => `visibility ${theme.transition.duration.fast} ease-in-out`};
padding: ${({ padded }) => (padded ? '20px 35px 20px 20px' : '2px 0px')};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
min-width: 290px;
&:not(:last-of-type) {
margin-right: 20px;
}
`}
`
const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
`
const ColumnContainer = styled(AutoColumn)`
margin: 0 12px;
`
const PopupAlertTriangle = styled(AlertTriangleFilled)`
flex-shrink: 0;
width: 32px;
height: 32px;
`
export function FailedNetworkSwitchPopup({ chainId, onClose }: { chainId: ChainId; onClose: () => void }) {
const chainInfo = getChainInfo(chainId)
return (
<PopupContainer padded>
<StyledClose $padding={20} onClick={onClose} />
<RowNoFlex gap="12px">
<PopupAlertTriangle />
<ColumnContainer gap="sm">
<ThemedText.SubHeader color="textSecondary">
<Trans>Failed to switch networks</Trans>
</ThemedText.SubHeader>
<ThemedText.BodySmall color="textSecondary">
<Trans>To use Uniswap on {chainInfo.label}, switch the network in your wallet’s settings.</Trans>
</ThemedText.BodySmall>
</ColumnContainer>
</RowNoFlex>
</PopupContainer>
)
}
const Descriptor = styled(ThemedText.BodySmall)`
${EllipsisStyle}
`
type ActivityPopupContentProps = { activity: Activity; onClick: () => void; onClose: () => void }
function ActivityPopupContent({ activity, onClick, onClose }: ActivityPopupContentProps) {
const success = activity.status === TransactionStatus.Confirmed
const { ENSName } = useENSName(activity?.otherAccount)
return (
<PopupContainer>
<StyledClose $padding={16} onClick={onClose} />
<PortfolioRow
left={
success ? (
<Column>
<PortfolioLogo
chainId={activity.chainId}
currencies={activity.currencies}
images={activity.logos}
accountAddress={activity.otherAccount}
/>
</Column>
) : (
<PopupAlertTriangle />
)
}
title={<ThemedText.SubHeader>{activity.title}</ThemedText.SubHeader>}
descriptor={
<Descriptor color="textSecondary">
{activity.descriptor}
{ENSName ?? activity.otherAccount}
</Descriptor>
}
onClick={onClick}
/>
</PopupContainer>
)
}
export function TransactionPopupContent({
chainId,
hash,
onClose,
}: {
chainId: ChainId
hash: string
onClose: () => void
}) {
const transaction = useTransaction(hash)
const tokens = useAllTokensMultichain()
if (!transaction) return null
const activity = transactionToActivity(transaction, chainId, tokens)
if (!activity) return null
const onClick = () =>
window.open(getExplorerLink(activity.chainId, activity.hash, ExplorerDataType.TRANSACTION), '_blank')
return <ActivityPopupContent activity={activity} onClose={onClose} onClick={onClick} />
}
export function UniswapXOrderPopupContent({ orderHash, onClose }: { orderHash: string; onClose: () => void }) {
const order = useOrder(orderHash)
const tokens = useAllTokensMultichain()
const openOffchainActivityModal = useOpenOffchainActivityModal()
if (!order) return null
const activity = signatureToActivity(order, tokens)
if (!activity) return null
const onClick = () => openOffchainActivityModal({ orderHash, status: order.status })
return <ActivityPopupContent activity={activity} onClose={onClose} onClick={onClick} />
}
import { useWeb3React } from '@web3-react/core'
import { useEffect } from 'react'
import { X } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { useRemovePopup } from '../../state/application/hooks'
import { PopupContent } from '../../state/application/reducer'
import FailedNetworkSwitchPopup from './FailedNetworkSwitchPopup'
import TransactionPopup from './TransactionPopup'
const StyledClose = styled(X)<{ $padding: number }>`
position: absolute;
right: ${({ $padding }) => `${$padding}px`};
top: ${({ $padding }) => `${$padding}px`};
:hover {
cursor: pointer;
}
`
const PopupCss = css<{ show: boolean }>`
display: inline-block;
width: 100%;
visibility: ${({ show }) => (show ? 'visible' : 'hidden')};
background-color: ${({ theme }) => theme.backgroundSurface};
position: relative;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-radius: 16px;
overflow: hidden;
box-shadow: ${({ theme }) => theme.deepShadow};
transition: ${({ theme }) => `visibility ${theme.transition.duration.fast} ease-in-out`};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
min-width: 290px;
&:not(:last-of-type) {
margin-right: 20px;
}
`}
`
const TransactionPopupContainer = styled.div`
${PopupCss}
padding: 2px 0px;
`
const FailedSwitchNetworkPopupContainer = styled.div<{ show: boolean }>`
${PopupCss}
padding: 20px 35px 20px 20px;
`
import { PopupContent, PopupType } from '../../state/application/reducer'
import { FailedNetworkSwitchPopup, TransactionPopupContent, UniswapXOrderPopupContent } from './PopupContent'
export default function PopupItem({
removeAfterMs,
......@@ -56,7 +15,7 @@ export default function PopupItem({
popKey: string
}) {
const removePopup = useRemovePopup()
const theme = useTheme()
const onClose = () => removePopup(popKey)
useEffect(() => {
if (removeAfterMs === null) return undefined
......@@ -70,20 +29,17 @@ export default function PopupItem({
}
}, [popKey, removeAfterMs, removePopup])
if ('txn' in content) {
return (
<TransactionPopupContainer show={true}>
<StyledClose $padding={16} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
<TransactionPopup hash={content.txn.hash} />
</TransactionPopupContainer>
)
} else if ('failedSwitchNetwork' in content) {
return (
<FailedSwitchNetworkPopupContainer show={true}>
<StyledClose $padding={20} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
<FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
</FailedSwitchNetworkPopupContainer>
)
const { chainId } = useWeb3React()
switch (content.type) {
case PopupType.Transaction: {
return chainId ? <TransactionPopupContent hash={content.hash} chainId={chainId} onClose={onClose} /> : null
}
case PopupType.Order: {
return <UniswapXOrderPopupContent orderHash={content.orderHash} onClose={onClose} />
}
case PopupType.FailedSwitchNetwork: {
return <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} onClose={onClose} />
}
}
return null
}
import { useWeb3React } from '@web3-react/core'
import { parseLocalActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow'
import Column from 'components/Column'
import { useAllTokensMultichain } from 'hooks/Tokens'
import useENSName from 'hooks/useENSName'
import { useTransaction } from 'state/transactions/hooks'
import { TransactionDetails } from 'state/transactions/types'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PopupAlertTriangle } from './FailedNetworkSwitchPopup'
const Descriptor = styled(ThemedText.BodySmall)`
${EllipsisStyle}
`
function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chainId: number }) {
const success = tx.receipt?.status === 1
const tokens = useAllTokensMultichain()
const activity = parseLocalActivity(tx, chainId, tokens)
const { ENSName } = useENSName(activity?.otherAccount)
if (!activity) return null
const explorerUrl = getExplorerLink(chainId, tx.hash, ExplorerDataType.TRANSACTION)
return (
<PortfolioRow
left={
success ? (
<Column>
<PortfolioLogo
chainId={chainId}
currencies={activity.currencies}
images={activity.logos}
accountAddress={activity.otherAccount}
/>
</Column>
) : (
<PopupAlertTriangle />
)
}
title={<ThemedText.SubHeader>{activity.title}</ThemedText.SubHeader>}
descriptor={
<Descriptor color="textSecondary">
{activity.descriptor}
{ENSName ?? activity.otherAccount}
</Descriptor>
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
)
}
export default function TransactionPopup({ hash }: { hash: string }) {
const { chainId } = useWeb3React()
const tx = useTransaction(hash)
if (!chainId || !tx) return null
return <TransactionPopupContent tx={tx} chainId={chainId} />
}
import { RowBetween } from 'components/Row'
import { darken } from 'polished'
import { PropsWithChildren } from 'react'
import styled from 'styled-components/macro'
const Button = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
align-items: center;
background: transparent;
border: 2px solid ${({ theme, isActive }) => (isActive ? theme.accentAction : theme.backgroundOutline)};
border-radius: 50%;
cursor: pointer;
display: flex;
outline: none;
padding: 5px;
width: fit-content;
`
const ButtonFill = styled.span<{ isActive?: boolean }>`
background: ${({ theme, isActive }) => (isActive ? theme.accentAction : theme.textTertiary)};
border-radius: 50%;
:hover {
background: ${({ isActive, theme }) =>
isActive ? darken(0.05, theme.accentAction) : darken(0.05, theme.deprecated_bg4)};
color: ${({ isActive, theme }) => (isActive ? theme.white : theme.textTertiary)};
}
width: 10px;
height: 10px;
opacity: ${({ isActive }) => (isActive ? 1 : 0)};
`
const Container = styled(RowBetween)`
cursor: pointer;
`
interface RadioProps {
className?: string
isActive: boolean
toggle: () => void
}
export default function Radio({ className, isActive, children, toggle }: PropsWithChildren<RadioProps>) {
return (
<Container className={className} onClick={toggle}>
{children}
<Button isActive={isActive}>
<ButtonFill isActive={isActive} />
</Button>
</Container>
)
}
import Row from 'components/Row'
import { useRef } from 'react'
import styled from 'styled-components/macro'
import { v4 as uuid } from 'uuid'
import { BoxProps } from '../../nft/components/Box'
// Gradient with a fallback to solid color.
const Gradient = styled.div`
color: #4673fa;
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
background-image: linear-gradient(91.39deg, #4673fa -101.76%, #9646fa 101.76%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
`
export { Gradient as UniswapXGradient }
// Uniswap X SVG icon with gradient, copied from Figma.
// In order for gradient to work, we must give its definition a unique ID that does not collide
// with other occurences of this component on the page.
export const UniswapXRouterIcon = () => {
const componentIdRef = useRef(uuid())
const componentId = `AutoRouterIconGradient${componentIdRef.current}`
return (
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient
id={componentId}
x1="-10.1807"
y1="-12.0006"
x2="10.6573"
y2="-11.6017"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#4673FA" />
<stop offset="1" stopColor="#9646FA" />
</linearGradient>
</defs>
<path
d="M9.97131 6.19803C9.91798 6.07737 9.79866 6.00003 9.66666 6.00003H6.66666V1.00003C6.66666 0.862034 6.58201 0.738037 6.45267 0.688704C6.32267 0.638704 6.17799 0.674696 6.08532 0.776696L0.0853237 7.44336C-0.00267631 7.54136 -0.0253169 7.68137 0.0286831 7.80204C0.0820164 7.9227 0.20133 8.00003 0.33333 8.00003H3.33333V13C3.33333 13.138 3.41799 13.262 3.54732 13.3114C3.58665 13.326 3.62666 13.3334 3.66666 13.3334C3.75933 13.3334 3.85 13.2947 3.91467 13.2227L9.91467 6.55603C10.0027 6.4587 10.0246 6.31803 9.97131 6.19803Z"
fill={`url(#${componentId})`}
/>
</svg>
)
}
export type UnswapXRouterLabelProps = BoxProps & {
disableTextGradient?: boolean
}
export default function UniswapXRouterLabel({ children, disableTextGradient, ...rest }: UnswapXRouterLabelProps) {
return (
<Row gap="xs" width="auto" {...rest} style={{ display: 'inline-flex', ...rest.style }}>
<UniswapXRouterIcon />
{disableTextGradient ? children : <Gradient>{children}</Gradient>}
</Row>
)
}
import { InterfaceTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils'
import { ThemedText } from 'theme'
import UniswapXRouterLabel from './UniswapXRouterLabel'
export default function RouterLabel({ trade }: { trade: InterfaceTrade }) {
if (isUniswapXTrade(trade)) {
return (
<UniswapXRouterLabel>
<ThemedText.BodySmall>Uniswap X</ThemedText.BodySmall>
</UniswapXRouterLabel>
)
}
if (trade.fromClientRouter) {
return <ThemedText.BodySmall>Uniswap Client</ThemedText.BodySmall>
}
return <ThemedText.BodySmall>Uniswap API</ThemedText.BodySmall>
}
import store from 'state'
import { RouterPreference } from 'state/routing/slice'
import { updateUserRouterPreference } from 'state/user/reducer'
import { fireEvent, render, screen } from 'test-utils/render'
import RouterPreferenceSettings from '.'
jest.mock('featureFlags/flags/uniswapx', () => ({
useUniswapXEnabled: () => true,
}))
describe('RouterPreferenceSettings', () => {
// Restore to default router preference before each unit test
beforeEach(() => {
store.dispatch(updateUserRouterPreference({ userRouterPreference: RouterPreference.API }))
})
it('toggles `Uniswap X` router preference', () => {
render(<RouterPreferenceSettings />)
const uniswapXToggle = screen.getByTestId('toggle-uniswap-x-button')
fireEvent.click(uniswapXToggle)
expect(uniswapXToggle).toHaveAttribute('aria-selected', 'true')
expect(store.getState().user.userRouterPreference).toEqual(RouterPreference.X)
fireEvent.click(uniswapXToggle)
expect(uniswapXToggle).toHaveAttribute('aria-selected', 'false')
expect(store.getState().user.userRouterPreference).toEqual(RouterPreference.API)
})
it('toggles `Local Routing` router preference', () => {
render(<RouterPreferenceSettings />)
const localRoutingToggle = screen.getByTestId('toggle-local-routing-button')
fireEvent.click(localRoutingToggle)
expect(localRoutingToggle).toHaveAttribute('aria-selected', 'true')
expect(store.getState().user.userRouterPreference).toEqual(RouterPreference.CLIENT)
fireEvent.click(localRoutingToggle)
expect(localRoutingToggle).toHaveAttribute('aria-selected', 'false')
expect(store.getState().user.userRouterPreference).toEqual(RouterPreference.API)
})
})
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import Radio from 'components/Radio'
import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { RowBetween, RowFixed } from 'components/Row'
import Toggle from 'components/Toggle'
import { isUniswapXSupportedChain } from 'constants/chains'
import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx'
import { useAppDispatch } from 'state/hooks'
import { RouterPreference } from 'state/routing/slice'
import { useRouterPreference } from 'state/user/hooks'
import { updateDisabledUniswapX } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Divider, ExternalLink, ThemedText } from 'theme'
const Preference = styled(Radio)`
background-color: ${({ theme }) => theme.backgroundModule};
padding: 12px 16px;
`
const PreferencesContainer = styled(Column)`
gap: 1.5px;
border-radius: 12px;
overflow: hidden;
const InlineLink = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.accentAction};
display: inline;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`
export default function RouterPreferenceSettings() {
const { chainId } = useWeb3React()
const [routerPreference, setRouterPreference] = useRouterPreference()
const isAutoRoutingActive = routerPreference === RouterPreference.AUTO
const uniswapXEnabled = useUniswapXEnabled() && chainId && isUniswapXSupportedChain(chainId)
const dispatch = useAppDispatch()
return (
<Column gap="md">
<>
{uniswapXEnabled && (
<>
<RowBetween gap="sm">
<RowFixed>
<Column gap="xs">
<ThemedText.BodySecondary>
<UniswapXBrandMark />
</ThemedText.BodySecondary>
<ThemedText.Caption color="textSecondary">
<Trans>When available, aggregates liquidity sources for better prices and gas free swaps.</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/17515415311501">
<InlineLink>Learn more</InlineLink>
</ExternalLink>
</ThemedText.Caption>
</Column>
</RowFixed>
<Toggle
id="toggle-uniswap-x-button"
isActive={routerPreference === RouterPreference.X}
toggle={() => {
if (routerPreference === RouterPreference.X) {
// We need to remember if a user disables Uniswap X, so we don't show the opt-in flow again.
dispatch(updateDisabledUniswapX({ disabledUniswapX: true }))
}
setRouterPreference(routerPreference === RouterPreference.X ? RouterPreference.API : RouterPreference.X)
}}
/>
</RowBetween>
<Divider />
</>
)}
<RowBetween gap="sm">
<RowFixed>
<Column gap="xs">
<ThemedText.BodySecondary>
<Trans>Auto Router API</Trans>
<Trans>Local routing</Trans>
</ThemedText.BodySecondary>
<ThemedText.Caption color="textSecondary">
<Trans>Use the Uniswap Labs API to get faster quotes.</Trans>
</ThemedText.Caption>
</Column>
</RowFixed>
<Toggle
id="toggle-optimized-router-button"
isActive={isAutoRoutingActive}
toggle={() => setRouterPreference(isAutoRoutingActive ? RouterPreference.API : RouterPreference.AUTO)}
id="toggle-local-routing-button"
isActive={routerPreference === RouterPreference.CLIENT}
toggle={() =>
setRouterPreference(
routerPreference === RouterPreference.CLIENT ? RouterPreference.API : RouterPreference.CLIENT
)
}
/>
</RowBetween>
{!isAutoRoutingActive && (
<PreferencesContainer>
<Preference
isActive={routerPreference === RouterPreference.API}
toggle={() => setRouterPreference(RouterPreference.API)}
>
<Column gap="xs">
<ThemedText.BodyPrimary>
<Trans>Uniswap API</Trans>
</ThemedText.BodyPrimary>
<ThemedText.Caption color="textSecondary">
<Trans>Finds the best route on the Uniswap Protocol using the Uniswap Labs Routing API.</Trans>
</ThemedText.Caption>
</Column>
</Preference>
<Preference
isActive={routerPreference === RouterPreference.CLIENT}
toggle={() => setRouterPreference(RouterPreference.CLIENT)}
>
<Column gap="xs">
<ThemedText.BodyPrimary>
<Trans>Uniswap client</Trans>
</ThemedText.BodyPrimary>
<ThemedText.Caption color="textSecondary">
<Trans>
Finds the best route on the Uniswap Protocol through your browser. May result in high latency and
prices.
</Trans>
</ThemedText.Caption>
</Column>
</Preference>
</PreferencesContainer>
)}
</Column>
</>
)
}
import { Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column'
import { isSupportedChain, L2_CHAIN_IDS } from 'constants/chains'
import useDisableScrolling from 'hooks/useDisableScrolling'
......@@ -7,6 +8,8 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef } from 'react'
import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { InterfaceTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components/macro'
import { Divider } from 'theme'
......@@ -36,11 +39,23 @@ const MenuFlyout = styled(AutoColumn)`
min-width: 18.125rem;
`};
user-select: none;
padding: 16px;
`
const ExpandColumn = styled(AutoColumn)`
gap: 16px;
padding: 1rem;
padding-top: 16px;
`
export default function SettingsTab({ autoSlippage, chainId }: { autoSlippage: Percent; chainId?: number }) {
export default function SettingsTab({
autoSlippage,
chainId,
trade,
}: {
autoSlippage: Percent
chainId?: number
trade?: InterfaceTrade
}) {
const { chainId: connectedChainId } = useWeb3React()
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
......@@ -59,15 +74,21 @@ export default function SettingsTab({ autoSlippage, chainId }: { autoSlippage: P
<MenuButton disabled={!isChainSupported || chainId !== connectedChainId} isActive={isOpen} onClick={toggleMenu} />
{isOpen && (
<MenuFlyout>
<RouterPreferenceSettings />
<Divider />
<MaxSlippageSettings autoSlippage={autoSlippage} />
{showDeadlineSettings && (
<>
<AutoColumn gap="16px">
<RouterPreferenceSettings />
</AutoColumn>
<AnimatedDropdown open={!isUniswapXTrade(trade)}>
<ExpandColumn>
<Divider />
<TransactionDeadlineSettings />
</>
)}
<MaxSlippageSettings autoSlippage={autoSlippage} />
{showDeadlineSettings && (
<>
<Divider />
<TransactionDeadlineSettings />
</>
)}
</ExpandColumn>
</AnimatedDropdown>
</MenuFlyout>
)}
</Menu>
......
......@@ -78,7 +78,7 @@ export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
}
return (
<Wrapper id={id} isActive={isActive} onClick={switchToggle}>
<Wrapper id={id} data-testid={id} role="option" aria-selected={isActive} isActive={isActive} onClick={switchToggle}>
<ToggleElement isActive={isActive} bgColor={bgColor} isInitialToggleLoad={isInitialToggleLoad} />
</Wrapper>
)
......
......@@ -6,12 +6,15 @@ import noop from 'utils/noop'
import Popover, { PopoverProps } from '../Popover'
export enum TooltipSize {
ExtraSmall = '200px',
Small = '256px',
Large = '400px',
}
const getPaddingForSize = (size: TooltipSize) => {
switch (size) {
case TooltipSize.ExtraSmall:
return '8px'
case TooltipSize.Small:
return '12px'
case TooltipSize.Large:
......
import { useWeb3React } from '@web3-react/core'
import { OffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner'
import AddressClaimModal from 'components/claim/AddressClaimModal'
......@@ -28,6 +29,7 @@ export default function TopLevelModals() {
<Bag />
<UniwalletModal />
<UniswapWalletBanner />
<OffchainActivityModal />
<TransactionCompleteModal />
<AirdropModal />
<FiatOnrampModal />
......
......@@ -14,6 +14,7 @@ import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { darken } from 'polished'
import { useCallback, useMemo } from 'react'
import { useAppSelector } from 'state/hooks'
import { usePendingOrders } from 'state/signatures/hooks'
import styled from 'styled-components/macro'
import { colors } from 'theme/colors'
import { flexRowNoWrap } from 'theme/styles'
......@@ -151,9 +152,11 @@ function Web3StatusInner() {
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
}, [allTransactions])
const pending = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
const pendingOrders = usePendingOrders()
const hasPendingTransactions = !!pending.length
const pendingTxs = sortedRecentTransactions.filter((tx) => !tx.receipt).map((tx) => tx.hash)
const hasPendingActivity = !!pendingTxs.length || !!pendingOrders.length
if (account) {
return (
......@@ -166,16 +169,16 @@ function Web3StatusInner() {
disabled={Boolean(switchingChain)}
data-testid="web3-status-connected"
onClick={handleWalletDropdownClick}
pending={hasPendingTransactions}
pending={hasPendingActivity}
isClaimAvailable={isClaimAvailable}
>
{!hasPendingTransactions && (
<StatusIcon size={24} account={account} connection={connection} showMiniIcons={false} />
{!hasPendingActivity && (
<StatusIcon account={account} size={24} connection={connection} showMiniIcons={false} />
)}
{hasPendingTransactions ? (
{hasPendingActivity ? (
<RowBetween>
<Text>
<Trans>{pending?.length} Pending</Trans>
<Trans>{pendingTxs.length + pendingOrders.length} Pending</Trans>
</Text>{' '}
<Loader stroke="white" />
</RowBetween>
......
......@@ -21,7 +21,7 @@ describe('AdvancedSwapDetails.tsx', () => {
})
it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => {
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = '1.00'
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = 1.0
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum input/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
......
import { Trans } from '@lingui/macro'
import { Plural, Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { formatCurrencyAmount, formatPriceImpact, formatUSDPrice, NumberType } from '@uniswap/conedison/format'
import { formatCurrencyAmount, formatNumber, formatPriceImpact, NumberType } from '@uniswap/conedison/format'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
import { Separator, ThemedText } from '../../theme'
import Column from '../Column'
import RouterLabel from '../RouterLabel'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip, TooltipSize } from '../Tooltip'
import RouterLabel from './RouterLabel'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import SwapRoute from './SwapRoute'
interface AdvancedSwapDetailsProps {
......@@ -43,11 +45,14 @@ function TextWithLoadingPlaceholder({
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
const txCount = getTransactionCount(trade)
const supportsGasEstimate = chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
return (
<Column gap="md">
<Separator />
{!trade.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
{supportsGasEstimate && (
<RowBetween>
<MouseoverTooltip
text={
......@@ -57,24 +62,37 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
}
>
<ThemedText.BodySmall color="textSecondary">
<Trans>Network fee</Trans>
<Plural value={txCount} one="Network fee" other="Network fees" />
</ThemedText.BodySmall>
</MouseoverTooltip>
<MouseoverTooltip
placement="right"
size={TooltipSize.Small}
text={<GasBreakdownTooltip trade={trade} hideUniswapXDescription />}
>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>
{`${trade.totalGasUseEstimateUSD ? '~' : ''}${formatNumber(
trade.totalGasUseEstimateUSD,
NumberType.FiatGasPrice
)}`}
</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</MouseoverTooltip>
</RowBetween>
)}
{isClassicTrade(trade) && (
<RowBetween>
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<ThemedText.BodySmall color="textSecondary">
<Trans>Price Impact</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>~{formatUSDPrice(trade.gasUseEstimateUSD)}</ThemedText.BodySmall>
<ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
)}
<RowBetween>
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<ThemedText.BodySmall color="textSecondary">
<Trans>Price Impact</Trans>
</ThemedText.BodySmall>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder>
</RowBetween>
<RowBetween>
<RowFixed>
<MouseoverTooltip
......@@ -128,17 +146,32 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
<ThemedText.BodySmall color="textSecondary">
<Trans>Order routing</Trans>
</ThemedText.BodySmall>
<MouseoverTooltip
size={TooltipSize.Large}
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
>
<RouterLabel />
</MouseoverTooltip>
{isClassicTrade(trade) ? (
<MouseoverTooltip
size={TooltipSize.Large}
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
>
<RouterLabel trade={trade} />
</MouseoverTooltip>
) : (
<MouseoverTooltip
size={TooltipSize.Small}
text={<GasBreakdownTooltip trade={trade} hideFees />}
placement="right"
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
>
<RouterLabel trade={trade} />
</MouseoverTooltip>
)}
</RowBetween>
</Column>
)
......
......@@ -6,7 +6,8 @@ import {
SwapEventName,
SwapPriceUpdateUserResponse,
} from '@uniswap/analytics-events'
import { Percent } from '@uniswap/sdk-core'
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { Currency, Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import Badge from 'components/Badge'
import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal'
......@@ -16,9 +17,14 @@ import { USDT as USDT_MAINNET } from 'constants/tokens'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious'
import { SwapResult } from 'hooks/useSwapCallback'
import useWrapCallback from 'hooks/useWrapCallback'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useCallback, useEffect, useState } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { Field } from 'state/swap/actions'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import invariant from 'tiny-invariant'
......@@ -35,6 +41,7 @@ import SwapModalHeader from './SwapModalHeader'
export enum ConfirmModalState {
REVIEWING,
WRAPPING,
RESETTING_USDT,
APPROVING_TOKEN,
PERMITTING,
......@@ -64,12 +71,14 @@ function useConfirmModalState({
onSwap,
allowance,
doesTradeDiffer,
onCurrencySelection,
}: {
trade: InterfaceTrade
allowedSlippage: Percent
onSwap: () => void
allowance: Allowance
doesTradeDiffer: boolean
onCurrencySelection: (field: Field, currency: Currency) => void
}) {
const [confirmModalState, setConfirmModalState] = useState<ConfirmModalState>(ConfirmModalState.REVIEWING)
const [approvalError, setApprovalError] = useState<PendingModalError>()
......@@ -80,6 +89,9 @@ function useConfirmModalState({
// at the bottom of the modal, even after they complete steps 1 and 2.
const generateRequiredSteps = useCallback(() => {
const steps: PendingConfirmModalState[] = []
if (trade.fillType === TradeFillType.UniswapX && trade.wrapInfo.needsWrap) {
steps.push(ConfirmModalState.WRAPPING)
}
// Any existing USDT allowance needs to be reset before we can approve the new amount (mainnet only).
// See the `approve` function here: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7#code
if (
......@@ -97,12 +109,22 @@ function useConfirmModalState({
}
steps.push(ConfirmModalState.PENDING_CONFIRMATION)
return steps
}, [allowance])
}, [allowance, trade])
const { chainId } = useWeb3React()
const trace = useTrace()
const maximumAmountIn = useMaxAmountIn(trade, allowedSlippage)
const nativeCurrency = useNativeCurrency(chainId)
const [wrapTxHash, setWrapTxHash] = useState<string>()
const { execute: onWrap } = useWrapCallback(
nativeCurrency,
trade.inputAmount.currency,
formatCurrencyAmount(trade.inputAmount, NumberType.SwapTradeAmount)
)
const wrapConfirmed = useIsTransactionConfirmed(wrapTxHash)
const prevWrapConfirmed = usePrevious(wrapConfirmed)
const catchUserReject = async (e: any, errorType: PendingModalError) => {
setConfirmModalState(ConfirmModalState.REVIEWING)
if (didUserReject(e)) return
......@@ -113,6 +135,24 @@ function useConfirmModalState({
const performStep = useCallback(
async (step: ConfirmModalState) => {
switch (step) {
case ConfirmModalState.WRAPPING:
setConfirmModalState(ConfirmModalState.WRAPPING)
onWrap?.()
.then((wrapTxHash) => {
setWrapTxHash(wrapTxHash)
// After the wrap has succeeded, reset the input currency to be WETH
// because the trade will be on WETH -> token
onCurrencySelection(Field.INPUT, trade.inputAmount.currency)
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
chain_id: chainId,
token_symbol: maximumAmountIn?.currency.symbol,
token_address: maximumAmountIn?.currency.address,
...trade,
...trace,
})
})
.catch((e) => catchUserReject(e, PendingModalError.WRAP_ERROR))
break
case ConfirmModalState.RESETTING_USDT:
setConfirmModalState(ConfirmModalState.RESETTING_USDT)
invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required')
......@@ -151,7 +191,17 @@ function useConfirmModalState({
break
}
},
[allowance, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, onSwap, trace]
[
allowance,
chainId,
maximumAmountIn?.currency.address,
maximumAmountIn?.currency.symbol,
onSwap,
onWrap,
trace,
trade,
onCurrencySelection,
]
)
const startSwapFlow = useCallback(() => {
......@@ -163,6 +213,15 @@ function useConfirmModalState({
const previousSetupApprovalNeeded = usePrevious(
allowance.state === AllowanceState.REQUIRED ? allowance.needsSetupApproval : undefined
)
useEffect(() => {
// If the wrapping step finished, trigger the next step (allowance or swap).
if (wrapConfirmed && !prevWrapConfirmed) {
// moves on to either approve WETH or to swap submission
performStep(pendingModalSteps[1])
}
}, [pendingModalSteps, performStep, prevWrapConfirmed, wrapConfirmed])
useEffect(() => {
if (
allowance.state === AllowanceState.REQUIRED &&
......@@ -202,45 +261,51 @@ function useConfirmModalState({
setApprovalError(undefined)
}
return { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps }
return { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps, wrapTxHash }
}
export default function ConfirmSwapModal({
trade,
inputCurrency,
originalTrade,
onAcceptChanges,
allowedSlippage,
allowance,
onConfirm,
onDismiss,
onCurrencySelection,
swapError,
txHash,
swapResult,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
}: {
trade: InterfaceTrade
inputCurrency?: Currency
originalTrade?: InterfaceTrade
txHash?: string
swapResult?: SwapResult
allowedSlippage: Percent
allowance: Allowance
onAcceptChanges: () => void
onConfirm: () => void
swapError?: Error
onDismiss: () => void
onCurrencySelection: (field: Field, currency: Currency) => void
swapQuoteReceivedDate?: Date
fiatValueInput: { data?: number; isLoading: boolean }
fiatValueOutput: { data?: number; isLoading: boolean }
}) {
const { chainId } = useWeb3React()
const doesTradeDiffer = originalTrade && tradeMeaningfullyDiffers(trade, originalTrade, allowedSlippage)
const { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps } = useConfirmModalState({
trade,
allowedSlippage,
onSwap: onConfirm,
allowance,
doesTradeDiffer: Boolean(doesTradeDiffer),
})
const { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps, wrapTxHash } =
useConfirmModalState({
trade,
allowedSlippage,
onSwap: onConfirm,
onCurrencySelection,
allowance,
doesTradeDiffer: Boolean(doesTradeDiffer),
})
const swapFailed = Boolean(swapError) && !didUserReject(swapError)
useEffect(() => {
......@@ -282,8 +347,8 @@ export default function ConfirmSwapModal({
if (confirmModalState !== ConfirmModalState.REVIEWING && !showAcceptChanges) {
return null
}
return <SwapModalHeader trade={trade} allowedSlippage={allowedSlippage} />
}, [allowedSlippage, confirmModalState, showAcceptChanges, trade])
return <SwapModalHeader inputCurrency={inputCurrency} trade={trade} allowedSlippage={allowedSlippage} />
}, [allowedSlippage, confirmModalState, showAcceptChanges, trade, inputCurrency])
const modalBottom = useCallback(() => {
if (confirmModalState === ConfirmModalState.REVIEWING || showAcceptChanges) {
......@@ -291,7 +356,7 @@ export default function ConfirmSwapModal({
<SwapModalFooter
onConfirm={startSwapFlow}
trade={trade}
hash={txHash}
swapResult={swapResult}
allowedSlippage={allowedSlippage}
disabledConfirm={showAcceptChanges}
swapQuoteReceivedDate={swapQuoteReceivedDate}
......@@ -309,7 +374,8 @@ export default function ConfirmSwapModal({
steps={pendingModalSteps}
currentStep={confirmModalState}
trade={trade}
swapTxHash={txHash}
swapResult={swapResult}
wrapTxHash={wrapTxHash}
tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending}
revocationPending={allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending}
/>
......@@ -319,7 +385,8 @@ export default function ConfirmSwapModal({
showAcceptChanges,
pendingModalSteps,
trade,
txHash,
swapResult,
wrapTxHash,
allowance,
allowedSlippage,
swapQuoteReceivedDate,
......
import { Trans } from '@lingui/macro'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import { AutoColumn } from 'components/Column'
import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel'
import Row from 'components/Row'
import { ReactNode } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components/macro'
import { Divider, ExternalLink, ThemedText } from 'theme'
const Container = styled(AutoColumn)`
padding: 4px;
`
const InlineLink = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.accentAction};
display: inline;
cursor: pointer;
&:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const InlineUniswapXGradient = styled(UniswapXGradient)`
display: inline;
`
const GasCostItem = ({
title,
amount,
itemValue,
}: {
title: ReactNode
itemValue?: React.ReactNode
amount?: number
}) => {
return (
<Row justify="space-between">
<ThemedText.SubHeaderSmall>{title}</ThemedText.SubHeaderSmall>
<ThemedText.SubHeaderSmall color="textPrimary">
{itemValue ?? formatNumber(amount, NumberType.FiatGasPrice)}
</ThemedText.SubHeaderSmall>
</Row>
)
}
export function GasBreakdownTooltip({
trade,
hideFees = false,
hideUniswapXDescription = false,
}: {
trade: InterfaceTrade
hideFees?: boolean
hideUniswapXDescription?: boolean
}) {
const swapEstimate = isClassicTrade(trade) ? trade.gasUseEstimateUSD : undefined
const approvalEstimate = trade.approveInfo.needsApprove ? trade.approveInfo.approveGasEstimateUSD : undefined
const wrapEstimate =
isUniswapXTrade(trade) && trade.wrapInfo.needsWrap ? trade.wrapInfo.wrapGasEstimateUSD : undefined
return (
<Container gap="md">
{(wrapEstimate || approvalEstimate) && !hideFees && (
<>
<AutoColumn gap="sm">
{wrapEstimate && <GasCostItem title={<Trans>Wrap ETH</Trans>} amount={wrapEstimate} />}
{approvalEstimate && (
<GasCostItem
title={<Trans>Allow {trade.inputAmount.currency.symbol} (one time)</Trans>}
amount={approvalEstimate}
/>
)}
{swapEstimate && <GasCostItem title={<Trans>Swap</Trans>} amount={swapEstimate} />}
{isUniswapXTrade(trade) && (
<GasCostItem title={<Trans>Swap</Trans>} itemValue={<UniswapXRouterLabel>$0</UniswapXRouterLabel>} />
)}
</AutoColumn>
<Divider />
</>
)}
{isUniswapXTrade(trade) && !hideUniswapXDescription ? (
<ThemedText.Caption color="textSecondary">
<Trans>
<InlineUniswapXGradient>UniswapX</InlineUniswapXGradient> aggregates liquidity sources for better prices and
gas free swaps.
</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/17515415311501">
<InlineLink>
<Trans>Learn more</Trans>
</InlineLink>
</ExternalLink>
</ThemedText.Caption>
) : (
<ThemedText.Caption color="textSecondary">
<Trans>Network Fees are paid to the Ethereum network to secure transactions.</Trans>{' '}
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/8370337377805-What-is-a-network-fee-">
<InlineLink>
<Trans>Learn more</Trans>
</InlineLink>
</ExternalLink>
</ThemedText.Caption>
)}
</Container>
)
}
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import { LoadingOpacityContainer } from 'components/Loader/styled'
import { RowFixed } from 'components/Row'
import { UniswapXRouterIcon } from 'components/RouterLabel/UniswapXRouterLabel'
import Row, { RowFixed } from 'components/Row'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { InterfaceTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg'
import SwapRoute from './SwapRoute'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
const StyledGasIcon = styled(GasIcon)`
margin-right: 4px;
height: 18px;
// We apply the following to all children of the SVG in order to override the default color
......@@ -20,41 +24,37 @@ const StyledGasIcon = styled(GasIcon)`
}
`
export default function GasEstimateTooltip({
trade,
loading,
disabled,
}: {
trade: InterfaceTrade // dollar amount in active chain's stablecoin
loading: boolean
disabled?: boolean
}) {
const formattedGasPriceString = trade?.gasUseEstimateUSD
? trade.gasUseEstimateUSD === '0.00'
? '<$0.01'
: '$' + trade.gasUseEstimateUSD
: undefined
export default function GasEstimateTooltip({ trade, loading }: { trade?: InterfaceTrade; loading: boolean }) {
const { chainId } = useWeb3React()
if (!trade || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)) {
return null
}
return (
<MouseoverTooltip
disabled={disabled}
size={TooltipSize.Large}
// TODO(WEB-2246)
// Most of Swap-related components accept either `syncing`, `loading` or both props at the same time.
// We are often using them interchangeably, or pass both values as one of them (`syncing={loading || syncing}`).
// This is confusing and can lead to unpredicted UI behavior. We should refactor and unify this.
text={<SwapRoute trade={trade} syncing={loading} />}
size={TooltipSize.Small}
text={<GasBreakdownTooltip trade={trade} />}
onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
})
}}
placement="bottom"
placement="right"
>
<LoadingOpacityContainer $loading={loading}>
<RowFixed>
<StyledGasIcon />
<ThemedText.BodySmall color="textSecondary">{formattedGasPriceString}</ThemedText.BodySmall>
<RowFixed gap="xs">
{isUniswapXTrade(trade) ? <UniswapXRouterIcon /> : <StyledGasIcon />}
<ThemedText.BodySmall color="textSecondary">
<Row gap="xs">
<div>{formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)}</div>
{isUniswapXTrade(trade) && (
<div>
<s>{formatNumber(trade.classicGasUseEstimateUSD, NumberType.FiatGasPrice)}</s>
</div>
)}
</Row>
</ThemedText.BodySmall>
</RowFixed>
</LoadingOpacityContainer>
</MouseoverTooltip>
......
......@@ -13,6 +13,7 @@ export enum PendingModalError {
TOKEN_APPROVAL_ERROR,
PERMIT_ERROR,
CONFIRMATION_ERROR,
WRAP_ERROR,
}
interface ErrorModalContentProps {
......@@ -45,6 +46,10 @@ function getErrorContent(errorType: PendingModalError) {
return {
title: <Trans>Swap failed</Trans>,
}
case PendingModalError.WRAP_ERROR:
return {
title: <Trans>Wrap failed</Trans>,
}
}
}
......
......@@ -59,7 +59,7 @@ const FadeWrapper = styled.div<{ $scale: boolean }>`
}
`
function FadePresence({
export function FadePresence({
children,
className,
$scale = false,
......
......@@ -6,7 +6,7 @@ import { InterfaceTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
export function TradeSummary({ trade }: { trade: InterfaceTrade }) {
export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> }) {
const theme = useTheme()
return (
<Row gap="sm" justify="center" align="center">
......
import { t, Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/sdk-core'
import { ChainId, Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { OrderContent } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import { ColumnCenter } from 'components/Column'
import Column from 'components/Column'
import Row from 'components/Row'
import { SwapResult } from 'hooks/useSwapCallback'
import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { ReactNode, useRef } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { useOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink } from 'theme'
......@@ -90,6 +94,7 @@ export type PendingConfirmModalState = Extract<
| ConfirmModalState.APPROVING_TOKEN
| ConfirmModalState.PERMITTING
| ConfirmModalState.PENDING_CONFIRMATION
| ConfirmModalState.WRAPPING
| ConfirmModalState.RESETTING_USDT
>
......@@ -105,7 +110,8 @@ interface PendingModalContentProps {
steps: PendingConfirmModalState[]
currentStep: PendingConfirmModalState
trade?: InterfaceTrade
swapTxHash?: string
swapResult?: SwapResult
wrapTxHash?: string
hideStepIndicators?: boolean
tokenApprovalPending?: boolean
revocationPending?: boolean
......@@ -117,25 +123,39 @@ interface ContentArgs {
trade?: InterfaceTrade
swapConfirmed: boolean
swapPending: boolean
wrapPending: boolean
tokenApprovalPending: boolean
revocationPending: boolean
swapTxHash?: string
swapResult?: SwapResult
chainId?: number
order?: UniswapXOrderDetails
}
function getContent(args: ContentArgs): PendingModalStep {
const {
step,
wrapPending,
approvalCurrency,
swapConfirmed,
swapPending,
tokenApprovalPending,
revocationPending,
trade,
swapTxHash,
swapResult,
chainId,
} = args
switch (step) {
case ConfirmModalState.WRAPPING:
return {
title: t`Wrap ETH`,
subtitle: (
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/16015852009997">
<Trans>Why is this required?</Trans>
</ExternalLink>
),
label: wrapPending ? t`Pending...` : t`Proceed in your wallet`,
}
case ConfirmModalState.RESETTING_USDT:
return {
title: t`Reset USDT`,
......@@ -162,22 +182,32 @@ function getContent(args: ContentArgs): PendingModalStep {
),
label: t`Proceed in your wallet`,
}
case ConfirmModalState.PENDING_CONFIRMATION:
case ConfirmModalState.PENDING_CONFIRMATION: {
let labelText: string | null = null
let href: string | null = null
if (chainId && swapConfirmed && swapResult && swapResult.type === TradeFillType.Classic) {
labelText = t`View on Explorer`
href = getExplorerLink(chainId, swapResult.response.hash, ExplorerDataType.TRANSACTION)
} else if (swapPending && trade?.fillType === TradeFillType.UniswapX) {
labelText = t`Learn more about swapping with UniswapX`
href = 'https://support.uniswap.org/hc/en-us/articles/17515415311501'
} else if (swapPending) {
labelText = t`Proceed in your wallet`
}
return {
title: swapPending ? t`Transaction submitted` : swapConfirmed ? t`Success` : t`Confirm Swap`,
title: swapPending ? t`Swap submitted` : swapConfirmed ? t`Success` : t`Confirm Swap`,
subtitle: trade ? <TradeSummary trade={trade} /> : null,
label:
swapConfirmed && swapTxHash && chainId ? (
<ExternalLink
href={getExplorerLink(chainId, swapTxHash, ExplorerDataType.TRANSACTION)}
color="textSecondary"
>
<Trans>View on Explorer</Trans>
</ExternalLink>
) : !swapPending ? (
t`Proceed in your wallet`
) : null,
label: href ? (
<ExternalLink href={href} color="textSecondary">
{labelText}
</ExternalLink>
) : (
labelText
),
}
}
}
}
......@@ -185,25 +215,41 @@ export function PendingModalContent({
steps,
currentStep,
trade,
swapTxHash,
swapResult,
wrapTxHash,
hideStepIndicators,
tokenApprovalPending = false,
revocationPending = false,
}: PendingModalContentProps) {
const { chainId } = useWeb3React()
const swapConfirmed = useIsTransactionConfirmed(swapTxHash)
const swapPending = swapTxHash !== undefined && !swapConfirmed
const classicSwapConfirmed = useIsTransactionConfirmed(
swapResult?.type === TradeFillType.Classic ? swapResult.response.hash : undefined
)
const wrapConfirmed = useIsTransactionConfirmed(wrapTxHash)
// TODO(UniswapX): Support UniswapX status here too
const uniswapXSwapConfirmed = Boolean(swapResult)
const swapConfirmed = TradeFillType.Classic ? classicSwapConfirmed : uniswapXSwapConfirmed
const swapPending = swapResult !== undefined && !swapConfirmed
const wrapPending = wrapTxHash != undefined && !wrapConfirmed
const { label, button } = getContent({
step: currentStep,
approvalCurrency: trade?.inputAmount.currency,
swapConfirmed,
swapPending,
wrapPending,
tokenApprovalPending,
revocationPending,
swapTxHash,
swapResult,
trade,
chainId,
})
const order = useOrder(swapResult?.type === TradeFillType.UniswapX ? swapResult.response.orderHash : '')
const currentStepContainerRef = useRef<HTMLDivElement>(null)
useUnmountingAnimation(currentStepContainerRef, () => AnimationType.EXITING)
......@@ -211,6 +257,11 @@ export function PendingModalContent({
return null
}
// Return finalized-order-specifc content if available
if (order && order.status !== UniswapXOrderStatus.OPEN) {
return <OrderContent order={{ status: order.status, orderHash: order.orderHash, details: order }} />
}
// On mainnet, we show the success icon once the tx is sent, since it takes longer to confirm than on L2s.
const showSuccess = swapConfirmed || (swapPending && chainId === ChainId.MAINNET)
......@@ -235,9 +286,13 @@ export function PendingModalContent({
{/* Scales in for the final step if the swap is pending user signature or onchain confirmation. */}
{((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) ||
tokenApprovalPending ||
wrapPending ||
revocationPending) && <LoadingIndicatorOverlay />}
</LogoContainer>
<HeaderContainer gap="md" $disabled={revocationPending || tokenApprovalPending || (swapPending && !showSuccess)}>
<HeaderContainer
gap="md"
$disabled={revocationPending || tokenApprovalPending || wrapPending || (swapPending && !showSuccess)}
>
<AnimationWrapper>
{steps.map((step) => {
const { title, subtitle } = getContent({
......@@ -245,9 +300,10 @@ export function PendingModalContent({
approvalCurrency: trade?.inputAmount.currency,
swapConfirmed,
swapPending,
tokenApprovalPending,
wrapPending,
revocationPending,
swapTxHash,
tokenApprovalPending,
swapResult,
trade,
})
// We only render one step at a time, but looping through the array allows us to keep
......
import { RouterPreference } from 'state/routing/slice'
import { useRouterPreference } from 'state/user/hooks'
import { ThemedText } from 'theme'
export default function RouterLabel() {
const [routerPreference] = useRouterPreference()
switch (routerPreference) {
case RouterPreference.AUTO:
case RouterPreference.API:
return <ThemedText.BodySmall>Uniswap API</ThemedText.BodySmall>
case RouterPreference.CLIENT:
return <ThemedText.BodySmall>Uniswap Client</ThemedText.BodySmall>
}
}
......@@ -26,7 +26,7 @@ describe.skip('SwapDetailsDropdown.tsx', () => {
})
it('is interactive once loaded', async () => {
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = '1.00'
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = 1.0
render(
<SwapDetailsDropdown
trade={TEST_TRADE_EXACT_INPUT}
......
......@@ -2,13 +2,11 @@ import { Trans } from '@lingui/macro'
import { TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import Column from 'components/Column'
import { LoadingOpacityContainer } from 'components/Loader/styled'
import { RowBetween, RowFixed } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { formatEventPropertiesForTrade } from 'lib/utils/analytics'
import { formatCommonPropertiesForTrade } from 'lib/utils/analytics'
import { useState } from 'react'
import { ChevronDown } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
......@@ -101,7 +99,6 @@ interface SwapDetailsInlineProps {
export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSlippage }: SwapDetailsInlineProps) {
const theme = useTheme()
const { chainId } = useWeb3React()
const [showDetails, setShowDetails] = useState(false)
const trace = useTrace()
......@@ -112,7 +109,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
name={SwapEventName.SWAP_DETAILS_EXPANDED}
element={InterfaceElementName.SWAP_DETAILS_DROPDOWN}
properties={{
...(trade ? formatEventPropertiesForTrade(trade, allowedSlippage) : {}),
...(trade ? formatCommonPropertiesForTrade(trade, allowedSlippage) : {}),
...trace,
}}
shouldLogImpression={!showDetails}
......@@ -142,12 +139,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
) : null}
</RowFixed>
<RowFixed>
{!trade?.gasUseEstimateUSD ||
showDetails ||
!chainId ||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<GasEstimateTooltip trade={trade} loading={syncing || loading} disabled={showDetails} />
)}
{!showDetails && <GasEstimateTooltip trade={trade} loading={syncing || loading} />}
<RotatingArrow
stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
open={Boolean(trade && showDetails)}
......
import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
......@@ -18,7 +19,15 @@ const HeaderButtonContainer = styled(RowFixed)`
gap: 16px;
`
export default function SwapHeader({ autoSlippage, chainId }: { autoSlippage: Percent; chainId?: number }) {
export default function SwapHeader({
autoSlippage,
chainId,
trade,
}: {
autoSlippage: Percent
chainId?: number
trade?: InterfaceTrade
}) {
const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
return (
......@@ -30,7 +39,7 @@ export default function SwapHeader({ autoSlippage, chainId }: { autoSlippage: Pe
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
</HeaderButtonContainer>
<RowFixed>
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} />
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} trade={trade} />
</RowFixed>
</StyledSwapHeader>
)
......
......@@ -9,7 +9,7 @@ describe('SwapModalFooter.tsx', () => {
<SwapModalFooter
trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
......@@ -45,7 +45,7 @@ describe('SwapModalFooter.tsx', () => {
<SwapModalFooter
trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
......@@ -73,7 +73,7 @@ describe('SwapModalFooter.tsx', () => {
<SwapModalFooter
trade={TEST_TRADE_EXACT_OUTPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined}
swapResult={undefined}
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
......
import { Trans } from '@lingui/macro'
import { Plural, Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { formatPriceImpact } from '@uniswap/conedison/format'
import { formatNumber, formatPriceImpact, NumberType } from '@uniswap/conedison/format'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import { MouseoverTooltip } from 'components/Tooltip'
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { SwapResult } from 'hooks/useSwapCallback'
import useTransactionDeadline from 'hooks/useTransactionDeadline'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import { RouterPreference } from 'state/routing/slice'
import { InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
......@@ -22,6 +24,7 @@ import { getPriceImpactWarning } from 'utils/prices'
import { ButtonError, SmallButtonPrimary } from '../Button'
import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import { SwapCallbackError, SwapShowAcceptChanges } from './styleds'
import { Label } from './SwapModalHeaderAmount'
......@@ -47,7 +50,7 @@ const DetailRowValue = styled(ThemedText.BodySmall)`
export default function SwapModalFooter({
trade,
allowedSlippage,
hash,
swapResult,
onConfirm,
swapErrorMessage,
disabledConfirm,
......@@ -58,7 +61,7 @@ export default function SwapModalFooter({
onAcceptChanges,
}: {
trade: InterfaceTrade
hash?: string
swapResult?: SwapResult
allowedSlippage: Percent
onConfirm: () => void
swapErrorMessage?: ReactNode
......@@ -72,7 +75,7 @@ export default function SwapModalFooter({
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
const [routerPreference] = useRouterPreference()
const routes = getRoutingDiagramEntries(trade)
const routes = isClassicTrade(trade) ? getRoutingDiagramEntries(trade) : undefined
const theme = useTheme()
const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId)
......@@ -80,6 +83,7 @@ export default function SwapModalFooter({
const label = `${trade.executionPrice.baseCurrency?.symbol} `
const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}`
const formattedPrice = formatTransactionAmount(priceToPreciseFloat(trade.executionPrice))
const txCount = getTransactionCount(trade)
return (
<>
......@@ -102,24 +106,28 @@ export default function SwapModalFooter({
}
>
<Label cursor="help">
<Trans>Network fee</Trans>
<Plural value={txCount} one="Network fee" other="Network fees" />
</Label>
</MouseoverTooltip>
<DetailRowValue>{trade.gasUseEstimateUSD ? `~$${trade.gasUseEstimateUSD}` : '-'}</DetailRowValue>
</Row>
</ThemedText.BodySmall>
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<Label cursor="help">
<Trans>Price impact</Trans>
</Label>
<MouseoverTooltip placement="right" size={TooltipSize.Small} text={<GasBreakdownTooltip trade={trade} />}>
<DetailRowValue>{formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)}</DetailRowValue>
</MouseoverTooltip>
<DetailRowValue color={getPriceImpactWarning(trade.priceImpact)}>
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue>
</Row>
</ThemedText.BodySmall>
{isClassicTrade(trade) && (
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
<Label cursor="help">
<Trans>Price impact</Trans>
</Label>
</MouseoverTooltip>
<DetailRowValue color={getPriceImpactWarning(trade.priceImpact)}>
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue>
</Row>
</ThemedText.BodySmall>
)}
<ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip
......@@ -175,11 +183,11 @@ export default function SwapModalFooter({
name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED}
properties={formatSwapButtonClickEventProperties({
trade,
hash,
swapResult,
allowedSlippage,
transactionDeadlineSecondsSinceEpoch,
isAutoSlippage,
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API,
isAutoRouterApi: routerPreference === RouterPreference.API,
swapQuoteReceivedDate,
routes,
fiatValueInput: fiatValueInput.data,
......
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
import {
ETH_MAINNET,
TEST_ALLOWED_SLIPPAGE,
TEST_DUTCH_TRADE_ETH_INPUT,
TEST_TRADE_EXACT_INPUT,
TEST_TRADE_EXACT_OUTPUT,
} from 'test-utils/constants'
import { render, screen } from 'test-utils/render'
import SwapModalHeader from './SwapModalHeader'
......@@ -23,6 +29,26 @@ describe('SwapModalHeader.tsx', () => {
)
})
it('renders ETH input token for an ETH input UniswapX swap', () => {
const { asFragment } = render(
<SwapModalHeader
inputCurrency={ETH_MAINNET}
trade={TEST_DUTCH_TRADE_ETH_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument()
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount(TEST_DUTCH_TRADE_ETH_INPUT.inputAmount, NumberType.TokenTx)} ${ETH_MAINNET.symbol}`
)
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
`${formatCurrencyAmount(TEST_DUTCH_TRADE_ETH_INPUT.outputAmount, NumberType.TokenTx)} ${
TEST_DUTCH_TRADE_ETH_INPUT.outputAmount.currency.symbol ?? ''
}`
)
})
it('test trade exact output, no recipient', () => {
const { asFragment } = render(
<SwapModalHeader trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
......
import { Trans } from '@lingui/macro'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import Column, { AutoColumn } from 'components/Column'
import { useUSDPrice } from 'hooks/useUSDPrice'
import { InterfaceTrade } from 'state/routing/types'
......@@ -19,9 +19,11 @@ const HeaderContainer = styled(AutoColumn)`
export default function SwapModalHeader({
trade,
inputCurrency,
allowedSlippage,
}: {
trade: InterfaceTrade
inputCurrency?: Currency
allowedSlippage: Percent
}) {
const fiatValueInput = useUSDPrice(trade.inputAmount)
......@@ -34,12 +36,14 @@ export default function SwapModalHeader({
field={Field.INPUT}
label={<Trans>You pay</Trans>}
amount={trade.inputAmount}
currency={inputCurrency ?? trade.inputAmount.currency}
usdAmount={fiatValueInput.data}
/>
<SwapModalHeaderAmount
field={Field.OUTPUT}
label={<Trans>You receive</Trans>}
amount={trade.outputAmount}
currency={trade.outputAmount.currency}
usdAmount={fiatValueOutput.data}
tooltipText={
trade.tradeType === TradeType.EXACT_INPUT ? (
......
......@@ -37,11 +37,15 @@ interface AmountProps {
field: Field
tooltipText?: ReactNode
label: ReactNode
amount?: CurrencyAmount<Currency>
amount: CurrencyAmount<Currency>
usdAmount?: number
// The currency used here can be different than the currency denoted in the `amount` prop
// For UniswapX ETH input trades, the trade object will have WETH as the amount.currency, but
// the user's real input currency is ETH, so show ETH instead
currency: Currency
}
export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, field }: AmountProps) {
export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, field, currency }: AmountProps) {
let formattedAmount = formatCurrencyAmount(amount, NumberType.TokenTx)
if (formattedAmount.length > MAX_AMOUNT_STR_LENGTH) {
formattedAmount = formatCurrencyAmount(amount, NumberType.SwapTradeAmount)
......@@ -57,7 +61,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
</ThemedText.BodySecondary>
<Column gap="xs">
<ResponsiveHeadline data-testid={`${field}-amount`}>
{formattedAmount} {amount?.currency.symbol}
{formattedAmount} {currency?.symbol}
</ResponsiveHeadline>
{usdAmount && (
<ThemedText.BodySmall color="textTertiary">
......@@ -66,7 +70,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
)}
</Column>
</Column>
{amount?.currency && <CurrencyLogo currency={amount.currency} size="36px" />}
<CurrencyLogo currency={currency} size="36px" />
</Row>
)
}
......@@ -5,13 +5,13 @@ import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { InterfaceTrade } from 'state/routing/types'
import { ClassicTrade } from 'state/routing/types'
import { Separator, ThemedText } from 'theme'
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
import RouterLabel from './RouterLabel'
import RouterLabel from '../RouterLabel'
export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; syncing: boolean }) {
export default function SwapRoute({ trade, syncing }: { trade: ClassicTrade; syncing: boolean }) {
const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported()
......@@ -21,14 +21,14 @@ export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; s
// TODO(WEB-2022)
// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`?
trade.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
? trade.gasUseEstimateUSD === '0.00'
? trade.gasUseEstimateUSD === 0
? '<$0.01'
: '$' + trade.gasUseEstimateUSD
: '$' + trade.gasUseEstimateUSD.toFixed(2)
: undefined
return (
<Column gap="md">
<RouterLabel />
<RouterLabel trade={trade} />
<Separator />
{syncing ? (
<LoadingRows>
......
......@@ -93,9 +93,15 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
</div>
</div>
<div
class="c7 css-zhpkf8"
class="c5"
>
~$1.00
<div>
<div
class="c7 css-zhpkf8"
>
~$1.00
</div>
</div>
</div>
</div>
<div
......
......@@ -25,6 +25,24 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start;
}
.c11 {
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: 4px;
}
.c4 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
......@@ -38,15 +56,22 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
width: fit-content;
}
.c12 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
margin: -xs;
}
.c9 {
color: #0D111C;
}
.c12 {
.c14 {
color: #7780A0;
}
.c16 {
.c18 {
width: 100%;
height: 1px;
background-color: #D2D9EE;
......@@ -66,7 +91,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start;
}
.c15 {
.c17 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
......@@ -94,12 +119,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
height: inherit;
}
.c11 {
margin-right: 4px;
.c13 {
height: 18px;
}
.c11 > * {
.c13 > * {
stroke: #98A1C0;
}
......@@ -144,7 +168,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
cursor: pointer;
}
.c13 {
.c15 {
-webkit-transform: none;
-ms-transform: none;
transform: none;
......@@ -153,7 +177,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: transform 0.1s linear;
}
.c14 {
.c16 {
padding-top: 12px;
}
......@@ -201,15 +225,15 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c7"
>
<div
class="c2 c3 c6"
class="c2 c11 c12"
>
<svg
class="c11"
class="c13"
>
gas-icon.svg
</svg>
<div
class="c12 css-zhpkf8"
class="c14 css-zhpkf8"
>
$1.00
</div>
......@@ -218,7 +242,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div>
</div>
<svg
class="c13"
class="c15"
fill="none"
height="24"
stroke="#98A1C0"
......@@ -240,14 +264,14 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
>
<div>
<div
class="c14"
class="c16"
data-testid="advanced-swap-details"
>
<div
class="c15"
class="c17"
>
<div
class="c16"
class="c18"
/>
<div
class="c2 c3 c4"
......@@ -257,7 +281,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
>
<div>
<div
class="c12 css-zhpkf8"
class="c14 css-zhpkf8"
>
Network fee
</div>
......@@ -300,7 +324,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
>
<div>
<div
class="c12 css-zhpkf8"
class="c14 css-zhpkf8"
>
Minimum output
</div>
......@@ -324,7 +348,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
>
<div>
<div
class="c12 css-zhpkf8"
class="c14 css-zhpkf8"
>
Expected output
</div>
......@@ -338,13 +362,13 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div>
</div>
<div
class="c16"
class="c18"
/>
<div
class="c2 c3 c4"
>
<div
class="c12 css-zhpkf8"
class="c14 css-zhpkf8"
>
Order routing
</div>
......
......@@ -110,9 +110,15 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
</div>
</div>
<div
class="c2 c6 css-zhpkf8"
class="c7"
>
~$1.00
<div>
<div
class="c2 c6 css-zhpkf8"
>
$1.00
</div>
</div>
</div>
</div>
</div>
......
......@@ -218,6 +218,224 @@ exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] =
</DocumentFragment>
`;
exports[`SwapModalHeader.tsx renders ETH input token for an ETH input UniswapX swap 1`] = `
<DocumentFragment>
.c3 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c4 {
width: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 0;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
gap: 12px;
}
.c6 {
color: #7780A0;
}
.c8 {
color: #0D111C;
}
.c12 {
width: 100%;
height: 1px;
border-width: 0;
margin: 0;
background-color: #D2D9EE;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 24px;
}
.c5 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
gap: 4px;
}
.c0 {
display: grid;
grid-auto-rows: auto;
grid-row-gap: 8px;
}
.c11 {
--size: 36px;
border-radius: 100px;
color: #0D111C;
background-color: #E8ECFB;
font-size: calc(var(--size) / 3);
font-weight: 500;
height: 36px;
line-height: 36px;
text-align: center;
width: 36px;
}
.c10 {
position: relative;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
.c7 {
display: inline-block;
height: inherit;
}
.c9 {
cursor: help;
color: #7780A0;
margin-right: 8px;
}
.c13 {
margin: 16px 2px 24px 2px;
}
.c1 {
margin-top: 16px;
}
<div
class="c0 c1"
>
<div
class="c2"
>
<div
class="c3 c4"
>
<div
class="c5"
>
<div
class="c6 css-1jljtub"
>
<div
class="c7"
>
<div>
<div
class="c8 c9 css-zhpkf8"
cursor="help"
>
You pay
</div>
</div>
</div>
</div>
<div
class="c5"
>
<div
class="c8 css-z2fexy"
data-testid="INPUT-amount"
>
&lt;0.00001 ETH
</div>
</div>
</div>
<div
class="c10"
>
<div
class="c11"
>
ETH
</div>
</div>
</div>
<div
class="c3 c4"
>
<div
class="c5"
>
<div
class="c6 css-1jljtub"
>
<div
class="c7"
>
<div>
<div
class="c8 c9 css-zhpkf8"
cursor="help"
>
You receive
</div>
</div>
</div>
</div>
<div
class="c5"
>
<div
class="c8 css-z2fexy"
data-testid="OUTPUT-amount"
>
&lt;0.00001 DEF
</div>
</div>
</div>
<div
class="c10"
>
<div
class="c11"
>
DEF
</div>
</div>
</div>
</div>
<div
class="c12 c13"
/>
</div>
</DocumentFragment>
`;
exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
<DocumentFragment>
.c3 {
......
......@@ -5,6 +5,7 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { AutoColumn } from '../Column'
export const PageWrapper = styled.div`
......@@ -38,6 +39,109 @@ export const SwapWrapper = styled.main<{ chainId?: number }>`
}
`
export const UniswapPopoverContainer = styled.div`
padding: 18px;
color: ${({ theme }) => theme.textPrimary};
font-weight: 400;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
border: 1px solid ${({ theme }) => theme.backgroundInteractive};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
position: relative;
overflow: hidden;
`
const springDownKeyframes = `@keyframes spring-down {
0% { transform: translateY(-80px); }
25% { transform: translateY(4px); }
50% { transform: translateY(-1px); }
75% { transform: translateY(0px); }
100% { transform: translateY(0px); }
}`
const backUpKeyframes = `@keyframes back-up {
0% { transform: translateY(0px); }
100% { transform: translateY(-80px); }
}`
export const UniswapXShine = (props: any) => {
const isDarkMode = useIsDarkMode()
return <UniswapXShineInner {...props} style={{ opacity: isDarkMode ? 0.15 : 0.05, ...props.style }} />
}
const UniswapXShineInner = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
background: linear-gradient(130deg, transparent 20%, ${({ theme }) => theme.accentAction}, transparent 80%);
opacity: 0.15;
`
// overflow hidden to hide the SwapMustacheShadow
export const SwapOptInSmallContainer = styled.div<{ visible: boolean; shouldAnimate: boolean }>`
overflow: hidden;
margin-top: -14px;
transform: translateY(${({ visible }) => (visible ? 0 : -80)}px);
transition: all ease 400ms;
animation: ${({ visible, shouldAnimate }) =>
!shouldAnimate ? '' : visible ? `spring-down 900ms ease forwards` : 'back-up 200ms ease forwards'};
${springDownKeyframes}
${backUpKeyframes}
`
export const UniswapXOptInLargeContainerPositioner = styled.div`
position: absolute;
top: 211px;
right: ${-320 - 15}px;
width: 320px;
align-items: center;
min-height: 170px;
display: flex;
pointer-events: none;
`
export const UniswapXOptInLargeContainer = styled.div<{ visible: boolean }>`
opacity: ${({ visible }) => (visible ? 1 : 0)};
transform: ${({ visible }) => `translateY(${visible ? 0 : -6}px)`};
transition: all ease-in 300ms;
transition-delay: ${({ visible }) => (visible ? '350ms' : '0')};
pointer-events: ${({ visible }) => (visible ? 'auto' : 'none')};
`
export const SwapMustache = styled.main`
position: relative;
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 16px;
border-top-left-radius: 0;
border-top-right-radius: 0;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-top-width: 0;
padding: 18px;
padding-top: calc(12px + 18px);
z-index: 0;
transition: transform 250ms ease;
`
export const SwapMustacheShadow = styled.main`
position: absolute;
top: 0;
left: 0;
border-radius: 16px;
height: 100%;
width: 100%;
transform: translateY(-100%);
box-shadow: 0 0 20px 20px ${({ theme }) => theme.backgroundBackdrop};
background: red;
`
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
border-radius: 12px;
height: 40px;
......
......@@ -90,3 +90,7 @@ export type SupportedL2ChainId = (typeof L2_CHAIN_IDS)[number]
export function isPolygonChain(chainId: number): chainId is ChainId.POLYGON | ChainId.POLYGON_MUMBAI {
return chainId === ChainId.POLYGON || chainId === ChainId.POLYGON_MUMBAI
}
export function isUniswapXSupportedChain(chainId: number) {
return chainId === ChainId.MAINNET
}
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUniswapXSyntheticQuoteFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.uniswapXSyntheticQuote)
}
export function useUniswapXSyntheticQuoteEnabled(): boolean {
return useUniswapXSyntheticQuoteFlag() === BaseVariant.Enabled
}
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUniswapXFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.uniswapXEnabled)
}
export function useUniswapXEnabled(): boolean {
return useUniswapXFlag() === BaseVariant.Enabled
}
export { BaseVariant as UniswapXVariant }
......@@ -11,7 +11,8 @@ export enum FeatureFlag {
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
detailsV2 = 'details_v2',
debounceSwapQuote = 'debounce_swap_quote',
nativeUsdcArbitrum = 'web_usdc_arbitrum',
uniswapXEnabled = 'uniswapx_enabled',
uniswapXSyntheticQuote = 'uniswapx_synthetic_quote',
routingAPIPrice = 'routing_api_price',
}
......
......@@ -102,13 +102,44 @@ fragment TransactionParts on Transaction {
nonce
}
fragment TransactionDetailsParts on TransactionDetails {
id
type
from
to
hash
nonce
status
}
fragment SwapOrderDetailsParts on SwapOrderDetails {
id
offerer
hash
orderStatus: status
inputToken {
...TokenAssetParts
}
inputTokenQuantity
outputToken {
...TokenAssetParts
}
outputTokenQuantity
}
fragment AssetActivityParts on AssetActivity {
id
timestamp
type
chain
transaction {
...TransactionParts
details {
__typename
... on TransactionDetails {
...TransactionDetailsParts
}
... on SwapOrderDetails {
...SwapOrderDetailsParts
}
}
assetChanges {
__typename
......@@ -130,10 +161,11 @@ fragment AssetActivityParts on AssetActivity {
}
}
query TransactionList($account: String!) {
# TODO(UniswapX): return to a pagesize of 50 pre-launch
query Activity($account: String!) {
portfolios(ownerAddresses: [$account]) {
id
assetActivities(pageSize: 50, page: 1) {
assetActivities(pageSize: 100, page: 1, includeOffChain: true) {
...AssetActivityParts
}
}
......
......@@ -8,7 +8,7 @@ import { L2_CHAIN_IDS } from 'constants/chains'
import JSBI from 'jsbi'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { ClassicTrade } from 'state/routing/types'
import useGasPrice from './useGasPrice'
import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice'
......@@ -71,8 +71,9 @@ const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 100) // 5%
/**
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
* Auto slippage is only relevant for Classic swaps because UniswapX slippage is determined by the backend service
*/
export default function useAutoSlippageTolerance(trade?: InterfaceTrade): Percent {
export default function useClassicAutoSlippageTolerance(trade?: ClassicTrade): Percent {
const { chainId } = useWeb3React()
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
const outputDollarValue = useStablecoinValue(trade?.outputAmount)
......
......@@ -54,7 +54,8 @@ describe('#useBestV3Trade ExactIn', () => {
USDCAmount,
DAI,
RouterPreference.CLIENT,
true // skipFetch
true, // skipFetch
undefined
)
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
......@@ -72,7 +73,8 @@ describe('#useBestV3Trade ExactIn', () => {
USDCAmount,
DAI,
RouterPreference.CLIENT,
true // skipFetch
true, // skipFetch
undefined
)
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
})
......@@ -132,7 +134,8 @@ describe('#useBestV3Trade ExactOut', () => {
DAIAmount,
USDC_MAINNET,
RouterPreference.CLIENT,
true // skipFetch
true, // skipFetch
undefined
)
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
......@@ -150,7 +153,8 @@ describe('#useBestV3Trade ExactOut', () => {
DAIAmount,
USDC_MAINNET,
RouterPreference.CLIENT,
true // skipFetch
true, // skipFetch
undefined
)
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
})
......
......@@ -3,7 +3,8 @@ import { useWeb3React } from '@web3-react/core'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { DebounceSwapQuoteVariant, useDebounceSwapQuoteFlag } from 'featureFlags/flags/debounceSwapQuote'
import { useMemo } from 'react'
import { InterfaceTrade, QuoteMethod, TradeState } from 'state/routing/types'
import { RouterPreference } from 'state/routing/slice'
import { ClassicTrade, InterfaceTrade, QuoteMethod, TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { useRouterPreference } from 'state/user/hooks'
......@@ -18,6 +19,27 @@ const DEBOUNCE_TIME = 350
// Temporary until we remove the feature flag.
const DEBOUNCE_TIME_INCREASED = 650
export function useBestTrade(
tradeType: TradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency,
routerPreferenceOverride?: RouterPreference.X,
account?: string
): {
state: TradeState
trade?: InterfaceTrade
}
export function useBestTrade(
tradeType: TradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency,
routerPreferenceOverride?: RouterPreference.API | RouterPreference.CLIENT,
account?: string
): {
state: TradeState
trade?: ClassicTrade
}
/**
* Returns the best v2+v3 trade for a desired swap.
* @param tradeType whether the swap is an exact in/out
......@@ -27,7 +49,9 @@ const DEBOUNCE_TIME_INCREASED = 650
export function useBestTrade(
tradeType: TradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
otherCurrency?: Currency,
routerPreferenceOverride?: RouterPreference,
account?: string
): {
state: TradeState
trade?: InterfaceTrade
......@@ -59,8 +83,9 @@ export function useBestTrade(
tradeType,
amountSpecified ? debouncedAmount : undefined,
debouncedOtherCurrency,
routerPreference,
!(autoRouterSupported && shouldGetTrade) // skip fetching
routerPreferenceOverride ?? routerPreference,
!(autoRouterSupported && shouldGetTrade), // skip fetching
account
)
const inDebounce =
......
......@@ -4,7 +4,7 @@ import { useWeb3React } from '@web3-react/core'
import JSBI from 'jsbi'
import { useSingleContractWithCallData } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { ClassicTrade, InterfaceTrade, TradeState } from 'state/routing/types'
import { ClassicTrade, InterfaceTrade, QuoteMethod, TradeState } from 'state/routing/types'
import { isCelo } from '../constants/tokens'
import { useAllV3Routes } from './useAllV3Routes'
......@@ -23,6 +23,7 @@ const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
const DEFAULT_GAS_QUOTE = 2_000_000
// TODO (UniswapX or in general): Deprecate this?
/**
* Returns the best v3 trade for a desired swap
* @param tradeType whether the swap is an exact in/out
......@@ -145,6 +146,9 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
},
],
tradeType,
quoteMethod: QuoteMethod.CLIENT_SIDE,
// When using SOR, we don't have gas values from routing-api, so we can't calculate approve gas info
approveInfo: { needsApprove: false },
}),
}
}, [amountSpecified, currenciesAreTheSame, currencyIn, currencyOut, quotesResults, routes, routesLoading, tradeType])
......
......@@ -6,6 +6,7 @@ import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'h
import { useRevokeTokenAllowance, useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { TradeFillType } from 'state/routing/types'
import { useHasPendingApproval, useHasPendingRevocation, useTransactionAdder } from 'state/transactions/hooks'
enum ApprovalState {
......@@ -43,7 +44,11 @@ export type Allowance =
}
| AllowanceRequired
export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spender?: string): Allowance {
export default function usePermit2Allowance(
amount?: CurrencyAmount<Token>,
spender?: string,
tradeFillType?: TradeFillType
): Allowance {
const { account } = useWeb3React()
const token = amount?.currency
......@@ -100,7 +105,10 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
}, [amount, now, permitAllowance, permitExpiration])
const shouldRequestApproval = !(isApproved || isApprovalLoading)
const shouldRequestSignature = !(isPermitted || isSigned)
// UniswapX trades do not need a permit signature step in between because the swap step _is_ the permit signature
const shouldRequestSignature = tradeFillType !== TradeFillType.UniswapX && !(isPermitted || isSigned)
const addTransaction = useTransactionAdder()
const approveAndPermit = useCallback(async () => {
if (shouldRequestApproval) {
......
......@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core'
import { getConnection } from 'connection'
import { didUserReject } from 'connection/utils'
import { useCallback } from 'react'
import { addPopup } from 'state/application/reducer'
import { addPopup, PopupType } from 'state/application/reducer'
import { useAppDispatch } from 'state/hooks'
import { useSwitchChain } from './useSwitchChain'
......@@ -24,7 +24,12 @@ export default function useSelectChain() {
} catch (error) {
if (!didUserReject(connection, error) && error.code !== -32002 /* request already pending */) {
console.error('Failed to switch networks', error)
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: 'failed-network-switch' }))
dispatch(
addPopup({
content: { failedSwitchNetwork: targetChain, type: PopupType.FailedSwitchNetwork },
key: 'failed-network-switch',
})
)
}
}
},
......
......@@ -85,7 +85,7 @@ export function useStablecoinValue(currencyAmount: CurrencyAmount<Currency> | un
* @param fiatValue string representation of a USD amount
* @returns CurrencyAmount where currency is stablecoin on active chain
*/
export function useStablecoinAmountFromFiatValue(fiatValue: string | null | undefined) {
export function useStablecoinAmountFromFiatValue(fiatValue: number | null | undefined) {
const { chainId } = useWeb3React()
const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined
......@@ -95,7 +95,7 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
const parsedForDecimals = fiatValue.toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
......
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { PermitSignature } from 'hooks/usePermitAllowance'
import { useMemo } from 'react'
import { useCallback } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { useAddOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import { useTransactionAdder } from '../state/transactions/hooks'
import { TransactionInfo, TransactionType } from '../state/transactions/types'
import {
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
TransactionType,
} from '../state/transactions/types'
import { currencyId } from '../utils/currencyId'
import useTransactionDeadline from './useTransactionDeadline'
import { useUniswapXSwapCallback } from './useUniswapXSwapCallback'
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
// returns a function that will execute a swap, if the parameters are all valid
export type SwapResult = Awaited<ReturnType<ReturnType<typeof useSwapCallback>>>
// 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: Trade<Currency, Currency, TradeType> | undefined, // trade to execute, required
trade: InterfaceTrade | undefined, // trade to execute, required
fiatValues: { amountIn?: number; amountOut?: number }, // usd values for amount in and out, logged for analytics
allowedSlippage: Percent, // in bips
permitSignature: PermitSignature | undefined
): { callback: null | (() => Promise<string>) } {
) {
const deadline = useTransactionDeadline()
const addTransaction = useTransactionAdder()
const addOrder = useAddOrder()
const { account, chainId } = useWeb3React()
const universalRouterSwapCallback = useUniversalRouterSwapCallback(trade, fiatValues, {
slippageTolerance: allowedSlippage,
deadline,
permit: permitSignature,
const uniswapXSwapCallback = useUniswapXSwapCallback({
trade: isUniswapXTrade(trade) ? trade : undefined,
allowedSlippage,
fiatValues,
})
const swapCallback = universalRouterSwapCallback
const callback = useMemo(() => {
if (!trade || !swapCallback) return null
const info: TransactionInfo = {
const universalRouterSwapCallback = useUniversalRouterSwapCallback(
isClassicTrade(trade) ? trade : undefined,
fiatValues,
{
slippageTolerance: allowedSlippage,
deadline,
permit: permitSignature,
}
)
const swapCallback = isUniswapXTrade(trade) ? uniswapXSwapCallback : universalRouterSwapCallback
return useCallback(async () => {
if (!trade) throw new Error('missing trade')
if (!account || !chainId) throw new Error('wallet must be connected to swap')
const result = await swapCallback()
const swapInfo: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo = {
type: TransactionType.SWAP,
inputCurrencyId: currencyId(trade.inputAmount.currency),
outputCurrencyId: currencyId(trade.outputAmount.currency),
isUniswapXOrder: result.type === TradeFillType.UniswapX,
...(trade.tradeType === TradeType.EXACT_INPUT
? {
tradeType: TradeType.EXACT_INPUT,
......@@ -48,14 +77,19 @@ export function useSwapCallback(
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}),
}
return () =>
swapCallback().then((response) => {
addTransaction(response, info, deadline?.toNumber())
return response.hash
})
}, [addTransaction, allowedSlippage, deadline, swapCallback, trade])
return {
callback,
}
if (result.type === TradeFillType.UniswapX) {
addOrder(
account,
result.response.orderHash,
chainId,
result.response.deadline,
swapInfo as UniswapXOrderDetails['swapInfo']
)
} else {
addTransaction(result.response, swapInfo, deadline?.toNumber())
}
return result
}, [account, addOrder, addTransaction, allowedSlippage, chainId, deadline, swapCallback, trade])
}
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { SwapEventName } from '@uniswap/analytics-events'
import { signTypedData } from '@uniswap/conedison/provider/signing'
import { Percent } from '@uniswap/sdk-core'
import { DutchOrder, DutchOrderBuilder } from '@uniswap/uniswapx-sdk'
import { useWeb3React } from '@web3-react/core'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react'
import { DutchOrderTrade, TradeFillType } from 'state/routing/types'
import { trace } from 'tracing/trace'
import { UserRejectedRequestError } from 'utils/errors'
import { didUserReject, swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage'
const DEFAULT_START_TIME_PADDING_SECONDS = 30
type DutchAuctionOrderError = { errorCode?: number; detail?: string }
type DutchAuctionOrderSuccess = { hash: string }
type DutchAuctionOrderResponse = DutchAuctionOrderError | DutchAuctionOrderSuccess
const isErrorResponse = (res: Response, order: DutchAuctionOrderResponse): order is DutchAuctionOrderError =>
res.status < 200 || res.status > 202
const UNISWAP_API_URL = process.env.REACT_APP_UNISWAP_API_URL
if (UNISWAP_API_URL === undefined) {
throw new Error(`UNISWAP_API_URL must be a defined environment variable`)
}
export function useUniswapXSwapCallback({
trade,
allowedSlippage,
fiatValues,
}: {
trade?: DutchOrderTrade
fiatValues: { amountIn?: number; amountOut?: number }
allowedSlippage: Percent
}) {
const { account, provider } = useWeb3React()
const analyticsContext = useTrace()
return useCallback(
async () =>
trace('swapx.send', async ({ setTraceData, setTraceStatus }) => {
if (!account) throw new Error('missing account')
if (!provider) throw new Error('missing provider')
if (!trade) throw new Error('missing trade')
const signDutchOrder = async (): Promise<{ signature: string; updatedOrder: DutchOrder }> => {
try {
const startTime = Math.floor(Date.now() / 1000) + DEFAULT_START_TIME_PADDING_SECONDS
setTraceData('startTime', startTime)
const endTime = startTime + trade.auctionPeriodSecs
setTraceData('endTime', endTime)
const deadline = endTime + trade.deadlineBufferSecs
setTraceData('deadline', deadline)
// Set timestamp and account based values when the user clicks 'swap' to make them as recent as possible
const updatedOrder = DutchOrderBuilder.fromOrder(trade.order)
.decayStartTime(startTime)
.decayEndTime(endTime)
.deadline(deadline)
.swapper(account)
.nonFeeRecipient(account)
.build()
const { domain, types, values } = updatedOrder.permitData()
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
if (deadline < Math.floor(Date.now() / 1000)) {
return signDutchOrder()
}
return { signature, updatedOrder }
} catch (swapError) {
if (didUserReject(swapError)) {
setTraceStatus('cancelled')
throw new UserRejectedRequestError(swapErrorToUserReadableMessage(swapError))
}
throw new Error(swapErrorToUserReadableMessage(swapError))
}
}
const { signature, updatedOrder } = await signDutchOrder()
sendAnalyticsEvent(SwapEventName.SWAP_SIGNED, {
...formatSwapSignedAnalyticsEventProperties({
trade,
allowedSlippage,
fiatValues,
}),
...analyticsContext,
})
const res = await fetch(`${UNISWAP_API_URL}/order`, {
method: 'POST',
body: JSON.stringify({
encodedOrder: updatedOrder.serialize(),
signature,
chainId: updatedOrder.chainId,
quoteId: trade.quoteId,
}),
})
const body = (await res.json()) as DutchAuctionOrderResponse
// TODO(UniswapX): For now, `errorCode` is not always present in the response, so we have to fallback
// check for status code and perform this type narrowing.
if (isErrorResponse(res, body)) {
// TODO(UniswapX): Provide a similar utility to `swapErrorToUserReadableMessage` once
// backend team provides a list of error codes and potential messages
throw new Error(`${body.errorCode ?? body.detail ?? 'Unknown error'}`)
}
return {
type: TradeFillType.UniswapX as const,
response: { orderHash: body.hash, deadline: updatedOrder.info.deadline },
}
}),
[account, provider, trade, allowedSlippage, fiatValues, analyticsContext]
)
}
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { SwapEventName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Percent } from '@uniswap/sdk-core'
import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { FeeOptions, toHex } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react'
import { ClassicTrade, TradeFillType } from 'state/routing/types'
import { trace } from 'tracing/trace'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import { UserRejectedRequestError } from 'utils/errors'
......@@ -45,14 +44,14 @@ interface SwapOptions {
}
export function useUniversalRouterSwapCallback(
trade: Trade<Currency, Currency, TradeType> | undefined,
trade: ClassicTrade | undefined,
fiatValues: { amountIn?: number; amountOut?: number },
options: SwapOptions
) {
const { account, chainId, provider } = useWeb3React()
const analyticsContext = useTrace()
return useCallback(async (): Promise<TransactionResponse> => {
return useCallback(async () => {
return trace('swap.send', async ({ setTraceData, setTraceStatus, setTraceError }) => {
try {
if (!account) throw new Error('missing account')
......@@ -108,7 +107,10 @@ export function useUniversalRouterSwapCallback(
}
return response
})
return response
return {
type: TradeFillType.Classic as const,
response,
}
} catch (swapError: unknown) {
if (swapError instanceof ModifiedSwapError) throw swapError
......
......@@ -60,7 +60,7 @@ export default function useWrapCallback(
inputCurrency: Currency | undefined | null,
outputCurrency: Currency | undefined | null,
typedValue: string | undefined
): { wrapType: WrapType; execute?: () => Promise<void>; inputError?: WrapInputError } {
): { wrapType: WrapType; execute?: () => Promise<string | undefined>; inputError?: WrapInputError } {
const { chainId, account } = useWeb3React()
const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency ?? undefined)
......@@ -99,37 +99,34 @@ export default function useWrapCallback(
execute:
sufficientBalance && inputAmount
? async () => {
try {
const network = await wethContract.provider.getNetwork()
if (
network.chainId !== chainId ||
wethContract.address !== WRAPPED_NATIVE_CURRENCY[network.chainId]?.address
) {
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_INVALIDATED, {
...eventProperties,
contract_address: wethContract.address,
contract_chain_id: network.chainId,
type: WrapType.WRAP,
})
const error = new Error(`Invalid WETH contract
Please file a bug detailing how this happened - https://github.com/Uniswap/interface/issues/new?labels=bug&template=bug-report.md&title=Invalid%20WETH%20contract`)
setError(error)
throw error
}
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.quotient.toString(16)}` })
addTransaction(txReceipt, {
type: TransactionType.WRAP,
unwrapped: false,
currencyAmountRaw: inputAmount?.quotient.toString(),
chainId,
})
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
const network = await wethContract.provider.getNetwork()
if (
network.chainId !== chainId ||
wethContract.address !== WRAPPED_NATIVE_CURRENCY[network.chainId]?.address
) {
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_INVALIDATED, {
...eventProperties,
contract_address: wethContract.address,
contract_chain_id: network.chainId,
type: WrapType.WRAP,
})
} catch (error) {
console.error('Could not deposit', error)
const error = new Error(`Invalid WETH contract
Please file a bug detailing how this happened - https://github.com/Uniswap/interface/issues/new?labels=bug&template=bug-report.md&title=Invalid%20WETH%20contract`)
setError(error)
throw error
}
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.quotient.toString(16)}` })
addTransaction(txReceipt, {
type: TransactionType.WRAP,
unwrapped: false,
currencyAmountRaw: inputAmount?.quotient.toString(),
chainId,
})
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
...eventProperties,
type: WrapType.WRAP,
})
return txReceipt.hash
}
: undefined,
inputError: sufficientBalance
......@@ -144,21 +141,18 @@ Please file a bug detailing how this happened - https://github.com/Uniswap/inter
execute:
sufficientBalance && inputAmount
? async () => {
try {
const txReceipt = await wethContract.withdraw(`0x${inputAmount.quotient.toString(16)}`)
addTransaction(txReceipt, {
type: TransactionType.WRAP,
unwrapped: true,
currencyAmountRaw: inputAmount?.quotient.toString(),
chainId,
})
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
...eventProperties,
type: WrapType.UNWRAP,
})
} catch (error) {
console.error('Could not withdraw', error)
}
const txReceipt = await wethContract.withdraw(`0x${inputAmount.quotient.toString(16)}`)
addTransaction(txReceipt, {
type: TransactionType.WRAP,
unwrapped: true,
currencyAmountRaw: inputAmount?.quotient.toString(),
chainId,
})
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
...eventProperties,
type: WrapType.UNWRAP,
})
return txReceipt.hash
}
: undefined,
inputError: sufficientBalance
......
......@@ -23,6 +23,7 @@ import store from './state'
import ApplicationUpdater from './state/application/updater'
import ListsUpdater from './state/lists/updater'
import LogsUpdater from './state/logs/updater'
import OrderUpdater from './state/signatures/updater'
import TransactionUpdater from './state/transactions/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme'
import RadialGradientByChainUpdater from './theme/components/RadialGradientByChainUpdater'
......@@ -39,6 +40,7 @@ function Updaters() {
<SystemThemeUpdater />
<ApplicationUpdater />
<TransactionUpdater />
<OrderUpdater />
<MulticallUpdater />
<LogsUpdater />
</>
......
export enum UniswapXOrderStatus {
FILLED = 'filled',
OPEN = 'open',
EXPIRED = 'expired',
ERROR = 'error',
CANCELLED = 'cancelled',
INSUFFICIENT_FUNDS = 'insufficient-funds',
}
interface BaseUniswapXBackendOrder {
orderStatus: UniswapXOrderStatus
orderHash: string
offerer: string
createdAt: number
chainId: number
input: {
endAmount: string
token: string
startAmount: string
}
outputs: [
{
recipient: string
startAmount: string
endAmount: string
token: string
}
]
}
interface NonfilledUniswapXBackendOrder extends BaseUniswapXBackendOrder {
orderStatus:
| UniswapXOrderStatus.OPEN
| UniswapXOrderStatus.EXPIRED
| UniswapXOrderStatus.ERROR
| UniswapXOrderStatus.CANCELLED
| UniswapXOrderStatus.INSUFFICIENT_FUNDS
}
interface FilledUniswapXBackendOrder extends BaseUniswapXBackendOrder {
orderStatus: UniswapXOrderStatus.FILLED
txHash: string
settledAmounts: [
{
tokenOut: string
amountOut: string
}
]
}
export type UniswapXBackendOrder = FilledUniswapXBackendOrder | NonfilledUniswapXBackendOrder
export type OrderQueryResponse = {
orders: UniswapXBackendOrder[]
}
import { useWeb3React } from '@web3-react/core'
import ms from 'ms.macro'
import { useEffect } from 'react'
import { isFinalizedOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import { OrderQueryResponse, UniswapXBackendOrder } from './types'
const UNISWAP_API_URL = process.env.REACT_APP_UNISWAP_API_URL
if (UNISWAP_API_URL === undefined) {
throw new Error(`UNISWAP_API_URL must be a defined environment variable`)
}
function fetchOrderStatuses(account: string, orders: UniswapXOrderDetails[]) {
const orderHashes = orders.map((order) => order.orderHash).join(',')
return global.fetch(`${UNISWAP_API_URL}/orders?swapper=${account}&orderHashes=${orderHashes}`)
}
const OFF_CHAIN_ORDER_STATUS_POLLING = ms`2s` // every 2 seconds
interface UpdaterProps {
pendingOrders: UniswapXOrderDetails[]
onOrderUpdate: (order: UniswapXOrderDetails, backendUpdate: UniswapXBackendOrder) => void
}
export default function OrderUpdater({ pendingOrders, onOrderUpdate }: UpdaterProps): null {
const { account } = useWeb3React()
useEffect(() => {
async function getOrderStatuses() {
if (!account || pendingOrders.length === 0) return
// Stop polling if all orders in our queue have "finalized" states
if (pendingOrders.every((order) => isFinalizedOrder(order.status))) {
clearInterval(interval)
return
}
try {
const pollOrderStatus = await fetchOrderStatuses(account, pendingOrders)
const orderStatuses: OrderQueryResponse = await pollOrderStatus.json()
pendingOrders.forEach((pendingOrder) => {
const updatedOrder = orderStatuses.orders.find((order) => order.orderHash === pendingOrder.orderHash)
if (updatedOrder) {
onOrderUpdate(pendingOrder, updatedOrder)
}
})
} catch (e) {
console.error('Error fetching order statuses', e)
}
}
const interval = setInterval(getOrderStatuses, OFF_CHAIN_ORDER_STATUS_POLLING)
return () => clearInterval(interval)
}, [account, onOrderUpdate, pendingOrders])
return null
}
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useRoutingAPIForPrice } from 'featureFlags/flags/priceRoutingApi'
import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useMemo } from 'react'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
import { currencyAddressForSwapQuote } from 'state/routing/utils'
......@@ -10,27 +12,33 @@ import { currencyAddressForSwapQuote } from 'state/routing/utils'
* be destructured.
*/
export function useRoutingAPIArguments({
account,
tokenIn,
tokenOut,
amount,
tradeType,
routerPreference,
}: {
account?: string
tokenIn?: Currency
tokenOut?: Currency
amount?: CurrencyAmount<Currency>
tradeType: TradeType
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}): GetQuoteArgs | undefined {
const uniswapXEnabled = useUniswapXEnabled()
const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled()
const isRoutingAPIPrice = useRoutingAPIForPrice()
return useMemo(
() =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped)
? undefined
: {
account,
amount: amount.quotient.toString(),
tokenInAddress: currencyAddressForSwapQuote(tokenIn),
tokenInChainId: tokenIn.wrapped.chainId,
tokenInChainId: tokenIn.chainId,
tokenInDecimals: tokenIn.wrapped.decimals,
tokenInSymbol: tokenIn.wrapped.symbol,
tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
......@@ -40,7 +48,20 @@ export function useRoutingAPIArguments({
routerPreference,
tradeType,
isRoutingAPIPrice,
needsWrapIfUniswapX: tokenIn.isNative,
uniswapXEnabled,
uniswapXForceSyntheticQuotes,
},
[amount, routerPreference, tokenIn, tokenOut, tradeType, isRoutingAPIPrice]
[
account,
amount,
routerPreference,
tokenIn,
tokenOut,
tradeType,
uniswapXEnabled,
isRoutingAPIPrice,
uniswapXForceSyntheticQuotes,
]
)
}
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Price, Token, TradeType } from '@uniswap/sdk-core'
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 { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { computeRealizedPriceImpact } from 'utils/prices'
export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => {
......@@ -34,44 +34,66 @@ export const getPriceUpdateBasisPoints = (
return formatPercentInBasisPointsNumber(changePercentage)
}
export function formatCommonPropertiesForTrade(trade: InterfaceTrade, allowedSlippage: Percent) {
return {
routing: trade.fillType,
type: trade.tradeType,
ura_quote_id: isUniswapXTrade(trade) ? trade.quoteId : undefined,
ura_request_id: trade.requestId,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: isClassicTrade(trade)
? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade))
: undefined,
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
estimated_network_fee_usd: isClassicTrade(trade) ? trade.gasUseEstimateUSD : trade.classicGasUseEstimateUSD,
minimum_output_after_slippage: trade.minimumAmountOut(allowedSlippage).toSignificant(6),
allowed_slippage: formatPercentNumber(allowedSlippage),
method: getQuoteMethod(trade),
}
}
export const formatSwapSignedAnalyticsEventProperties = ({
trade,
allowedSlippage,
fiatValues,
txHash,
}: {
trade: InterfaceTrade | Trade<Currency, Currency, TradeType>
trade: InterfaceTrade
allowedSlippage: Percent
fiatValues: { amountIn?: number; amountOut?: number }
txHash: string
txHash?: string
}) => ({
transaction_hash: txHash,
token_in_amount_usd: fiatValues.amountIn,
token_out_amount_usd: fiatValues.amountOut,
...formatEventPropertiesForTrade(trade, allowedSlippage),
...formatCommonPropertiesForTrade(trade, allowedSlippage),
})
export const formatEventPropertiesForTrade = (
trade: Trade<Currency, Currency, TradeType>,
function getQuoteMethod(trade: InterfaceTrade) {
if (isUniswapXTrade(trade)) return QuoteMethod.ROUTING_API
return trade.quoteMethod
}
export const formatSwapQuoteReceivedEventProperties = (
trade: InterfaceTrade,
allowedSlippage: Percent,
gasUseEstimateUSD?: string,
method?: QuoteMethod
swapQuoteReceivedDate: Date
) => {
return {
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined,
estimated_network_fee_usd: gasUseEstimateUSD,
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
minimum_output_after_slippage: trade.minimumAmountOut(allowedSlippage).toSignificant(6),
allowed_slippage: formatPercentNumber(allowedSlippage),
method,
...formatCommonPropertiesForTrade(trade, allowedSlippage),
swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
swap_quote_received_timestamp: swapQuoteReceivedDate.getTime(),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
token_in_amount_max: trade.maximumAmountIn(allowedSlippage),
token_out_amount_min: trade.minimumAmountOut(allowedSlippage),
}
}
......@@ -2,19 +2,27 @@ import { Currency, CurrencyAmount, NativeCurrency, Percent, Token, TradeType } f
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useBestTrade } from 'hooks/useBestTrade'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { RouterPreference } from 'state/routing/slice'
import { ClassicTrade, TradeState } from 'state/routing/types'
import { isClassicTrade } from 'state/routing/utils'
export default function useDerivedPayWithAnyTokenSwapInfo(
inputCurrency?: Currency,
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): {
state: TradeState
trade?: InterfaceTrade
trade?: ClassicTrade
maximumAmountIn?: CurrencyAmount<Token>
allowedSlippage: Percent
} {
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined)
const allowedSlippage = useAutoSlippageTolerance(trade)
const { state, trade } = useBestTrade(
TradeType.EXACT_OUTPUT,
parsedOutputAmount,
inputCurrency ?? undefined,
RouterPreference.API
)
const allowedSlippage = useAutoSlippageTolerance(isClassicTrade(trade) ? trade : undefined)
const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
......
......@@ -4,6 +4,7 @@ import { Allowance } from 'hooks/usePermit2Allowance'
import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
import { useEffect } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { isClassicTrade } from 'state/routing/utils'
import { useTokenInput } from './useTokenInput'
......@@ -13,7 +14,7 @@ export default function usePayWithAnyTokenSwap(
allowedSlippage?: Percent
) {
const setTokenTradeInput = useTokenInput((state) => state.setTokenTradeInput)
const hasRoutes = !!trade && trade.routes
const hasRoutes = isClassicTrade(trade) && trade.routes
const hasInputAmount = !!trade && !!trade.inputAmount && trade.inputAmount.currency.isToken
const hasAllowance = !!allowedSlippage && !!allowance
......
import { Percent } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { ClassicTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro'
import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
......@@ -14,7 +14,7 @@ interface PriceImpactSeverity {
color: string
}
export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined {
export function usePriceImpact(trade?: ClassicTrade): PriceImpact | undefined {
const theme = useTheme()
return useMemo(() => {
......
......@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { Pool } from '@uniswap/v3-sdk'
import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks'
import { InterfaceTrade } from 'state/routing/types'
import { ClassicTrade } from 'state/routing/types'
interface SwapAmounts {
inputAmount: CurrencyAmount<Currency>
......@@ -108,7 +108,7 @@ function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput {
}
}
export function buildAllTradeRouteInputs(trade: InterfaceTrade): {
export function buildAllTradeRouteInputs(trade: ClassicTrade): {
mixedTokenTradeRouteInputs?: TokenTradeRouteInput[]
v2TokenTradeRouteInputs?: TokenTradeRouteInput[]
v3TokenTradeRouteInputs?: TokenTradeRouteInput[]
......
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace } from '@uniswap/analytics'
import Column from 'components/Column'
import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { Arrow } from 'components/Popover'
import UniswapXRouterLabel from 'components/RouterLabel/UniswapXRouterLabel'
import Row from 'components/Row'
import {
SwapMustache,
SwapMustacheShadow,
SwapOptInSmallContainer,
UniswapPopoverContainer,
UniswapXOptInLargeContainer,
UniswapXOptInLargeContainerPositioner,
UniswapXShine,
} from 'components/swap/styleds'
import { formatCommonPropertiesForTrade } from 'lib/utils/analytics'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
import { X } from 'react-feather'
import { Text } from 'rebass'
import { useAppDispatch } from 'state/hooks'
import { RouterPreference } from 'state/routing/slice'
import { isClassicTrade } from 'state/routing/utils'
import { SwapInfo } from 'state/swap/hooks'
import { useRouterPreference, useUserDisabledUniswapX } from 'state/user/hooks'
import { updateDisabledUniswapX } from 'state/user/reducer'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
export const UniswapXOptIn = (props: { swapInfo: SwapInfo; isSmall: boolean }) => {
const {
trade: { trade },
} = props.swapInfo
const userDisabledUniswapX = useUserDisabledUniswapX()
const isOnClassic = Boolean(trade && isClassicTrade(trade) && trade.isUniswapXBetter && !userDisabledUniswapX)
const [hasEverShown, setHasEverShown] = useState(false)
if (isOnClassic && !hasEverShown) {
setHasEverShown(true)
}
// avoid some work if never needed to show
if (!hasEverShown) {
return null
}
return <OptInContents isOnClassic={isOnClassic} {...props} />
}
const OptInContents = ({
swapInfo,
isOnClassic,
isSmall,
}: {
swapInfo: SwapInfo
isOnClassic: boolean
isSmall: boolean
}) => {
const {
trade: { trade },
allowedSlippage,
} = swapInfo
const [, setRouterPreference] = useRouterPreference()
const dispatch = useAppDispatch()
const [showYoureIn, setShowYoureIn] = useState(false)
const [isVisible, setIsVisible] = useState(false)
// adding this as we need to mount and then set shouldAnimate = true after it mounts to avoid a flicker on initial mount
const [shouldAnimate, setShouldAnimate] = useState(false)
// delayed a second to allow mount animation
useEffect(() => {
setIsVisible(Boolean(trade && isOnClassic))
}, [isOnClassic, trade])
useEffect(() => {
if (!isVisible || shouldAnimate) return
// delay visible animation a bit
const tm = setTimeout(() => setShouldAnimate(true), 350)
return () => clearTimeout(tm)
}, [isVisible, shouldAnimate])
const tryItNowElement = (
<ThemedText.BodySecondary
color="accentAction"
fontSize={14}
fontWeight="500"
onClick={() => {
// slight delay before hiding
setTimeout(() => {
setShowYoureIn(true)
setTimeout(() => {
setShowYoureIn(false)
}, 5000)
}, 200)
if (!trade) return
sendAnalyticsEvent('UniswapX Opt In Toggled', {
...formatCommonPropertiesForTrade(trade, allowedSlippage),
new_preference: RouterPreference.X,
})
setRouterPreference(RouterPreference.X)
}}
style={{
cursor: 'pointer',
}}
>
Try it now
</ThemedText.BodySecondary>
)
const containerRef = useRef<HTMLDivElement>()
const wrapTrace = (children: JSX.Element) => {
return (
<Trace
shouldLogImpression={isVisible}
name="UniswapX Opt In Impression"
properties={trade ? formatCommonPropertiesForTrade(trade, allowedSlippage) : undefined}
>
{children}
</Trace>
)
}
if (isSmall) {
return wrapTrace(
<SwapOptInSmallContainer ref={containerRef as any} visible={isVisible} shouldAnimate={shouldAnimate}>
<SwapMustache>
<UniswapXShine />
<SwapMustacheShadow />
<Row justify="space-between" align="center" flexWrap="wrap">
<Text fontSize={14} fontWeight={400} lineHeight="20px">
<Trans>Try gas free swaps with the</Trans>
<br />
<UniswapXBrandMark fontWeight="bold" style={{ transform: `translateY(1px)`, margin: '0 2px' }} />{' '}
<Trans>Beta</Trans>
</Text>
{tryItNowElement}
</Row>
</SwapMustache>
</SwapOptInSmallContainer>
)
}
return wrapTrace(
<>
{/* first popover: intro */}
<UniswapXOptInPopover shiny visible={isVisible && !showYoureIn}>
<CloseIcon
size={18}
onClick={() => {
if (!trade) return
sendAnalyticsEvent('UniswapX Opt In Toggled', {
...formatCommonPropertiesForTrade(trade, allowedSlippage),
new_preference: RouterPreference.API,
})
setRouterPreference(RouterPreference.API)
dispatch(updateDisabledUniswapX({ disabledUniswapX: true }))
}}
/>
<Column>
<Text fontSize={14} fontWeight={400} lineHeight="20px">
<Trans>Try the</Trans>{' '}
<UniswapXBrandMark fontWeight="bold" style={{ transform: `translateY(2px)`, margin: '0 1px' }} />{' '}
<Trans>Beta</Trans>
<ul style={{ margin: '5px 0 12px 24px', lineHeight: '24px', padding: 0 }}>
<li>
<Trans>Gas free swaps</Trans>
</li>
<li>
<Trans>MEV protection</Trans>
</li>
<li>
<Trans>Better prices and more liquidity</Trans>
</li>
</ul>
</Text>
</Column>
{tryItNowElement}
</UniswapXOptInPopover>
{/* second popover: you're in! */}
<UniswapXOptInPopover visible={showYoureIn}>
<UniswapXRouterLabel disableTextGradient>
<Text fontSize={14} fontWeight={500} lineHeight="20px">
<Trans>You&apos;re in!</Trans>
</Text>
</UniswapXRouterLabel>
<ThemedText.BodySecondary style={{ marginTop: 8 }} fontSize={14}>
<Trans>You can turn it off at anytime in settings</Trans>
</ThemedText.BodySecondary>
</UniswapXOptInPopover>
</>
)
}
const UniswapXOptInPopover = (props: PropsWithChildren<{ visible: boolean; shiny?: boolean }>) => {
return (
// positioner ensures no matter the height of the inner content
// it sits at the same position from the top of the swap area
<UniswapXOptInLargeContainerPositioner>
<UniswapXOptInLargeContainer visible={props.visible}>
<Arrow className="arrow-right" style={{ position: 'absolute', bottom: '50%', left: -3.5, zIndex: 100 }} />
<UniswapPopoverContainer>
{props.shiny && <UniswapXShine style={{ zIndex: 0 }} />}
{props.children}
</UniswapPopoverContainer>
</UniswapXOptInLargeContainer>
</UniswapXOptInLargeContainerPositioner>
)
}
const CloseIcon = styled(X)`
color: ${({ theme }) => theme.textTertiary};
cursor: pointer;
position: absolute;
top: 14px;
right: 14px;
`
This diff is collapsed.
......@@ -2,13 +2,23 @@ import { createSlice, nanoid } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk-core'
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
export enum PopupType {
Transaction = 'transaction',
Order = 'order',
FailedSwitchNetwork = 'failedSwitchNetwork',
}
export type PopupContent =
| {
txn: {
hash: string
}
type: PopupType.Transaction
hash: string
}
| {
type: PopupType.Order
orderHash: string
}
| {
type: PopupType.FailedSwitchNetwork
failedSwitchNetwork: ChainId
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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