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" ...@@ -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_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy" 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_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395" REACT_APP_UNISWAP_API_URL="https://api.uniswap.org/v2"
\ No newline at end of file REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
...@@ -41,7 +41,7 @@ describe('Swap errors', () => { ...@@ -41,7 +41,7 @@ describe('Swap errors', () => {
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') 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('confirmation-close-icon')).click()
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
...@@ -89,7 +89,7 @@ describe('Swap errors', () => { ...@@ -89,7 +89,7 @@ describe('Swap errors', () => {
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') 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('confirmation-close-icon')).click()
} }
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending') cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
......
import { FeatureFlag } from '../../../src/featureFlags'
import { getTestSelector } from '../../utils' import { getTestSelector } from '../../utils'
describe('Swap settings', () => { describe('Swap settings', () => {
it('Opens and closes the settings menu', () => { 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.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click() cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Max slippage').should('exist') cy.contains('Max slippage').should('exist')
cy.contains('Transaction deadline').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.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist') cy.contains('Settings').should('not.exist')
}) })
......
...@@ -69,9 +69,9 @@ describe('Swap', () => { ...@@ -69,9 +69,9 @@ describe('Swap', () => {
cy.contains('Review swap') cy.contains('Review swap')
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') 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('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') cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
// Mine transaction // Mine transaction
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -135,8 +135,8 @@ ...@@ -135,8 +135,8 @@
"ts-transform-graphql-tag": "^0.2.1", "ts-transform-graphql-tag": "^0.2.1",
"typechain": "^5.0.0", "typechain": "^5.0.0",
"typescript": "^4.4.3", "typescript": "^4.4.3",
"wrangler": "https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/4925945367/npm-package-wrangler-3048",
"webpack-retry-chunk-load-plugin": "^3.1.1", "webpack-retry-chunk-load-plugin": "^3.1.1",
"wrangler": "https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/4925945367/npm-package-wrangler-3048",
"yarn-deduplicate": "^6.0.0" "yarn-deduplicate": "^6.0.0"
}, },
"dependencies": { "dependencies": {
...@@ -168,7 +168,7 @@ ...@@ -168,7 +168,7 @@
"@types/react-window-infinite-loader": "^1.0.6", "@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.3.1", "@uniswap/analytics": "^1.3.1",
"@uniswap/analytics-events": "^2.13.0", "@uniswap/analytics-events": "^2.13.0",
"@uniswap/conedison": "^1.7.1", "@uniswap/conedison": "^1.8.0",
"@uniswap/governance": "^1.0.2", "@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2", "@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1", "@uniswap/merkle-distributor": "1.0.1",
...@@ -177,7 +177,8 @@ ...@@ -177,7 +177,8 @@
"@uniswap/router-sdk": "^1.3.0", "@uniswap/router-sdk": "^1.3.0",
"@uniswap/sdk-core": "^3.2.6", "@uniswap/sdk-core": "^3.2.6",
"@uniswap/smart-order-router": "3.13.5", "@uniswap/smart-order-router": "3.13.5",
"@uniswap/token-lists": "^1.0.0-beta.31", "@uniswap/token-lists": "^1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "^1.0.0",
"@uniswap/universal-router-sdk": "^1.5.3", "@uniswap/universal-router-sdk": "^1.5.3",
"@uniswap/v2-core": "1.0.0", "@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0", "@uniswap/v2-periphery": "^1.1.0-beta.0",
......
<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>
<svg width="74" height="72" viewBox="0 0 74 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.3747 21.5043C24.671 21.5043 24.9274 21.7105 24.991 21.9998C25.1664 22.7982 25.1791 24.1098 24.6141 25.1601C24.322 25.703 23.8683 26.1865 23.2031 26.4658C22.5432 26.7429 21.7375 26.7931 20.7823 26.5871C19.5609 26.3237 18.3363 25.6271 17.1951 24.7335C16.0477 23.8349 14.9472 22.7077 13.9724 21.5319C12.9967 20.3551 12.137 19.1177 11.4739 17.9903C10.8173 16.8739 10.3306 15.8269 10.132 15.0366C10.059 14.7459 10.2005 14.444 10.4706 14.3141C10.7407 14.1843 11.0649 14.2624 11.2463 14.501C12.087 15.6072 14.0213 17.3714 16.4539 18.8598C18.8877 20.3489 21.7272 21.5043 24.3747 21.5043Z" fill="white" stroke="black" stroke-width="1.26191" stroke-linejoin="round"/>
<mask id="path-2-outside-1_1930_115608" maskUnits="userSpaceOnUse" x="0.0805664" y="18.0469" width="56" height="43" fill="black">
<rect fill="white" x="0.0805664" y="18.0469" width="56" height="43"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.4887 52.6749C51.8537 49.2159 53.8899 44.7033 53.8899 39.7671C53.8899 28.8759 43.9775 20.0469 31.7499 20.0469C19.5224 20.0469 9.60998 28.8759 9.60998 39.7671C9.60998 40.805 9.69999 41.8241 9.87343 42.8186C5.25341 44.2149 2.08057 47.1039 2.08057 50.4413C2.08057 55.1444 8.38116 58.9569 16.1533 58.9569C18.6361 58.9569 20.9687 58.5679 22.9937 57.885C25.6793 58.9161 28.6397 59.4874 31.7499 59.4874C38.4356 59.4874 44.4292 56.8479 48.4887 52.6749Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.4887 52.6749C51.8537 49.2159 53.8899 44.7033 53.8899 39.7671C53.8899 28.8759 43.9775 20.0469 31.7499 20.0469C19.5224 20.0469 9.60998 28.8759 9.60998 39.7671C9.60998 40.805 9.69999 41.8241 9.87343 42.8186C5.25341 44.2149 2.08057 47.1039 2.08057 50.4413C2.08057 55.1444 8.38116 58.9569 16.1533 58.9569C18.6361 58.9569 20.9687 58.5679 22.9937 57.885C25.6793 58.9161 28.6397 59.4874 31.7499 59.4874C38.4356 59.4874 44.4292 56.8479 48.4887 52.6749Z" fill="white"/>
<path d="M48.4887 52.6749L49.3932 53.5549L48.4887 52.6749ZM9.87343 42.8186L10.2385 44.0265L11.3086 43.7031L11.1166 42.6018L9.87343 42.8186ZM22.9937 57.885L23.446 56.707L23.0214 56.544L22.5905 56.6893L22.9937 57.885ZM52.628 39.7671C52.628 44.3432 50.743 48.548 47.5842 51.795L49.3932 53.5549C52.9645 49.8838 55.1518 45.0634 55.1518 39.7671H52.628ZM31.7499 21.3088C43.4219 21.3088 52.628 29.7062 52.628 39.7671H55.1518C55.1518 28.0457 44.5332 18.785 31.7499 18.785V21.3088ZM10.8719 39.7671C10.8719 29.7062 20.078 21.3088 31.7499 21.3088V18.785C18.9667 18.785 8.34807 28.0457 8.34807 39.7671H10.8719ZM11.1166 42.6018C10.9555 41.6784 10.8719 40.7318 10.8719 39.7671H8.34807C8.34807 40.8781 8.44444 41.9697 8.63029 43.0354L11.1166 42.6018ZM3.34248 50.4413C3.34248 47.9707 5.77784 45.3747 10.2385 44.0265L9.50835 41.6106C4.72899 43.0551 0.818657 46.2371 0.818657 50.4413H3.34248ZM16.1533 57.695C12.4585 57.695 9.17438 56.7862 6.85568 55.3831C4.5135 53.9658 3.34248 52.1807 3.34248 50.4413H0.818657C0.818657 53.405 2.79793 55.8776 5.54909 57.5424C8.32373 59.2214 12.076 60.2188 16.1533 60.2188V57.695ZM22.5905 56.6893C20.7028 57.3258 18.5071 57.695 16.1533 57.695V60.2188C18.7651 60.2188 21.2345 59.8099 23.3969 59.0808L22.5905 56.6893ZM31.7499 58.2255C28.7946 58.2255 25.9875 57.6827 23.446 56.707L22.5414 59.0631C25.3711 60.1496 28.4849 60.7493 31.7499 60.7493V58.2255ZM47.5842 51.795C43.7692 55.7165 38.1051 58.2255 31.7499 58.2255V60.7493C38.7662 60.7493 45.0891 57.9792 49.3932 53.5549L47.5842 51.795Z" fill="black" mask="url(#path-2-outside-1_1930_115608)"/>
<path d="M39.0719 21.5043C38.7762 21.5043 38.5201 21.7097 38.456 21.9984C38.2783 22.7979 38.2654 24.1112 38.8376 25.1627C39.1334 25.7063 39.592 26.1886 40.2622 26.4668C40.9266 26.7426 41.7385 26.793 42.7026 26.5874C43.9355 26.3246 45.1727 25.6291 46.3268 24.7356C47.487 23.8374 48.6 22.7104 49.586 21.5347C50.5728 20.3579 51.4423 19.1205 52.1131 17.993C52.7773 16.8766 53.2698 15.8293 53.4709 15.0382C53.5446 14.7481 53.4043 14.4461 53.135 14.3153C52.8657 14.1845 52.5416 14.2609 52.3592 14.4982C51.5085 15.6046 49.5517 17.3692 47.0903 18.8581C44.6282 20.3475 41.7536 21.5043 39.0719 21.5043Z" fill="white" stroke="black" stroke-width="1.26191" stroke-linejoin="round"/>
<path d="M9.27317 46.8448L9.50259 46.4782C9.93019 45.7949 9.38096 45.0323 8.64133 45.3527C7.81405 45.7111 6.82196 46.2689 5.87019 47.1255C5.44297 47.51 5.12141 47.8905 4.88077 48.2545C3.88061 49.7674 5.42411 51.2381 7.23768 51.2541C8.19162 51.2626 9.28694 51.2174 10.3799 51.0555C11.8292 50.8408 13.0434 50.1513 13.916 49.4901C14.561 49.0012 14.1621 48.1301 13.3534 48.1626L10.1134 48.2925C9.35325 48.323 8.8696 47.4897 9.27317 46.8448Z" fill="black"/>
<path d="M22.3604 55.8761C23.8648 55.9852 25.2756 55.8261 26.3472 55.4561C26.8819 55.2714 27.362 55.024 27.7263 54.7048C28.0935 54.3831 28.3749 53.9571 28.4127 53.4357C28.4505 52.9142 28.2335 52.4521 27.9166 52.0807C27.6022 51.7123 27.1628 51.3982 26.6604 51.1384C25.6534 50.6176 24.2803 50.2565 22.7758 50.1474C21.2714 50.0383 19.8606 50.1974 18.789 50.5675C18.2544 50.7521 17.7742 50.9995 17.4099 51.3187C17.0428 51.6404 16.7613 52.0665 16.7235 52.5879C16.6857 53.1093 16.9027 53.5714 17.2196 53.9428C17.534 54.3112 17.9735 54.6253 18.4759 54.8852C19.4829 55.4059 20.8559 55.767 22.3604 55.8761Z" fill="white" stroke="black" stroke-width="1.26191"/>
<line y1="-0.630955" x2="10.458" y2="-0.630955" transform="matrix(0.99738 0.0723377 -0.0723344 0.99738 17.2944 53.4453)" stroke="black" stroke-width="1.26191"/>
<line y1="-0.630955" x2="5.19898" y2="-0.630955" transform="matrix(-0.0723344 0.99738 -0.99738 -0.0723377 23.6921 50.0703)" stroke="black" stroke-width="1.26191"/>
<line y1="-0.630955" x2="5.19898" y2="-0.630955" transform="matrix(-0.0723344 0.99738 -0.99738 -0.0723377 19.9366 50.5742)" stroke="black" stroke-width="1.26191"/>
<path d="M23.6113 12.1879C23.4573 11.9346 23.1478 11.8226 22.8674 11.9188C22.587 12.015 22.4114 12.2934 22.4453 12.5878L23.4514 21.3197C23.618 22.7653 24.9556 23.7808 26.3928 23.5526C28.1885 23.2675 29.1214 21.2523 28.1769 19.6986L23.6113 12.1879Z" fill="url(#paint0_linear_1930_115608)" stroke="black" stroke-width="1.26191" stroke-linejoin="round"/>
<mask id="path-11-outside-2_1930_115608" maskUnits="userSpaceOnUse" x="53.3934" y="34.8259" width="22.6706" height="24.8274" fill="black">
<rect fill="white" x="53.3934" y="34.8259" width="22.6706" height="24.8274"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M57.6261 54.012C57.135 53.9721 56.7516 53.8134 56.5049 53.5249C55.2906 52.1043 57.8521 48.0728 62.2261 44.5205C66.6002 40.9681 71.1305 39.24 72.3449 40.6606C73.078 41.5183 72.435 43.3274 70.8479 45.4082C71.665 45.4935 72.234 45.7574 72.4517 46.2109C73.1324 47.6291 70.1196 50.3641 65.7223 52.3195C62.4196 53.7883 59.2752 54.3996 57.6261 54.012Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M57.6261 54.012C57.135 53.9721 56.7516 53.8134 56.5049 53.5249C55.2906 52.1043 57.8521 48.0728 62.2261 44.5205C66.6002 40.9681 71.1305 39.24 72.3449 40.6606C73.078 41.5183 72.435 43.3274 70.8479 45.4082C71.665 45.4935 72.234 45.7574 72.4517 46.2109C73.1324 47.6291 70.1196 50.3641 65.7223 52.3195C62.4196 53.7883 59.2752 54.3996 57.6261 54.012Z" fill="url(#paint1_linear_1930_115608)"/>
<path d="M57.6261 54.012L57.9148 52.7835C57.8534 52.7691 57.791 52.7593 57.7282 52.7542L57.6261 54.012ZM56.5049 53.5249L55.5457 54.3449L55.5457 54.3449L56.5049 53.5249ZM62.2261 44.5205L61.4306 43.5409L62.2261 44.5205ZM72.3449 40.6606L71.3857 41.4806L71.3857 41.4806L72.3449 40.6606ZM70.8479 45.4082L69.8445 44.6428C69.5678 45.0056 69.5085 45.4895 69.6893 45.9084C69.8702 46.3272 70.2631 46.6159 70.7169 46.6633L70.8479 45.4082ZM72.4517 46.2109L71.314 46.757L72.4517 46.2109ZM65.7223 52.3195L66.2351 53.4726L65.7223 52.3195ZM57.7282 52.7542C57.5875 52.7428 57.5078 52.7177 57.4715 52.7021C57.44 52.6886 57.4475 52.6855 57.4642 52.705L55.5457 54.3449C56.0816 54.9718 56.8316 55.2135 57.524 55.2697L57.7282 52.7542ZM57.4642 52.705C57.5255 52.7768 57.4073 52.7335 57.5045 52.2864C57.5986 51.8541 57.859 51.248 58.335 50.4988C59.279 49.0131 60.9099 47.2151 63.0217 45.5L61.4306 43.5409C59.1683 45.3782 57.3314 47.3721 56.2048 49.1453C55.6454 50.0257 55.2198 50.9159 55.0384 51.75C54.8602 52.5693 54.8772 53.5627 55.5457 54.3449L57.4642 52.705ZM63.0217 45.5C65.1329 43.7855 67.244 42.5441 68.9202 41.9047C69.7645 41.5827 70.4326 41.4388 70.9004 41.4263C71.3906 41.4132 71.4384 41.5422 71.3857 41.4806L73.3041 39.8407C72.6442 39.0687 71.6703 38.881 70.8329 38.9034C69.973 38.9264 69.0075 39.1702 68.0207 39.5466C66.0346 40.3042 63.6935 41.7031 61.4306 43.5409L63.0217 45.5ZM71.3857 41.4806C71.3132 41.3958 71.4919 41.4945 71.2668 42.1842C71.0603 42.8169 70.5933 43.6612 69.8445 44.6428L71.8512 46.1735C72.6896 45.0744 73.3376 43.9737 73.6661 42.9673C73.976 42.0178 74.1096 40.7831 73.3041 39.8407L71.3857 41.4806ZM70.7169 46.6633C71.0333 46.6963 71.2227 46.7572 71.3185 46.8042C71.4057 46.8471 71.3576 46.8477 71.314 46.757L73.5893 45.6649C73.0698 44.5825 71.8893 44.2481 70.9789 44.1531L70.7169 46.6633ZM71.314 46.757C71.2372 46.5968 71.3566 46.582 71.1885 46.902C71.0285 47.2068 70.6953 47.6281 70.1431 48.1294C69.05 49.1216 67.321 50.2275 65.2095 51.1665L66.2351 53.4726C68.5208 52.4561 70.4969 51.2168 71.8394 49.9981C72.5049 49.394 73.0722 48.7434 73.423 48.0755C73.7658 47.4227 74.0066 46.5342 73.5893 45.6649L71.314 46.757ZM65.2095 51.1665C63.6302 51.8688 62.1053 52.3583 60.8007 52.6257C59.4622 52.9 58.4759 52.9154 57.9148 52.7835L57.3374 55.2404C58.4254 55.4961 59.8358 55.3997 61.3075 55.0981C62.813 54.7896 64.5117 54.239 66.2351 53.4726L65.2095 51.1665Z" fill="black" mask="url(#path-11-outside-2_1930_115608)"/>
<ellipse cx="14.8446" cy="37.3595" rx="3.31248" ry="4.96883" fill="black"/>
<ellipse cx="14.9235" cy="37.3605" rx="1.4985" ry="2.60272" fill="white"/>
<ellipse cx="27.7795" cy="37.3595" rx="3.31248" ry="4.96883" fill="black"/>
<ellipse cx="27.8581" cy="37.3605" rx="1.4985" ry="2.60272" fill="white"/>
<path d="M34.2463 54.3156C34.2463 51.2381 35.3257 47.5016 37.9302 45.6216C40.1969 43.9853 43.0541 43.828 45.7611 44.284" stroke="black" stroke-width="1.26191"/>
<defs>
<linearGradient id="paint0_linear_1930_115608" x1="27.4989" y1="4.1016" x2="19.0152" y2="20.2113" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF24A7"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint1_linear_1930_115608" x1="66.7203" y1="38.9751" x2="63.2492" y2="55.4808" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF24A7"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
</defs>
</svg>
...@@ -283,7 +283,7 @@ function SwapSummary({ info }: { info: ExactInputSwapTransactionInfo | ExactOutp ...@@ -283,7 +283,7 @@ function SwapSummary({ info }: { info: ExactInputSwapTransactionInfo | ExactOutp
/>{' '} />{' '}
for{' '} for{' '}
<FormattedCurrencyAmountManaged <FormattedCurrencyAmountManaged
rawAmount={info.expectedOutputCurrencyAmountRaw} rawAmount={info.settledOutputCurrencyAmountRaw ?? info.expectedOutputCurrencyAmountRaw}
currencyId={info.outputCurrencyId} currencyId={info.outputCurrencyId}
sigFigs={6} sigFigs={6}
/> />
......
...@@ -3,8 +3,10 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an ...@@ -3,8 +3,10 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
import Column from 'components/Column' import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner' import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import Row from 'components/Row'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName' import useENSName from 'hooks/useENSName'
import { useCallback } from 'react'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme' import { EllipsisStyle, ThemedText } from 'theme'
import { shortenAddress } from 'utils' import { shortenAddress } from 'utils'
...@@ -12,6 +14,7 @@ import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' ...@@ -12,6 +14,7 @@ import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo' import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow from '../PortfolioRow' import PortfolioRow from '../PortfolioRow'
import { useOpenOffchainActivityModal } from './OffchainActivityModal'
import { useTimeSince } from './parseRemote' import { useTimeSince } from './parseRemote'
import { Activity } from './types' import { Activity } from './types'
...@@ -26,16 +29,36 @@ const StyledTimestamp = styled(ThemedText.Caption)` ...@@ -26,16 +29,36 @@ const StyledTimestamp = styled(ThemedText.Caption)`
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on; font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
` `
export function ActivityRow({ function StatusIndicator({ activity: { status, timestamp } }: { activity: Activity }) {
activity: { chainId, status, title, descriptor, logos, otherAccount, currencies, timestamp, hash },
}: {
activity: Activity
}) {
const { ENSName } = useENSName(otherAccount)
const timeSince = useTimeSince(timestamp) 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 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 ( return (
<TraceEvent <TraceEvent
events={[BrowserEvent.onClick]} events={[BrowserEvent.onClick]}
...@@ -49,23 +72,20 @@ export function ActivityRow({ ...@@ -49,23 +72,20 @@ export function ActivityRow({
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} /> <PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column> </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={ descriptor={
<ActivityRowDescriptor color="textSecondary"> <ActivityRowDescriptor color="textSecondary">
{descriptor} {descriptor}
{ENSName ?? shortenAddress(otherAccount)} {ENSName ?? shortenAddress(otherAccount)}
</ActivityRowDescriptor> </ActivityRowDescriptor>
} }
right={ right={<StatusIndicator activity={activity} />}
status === TransactionStatus.Pending ? ( onClick={onClick}
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
/> />
</TraceEvent> </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' ...@@ -3,7 +3,7 @@ import { useAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column' import Column from 'components/Column'
import { LoadingBubble } from 'components/Tokens/loading' import { LoadingBubble } from 'components/Tokens/loading'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns' 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 { PollingInterval } from 'graphql/data/util'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
...@@ -91,7 +91,7 @@ function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account ...@@ -91,7 +91,7 @@ function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account
if (!remoteTx) return false if (!remoteTx) return false
// Cancellations are only possible when both nonce and tx.from are the same // 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 // If the remote tx has a different hash than the local tx, the local tx was cancelled
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase() return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
} }
...@@ -126,11 +126,7 @@ export function ActivityTab({ account }: { account: string }) { ...@@ -126,11 +126,7 @@ export function ActivityTab({ account }: { account: string }) {
const localMap = useLocalActivities(account) const localMap = useLocalActivities(account)
const { data, loading, refetch } = useTransactionListQuery({ const { data, loading, refetch } = useActivityQuery({ variables: { account } })
variables: { account },
errorPolicy: 'all',
fetchPolicy: 'cache-first',
})
// We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer // We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer
useEffect(() => { useEffect(() => {
......
...@@ -12,7 +12,9 @@ import { ...@@ -12,7 +12,9 @@ import {
} from 'state/transactions/types' } from 'state/transactions/types'
import { renderHook } from 'test-utils/render' 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( function mockSwapInfo(
type: MockTradeType, type: MockTradeType,
...@@ -30,6 +32,7 @@ function mockSwapInfo( ...@@ -30,6 +32,7 @@ function mockSwapInfo(
outputCurrencyId: outputCurrency.address, outputCurrencyId: outputCurrency.address,
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw, expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw, minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
isUniswapXOrder: false,
} }
} else { } else {
return { return {
...@@ -40,6 +43,7 @@ function mockSwapInfo( ...@@ -40,6 +43,7 @@ function mockSwapInfo(
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw, maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address, outputCurrencyId: outputCurrency.address,
outputCurrencyAmountRaw, outputCurrencyAmountRaw,
isUniswapXOrder: false,
} }
} }
} }
...@@ -247,26 +251,12 @@ describe('parseLocalActivity', () => { ...@@ -247,26 +251,12 @@ describe('parseLocalActivity', () => {
}, },
} as TransactionDetails } as TransactionDetails
const chainId = ChainId.MAINNET const chainId = ChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({ expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toEqual({
chainId: 1, chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI], currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI', descriptor: '1.00 USDC for 1.00 DAI',
hash: undefined, hash: undefined,
receipt: { from: undefined,
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',
},
status: 'CONFIRMED', status: 'CONFIRMED',
timestamp: NaN, timestamp: NaN,
title: 'Swapped', title: 'Swapped',
...@@ -288,7 +278,7 @@ describe('parseLocalActivity', () => { ...@@ -288,7 +278,7 @@ describe('parseLocalActivity', () => {
}, },
} as TransactionDetails } as TransactionDetails
const chainId = ChainId.MAINNET const chainId = ChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({ expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
chainId: 1, chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI], currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI', descriptor: '1.00 USDC for 1.00 DAI',
...@@ -313,7 +303,7 @@ describe('parseLocalActivity', () => { ...@@ -313,7 +303,7 @@ describe('parseLocalActivity', () => {
} as TransactionDetails } as TransactionDetails
const chainId = ChainId.MAINNET const chainId = ChainId.MAINNET
const tokens = {} as ChainTokenMap const tokens = {} as ChainTokenMap
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({ expect(transactionToActivity(details, chainId, tokens)).toMatchObject({
chainId: 1, chainId: 1,
currencies: [undefined, undefined], currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown', descriptor: 'Unknown for Unknown',
...@@ -349,10 +339,7 @@ describe('parseLocalActivity', () => { ...@@ -349,10 +339,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`, descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -367,10 +354,7 @@ describe('parseLocalActivity', () => { ...@@ -367,10 +354,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`, descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -385,10 +369,7 @@ describe('parseLocalActivity', () => { ...@@ -385,10 +369,7 @@ describe('parseLocalActivity', () => {
descriptor: MockDAI.symbol, descriptor: MockDAI.symbol,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -402,10 +383,6 @@ describe('parseLocalActivity', () => { ...@@ -402,10 +383,6 @@ describe('parseLocalActivity', () => {
descriptor: MockUSDT.symbol, descriptor: MockUSDT.symbol,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -422,10 +399,7 @@ describe('parseLocalActivity', () => { ...@@ -422,10 +399,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`, descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -442,10 +416,7 @@ describe('parseLocalActivity', () => { ...@@ -442,10 +416,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`, descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -460,10 +431,7 @@ describe('parseLocalActivity', () => { ...@@ -460,10 +431,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -478,10 +446,7 @@ describe('parseLocalActivity', () => { ...@@ -478,10 +446,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -496,10 +461,7 @@ describe('parseLocalActivity', () => { ...@@ -496,10 +461,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -514,10 +476,7 @@ describe('parseLocalActivity', () => { ...@@ -514,10 +476,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
...@@ -532,10 +491,29 @@ describe('parseLocalActivity', () => { ...@@ -532,10 +491,29 @@ describe('parseLocalActivity', () => {
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`, descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
hash, hash,
status: MockTxStatus.Confirmed, status: MockTxStatus.Confirmed,
receipt: { from: mockAccount2,
id: hash,
status: MockTxStatus.Confirmed,
},
}) })
}) })
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' ...@@ -3,9 +3,12 @@ import { t } from '@lingui/macro'
import { formatCurrencyAmount } from '@uniswap/conedison/format' import { formatCurrencyAmount } from '@uniswap/conedison/format'
import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { nativeOnChain } from '@uniswap/smart-order-router' import { nativeOnChain } from '@uniswap/smart-order-router'
import UniswapXBolt from 'assets/svg/bolt.svg'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens' import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
import { useMemo } from 'react' 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 { useMultichainTransactions } from 'state/transactions/hooks'
import { import {
AddLiquidityV2PoolTransactionInfo, AddLiquidityV2PoolTransactionInfo,
...@@ -22,7 +25,7 @@ import { ...@@ -22,7 +25,7 @@ import {
WrapTransactionInfo, WrapTransactionInfo,
} from 'state/transactions/types' } from 'state/transactions/types'
import { getActivityTitle } from '../constants' import { getActivityTitle, OrderTextTable } from '../constants'
import { Activity, ActivityMap } from './types' import { Activity, ActivityMap } from './types'
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined { function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
...@@ -52,12 +55,13 @@ function parseSwap( ...@@ -52,12 +55,13 @@ function parseSwap(
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens) const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
const [inputRaw, outputRaw] = const [inputRaw, outputRaw] =
swap.tradeType === TradeType.EXACT_INPUT swap.tradeType === TradeType.EXACT_INPUT
? [swap.inputCurrencyAmountRaw, swap.expectedOutputCurrencyAmountRaw] ? [swap.inputCurrencyAmountRaw, swap.settledOutputCurrencyAmountRaw ?? swap.expectedOutputCurrencyAmountRaw]
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw] : [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
return { return {
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw), descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw),
currencies: [tokenIn, tokenOut], currencies: [tokenIn, tokenOut],
prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined,
} }
} }
...@@ -134,7 +138,7 @@ function parseMigrateCreateV3( ...@@ -134,7 +138,7 @@ function parseMigrateCreateV3(
return { descriptor, currencies: [baseCurrency, quoteCurrency] } return { descriptor, currencies: [baseCurrency, quoteCurrency] }
} }
export function parseLocalActivity( export function transactionToActivity(
details: TransactionDetails, details: TransactionDetails,
chainId: ChainId, chainId: ChainId,
tokens: ChainTokenMap tokens: ChainTokenMap
...@@ -146,22 +150,13 @@ export function parseLocalActivity( ...@@ -146,22 +150,13 @@ export function parseLocalActivity(
? TransactionStatus.Confirmed ? TransactionStatus.Confirmed
: TransactionStatus.Failed : TransactionStatus.Failed
const receipt = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
...details,
status,
}
: undefined
const defaultFields = { const defaultFields = {
hash: details.hash, hash: details.hash,
chainId, chainId,
title: getActivityTitle(details.info.type, status), title: getActivityTitle(details.info.type, status),
status, status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000, timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt, from: details.from,
nonce: details.nonce, nonce: details.nonce,
} }
...@@ -192,17 +187,53 @@ export function parseLocalActivity( ...@@ -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 { export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions() const allTransactions = useMultichainTransactions()
const allSignatures = useAllSignatures()
const tokens = useAllTokensMultichain() const tokens = useAllTokensMultichain()
return useMemo(() => { return useMemo(() => {
const activityByHash: ActivityMap = {} const activityMap: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) { for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue 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 { t } from '@lingui/macro'
import { formatFiatPrice, formatNumberOrString, NumberType } from '@uniswap/conedison/format' import { formatFiatPrice, formatNumberOrString, NumberType } from '@uniswap/conedison/format'
import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESSES } from '@uniswap/sdk-core' 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 moonpayLogoSrc from 'assets/svg/moonpay.svg'
import { nativeOnChain } from 'constants/tokens' import { nativeOnChain } from 'constants/tokens'
import { import {
...@@ -10,15 +11,18 @@ import { ...@@ -10,15 +11,18 @@ import {
NftApprovalPartsFragment, NftApprovalPartsFragment,
NftApproveForAllPartsFragment, NftApproveForAllPartsFragment,
NftTransferPartsFragment, NftTransferPartsFragment,
SwapOrderDetailsPartsFragment,
TokenApprovalPartsFragment, TokenApprovalPartsFragment,
TokenAssetPartsFragment,
TokenTransferPartsFragment, TokenTransferPartsFragment,
TransactionDetailsPartsFragment,
} from 'graphql/data/__generated__/types-and-hooks' } from 'graphql/data/__generated__/types-and-hooks'
import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util' import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import ms from 'ms.macro' import ms from 'ms.macro'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { isAddress } from 'utils' import { isAddress } from 'utils'
import { MOONPAY_SENDER_ADDRESSES } from '../constants' import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
import { Activity } from './types' import { Activity } from './types'
type TransactionChanges = { type TransactionChanges = {
...@@ -74,10 +78,10 @@ function isSameAddress(a?: string, b?: string) { ...@@ -74,10 +78,10 @@ function isSameAddress(a?: string, b?: string) {
return a === b || a?.toLowerCase() === b?.toLowerCase() // Lazy-lowercases the addresses return a === b || a?.toLowerCase() === b?.toLowerCase() // Lazy-lowercases the addresses
} }
function callsPositionManagerContract(assetActivity: AssetActivityPartsFragment) { function callsPositionManagerContract(assetActivity: TransactionActivity) {
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain) const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
if (!supportedChain) return false 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 // Gets counts for number of NFTs in each collection present
...@@ -116,6 +120,20 @@ function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferP ...@@ -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 * @param transactedValue Transacted value amount from TokenTransfer API response
...@@ -145,13 +163,17 @@ function parseSwap(changes: TransactionChanges) { ...@@ -145,13 +163,17 @@ function parseSwap(changes: TransactionChanges) {
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx) const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
return { return {
title: getSwapTitle(sent, received), 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` } return { title: t`Unknown Swap` }
} }
function parseSwapOrder(changes: TransactionChanges) {
return { ...parseSwap(changes), prefixIconSrc: UniswapXBolt }
}
function parseApprove(changes: TransactionChanges) { function parseApprove(changes: TransactionChanges) {
if (changes.TokenApproval.length === 1) { if (changes.TokenApproval.length === 1) {
const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved` const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved`
...@@ -174,7 +196,10 @@ function parseLPTransfers(changes: TransactionChanges) { ...@@ -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 // 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 // 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)) { if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
...@@ -221,7 +246,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActiv ...@@ -221,7 +246,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActiv
return { title: t`Unknown Send` } return { title: t`Unknown Send` }
} }
function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) { function parseMint(changes: TransactionChanges, assetActivity: TransactionActivity) {
const collectionMap = getCollectionCounts(changes.NftTransfer) const collectionMap = getCollectionCounts(changes.NftTransfer)
if (Object.keys(collectionMap).length === 1) { if (Object.keys(collectionMap).length === 1) {
const collectionName = Object.keys(collectionMap)[0] const collectionName = Object.keys(collectionMap)[0]
...@@ -235,13 +260,14 @@ function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPart ...@@ -235,13 +260,14 @@ function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPart
return { title: t`Unknown Mint` } return { title: t`Unknown Mint` }
} }
function parseUnknown(_changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) { function parseUnknown(_changes: TransactionChanges, assetActivity: TransactionActivity) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.transaction.to.toLowerCase()] } 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 } = { const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
[ActivityType.Swap]: parseSwap, [ActivityType.Swap]: parseSwap,
[ActivityType.SwapOrder]: parseSwapOrder,
[ActivityType.Approve]: parseApprove, [ActivityType.Approve]: parseApprove,
[ActivityType.Send]: parseSendReceive, [ActivityType.Send]: parseSendReceive,
[ActivityType.Receive]: parseSendReceive, [ActivityType.Receive]: parseSendReceive,
...@@ -262,8 +288,47 @@ function getLogoSrcs(changes: TransactionChanges): string[] { ...@@ -262,8 +288,47 @@ function getLogoSrcs(changes: TransactionChanges): string[] {
return Array.from(logoSet).filter(Boolean) as 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 { function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
try { try {
if (assetActivity.details.__typename === 'SwapOrderDetails') {
return parseUniswapXOrder(assetActivity as OrderActivity)
}
const changes = assetActivity.assetChanges.reduce( const changes = assetActivity.assetChanges.reduce(
(acc: TransactionChanges, assetChange) => { (acc: TransactionChanges, assetChange) => {
if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange) if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange)
...@@ -285,18 +350,18 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit ...@@ -285,18 +350,18 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
return undefined return undefined
} }
const defaultFields = { const defaultFields = {
hash: assetActivity.transaction.hash, hash: assetActivity.details.hash,
chainId: supportedChain, chainId: supportedChain,
status: assetActivity.transaction.status, status: assetActivity.details.status,
timestamp: assetActivity.timestamp, timestamp: assetActivity.timestamp,
logos: getLogoSrcs(changes), logos: getLogoSrcs(changes),
title: assetActivity.type, title: assetActivity.type,
descriptor: assetActivity.transaction.to, descriptor: assetActivity.details.to,
receipt: assetActivity.transaction, from: assetActivity.details.from,
nonce: assetActivity.transaction.nonce, nonce: assetActivity.details.nonce,
} }
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity)
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity as TransactionActivity)
return { ...defaultFields, ...parsedFields } return { ...defaultFields, ...parsedFields }
} catch (e) { } catch (e) {
console.error('Failed to parse activity', e, assetActivity) console.error('Failed to parse activity', e, assetActivity)
......
import { ChainId, Currency } from '@uniswap/sdk-core' import { ChainId, Currency } from '@uniswap/sdk-core'
import { AssetActivityPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
type Receipt = AssetActivityPartsFragment['transaction']
export type Activity = { export type Activity = {
hash: string hash: string
chainId: ChainId chainId: ChainId
status: TransactionStatus 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 timestamp: number
title: string title: string
descriptor?: string descriptor?: string
logos?: Array<string | undefined> logos?: Array<string | undefined>
currencies?: Array<Currency | undefined> currencies?: Array<Currency | undefined>
otherAccount?: string otherAccount?: string
receipt?: Omit<Receipt, 'nonce'> from: string
nonce?: number | null 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 { 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' import { TransactionType } from 'state/transactions/types'
// use even number because rows are in groups of 2 // use even number because rows are in groups of 2
...@@ -159,6 +160,38 @@ export function getActivityTitle(type: TransactionType, status: TransactionStatu ...@@ -159,6 +160,38 @@ export function getActivityTitle(type: TransactionType, status: TransactionStatu
return TransactionTitleTable[type][status] 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 // Non-exhaustive list of addresses Moonpay uses when sending purchased tokens
export const MOONPAY_SENDER_ADDRESSES = [ export const MOONPAY_SENDER_ADDRESSES = [
'0x8216874887415e2650d12d53ff53516f04a74fd7', '0x8216874887415e2650d12d53ff53516f04a74fd7',
...@@ -166,3 +199,11 @@ export const MOONPAY_SENDER_ADDRESSES = [ ...@@ -166,3 +199,11 @@ export const MOONPAY_SENDER_ADDRESSES = [
'0xb287eac48ab21c5fb1d3723830d60b4c797555b0', '0xb287eac48ab21c5fb1d3723830d60b4c797555b0',
'0xd108fd0e8c8e71552a167e7a44ff1d345d233ba6', '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 ...@@ -21,7 +21,6 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
velocity: 0.01, velocity: 0.01,
}, },
}) })
return ( return (
<animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}> <animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}>
<div ref={ref}>{children}</div> <div ref={ref}>{children}</div>
......
...@@ -2,6 +2,8 @@ import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'fe ...@@ -2,6 +2,8 @@ import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'fe
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails' import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
import { useRoutingAPIForPriceFlag } from 'featureFlags/flags/priceRoutingApi' import { useRoutingAPIForPriceFlag } from 'featureFlags/flags/priceRoutingApi'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' 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 { useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react' import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
import { X } from 'react-feather' import { X } from 'react-feather'
...@@ -208,11 +210,23 @@ export default function FeatureFlagModal() { ...@@ -208,11 +210,23 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.detailsV2} featureFlag={FeatureFlag.detailsV2}
label="Use the new details page for nfts" 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 <FeatureFlagOption
variant={BaseVariant} variant={BaseVariant}
value={useRoutingAPIForPriceFlag()} value={useRoutingAPIForPriceFlag()}
featureFlag={FeatureFlag.routingAPIPrice} 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"> <FeatureFlagGroup name="Debug">
<FeatureFlagOption <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` ...@@ -20,7 +20,7 @@ const ReferenceElement = styled.div`
height: inherit; height: inherit;
` `
const Arrow = styled.div` export const Arrow = styled.div`
width: 8px; width: 8px;
height: 8px; height: 8px;
z-index: 9998; 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 { useEffect } from 'react'
import { X } from 'react-feather'
import styled, { css, useTheme } from 'styled-components/macro'
import { useRemovePopup } from '../../state/application/hooks' import { useRemovePopup } from '../../state/application/hooks'
import { PopupContent } from '../../state/application/reducer' import { PopupContent, PopupType } from '../../state/application/reducer'
import FailedNetworkSwitchPopup from './FailedNetworkSwitchPopup' import { FailedNetworkSwitchPopup, TransactionPopupContent, UniswapXOrderPopupContent } from './PopupContent'
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;
`
export default function PopupItem({ export default function PopupItem({
removeAfterMs, removeAfterMs,
...@@ -56,7 +15,7 @@ export default function PopupItem({ ...@@ -56,7 +15,7 @@ export default function PopupItem({
popKey: string popKey: string
}) { }) {
const removePopup = useRemovePopup() const removePopup = useRemovePopup()
const theme = useTheme() const onClose = () => removePopup(popKey)
useEffect(() => { useEffect(() => {
if (removeAfterMs === null) return undefined if (removeAfterMs === null) return undefined
...@@ -70,20 +29,17 @@ export default function PopupItem({ ...@@ -70,20 +29,17 @@ export default function PopupItem({
} }
}, [popKey, removeAfterMs, removePopup]) }, [popKey, removeAfterMs, removePopup])
if ('txn' in content) { const { chainId } = useWeb3React()
return (
<TransactionPopupContainer show={true}> switch (content.type) {
<StyledClose $padding={16} color={theme.textSecondary} onClick={() => removePopup(popKey)} /> case PopupType.Transaction: {
<TransactionPopup hash={content.txn.hash} /> return chainId ? <TransactionPopupContent hash={content.hash} chainId={chainId} onClose={onClose} /> : null
</TransactionPopupContainer> }
) case PopupType.Order: {
} else if ('failedSwitchNetwork' in content) { return <UniswapXOrderPopupContent orderHash={content.orderHash} onClose={onClose} />
return ( }
<FailedSwitchNetworkPopupContainer show={true}> case PopupType.FailedSwitchNetwork: {
<StyledClose $padding={20} color={theme.textSecondary} onClick={() => removePopup(popKey)} /> return <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} onClose={onClose} />
<FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} /> }
</FailedSwitchNetworkPopupContainer>
)
} }
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 { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column' import Column from 'components/Column'
import Radio from 'components/Radio' import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { RowBetween, RowFixed } from 'components/Row' import { RowBetween, RowFixed } from 'components/Row'
import Toggle from 'components/Toggle' 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 { RouterPreference } from 'state/routing/slice'
import { useRouterPreference } from 'state/user/hooks' import { useRouterPreference } from 'state/user/hooks'
import { updateDisabledUniswapX } from 'state/user/reducer'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { Divider, ExternalLink, ThemedText } from 'theme'
const Preference = styled(Radio)` const InlineLink = styled(ThemedText.Caption)`
background-color: ${({ theme }) => theme.backgroundModule}; color: ${({ theme }) => theme.accentAction};
padding: 12px 16px; display: inline;
` cursor: pointer;
&:hover {
const PreferencesContainer = styled(Column)` opacity: 0.8;
gap: 1.5px; }
border-radius: 12px;
overflow: hidden;
` `
export default function RouterPreferenceSettings() { export default function RouterPreferenceSettings() {
const { chainId } = useWeb3React()
const [routerPreference, setRouterPreference] = useRouterPreference() const [routerPreference, setRouterPreference] = useRouterPreference()
const uniswapXEnabled = useUniswapXEnabled() && chainId && isUniswapXSupportedChain(chainId)
const isAutoRoutingActive = routerPreference === RouterPreference.AUTO const dispatch = useAppDispatch()
return ( 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"> <RowBetween gap="sm">
<RowFixed> <RowFixed>
<Column gap="xs"> <Column gap="xs">
<ThemedText.BodySecondary> <ThemedText.BodySecondary>
<Trans>Auto Router API</Trans> <Trans>Local routing</Trans>
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
<ThemedText.Caption color="textSecondary">
<Trans>Use the Uniswap Labs API to get faster quotes.</Trans>
</ThemedText.Caption>
</Column> </Column>
</RowFixed> </RowFixed>
<Toggle <Toggle
id="toggle-optimized-router-button" id="toggle-local-routing-button"
isActive={isAutoRoutingActive} isActive={routerPreference === RouterPreference.CLIENT}
toggle={() => setRouterPreference(isAutoRoutingActive ? RouterPreference.API : RouterPreference.AUTO)} toggle={() =>
setRouterPreference(
routerPreference === RouterPreference.CLIENT ? RouterPreference.API : RouterPreference.CLIENT
)
}
/> />
</RowBetween> </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 { Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { isSupportedChain, L2_CHAIN_IDS } from 'constants/chains' import { isSupportedChain, L2_CHAIN_IDS } from 'constants/chains'
import useDisableScrolling from 'hooks/useDisableScrolling' import useDisableScrolling from 'hooks/useDisableScrolling'
...@@ -7,6 +8,8 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside' ...@@ -7,6 +8,8 @@ import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useRef } from 'react' import { useRef } from 'react'
import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks' import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' 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 styled from 'styled-components/macro'
import { Divider } from 'theme' import { Divider } from 'theme'
...@@ -36,11 +39,23 @@ const MenuFlyout = styled(AutoColumn)` ...@@ -36,11 +39,23 @@ const MenuFlyout = styled(AutoColumn)`
min-width: 18.125rem; min-width: 18.125rem;
`}; `};
user-select: none; user-select: none;
padding: 16px;
`
const ExpandColumn = styled(AutoColumn)`
gap: 16px; 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 { chainId: connectedChainId } = useWeb3React()
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId)) const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
...@@ -59,15 +74,21 @@ export default function SettingsTab({ autoSlippage, chainId }: { autoSlippage: P ...@@ -59,15 +74,21 @@ export default function SettingsTab({ autoSlippage, chainId }: { autoSlippage: P
<MenuButton disabled={!isChainSupported || chainId !== connectedChainId} isActive={isOpen} onClick={toggleMenu} /> <MenuButton disabled={!isChainSupported || chainId !== connectedChainId} isActive={isOpen} onClick={toggleMenu} />
{isOpen && ( {isOpen && (
<MenuFlyout> <MenuFlyout>
<RouterPreferenceSettings /> <AutoColumn gap="16px">
<Divider /> <RouterPreferenceSettings />
<MaxSlippageSettings autoSlippage={autoSlippage} /> </AutoColumn>
{showDeadlineSettings && ( <AnimatedDropdown open={!isUniswapXTrade(trade)}>
<> <ExpandColumn>
<Divider /> <Divider />
<TransactionDeadlineSettings /> <MaxSlippageSettings autoSlippage={autoSlippage} />
</> {showDeadlineSettings && (
)} <>
<Divider />
<TransactionDeadlineSettings />
</>
)}
</ExpandColumn>
</AnimatedDropdown>
</MenuFlyout> </MenuFlyout>
)} )}
</Menu> </Menu>
......
...@@ -78,7 +78,7 @@ export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) { ...@@ -78,7 +78,7 @@ export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
} }
return ( 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} /> <ToggleElement isActive={isActive} bgColor={bgColor} isInitialToggleLoad={isInitialToggleLoad} />
</Wrapper> </Wrapper>
) )
......
...@@ -6,12 +6,15 @@ import noop from 'utils/noop' ...@@ -6,12 +6,15 @@ import noop from 'utils/noop'
import Popover, { PopoverProps } from '../Popover' import Popover, { PopoverProps } from '../Popover'
export enum TooltipSize { export enum TooltipSize {
ExtraSmall = '200px',
Small = '256px', Small = '256px',
Large = '400px', Large = '400px',
} }
const getPaddingForSize = (size: TooltipSize) => { const getPaddingForSize = (size: TooltipSize) => {
switch (size) { switch (size) {
case TooltipSize.ExtraSmall:
return '8px'
case TooltipSize.Small: case TooltipSize.Small:
return '12px' return '12px'
case TooltipSize.Large: case TooltipSize.Large:
......
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { OffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import UniwalletModal from 'components/AccountDrawer/UniwalletModal' import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner' import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner'
import AddressClaimModal from 'components/claim/AddressClaimModal' import AddressClaimModal from 'components/claim/AddressClaimModal'
...@@ -28,6 +29,7 @@ export default function TopLevelModals() { ...@@ -28,6 +29,7 @@ export default function TopLevelModals() {
<Bag /> <Bag />
<UniwalletModal /> <UniwalletModal />
<UniswapWalletBanner /> <UniswapWalletBanner />
<OffchainActivityModal />
<TransactionCompleteModal /> <TransactionCompleteModal />
<AirdropModal /> <AirdropModal />
<FiatOnrampModal /> <FiatOnrampModal />
......
...@@ -14,6 +14,7 @@ import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable' ...@@ -14,6 +14,7 @@ import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { darken } from 'polished' import { darken } from 'polished'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useAppSelector } from 'state/hooks' import { useAppSelector } from 'state/hooks'
import { usePendingOrders } from 'state/signatures/hooks'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { colors } from 'theme/colors' import { colors } from 'theme/colors'
import { flexRowNoWrap } from 'theme/styles' import { flexRowNoWrap } from 'theme/styles'
...@@ -151,9 +152,11 @@ function Web3StatusInner() { ...@@ -151,9 +152,11 @@ function Web3StatusInner() {
return txs.filter(isTransactionRecent).sort(newTransactionsFirst) return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
}, [allTransactions]) }, [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) { if (account) {
return ( return (
...@@ -166,16 +169,16 @@ function Web3StatusInner() { ...@@ -166,16 +169,16 @@ function Web3StatusInner() {
disabled={Boolean(switchingChain)} disabled={Boolean(switchingChain)}
data-testid="web3-status-connected" data-testid="web3-status-connected"
onClick={handleWalletDropdownClick} onClick={handleWalletDropdownClick}
pending={hasPendingTransactions} pending={hasPendingActivity}
isClaimAvailable={isClaimAvailable} isClaimAvailable={isClaimAvailable}
> >
{!hasPendingTransactions && ( {!hasPendingActivity && (
<StatusIcon size={24} account={account} connection={connection} showMiniIcons={false} /> <StatusIcon account={account} size={24} connection={connection} showMiniIcons={false} />
)} )}
{hasPendingTransactions ? ( {hasPendingActivity ? (
<RowBetween> <RowBetween>
<Text> <Text>
<Trans>{pending?.length} Pending</Trans> <Trans>{pendingTxs.length + pendingOrders.length} Pending</Trans>
</Text>{' '} </Text>{' '}
<Loader stroke="white" /> <Loader stroke="white" />
</RowBetween> </RowBetween>
......
...@@ -21,7 +21,7 @@ describe('AdvancedSwapDetails.tsx', () => { ...@@ -21,7 +21,7 @@ describe('AdvancedSwapDetails.tsx', () => {
}) })
it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => { 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} />) render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
await act(() => userEvent.hover(screen.getByText(/Maximum input/i))) await act(() => userEvent.hover(screen.getByText(/Maximum input/i)))
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible() 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 { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' 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 { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { LoadingRows } from 'components/Loader/styled' import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
import { Separator, ThemedText } from '../../theme' import { Separator, ThemedText } from '../../theme'
import Column from '../Column' import Column from '../Column'
import RouterLabel from '../RouterLabel'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip, TooltipSize } from '../Tooltip' import { MouseoverTooltip, TooltipSize } from '../Tooltip'
import RouterLabel from './RouterLabel' import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import SwapRoute from './SwapRoute' import SwapRoute from './SwapRoute'
interface AdvancedSwapDetailsProps { interface AdvancedSwapDetailsProps {
...@@ -43,11 +45,14 @@ function TextWithLoadingPlaceholder({ ...@@ -43,11 +45,14 @@ function TextWithLoadingPlaceholder({
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) { export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId) const nativeCurrency = useNativeCurrency(chainId)
const txCount = getTransactionCount(trade)
const supportsGasEstimate = chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
return ( return (
<Column gap="md"> <Column gap="md">
<Separator /> <Separator />
{!trade.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : ( {supportsGasEstimate && (
<RowBetween> <RowBetween>
<MouseoverTooltip <MouseoverTooltip
text={ text={
...@@ -57,24 +62,37 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: ...@@ -57,24 +62,37 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
} }
> >
<ThemedText.BodySmall color="textSecondary"> <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> </ThemedText.BodySmall>
</MouseoverTooltip> </MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}> <TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.BodySmall>~{formatUSDPrice(trade.gasUseEstimateUSD)}</ThemedText.BodySmall> <ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
</TextWithLoadingPlaceholder> </TextWithLoadingPlaceholder>
</RowBetween> </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> <RowBetween>
<RowFixed> <RowFixed>
<MouseoverTooltip <MouseoverTooltip
...@@ -128,17 +146,32 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: ...@@ -128,17 +146,32 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
<ThemedText.BodySmall color="textSecondary"> <ThemedText.BodySmall color="textSecondary">
<Trans>Order routing</Trans> <Trans>Order routing</Trans>
</ThemedText.BodySmall> </ThemedText.BodySmall>
<MouseoverTooltip {isClassicTrade(trade) ? (
size={TooltipSize.Large} <MouseoverTooltip
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />} size={TooltipSize.Large}
onOpen={() => { text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, { onOpen={() => {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW, sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
}) element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
}} })
> }}
<RouterLabel /> >
</MouseoverTooltip> <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> </RowBetween>
</Column> </Column>
) )
......
...@@ -6,7 +6,8 @@ import { ...@@ -6,7 +6,8 @@ import {
SwapEventName, SwapEventName,
SwapPriceUpdateUserResponse, SwapPriceUpdateUserResponse,
} from '@uniswap/analytics-events' } 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 { useWeb3React } from '@web3-react/core'
import Badge from 'components/Badge' import Badge from 'components/Badge'
import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal' import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal'
...@@ -16,9 +17,14 @@ import { USDT as USDT_MAINNET } from 'constants/tokens' ...@@ -16,9 +17,14 @@ import { USDT as USDT_MAINNET } from 'constants/tokens'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn' import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance' import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious' 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 { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useCallback, useEffect, useState } from 'react' 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 styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
...@@ -35,6 +41,7 @@ import SwapModalHeader from './SwapModalHeader' ...@@ -35,6 +41,7 @@ import SwapModalHeader from './SwapModalHeader'
export enum ConfirmModalState { export enum ConfirmModalState {
REVIEWING, REVIEWING,
WRAPPING,
RESETTING_USDT, RESETTING_USDT,
APPROVING_TOKEN, APPROVING_TOKEN,
PERMITTING, PERMITTING,
...@@ -64,12 +71,14 @@ function useConfirmModalState({ ...@@ -64,12 +71,14 @@ function useConfirmModalState({
onSwap, onSwap,
allowance, allowance,
doesTradeDiffer, doesTradeDiffer,
onCurrencySelection,
}: { }: {
trade: InterfaceTrade trade: InterfaceTrade
allowedSlippage: Percent allowedSlippage: Percent
onSwap: () => void onSwap: () => void
allowance: Allowance allowance: Allowance
doesTradeDiffer: boolean doesTradeDiffer: boolean
onCurrencySelection: (field: Field, currency: Currency) => void
}) { }) {
const [confirmModalState, setConfirmModalState] = useState<ConfirmModalState>(ConfirmModalState.REVIEWING) const [confirmModalState, setConfirmModalState] = useState<ConfirmModalState>(ConfirmModalState.REVIEWING)
const [approvalError, setApprovalError] = useState<PendingModalError>() const [approvalError, setApprovalError] = useState<PendingModalError>()
...@@ -80,6 +89,9 @@ function useConfirmModalState({ ...@@ -80,6 +89,9 @@ function useConfirmModalState({
// at the bottom of the modal, even after they complete steps 1 and 2. // at the bottom of the modal, even after they complete steps 1 and 2.
const generateRequiredSteps = useCallback(() => { const generateRequiredSteps = useCallback(() => {
const steps: PendingConfirmModalState[] = [] 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). // 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 // See the `approve` function here: https://etherscan.io/address/0xdAC17F958D2ee523a2206206994597C13D831ec7#code
if ( if (
...@@ -97,12 +109,22 @@ function useConfirmModalState({ ...@@ -97,12 +109,22 @@ function useConfirmModalState({
} }
steps.push(ConfirmModalState.PENDING_CONFIRMATION) steps.push(ConfirmModalState.PENDING_CONFIRMATION)
return steps return steps
}, [allowance]) }, [allowance, trade])
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const trace = useTrace() const trace = useTrace()
const maximumAmountIn = useMaxAmountIn(trade, allowedSlippage) 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) => { const catchUserReject = async (e: any, errorType: PendingModalError) => {
setConfirmModalState(ConfirmModalState.REVIEWING) setConfirmModalState(ConfirmModalState.REVIEWING)
if (didUserReject(e)) return if (didUserReject(e)) return
...@@ -113,6 +135,24 @@ function useConfirmModalState({ ...@@ -113,6 +135,24 @@ function useConfirmModalState({
const performStep = useCallback( const performStep = useCallback(
async (step: ConfirmModalState) => { async (step: ConfirmModalState) => {
switch (step) { 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: case ConfirmModalState.RESETTING_USDT:
setConfirmModalState(ConfirmModalState.RESETTING_USDT) setConfirmModalState(ConfirmModalState.RESETTING_USDT)
invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required') invariant(allowance.state === AllowanceState.REQUIRED, 'Allowance should be required')
...@@ -151,7 +191,17 @@ function useConfirmModalState({ ...@@ -151,7 +191,17 @@ function useConfirmModalState({
break 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(() => { const startSwapFlow = useCallback(() => {
...@@ -163,6 +213,15 @@ function useConfirmModalState({ ...@@ -163,6 +213,15 @@ function useConfirmModalState({
const previousSetupApprovalNeeded = usePrevious( const previousSetupApprovalNeeded = usePrevious(
allowance.state === AllowanceState.REQUIRED ? allowance.needsSetupApproval : undefined 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(() => { useEffect(() => {
if ( if (
allowance.state === AllowanceState.REQUIRED && allowance.state === AllowanceState.REQUIRED &&
...@@ -202,45 +261,51 @@ function useConfirmModalState({ ...@@ -202,45 +261,51 @@ function useConfirmModalState({
setApprovalError(undefined) setApprovalError(undefined)
} }
return { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps } return { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps, wrapTxHash }
} }
export default function ConfirmSwapModal({ export default function ConfirmSwapModal({
trade, trade,
inputCurrency,
originalTrade, originalTrade,
onAcceptChanges, onAcceptChanges,
allowedSlippage, allowedSlippage,
allowance, allowance,
onConfirm, onConfirm,
onDismiss, onDismiss,
onCurrencySelection,
swapError, swapError,
txHash, swapResult,
swapQuoteReceivedDate, swapQuoteReceivedDate,
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
}: { }: {
trade: InterfaceTrade trade: InterfaceTrade
inputCurrency?: Currency
originalTrade?: InterfaceTrade originalTrade?: InterfaceTrade
txHash?: string swapResult?: SwapResult
allowedSlippage: Percent allowedSlippage: Percent
allowance: Allowance allowance: Allowance
onAcceptChanges: () => void onAcceptChanges: () => void
onConfirm: () => void onConfirm: () => void
swapError?: Error swapError?: Error
onDismiss: () => void onDismiss: () => void
onCurrencySelection: (field: Field, currency: Currency) => void
swapQuoteReceivedDate?: Date swapQuoteReceivedDate?: Date
fiatValueInput: { data?: number; isLoading: boolean } fiatValueInput: { data?: number; isLoading: boolean }
fiatValueOutput: { data?: number; isLoading: boolean } fiatValueOutput: { data?: number; isLoading: boolean }
}) { }) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const doesTradeDiffer = originalTrade && tradeMeaningfullyDiffers(trade, originalTrade, allowedSlippage) const doesTradeDiffer = originalTrade && tradeMeaningfullyDiffers(trade, originalTrade, allowedSlippage)
const { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps } = useConfirmModalState({ const { startSwapFlow, onCancel, confirmModalState, approvalError, pendingModalSteps, wrapTxHash } =
trade, useConfirmModalState({
allowedSlippage, trade,
onSwap: onConfirm, allowedSlippage,
allowance, onSwap: onConfirm,
doesTradeDiffer: Boolean(doesTradeDiffer), onCurrencySelection,
}) allowance,
doesTradeDiffer: Boolean(doesTradeDiffer),
})
const swapFailed = Boolean(swapError) && !didUserReject(swapError) const swapFailed = Boolean(swapError) && !didUserReject(swapError)
useEffect(() => { useEffect(() => {
...@@ -282,8 +347,8 @@ export default function ConfirmSwapModal({ ...@@ -282,8 +347,8 @@ export default function ConfirmSwapModal({
if (confirmModalState !== ConfirmModalState.REVIEWING && !showAcceptChanges) { if (confirmModalState !== ConfirmModalState.REVIEWING && !showAcceptChanges) {
return null return null
} }
return <SwapModalHeader trade={trade} allowedSlippage={allowedSlippage} /> return <SwapModalHeader inputCurrency={inputCurrency} trade={trade} allowedSlippage={allowedSlippage} />
}, [allowedSlippage, confirmModalState, showAcceptChanges, trade]) }, [allowedSlippage, confirmModalState, showAcceptChanges, trade, inputCurrency])
const modalBottom = useCallback(() => { const modalBottom = useCallback(() => {
if (confirmModalState === ConfirmModalState.REVIEWING || showAcceptChanges) { if (confirmModalState === ConfirmModalState.REVIEWING || showAcceptChanges) {
...@@ -291,7 +356,7 @@ export default function ConfirmSwapModal({ ...@@ -291,7 +356,7 @@ export default function ConfirmSwapModal({
<SwapModalFooter <SwapModalFooter
onConfirm={startSwapFlow} onConfirm={startSwapFlow}
trade={trade} trade={trade}
hash={txHash} swapResult={swapResult}
allowedSlippage={allowedSlippage} allowedSlippage={allowedSlippage}
disabledConfirm={showAcceptChanges} disabledConfirm={showAcceptChanges}
swapQuoteReceivedDate={swapQuoteReceivedDate} swapQuoteReceivedDate={swapQuoteReceivedDate}
...@@ -309,7 +374,8 @@ export default function ConfirmSwapModal({ ...@@ -309,7 +374,8 @@ export default function ConfirmSwapModal({
steps={pendingModalSteps} steps={pendingModalSteps}
currentStep={confirmModalState} currentStep={confirmModalState}
trade={trade} trade={trade}
swapTxHash={txHash} swapResult={swapResult}
wrapTxHash={wrapTxHash}
tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending} tokenApprovalPending={allowance.state === AllowanceState.REQUIRED && allowance.isApprovalPending}
revocationPending={allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending} revocationPending={allowance.state === AllowanceState.REQUIRED && allowance.isRevocationPending}
/> />
...@@ -319,7 +385,8 @@ export default function ConfirmSwapModal({ ...@@ -319,7 +385,8 @@ export default function ConfirmSwapModal({
showAcceptChanges, showAcceptChanges,
pendingModalSteps, pendingModalSteps,
trade, trade,
txHash, swapResult,
wrapTxHash,
allowance, allowance,
allowedSlippage, allowedSlippage,
swapQuoteReceivedDate, 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 { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' 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 { 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 { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { isUniswapXTrade } from 'state/routing/utils'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg' import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg'
import SwapRoute from './SwapRoute' import { GasBreakdownTooltip } from './GasBreakdownTooltip'
const StyledGasIcon = styled(GasIcon)` const StyledGasIcon = styled(GasIcon)`
margin-right: 4px;
height: 18px; height: 18px;
// We apply the following to all children of the SVG in order to override the default color // 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)` ...@@ -20,41 +24,37 @@ const StyledGasIcon = styled(GasIcon)`
} }
` `
export default function GasEstimateTooltip({ export default function GasEstimateTooltip({ trade, loading }: { trade?: InterfaceTrade; loading: boolean }) {
trade, const { chainId } = useWeb3React()
loading,
disabled, if (!trade || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)) {
}: { return null
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
return ( return (
<MouseoverTooltip <MouseoverTooltip
disabled={disabled} size={TooltipSize.Small}
size={TooltipSize.Large} text={<GasBreakdownTooltip trade={trade} />}
// 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} />}
onOpen={() => { onOpen={() => {
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, { sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW, element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
}) })
}} }}
placement="bottom" placement="right"
> >
<LoadingOpacityContainer $loading={loading}> <LoadingOpacityContainer $loading={loading}>
<RowFixed> <RowFixed gap="xs">
<StyledGasIcon /> {isUniswapXTrade(trade) ? <UniswapXRouterIcon /> : <StyledGasIcon />}
<ThemedText.BodySmall color="textSecondary">{formattedGasPriceString}</ThemedText.BodySmall> <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> </RowFixed>
</LoadingOpacityContainer> </LoadingOpacityContainer>
</MouseoverTooltip> </MouseoverTooltip>
......
...@@ -13,6 +13,7 @@ export enum PendingModalError { ...@@ -13,6 +13,7 @@ export enum PendingModalError {
TOKEN_APPROVAL_ERROR, TOKEN_APPROVAL_ERROR,
PERMIT_ERROR, PERMIT_ERROR,
CONFIRMATION_ERROR, CONFIRMATION_ERROR,
WRAP_ERROR,
} }
interface ErrorModalContentProps { interface ErrorModalContentProps {
...@@ -45,6 +46,10 @@ function getErrorContent(errorType: PendingModalError) { ...@@ -45,6 +46,10 @@ function getErrorContent(errorType: PendingModalError) {
return { return {
title: <Trans>Swap failed</Trans>, 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 }>` ...@@ -59,7 +59,7 @@ const FadeWrapper = styled.div<{ $scale: boolean }>`
} }
` `
function FadePresence({ export function FadePresence({
children, children,
className, className,
$scale = false, $scale = false,
......
...@@ -6,7 +6,7 @@ import { InterfaceTrade } from 'state/routing/types' ...@@ -6,7 +6,7 @@ import { InterfaceTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro' import { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
export function TradeSummary({ trade }: { trade: InterfaceTrade }) { export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> }) {
const theme = useTheme() const theme = useTheme()
return ( return (
<Row gap="sm" justify="center" align="center"> <Row gap="sm" justify="center" align="center">
......
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core' import { ChainId, Currency } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { OrderContent } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal'
import { ColumnCenter } from 'components/Column' import { ColumnCenter } from 'components/Column'
import Column from 'components/Column' import Column from 'components/Column'
import Row from 'components/Row' import Row from 'components/Row'
import { SwapResult } from 'hooks/useSwapCallback'
import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation' import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { ReactNode, useRef } from 'react' 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 { useIsTransactionConfirmed } from 'state/transactions/hooks'
import styled, { css, keyframes } from 'styled-components/macro' import styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink } from 'theme' import { ExternalLink } from 'theme'
...@@ -90,6 +94,7 @@ export type PendingConfirmModalState = Extract< ...@@ -90,6 +94,7 @@ export type PendingConfirmModalState = Extract<
| ConfirmModalState.APPROVING_TOKEN | ConfirmModalState.APPROVING_TOKEN
| ConfirmModalState.PERMITTING | ConfirmModalState.PERMITTING
| ConfirmModalState.PENDING_CONFIRMATION | ConfirmModalState.PENDING_CONFIRMATION
| ConfirmModalState.WRAPPING
| ConfirmModalState.RESETTING_USDT | ConfirmModalState.RESETTING_USDT
> >
...@@ -105,7 +110,8 @@ interface PendingModalContentProps { ...@@ -105,7 +110,8 @@ interface PendingModalContentProps {
steps: PendingConfirmModalState[] steps: PendingConfirmModalState[]
currentStep: PendingConfirmModalState currentStep: PendingConfirmModalState
trade?: InterfaceTrade trade?: InterfaceTrade
swapTxHash?: string swapResult?: SwapResult
wrapTxHash?: string
hideStepIndicators?: boolean hideStepIndicators?: boolean
tokenApprovalPending?: boolean tokenApprovalPending?: boolean
revocationPending?: boolean revocationPending?: boolean
...@@ -117,25 +123,39 @@ interface ContentArgs { ...@@ -117,25 +123,39 @@ interface ContentArgs {
trade?: InterfaceTrade trade?: InterfaceTrade
swapConfirmed: boolean swapConfirmed: boolean
swapPending: boolean swapPending: boolean
wrapPending: boolean
tokenApprovalPending: boolean tokenApprovalPending: boolean
revocationPending: boolean revocationPending: boolean
swapTxHash?: string swapResult?: SwapResult
chainId?: number chainId?: number
order?: UniswapXOrderDetails
} }
function getContent(args: ContentArgs): PendingModalStep { function getContent(args: ContentArgs): PendingModalStep {
const { const {
step, step,
wrapPending,
approvalCurrency, approvalCurrency,
swapConfirmed, swapConfirmed,
swapPending, swapPending,
tokenApprovalPending, tokenApprovalPending,
revocationPending, revocationPending,
trade, trade,
swapTxHash, swapResult,
chainId, chainId,
} = args } = args
switch (step) { 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: case ConfirmModalState.RESETTING_USDT:
return { return {
title: t`Reset USDT`, title: t`Reset USDT`,
...@@ -162,22 +182,32 @@ function getContent(args: ContentArgs): PendingModalStep { ...@@ -162,22 +182,32 @@ function getContent(args: ContentArgs): PendingModalStep {
), ),
label: t`Proceed in your wallet`, 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 { 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, subtitle: trade ? <TradeSummary trade={trade} /> : null,
label: label: href ? (
swapConfirmed && swapTxHash && chainId ? ( <ExternalLink href={href} color="textSecondary">
<ExternalLink {labelText}
href={getExplorerLink(chainId, swapTxHash, ExplorerDataType.TRANSACTION)} </ExternalLink>
color="textSecondary" ) : (
> labelText
<Trans>View on Explorer</Trans> ),
</ExternalLink>
) : !swapPending ? (
t`Proceed in your wallet`
) : null,
} }
}
} }
} }
...@@ -185,25 +215,41 @@ export function PendingModalContent({ ...@@ -185,25 +215,41 @@ export function PendingModalContent({
steps, steps,
currentStep, currentStep,
trade, trade,
swapTxHash, swapResult,
wrapTxHash,
hideStepIndicators, hideStepIndicators,
tokenApprovalPending = false, tokenApprovalPending = false,
revocationPending = false, revocationPending = false,
}: PendingModalContentProps) { }: PendingModalContentProps) {
const { chainId } = useWeb3React() 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({ const { label, button } = getContent({
step: currentStep, step: currentStep,
approvalCurrency: trade?.inputAmount.currency, approvalCurrency: trade?.inputAmount.currency,
swapConfirmed, swapConfirmed,
swapPending, swapPending,
wrapPending,
tokenApprovalPending, tokenApprovalPending,
revocationPending, revocationPending,
swapTxHash, swapResult,
trade, trade,
chainId, chainId,
}) })
const order = useOrder(swapResult?.type === TradeFillType.UniswapX ? swapResult.response.orderHash : '')
const currentStepContainerRef = useRef<HTMLDivElement>(null) const currentStepContainerRef = useRef<HTMLDivElement>(null)
useUnmountingAnimation(currentStepContainerRef, () => AnimationType.EXITING) useUnmountingAnimation(currentStepContainerRef, () => AnimationType.EXITING)
...@@ -211,6 +257,11 @@ export function PendingModalContent({ ...@@ -211,6 +257,11 @@ export function PendingModalContent({
return null 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. // 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) const showSuccess = swapConfirmed || (swapPending && chainId === ChainId.MAINNET)
...@@ -235,9 +286,13 @@ export function PendingModalContent({ ...@@ -235,9 +286,13 @@ export function PendingModalContent({
{/* Scales in for the final step if the swap is pending user signature or onchain confirmation. */} {/* Scales in for the final step if the swap is pending user signature or onchain confirmation. */}
{((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) || {((currentStep === ConfirmModalState.PENDING_CONFIRMATION && !showSuccess) ||
tokenApprovalPending || tokenApprovalPending ||
wrapPending ||
revocationPending) && <LoadingIndicatorOverlay />} revocationPending) && <LoadingIndicatorOverlay />}
</LogoContainer> </LogoContainer>
<HeaderContainer gap="md" $disabled={revocationPending || tokenApprovalPending || (swapPending && !showSuccess)}> <HeaderContainer
gap="md"
$disabled={revocationPending || tokenApprovalPending || wrapPending || (swapPending && !showSuccess)}
>
<AnimationWrapper> <AnimationWrapper>
{steps.map((step) => { {steps.map((step) => {
const { title, subtitle } = getContent({ const { title, subtitle } = getContent({
...@@ -245,9 +300,10 @@ export function PendingModalContent({ ...@@ -245,9 +300,10 @@ export function PendingModalContent({
approvalCurrency: trade?.inputAmount.currency, approvalCurrency: trade?.inputAmount.currency,
swapConfirmed, swapConfirmed,
swapPending, swapPending,
tokenApprovalPending, wrapPending,
revocationPending, revocationPending,
swapTxHash, tokenApprovalPending,
swapResult,
trade, trade,
}) })
// We only render one step at a time, but looping through the array allows us to keep // 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', () => { ...@@ -26,7 +26,7 @@ describe.skip('SwapDetailsDropdown.tsx', () => {
}) })
it('is interactive once loaded', async () => { it('is interactive once loaded', async () => {
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = '1.00' TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = 1.0
render( render(
<SwapDetailsDropdown <SwapDetailsDropdown
trade={TEST_TRADE_EXACT_INPUT} trade={TEST_TRADE_EXACT_INPUT}
......
...@@ -2,13 +2,11 @@ import { Trans } from '@lingui/macro' ...@@ -2,13 +2,11 @@ import { Trans } from '@lingui/macro'
import { TraceEvent, useTrace } from '@uniswap/analytics' import { TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import AnimatedDropdown from 'components/AnimatedDropdown' import AnimatedDropdown from 'components/AnimatedDropdown'
import Column from 'components/Column' import Column from 'components/Column'
import { LoadingOpacityContainer } from 'components/Loader/styled' import { LoadingOpacityContainer } from 'components/Loader/styled'
import { RowBetween, RowFixed } from 'components/Row' import { RowBetween, RowFixed } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { formatCommonPropertiesForTrade } from 'lib/utils/analytics'
import { formatEventPropertiesForTrade } from 'lib/utils/analytics'
import { useState } from 'react' import { useState } from 'react'
import { ChevronDown } from 'react-feather' import { ChevronDown } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
...@@ -101,7 +99,6 @@ interface SwapDetailsInlineProps { ...@@ -101,7 +99,6 @@ interface SwapDetailsInlineProps {
export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSlippage }: SwapDetailsInlineProps) { export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSlippage }: SwapDetailsInlineProps) {
const theme = useTheme() const theme = useTheme()
const { chainId } = useWeb3React()
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
const trace = useTrace() const trace = useTrace()
...@@ -112,7 +109,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl ...@@ -112,7 +109,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
name={SwapEventName.SWAP_DETAILS_EXPANDED} name={SwapEventName.SWAP_DETAILS_EXPANDED}
element={InterfaceElementName.SWAP_DETAILS_DROPDOWN} element={InterfaceElementName.SWAP_DETAILS_DROPDOWN}
properties={{ properties={{
...(trade ? formatEventPropertiesForTrade(trade, allowedSlippage) : {}), ...(trade ? formatCommonPropertiesForTrade(trade, allowedSlippage) : {}),
...trace, ...trace,
}} }}
shouldLogImpression={!showDetails} shouldLogImpression={!showDetails}
...@@ -142,12 +139,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl ...@@ -142,12 +139,7 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
) : null} ) : null}
</RowFixed> </RowFixed>
<RowFixed> <RowFixed>
{!trade?.gasUseEstimateUSD || {!showDetails && <GasEstimateTooltip trade={trade} loading={syncing || loading} />}
showDetails ||
!chainId ||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<GasEstimateTooltip trade={trade} loading={syncing || loading} disabled={showDetails} />
)}
<RotatingArrow <RotatingArrow
stroke={trade ? theme.textTertiary : theme.deprecated_bg3} stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
open={Boolean(trade && showDetails)} open={Boolean(trade && showDetails)}
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton' import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
...@@ -18,7 +19,15 @@ const HeaderButtonContainer = styled(RowFixed)` ...@@ -18,7 +19,15 @@ const HeaderButtonContainer = styled(RowFixed)`
gap: 16px; 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() const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
return ( return (
...@@ -30,7 +39,7 @@ export default function SwapHeader({ autoSlippage, chainId }: { autoSlippage: Pe ...@@ -30,7 +39,7 @@ export default function SwapHeader({ autoSlippage, chainId }: { autoSlippage: Pe
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />} {fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
</HeaderButtonContainer> </HeaderButtonContainer>
<RowFixed> <RowFixed>
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} /> <SettingsTab autoSlippage={autoSlippage} chainId={chainId} trade={trade} />
</RowFixed> </RowFixed>
</StyledSwapHeader> </StyledSwapHeader>
) )
......
...@@ -9,7 +9,7 @@ describe('SwapModalFooter.tsx', () => { ...@@ -9,7 +9,7 @@ describe('SwapModalFooter.tsx', () => {
<SwapModalFooter <SwapModalFooter
trade={TEST_TRADE_EXACT_INPUT} trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined} swapResult={undefined}
onConfirm={jest.fn()} onConfirm={jest.fn()}
swapErrorMessage={undefined} swapErrorMessage={undefined}
disabledConfirm={false} disabledConfirm={false}
...@@ -45,7 +45,7 @@ describe('SwapModalFooter.tsx', () => { ...@@ -45,7 +45,7 @@ describe('SwapModalFooter.tsx', () => {
<SwapModalFooter <SwapModalFooter
trade={TEST_TRADE_EXACT_INPUT} trade={TEST_TRADE_EXACT_INPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined} swapResult={undefined}
onConfirm={jest.fn()} onConfirm={jest.fn()}
swapErrorMessage={undefined} swapErrorMessage={undefined}
disabledConfirm={false} disabledConfirm={false}
...@@ -73,7 +73,7 @@ describe('SwapModalFooter.tsx', () => { ...@@ -73,7 +73,7 @@ describe('SwapModalFooter.tsx', () => {
<SwapModalFooter <SwapModalFooter
trade={TEST_TRADE_EXACT_OUTPUT} trade={TEST_TRADE_EXACT_OUTPUT}
allowedSlippage={TEST_ALLOWED_SLIPPAGE} allowedSlippage={TEST_ALLOWED_SLIPPAGE}
hash={undefined} swapResult={undefined}
onConfirm={jest.fn()} onConfirm={jest.fn()}
swapErrorMessage={undefined} swapErrorMessage={undefined}
disabledConfirm={false} disabledConfirm={false}
......
import { Trans } from '@lingui/macro' import { Plural, Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics' import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events' 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 { Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column' 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 useTransactionDeadline from 'hooks/useTransactionDeadline'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import { RouterPreference } from 'state/routing/slice' import { RouterPreference } from 'state/routing/slice'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { getTransactionCount, isClassicTrade } from 'state/routing/utils'
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks' import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
...@@ -22,6 +24,7 @@ import { getPriceImpactWarning } from 'utils/prices' ...@@ -22,6 +24,7 @@ import { getPriceImpactWarning } from 'utils/prices'
import { ButtonError, SmallButtonPrimary } from '../Button' import { ButtonError, SmallButtonPrimary } from '../Button'
import Row, { AutoRow, RowBetween, RowFixed } from '../Row' import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
import { GasBreakdownTooltip } from './GasBreakdownTooltip'
import { SwapCallbackError, SwapShowAcceptChanges } from './styleds' import { SwapCallbackError, SwapShowAcceptChanges } from './styleds'
import { Label } from './SwapModalHeaderAmount' import { Label } from './SwapModalHeaderAmount'
...@@ -47,7 +50,7 @@ const DetailRowValue = styled(ThemedText.BodySmall)` ...@@ -47,7 +50,7 @@ const DetailRowValue = styled(ThemedText.BodySmall)`
export default function SwapModalFooter({ export default function SwapModalFooter({
trade, trade,
allowedSlippage, allowedSlippage,
hash, swapResult,
onConfirm, onConfirm,
swapErrorMessage, swapErrorMessage,
disabledConfirm, disabledConfirm,
...@@ -58,7 +61,7 @@ export default function SwapModalFooter({ ...@@ -58,7 +61,7 @@ export default function SwapModalFooter({
onAcceptChanges, onAcceptChanges,
}: { }: {
trade: InterfaceTrade trade: InterfaceTrade
hash?: string swapResult?: SwapResult
allowedSlippage: Percent allowedSlippage: Percent
onConfirm: () => void onConfirm: () => void
swapErrorMessage?: ReactNode swapErrorMessage?: ReactNode
...@@ -72,7 +75,7 @@ export default function SwapModalFooter({ ...@@ -72,7 +75,7 @@ export default function SwapModalFooter({
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto' const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
const [routerPreference] = useRouterPreference() const [routerPreference] = useRouterPreference()
const routes = getRoutingDiagramEntries(trade) const routes = isClassicTrade(trade) ? getRoutingDiagramEntries(trade) : undefined
const theme = useTheme() const theme = useTheme()
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const nativeCurrency = useNativeCurrency(chainId) const nativeCurrency = useNativeCurrency(chainId)
...@@ -80,6 +83,7 @@ export default function SwapModalFooter({ ...@@ -80,6 +83,7 @@ export default function SwapModalFooter({
const label = `${trade.executionPrice.baseCurrency?.symbol} ` const label = `${trade.executionPrice.baseCurrency?.symbol} `
const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}` const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}`
const formattedPrice = formatTransactionAmount(priceToPreciseFloat(trade.executionPrice)) const formattedPrice = formatTransactionAmount(priceToPreciseFloat(trade.executionPrice))
const txCount = getTransactionCount(trade)
return ( return (
<> <>
...@@ -102,24 +106,28 @@ export default function SwapModalFooter({ ...@@ -102,24 +106,28 @@ export default function SwapModalFooter({
} }
> >
<Label cursor="help"> <Label cursor="help">
<Trans>Network fee</Trans> <Plural value={txCount} one="Network fee" other="Network fees" />
</Label> </Label>
</MouseoverTooltip> </MouseoverTooltip>
<DetailRowValue>{trade.gasUseEstimateUSD ? `~$${trade.gasUseEstimateUSD}` : '-'}</DetailRowValue> <MouseoverTooltip placement="right" size={TooltipSize.Small} text={<GasBreakdownTooltip trade={trade} />}>
</Row> <DetailRowValue>{formatNumber(trade.totalGasUseEstimateUSD, NumberType.FiatGasPrice)}</DetailRowValue>
</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> </MouseoverTooltip>
<DetailRowValue color={getPriceImpactWarning(trade.priceImpact)}>
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
</DetailRowValue>
</Row> </Row>
</ThemedText.BodySmall> </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> <ThemedText.BodySmall>
<Row align="flex-start" justify="space-between" gap="sm"> <Row align="flex-start" justify="space-between" gap="sm">
<MouseoverTooltip <MouseoverTooltip
...@@ -175,11 +183,11 @@ export default function SwapModalFooter({ ...@@ -175,11 +183,11 @@ export default function SwapModalFooter({
name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED} name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED}
properties={formatSwapButtonClickEventProperties({ properties={formatSwapButtonClickEventProperties({
trade, trade,
hash, swapResult,
allowedSlippage, allowedSlippage,
transactionDeadlineSecondsSinceEpoch, transactionDeadlineSecondsSinceEpoch,
isAutoSlippage, isAutoSlippage,
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API, isAutoRouterApi: routerPreference === RouterPreference.API,
swapQuoteReceivedDate, swapQuoteReceivedDate,
routes, routes,
fiatValueInput: fiatValueInput.data, fiatValueInput: fiatValueInput.data,
......
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format' 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 { render, screen } from 'test-utils/render'
import SwapModalHeader from './SwapModalHeader' import SwapModalHeader from './SwapModalHeader'
...@@ -23,6 +29,26 @@ describe('SwapModalHeader.tsx', () => { ...@@ -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', () => { it('test trade exact output, no recipient', () => {
const { asFragment } = render( const { asFragment } = render(
<SwapModalHeader trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} /> <SwapModalHeader trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
......
import { Trans } from '@lingui/macro' 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 Column, { AutoColumn } from 'components/Column'
import { useUSDPrice } from 'hooks/useUSDPrice' import { useUSDPrice } from 'hooks/useUSDPrice'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
...@@ -19,9 +19,11 @@ const HeaderContainer = styled(AutoColumn)` ...@@ -19,9 +19,11 @@ const HeaderContainer = styled(AutoColumn)`
export default function SwapModalHeader({ export default function SwapModalHeader({
trade, trade,
inputCurrency,
allowedSlippage, allowedSlippage,
}: { }: {
trade: InterfaceTrade trade: InterfaceTrade
inputCurrency?: Currency
allowedSlippage: Percent allowedSlippage: Percent
}) { }) {
const fiatValueInput = useUSDPrice(trade.inputAmount) const fiatValueInput = useUSDPrice(trade.inputAmount)
...@@ -34,12 +36,14 @@ export default function SwapModalHeader({ ...@@ -34,12 +36,14 @@ export default function SwapModalHeader({
field={Field.INPUT} field={Field.INPUT}
label={<Trans>You pay</Trans>} label={<Trans>You pay</Trans>}
amount={trade.inputAmount} amount={trade.inputAmount}
currency={inputCurrency ?? trade.inputAmount.currency}
usdAmount={fiatValueInput.data} usdAmount={fiatValueInput.data}
/> />
<SwapModalHeaderAmount <SwapModalHeaderAmount
field={Field.OUTPUT} field={Field.OUTPUT}
label={<Trans>You receive</Trans>} label={<Trans>You receive</Trans>}
amount={trade.outputAmount} amount={trade.outputAmount}
currency={trade.outputAmount.currency}
usdAmount={fiatValueOutput.data} usdAmount={fiatValueOutput.data}
tooltipText={ tooltipText={
trade.tradeType === TradeType.EXACT_INPUT ? ( trade.tradeType === TradeType.EXACT_INPUT ? (
......
...@@ -37,11 +37,15 @@ interface AmountProps { ...@@ -37,11 +37,15 @@ interface AmountProps {
field: Field field: Field
tooltipText?: ReactNode tooltipText?: ReactNode
label: ReactNode label: ReactNode
amount?: CurrencyAmount<Currency> amount: CurrencyAmount<Currency>
usdAmount?: number 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) let formattedAmount = formatCurrencyAmount(amount, NumberType.TokenTx)
if (formattedAmount.length > MAX_AMOUNT_STR_LENGTH) { if (formattedAmount.length > MAX_AMOUNT_STR_LENGTH) {
formattedAmount = formatCurrencyAmount(amount, NumberType.SwapTradeAmount) formattedAmount = formatCurrencyAmount(amount, NumberType.SwapTradeAmount)
...@@ -57,7 +61,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f ...@@ -57,7 +61,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
</ThemedText.BodySecondary> </ThemedText.BodySecondary>
<Column gap="xs"> <Column gap="xs">
<ResponsiveHeadline data-testid={`${field}-amount`}> <ResponsiveHeadline data-testid={`${field}-amount`}>
{formattedAmount} {amount?.currency.symbol} {formattedAmount} {currency?.symbol}
</ResponsiveHeadline> </ResponsiveHeadline>
{usdAmount && ( {usdAmount && (
<ThemedText.BodySmall color="textTertiary"> <ThemedText.BodySmall color="textTertiary">
...@@ -66,7 +70,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f ...@@ -66,7 +70,7 @@ export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, f
)} )}
</Column> </Column>
</Column> </Column>
{amount?.currency && <CurrencyLogo currency={amount.currency} size="36px" />} <CurrencyLogo currency={currency} size="36px" />
</Row> </Row>
) )
} }
...@@ -5,13 +5,13 @@ import { LoadingRows } from 'components/Loader/styled' ...@@ -5,13 +5,13 @@ import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram' import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported' import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { InterfaceTrade } from 'state/routing/types' import { ClassicTrade } from 'state/routing/types'
import { Separator, ThemedText } from 'theme' import { Separator, ThemedText } from 'theme'
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries' 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 { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported() const autoRouterSupported = useAutoRouterSupported()
...@@ -21,14 +21,14 @@ export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; s ...@@ -21,14 +21,14 @@ export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; s
// TODO(WEB-2022) // TODO(WEB-2022)
// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`? // 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 && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
? trade.gasUseEstimateUSD === '0.00' ? trade.gasUseEstimateUSD === 0
? '<$0.01' ? '<$0.01'
: '$' + trade.gasUseEstimateUSD : '$' + trade.gasUseEstimateUSD.toFixed(2)
: undefined : undefined
return ( return (
<Column gap="md"> <Column gap="md">
<RouterLabel /> <RouterLabel trade={trade} />
<Separator /> <Separator />
{syncing ? ( {syncing ? (
<LoadingRows> <LoadingRows>
......
...@@ -93,9 +93,15 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = ` ...@@ -93,9 +93,15 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
class="c7 css-zhpkf8" class="c5"
> >
~$1.00 <div>
<div
class="c7 css-zhpkf8"
>
~$1.00
</div>
</div>
</div> </div>
</div> </div>
<div <div
......
...@@ -25,6 +25,24 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -25,6 +25,24 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start; 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 { .c4 {
-webkit-box-pack: justify; -webkit-box-pack: justify;
-webkit-justify-content: space-between; -webkit-justify-content: space-between;
...@@ -38,15 +56,22 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -38,15 +56,22 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
width: fit-content; width: fit-content;
} }
.c12 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
margin: -xs;
}
.c9 { .c9 {
color: #0D111C; color: #0D111C;
} }
.c12 { .c14 {
color: #7780A0; color: #7780A0;
} }
.c16 { .c18 {
width: 100%; width: 100%;
height: 1px; height: 1px;
background-color: #D2D9EE; background-color: #D2D9EE;
...@@ -66,7 +91,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -66,7 +91,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
justify-content: flex-start; justify-content: flex-start;
} }
.c15 { .c17 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
...@@ -94,12 +119,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -94,12 +119,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
height: inherit; height: inherit;
} }
.c11 { .c13 {
margin-right: 4px;
height: 18px; height: 18px;
} }
.c11 > * { .c13 > * {
stroke: #98A1C0; stroke: #98A1C0;
} }
...@@ -144,7 +168,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -144,7 +168,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
cursor: pointer; cursor: pointer;
} }
.c13 { .c15 {
-webkit-transform: none; -webkit-transform: none;
-ms-transform: none; -ms-transform: none;
transform: none; transform: none;
...@@ -153,7 +177,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -153,7 +177,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
transition: transform 0.1s linear; transition: transform 0.1s linear;
} }
.c14 { .c16 {
padding-top: 12px; padding-top: 12px;
} }
...@@ -201,15 +225,15 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -201,15 +225,15 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
class="c7" class="c7"
> >
<div <div
class="c2 c3 c6" class="c2 c11 c12"
> >
<svg <svg
class="c11" class="c13"
> >
gas-icon.svg gas-icon.svg
</svg> </svg>
<div <div
class="c12 css-zhpkf8" class="c14 css-zhpkf8"
> >
$1.00 $1.00
</div> </div>
...@@ -218,7 +242,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -218,7 +242,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div> </div>
</div> </div>
<svg <svg
class="c13" class="c15"
fill="none" fill="none"
height="24" height="24"
stroke="#98A1C0" stroke="#98A1C0"
...@@ -240,14 +264,14 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -240,14 +264,14 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
> >
<div> <div>
<div <div
class="c14" class="c16"
data-testid="advanced-swap-details" data-testid="advanced-swap-details"
> >
<div <div
class="c15" class="c17"
> >
<div <div
class="c16" class="c18"
/> />
<div <div
class="c2 c3 c4" class="c2 c3 c4"
...@@ -257,7 +281,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -257,7 +281,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
> >
<div> <div>
<div <div
class="c12 css-zhpkf8" class="c14 css-zhpkf8"
> >
Network fee Network fee
</div> </div>
...@@ -300,7 +324,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -300,7 +324,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
> >
<div> <div>
<div <div
class="c12 css-zhpkf8" class="c14 css-zhpkf8"
> >
Minimum output Minimum output
</div> </div>
...@@ -324,7 +348,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -324,7 +348,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
> >
<div> <div>
<div <div
class="c12 css-zhpkf8" class="c14 css-zhpkf8"
> >
Expected output Expected output
</div> </div>
...@@ -338,13 +362,13 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = ` ...@@ -338,13 +362,13 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
</div> </div>
</div> </div>
<div <div
class="c16" class="c18"
/> />
<div <div
class="c2 c3 c4" class="c2 c3 c4"
> >
<div <div
class="c12 css-zhpkf8" class="c14 css-zhpkf8"
> >
Order routing Order routing
</div> </div>
......
...@@ -110,9 +110,15 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = ...@@ -110,9 +110,15 @@ exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] =
</div> </div>
</div> </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> </div>
</div> </div>
......
...@@ -218,6 +218,224 @@ exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] = ...@@ -218,6 +218,224 @@ exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] =
</DocumentFragment> </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`] = ` exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
<DocumentFragment> <DocumentFragment>
.c3 { .c3 {
......
...@@ -5,6 +5,7 @@ import { AlertTriangle } from 'react-feather' ...@@ -5,6 +5,7 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components/macro' import styled, { css } from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
export const PageWrapper = styled.div` export const PageWrapper = styled.div`
...@@ -38,6 +39,109 @@ export const SwapWrapper = styled.main<{ chainId?: number }>` ...@@ -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 }>` export const ArrowWrapper = styled.div<{ clickable: boolean }>`
border-radius: 12px; border-radius: 12px;
height: 40px; height: 40px;
......
...@@ -90,3 +90,7 @@ export type SupportedL2ChainId = (typeof L2_CHAIN_IDS)[number] ...@@ -90,3 +90,7 @@ export type SupportedL2ChainId = (typeof L2_CHAIN_IDS)[number]
export function isPolygonChain(chainId: number): chainId is ChainId.POLYGON | ChainId.POLYGON_MUMBAI { export function isPolygonChain(chainId: number): chainId is ChainId.POLYGON | ChainId.POLYGON_MUMBAI {
return chainId === ChainId.POLYGON || chainId === 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 { ...@@ -11,7 +11,8 @@ export enum FeatureFlag {
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page', fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
detailsV2 = 'details_v2', detailsV2 = 'details_v2',
debounceSwapQuote = 'debounce_swap_quote', debounceSwapQuote = 'debounce_swap_quote',
nativeUsdcArbitrum = 'web_usdc_arbitrum', uniswapXEnabled = 'uniswapx_enabled',
uniswapXSyntheticQuote = 'uniswapx_synthetic_quote',
routingAPIPrice = 'routing_api_price', routingAPIPrice = 'routing_api_price',
} }
......
...@@ -102,13 +102,44 @@ fragment TransactionParts on Transaction { ...@@ -102,13 +102,44 @@ fragment TransactionParts on Transaction {
nonce 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 { fragment AssetActivityParts on AssetActivity {
id id
timestamp timestamp
type type
chain chain
transaction { details {
...TransactionParts __typename
... on TransactionDetails {
...TransactionDetailsParts
}
... on SwapOrderDetails {
...SwapOrderDetailsParts
}
} }
assetChanges { assetChanges {
__typename __typename
...@@ -130,10 +161,11 @@ fragment AssetActivityParts on AssetActivity { ...@@ -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]) { portfolios(ownerAddresses: [$account]) {
id id
assetActivities(pageSize: 50, page: 1) { assetActivities(pageSize: 100, page: 1, includeOffChain: true) {
...AssetActivityParts ...AssetActivityParts
} }
} }
......
...@@ -8,7 +8,7 @@ import { L2_CHAIN_IDS } from 'constants/chains' ...@@ -8,7 +8,7 @@ import { L2_CHAIN_IDS } from 'constants/chains'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import useNativeCurrency from 'lib/hooks/useNativeCurrency' import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react' import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { ClassicTrade } from 'state/routing/types'
import useGasPrice from './useGasPrice' import useGasPrice from './useGasPrice'
import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice' import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice'
...@@ -71,8 +71,9 @@ const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 100) // 5% ...@@ -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. * 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 { chainId } = useWeb3React()
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId) const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
const outputDollarValue = useStablecoinValue(trade?.outputAmount) const outputDollarValue = useStablecoinValue(trade?.outputAmount)
......
...@@ -54,7 +54,8 @@ describe('#useBestV3Trade ExactIn', () => { ...@@ -54,7 +54,8 @@ describe('#useBestV3Trade ExactIn', () => {
USDCAmount, USDCAmount,
DAI, DAI,
RouterPreference.CLIENT, RouterPreference.CLIENT,
true // skipFetch true, // skipFetch
undefined
) )
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI) expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
...@@ -72,7 +73,8 @@ describe('#useBestV3Trade ExactIn', () => { ...@@ -72,7 +73,8 @@ describe('#useBestV3Trade ExactIn', () => {
USDCAmount, USDCAmount,
DAI, DAI,
RouterPreference.CLIENT, RouterPreference.CLIENT,
true // skipFetch true, // skipFetch
undefined
) )
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined }) expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
}) })
...@@ -132,7 +134,8 @@ describe('#useBestV3Trade ExactOut', () => { ...@@ -132,7 +134,8 @@ describe('#useBestV3Trade ExactOut', () => {
DAIAmount, DAIAmount,
USDC_MAINNET, USDC_MAINNET,
RouterPreference.CLIENT, RouterPreference.CLIENT,
true // skipFetch true, // skipFetch
undefined
) )
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET) expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined }) expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
...@@ -150,7 +153,8 @@ describe('#useBestV3Trade ExactOut', () => { ...@@ -150,7 +153,8 @@ describe('#useBestV3Trade ExactOut', () => {
DAIAmount, DAIAmount,
USDC_MAINNET, USDC_MAINNET,
RouterPreference.CLIENT, RouterPreference.CLIENT,
true // skipFetch true, // skipFetch
undefined
) )
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined }) expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
}) })
......
...@@ -3,7 +3,8 @@ import { useWeb3React } from '@web3-react/core' ...@@ -3,7 +3,8 @@ import { useWeb3React } from '@web3-react/core'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { DebounceSwapQuoteVariant, useDebounceSwapQuoteFlag } from 'featureFlags/flags/debounceSwapQuote' import { DebounceSwapQuoteVariant, useDebounceSwapQuoteFlag } from 'featureFlags/flags/debounceSwapQuote'
import { useMemo } from 'react' 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 { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
import { useRouterPreference } from 'state/user/hooks' import { useRouterPreference } from 'state/user/hooks'
...@@ -18,6 +19,27 @@ const DEBOUNCE_TIME = 350 ...@@ -18,6 +19,27 @@ const DEBOUNCE_TIME = 350
// Temporary until we remove the feature flag. // Temporary until we remove the feature flag.
const DEBOUNCE_TIME_INCREASED = 650 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. * Returns the best v2+v3 trade for a desired swap.
* @param tradeType whether the swap is an exact in/out * @param tradeType whether the swap is an exact in/out
...@@ -27,7 +49,9 @@ const DEBOUNCE_TIME_INCREASED = 650 ...@@ -27,7 +49,9 @@ const DEBOUNCE_TIME_INCREASED = 650
export function useBestTrade( export function useBestTrade(
tradeType: TradeType, tradeType: TradeType,
amountSpecified?: CurrencyAmount<Currency>, amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency otherCurrency?: Currency,
routerPreferenceOverride?: RouterPreference,
account?: string
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: InterfaceTrade
...@@ -59,8 +83,9 @@ export function useBestTrade( ...@@ -59,8 +83,9 @@ export function useBestTrade(
tradeType, tradeType,
amountSpecified ? debouncedAmount : undefined, amountSpecified ? debouncedAmount : undefined,
debouncedOtherCurrency, debouncedOtherCurrency,
routerPreference, routerPreferenceOverride ?? routerPreference,
!(autoRouterSupported && shouldGetTrade) // skip fetching !(autoRouterSupported && shouldGetTrade), // skip fetching
account
) )
const inDebounce = const inDebounce =
......
...@@ -4,7 +4,7 @@ import { useWeb3React } from '@web3-react/core' ...@@ -4,7 +4,7 @@ import { useWeb3React } from '@web3-react/core'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { useSingleContractWithCallData } from 'lib/hooks/multicall' import { useSingleContractWithCallData } from 'lib/hooks/multicall'
import { useMemo } from 'react' 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 { isCelo } from '../constants/tokens'
import { useAllV3Routes } from './useAllV3Routes' import { useAllV3Routes } from './useAllV3Routes'
...@@ -23,6 +23,7 @@ const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = { ...@@ -23,6 +23,7 @@ const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
const DEFAULT_GAS_QUOTE = 2_000_000 const DEFAULT_GAS_QUOTE = 2_000_000
// TODO (UniswapX or in general): Deprecate this?
/** /**
* Returns the best v3 trade for a desired swap * Returns the best v3 trade for a desired swap
* @param tradeType whether the swap is an exact in/out * @param tradeType whether the swap is an exact in/out
...@@ -145,6 +146,9 @@ export function useClientSideV3Trade<TTradeType extends TradeType>( ...@@ -145,6 +146,9 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
}, },
], ],
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]) }, [amountSpecified, currenciesAreTheSame, currencyIn, currencyOut, quotesResults, routes, routesLoading, tradeType])
......
...@@ -6,6 +6,7 @@ import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'h ...@@ -6,6 +6,7 @@ import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'h
import { useRevokeTokenAllowance, useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance' import { useRevokeTokenAllowance, useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance'
import useInterval from 'lib/hooks/useInterval' import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { TradeFillType } from 'state/routing/types'
import { useHasPendingApproval, useHasPendingRevocation, useTransactionAdder } from 'state/transactions/hooks' import { useHasPendingApproval, useHasPendingRevocation, useTransactionAdder } from 'state/transactions/hooks'
enum ApprovalState { enum ApprovalState {
...@@ -43,7 +44,11 @@ export type Allowance = ...@@ -43,7 +44,11 @@ export type Allowance =
} }
| AllowanceRequired | 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 { account } = useWeb3React()
const token = amount?.currency const token = amount?.currency
...@@ -100,7 +105,10 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen ...@@ -100,7 +105,10 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
}, [amount, now, permitAllowance, permitExpiration]) }, [amount, now, permitAllowance, permitExpiration])
const shouldRequestApproval = !(isApproved || isApprovalLoading) 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 addTransaction = useTransactionAdder()
const approveAndPermit = useCallback(async () => { const approveAndPermit = useCallback(async () => {
if (shouldRequestApproval) { if (shouldRequestApproval) {
......
...@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core' ...@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core'
import { getConnection } from 'connection' import { getConnection } from 'connection'
import { didUserReject } from 'connection/utils' import { didUserReject } from 'connection/utils'
import { useCallback } from 'react' import { useCallback } from 'react'
import { addPopup } from 'state/application/reducer' import { addPopup, PopupType } from 'state/application/reducer'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { useSwitchChain } from './useSwitchChain' import { useSwitchChain } from './useSwitchChain'
...@@ -24,7 +24,12 @@ export default function useSelectChain() { ...@@ -24,7 +24,12 @@ export default function useSelectChain() {
} catch (error) { } catch (error) {
if (!didUserReject(connection, error) && error.code !== -32002 /* request already pending */) { if (!didUserReject(connection, error) && error.code !== -32002 /* request already pending */) {
console.error('Failed to switch networks', error) 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 ...@@ -85,7 +85,7 @@ export function useStablecoinValue(currencyAmount: CurrencyAmount<Currency> | un
* @param fiatValue string representation of a USD amount * @param fiatValue string representation of a USD amount
* @returns CurrencyAmount where currency is stablecoin on active chain * @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 { chainId } = useWeb3React()
const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined
...@@ -95,7 +95,7 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde ...@@ -95,7 +95,7 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
} }
// trim for decimal precision when parsing // trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString() const parsedForDecimals = fiatValue.toFixed(stablecoin.decimals).toString()
try { try {
// parse USD string into CurrencyAmount based on stablecoin decimals // parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin) return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
......
import { Trade } from '@uniswap/router-sdk' import { Percent, TradeType } from '@uniswap/sdk-core'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core'
import { PermitSignature } from 'hooks/usePermitAllowance' 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 { 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 { currencyId } from '../utils/currencyId'
import useTransactionDeadline from './useTransactionDeadline' import useTransactionDeadline from './useTransactionDeadline'
import { useUniswapXSwapCallback } from './useUniswapXSwapCallback'
import { useUniversalRouterSwapCallback } from './useUniversalRouter' 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 // and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback( 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 fiatValues: { amountIn?: number; amountOut?: number }, // usd values for amount in and out, logged for analytics
allowedSlippage: Percent, // in bips allowedSlippage: Percent, // in bips
permitSignature: PermitSignature | undefined permitSignature: PermitSignature | undefined
): { callback: null | (() => Promise<string>) } { ) {
const deadline = useTransactionDeadline() const deadline = useTransactionDeadline()
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const addOrder = useAddOrder()
const { account, chainId } = useWeb3React()
const universalRouterSwapCallback = useUniversalRouterSwapCallback(trade, fiatValues, { const uniswapXSwapCallback = useUniswapXSwapCallback({
slippageTolerance: allowedSlippage, trade: isUniswapXTrade(trade) ? trade : undefined,
deadline, allowedSlippage,
permit: permitSignature, fiatValues,
}) })
const swapCallback = universalRouterSwapCallback
const callback = useMemo(() => { const universalRouterSwapCallback = useUniversalRouterSwapCallback(
if (!trade || !swapCallback) return null isClassicTrade(trade) ? trade : undefined,
const info: TransactionInfo = { 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, type: TransactionType.SWAP,
inputCurrencyId: currencyId(trade.inputAmount.currency), inputCurrencyId: currencyId(trade.inputAmount.currency),
outputCurrencyId: currencyId(trade.outputAmount.currency), outputCurrencyId: currencyId(trade.outputAmount.currency),
isUniswapXOrder: result.type === TradeFillType.UniswapX,
...(trade.tradeType === TradeType.EXACT_INPUT ...(trade.tradeType === TradeType.EXACT_INPUT
? { ? {
tradeType: TradeType.EXACT_INPUT, tradeType: TradeType.EXACT_INPUT,
...@@ -48,14 +77,19 @@ export function useSwapCallback( ...@@ -48,14 +77,19 @@ export function useSwapCallback(
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}), }),
} }
return () =>
swapCallback().then((response) => { if (result.type === TradeFillType.UniswapX) {
addTransaction(response, info, deadline?.toNumber()) addOrder(
return response.hash account,
}) result.response.orderHash,
}, [addTransaction, allowedSlippage, deadline, swapCallback, trade]) chainId,
result.response.deadline,
return { swapInfo as UniswapXOrderDetails['swapInfo']
callback, )
} } 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 { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro' import { t } from '@lingui/macro'
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics' import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { SwapEventName } from '@uniswap/analytics-events' import { SwapEventName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk' import { Percent } from '@uniswap/sdk-core'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { FeeOptions, toHex } from '@uniswap/v3-sdk' import { FeeOptions, toHex } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics' import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react' import { useCallback } from 'react'
import { ClassicTrade, TradeFillType } from 'state/routing/types'
import { trace } from 'tracing/trace' import { trace } from 'tracing/trace'
import { calculateGasMargin } from 'utils/calculateGasMargin' import { calculateGasMargin } from 'utils/calculateGasMargin'
import { UserRejectedRequestError } from 'utils/errors' import { UserRejectedRequestError } from 'utils/errors'
...@@ -45,14 +44,14 @@ interface SwapOptions { ...@@ -45,14 +44,14 @@ interface SwapOptions {
} }
export function useUniversalRouterSwapCallback( export function useUniversalRouterSwapCallback(
trade: Trade<Currency, Currency, TradeType> | undefined, trade: ClassicTrade | undefined,
fiatValues: { amountIn?: number; amountOut?: number }, fiatValues: { amountIn?: number; amountOut?: number },
options: SwapOptions options: SwapOptions
) { ) {
const { account, chainId, provider } = useWeb3React() const { account, chainId, provider } = useWeb3React()
const analyticsContext = useTrace() const analyticsContext = useTrace()
return useCallback(async (): Promise<TransactionResponse> => { return useCallback(async () => {
return trace('swap.send', async ({ setTraceData, setTraceStatus, setTraceError }) => { return trace('swap.send', async ({ setTraceData, setTraceStatus, setTraceError }) => {
try { try {
if (!account) throw new Error('missing account') if (!account) throw new Error('missing account')
...@@ -108,7 +107,10 @@ export function useUniversalRouterSwapCallback( ...@@ -108,7 +107,10 @@ export function useUniversalRouterSwapCallback(
} }
return response return response
}) })
return response return {
type: TradeFillType.Classic as const,
response,
}
} catch (swapError: unknown) { } catch (swapError: unknown) {
if (swapError instanceof ModifiedSwapError) throw swapError if (swapError instanceof ModifiedSwapError) throw swapError
......
...@@ -60,7 +60,7 @@ export default function useWrapCallback( ...@@ -60,7 +60,7 @@ export default function useWrapCallback(
inputCurrency: Currency | undefined | null, inputCurrency: Currency | undefined | null,
outputCurrency: Currency | undefined | null, outputCurrency: Currency | undefined | null,
typedValue: string | undefined typedValue: string | undefined
): { wrapType: WrapType; execute?: () => Promise<void>; inputError?: WrapInputError } { ): { wrapType: WrapType; execute?: () => Promise<string | undefined>; inputError?: WrapInputError } {
const { chainId, account } = useWeb3React() const { chainId, account } = useWeb3React()
const wethContract = useWETHContract() const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency ?? undefined) const balance = useCurrencyBalance(account ?? undefined, inputCurrency ?? undefined)
...@@ -99,37 +99,34 @@ export default function useWrapCallback( ...@@ -99,37 +99,34 @@ export default function useWrapCallback(
execute: execute:
sufficientBalance && inputAmount sufficientBalance && inputAmount
? async () => { ? async () => {
try { const network = await wethContract.provider.getNetwork()
const network = await wethContract.provider.getNetwork() if (
if ( network.chainId !== chainId ||
network.chainId !== chainId || wethContract.address !== WRAPPED_NATIVE_CURRENCY[network.chainId]?.address
wethContract.address !== WRAPPED_NATIVE_CURRENCY[network.chainId]?.address ) {
) { sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_INVALIDATED, {
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, {
...eventProperties, ...eventProperties,
contract_address: wethContract.address,
contract_chain_id: network.chainId,
type: WrapType.WRAP, type: WrapType.WRAP,
}) })
} catch (error) { const error = new Error(`Invalid WETH contract
console.error('Could not deposit', error) 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, : undefined,
inputError: sufficientBalance inputError: sufficientBalance
...@@ -144,21 +141,18 @@ Please file a bug detailing how this happened - https://github.com/Uniswap/inter ...@@ -144,21 +141,18 @@ Please file a bug detailing how this happened - https://github.com/Uniswap/inter
execute: execute:
sufficientBalance && inputAmount sufficientBalance && inputAmount
? async () => { ? async () => {
try { const txReceipt = await wethContract.withdraw(`0x${inputAmount.quotient.toString(16)}`)
const txReceipt = await wethContract.withdraw(`0x${inputAmount.quotient.toString(16)}`) addTransaction(txReceipt, {
addTransaction(txReceipt, { type: TransactionType.WRAP,
type: TransactionType.WRAP, unwrapped: true,
unwrapped: true, currencyAmountRaw: inputAmount?.quotient.toString(),
currencyAmountRaw: inputAmount?.quotient.toString(), chainId,
chainId, })
}) sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, { ...eventProperties,
...eventProperties, type: WrapType.UNWRAP,
type: WrapType.UNWRAP, })
}) return txReceipt.hash
} catch (error) {
console.error('Could not withdraw', error)
}
} }
: undefined, : undefined,
inputError: sufficientBalance inputError: sufficientBalance
......
...@@ -23,6 +23,7 @@ import store from './state' ...@@ -23,6 +23,7 @@ import store from './state'
import ApplicationUpdater from './state/application/updater' import ApplicationUpdater from './state/application/updater'
import ListsUpdater from './state/lists/updater' import ListsUpdater from './state/lists/updater'
import LogsUpdater from './state/logs/updater' import LogsUpdater from './state/logs/updater'
import OrderUpdater from './state/signatures/updater'
import TransactionUpdater from './state/transactions/updater' import TransactionUpdater from './state/transactions/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme' import ThemeProvider, { ThemedGlobalStyle } from './theme'
import RadialGradientByChainUpdater from './theme/components/RadialGradientByChainUpdater' import RadialGradientByChainUpdater from './theme/components/RadialGradientByChainUpdater'
...@@ -39,6 +40,7 @@ function Updaters() { ...@@ -39,6 +40,7 @@ function Updaters() {
<SystemThemeUpdater /> <SystemThemeUpdater />
<ApplicationUpdater /> <ApplicationUpdater />
<TransactionUpdater /> <TransactionUpdater />
<OrderUpdater />
<MulticallUpdater /> <MulticallUpdater />
<LogsUpdater /> <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 { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useRoutingAPIForPrice } from 'featureFlags/flags/priceRoutingApi' import { useRoutingAPIForPrice } from 'featureFlags/flags/priceRoutingApi'
import { useUniswapXEnabled } from 'featureFlags/flags/uniswapx'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useMemo } from 'react' import { useMemo } from 'react'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice' import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
import { currencyAddressForSwapQuote } from 'state/routing/utils' import { currencyAddressForSwapQuote } from 'state/routing/utils'
...@@ -10,27 +12,33 @@ import { currencyAddressForSwapQuote } from 'state/routing/utils' ...@@ -10,27 +12,33 @@ import { currencyAddressForSwapQuote } from 'state/routing/utils'
* be destructured. * be destructured.
*/ */
export function useRoutingAPIArguments({ export function useRoutingAPIArguments({
account,
tokenIn, tokenIn,
tokenOut, tokenOut,
amount, amount,
tradeType, tradeType,
routerPreference, routerPreference,
}: { }: {
account?: string
tokenIn?: Currency tokenIn?: Currency
tokenOut?: Currency tokenOut?: Currency
amount?: CurrencyAmount<Currency> amount?: CurrencyAmount<Currency>
tradeType: TradeType tradeType: TradeType
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
}): GetQuoteArgs | undefined { }): GetQuoteArgs | undefined {
const uniswapXEnabled = useUniswapXEnabled()
const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled()
const isRoutingAPIPrice = useRoutingAPIForPrice() const isRoutingAPIPrice = useRoutingAPIForPrice()
return useMemo( return useMemo(
() => () =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped) !tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut) || tokenIn.wrapped.equals(tokenOut.wrapped)
? undefined ? undefined
: { : {
account,
amount: amount.quotient.toString(), amount: amount.quotient.toString(),
tokenInAddress: currencyAddressForSwapQuote(tokenIn), tokenInAddress: currencyAddressForSwapQuote(tokenIn),
tokenInChainId: tokenIn.wrapped.chainId, tokenInChainId: tokenIn.chainId,
tokenInDecimals: tokenIn.wrapped.decimals, tokenInDecimals: tokenIn.wrapped.decimals,
tokenInSymbol: tokenIn.wrapped.symbol, tokenInSymbol: tokenIn.wrapped.symbol,
tokenOutAddress: currencyAddressForSwapQuote(tokenOut), tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
...@@ -40,7 +48,20 @@ export function useRoutingAPIArguments({ ...@@ -40,7 +48,20 @@ export function useRoutingAPIArguments({
routerPreference, routerPreference,
tradeType, tradeType,
isRoutingAPIPrice, 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 } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, Price, Token, TradeType } from '@uniswap/sdk-core'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { InterfaceTrade, QuoteMethod } from 'state/routing/types' import { InterfaceTrade, QuoteMethod } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { computeRealizedPriceImpact } from 'utils/prices' import { computeRealizedPriceImpact } from 'utils/prices'
export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => { export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => {
...@@ -34,44 +34,66 @@ export const getPriceUpdateBasisPoints = ( ...@@ -34,44 +34,66 @@ export const getPriceUpdateBasisPoints = (
return formatPercentInBasisPointsNumber(changePercentage) 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 = ({ export const formatSwapSignedAnalyticsEventProperties = ({
trade, trade,
allowedSlippage, allowedSlippage,
fiatValues, fiatValues,
txHash, txHash,
}: { }: {
trade: InterfaceTrade | Trade<Currency, Currency, TradeType> trade: InterfaceTrade
allowedSlippage: Percent allowedSlippage: Percent
fiatValues: { amountIn?: number; amountOut?: number } fiatValues: { amountIn?: number; amountOut?: number }
txHash: string txHash?: string
}) => ({ }) => ({
transaction_hash: txHash, transaction_hash: txHash,
token_in_amount_usd: fiatValues.amountIn, token_in_amount_usd: fiatValues.amountIn,
token_out_amount_usd: fiatValues.amountOut, token_out_amount_usd: fiatValues.amountOut,
...formatEventPropertiesForTrade(trade, allowedSlippage), ...formatCommonPropertiesForTrade(trade, allowedSlippage),
}) })
export const formatEventPropertiesForTrade = ( function getQuoteMethod(trade: InterfaceTrade) {
trade: Trade<Currency, Currency, TradeType>, if (isUniswapXTrade(trade)) return QuoteMethod.ROUTING_API
return trade.quoteMethod
}
export const formatSwapQuoteReceivedEventProperties = (
trade: InterfaceTrade,
allowedSlippage: Percent, allowedSlippage: Percent,
gasUseEstimateUSD?: string, swapQuoteReceivedDate: Date
method?: QuoteMethod
) => { ) => {
return { return {
token_in_symbol: trade.inputAmount.currency.symbol, ...formatCommonPropertiesForTrade(trade, allowedSlippage),
token_out_symbol: trade.outputAmount.currency.symbol, swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
token_in_address: getTokenAddress(trade.inputAmount.currency), swap_quote_received_timestamp: swapQuoteReceivedDate.getTime(),
token_out_address: getTokenAddress(trade.outputAmount.currency), allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined, token_in_amount_max: trade.maximumAmountIn(allowedSlippage),
estimated_network_fee_usd: gasUseEstimateUSD, token_out_amount_min: trade.minimumAmountOut(allowedSlippage),
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,
} }
} }
...@@ -2,19 +2,27 @@ import { Currency, CurrencyAmount, NativeCurrency, Percent, Token, TradeType } f ...@@ -2,19 +2,27 @@ import { Currency, CurrencyAmount, NativeCurrency, Percent, Token, TradeType } f
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useBestTrade } from 'hooks/useBestTrade' import { useBestTrade } from 'hooks/useBestTrade'
import { useMemo } from 'react' 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( export default function useDerivedPayWithAnyTokenSwapInfo(
inputCurrency?: Currency, inputCurrency?: Currency,
parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token> parsedOutputAmount?: CurrencyAmount<NativeCurrency | Token>
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: ClassicTrade
maximumAmountIn?: CurrencyAmount<Token> maximumAmountIn?: CurrencyAmount<Token>
allowedSlippage: Percent allowedSlippage: Percent
} { } {
const { state, trade } = useBestTrade(TradeType.EXACT_OUTPUT, parsedOutputAmount, inputCurrency ?? undefined) const { state, trade } = useBestTrade(
const allowedSlippage = useAutoSlippageTolerance(trade) TradeType.EXACT_OUTPUT,
parsedOutputAmount,
inputCurrency ?? undefined,
RouterPreference.API
)
const allowedSlippage = useAutoSlippageTolerance(isClassicTrade(trade) ? trade : undefined)
const maximumAmountIn = useMemo(() => { const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage) const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
......
...@@ -4,6 +4,7 @@ import { Allowance } from 'hooks/usePermit2Allowance' ...@@ -4,6 +4,7 @@ import { Allowance } from 'hooks/usePermit2Allowance'
import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes' import { buildAllTradeRouteInputs } from 'nft/utils/tokenRoutes'
import { useEffect } from 'react' import { useEffect } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import { isClassicTrade } from 'state/routing/utils'
import { useTokenInput } from './useTokenInput' import { useTokenInput } from './useTokenInput'
...@@ -13,7 +14,7 @@ export default function usePayWithAnyTokenSwap( ...@@ -13,7 +14,7 @@ export default function usePayWithAnyTokenSwap(
allowedSlippage?: Percent allowedSlippage?: Percent
) { ) {
const setTokenTradeInput = useTokenInput((state) => state.setTokenTradeInput) 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 hasInputAmount = !!trade && !!trade.inputAmount && trade.inputAmount.currency.isToken
const hasAllowance = !!allowedSlippage && !!allowance const hasAllowance = !!allowedSlippage && !!allowance
......
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useMemo } from 'react' import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types' import { ClassicTrade } from 'state/routing/types'
import { useTheme } from 'styled-components/macro' import { useTheme } from 'styled-components/macro'
import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices' import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
...@@ -14,7 +14,7 @@ interface PriceImpactSeverity { ...@@ -14,7 +14,7 @@ interface PriceImpactSeverity {
color: string color: string
} }
export function usePriceImpact(trade?: InterfaceTrade): PriceImpact | undefined { export function usePriceImpact(trade?: ClassicTrade): PriceImpact | undefined {
const theme = useTheme() const theme = useTheme()
return useMemo(() => { return useMemo(() => {
......
...@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' ...@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk' import { Pair } from '@uniswap/v2-sdk'
import { Pool } from '@uniswap/v3-sdk' import { Pool } from '@uniswap/v3-sdk'
import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput } from 'graphql/data/__generated__/types-and-hooks' 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 { interface SwapAmounts {
inputAmount: CurrencyAmount<Currency> inputAmount: CurrencyAmount<Currency>
...@@ -108,7 +108,7 @@ function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput { ...@@ -108,7 +108,7 @@ function buildTradeRouteInput(swap: Swap): TokenTradeRouteInput {
} }
} }
export function buildAllTradeRouteInputs(trade: InterfaceTrade): { export function buildAllTradeRouteInputs(trade: ClassicTrade): {
mixedTokenTradeRouteInputs?: TokenTradeRouteInput[] mixedTokenTradeRouteInputs?: TokenTradeRouteInput[]
v2TokenTradeRouteInputs?: TokenTradeRouteInput[] v2TokenTradeRouteInputs?: TokenTradeRouteInput[]
v3TokenTradeRouteInputs?: 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;
`
...@@ -14,54 +14,55 @@ import { ChainId, Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk- ...@@ -14,54 +14,55 @@ import { ChainId, Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer' import { useToggleAccountDrawer } from 'components/AccountDrawer'
import { sendEvent } from 'components/analytics' import AddressInputPanel from 'components/AddressInputPanel'
import { ButtonError, ButtonLight, ButtonPrimary } from 'components/Button'
import { GrayCard } from 'components/Card'
import { AutoColumn } from 'components/Column'
import SwapCurrencyInputPanel from 'components/CurrencyInputPanel/SwapCurrencyInputPanel'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert' import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import { AutoRow } from 'components/Row'
import confirmPriceImpactWithoutFee from 'components/swap/confirmPriceImpactWithoutFee'
import ConfirmSwapModal from 'components/swap/ConfirmSwapModal'
import PriceImpactModal from 'components/swap/PriceImpactModal' import PriceImpactModal from 'components/swap/PriceImpactModal'
import PriceImpactWarning from 'components/swap/PriceImpactWarning' import PriceImpactWarning from 'components/swap/PriceImpactWarning'
import { ArrowWrapper, PageWrapper, SwapWrapper } from 'components/swap/styleds'
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown' import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
import SwapHeader from 'components/swap/SwapHeader'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { asSupportedChain, isSupportedChain } from 'constants/chains' import { asSupportedChain, isSupportedChain } from 'constants/chains'
import useENSAddress from 'hooks/useENSAddress' import { getSwapCurrencyId, TOKEN_SHORTHANDS } from 'constants/tokens'
import { useCurrency, useDefaultActiveTokens } from 'hooks/Tokens'
import { useIsSwapUnsupported } from 'hooks/useIsSwapUnsupported'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn' import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance' import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
import { useSwapCallback } from 'hooks/useSwapCallback' import { SwapResult, useSwapCallback } from 'hooks/useSwapCallback'
import { useSwitchChain } from 'hooks/useSwitchChain' import { useSwitchChain } from 'hooks/useSwitchChain'
import { useUSDPrice } from 'hooks/useUSDPrice' import { useUSDPrice } from 'hooks/useUSDPrice'
import useWrapCallback, { WrapErrorText, WrapType } from 'hooks/useWrapCallback'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { formatEventPropertiesForTrade } from 'lib/utils/analytics' import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics'
import { ReactNode, useCallback, useEffect, useMemo, useReducer, useState } from 'react' import { ReactNode, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { ArrowDown } from 'react-feather' import { ArrowDown } from 'react-feather'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { useAppSelector } from 'state/hooks' import { useAppSelector } from 'state/hooks'
import { InterfaceTrade, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { Field, replaceSwapState } from 'state/swap/actions'
import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from 'state/swap/hooks'
import swapReducer, { initialState as initialSwapState, SwapState } from 'state/swap/reducer'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { LinkStyledButton, ThemedText } from 'theme'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { maxAmountSpend } from 'utils/maxAmountSpend'
import { computeRealizedPriceImpact, warningSeverity } from 'utils/prices'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage' import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
import AddressInputPanel from '../../components/AddressInputPanel' import { useScreenSize } from '../../hooks/useScreenSize'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' import { UniswapXOptIn } from './UniswapXOptIn'
import { GrayCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import SwapCurrencyInputPanel from '../../components/CurrencyInputPanel/SwapCurrencyInputPanel'
import { AutoRow } from '../../components/Row'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
import { ArrowWrapper, PageWrapper, SwapWrapper } from '../../components/swap/styleds'
import SwapHeader from '../../components/swap/SwapHeader'
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import { getSwapCurrencyId, TOKEN_SHORTHANDS } from '../../constants/tokens'
import { useCurrency, useDefaultActiveTokens } from '../../hooks/Tokens'
import { useIsSwapUnsupported } from '../../hooks/useIsSwapUnsupported'
import useWrapCallback, { WrapErrorText, WrapType } from '../../hooks/useWrapCallback'
import { Field, replaceSwapState } from '../../state/swap/actions'
import { useDefaultsFromURLSearch, useDerivedSwapInfo, useSwapActionHandlers } from '../../state/swap/hooks'
import swapReducer, { initialState as initialSwapState, SwapState } from '../../state/swap/reducer'
import { LinkStyledButton, ThemedText } from '../../theme'
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeRealizedPriceImpact, warningSeverity } from '../../utils/prices'
export const ArrowContainer = styled.div` export const ArrowContainer = styled.div`
display: inline-flex; display: inline-flex;
...@@ -116,7 +117,7 @@ function getIsValidSwapQuote( ...@@ -116,7 +117,7 @@ function getIsValidSwapQuote(
tradeState: TradeState, tradeState: TradeState,
swapInputError?: ReactNode swapInputError?: ReactNode
): boolean { ): boolean {
return Boolean(swapInputError && trade && tradeState === TradeState.VALID) return Boolean(!swapInputError && trade && tradeState === TradeState.VALID)
} }
function largerPercentValue(a?: Percent, b?: Percent) { function largerPercentValue(a?: Percent, b?: Percent) {
...@@ -130,8 +131,6 @@ function largerPercentValue(a?: Percent, b?: Percent) { ...@@ -130,8 +131,6 @@ function largerPercentValue(a?: Percent, b?: Percent) {
return undefined return undefined
} }
const TRADE_STRING = 'SwapRouter'
export default function SwapPage({ className }: { className?: string }) { export default function SwapPage({ className }: { className?: string }) {
const { chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const loadedUrlParams = useDefaultsFromURLSearch() const loadedUrlParams = useDefaultsFromURLSearch()
...@@ -258,20 +257,21 @@ export function Swap({ ...@@ -258,20 +257,21 @@ export function Swap({
tradeToConfirm: undefined, tradeToConfirm: undefined,
swapError: undefined, swapError: undefined,
showConfirm: false, showConfirm: false,
txHash: undefined, swapResult: undefined,
}) })
} }
}, [connectedChainId, prefilledState, previousConnectedChainId, previousPrefilledState]) }, [connectedChainId, prefilledState, previousConnectedChainId, previousPrefilledState])
const swapInfo = useDerivedSwapInfo(state, chainId)
const { const {
trade: { state: tradeState, trade, method }, trade: { state: tradeState, trade },
allowedSlippage, allowedSlippage,
autoSlippage, autoSlippage,
currencyBalances, currencyBalances,
parsedAmount, parsedAmount,
currencies, currencies,
inputError: swapInputError, inputError: swapInputError,
} = useDerivedSwapInfo(state, chainId) } = swapInfo
const { const {
wrapType, wrapType,
...@@ -279,7 +279,6 @@ export function Swap({ ...@@ -279,7 +279,6 @@ export function Swap({
inputError: wrapInputError, inputError: wrapInputError,
} = useWrapCallback(currencies[Field.INPUT], currencies[Field.OUTPUT], typedValue) } = useWrapCallback(currencies[Field.INPUT], currencies[Field.OUTPUT], typedValue)
const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE
const { address: recipientAddress } = useENSAddress(recipient)
const parsedAmounts = useMemo( const parsedAmounts = useMemo(
() => () =>
...@@ -301,7 +300,11 @@ export function Swap({ ...@@ -301,7 +300,11 @@ export function Swap({
const showFiatValueOutput = Boolean(parsedAmounts[Field.OUTPUT]) const showFiatValueOutput = Boolean(parsedAmounts[Field.OUTPUT])
const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo( const [routeNotFound, routeIsLoading, routeIsSyncing] = useMemo(
() => [!trade?.swaps, TradeState.LOADING === tradeState, TradeState.LOADING === tradeState && Boolean(trade)], () => [
tradeState === TradeState.NO_ROUTE_FOUND,
tradeState === TradeState.LOADING,
tradeState === TradeState.LOADING && Boolean(trade),
],
[trade, tradeState] [trade, tradeState]
) )
...@@ -309,14 +312,13 @@ export function Swap({ ...@@ -309,14 +312,13 @@ export function Swap({
const fiatValueTradeOutput = useUSDPrice(trade?.outputAmount) const fiatValueTradeOutput = useUSDPrice(trade?.outputAmount)
const stablecoinPriceImpact = useMemo( const stablecoinPriceImpact = useMemo(
() => () =>
routeIsSyncing || !trade routeIsSyncing || !isClassicTrade(trade)
? undefined ? undefined
: computeFiatValuePriceImpact(fiatValueTradeInput.data, fiatValueTradeOutput.data), : computeFiatValuePriceImpact(fiatValueTradeInput.data, fiatValueTradeOutput.data),
[fiatValueTradeInput, fiatValueTradeOutput, routeIsSyncing, trade] [fiatValueTradeInput, fiatValueTradeOutput, routeIsSyncing, trade]
) )
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers(dispatch) const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers(dispatch)
const isValid = !swapInputError
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const handleTypeInput = useCallback( const handleTypeInput = useCallback(
...@@ -342,16 +344,16 @@ export function Swap({ ...@@ -342,16 +344,16 @@ export function Swap({
}, [navigate]) }, [navigate])
// modal and loading // modal and loading
const [{ showConfirm, tradeToConfirm, swapError, txHash }, setSwapState] = useState<{ const [{ showConfirm, tradeToConfirm, swapError, swapResult }, setSwapState] = useState<{
showConfirm: boolean showConfirm: boolean
tradeToConfirm?: InterfaceTrade tradeToConfirm?: InterfaceTrade
swapError?: Error swapError?: Error
txHash?: string swapResult?: SwapResult
}>({ }>({
showConfirm: false, showConfirm: false,
tradeToConfirm: undefined, tradeToConfirm: undefined,
swapError: undefined, swapError: undefined,
txHash: undefined, swapResult: undefined,
}) })
const formattedAmounts = useMemo( const formattedAmounts = useMemo(
...@@ -374,7 +376,8 @@ export function Swap({ ...@@ -374,7 +376,8 @@ export function Swap({
(parsedAmounts[Field.INPUT]?.currency.isToken (parsedAmounts[Field.INPUT]?.currency.isToken
? (parsedAmounts[Field.INPUT] as CurrencyAmount<Token>) ? (parsedAmounts[Field.INPUT] as CurrencyAmount<Token>)
: undefined), : undefined),
isSupportedChain(chainId) ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined isSupportedChain(chainId) ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined,
trade?.fillType
) )
const maxInputAmount: CurrencyAmount<Currency> | undefined = useMemo( const maxInputAmount: CurrencyAmount<Currency> | undefined = useMemo(
...@@ -387,7 +390,7 @@ export function Swap({ ...@@ -387,7 +390,7 @@ export function Swap({
}, [fiatValueTradeInput, fiatValueTradeOutput]) }, [fiatValueTradeInput, fiatValueTradeOutput])
// the callback to execute the swap // the callback to execute the swap
const { callback: swapCallback } = useSwapCallback( const swapCallback = useSwapCallback(
trade, trade,
swapFiatValues, swapFiatValues,
allowedSlippage, allowedSlippage,
...@@ -399,7 +402,7 @@ export function Swap({ ...@@ -399,7 +402,7 @@ export function Swap({
tradeToConfirm: trade, tradeToConfirm: trade,
swapError: undefined, swapError: undefined,
showConfirm: true, showConfirm: true,
txHash: undefined, swapResult: undefined,
}) })
}, [trade]) }, [trade])
...@@ -413,61 +416,34 @@ export function Swap({ ...@@ -413,61 +416,34 @@ export function Swap({
setSwapState((currentState) => ({ setSwapState((currentState) => ({
...currentState, ...currentState,
swapError: undefined, swapError: undefined,
txHash: undefined, swapResult: undefined,
})) }))
swapCallback() swapCallback()
.then((hash) => { .then((result) => {
setSwapState((currentState) => ({ setSwapState((currentState) => ({
...currentState, ...currentState,
swapError: undefined, swapError: undefined,
txHash: hash, swapResult: result,
})) }))
sendEvent({
category: 'Swap',
action: 'transaction hash',
label: hash,
})
sendEvent({
category: 'Swap',
action:
recipient === null
? 'Swap w/o Send'
: (recipientAddress ?? recipient) === account
? 'Swap w/o Send + recipient'
: 'Swap w/ Send',
label: [TRADE_STRING, trade?.inputAmount?.currency?.symbol, trade?.outputAmount?.currency?.symbol, 'MH'].join(
'/'
),
})
}) })
.catch((error) => { .catch((error) => {
if (!didUserReject(error)) {
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, {
confirmedTrade: tradeToConfirm,
})
}
setSwapState((currentState) => ({ setSwapState((currentState) => ({
...currentState, ...currentState,
swapError: error, swapError: error,
txHash: undefined, swapResult: undefined,
})) }))
}) })
}, [ }, [swapCallback, stablecoinPriceImpact])
swapCallback,
stablecoinPriceImpact,
recipient,
recipientAddress,
account,
trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol,
tradeToConfirm,
])
// errors // errors
const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState<Date | undefined>() const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState<Date | undefined>()
// warnings on the greater of fiat value price impact and execution price impact // warnings on the greater of fiat value price impact and execution price impact
const { priceImpactSeverity, largerPriceImpact } = useMemo(() => { const { priceImpactSeverity, largerPriceImpact } = useMemo(() => {
if (isUniswapXTrade(trade)) {
return { priceImpactSeverity: 0, largerPriceImpact: undefined }
}
const marketPriceImpact = trade?.priceImpact ? computeRealizedPriceImpact(trade) : undefined const marketPriceImpact = trade?.priceImpact ? computeRealizedPriceImpact(trade) : undefined
const largerPriceImpact = largerPercentValue(marketPriceImpact, stablecoinPriceImpact) const largerPriceImpact = largerPercentValue(marketPriceImpact, stablecoinPriceImpact)
return { priceImpactSeverity: warningSeverity(largerPriceImpact), largerPriceImpact } return { priceImpactSeverity: warningSeverity(largerPriceImpact), largerPriceImpact }
...@@ -475,11 +451,11 @@ export function Swap({ ...@@ -475,11 +451,11 @@ export function Swap({
const handleConfirmDismiss = useCallback(() => { const handleConfirmDismiss = useCallback(() => {
setSwapState((currentState) => ({ ...currentState, showConfirm: false })) setSwapState((currentState) => ({ ...currentState, showConfirm: false }))
// if there was a tx hash, we want to clear the input // If there was a swap, we want to clear the input
if (txHash) { if (swapResult) {
onUserInput(Field.INPUT, '') onUserInput(Field.INPUT, '')
} }
}, [onUserInput, txHash]) }, [onUserInput, swapResult])
const handleAcceptChanges = useCallback(() => { const handleAcceptChanges = useCallback(() => {
setSwapState((currentState) => ({ ...currentState, tradeToConfirm: trade })) setSwapState((currentState) => ({ ...currentState, tradeToConfirm: trade }))
...@@ -500,10 +476,6 @@ export function Swap({ ...@@ -500,10 +476,6 @@ export function Swap({
const handleMaxInput = useCallback(() => { const handleMaxInput = useCallback(() => {
maxInputAmount && onUserInput(Field.INPUT, maxInputAmount.toExact()) maxInputAmount && onUserInput(Field.INPUT, maxInputAmount.toExact())
sendEvent({
category: 'Swap',
action: 'Max',
})
}, [maxInputAmount, onUserInput]) }, [maxInputAmount, onUserInput])
const handleOutputSelect = useCallback( const handleOutputSelect = useCallback(
...@@ -519,27 +491,30 @@ export function Swap({ ...@@ -519,27 +491,30 @@ export function Swap({
[onCurrencyChange, onCurrencySelection, state] [onCurrencyChange, onCurrencySelection, state]
) )
const showPriceImpactWarning = largerPriceImpact && priceImpactSeverity > 3 const showPriceImpactWarning = isClassicTrade(trade) && largerPriceImpact && priceImpactSeverity > 3
const prevTrade = usePrevious(trade) const prevTrade = usePrevious(trade)
useEffect(() => { useEffect(() => {
if (!trade || prevTrade === trade) return // no new swap quote to log if (!trade || prevTrade === trade) return // no new swap quote to log
setSwapQuoteReceivedDate(new Date()) const now = new Date()
setSwapQuoteReceivedDate(now)
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, { sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, {
...formatEventPropertiesForTrade(trade, allowedSlippage, trade.gasUseEstimateUSD ?? undefined, method), ...formatSwapQuoteReceivedEventProperties(trade, allowedSlippage, now),
...trace, ...trace,
}) })
}, [prevTrade, trade, trace, allowedSlippage, method]) }, [prevTrade, trade, trace, allowedSlippage])
const showDetailsDropdown = Boolean( const showDetailsDropdown = Boolean(
!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing) !showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)
) )
const inputCurrency = currencies[Field.INPUT] ?? undefined
const switchChain = useSwitchChain() const switchChain = useSwitchChain()
const switchingChain = useAppSelector((state) => state.wallets.switchingChain) const switchingChain = useAppSelector((state) => state.wallets.switchingChain)
const showOptInSmall = !useScreenSize().navSearchInputVisible
return ( const swapElement = (
<SwapWrapper chainId={chainId} className={className} id="swap-page"> <SwapWrapper chainId={chainId} className={className} id="swap-page">
<TokenSafetyModal <TokenSafetyModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning} isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
...@@ -549,13 +524,15 @@ export function Swap({ ...@@ -549,13 +524,15 @@ export function Swap({
onCancel={handleDismissTokenWarning} onCancel={handleDismissTokenWarning}
showCancel={true} showCancel={true}
/> />
<SwapHeader autoSlippage={autoSlippage} chainId={chainId} /> <SwapHeader trade={trade} autoSlippage={autoSlippage} chainId={chainId} />
{trade && showConfirm && ( {trade && showConfirm && (
<ConfirmSwapModal <ConfirmSwapModal
trade={trade} trade={trade}
inputCurrency={inputCurrency}
originalTrade={tradeToConfirm} originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges} onAcceptChanges={handleAcceptChanges}
txHash={txHash} onCurrencySelection={onCurrencySelection}
swapResult={swapResult}
allowedSlippage={allowedSlippage} allowedSlippage={allowedSlippage}
onConfirm={handleSwap} onConfirm={handleSwap}
allowance={allowance} allowance={allowance}
...@@ -707,7 +684,7 @@ export function Swap({ ...@@ -707,7 +684,7 @@ export function Swap({
) : showWrap ? ( ) : showWrap ? (
<ButtonPrimary <ButtonPrimary
disabled={Boolean(wrapInputError)} disabled={Boolean(wrapInputError)}
onClick={onWrap} onClick={() => onWrap?.().catch((e) => console.error('Could not wrap/unwrap', e))}
fontWeight={600} fontWeight={600}
data-testid="wrap-button" data-testid="wrap-button"
> >
...@@ -737,8 +714,8 @@ export function Swap({ ...@@ -737,8 +714,8 @@ export function Swap({
}} }}
id="swap-button" id="swap-button"
data-testid="swap-button" data-testid="swap-button"
disabled={!isValid || routeIsSyncing || routeIsLoading} disabled={!getIsValidSwapQuote(trade, tradeState, swapInputError)}
error={isValid && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED} error={!swapInputError && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED}
> >
<Text fontSize={20} fontWeight={600}> <Text fontSize={20} fontWeight={600}>
{swapInputError ? ( {swapInputError ? (
...@@ -756,6 +733,14 @@ export function Swap({ ...@@ -756,6 +733,14 @@ export function Swap({
)} )}
</div> </div>
</AutoColumn> </AutoColumn>
{!showOptInSmall && <UniswapXOptIn isSmall={false} swapInfo={swapInfo} />}
</SwapWrapper> </SwapWrapper>
) )
return (
<>
{swapElement}
{showOptInSmall && <UniswapXOptIn isSmall swapInfo={swapInfo} />}
</>
)
} }
...@@ -2,13 +2,23 @@ import { createSlice, nanoid } from '@reduxjs/toolkit' ...@@ -2,13 +2,23 @@ import { createSlice, nanoid } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc' import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc'
export enum PopupType {
Transaction = 'transaction',
Order = 'order',
FailedSwitchNetwork = 'failedSwitchNetwork',
}
export type PopupContent = export type PopupContent =
| { | {
txn: { type: PopupType.Transaction
hash: string hash: string
} }
| {
type: PopupType.Order
orderHash: string
} }
| { | {
type: PopupType.FailedSwitchNetwork
failedSwitchNetwork: ChainId failedSwitchNetwork: ChainId
} }
......
...@@ -6,9 +6,9 @@ import { isTestEnv } from 'utils/env' ...@@ -6,9 +6,9 @@ import { isTestEnv } from 'utils/env'
import { updateVersion } from './global/actions' import { updateVersion } from './global/actions'
import { sentryEnhancer } from './logging' import { sentryEnhancer } from './logging'
import reducer from './reducer' import reducer from './reducer'
import { routingApiV2 } from './routing/slice' import { routingApi } from './routing/slice'
const PERSISTED_KEYS: string[] = ['user', 'transactions', 'lists'] const PERSISTED_KEYS: string[] = ['user', 'transactions', 'signatures', 'lists']
const store = configureStore({ const store = configureStore({
reducer, reducer,
...@@ -20,10 +20,10 @@ const store = configureStore({ ...@@ -20,10 +20,10 @@ const store = configureStore({
// meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok // meta.arg and meta.baseQueryMeta are defaults. payload.trade is a nonserializable return value, but that's ok
// because we are not adding it into any persisted store that requires serialization (e.g. localStorage) // because we are not adding it into any persisted store that requires serialization (e.g. localStorage)
ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'], ignoredActionPaths: ['meta.arg', 'meta.baseQueryMeta', 'payload.trade'],
ignoredPaths: [routingApiV2.reducerPath], ignoredPaths: [routingApi.reducerPath],
}, },
}) })
.concat(routingApiV2.middleware) .concat(routingApi.middleware)
.concat(save({ states: PERSISTED_KEYS, debounce: 1000 })), .concat(save({ states: PERSISTED_KEYS, debounce: 1000 })),
preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }), preloadedState: load({ states: PERSISTED_KEYS, disableWarnings: isTestEnv() }),
}) })
......
...@@ -7,7 +7,8 @@ import lists from './lists/reducer' ...@@ -7,7 +7,8 @@ import lists from './lists/reducer'
import logs from './logs/slice' import logs from './logs/slice'
import mint from './mint/reducer' import mint from './mint/reducer'
import mintV3 from './mint/v3/reducer' import mintV3 from './mint/v3/reducer'
import { routingApiV2 } from './routing/slice' import { routingApi } from './routing/slice'
import signatures from './signatures/reducer'
import transactions from './transactions/reducer' import transactions from './transactions/reducer'
import user from './user/reducer' import user from './user/reducer'
import wallets from './wallets/reducer' import wallets from './wallets/reducer'
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
application, application,
user, user,
transactions, transactions,
signatures,
wallets, wallets,
mint, mint,
mintV3, mintV3,
...@@ -24,5 +26,5 @@ export default { ...@@ -24,5 +26,5 @@ export default {
multicall: multicall.reducer, multicall: multicall.reducer,
lists, lists,
logs, logs,
[routingApiV2.reducerPath]: routingApiV2.reducer, [routingApi.reducerPath]: routingApi.reducer,
} }
import { MaxUint256, PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { Currency, SupportedChainsType } from '@uniswap/sdk-core'
import ERC20_ABI from 'abis/erc20.json'
import { Erc20, Weth } from 'abis/types'
import WETH_ABI from 'abis/weth.json'
import { RPC_PROVIDERS } from 'constants/providers'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { getContract } from 'utils'
import { ApproveInfo, WrapInfo } from './types'
// TODO(UniswapX): add fallback gas limits per chain? l2s have higher costs
const WRAP_FALLBACK_GAS_LIMIT = 45_000
const APPROVE_FALLBACK_GAS_LIMIT = 65_000
export async function getApproveInfo(
account: string | undefined,
currency: Currency,
amount: string,
usdCostPerGas?: number
): Promise<ApproveInfo> {
// native currencies do not need token approvals
if (currency.isNative) return { needsApprove: false }
// If any of these arguments aren't provided, then we cannot generate approval cost info
if (!account || !usdCostPerGas) return { needsApprove: false }
const provider = RPC_PROVIDERS[currency.chainId as SupportedChainsType]
const tokenContract = getContract(currency.address, ERC20_ABI, provider) as Erc20
let approveGasUseEstimate
try {
const allowance = await tokenContract.callStatic.allowance(account, PERMIT2_ADDRESS)
if (!allowance.lt(amount)) return { needsApprove: false }
} catch (_) {
// If contract lookup fails (eg if Infura goes down), then don't show gas info for approving the token
return { needsApprove: false }
}
try {
const approveTx = await tokenContract.populateTransaction.approve(PERMIT2_ADDRESS, MaxUint256)
approveGasUseEstimate = (await provider.estimateGas({ from: account, ...approveTx })).toNumber()
} catch (_) {
// estimateGas will error if the account doesn't have sufficient token balance, but we should show an estimated cost anyway
approveGasUseEstimate = APPROVE_FALLBACK_GAS_LIMIT
}
return { needsApprove: true, approveGasEstimateUSD: approveGasUseEstimate * usdCostPerGas }
}
export async function getWrapInfo(
needsWrap: boolean,
account: string | undefined,
chainId: SupportedChainsType,
amount: string,
usdCostPerGas?: number
): Promise<WrapInfo> {
if (!needsWrap) return { needsWrap: false }
const provider = RPC_PROVIDERS[chainId]
const wethAddress = WRAPPED_NATIVE_CURRENCY[chainId]?.address
// If any of these arguments aren't provided, then we cannot generate wrap cost info
if (!wethAddress || !usdCostPerGas) return { needsWrap: false }
let wrapGasUseEstimate
try {
const wethContract = getContract(wethAddress, WETH_ABI, provider, account) as Weth
const wethTx = await wethContract.populateTransaction.deposit({ value: amount })
// estimateGas will error if the account doesn't have sufficient ETH balance, but we should show an estimated cost anyway
wrapGasUseEstimate = (await provider.estimateGas({ from: account, ...wethTx })).toNumber()
} catch (_) {
wrapGasUseEstimate = WRAP_FALLBACK_GAS_LIMIT
}
return { needsWrap: true, wrapGasEstimateUSD: wrapGasUseEstimate * usdCostPerGas }
}
import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react' import { createApi, fetchBaseQuery, FetchBaseQueryError } from '@reduxjs/toolkit/query/react'
import { Protocol } from '@uniswap/router-sdk' import { Protocol } from '@uniswap/router-sdk'
import { TradeType } from '@uniswap/sdk-core' import { ChainId, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/sdk-core' import { isUniswapXSupportedChain } from 'constants/chains'
import { getClientSideQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter' import { getClientSideQuote } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro' import ms from 'ms.macro'
import { trace } from 'tracing/trace' import { trace } from 'tracing/trace'
import { QuoteMethod, QuoteReponse, QuoteState, TradeResult } from './types' import {
QuoteMethod,
QuoteState,
RoutingConfig,
SwapRouterNativeAssets,
TradeResult,
URAQuoteResponse,
URAQuoteType,
} from './types'
import { getRouter, isExactInput, shouldUseAPIRouter, transformRoutesToTrade } from './utils' import { getRouter, isExactInput, shouldUseAPIRouter, transformRoutesToTrade } from './utils'
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 enum RouterPreference {
X = 'uniswapx',
API = 'api',
CLIENT = 'client',
}
// This is excluded from `RouterPreference` enum because it's only used
// internally for token -> USDC trades to get a USD value.
export const INTERNAL_ROUTER_PREFERENCE_PRICE = 'price' as const
const CLIENT_PARAMS = {
protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED],
}
export interface GetQuoteArgs { export interface GetQuoteArgs {
tokenInAddress: string tokenInAddress: string
tokenInChainId: ChainId tokenInChainId: ChainId
...@@ -19,41 +46,65 @@ export interface GetQuoteArgs { ...@@ -19,41 +46,65 @@ export interface GetQuoteArgs {
tokenOutDecimals: number tokenOutDecimals: number
tokenOutSymbol?: string tokenOutSymbol?: string
amount: string amount: string
account?: string
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
tradeType: TradeType tradeType: TradeType
needsWrapIfUniswapX: boolean
uniswapXEnabled: boolean
uniswapXForceSyntheticQuotes: boolean
isRoutingAPIPrice?: boolean isRoutingAPIPrice?: boolean
} }
export enum RouterPreference { const protocols: Protocol[] = [Protocol.V2, Protocol.V3, Protocol.MIXED]
AUTO = 'auto',
API = 'api', // routing API quote query params: https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/schema/quote-schema.ts
CLIENT = 'client', const DEFAULT_QUERY_PARAMS = {
protocols,
} }
// This is excluded from `RouterPreference` enum because it's only used function getRoutingAPIConfig(args: GetQuoteArgs): RoutingConfig {
// internally for token -> USDC trades to get a USD value. const { account, tradeType, tokenOutAddress, tokenInChainId, uniswapXForceSyntheticQuotes } = args
export const INTERNAL_ROUTER_PREFERENCE_PRICE = 'price' as const
const CLIENT_PARAMS = { const uniswapx = {
protocols: [Protocol.V2, Protocol.V3, Protocol.MIXED], useSyntheticQuotes: uniswapXForceSyntheticQuotes,
} // Protocol supports swap+send to different destination address, but
// for now recipient === swapper
recipient: account,
swapper: account,
routingType: URAQuoteType.DUTCH_LIMIT,
}
// routing API quote query params: https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/schema/quote-schema.ts const classic = {
const CLASSIC_SWAP_QUERY_PARAMS = { ...DEFAULT_QUERY_PARAMS,
...CLIENT_PARAMS, routingType: URAQuoteType.CLASSIC,
routingType: 'CLASSIC', }
const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets)
// UniswapX doesn't support native out, exact-out, or non-mainnet trades (yet),
// so even if the user has selected UniswapX as their router preference, force them to receive a Classic quote.
if (
!args.uniswapXEnabled ||
tokenOutIsNative ||
tradeType === TradeType.EXACT_OUTPUT ||
!isUniswapXSupportedChain(tokenInChainId)
) {
return [classic]
}
return [uniswapx, classic]
} }
export const routingApiV2 = createApi({ export const routingApi = createApi({
reducerPath: 'routingApiV2', reducerPath: 'routingApi',
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
baseUrl: 'https://api.uniswap.org/v2/', baseUrl: UNISWAP_API_URL,
}), }),
endpoints: (build) => ({ endpoints: (build) => ({
getQuote: build.query<TradeResult, GetQuoteArgs>({ getQuote: build.query<TradeResult, GetQuoteArgs>({
async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) { async onQueryStarted(args: GetQuoteArgs, { queryFulfilled }) {
trace( trace(
'quote-v2', 'quote',
async ({ setTraceError, setTraceStatus }) => { async ({ setTraceError, setTraceStatus }) => {
try { try {
await queryFulfilled await queryFulfilled
...@@ -73,16 +124,14 @@ export const routingApiV2 = createApi({ ...@@ -73,16 +124,14 @@ export const routingApiV2 = createApi({
data: { data: {
...args, ...args,
isPrice: args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE, isPrice: args.routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE,
isAutoRouter: isAutoRouter: args.routerPreference === RouterPreference.API,
args.routerPreference === RouterPreference.AUTO || args.routerPreference === RouterPreference.API,
}, },
} }
) )
}, },
async queryFn(args: GetQuoteArgs, _api, _extraOptions, fetch) { async queryFn(args, _api, _extraOptions, fetch) {
let fellBack = false const fellBack = false
if (shouldUseAPIRouter(args)) { if (shouldUseAPIRouter(args)) {
fellBack = true
try { try {
const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args const { tokenInAddress, tokenInChainId, tokenOutAddress, tokenOutChainId, amount, tradeType } = args
const type = isExactInput(tradeType) ? 'EXACT_INPUT' : 'EXACT_OUTPUT' const type = isExactInput(tradeType) ? 'EXACT_INPUT' : 'EXACT_OUTPUT'
...@@ -94,7 +143,7 @@ export const routingApiV2 = createApi({ ...@@ -94,7 +143,7 @@ export const routingApiV2 = createApi({
tokenOut: tokenOutAddress, tokenOut: tokenOutAddress,
amount, amount,
type, type,
configs: [CLASSIC_SWAP_QUERY_PARAMS], configs: getRoutingAPIConfig(args),
} }
const response = await fetch({ const response = await fetch({
...@@ -119,13 +168,15 @@ export const routingApiV2 = createApi({ ...@@ -119,13 +168,15 @@ export const routingApiV2 = createApi({
} }
} }
const quoteData = response.data as QuoteReponse const uraQuoteResponse = response.data as URAQuoteResponse
const tradeResult = transformRoutesToTrade(args, quoteData.quote) const tradeResult = await transformRoutesToTrade(args, uraQuoteResponse, QuoteMethod.ROUTING_API)
return { data: { ...tradeResult, method: QuoteMethod.ROUTING_API } } return { data: tradeResult }
} catch (error: any) { } catch (error: any) {
console.warn( console.warn(
`GetQuote failed on API v2, falling back to client: ${error?.message ?? error?.detail ?? error}` `GetQuote failed on Unified Routing API, falling back to client: ${
error?.message ?? error?.detail ?? error
}`
) )
} }
} }
...@@ -134,7 +185,9 @@ export const routingApiV2 = createApi({ ...@@ -134,7 +185,9 @@ export const routingApiV2 = createApi({
const router = getRouter(args.tokenInChainId) const router = getRouter(args.tokenInChainId)
const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS) const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS)
if (quoteResult.state === QuoteState.SUCCESS) { if (quoteResult.state === QuoteState.SUCCESS) {
return { data: { ...transformRoutesToTrade(args, quoteResult.data), method } } return {
data: await transformRoutesToTrade(args, quoteResult.data, method),
}
} else { } else {
return { data: quoteResult } return { data: quoteResult }
} }
...@@ -144,8 +197,11 @@ export const routingApiV2 = createApi({ ...@@ -144,8 +197,11 @@ export const routingApiV2 = createApi({
} }
}, },
keepUnusedDataFor: ms`10s`, keepUnusedDataFor: ms`10s`,
extraOptions: {
maxRetries: 0,
},
}), }),
}), }),
}) })
export const { useGetQuoteQuery } = routingApiV2 export const { useGetQuoteQuery } = routingApi
import { MixedRouteSDK, Trade } from '@uniswap/router-sdk' import { MixedRouteSDK, Protocol, Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { DutchOrderInfo, DutchOrderInfoJSON, DutchOrderTrade as IDutchOrderTrade } from '@uniswap/uniswapx-sdk'
import { Route as V2Route } from '@uniswap/v2-sdk' import { Route as V2Route } from '@uniswap/v2-sdk'
import { Route as V3Route } from '@uniswap/v3-sdk' import { Route as V3Route } from '@uniswap/v3-sdk'
import { RouterPreference } from './slice'
export enum TradeState { export enum TradeState {
LOADING, LOADING,
INVALID, INVALID,
...@@ -57,8 +56,9 @@ export type V2PoolInRoute = { ...@@ -57,8 +56,9 @@ export type V2PoolInRoute = {
address?: string address?: string
} }
export interface QuoteData { export interface ClassicQuoteData {
quoteId?: string quoteId?: string
requestId?: string
blockNumber: string blockNumber: string
amount: string amount: string
amountDecimals: string amountDecimals: string
...@@ -76,50 +76,163 @@ export interface QuoteData { ...@@ -76,50 +76,163 @@ export interface QuoteData {
routeString: string routeString: string
} }
export type QuoteReponse = { type URADutchOrderQuoteResponse = {
routing: RouterPreference.API routing: URAQuoteType.DUTCH_LIMIT
quote: QuoteData quote: {
auctionPeriodSecs: number
deadlineBufferSecs: number
orderInfo: DutchOrderInfoJSON
quoteId?: string
requestId?: string
slippageTolerance: string
}
allQuotes: Array<URAQuoteResponse>
}
type URAClassicQuoteResponse = {
routing: URAQuoteType.CLASSIC
quote: ClassicQuoteData
allQuotes: Array<URAQuoteResponse>
}
export type URAQuoteResponse = URAClassicQuoteResponse | URADutchOrderQuoteResponse
export function isClassicQuoteResponse(data: URAQuoteResponse): data is URAClassicQuoteResponse {
return data.routing === URAQuoteType.CLASSIC
}
export enum TradeFillType {
Classic = 'classic', // Uniswap V1, V2, and V3 trades with on-chain routes
UniswapX = 'uniswap_x', // off-chain trades, no routes
} }
export class ClassicTrade< export type ApproveInfo = { needsApprove: true; approveGasEstimateUSD: number } | { needsApprove: false }
TInput extends Currency, export type WrapInfo = { needsWrap: true; wrapGasEstimateUSD: number } | { needsWrap: false }
TOutput extends Currency,
TTradeType extends TradeType export class ClassicTrade extends Trade<Currency, Currency, TradeType> {
> extends Trade<TInput, TOutput, TTradeType> { public readonly fillType = TradeFillType.Classic
gasUseEstimateUSD: string | null | undefined approveInfo: ApproveInfo
gasUseEstimateUSD?: number // gas estimate for swaps
blockNumber: string | null | undefined blockNumber: string | null | undefined
isUniswapXBetter: boolean | undefined
fromClientRouter: boolean | undefined
requestId: string | undefined
quoteMethod: QuoteMethod
constructor({ constructor({
gasUseEstimateUSD, gasUseEstimateUSD,
blockNumber, blockNumber,
isUniswapXBetter,
requestId,
quoteMethod,
approveInfo,
...routes ...routes
}: { }: {
gasUseEstimateUSD?: string | null gasUseEstimateUSD?: number
totalGasUseEstimateUSD?: number
blockNumber?: string | null blockNumber?: string | null
isUniswapXBetter?: boolean
requestId?: string
quoteMethod: QuoteMethod
fromClientRouter?: boolean
approveInfo: ApproveInfo
v2Routes: { v2Routes: {
routev2: V2Route<TInput, TOutput> routev2: V2Route<Currency, Currency>
inputAmount: CurrencyAmount<TInput> inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<TOutput> outputAmount: CurrencyAmount<Currency>
}[] }[]
v3Routes: { v3Routes: {
routev3: V3Route<TInput, TOutput> routev3: V3Route<Currency, Currency>
inputAmount: CurrencyAmount<TInput> inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<TOutput> outputAmount: CurrencyAmount<Currency>
}[] }[]
tradeType: TTradeType tradeType: TradeType
mixedRoutes?: { mixedRoutes?: {
mixedRoute: MixedRouteSDK<TInput, TOutput> mixedRoute: MixedRouteSDK<Currency, Currency>
inputAmount: CurrencyAmount<TInput> inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<TOutput> outputAmount: CurrencyAmount<Currency>
}[] }[]
}) { }) {
super(routes) super(routes)
this.blockNumber = blockNumber this.blockNumber = blockNumber
this.gasUseEstimateUSD = gasUseEstimateUSD this.gasUseEstimateUSD = gasUseEstimateUSD
this.isUniswapXBetter = isUniswapXBetter
this.requestId = requestId
this.quoteMethod = quoteMethod
this.approveInfo = approveInfo
}
// gas estimate for maybe approve + swap
public get totalGasUseEstimateUSD(): number | undefined {
if (this.approveInfo.needsApprove && this.gasUseEstimateUSD) {
return this.approveInfo.approveGasEstimateUSD + this.gasUseEstimateUSD
}
return this.gasUseEstimateUSD
}
}
export class DutchOrderTrade extends IDutchOrderTrade<Currency, Currency, TradeType> {
public readonly fillType = TradeFillType.UniswapX
quoteId?: string
requestId?: string
wrapInfo: WrapInfo
approveInfo: ApproveInfo
// The gas estimate of the reference classic trade, if there is one.
classicGasUseEstimateUSD?: number
auctionPeriodSecs: number
deadlineBufferSecs: number
slippageTolerance: Percent
constructor({
currencyIn,
currenciesOut,
orderInfo,
tradeType,
quoteId,
requestId,
wrapInfo,
approveInfo,
classicGasUseEstimateUSD,
auctionPeriodSecs,
deadlineBufferSecs,
slippageTolerance,
}: {
currencyIn: Currency
currenciesOut: Currency[]
orderInfo: DutchOrderInfo
tradeType: TradeType
quoteId?: string
requestId?: string
approveInfo: ApproveInfo
wrapInfo: WrapInfo
classicGasUseEstimateUSD?: number
auctionPeriodSecs: number
deadlineBufferSecs: number
slippageTolerance: Percent
}) {
super({ currencyIn, currenciesOut, orderInfo, tradeType })
this.quoteId = quoteId
this.requestId = requestId
this.approveInfo = approveInfo
this.wrapInfo = wrapInfo
this.classicGasUseEstimateUSD = classicGasUseEstimateUSD
this.auctionPeriodSecs = auctionPeriodSecs
this.deadlineBufferSecs = deadlineBufferSecs
this.slippageTolerance = slippageTolerance
}
public get totalGasUseEstimateUSD(): number {
if (this.wrapInfo.needsWrap && this.approveInfo.needsApprove) {
return this.wrapInfo.wrapGasEstimateUSD + this.approveInfo.approveGasEstimateUSD
}
if (this.wrapInfo.needsWrap) return this.wrapInfo.wrapGasEstimateUSD
if (this.approveInfo.needsApprove) return this.approveInfo.approveGasEstimateUSD
return 0
} }
} }
export type InterfaceTrade = ClassicTrade<Currency, Currency, TradeType> export type InterfaceTrade = ClassicTrade | DutchOrderTrade
export enum QuoteState { export enum QuoteState {
SUCCESS = 'Success', SUCCESS = 'Success',
...@@ -133,19 +246,17 @@ export type QuoteResult = ...@@ -133,19 +246,17 @@ export type QuoteResult =
} }
| { | {
state: QuoteState.SUCCESS state: QuoteState.SUCCESS
data: QuoteData data: URAQuoteResponse
} }
export type TradeResult = export type TradeResult =
| { | {
state: QuoteState.NOT_FOUND state: QuoteState.NOT_FOUND
trade?: undefined trade?: undefined
method?: QuoteMethod
} }
| { | {
state: QuoteState.SUCCESS state: QuoteState.SUCCESS
trade: InterfaceTrade trade: InterfaceTrade
method?: QuoteMethod
} }
export enum PoolType { export enum PoolType {
...@@ -162,3 +273,20 @@ export enum SwapRouterNativeAssets { ...@@ -162,3 +273,20 @@ export enum SwapRouterNativeAssets {
AVAX = 'AVAX', AVAX = 'AVAX',
ETH = 'ETH', ETH = 'ETH',
} }
export enum URAQuoteType {
CLASSIC = 'CLASSIC',
DUTCH_LIMIT = 'DUTCH_LIMIT',
}
type ClassicAPIConfig = {
protocols: Protocol[]
}
type UniswapXConfig = {
swapper?: string
exclusivityOverrideBps?: number
auctionPeriodSecs?: number
}
export type RoutingConfig = (UniswapXConfig | ClassicAPIConfig)[]
...@@ -7,13 +7,37 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments ...@@ -7,13 +7,37 @@ import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments
import ms from 'ms.macro' import ms from 'ms.macro'
import { useMemo } from 'react' import { useMemo } from 'react'
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice' import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
import { useGetQuoteQuery as useGetQuoteQueryV2 } from 'state/routing/slice' import { useGetQuoteQuery } from 'state/routing/slice'
import { InterfaceTrade, QuoteMethod, QuoteState, TradeState } from './types' import { ClassicTrade, InterfaceTrade, QuoteMethod, QuoteState, TradeState } from './types'
const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const
const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const
export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined,
routerPreference: typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
skipFetch?: boolean,
account?: string
): {
state: TradeState
trade?: ClassicTrade
}
export function useRoutingAPITrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined,
routerPreference: RouterPreference,
skipFetch?: boolean,
account?: string
): {
state: TradeState
trade?: InterfaceTrade
}
/** /**
* Returns the best trade by invoking the routing api or the smart order router on the client * Returns the best trade by invoking the routing api or the smart order router on the client
* @param tradeType whether the swap is an exact in/out * @param tradeType whether the swap is an exact in/out
...@@ -25,7 +49,8 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -25,7 +49,8 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
amountSpecified: CurrencyAmount<Currency> | undefined, amountSpecified: CurrencyAmount<Currency> | undefined,
otherCurrency: Currency | undefined, otherCurrency: Currency | undefined,
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE, routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE,
skipFetch = false skipFetch = false,
account?: string
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: InterfaceTrade
...@@ -40,6 +65,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -40,6 +65,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
) )
const queryArgs = useRoutingAPIArguments({ const queryArgs = useRoutingAPIArguments({
account,
tokenIn: currencyIn, tokenIn: currencyIn,
tokenOut: currencyOut, tokenOut: currencyOut,
amount: skipFetch ? undefined : amountSpecified, amount: skipFetch ? undefined : amountSpecified,
...@@ -50,8 +76,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -50,8 +76,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
const { const {
isError, isError,
data: tradeResult, data: tradeResult,
error,
currentData: currentTradeResult, currentData: currentTradeResult,
} = useGetQuoteQueryV2(queryArgs ?? skipToken, { } = useGetQuoteQuery(queryArgs ?? skipToken, {
// Price-fetching is informational and costly, so it's done less frequently. // Price-fetching is informational and costly, so it's done less frequently.
pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME, pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms`1m` : AVERAGE_L1_BLOCK_TIME,
// If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period // If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period
...@@ -64,7 +91,11 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -64,7 +91,11 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
// If we don't want to fetch new trades, but have valid inputs, return the stale trade. // If we don't want to fetch new trades, but have valid inputs, return the stale trade.
return { state: TradeState.STALE, trade: tradeResult?.trade } return { state: TradeState.STALE, trade: tradeResult?.trade }
} else if (!amountSpecified || isError || !queryArgs) { } else if (!amountSpecified || isError || !queryArgs) {
return { state: TradeState.INVALID, trade: undefined } return {
state: TradeState.INVALID,
trade: undefined,
error: JSON.stringify(error),
}
} else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) { } else if (tradeResult?.state === QuoteState.NOT_FOUND && isCurrent) {
return TRADE_NOT_FOUND return TRADE_NOT_FOUND
} else if (!tradeResult?.trade) { } else if (!tradeResult?.trade) {
...@@ -74,19 +105,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -74,19 +105,9 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return { return {
state: isCurrent ? TradeState.VALID : TradeState.LOADING, state: isCurrent ? TradeState.VALID : TradeState.LOADING,
trade: tradeResult.trade, trade: tradeResult.trade,
method: tradeResult?.method,
} }
} }
}, [ }, [amountSpecified, error, isCurrent, isError, queryArgs, skipFetch, tradeResult?.state, tradeResult?.trade])
amountSpecified,
isCurrent,
isError,
queryArgs,
skipFetch,
tradeResult?.state,
tradeResult?.trade,
tradeResult?.method,
])
} }
// only want to enable this when app hook called // only want to enable this when app hook called
......
...@@ -15,12 +15,12 @@ const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString( ...@@ -15,12 +15,12 @@ const amount = (raw: TemplateStringsArray) => (parseInt(raw[0]) * 1e6).toString(
describe('#useRoute', () => { describe('#useRoute', () => {
it('handles empty edges and nodes', () => { it('handles empty edges and nodes', () => {
const result = computeRoutes(false, false, []) const result = computeRoutes(USDC, DAI, [])
expect(result).toEqual([]) expect(result).toEqual([])
}) })
it('handles a single route trade from DAI to USDC from v3', () => { it('handles a single route trade from DAI to USDC from v3', () => {
const result = computeRoutes(false, false, [ const result = computeRoutes(DAI, USDC, [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
...@@ -50,7 +50,7 @@ describe('#useRoute', () => { ...@@ -50,7 +50,7 @@ describe('#useRoute', () => {
}) })
it('handles a single route trade from DAI to USDC from v2', () => { it('handles a single route trade from DAI to USDC from v2', () => {
const result = computeRoutes(false, false, [ const result = computeRoutes(DAI, USDC, [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
...@@ -84,7 +84,7 @@ describe('#useRoute', () => { ...@@ -84,7 +84,7 @@ describe('#useRoute', () => {
}) })
it('handles a multi-route trade from DAI to USDC', () => { it('handles a multi-route trade from DAI to USDC', () => {
const result = computeRoutes(false, false, [ const result = computeRoutes(DAI, USDC, [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
...@@ -151,7 +151,7 @@ describe('#useRoute', () => { ...@@ -151,7 +151,7 @@ describe('#useRoute', () => {
}) })
it('handles a single route trade with same token pair, different fee tiers', () => { it('handles a single route trade with same token pair, different fee tiers', () => {
const result = computeRoutes(false, false, [ const result = computeRoutes(DAI, USDC, [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
...@@ -191,7 +191,7 @@ describe('#useRoute', () => { ...@@ -191,7 +191,7 @@ describe('#useRoute', () => {
}) })
it('computes mixed routes correctly', () => { it('computes mixed routes correctly', () => {
const result = computeRoutes(false, false, [ const result = computeRoutes(DAI, MKR, [
[ [
{ {
type: PoolType.V3Pool, type: PoolType.V3Pool,
...@@ -236,7 +236,7 @@ describe('#useRoute', () => { ...@@ -236,7 +236,7 @@ describe('#useRoute', () => {
it('outputs native ETH as input currency', () => { it('outputs native ETH as input currency', () => {
const WETH = ETH.wrapped const WETH = ETH.wrapped
const result = computeRoutes(true, false, [ const result = computeRoutes(ETH, USDC, [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
...@@ -263,7 +263,7 @@ describe('#useRoute', () => { ...@@ -263,7 +263,7 @@ describe('#useRoute', () => {
it('outputs native ETH as output currency', () => { it('outputs native ETH as output currency', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
const result = computeRoutes(false, true, [ const result = computeRoutes(USDC, ETH, [
[ [
{ {
type: 'v3-pool', type: 'v3-pool',
...@@ -290,7 +290,7 @@ describe('#useRoute', () => { ...@@ -290,7 +290,7 @@ describe('#useRoute', () => {
it('outputs native ETH as input currency for v2 routes', () => { it('outputs native ETH as input currency for v2 routes', () => {
const WETH = ETH.wrapped const WETH = ETH.wrapped
const result = computeRoutes(true, false, [ const result = computeRoutes(ETH, USDC, [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
...@@ -321,7 +321,7 @@ describe('#useRoute', () => { ...@@ -321,7 +321,7 @@ describe('#useRoute', () => {
it('outputs native ETH as output currency for v2 routes', () => { it('outputs native ETH as output currency for v2 routes', () => {
const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH') const WETH = new Token(1, ETH.wrapped.address, 18, 'WETH')
const result = computeRoutes(false, true, [ const result = computeRoutes(USDC, ETH, [
[ [
{ {
type: 'v2-pool', type: 'v2-pool',
......
import { BigNumber } from '@ethersproject/bignumber'
import { MixedRouteSDK } from '@uniswap/router-sdk' import { MixedRouteSDK } from '@uniswap/router-sdk'
import { ChainId, Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' import { ChainId, Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { AlphaRouter } from '@uniswap/smart-order-router' import { AlphaRouter } from '@uniswap/smart-order-router'
import { DutchOrderInfo, DutchOrderInfoJSON } from '@uniswap/uniswapx-sdk'
import { Pair, Route as V2Route } from '@uniswap/v2-sdk' import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk' import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
import { asSupportedChain } from 'constants/chains' import { asSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { RPC_PROVIDERS } from 'constants/providers'
import { isAvalanche, isBsc, isMatic, nativeOnChain } from 'constants/tokens' import { isAvalanche, isBsc, isMatic, nativeOnChain } from 'constants/tokens'
import { toSlippagePercent } from 'utils/slippage'
import { getApproveInfo, getWrapInfo } from './gas'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from './slice' import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from './slice'
import { import {
ClassicQuoteData,
ClassicTrade, ClassicTrade,
DutchOrderTrade,
InterfaceTrade,
isClassicQuoteResponse,
PoolType, PoolType,
QuoteData, QuoteMethod,
QuoteState, QuoteState,
SwapRouterNativeAssets, SwapRouterNativeAssets,
TradeFillType,
TradeResult, TradeResult,
URAQuoteResponse,
URAQuoteType,
V2PoolInRoute, V2PoolInRoute,
V3PoolInRoute, V3PoolInRoute,
} from './types' } from './types'
interface RouteResult {
routev3: V3Route<Currency, Currency> | null
routev2: V2Route<Currency, Currency> | null
mixedRoute: MixedRouteSDK<Currency, Currency> | null
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}
const routers = new Map<ChainId, AlphaRouter>() const routers = new Map<ChainId, AlphaRouter>()
export function getRouter(chainId: ChainId): AlphaRouter { export function getRouter(chainId: ChainId): AlphaRouter {
const router = routers.get(chainId) const router = routers.get(chainId)
...@@ -40,27 +59,16 @@ export function getRouter(chainId: ChainId): AlphaRouter { ...@@ -40,27 +59,16 @@ export function getRouter(chainId: ChainId): AlphaRouter {
* create a `Trade`. * create a `Trade`.
*/ */
export function computeRoutes( export function computeRoutes(
tokenInIsNative: boolean, currencyIn: Currency,
tokenOutIsNative: boolean, currencyOut: Currency,
routes: QuoteData['route'] routes: ClassicQuoteData['route']
): ): RouteResult[] | undefined {
| {
routev3: V3Route<Currency, Currency> | null
routev2: V2Route<Currency, Currency> | null
mixedRoute: MixedRouteSDK<Currency, Currency> | null
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
}[]
| undefined {
if (routes.length === 0) return [] if (routes.length === 0) return []
const tokenIn = routes[0]?.[0]?.tokenIn const tokenIn = routes[0]?.[0]?.tokenIn
const tokenOut = routes[0]?.[routes[0]?.length - 1]?.tokenOut const tokenOut = routes[0]?.[routes[0]?.length - 1]?.tokenOut
if (!tokenIn || !tokenOut) throw new Error('Expected both tokenIn and tokenOut to be present') if (!tokenIn || !tokenOut) throw new Error('Expected both tokenIn and tokenOut to be present')
const parsedCurrencyIn = tokenInIsNative ? nativeOnChain(tokenIn.chainId) : parseToken(tokenIn)
const parsedCurrencyOut = tokenOutIsNative ? nativeOnChain(tokenOut.chainId) : parseToken(tokenOut)
try { try {
return routes.map((route) => { return routes.map((route) => {
if (route.length === 0) { if (route.length === 0) {
...@@ -77,14 +85,12 @@ export function computeRoutes( ...@@ -77,14 +85,12 @@ export function computeRoutes(
const isOnlyV3 = isVersionedRoute<V3PoolInRoute>(PoolType.V3Pool, route) const isOnlyV3 = isVersionedRoute<V3PoolInRoute>(PoolType.V3Pool, route)
return { return {
routev3: isOnlyV3 ? new V3Route(route.map(parsePool), parsedCurrencyIn, parsedCurrencyOut) : null, routev3: isOnlyV3 ? new V3Route(route.map(parsePool), currencyIn, currencyOut) : null,
routev2: isOnlyV2 ? new V2Route(route.map(parsePair), parsedCurrencyIn, parsedCurrencyOut) : null, routev2: isOnlyV2 ? new V2Route(route.map(parsePair), currencyIn, currencyOut) : null,
mixedRoute: mixedRoute:
!isOnlyV3 && !isOnlyV2 !isOnlyV3 && !isOnlyV2 ? new MixedRouteSDK(route.map(parsePoolOrPair), currencyIn, currencyOut) : null,
? new MixedRouteSDK(route.map(parsePoolOrPair), parsedCurrencyIn, parsedCurrencyOut) inputAmount: CurrencyAmount.fromRawAmount(currencyIn, rawAmountIn),
: null, outputAmount: CurrencyAmount.fromRawAmount(currencyOut, rawAmountOut),
inputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyIn, rawAmountIn),
outputAmount: CurrencyAmount.fromRawAmount(parsedCurrencyOut, rawAmountOut),
} }
}) })
} catch (e) { } catch (e) {
...@@ -104,19 +110,112 @@ function isVersionedRoute<T extends V2PoolInRoute | V3PoolInRoute>( ...@@ -104,19 +110,112 @@ function isVersionedRoute<T extends V2PoolInRoute | V3PoolInRoute>(
return route.every((pool) => pool.type === type) return route.every((pool) => pool.type === type)
} }
export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): TradeResult { function toDutchOrderInfo(orderInfoJSON: DutchOrderInfoJSON): DutchOrderInfo {
const { tokenInAddress, tokenOutAddress, tradeType } = args const { nonce, input, outputs, exclusivityOverrideBps } = orderInfoJSON
return {
...orderInfoJSON,
nonce: BigNumber.from(nonce),
exclusivityOverrideBps: BigNumber.from(exclusivityOverrideBps),
input: {
...input,
startAmount: BigNumber.from(input.startAmount),
endAmount: BigNumber.from(input.endAmount),
},
outputs: outputs.map((output) => ({
...output,
startAmount: BigNumber.from(output.startAmount),
endAmount: BigNumber.from(output.endAmount),
})),
}
}
// Prepares the currencies used for the actual Swap (either UniswapX or Universal Router)
// May not match `currencyIn` that the user selected because for ETH inputs in UniswapX, the actual
// swap will use WETH.
function getTradeCurrencies(args: GetQuoteArgs, isUniswapXTrade: boolean): [Currency, Currency] {
const {
tokenInAddress,
tokenInChainId,
tokenInDecimals,
tokenInSymbol,
tokenOutAddress,
tokenOutChainId,
tokenOutDecimals,
tokenOutSymbol,
} = args
const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenInAddress as SwapRouterNativeAssets) const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenInAddress as SwapRouterNativeAssets)
const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets) const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOutAddress as SwapRouterNativeAssets)
const { gasUseEstimateUSD, blockNumber } = data
const routes = computeRoutes(tokenInIsNative, tokenOutIsNative, data.route)
const trade = new ClassicTrade({ const currencyIn = tokenInIsNative
? nativeOnChain(tokenInChainId)
: parseToken({ address: tokenInAddress, chainId: tokenInChainId, decimals: tokenInDecimals, symbol: tokenInSymbol })
const currencyOut = tokenOutIsNative
? nativeOnChain(tokenOutChainId)
: parseToken({
address: tokenOutAddress,
chainId: tokenOutChainId,
decimals: tokenOutDecimals,
symbol: tokenOutSymbol,
})
if (!isUniswapXTrade) {
return [currencyIn, currencyOut]
}
return [currencyIn.isNative ? currencyIn.wrapped : currencyIn, currencyOut]
}
function getClassicTradeDetails(
currencyIn: Currency,
currencyOut: Currency,
data: URAQuoteResponse
): {
gasUseEstimate?: number
gasUseEstimateUSD?: number
blockNumber?: string
routes?: RouteResult[]
} {
const classicQuote =
data.routing === URAQuoteType.CLASSIC ? data.quote : data.allQuotes.find(isClassicQuoteResponse)?.quote
return {
gasUseEstimate: classicQuote?.gasUseEstimate ? parseFloat(classicQuote.gasUseEstimate) : undefined,
gasUseEstimateUSD: classicQuote?.gasUseEstimateUSD ? parseFloat(classicQuote.gasUseEstimateUSD) : undefined,
blockNumber: classicQuote?.blockNumber,
routes: classicQuote ? computeRoutes(currencyIn, currencyOut, classicQuote.route) : undefined,
}
}
export async function transformRoutesToTrade(
args: GetQuoteArgs,
data: URAQuoteResponse,
quoteMethod: QuoteMethod
): Promise<TradeResult> {
const { tradeType, needsWrapIfUniswapX, routerPreference, account, amount } = args
// During the opt-in period, only return UniswapX quotes if the user has turned on the setting,
// even if it is the better quote.
const showUniswapXTrade = data.routing === URAQuoteType.DUTCH_LIMIT && routerPreference === RouterPreference.X
const [currencyIn, currencyOut] = getTradeCurrencies(args, showUniswapXTrade)
const { gasUseEstimateUSD, blockNumber, routes, gasUseEstimate } = getClassicTradeDetails(
currencyIn,
currencyOut,
data
)
// If the top-level URA quote type is DUTCH_LIMIT, then UniswapX is better for the user
const isUniswapXBetter = data.routing === URAQuoteType.DUTCH_LIMIT
// Some sus javascript float math but it's ok because its just an estimate for display purposes
const usdCostPerGas = gasUseEstimateUSD && gasUseEstimate ? gasUseEstimateUSD / gasUseEstimate : undefined
const approveInfo = await getApproveInfo(account, currencyIn, amount, usdCostPerGas)
const classicTrade = new ClassicTrade({
v2Routes: v2Routes:
routes routes
?.filter( ?.filter((r): r is RouteResult & { routev2: NonNullable<RouteResult['routev2']> } => r.routev2 !== null)
(r): r is (typeof routes)[0] & { routev2: NonNullable<(typeof routes)[0]['routev2']> } => r.routev2 !== null
)
.map(({ routev2, inputAmount, outputAmount }) => ({ .map(({ routev2, inputAmount, outputAmount }) => ({
routev2, routev2,
inputAmount, inputAmount,
...@@ -124,9 +223,7 @@ export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): Tra ...@@ -124,9 +223,7 @@ export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): Tra
})) ?? [], })) ?? [],
v3Routes: v3Routes:
routes routes
?.filter( ?.filter((r): r is RouteResult & { routev3: NonNullable<RouteResult['routev3']> } => r.routev3 !== null)
(r): r is (typeof routes)[0] & { routev3: NonNullable<(typeof routes)[0]['routev3']> } => r.routev3 !== null
)
.map(({ routev3, inputAmount, outputAmount }) => ({ .map(({ routev3, inputAmount, outputAmount }) => ({
routev3, routev3,
inputAmount, inputAmount,
...@@ -135,8 +232,7 @@ export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): Tra ...@@ -135,8 +232,7 @@ export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): Tra
mixedRoutes: mixedRoutes:
routes routes
?.filter( ?.filter(
(r): r is (typeof routes)[0] & { mixedRoute: NonNullable<(typeof routes)[0]['mixedRoute']> } => (r): r is RouteResult & { mixedRoute: NonNullable<RouteResult['mixedRoute']> } => r.mixedRoute !== null
r.mixedRoute !== null
) )
.map(({ mixedRoute, inputAmount, outputAmount }) => ({ .map(({ mixedRoute, inputAmount, outputAmount }) => ({
mixedRoute, mixedRoute,
...@@ -144,14 +240,45 @@ export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): Tra ...@@ -144,14 +240,45 @@ export function transformRoutesToTrade(args: GetQuoteArgs, data: QuoteData): Tra
outputAmount, outputAmount,
})) ?? [], })) ?? [],
tradeType, tradeType,
gasUseEstimateUSD: parseFloat(gasUseEstimateUSD).toFixed(2).toString(), gasUseEstimateUSD,
approveInfo,
blockNumber, blockNumber,
isUniswapXBetter,
requestId: data.quote.requestId,
quoteMethod,
}) })
return { state: QuoteState.SUCCESS, trade } // During the opt-in period, only return UniswapX quotes if the user has turned on the setting,
// even if it is the better quote.
if (isUniswapXBetter && args.routerPreference === RouterPreference.X) {
const orderInfo = toDutchOrderInfo(data.quote.orderInfo)
const wrapInfo = await getWrapInfo(needsWrapIfUniswapX, account, currencyIn.chainId, amount, usdCostPerGas)
const uniswapXTrade = new DutchOrderTrade({
currencyIn,
currenciesOut: [currencyOut],
orderInfo,
tradeType,
quoteId: data.quote.quoteId,
requestId: data.quote.requestId,
classicGasUseEstimateUSD: classicTrade.totalGasUseEstimateUSD,
wrapInfo,
approveInfo,
auctionPeriodSecs: data.quote.auctionPeriodSecs,
deadlineBufferSecs: data.quote.deadlineBufferSecs,
slippageTolerance: toSlippagePercent(data.quote.slippageTolerance),
})
return {
state: QuoteState.SUCCESS,
trade: uniswapXTrade,
}
}
return { state: QuoteState.SUCCESS, trade: classicTrade }
} }
function parseToken({ address, chainId, decimals, symbol }: QuoteData['route'][0][0]['tokenIn']): Token { function parseToken({ address, chainId, decimals, symbol }: ClassicQuoteData['route'][0][0]['tokenIn']): Token {
return new Token(chainId, address, parseInt(decimals.toString()), symbol) return new Token(chainId, address, parseInt(decimals.toString()), symbol)
} }
...@@ -188,11 +315,34 @@ export function currencyAddressForSwapQuote(currency: Currency): string { ...@@ -188,11 +315,34 @@ export function currencyAddressForSwapQuote(currency: Currency): string {
return currency.address return currency.address
} }
export function isClassicTrade(trade?: InterfaceTrade): trade is ClassicTrade {
return trade?.fillType === TradeFillType.Classic
}
export function isUniswapXTrade(trade?: InterfaceTrade): trade is DutchOrderTrade {
return trade?.fillType === TradeFillType.UniswapX
}
export function shouldUseAPIRouter(args: GetQuoteArgs): boolean { export function shouldUseAPIRouter(args: GetQuoteArgs): boolean {
const { routerPreference, isRoutingAPIPrice } = args const { routerPreference, isRoutingAPIPrice } = args
if (routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE && isRoutingAPIPrice) { if (routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE && isRoutingAPIPrice) {
return true return true
} }
return routerPreference === RouterPreference.API || routerPreference === RouterPreference.AUTO return routerPreference === RouterPreference.API || routerPreference === RouterPreference.X
}
export function getTransactionCount(trade: InterfaceTrade): number {
let count = 0
if (trade.approveInfo.needsApprove) {
count++ // approval step, which can happen in both classic and uniswapx
}
if (isUniswapXTrade(trade)) {
if (trade.wrapInfo.needsWrap) {
count++ // wrapping step for uniswapx
}
} else {
count++ // classic onchain swap
}
return count
} }
import { ChainId } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { useCallback, useMemo } from 'react'
import { useDispatch } from 'react-redux'
import { useAppSelector } from 'state/hooks'
import { addSignature } from './reducer'
import { SignatureDetails, SignatureType, UniswapXOrderDetails } from './types'
export function useAllSignatures(): { [id: string]: SignatureDetails } {
const { account } = useWeb3React()
const signatures = useAppSelector((state) => state.signatures) ?? {}
if (!account || !signatures[account]) return {}
return signatures[account]
}
export function usePendingOrders(): UniswapXOrderDetails[] {
const signatures = useAllSignatures()
return useMemo(() => {
return Object.values(signatures).filter(isPendingOrder)
}, [signatures])
}
export function useOrder(orderHash: string): UniswapXOrderDetails | undefined {
const signatures = useAllSignatures()
return useMemo(() => {
const order = signatures[orderHash]
if (!order || order.type !== SignatureType.SIGN_UNISWAPX_ORDER) return undefined
return order
}, [signatures, orderHash])
}
export function useAddOrder() {
const dispatch = useDispatch()
return useCallback(
(
offerer: string,
orderHash: string,
chainId: ChainId,
expiry: number,
swapInfo: UniswapXOrderDetails['swapInfo']
) => {
dispatch(
addSignature({
type: SignatureType.SIGN_UNISWAPX_ORDER,
offerer,
id: orderHash,
chainId,
expiry,
orderHash,
swapInfo,
status: UniswapXOrderStatus.OPEN,
addedTime: Date.now(),
})
)
},
[dispatch]
)
}
export function isFinalizedOrder(orderStatus: UniswapXOrderStatus) {
return orderStatus !== UniswapXOrderStatus.OPEN
}
export function isOnChainOrder(orderStatus: UniswapXOrderStatus) {
return orderStatus === UniswapXOrderStatus.FILLED || orderStatus === UniswapXOrderStatus.CANCELLED
}
function isPendingOrder(signature: SignatureDetails): signature is UniswapXOrderDetails {
return signature.type === SignatureType.SIGN_UNISWAPX_ORDER && signature.status === UniswapXOrderStatus.OPEN
}
import { TradeType } from '@uniswap/sdk-core'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { createStore, Store } from 'redux'
import { TransactionType } from '../transactions/types'
import reducer, { addSignature, initialState, removeSignature, SignatureState, updateSignature } from './reducer'
import { SignatureDetails, SignatureType } from './types'
const account = '0xabc'
const signature: SignatureDetails = {
id: '0x0',
addedTime: 0,
expiry: 0,
status: UniswapXOrderStatus.OPEN,
type: SignatureType.SIGN_UNISWAPX_ORDER,
chainId: 1,
orderHash: '0x0',
offerer: account,
swapInfo: {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_INPUT,
inputCurrencyAmountRaw: '0',
minimumOutputCurrencyAmountRaw: '0',
expectedOutputCurrencyAmountRaw: '0',
inputCurrencyId: '0x1',
outputCurrencyId: '0x2',
isUniswapXOrder: true,
},
}
describe('signature reducer', () => {
let store: Store<SignatureState>
beforeEach(() => {
store = createStore(reducer, initialState)
})
describe('addSignature', () => {
it('adds the transaction', () => {
store.dispatch(addSignature(signature))
const txs = store.getState()
expect(txs).toStrictEqual({
[account]: {
[signature.id]: signature,
},
})
// Adding a signature w/ same id should throw
expect(() => store.dispatch(addSignature(signature))).toThrow()
})
})
describe('updateSignature', () => {
it('updates the signature', () => {
store.dispatch(addSignature(signature))
const updatedSignature = { ...signature, status: UniswapXOrderStatus.CANCELLED }
store.dispatch(updateSignature(updatedSignature))
const txs = store.getState()
expect(txs).toStrictEqual({
[account]: {
[signature.id]: updatedSignature,
},
})
expect(() => store.dispatch(updateSignature({ ...signature, id: 'non existent id' }))).toThrow()
})
})
describe('removeSignature', () => {
it('updates the signature', () => {
store.dispatch(addSignature(signature))
store.dispatch(removeSignature(signature))
const txs = store.getState()
expect(txs).toStrictEqual({
[account]: {},
})
})
})
})
import { createSlice } from '@reduxjs/toolkit'
import { SignatureDetails } from './types'
export interface SignatureState {
[account: string]: { [id: string]: SignatureDetails }
}
export const initialState: SignatureState = {}
const signatureSlice = createSlice({
name: 'signatures',
initialState,
reducers: {
addSignature(signatures, { payload }: { payload: SignatureDetails }) {
if (signatures[payload.offerer]?.[payload.id]) throw Error('Attempted to add existing signature.')
const accountSignatures = signatures[payload.offerer] ?? {}
accountSignatures[payload.id] = payload
signatures[payload.offerer] = accountSignatures
},
updateSignature(signatures, { payload }: { payload: SignatureDetails }) {
if (!signatures[payload.offerer]?.[payload.id]) throw Error('Attempted to update non-existent signature.')
signatures[payload.offerer][payload.id] = payload
},
removeSignature(signatures, { payload }: { payload: { offerer: string; id: string } }) {
if (signatures[payload.offerer][payload.id]) delete signatures[payload.offerer][payload.id]
},
},
})
export const { addSignature, updateSignature, removeSignature } = signatureSlice.actions
export default signatureSlice.reducer
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { ExactInputSwapTransactionInfo, ExactOutputSwapTransactionInfo } from '../transactions/types'
export enum SignatureType {
SIGN_UNISWAPX_ORDER = 'signUniswapXOrder',
}
interface BaseSignatureFields {
type: SignatureType
id: string
addedTime: number
chainId: number
expiry: number
offerer: string
}
export interface UniswapXOrderDetails extends BaseSignatureFields {
type: SignatureType.SIGN_UNISWAPX_ORDER
orderHash: string
status: UniswapXOrderStatus
swapInfo: (ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo) & { isUniswapXOrder: true }
txHash?: string
}
export type SignatureDetails = UniswapXOrderDetails
import { TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc'
import { UniswapXBackendOrder, UniswapXOrderStatus } from 'lib/hooks/orders/types'
import OrderUpdater from 'lib/hooks/orders/updater'
import { useCallback, useMemo } from 'react'
import { PopupType } from 'state/application/reducer'
import { useAppDispatch } from 'state/hooks'
import { addTransaction } from 'state/transactions/reducer'
import { toSerializableReceipt } from 'state/transactions/updater'
import { isL2ChainId } from 'utils/chains'
import { useAddPopup } from '../application/hooks'
import { useAllSignatures } from './hooks'
import { updateSignature } from './reducer'
import { SignatureType, UniswapXOrderDetails } from './types'
export default function Updater() {
const { provider } = useWeb3React()
const addPopup = useAddPopup()
const signatures = useAllSignatures()
const pendingOrders = useMemo(
() =>
Object.values(signatures).filter(
(signature) =>
signature.type === SignatureType.SIGN_UNISWAPX_ORDER && signature.status === UniswapXOrderStatus.OPEN
) as UniswapXOrderDetails[],
[signatures]
)
const dispatch = useAppDispatch()
const onOrderUpdate = useCallback(
(order: UniswapXOrderDetails, update: UniswapXBackendOrder) => {
if (order.status === update.orderStatus) return
const popupDismissalTime = isL2ChainId(order.chainId) ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS
const updatedOrder = { ...order, status: update.orderStatus }
if (update.orderStatus === UniswapXOrderStatus.FILLED && update.txHash) {
updatedOrder.txHash = update.txHash
// Updates the order to contain the settled/on-chain output amount
if (updatedOrder.swapInfo.tradeType === TradeType.EXACT_INPUT) {
updatedOrder.swapInfo = {
...updatedOrder.swapInfo,
settledOutputCurrencyAmountRaw: update.settledAmounts?.[0]?.amountOut,
}
}
// Wait to update a filled order until the on-chain tx is available.
provider?.getTransactionReceipt(update.txHash).then((receipt) => {
dispatch(
addTransaction({
chainId: updatedOrder.chainId,
from: updatedOrder.offerer, // TODO(WEB-2053): use filler as from once tx reducer is organized by account
hash: update.txHash,
info: updatedOrder.swapInfo,
receipt: toSerializableReceipt(receipt),
})
)
dispatch(updateSignature(updatedOrder))
addPopup({ type: PopupType.Transaction, hash: update.txHash }, update.txHash, popupDismissalTime)
})
} else {
dispatch(updateSignature(updatedOrder))
addPopup({ type: PopupType.Order, orderHash: order.orderHash }, updatedOrder.orderHash, popupDismissalTime)
}
},
[addPopup, dispatch, provider]
)
return <OrderUpdater pendingOrders={pendingOrders} onOrderUpdate={onOrderUpdate} />
}
...@@ -8,7 +8,8 @@ import { ParsedQs } from 'qs' ...@@ -8,7 +8,8 @@ import { ParsedQs } from 'qs'
import { ReactNode, useCallback, useEffect, useMemo } from 'react' import { ReactNode, useCallback, useEffect, useMemo } from 'react'
import { AnyAction } from 'redux' import { AnyAction } from 'redux'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { InterfaceTrade, QuoteMethod, TradeState } from 'state/routing/types' import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isClassicTrade, isUniswapXTrade } from 'state/routing/utils'
import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { useUserSlippageToleranceWithDefault } from 'state/user/hooks'
import { TOKEN_SHORTHANDS } from '../../constants/tokens' import { TOKEN_SHORTHANDS } from '../../constants/tokens'
...@@ -70,11 +71,7 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { ...@@ -70,11 +71,7 @@ const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02 '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02
} }
// from the current swap inputs, compute the best trade and return it. export type SwapInfo = {
export function useDerivedSwapInfo(
state: SwapState,
chainId: ChainId | undefined
): {
currencies: { [field in Field]?: Currency | null } currencies: { [field in Field]?: Currency | null }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> } currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
parsedAmount?: CurrencyAmount<Currency> parsedAmount?: CurrencyAmount<Currency>
...@@ -82,11 +79,15 @@ export function useDerivedSwapInfo( ...@@ -82,11 +79,15 @@ export function useDerivedSwapInfo(
trade: { trade: {
trade?: InterfaceTrade trade?: InterfaceTrade
state: TradeState state: TradeState
method?: QuoteMethod uniswapXGasUseEstimateUSD?: number
error?: any
} }
allowedSlippage: Percent allowedSlippage: Percent
autoSlippage: Percent autoSlippage: Percent
} { }
// from the current swap inputs, compute the best trade and return it.
export function useDerivedSwapInfo(state: SwapState, chainId: ChainId | undefined): SwapInfo {
const { account } = useWeb3React() const { account } = useWeb3React()
const { const {
...@@ -116,7 +117,9 @@ export function useDerivedSwapInfo( ...@@ -116,7 +117,9 @@ export function useDerivedSwapInfo(
const trade = useBestTrade( const trade = useBestTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount, parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined (isExactIn ? outputCurrency : inputCurrency) ?? undefined,
undefined,
account
) )
const currencyBalances = useMemo( const currencyBalances = useMemo(
...@@ -135,9 +138,18 @@ export function useDerivedSwapInfo( ...@@ -135,9 +138,18 @@ export function useDerivedSwapInfo(
[inputCurrency, outputCurrency] [inputCurrency, outputCurrency]
) )
// allowed slippage is either auto slippage, or custom user defined slippage if auto slippage disabled // allowed slippage for classic trades is either auto slippage, or custom user defined slippage if auto slippage disabled
const autoSlippage = useAutoSlippageTolerance(trade.trade) const classicAutoSlippage = useAutoSlippageTolerance(isClassicTrade(trade.trade) ? trade.trade : undefined)
const allowedSlippage = useUserSlippageToleranceWithDefault(autoSlippage)
// slippage for uniswapx trades is defined by the quote response
const uniswapXAutoSlippage = isUniswapXTrade(trade.trade) ? trade.trade.slippageTolerance : undefined
// Uniswap interface recommended slippage amount
const autoSlippage = uniswapXAutoSlippage ?? classicAutoSlippage
const classicAllowedSlippage = useUserSlippageToleranceWithDefault(autoSlippage)
// slippage amount used to submit the trade
const allowedSlippage = uniswapXAutoSlippage ?? classicAllowedSlippage
const inputError = useMemo(() => { const inputError = useMemo(() => {
let inputError: ReactNode | undefined let inputError: ReactNode | undefined
...@@ -164,14 +176,14 @@ export function useDerivedSwapInfo( ...@@ -164,14 +176,14 @@ export function useDerivedSwapInfo(
} }
// compare input balance to max input based on version // compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)] const [balanceIn, maxAmountIn] = [currencyBalances[Field.INPUT], trade?.trade?.maximumAmountIn(allowedSlippage)]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) { if (balanceIn && maxAmountIn && balanceIn.lessThan(maxAmountIn)) {
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans> inputError = <Trans>Insufficient {balanceIn.currency.symbol} balance</Trans>
} }
return inputError return inputError
}, [account, allowedSlippage, currencies, currencyBalances, parsedAmount, to, trade.trade]) }, [account, currencies, parsedAmount, to, currencyBalances, trade.trade, allowedSlippage])
return useMemo( return useMemo(
() => ({ () => ({
......
...@@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit' ...@@ -2,7 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { updateVersion } from '../global/actions' import { updateVersion } from '../global/actions'
import { TransactionDetails, TransactionInfo } from './types' import { SerializableTransactionReceipt, TransactionDetails, TransactionInfo } from './types'
// TODO(WEB-2053): update this to be a map of account -> chainId -> txHash -> TransactionDetails // TODO(WEB-2053): update this to be a map of account -> chainId -> txHash -> TransactionDetails
// to simplify usage, once we're able to invalidate localstorage // to simplify usage, once we're able to invalidate localstorage
...@@ -17,8 +17,9 @@ interface AddTransactionPayload { ...@@ -17,8 +17,9 @@ interface AddTransactionPayload {
from: string from: string
hash: string hash: string
info: TransactionInfo info: TransactionInfo
nonce: number nonce?: number
deadline?: number deadline?: number
receipt?: SerializableTransactionReceipt
} }
export const initialState: TransactionState = {} export const initialState: TransactionState = {}
...@@ -29,13 +30,13 @@ const transactionSlice = createSlice({ ...@@ -29,13 +30,13 @@ const transactionSlice = createSlice({
reducers: { reducers: {
addTransaction( addTransaction(
transactions, transactions,
{ payload: { chainId, from, hash, info, nonce, deadline } }: { payload: AddTransactionPayload } { payload: { chainId, from, hash, info, nonce, deadline, receipt } }: { payload: AddTransactionPayload }
) { ) {
if (transactions[chainId]?.[hash]) { if (transactions[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.') throw Error('Attempted to add existing transaction.')
} }
const txs = transactions[chainId] ?? {} const txs = transactions[chainId] ?? {}
txs[hash] = { hash, info, from, addedTime: Date.now(), nonce, deadline } txs[hash] = { hash, info, from, addedTime: Date.now(), nonce, deadline, receipt }
transactions[chainId] = txs transactions[chainId] = txs
}, },
clearAllTransactions(transactions, { payload: { chainId } }) { clearAllTransactions(transactions, { payload: { chainId } }) {
......
...@@ -87,6 +87,7 @@ interface BaseSwapTransactionInfo extends BaseTransactionInfo { ...@@ -87,6 +87,7 @@ interface BaseSwapTransactionInfo extends BaseTransactionInfo {
tradeType: TradeType tradeType: TradeType
inputCurrencyId: string inputCurrencyId: string
outputCurrencyId: string outputCurrencyId: string
isUniswapXOrder: boolean
} }
export interface ExactInputSwapTransactionInfo extends BaseSwapTransactionInfo { export interface ExactInputSwapTransactionInfo extends BaseSwapTransactionInfo {
...@@ -94,6 +95,7 @@ export interface ExactInputSwapTransactionInfo extends BaseSwapTransactionInfo { ...@@ -94,6 +95,7 @@ export interface ExactInputSwapTransactionInfo extends BaseSwapTransactionInfo {
inputCurrencyAmountRaw: string inputCurrencyAmountRaw: string
expectedOutputCurrencyAmountRaw: string expectedOutputCurrencyAmountRaw: string
minimumOutputCurrencyAmountRaw: string minimumOutputCurrencyAmountRaw: string
settledOutputCurrencyAmountRaw?: string
} }
export interface ExactOutputSwapTransactionInfo extends BaseSwapTransactionInfo { export interface ExactOutputSwapTransactionInfo extends BaseSwapTransactionInfo {
tradeType: TradeType.EXACT_OUTPUT tradeType: TradeType.EXACT_OUTPUT
......
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc' import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc'
import LibUpdater from 'lib/hooks/transactions/updater' import LibUpdater from 'lib/hooks/transactions/updater'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { PopupType } from 'state/application/reducer'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { L2_CHAIN_IDS } from '../../constants/chains' import { L2_CHAIN_IDS } from '../../constants/chains'
...@@ -9,6 +11,19 @@ import { useAddPopup } from '../application/hooks' ...@@ -9,6 +11,19 @@ import { useAddPopup } from '../application/hooks'
import { checkedTransaction, finalizeTransaction } from './reducer' import { checkedTransaction, finalizeTransaction } from './reducer'
import { SerializableTransactionReceipt, TransactionDetails } from './types' import { SerializableTransactionReceipt, TransactionDetails } from './types'
export function toSerializableReceipt(receipt: TransactionReceipt): SerializableTransactionReceipt {
return {
blockHash: receipt.blockHash,
blockNumber: receipt.blockNumber,
contractAddress: receipt.contractAddress,
from: receipt.from,
status: receipt.status,
to: receipt.to,
transactionHash: receipt.transactionHash,
transactionIndex: receipt.transactionIndex,
}
}
export default function Updater() { export default function Updater() {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const addPopup = useAddPopup() const addPopup = useAddPopup()
...@@ -30,27 +45,19 @@ export default function Updater() { ...@@ -30,27 +45,19 @@ export default function Updater() {
[dispatch] [dispatch]
) )
const onReceipt = useCallback( const onReceipt = useCallback(
({ chainId, hash, receipt }: { chainId: number; hash: string; receipt: SerializableTransactionReceipt }) => { ({ chainId, hash, receipt }: { chainId: number; hash: string; receipt: TransactionReceipt }) => {
dispatch( dispatch(
finalizeTransaction({ finalizeTransaction({
chainId, chainId,
hash, hash,
receipt: { receipt: toSerializableReceipt(receipt),
blockHash: receipt.blockHash,
blockNumber: receipt.blockNumber,
contractAddress: receipt.contractAddress,
from: receipt.from,
status: receipt.status,
to: receipt.to,
transactionHash: receipt.transactionHash,
transactionIndex: receipt.transactionIndex,
},
}) })
) )
addPopup( addPopup(
{ {
txn: { hash }, type: PopupType.Transaction,
hash,
}, },
hash, hash,
isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS
......
...@@ -5,7 +5,13 @@ import store from 'state' ...@@ -5,7 +5,13 @@ import store from 'state'
import { RouterPreference } from 'state/routing/slice' import { RouterPreference } from 'state/routing/slice'
import { renderHook } from 'test-utils/render' import { renderHook } from 'test-utils/render'
import { deserializeToken, serializeToken, useRouterPreference, useUserSlippageTolerance } from './hooks' import {
deserializeToken,
serializeToken,
useRouterPreference,
useUserDisabledUniswapX,
useUserSlippageTolerance,
} from './hooks'
import { updateUserSlippageTolerance } from './reducer' import { updateUserSlippageTolerance } from './reducer'
import { SlippageTolerance } from './types' import { SlippageTolerance } from './types'
...@@ -72,6 +78,15 @@ describe('useRouterPreference', () => { ...@@ -72,6 +78,15 @@ describe('useRouterPreference', () => {
current: [routerPreference], current: [routerPreference],
}, },
} = renderHook(() => useRouterPreference()) } = renderHook(() => useRouterPreference())
expect(routerPreference).toBe(RouterPreference.AUTO) expect(routerPreference).toBe(RouterPreference.API)
})
})
describe('useUserDisabledUniswapX', () => {
it('returns `false` by default', () => {
const {
result: { current: disabledUniswapX },
} = renderHook(() => useUserDisabledUniswapX())
expect(disabledUniswapX).toBe(false)
}) })
}) })
...@@ -222,6 +222,10 @@ export function useHideUniswapWalletBanner(): [boolean, () => void] { ...@@ -222,6 +222,10 @@ export function useHideUniswapWalletBanner(): [boolean, () => void] {
return [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] return [hideUniswapWalletBanner, toggleHideUniswapWalletBanner]
} }
export function useUserDisabledUniswapX(): boolean {
return useAppSelector((state) => state.user.disabledUniswapX) ?? false
}
/** /**
* Given two tokens return the liquidity token that represents its liquidity shares * Given two tokens return the liquidity token that represents its liquidity shares
* @param tokenA one of the two tokens * @param tokenA one of the two tokens
......
...@@ -48,6 +48,7 @@ export interface UserState { ...@@ -48,6 +48,7 @@ export interface UserState {
timestamp: number timestamp: number
URLWarningVisible: boolean URLWarningVisible: boolean
hideUniswapWalletBanner: boolean hideUniswapWalletBanner: boolean
disabledUniswapX?: boolean
// undefined means has not gone through A/B split yet // undefined means has not gone through A/B split yet
showSurveyPopup?: boolean showSurveyPopup?: boolean
} }
...@@ -59,7 +60,7 @@ function pairKey(token0Address: string, token1Address: string) { ...@@ -59,7 +60,7 @@ function pairKey(token0Address: string, token1Address: string) {
export const initialState: UserState = { export const initialState: UserState = {
selectedWallet: undefined, selectedWallet: undefined,
userLocale: null, userLocale: null,
userRouterPreference: RouterPreference.AUTO, userRouterPreference: RouterPreference.API,
userHideClosedPositions: false, userHideClosedPositions: false,
userSlippageTolerance: SlippageTolerance.Auto, userSlippageTolerance: SlippageTolerance.Auto,
userSlippageToleranceHasBeenMigratedToAuto: true, userSlippageToleranceHasBeenMigratedToAuto: true,
...@@ -100,6 +101,9 @@ const userSlice = createSlice({ ...@@ -100,6 +101,9 @@ const userSlice = createSlice({
updateHideUniswapWalletBanner(state, action) { updateHideUniswapWalletBanner(state, action) {
state.hideUniswapWalletBanner = action.payload.hideUniswapWalletBanner state.hideUniswapWalletBanner = action.payload.hideUniswapWalletBanner
}, },
updateDisabledUniswapX(state, action) {
state.disabledUniswapX = action.payload.disabledUniswapX
},
addSerializedToken(state, { payload: { serializedToken } }) { addSerializedToken(state, { payload: { serializedToken } }) {
if (!state.tokens) { if (!state.tokens) {
state.tokens = {} state.tokens = {}
...@@ -167,7 +171,26 @@ const userSlice = createSlice({ ...@@ -167,7 +171,26 @@ const userSlice = createSlice({
// If `userRouterPreference` is not present, reset to default // If `userRouterPreference` is not present, reset to default
if (typeof state.userRouterPreference !== 'string') { if (typeof state.userRouterPreference !== 'string') {
state.userRouterPreference = RouterPreference.AUTO state.userRouterPreference = RouterPreference.API
}
// If `userRouterPreference` is `AUTO`, migrate to `API`
if ((state.userRouterPreference as string) === 'auto') {
state.userRouterPreference = RouterPreference.API
}
//If `buyFiatFlowCompleted` is present, delete it using filtering
if ('buyFiatFlowCompleted' in state) {
//ignoring due to type errors occuring since we now remove this state
//@ts-ignore
delete state.buyFiatFlowCompleted
}
// If `buyFiatFlowCompleted` is present, delete it using filtering
if ('buyFiatFlowCompleted' in state) {
//ignoring due to type errors occuring since we now remove this state
//@ts-ignore
delete state.buyFiatFlowCompleted
} }
//If `buyFiatFlowCompleted` is present, delete it using filtering //If `buyFiatFlowCompleted` is present, delete it using filtering
...@@ -192,5 +215,6 @@ export const { ...@@ -192,5 +215,6 @@ export const {
updateUserLocale, updateUserLocale,
updateUserSlippageTolerance, updateUserSlippageTolerance,
updateHideUniswapWalletBanner, updateHideUniswapWalletBanner,
updateDisabledUniswapX,
} = userSlice.actions } = userSlice.actions
export default userSlice.reducer export default userSlice.reducer
import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core' import { ChainId, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { V3Route } from '@uniswap/smart-order-router' import { V3Route } from '@uniswap/smart-order-router'
import { FeeAmount, Pool } from '@uniswap/v3-sdk' import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import { nativeOnChain } from 'constants/tokens'
import { BigNumber } from 'ethers/lib/ethers'
import JSBI from 'jsbi' import JSBI from 'jsbi'
import { ClassicTrade } from 'state/routing/types' import { ClassicTrade, DutchOrderTrade, QuoteMethod } from 'state/routing/types'
export const TEST_TOKEN_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc') export const TEST_TOKEN_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc')
export const TEST_TOKEN_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def') export const TEST_TOKEN_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def')
export const TEST_TOKEN_3 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 'GHI', 'Ghi') export const TEST_TOKEN_3 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 'GHI', 'Ghi')
export const TEST_RECIPIENT_ADDRESS = '0x0000000000000000000000000000000000000004' export const TEST_RECIPIENT_ADDRESS = '0x0000000000000000000000000000000000000004'
export const ETH_MAINNET = nativeOnChain(ChainId.MAINNET)
export const TEST_POOL_12 = new Pool( export const TEST_POOL_12 = new Pool(
TEST_TOKEN_1, TEST_TOKEN_1,
...@@ -40,7 +43,9 @@ export const TEST_TRADE_EXACT_INPUT = new ClassicTrade({ ...@@ -40,7 +43,9 @@ export const TEST_TRADE_EXACT_INPUT = new ClassicTrade({
], ],
v2Routes: [], v2Routes: [],
tradeType: TradeType.EXACT_INPUT, tradeType: TradeType.EXACT_INPUT,
gasUseEstimateUSD: '1.00', gasUseEstimateUSD: 1.0,
approveInfo: { needsApprove: false },
quoteMethod: QuoteMethod.CLIENT_SIDE,
}) })
export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({ export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({
...@@ -53,6 +58,46 @@ export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({ ...@@ -53,6 +58,46 @@ export const TEST_TRADE_EXACT_OUTPUT = new ClassicTrade({
], ],
v2Routes: [], v2Routes: [],
tradeType: TradeType.EXACT_OUTPUT, tradeType: TradeType.EXACT_OUTPUT,
quoteMethod: QuoteMethod.CLIENT_SIDE,
approveInfo: { needsApprove: false },
}) })
export const TEST_ALLOWED_SLIPPAGE = new Percent(2, 100) export const TEST_ALLOWED_SLIPPAGE = new Percent(2, 100)
export const TEST_DUTCH_TRADE_ETH_INPUT = new DutchOrderTrade({
currencyIn: ETH_MAINNET.wrapped,
currenciesOut: [TEST_TOKEN_2],
orderInfo: {
reactor: 'test_reactor',
swapper: 'test_offerer',
nonce: BigNumber.from(1),
deadline: 1000,
decayStartTime: 0,
decayEndTime: 10,
additionalValidationContract: '0x0',
additionalValidationData: '0x0',
exclusiveFiller: '0x3456',
exclusivityOverrideBps: BigNumber.from(0),
input: {
token: ETH_MAINNET.wrapped.address,
startAmount: BigNumber.from(1000),
endAmount: BigNumber.from(900),
},
outputs: [
{
token: TEST_TOKEN_2.address,
startAmount: BigNumber.from(1000),
endAmount: BigNumber.from(900),
recipient: '0x0',
},
],
},
tradeType: TradeType.EXACT_INPUT,
quoteId: '0x0000000',
wrapInfo: { needsWrap: false },
approveInfo: { needsApprove: false },
classicGasUseEstimateUSD: 7.87,
auctionPeriodSecs: 120,
deadlineBufferSecs: 30,
slippageTolerance: new Percent(5, 100),
})
...@@ -46,6 +46,9 @@ export const ThemedText = { ...@@ -46,6 +46,9 @@ export const ThemedText = {
LabelSmall(props: TextProps) { LabelSmall(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={14} color="textSecondary" {...props} /> return <TextWrapper fontWeight={600} fontSize={14} color="textSecondary" {...props} />
}, },
LabelMicro(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={12} color="textSecondary" {...props} />
},
Link(props: TextProps) { Link(props: TextProps) {
return <TextWrapper fontWeight={600} fontSize={14} color="accentAction" {...props} /> return <TextWrapper fontWeight={600} fontSize={14} color="accentAction" {...props} />
}, },
......
...@@ -2,7 +2,7 @@ import { Protocol } from '@uniswap/router-sdk' ...@@ -2,7 +2,7 @@ import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk' import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk' import { FeeAmount } from '@uniswap/v3-sdk'
import { InterfaceTrade } from 'state/routing/types' import { ClassicTrade } from 'state/routing/types'
export interface RoutingDiagramEntry { export interface RoutingDiagramEntry {
percent: Percent percent: Percent
...@@ -15,7 +15,7 @@ const V2_DEFAULT_FEE_TIER = 3000 ...@@ -15,7 +15,7 @@ const V2_DEFAULT_FEE_TIER = 3000
/** /**
* Loops through all routes on a trade and returns an array of diagram entries. * Loops through all routes on a trade and returns an array of diagram entries.
*/ */
export default function getRoutingDiagramEntries(trade: InterfaceTrade): RoutingDiagramEntry[] { export default function getRoutingDiagramEntries(trade: ClassicTrade): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => { return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion = const portion =
trade.tradeType === TradeType.EXACT_INPUT trade.tradeType === TradeType.EXACT_INPUT
......
import { SwapPriceUpdateUserResponse } from '@uniswap/analytics-events' import { SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { SwapResult } from 'hooks/useSwapCallback'
import { import {
formatPercentInBasisPointsNumber, formatPercentInBasisPointsNumber,
formatPercentNumber, formatPercentNumber,
...@@ -8,12 +9,15 @@ import { ...@@ -8,12 +9,15 @@ import {
getDurationUntilTimestampSeconds, getDurationUntilTimestampSeconds,
getTokenAddress, getTokenAddress,
} from 'lib/utils/analytics' } from 'lib/utils/analytics'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { isClassicTrade } from 'state/routing/utils'
import { RoutingDiagramEntry } from './getRoutingDiagramEntries' import { RoutingDiagramEntry } from './getRoutingDiagramEntries'
import { computeRealizedPriceImpact } from './prices' import { computeRealizedPriceImpact } from './prices'
const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => { const formatRoutesEventProperties = (routes?: RoutingDiagramEntry[]) => {
if (!routes) return {}
const routesEventProperties: Record<string, any[]> = { const routesEventProperties: Record<string, any[]> = {
routes_percentages: [], routes_percentages: [],
routes_protocols: [], routes_protocols: [],
...@@ -57,20 +61,20 @@ export const formatSwapPriceUpdatedEventProperties = ( ...@@ -57,20 +61,20 @@ export const formatSwapPriceUpdatedEventProperties = (
interface AnalyticsEventProps { interface AnalyticsEventProps {
trade: InterfaceTrade trade: InterfaceTrade
hash?: string swapResult?: SwapResult
allowedSlippage: Percent allowedSlippage: Percent
transactionDeadlineSecondsSinceEpoch?: number transactionDeadlineSecondsSinceEpoch?: number
isAutoSlippage: boolean isAutoSlippage: boolean
isAutoRouterApi: boolean isAutoRouterApi: boolean
swapQuoteReceivedDate?: Date swapQuoteReceivedDate?: Date
routes: RoutingDiagramEntry[] routes?: RoutingDiagramEntry[]
fiatValueInput?: number fiatValueInput?: number
fiatValueOutput?: number fiatValueOutput?: number
} }
export const formatSwapButtonClickEventProperties = ({ export const formatSwapButtonClickEventProperties = ({
trade, trade,
hash, swapResult,
allowedSlippage, allowedSlippage,
transactionDeadlineSecondsSinceEpoch, transactionDeadlineSecondsSinceEpoch,
isAutoSlippage, isAutoSlippage,
...@@ -80,8 +84,9 @@ export const formatSwapButtonClickEventProperties = ({ ...@@ -80,8 +84,9 @@ export const formatSwapButtonClickEventProperties = ({
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
}: AnalyticsEventProps) => ({ }: AnalyticsEventProps) => ({
estimated_network_fee_usd: trade.gasUseEstimateUSD, estimated_network_fee_usd: isClassicTrade(trade) ? trade.gasUseEstimateUSD : undefined,
transaction_hash: hash, transaction_hash: swapResult?.type === TradeFillType.Classic ? swapResult.response.hash : undefined,
order_hash: swapResult?.type === TradeFillType.UniswapX ? swapResult.response.orderHash : undefined,
transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch), transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch),
token_in_address: trade ? getTokenAddress(trade.inputAmount.currency) : undefined, token_in_address: trade ? getTokenAddress(trade.inputAmount.currency) : undefined,
token_out_address: trade ? getTokenAddress(trade.outputAmount.currency) : undefined, token_out_address: trade ? getTokenAddress(trade.outputAmount.currency) : undefined,
...@@ -91,7 +96,9 @@ export const formatSwapButtonClickEventProperties = ({ ...@@ -91,7 +96,9 @@ export const formatSwapButtonClickEventProperties = ({
token_out_amount: trade ? formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals) : undefined, token_out_amount: trade ? formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals) : undefined,
token_in_amount_usd: fiatValueInput, token_in_amount_usd: fiatValueInput,
token_out_amount_usd: fiatValueOutput, token_out_amount_usd: fiatValueOutput,
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined, price_impact_basis_points: isClassicTrade(trade)
? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade))
: undefined,
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage), allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
is_auto_router_api: isAutoRouterApi, is_auto_router_api: isAutoRouterApi,
is_auto_slippage: isAutoSlippage, is_auto_slippage: isAutoSlippage,
...@@ -102,6 +109,6 @@ export const formatSwapButtonClickEventProperties = ({ ...@@ -102,6 +109,6 @@ export const formatSwapButtonClickEventProperties = ({
duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate
? getDurationFromDateMilliseconds(swapQuoteReceivedDate) ? getDurationFromDateMilliseconds(swapQuoteReceivedDate)
: undefined, : undefined,
swap_quote_block_number: trade.blockNumber, swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
...formatRoutesEventProperties(routes), ...formatRoutesEventProperties(routes),
}) })
import { Percent } from '@uniswap/sdk-core'
import { toSlippagePercent } from './slippage'
describe('slippage function', () => {
it('should turn slippage strings into Percents correctly', async () => {
const input = '0.5'
const percent = new Percent(5, 1000)
expect(toSlippagePercent(input).equalTo(percent)).toBeTruthy()
})
})
import { Percent } from '@uniswap/sdk-core'
const PRECISION = 10_000
const DENOMINATOR = PRECISION * 100
// turn "0.5" into Percent representing 0.5%
export function toSlippagePercent(slippage: string): Percent {
const numerator = Number(slippage) * PRECISION
return new Percent(numerator, DENOMINATOR)
}
...@@ -2,8 +2,8 @@ import { Protocol } from '@uniswap/router-sdk' ...@@ -2,8 +2,8 @@ import { Protocol } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { routeAmountsToString, SwapRoute } from '@uniswap/smart-order-router' import { routeAmountsToString, SwapRoute } from '@uniswap/smart-order-router'
import { Pool } from '@uniswap/v3-sdk' import { Pool } from '@uniswap/v3-sdk'
import { QuoteResult, QuoteState } from 'state/routing/types' import { QuoteResult, QuoteState, URAQuoteType } from 'state/routing/types'
import { QuoteData, V2PoolInRoute, V3PoolInRoute } from 'state/routing/types' import { ClassicQuoteData, V2PoolInRoute, V3PoolInRoute } from 'state/routing/types'
// from routing-api (https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/quote.ts#L243-L311) // from routing-api (https://github.com/Uniswap/routing-api/blob/main/lib/handlers/quote/quote.ts#L243-L311)
export function transformSwapRouteToGetQuoteResult( export function transformSwapRouteToGetQuoteResult(
...@@ -110,7 +110,7 @@ export function transformSwapRouteToGetQuoteResult( ...@@ -110,7 +110,7 @@ export function transformSwapRouteToGetQuoteResult(
routeResponse.push(curRoute) routeResponse.push(curRoute)
} }
const result: QuoteData = { const result: ClassicQuoteData = {
methodParameters, methodParameters,
blockNumber: blockNumber.toString(), blockNumber: blockNumber.toString(),
amount: amount.quotient.toString(), amount: amount.quotient.toString(),
...@@ -128,5 +128,5 @@ export function transformSwapRouteToGetQuoteResult( ...@@ -128,5 +128,5 @@ export function transformSwapRouteToGetQuoteResult(
routeString: routeAmountsToString(route), routeString: routeAmountsToString(route),
} }
return { state: QuoteState.SUCCESS, data: result } return { state: QuoteState.SUCCESS, data: { routing: URAQuoteType.CLASSIC, quote: result, allQuotes: [] } }
} }
...@@ -2316,7 +2316,7 @@ ...@@ -2316,7 +2316,7 @@
dependencies: dependencies:
"@ethersproject/logger" "^5.7.0" "@ethersproject/logger" "^5.7.0"
"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5", "@ethersproject/providers@^5.5.1", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2": "@ethersproject/providers@5.7.2", "@ethersproject/providers@^5", "@ethersproject/providers@^5.5.1", "@ethersproject/providers@^5.7.0", "@ethersproject/providers@^5.7.1", "@ethersproject/providers@^5.7.2":
version "5.7.2" version "5.7.2"
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb"
integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==
...@@ -6216,10 +6216,10 @@ ...@@ -6216,10 +6216,10 @@
react "^18.2.0" react "^18.2.0"
react-dom "^18.2.0" react-dom "^18.2.0"
"@uniswap/conedison@^1.7.1": "@uniswap/conedison@^1.8.0":
version "1.7.1" version "1.8.0"
resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.7.1.tgz#10076d57fe111abff106d3cd3704f02c37eb0c8e" resolved "https://registry.yarnpkg.com/@uniswap/conedison/-/conedison-1.8.0.tgz#60fcfa1475780350a719510ccee1f6a49f8c5cf8"
integrity sha512-iFH36R4cPy2daffrHoJ5/iLhgUFUVbNXujd7dYWPzAaS7XOleQ0tHrRmoUcwnO5Z50jDyZC0A86+yOs3UGj8bw== integrity sha512-EeC37bbrd4sJnqW3TxNhExdWUBvzyJDhbTXcbDzFdBh/bg5sOCu1NbYJEnrjwBHEfewc9nF2OjEzejJFyJUx9A==
"@uniswap/default-token-list@^11.2.0": "@uniswap/default-token-list@^11.2.0":
version "11.2.0" version "11.2.0"
...@@ -6297,7 +6297,7 @@ ...@@ -6297,7 +6297,7 @@
"@uniswap/v2-sdk" "^3.0.1" "@uniswap/v2-sdk" "^3.0.1"
"@uniswap/v3-sdk" "^3.8.3" "@uniswap/v3-sdk" "^3.8.3"
"@uniswap/sdk-core@^3.0.0-alpha.3", "@uniswap/sdk-core@^3.0.1", "@uniswap/sdk-core@^3.1.0", "@uniswap/sdk-core@^3.2.6": "@uniswap/sdk-core@^3.0.0-alpha.3", "@uniswap/sdk-core@^3.0.1", "@uniswap/sdk-core@^3.0.2", "@uniswap/sdk-core@^3.1.0", "@uniswap/sdk-core@^3.2.6":
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-3.2.6.tgz#1a652516fab0c6bc1420c2226648da967a10f52a" resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-3.2.6.tgz#1a652516fab0c6bc1420c2226648da967a10f52a"
integrity sha512-MvH/3G0W0sM2g7XjaUy9qU7IabxL/KQp/ucU0AQGpVxiTaAhmVRtsjkkv9UDyzpIXVrmevl4kRgV7KKE29UuXA== integrity sha512-MvH/3G0W0sM2g7XjaUy9qU7IabxL/KQp/ucU0AQGpVxiTaAhmVRtsjkkv9UDyzpIXVrmevl4kRgV7KKE29UuXA==
...@@ -6359,10 +6359,21 @@ ...@@ -6359,10 +6359,21 @@
dotenv "^14.2.0" dotenv "^14.2.0"
hardhat-watcher "^2.1.1" hardhat-watcher "^2.1.1"
"@uniswap/token-lists@^1.0.0-beta.31": "@uniswap/token-lists@^1.0.0-beta.31", "@uniswap/token-lists@^1.0.0-beta.33":
version "1.0.0-beta.31" version "1.0.0-beta.33"
resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.31.tgz#ff3852bd505ec7b4c276625c762ea79a93a919ec" resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.33.tgz#966ba96c9ccc8f0e9e09809890b438203f2b1911"
integrity sha512-BQVoelKCRf64IToPEs1wxiXOnhr/ukwPOF78XG11PrTAOL4F8umjYKFb8ZPv1/dIJsPaC7GhLSriEqyp94SasQ== integrity sha512-JQkXcpRI3jFG8y3/CGC4TS8NkDgcxXaOQuYW8Qdvd6DcDiIyg2vVYCG9igFEzF0G6UvxgHkBKC7cWCgzZNYvQg==
"@uniswap/uniswapx-sdk@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@uniswap/uniswapx-sdk/-/uniswapx-sdk-1.0.0.tgz#5e27a86aca4b9955da90ec8209f44bad257178d2"
integrity sha512-2e+74J8pTxrP7Vq2bw5Cae+Z1uLDUi/sMbvG+SFBM/qJM+rn5UFs5pdAaLTzPaEwgoL0aExlu8QJYchri0D2aA==
dependencies:
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/providers" "^5.7.0"
"@uniswap/permit2-sdk" "^1.2.0"
"@uniswap/sdk-core" "^3.0.2"
ethers "^5.7.0"
"@uniswap/universal-router-sdk@^1.3.8", "@uniswap/universal-router-sdk@^1.3.9", "@uniswap/universal-router-sdk@^1.5.3": "@uniswap/universal-router-sdk@^1.3.8", "@uniswap/universal-router-sdk@^1.3.9", "@uniswap/universal-router-sdk@^1.5.3":
version "1.5.3" version "1.5.3"
...@@ -11362,7 +11373,7 @@ ethereumjs-util@^7.1.0: ...@@ -11362,7 +11373,7 @@ ethereumjs-util@^7.1.0:
ethereum-cryptography "^0.1.3" ethereum-cryptography "^0.1.3"
rlp "^2.2.4" rlp "^2.2.4"
ethers@^5.3.1, ethers@^5.5.2, ethers@^5.6.0, ethers@^5.6.7, ethers@^5.7.1, ethers@^5.7.2: ethers@^5.3.1, ethers@^5.5.2, ethers@^5.6.0, ethers@^5.6.7, ethers@^5.7.0, ethers@^5.7.1, ethers@^5.7.2:
version "5.7.2" version "5.7.2"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e"
integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==
......
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