chore: merge widgets work into main (#3065)

* feat: design system (#2622)

* refactor: mv setInterval to lib (#2621)

* chore: widget tooling (#2620)

* chore: remove global styles from cosmos viewer

* chore: add generated svgs to bundle

* chore: alias lib within lib

* feat: widgets swap settings and arch (#2629)

* style: update theme

* feat: grid-based row/column

* feat: widget/modal arch

* feat: tooltip arch

* feat: atoms arch

* feat: swap settings

* chore: update deps

* fix: input width

* refactor: modularize Tooltip

* feat: add grow to Row

* style: true prop

* refactor: clean NumericInput

* fix: customizable data structure

* chore: sort styled-components

* fix: import ReactNode

* fix: svgr index generation

* chore: run tests on widgets (#2635)

* chore: widgets nits (#2636)

* fix: restrict type color to theme

* feat: add types

* fix: input width

* fix: header divider

* fix: eslint

* fix: color name

* fix: use inputs for a11y (#2646)

* fix: clearable customizable

* feat: accent hovered select option

* feat: custom slippage color

* fix: use buttons for a11y

* fix: widgets styles (#2654)

* style: add body1

* refactor: modularize theme/components

* refactor: modularize all text Input

* fix: toggle opacity

* test: fixture arch

* feat: rm gas price select

* fix: toggle styles/strings

* feat: mock toggle

* fix: dialog overflow clipping

* fix: mix-blend-mode for safari

* fix: clip-path for safari svg

* fix: mock toggle content

* fix: input margin

* fix: input and cursor

* fix: validate . input

* fix: unused useMemo

* feat: widgets empty state (#2657)

* refactor: TextButton

* feat: inline icons

* feat: swap empty state

* feat: define TokenSelect

* fix: always inline icons

* feat: recent transactions (#2661)

* feat: wallet button

* fix: tx deps

* feat: widgets token select (#2685)

* fix: line height of 1

* fix: button margin

* fix: update styles

* feat: token select

* refactor: mocks and types

* feat: close dialog on esc

* feat: focus input on token select

* refactor: layer swap elements

* feat: use token color

* fix: widget theme

* fix: use vibrant

* chore: lodash types

* fix: fixture props

* feat: smoother color extraction

* fix: vibrant dep

* perf: extract input token color too

* feat: eased token background

* feat: token color prefetching

* chore: mv polished to deps

* chore: package management

* fix: token background transition

* fix: better color transitions

* feat: widgets UI (#2742)

* feat: add swap states

* fix: widget-global box-sizing

* feat: desaturate and opacity on token approval

* feat: red balance on balance insufficient

* fix: states

* feat: action button

* refactor: action button

* feat: loading spinner border

* fix: typescript errors

* fix: token color transition

* fix: unused typings

* feat: swap summary sans tooltip

* refactor: swap state

* feat: swap summary

* refactor: simpler swap names

* fix: cutoffs around footer

* refactor: recent txs

* refactor: buttons

* feat: tx status

* fix: consistent formatting

* feat: tx error

* test: tx error

* test: widget decorator

* style: theming

* fix: clean up dialogs

* fix: clean up swap

* fix: clean up overlays

* fix: action button text on hover

* fix: pickAtom

* fix: pickAtom typings

* fix: smoother error transition

* feat: enter for toggle

* fix: select tabbing

* refactor: simplify dialogs

* feat: widgets polish (#2757)

* fix: loading spinner fallback for safari

* fix: use border for focus

* refactor: token options

* fix: use react toggle event

* fix: token select

* fix: inert content when modal

* fix: windowed token select

* chore: mv windowing utils to deps

* fix: windowing with no rerender

* feat: widget i18n (#2765)

* feat: configure widget i18n

* i18n: wrap translatable strings in macros

* fix: rm lib/locales

* refactor: t to trans

* feat: cosmos locale selector

* chore: widgets nits (#2786)

* fix: tooltip color

* fix: tx ttl tooltip

* fix: tooltip positioning

* fix: token list padding top

* style: responsive tx

* nit: fix summary copy

* chore: change byline

* feat(widgets): add new @web3-react cosmos decorator (#2799)

 add new @web3-react cosmos decorator and provider api to widget

* feat: token color mock (#2878)

* chore: merge main into widgets (#2893)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: center focused outline card (#2625)

* fix: add usdc to arbitrum/optimism common bases (#2641)

* remove WETH from optimism bases (#2640)

* use l2 logos in base pairs (#2634)

* fix: split calls into more chunks if they fail due to out of gas errors (#2630)

* fix: split calls into more chunks if they fail due to out of gas errors

* set to 100m gas

* back to 25m so we batch fewer calls

* do not pass through gas limit, some simplification of the code

* unused import

* fix: restrict @davatar usage to avoid 3p fetches (#2649)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): ensure chainIds match before fetching pool data (#2652)

* ensure chainIds match before fetching pool data

* debounce both input currencies, and only look for pairs on currencies that share a chainId

* pr feedback

* fix: use optional operator for chainId (#2666)

* chore: update token list (#2670)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: update token list (#2671)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: extend privacy and terms (#2623)

* initial iteration

* add logging

* added hook

* polish

* remove unused import

* add hash

* addressed pr feedback

* remove autorouter icon

* use firebase store

* style

* adjust recat ga

* log remove liquidity

* update copy

* addressed pr feedback

* addressed pr feedback

* prevent privacy content from dismissing modal

* make top-level key origin

* use hostname

* restore trm

* chore(i18n): synchronize translations from crowdin [skip ci]

* log full signed tx (#2681)

* refactor monitoring (#2682)

* chore: set final privacy learn more link' (#2684)

* add learn more button

* add final link

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: back arrow bug in wallet modal and fill tx for wallet (#2687)

* add tx to wallet connect

* remove id from env

* restore env

* block import of unsupported tokens (#2673)

generalize custom import token block ui

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps-dev): bump @uniswap/token-lists (#2699)

* chore(i18n): synchronize translations from crowdin [skip ci]

* try out 'dimension1' (#2704)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: walletconnect modal re-open after user rejection (#2693)

Co-authored-by: M0kY <moky@example.com>

* chore: update unsupported token list (#2689)

* chore: update unsupported token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: memoize the list stuff so the tokens are consistently clickable (#2724)

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: update cmc list link (#2710)

* update cmc lists

* update CMC url

* add token to unsupported list (#2732)

* don't overwrite localstorage lists when fetch throws (#2723)

* try cd1 for custom dimension (#2734)

* fix: Update walletlink-connector to 6.2.8 (#2655)

* Update walletlink-connector to 6.2.5 which has a walletlink update to support addEthereumChain+switchEthereumChain requests

* Update walletlink-connector to 6.2.7

* Update walletlink-connector to 6.2.8

* fix: Parse latest proposal description correctly

* add proposal start time (#2738)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: #2741 Increase liquidity form off center (#2746)

* fix: bump to latest token list including ENS token

* fix: remove deprecated optimism status url (#2771)

* feat: Menu update. Add help center & feature requests. Remove analytics & github. (#2709)

* Add help center, remove analytics from menu

* Add canny feature requests link, remove github link

* add coffee icon

* no unused imports eslint rule (#2773)

* chore(i18n): synchronize translations from crowdin [skip ci]

* add protocols param to quote endpoint (#2774)

* add protocols param to quote endpoint

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: lint error (#2775)

* fix(optimism): Optimism regenesis support (#2703)

* feat(optimism): optimistic kovan local regenesis changes

* use the regenesis version of the sdk

* remove the override no longer necessary

* diff rpc url

* back to kovan url

* lint error

* Optimism mainnet regenesis test (#2695)

* remove the optimism mainnet specific code and point to the mainnet regenesis rpc url

* point at the old mainnet multicall address

* bump the sdk version

* copy the list

* multicall address regenesis change

* revert the gas limit special casing for optimism

* bump the sdk version

* remove a couple other temporary edits

* unused test case

* specific version of v3-sdk

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: add support for 0.01% tier (#2769)

* chore: add support for 0.01% tier

* only show 1bps on mainnet

* rename VERY_LOW to LOWEST

* upgrade to v3-sdk 3.7.0

* add snapshot testing for lowest tier

* fix integration test

* fix integration test

* use ALL_SUPPORTED_CHAIN_IDS over string all

* consider 0.01% tier in pool (#2770)

* merge main and only consider lowest tier for mainnet

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): update block warning updater to check most recent block timestamp (#2777)

* update block warning updater to check most recent block timestamp

* stop doing dumb state manipulation

* fix: copy in network alert

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): remove redux from chain connectivity (#2781)

* remove redux from chain connectivity

* useMachineTimeMs instead of Date.now to force updates, useCurrentBlockTimestamp

* use useInterval

* change not created font size to 10 (#2785)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: format date using Date.toLocaleString (#2459)

* fix: format date using Date.toLocaleString

Fixes #2458

* fix: date typings

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: broken link to docs (#2816)

* chore: fix typo in useAllCurrencyCombinations.ts (#2778)

occurence -> occurrence

* chore: update typechain scripts for Windows (#2707)

There are two errors when deploying on Windows system:
1. Using single quotes in path argument doesn't seem to be accepted in typechain command
2. `?(v3-core|v3-periphery)` operator doesn't work

Here are fixes/workarounds.

* perf: lazy load vote related routes (#2468)

* perf: lazy load vote related routes

* wrap Switch in Suspense

* remove exact to match nested routes

* fix nested routes

* split Landing

* fix

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: Enable 3085 requests for coinbase wallet (#2753)

enable 3085 requests for coinbase wallet

* feat: set the auto slippage tolerance by the dollar value of gas (#2815)

* feat: set the auto slippage tolerance by the dollar value of gas

* comments

* min/max at 0.5% to 25%

* oops on constant

* address review feedback

* Fixing #2818 (#2820)

* Fix code style issues with ESLint

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: fix #2818

* chore(i18n): synchronize translations from crowdin [skip ci]

* log an event on max click (#2827)

* Add trailing slash to L2 info links (#2696)

Some links were broken. For example on /pools/ page click the 'Top Pools' CTA. It would mistakenly direct you to info.uniswap.org/optimismpools instead of optimism/pools

* fix(L2): block L2 tokens explicitly linked to L1 tokens that are blocked (#2721)

* block L2 tokens explicitly linked to L1 tokens that are blocked

* Fix code style issues with ESLint

* check for support on all connectors, and disable when the connector (or lack thereof) no longer supports 3085 (#2824)

* feat: display an ENS avatar (#2806)

* feat: ens avatar resolution

* chore: uninstall @davatar/react

* fix: add avatar alt

* feat: support data uris

* feat: support arweave uris

* feat: support erc721 avatars

* feat: support erc1155 avatars

* fix: jazzicon integration

* fix: clean usage of status icon

* fix: fix jazzicon svg offset

* refactor: share status icon component

* fix: pass memoized args to multicall

* Update locales.ts (#2825)

update Finnish from person (Suomalainen) to language (suomi)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore: fix the build blocking linter error

* chore: run linters with auto_fix = false for forks (#2852)

* fix: do not show urls if issue is not occurring on app.uniswap.org (#2855)

* fix: do not show urls if issue is not occurring on app.uniswap.org

fixes https://github.com/Uniswap/interface/issues/2572

* address comment

* fix: remove orphaned node (#2863)

* fix: remove orphaned node

* fix: react cleanup

* refactor: use ref for jazzicon (#2874)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps): bump ws from 5.2.2 to 5.2.3 (#2759)

Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump url-parse from 1.5.1 to 1.5.3 (#2504)

Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* add more tests for tryParseTick (#2110)

* fix(lint): clean up the eslint config (#2886)

* fix(lint): clean up the eslint config

* Fix code style issues with ESLint

* fix the linter errors that arose from using the proper config

* clean up the rebass text renames

* fix if statement, use the config

* use the same name prefix for both steps

* `TextPreset` -> `ThemedText`

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: Add routes for stakewise tokens (#2832)

* Add additional routes for stakewise tokens

* Reference StakeWise addresses with sdk tokens

* Sort token imports

* chore: yarn-deduplicate

* chore: lint widgets

* fix: use lib useInterval

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Micael Rodrigues <micaelr95@outlook.pt>
Co-authored-by: Justin Domingue <judo@uniswap.org>
Co-authored-by: Moody Salem <moodysalem@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M0kY <46133205+M0kY@users.noreply.github.com>
Co-authored-by: M0kY <moky@example.com>
Co-authored-by: Will Hennessy <hennessywill@gmail.com>
Co-authored-by: Brendan Weinstein <65564422+brendanww@users.noreply.github.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
Co-authored-by: Ben Krochta <35636764+bkrochta@users.noreply.github.com>
Co-authored-by: Moody Salem <moody.salem@gmail.com>
Co-authored-by: Raj <sukhrajghuman@live.com>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: Matthew Salamon <35425388+Matthews3301@users.noreply.github.com>
Co-authored-by: Sam Chen <chenxsan@gmail.com>
Co-authored-by: Ali Eray Kısabacak <eraykisabacak@hotmail.com>
Co-authored-by: Kimmo S <kkpsiren@gmail.com>
Co-authored-by: Dmitri Tsumak <tsumak.dmitri@gmail.com>

* chore: merge main into widgets (#2923)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: center focused outline card (#2625)

* fix: add usdc to arbitrum/optimism common bases (#2641)

* remove WETH from optimism bases (#2640)

* use l2 logos in base pairs (#2634)

* fix: split calls into more chunks if they fail due to out of gas errors (#2630)

* fix: split calls into more chunks if they fail due to out of gas errors

* set to 100m gas

* back to 25m so we batch fewer calls

* do not pass through gas limit, some simplification of the code

* unused import

* fix: restrict @davatar usage to avoid 3p fetches (#2649)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): ensure chainIds match before fetching pool data (#2652)

* ensure chainIds match before fetching pool data

* debounce both input currencies, and only look for pairs on currencies that share a chainId

* pr feedback

* fix: use optional operator for chainId (#2666)

* chore: update token list (#2670)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: update token list (#2671)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: extend privacy and terms (#2623)

* initial iteration

* add logging

* added hook

* polish

* remove unused import

* add hash

* addressed pr feedback

* remove autorouter icon

* use firebase store

* style

* adjust recat ga

* log remove liquidity

* update copy

* addressed pr feedback

* addressed pr feedback

* prevent privacy content from dismissing modal

* make top-level key origin

* use hostname

* restore trm

* chore(i18n): synchronize translations from crowdin [skip ci]

* log full signed tx (#2681)

* refactor monitoring (#2682)

* chore: set final privacy learn more link' (#2684)

* add learn more button

* add final link

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: back arrow bug in wallet modal and fill tx for wallet (#2687)

* add tx to wallet connect

* remove id from env

* restore env

* block import of unsupported tokens (#2673)

generalize custom import token block ui

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps-dev): bump @uniswap/token-lists (#2699)

* chore(i18n): synchronize translations from crowdin [skip ci]

* try out 'dimension1' (#2704)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: walletconnect modal re-open after user rejection (#2693)

Co-authored-by: M0kY <moky@example.com>

* chore: update unsupported token list (#2689)

* chore: update unsupported token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: memoize the list stuff so the tokens are consistently clickable (#2724)

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: update cmc list link (#2710)

* update cmc lists

* update CMC url

* add token to unsupported list (#2732)

* don't overwrite localstorage lists when fetch throws (#2723)

* try cd1 for custom dimension (#2734)

* fix: Update walletlink-connector to 6.2.8 (#2655)

* Update walletlink-connector to 6.2.5 which has a walletlink update to support addEthereumChain+switchEthereumChain requests

* Update walletlink-connector to 6.2.7

* Update walletlink-connector to 6.2.8

* fix: Parse latest proposal description correctly

* add proposal start time (#2738)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: #2741 Increase liquidity form off center (#2746)

* fix: bump to latest token list including ENS token

* fix: remove deprecated optimism status url (#2771)

* feat: Menu update. Add help center & feature requests. Remove analytics & github. (#2709)

* Add help center, remove analytics from menu

* Add canny feature requests link, remove github link

* add coffee icon

* no unused imports eslint rule (#2773)

* chore(i18n): synchronize translations from crowdin [skip ci]

* add protocols param to quote endpoint (#2774)

* add protocols param to quote endpoint

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: lint error (#2775)

* fix(optimism): Optimism regenesis support (#2703)

* feat(optimism): optimistic kovan local regenesis changes

* use the regenesis version of the sdk

* remove the override no longer necessary

* diff rpc url

* back to kovan url

* lint error

* Optimism mainnet regenesis test (#2695)

* remove the optimism mainnet specific code and point to the mainnet regenesis rpc url

* point at the old mainnet multicall address

* bump the sdk version

* copy the list

* multicall address regenesis change

* revert the gas limit special casing for optimism

* bump the sdk version

* remove a couple other temporary edits

* unused test case

* specific version of v3-sdk

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: add support for 0.01% tier (#2769)

* chore: add support for 0.01% tier

* only show 1bps on mainnet

* rename VERY_LOW to LOWEST

* upgrade to v3-sdk 3.7.0

* add snapshot testing for lowest tier

* fix integration test

* fix integration test

* use ALL_SUPPORTED_CHAIN_IDS over string all

* consider 0.01% tier in pool (#2770)

* merge main and only consider lowest tier for mainnet

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): update block warning updater to check most recent block timestamp (#2777)

* update block warning updater to check most recent block timestamp

* stop doing dumb state manipulation

* fix: copy in network alert

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): remove redux from chain connectivity (#2781)

* remove redux from chain connectivity

* useMachineTimeMs instead of Date.now to force updates, useCurrentBlockTimestamp

* use useInterval

* change not created font size to 10 (#2785)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: format date using Date.toLocaleString (#2459)

* fix: format date using Date.toLocaleString

Fixes #2458

* fix: date typings

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: broken link to docs (#2816)

* chore: fix typo in useAllCurrencyCombinations.ts (#2778)

occurence -> occurrence

* chore: update typechain scripts for Windows (#2707)

There are two errors when deploying on Windows system:
1. Using single quotes in path argument doesn't seem to be accepted in typechain command
2. `?(v3-core|v3-periphery)` operator doesn't work

Here are fixes/workarounds.

* perf: lazy load vote related routes (#2468)

* perf: lazy load vote related routes

* wrap Switch in Suspense

* remove exact to match nested routes

* fix nested routes

* split Landing

* fix

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: Enable 3085 requests for coinbase wallet (#2753)

enable 3085 requests for coinbase wallet

* feat: set the auto slippage tolerance by the dollar value of gas (#2815)

* feat: set the auto slippage tolerance by the dollar value of gas

* comments

* min/max at 0.5% to 25%

* oops on constant

* address review feedback

* Fixing #2818 (#2820)

* Fix code style issues with ESLint

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: fix #2818

* chore(i18n): synchronize translations from crowdin [skip ci]

* log an event on max click (#2827)

* Add trailing slash to L2 info links (#2696)

Some links were broken. For example on /pools/ page click the 'Top Pools' CTA. It would mistakenly direct you to info.uniswap.org/optimismpools instead of optimism/pools

* fix(L2): block L2 tokens explicitly linked to L1 tokens that are blocked (#2721)

* block L2 tokens explicitly linked to L1 tokens that are blocked

* Fix code style issues with ESLint

* check for support on all connectors, and disable when the connector (or lack thereof) no longer supports 3085 (#2824)

* feat: display an ENS avatar (#2806)

* feat: ens avatar resolution

* chore: uninstall @davatar/react

* fix: add avatar alt

* feat: support data uris

* feat: support arweave uris

* feat: support erc721 avatars

* feat: support erc1155 avatars

* fix: jazzicon integration

* fix: clean usage of status icon

* fix: fix jazzicon svg offset

* refactor: share status icon component

* fix: pass memoized args to multicall

* Update locales.ts (#2825)

update Finnish from person (Suomalainen) to language (suomi)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore: fix the build blocking linter error

* chore: run linters with auto_fix = false for forks (#2852)

* fix: do not show urls if issue is not occurring on app.uniswap.org (#2855)

* fix: do not show urls if issue is not occurring on app.uniswap.org

fixes https://github.com/Uniswap/interface/issues/2572

* address comment

* fix: remove orphaned node (#2863)

* fix: remove orphaned node

* fix: react cleanup

* refactor: use ref for jazzicon (#2874)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps): bump ws from 5.2.2 to 5.2.3 (#2759)

Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump url-parse from 1.5.1 to 1.5.3 (#2504)

Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* add more tests for tryParseTick (#2110)

* fix(lint): clean up the eslint config (#2886)

* fix(lint): clean up the eslint config

* Fix code style issues with ESLint

* fix the linter errors that arose from using the proper config

* clean up the rebass text renames

* fix if statement, use the config

* use the same name prefix for both steps

* `TextPreset` -> `ThemedText`

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: Add routes for stakewise tokens (#2832)

* Add additional routes for stakewise tokens

* Reference StakeWise addresses with sdk tokens

* Sort token imports

* fix: fix layout of proposal list items on the vote page on mobile (#2898)

* fix: fixing layout from using grid to flexbox

* fix: setting WrapSmall to nowrap due to layout issue on mobile

* fix: using width auto instead of disabling flex wrap

Co-authored-by: Julian Anderson <juliancanderson@gmail.com>

* fix: typo in arweave URI recognition (#2901)

* deleted files

* Revert "Merge branch 'main' of https://github.com/Uniswap/interface" (#2912)

This reverts commit bf7a40be7a0a37b5051b9a877bbea563fba5782d, reversing
changes made to 097b8361d4c09afd3cb681c4622145c555ced884.

* fix: inadvertent merges/reverts (#2915)

* Revert "Revert "Merge branch 'main' of https://github.com/Uniswap/interface" (#2912)"

This reverts commit 7d343dcfbdf75a2f91d28cefce84e4b1bace7b87.

* Revert "deleted files"

This reverts commit 097b8361d4c09afd3cb681c4622145c555ced884.

* refactor: Replace multicall implementation with library (#2768)

- Replace the local implementation of multicall with the new redux-multicall lib
- Create wrappers for redux-multicall hooks to inject block number and chainId

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Micael Rodrigues <micaelr95@outlook.pt>
Co-authored-by: Justin Domingue <judo@uniswap.org>
Co-authored-by: Moody Salem <moodysalem@users.noreply.github.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M0kY <46133205+M0kY@users.noreply.github.com>
Co-authored-by: M0kY <moky@example.com>
Co-authored-by: Will Hennessy <hennessywill@gmail.com>
Co-authored-by: Brendan Weinstein <65564422+brendanww@users.noreply.github.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
Co-authored-by: Ben Krochta <35636764+bkrochta@users.noreply.github.com>
Co-authored-by: Moody Salem <moody.salem@gmail.com>
Co-authored-by: Raj <sukhrajghuman@live.com>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: Matthew Salamon <35425388+Matthews3301@users.noreply.github.com>
Co-authored-by: Sam Chen <chenxsan@gmail.com>
Co-authored-by: Ali Eray Kısabacak <eraykisabacak@hotmail.com>
Co-authored-by: Kimmo S <kkpsiren@gmail.com>
Co-authored-by: Dmitri Tsumak <tsumak.dmitri@gmail.com>
Co-authored-by: Julian Anderson <juliancanderson@gmail.com>
Co-authored-by: Carlos Diaz-Padron <carlosdiazpadron@gmail.com>
Co-authored-by: J M Rossy <jm.rossy@gmail.com>

* feat: Multicall lib integration for widgets (#2946)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: center focused outline card (#2625)

* fix: add usdc to arbitrum/optimism common bases (#2641)

* remove WETH from optimism bases (#2640)

* use l2 logos in base pairs (#2634)

* fix: split calls into more chunks if they fail due to out of gas errors (#2630)

* fix: split calls into more chunks if they fail due to out of gas errors

* set to 100m gas

* back to 25m so we batch fewer calls

* do not pass through gas limit, some simplification of the code

* unused import

* fix: restrict @davatar usage to avoid 3p fetches (#2649)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): ensure chainIds match before fetching pool data (#2652)

* ensure chainIds match before fetching pool data

* debounce both input currencies, and only look for pairs on currencies that share a chainId

* pr feedback

* fix: use optional operator for chainId (#2666)

* chore: update token list (#2670)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: update token list (#2671)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: extend privacy and terms (#2623)

* initial iteration

* add logging

* added hook

* polish

* remove unused import

* add hash

* addressed pr feedback

* remove autorouter icon

* use firebase store

* style

* adjust recat ga

* log remove liquidity

* update copy

* addressed pr feedback

* addressed pr feedback

* prevent privacy content from dismissing modal

* make top-level key origin

* use hostname

* restore trm

* chore(i18n): synchronize translations from crowdin [skip ci]

* log full signed tx (#2681)

* refactor monitoring (#2682)

* chore: set final privacy learn more link' (#2684)

* add learn more button

* add final link

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: back arrow bug in wallet modal and fill tx for wallet (#2687)

* add tx to wallet connect

* remove id from env

* restore env

* block import of unsupported tokens (#2673)

generalize custom import token block ui

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps-dev): bump @uniswap/token-lists (#2699)

* chore(i18n): synchronize translations from crowdin [skip ci]

* try out 'dimension1' (#2704)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: walletconnect modal re-open after user rejection (#2693)

Co-authored-by: M0kY <moky@example.com>

* chore: update unsupported token list (#2689)

* chore: update unsupported token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: memoize the list stuff so the tokens are consistently clickable (#2724)

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: update cmc list link (#2710)

* update cmc lists

* update CMC url

* add token to unsupported list (#2732)

* don't overwrite localstorage lists when fetch throws (#2723)

* try cd1 for custom dimension (#2734)

* fix: Update walletlink-connector to 6.2.8 (#2655)

* Update walletlink-connector to 6.2.5 which has a walletlink update to support addEthereumChain+switchEthereumChain requests

* Update walletlink-connector to 6.2.7

* Update walletlink-connector to 6.2.8

* fix: Parse latest proposal description correctly

* add proposal start time (#2738)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: #2741 Increase liquidity form off center (#2746)

* fix: bump to latest token list including ENS token

* fix: remove deprecated optimism status url (#2771)

* feat: Menu update. Add help center & feature requests. Remove analytics & github. (#2709)

* Add help center, remove analytics from menu

* Add canny feature requests link, remove github link

* add coffee icon

* no unused imports eslint rule (#2773)

* chore(i18n): synchronize translations from crowdin [skip ci]

* add protocols param to quote endpoint (#2774)

* add protocols param to quote endpoint

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: lint error (#2775)

* fix(optimism): Optimism regenesis support (#2703)

* feat(optimism): optimistic kovan local regenesis changes

* use the regenesis version of the sdk

* remove the override no longer necessary

* diff rpc url

* back to kovan url

* lint error

* Optimism mainnet regenesis test (#2695)

* remove the optimism mainnet specific code and point to the mainnet regenesis rpc url

* point at the old mainnet multicall address

* bump the sdk version

* copy the list

* multicall address regenesis change

* revert the gas limit special casing for optimism

* bump the sdk version

* remove a couple other temporary edits

* unused test case

* specific version of v3-sdk

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: add support for 0.01% tier (#2769)

* chore: add support for 0.01% tier

* only show 1bps on mainnet

* rename VERY_LOW to LOWEST

* upgrade to v3-sdk 3.7.0

* add snapshot testing for lowest tier

* fix integration test

* fix integration test

* use ALL_SUPPORTED_CHAIN_IDS over string all

* consider 0.01% tier in pool (#2770)

* merge main and only consider lowest tier for mainnet

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): update block warning updater to check most recent block timestamp (#2777)

* update block warning updater to check most recent block timestamp

* stop doing dumb state manipulation

* fix: copy in network alert

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): remove redux from chain connectivity (#2781)

* remove redux from chain connectivity

* useMachineTimeMs instead of Date.now to force updates, useCurrentBlockTimestamp

* use useInterval

* change not created font size to 10 (#2785)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: format date using Date.toLocaleString (#2459)

* fix: format date using Date.toLocaleString

Fixes #2458

* fix: date typings

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: broken link to docs (#2816)

* chore: fix typo in useAllCurrencyCombinations.ts (#2778)

occurence -> occurrence

* chore: update typechain scripts for Windows (#2707)

There are two errors when deploying on Windows system:
1. Using single quotes in path argument doesn't seem to be accepted in typechain command
2. `?(v3-core|v3-periphery)` operator doesn't work

Here are fixes/workarounds.

* perf: lazy load vote related routes (#2468)

* perf: lazy load vote related routes

* wrap Switch in Suspense

* remove exact to match nested routes

* fix nested routes

* split Landing

* fix

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: Enable 3085 requests for coinbase wallet (#2753)

enable 3085 requests for coinbase wallet

* feat: set the auto slippage tolerance by the dollar value of gas (#2815)

* feat: set the auto slippage tolerance by the dollar value of gas

* comments

* min/max at 0.5% to 25%

* oops on constant

* address review feedback

* Fixing #2818 (#2820)

* Fix code style issues with ESLint

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: fix #2818

* chore(i18n): synchronize translations from crowdin [skip ci]

* log an event on max click (#2827)

* Add trailing slash to L2 info links (#2696)

Some links were broken. For example on /pools/ page click the 'Top Pools' CTA. It would mistakenly direct you to info.uniswap.org/optimismpools instead of optimism/pools

* fix(L2): block L2 tokens explicitly linked to L1 tokens that are blocked (#2721)

* block L2 tokens explicitly linked to L1 tokens that are blocked

* Fix code style issues with ESLint

* check for support on all connectors, and disable when the connector (or lack thereof) no longer supports 3085 (#2824)

* feat: display an ENS avatar (#2806)

* feat: ens avatar resolution

* chore: uninstall @davatar/react

* fix: add avatar alt

* feat: support data uris

* feat: support arweave uris

* feat: support erc721 avatars

* feat: support erc1155 avatars

* fix: jazzicon integration

* fix: clean usage of status icon

* fix: fix jazzicon svg offset

* refactor: share status icon component

* fix: pass memoized args to multicall

* Update locales.ts (#2825)

update Finnish from person (Suomalainen) to language (suomi)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore: fix the build blocking linter error

* chore: run linters with auto_fix = false for forks (#2852)

* fix: do not show urls if issue is not occurring on app.uniswap.org (#2855)

* fix: do not show urls if issue is not occurring on app.uniswap.org

fixes https://github.com/Uniswap/interface/issues/2572

* address comment

* fix: remove orphaned node (#2863)

* fix: remove orphaned node

* fix: react cleanup

* refactor: use ref for jazzicon (#2874)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps): bump ws from 5.2.2 to 5.2.3 (#2759)

Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump url-parse from 1.5.1 to 1.5.3 (#2504)

Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* add more tests for tryParseTick (#2110)

* fix(lint): clean up the eslint config (#2886)

* fix(lint): clean up the eslint config

* Fix code style issues with ESLint

* fix the linter errors that arose from using the proper config

* clean up the rebass text renames

* fix if statement, use the config

* use the same name prefix for both steps

* `TextPreset` -> `ThemedText`

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: Add routes for stakewise tokens (#2832)

* Add additional routes for stakewise tokens

* Reference StakeWise addresses with sdk tokens

* Sort token imports

* fix: fix layout of proposal list items on the vote page on mobile (#2898)

* fix: fixing layout from using grid to flexbox

* fix: setting WrapSmall to nowrap due to layout issue on mobile

* fix: using width auto instead of disabling flex wrap

Co-authored-by: Julian Anderson <juliancanderson@gmail.com>

* fix: typo in arweave URI recognition (#2901)

* deleted files

* Revert "Merge branch 'main' of https://github.com/Uniswap/interface" (#2912)

This reverts commit bf7a40be7a0a37b5051b9a877bbea563fba5782d, reversing
changes made to 097b8361d4c09afd3cb681c4622145c555ced884.

* fix: inadvertent merges/reverts (#2915)

* Revert "Revert "Merge branch 'main' of https://github.com/Uniswap/interface" (#2912)"

This reverts commit 7d343dcfbdf75a2f91d28cefce84e4b1bace7b87.

* Revert "deleted files"

This reverts commit 097b8361d4c09afd3cb681c4622145c555ced884.

* refactor: Replace multicall implementation with library (#2768)

- Replace the local implementation of multicall with the new redux-multicall lib
- Create wrappers for redux-multicall hooks to inject block number and chainId

* package.json tweaks

* add multicall lib and some basic provider things

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Micael Rodrigues <micaelr95@outlook.pt>
Co-authored-by: Justin Domingue <judo@uniswap.org>
Co-authored-by: Moody Salem <moodysalem@users.noreply.github.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M0kY <46133205+M0kY@users.noreply.github.com>
Co-authored-by: M0kY <moky@example.com>
Co-authored-by: Will Hennessy <hennessywill@gmail.com>
Co-authored-by: Brendan Weinstein <65564422+brendanww@users.noreply.github.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
Co-authored-by: Ben Krochta <35636764+bkrochta@users.noreply.github.com>
Co-authored-by: Moody Salem <moody.salem@gmail.com>
Co-authored-by: Raj <sukhrajghuman@live.com>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: Matthew Salamon <35425388+Matthews3301@users.noreply.github.com>
Co-authored-by: Sam Chen <chenxsan@gmail.com>
Co-authored-by: Ali Eray Kısabacak <eraykisabacak@hotmail.com>
Co-authored-by: Kimmo S <kkpsiren@gmail.com>
Co-authored-by: Dmitri Tsumak <tsumak.dmitri@gmail.com>
Co-authored-by: Julian Anderson <juliancanderson@gmail.com>
Co-authored-by: Carlos Diaz-Padron <carlosdiazpadron@gmail.com>
Co-authored-by: J M Rossy <jm.rossy@gmail.com>

* feat: widgets style update (#2939)

* feat: widgets empty state (#2951)

* chore: mv onHover to computed theme; reduce to 0.16

* chore: transparentize primary on hover

* chore: transparentize dynamic primary on hover

* style: restrict icon usage

Restricts icons to lib/icons. This ensures that icons are loaded as singletons outside of the React lifecycle. Doing otherwise hinders performance.

* fix: logo mix-blend-mode

* wip: empty states

* fix: accent/active colors

* wip: empty states

* fix: input hover states

* nit: specific user select

* nit: button transition

* nit: no button transition

* chore: better cosmos toggles

* chore: load inter

* make cosmos work with new required widget props (#2956)

* separate connector atoms (#2959)

* fix: widgets nits sans summary/status (#2960)

* fix: dynamic scrollbar

* feat: system theme hook

* nit: settings

* nit: large settings icons

* fix: accessible color computation

* fix: ignore status scroll for now

* fix: ignore txs scroll for now

* feat: widgets summary (#2980)

* fix: output first in toolbar

* fix: widget height

* feat: token color extraction toggle

* fix: header sizing

* fix: height nits

* chore: re-arch sub pages

* nit: height

* feat: border radius as range

* fix: exclude cosmos setter from hook deps

* feat: default width to 360

* feat: type classes

* fix: header height

* fix: default cosmos width to 360

* refactor: icon button

* wip: summary

* fix: scrollbar

* feat: summary

* fix: summary expando

* fix: widgets transitions (#2983)

* fix: action button height

* fix: summary scrollbar fading

* fix: summary fixture

* fix: action button transitions

* feat: widgets status (#2987)

* fix: action button height

* fix: summary scrollbar fading

* fix: summary fixture

* fix: action button transitions

* refactor: commit spinner as svg asset

* feat: status dialog

* fix: spinner rounding

* feat: widgets fonts and transitions (#2998)

* feat: fonts using @fontsource

* feat: dialog transitions

* fix: swap transitions

* Refactor use active web3 react (#3002)

* separate connector atoms

* refactor cosmos and set up widgets env var

* fix: cosmos modularization (#3014)

* fix: cosmos modularization

* fix: web3 in atom provider

* feat: make connectors resettable

* drop empty test (#3022)

* Revert "feat: make connectors resettable"

This reverts commit db5af68b9be1edf4d6e1b7dc8ed2004f19e33f16.

* undo dumb open reorder

* bump widget web3-react versions

* bump to fix tests

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>

* chore(widgets): Merge main into widgets (#3013)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: center focused outline card (#2625)

* fix: add usdc to arbitrum/optimism common bases (#2641)

* remove WETH from optimism bases (#2640)

* use l2 logos in base pairs (#2634)

* fix: split calls into more chunks if they fail due to out of gas errors (#2630)

* fix: split calls into more chunks if they fail due to out of gas errors

* set to 100m gas

* back to 25m so we batch fewer calls

* do not pass through gas limit, some simplification of the code

* unused import

* fix: restrict @davatar usage to avoid 3p fetches (#2649)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): ensure chainIds match before fetching pool data (#2652)

* ensure chainIds match before fetching pool data

* debounce both input currencies, and only look for pairs on currencies that share a chainId

* pr feedback

* fix: use optional operator for chainId (#2666)

* chore: update token list (#2670)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: update token list (#2671)

* update token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: extend privacy and terms (#2623)

* initial iteration

* add logging

* added hook

* polish

* remove unused import

* add hash

* addressed pr feedback

* remove autorouter icon

* use firebase store

* style

* adjust recat ga

* log remove liquidity

* update copy

* addressed pr feedback

* addressed pr feedback

* prevent privacy content from dismissing modal

* make top-level key origin

* use hostname

* restore trm

* chore(i18n): synchronize translations from crowdin [skip ci]

* log full signed tx (#2681)

* refactor monitoring (#2682)

* chore: set final privacy learn more link' (#2684)

* add learn more button

* add final link

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: back arrow bug in wallet modal and fill tx for wallet (#2687)

* add tx to wallet connect

* remove id from env

* restore env

* block import of unsupported tokens (#2673)

generalize custom import token block ui

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps-dev): bump @uniswap/token-lists (#2699)

* chore(i18n): synchronize translations from crowdin [skip ci]

* try out 'dimension1' (#2704)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: walletconnect modal re-open after user rejection (#2693)

Co-authored-by: M0kY <moky@example.com>

* chore: update unsupported token list (#2689)

* chore: update unsupported token list

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: memoize the list stuff so the tokens are consistently clickable (#2724)

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: update cmc list link (#2710)

* update cmc lists

* update CMC url

* add token to unsupported list (#2732)

* don't overwrite localstorage lists when fetch throws (#2723)

* try cd1 for custom dimension (#2734)

* fix: Update walletlink-connector to 6.2.8 (#2655)

* Update walletlink-connector to 6.2.5 which has a walletlink update to support addEthereumChain+switchEthereumChain requests

* Update walletlink-connector to 6.2.7

* Update walletlink-connector to 6.2.8

* fix: Parse latest proposal description correctly

* add proposal start time (#2738)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: #2741 Increase liquidity form off center (#2746)

* fix: bump to latest token list including ENS token

* fix: remove deprecated optimism status url (#2771)

* feat: Menu update. Add help center & feature requests. Remove analytics & github. (#2709)

* Add help center, remove analytics from menu

* Add canny feature requests link, remove github link

* add coffee icon

* no unused imports eslint rule (#2773)

* chore(i18n): synchronize translations from crowdin [skip ci]

* add protocols param to quote endpoint (#2774)

* add protocols param to quote endpoint

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: lint error (#2775)

* fix(optimism): Optimism regenesis support (#2703)

* feat(optimism): optimistic kovan local regenesis changes

* use the regenesis version of the sdk

* remove the override no longer necessary

* diff rpc url

* back to kovan url

* lint error

* Optimism mainnet regenesis test (#2695)

* remove the optimism mainnet specific code and point to the mainnet regenesis rpc url

* point at the old mainnet multicall address

* bump the sdk version

* copy the list

* multicall address regenesis change

* revert the gas limit special casing for optimism

* bump the sdk version

* remove a couple other temporary edits

* unused test case

* specific version of v3-sdk

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(i18n): synchronize translations from crowdin [skip ci]

* feat: add support for 0.01% tier (#2769)

* chore: add support for 0.01% tier

* only show 1bps on mainnet

* rename VERY_LOW to LOWEST

* upgrade to v3-sdk 3.7.0

* add snapshot testing for lowest tier

* fix integration test

* fix integration test

* use ALL_SUPPORTED_CHAIN_IDS over string all

* consider 0.01% tier in pool (#2770)

* merge main and only consider lowest tier for mainnet

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): update block warning updater to check most recent block timestamp (#2777)

* update block warning updater to check most recent block timestamp

* stop doing dumb state manipulation

* fix: copy in network alert

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix(L2): remove redux from chain connectivity (#2781)

* remove redux from chain connectivity

* useMachineTimeMs instead of Date.now to force updates, useCurrentBlockTimestamp

* use useInterval

* change not created font size to 10 (#2785)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: format date using Date.toLocaleString (#2459)

* fix: format date using Date.toLocaleString

Fixes #2458

* fix: date typings

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: broken link to docs (#2816)

* chore: fix typo in useAllCurrencyCombinations.ts (#2778)

occurence -> occurrence

* chore: update typechain scripts for Windows (#2707)

There are two errors when deploying on Windows system:
1. Using single quotes in path argument doesn't seem to be accepted in typechain command
2. `?(v3-core|v3-periphery)` operator doesn't work

Here are fixes/workarounds.

* perf: lazy load vote related routes (#2468)

* perf: lazy load vote related routes

* wrap Switch in Suspense

* remove exact to match nested routes

* fix nested routes

* split Landing

* fix

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: Enable 3085 requests for coinbase wallet (#2753)

enable 3085 requests for coinbase wallet

* feat: set the auto slippage tolerance by the dollar value of gas (#2815)

* feat: set the auto slippage tolerance by the dollar value of gas

* comments

* min/max at 0.5% to 25%

* oops on constant

* address review feedback

* Fixing #2818 (#2820)

* Fix code style issues with ESLint

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: fix #2818

* chore(i18n): synchronize translations from crowdin [skip ci]

* log an event on max click (#2827)

* Add trailing slash to L2 info links (#2696)

Some links were broken. For example on /pools/ page click the 'Top Pools' CTA. It would mistakenly direct you to info.uniswap.org/optimismpools instead of optimism/pools

* fix(L2): block L2 tokens explicitly linked to L1 tokens that are blocked (#2721)

* block L2 tokens explicitly linked to L1 tokens that are blocked

* Fix code style issues with ESLint

* check for support on all connectors, and disable when the connector (or lack thereof) no longer supports 3085 (#2824)

* feat: display an ENS avatar (#2806)

* feat: ens avatar resolution

* chore: uninstall @davatar/react

* fix: add avatar alt

* feat: support data uris

* feat: support arweave uris

* feat: support erc721 avatars

* feat: support erc1155 avatars

* fix: jazzicon integration

* fix: clean usage of status icon

* fix: fix jazzicon svg offset

* refactor: share status icon component

* fix: pass memoized args to multicall

* Update locales.ts (#2825)

update Finnish from person (Suomalainen) to language (suomi)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore: fix the build blocking linter error

* chore: run linters with auto_fix = false for forks (#2852)

* fix: do not show urls if issue is not occurring on app.uniswap.org (#2855)

* fix: do not show urls if issue is not occurring on app.uniswap.org

fixes https://github.com/Uniswap/interface/issues/2572

* address comment

* fix: remove orphaned node (#2863)

* fix: remove orphaned node

* fix: react cleanup

* refactor: use ref for jazzicon (#2874)

* chore(i18n): synchronize translations from crowdin [skip ci]

* chore(deps): bump ws from 5.2.2 to 5.2.3 (#2759)

Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump url-parse from 1.5.1 to 1.5.3 (#2504)

Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* add more tests for tryParseTick (#2110)

* fix(lint): clean up the eslint config (#2886)

* fix(lint): clean up the eslint config

* Fix code style issues with ESLint

* fix the linter errors that arose from using the proper config

* clean up the rebass text renames

* fix if statement, use the config

* use the same name prefix for both steps

* `TextPreset` -> `ThemedText`

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: Add routes for stakewise tokens (#2832)

* Add additional routes for stakewise tokens

* Reference StakeWise addresses with sdk tokens

* Sort token imports

* fix: fix layout of proposal list items on the vote page on mobile (#2898)

* fix: fixing layout from using grid to flexbox

* fix: setting WrapSmall to nowrap due to layout issue on mobile

* fix: using width auto instead of disabling flex wrap

Co-authored-by: Julian Anderson <juliancanderson@gmail.com>

* fix: typo in arweave URI recognition (#2901)

* deleted files

* Revert "Merge branch 'main' of https://github.com/Uniswap/interface" (#2912)

This reverts commit bf7a40be7a0a37b5051b9a877bbea563fba5782d, reversing
changes made to 097b8361d4c09afd3cb681c4622145c555ced884.

* fix: inadvertent merges/reverts (#2915)

* Revert "Revert "Merge branch 'main' of https://github.com/Uniswap/interface" (#2912)"

This reverts commit 7d343dcfbdf75a2f91d28cefce84e4b1bace7b87.

* Revert "deleted files"

This reverts commit 097b8361d4c09afd3cb681c4622145c555ced884.

* refactor: Replace multicall implementation with library (#2768)

- Replace the local implementation of multicall with the new redux-multicall lib
- Create wrappers for redux-multicall hooks to inject block number and chainId

* fix: introduce safeNamehash (#2925)

* namehash -> safeNamehash where necessary

* cleanup

* address comment

* feat: Add learn more link in TRM description (#2919)

* Add learn more link in TRM description

* Update src/components/PrivacyPolicy/index.tsx

Co-authored-by: Justin Domingue <judo@uniswap.org>

* give a bit more gas to balanceOf (#2943)

* fix: memoize hooks from /swap (#2949)

* fix: memoize hooks from /swap

* chore: rm console

* add fix for polygon proposal title (#2974)

* fix: display Uniswap token list in UI (#2821)

* fix: display Uniswap token list in UI

* chore: remove default-token-list build dependency

* fix: use ENS name for Uniswap token list

* fix: change Uniswap token list url

* fix: extend transaction deadline to 3 days (#2982)

* feat: integrate SwapRouter02 on L1/L2 + gas ui

* client-side smart order router support
* support auto router on L2s
* add swap router version in approval/swap callback GA events to save $ on approval txs
* add persistent UI view of gas estimate on L1s

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>

* Update CONTRIBUTING.md (#2992)

* feat: Update contribution spec (#2993)

* Update CONTRIBUTING.md (#2994)

* Update CONTRIBUTING.md (#2995)

* feat: Update contribution spec (#2996)

* chore(i18n): synchronize translations from crowdin [skip ci]

* fix: double scroll in manage token list (#3020)

* fix double scroll

* remove bottom padding

* restrict walletlink to mainnet only (#3024)

* increase warning timer (#3004)

* add index.html styles to widget

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Micael Rodrigues <micaelr95@outlook.pt>
Co-authored-by: Justin Domingue <judo@uniswap.org>
Co-authored-by: Moody Salem <moodysalem@users.noreply.github.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M0kY <46133205+M0kY@users.noreply.github.com>
Co-authored-by: M0kY <moky@example.com>
Co-authored-by: Will Hennessy <hennessywill@gmail.com>
Co-authored-by: Brendan Weinstein <65564422+brendanww@users.noreply.github.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
Co-authored-by: Ben Krochta <35636764+bkrochta@users.noreply.github.com>
Co-authored-by: Moody Salem <moody.salem@gmail.com>
Co-authored-by: Raj <sukhrajghuman@live.com>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: Matthew Salamon <35425388+Matthews3301@users.noreply.github.com>
Co-authored-by: Sam Chen <chenxsan@gmail.com>
Co-authored-by: Ali Eray Kısabacak <eraykisabacak@hotmail.com>
Co-authored-by: Kimmo S <kkpsiren@gmail.com>
Co-authored-by: Dmitri Tsumak <tsumak.dmitri@gmail.com>
Co-authored-by: Julian Anderson <juliancanderson@gmail.com>
Co-authored-by: Carlos Diaz-Padron <carlosdiazpadron@gmail.com>
Co-authored-by: J M Rossy <jm.rossy@gmail.com>
Co-authored-by: Barry G <bgitarts@gmail.com>
Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
Co-authored-by: Tina Zheng <59578595+tinaszheng@users.noreply.github.com>

* feat: widgets transitions (#3007)

* fix: logo target

* feat: settings transition

* feat: reverse transition

* fix: transitions will-change and durations

* fix: logo color

* fix: only will-change transform

* fix: header targets

* fix: clip modal transitions

* fix: token select header

* fix: safari transparent gradients

* fix: safari scrollbar

* fix: scroll overlay

* fix: safari bounce jank

* fix: firefox overscroll

* refactor: scrollbar hook

* feat: native event hook

* fix: details nowrap

* fix: settings cog transition

* feat: expando icon

* fix: expando transition

* refactor: cosmos web3 integration (#3052)

* chore: use zustand 4.0.0-beta for dynamic stores

* chore: use strict mode

* refactor: clean connector state

* chore: mv web3 state to cosmos selectors

* chore: dedup yarn.lock

* chore: define EthereumProvider in lib

* fix: destructure would not compile

* fix: make it bundle

* chore: prune deps

* refactor: use error handler instead of GA

* chore: add make-plural

* chore: add redux

* chore: yarn dedup

* chore: do not (re)load default locale

* fix: center error headings

* feat: error dialog for boundary

* fix: tighten up transitions

* test: include bundle depcheck

* fix: rm console

* fix: do not load empty sourceLocale

* fix: no lingui defaults

* refactor: mv svg to assets/svg

* chore: block font display

* fix: remove manual zustand resolution

* fix: svg generation script

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>

* chore: widget placeholders (#3061)

* chore: update comments

- typo
- performance comment was performant on retest

* nit: status placeholders

- prevent flashes of rerendering from lazy-loaded elements

* chore: initialize cosmos with json rpc

* refactor: token img component

- modularize the TokenImg
- add a placeholder for UX and broken images

* fix: recent tx token img usage

* pr feedback

* undo REACT_APP_IS_WIDGET network ternary

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Micael Rodrigues <micaelr95@outlook.pt>
Co-authored-by: Justin Domingue <judo@uniswap.org>
Co-authored-by: Moody Salem <moodysalem@users.noreply.github.com>
Co-authored-by: Ian Lapham <ian@uniswap.org>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: M0kY <46133205+M0kY@users.noreply.github.com>
Co-authored-by: M0kY <moky@example.com>
Co-authored-by: Will Hennessy <hennessywill@gmail.com>
Co-authored-by: Brendan Weinstein <65564422+brendanww@users.noreply.github.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
Co-authored-by: Ben Krochta <35636764+bkrochta@users.noreply.github.com>
Co-authored-by: Moody Salem <moody.salem@gmail.com>
Co-authored-by: Raj <sukhrajghuman@live.com>
Co-authored-by: Ikko Ashimine <eltociear@gmail.com>
Co-authored-by: Matthew Salamon <35425388+Matthews3301@users.noreply.github.com>
Co-authored-by: Sam Chen <chenxsan@gmail.com>
Co-authored-by: Ali Eray Kısabacak <eraykisabacak@hotmail.com>
Co-authored-by: Kimmo S <kkpsiren@gmail.com>
Co-authored-by: Dmitri Tsumak <tsumak.dmitri@gmail.com>
Co-authored-by: Julian Anderson <juliancanderson@gmail.com>
Co-authored-by: Carlos Diaz-Padron <carlosdiazpadron@gmail.com>
Co-authored-by: J M Rossy <jm.rossy@gmail.com>
Co-authored-by: Barry G <bgitarts@gmail.com>
Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
Co-authored-by: Tina Zheng <59578595+tinaszheng@users.noreply.github.com>
This commit is contained in:
Jordan Frankfurt 2022-01-05 12:38:53 -06:00 committed by GitHub
parent 5b7a80d10d
commit f9fc506db4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 4903 additions and 80667 deletions

2
.env

@ -1 +1 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"

43
.github/workflows/depcheck.yaml vendored Normal file

@ -0,0 +1,43 @@
name: Bundle Dependency Check
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
depcheck:
name: Bundle depcheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up node
uses: actions/setup-node@v2
with:
node-version: 14
registry-url: https://registry.npmjs.org
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Bundle
run: yarn bundle
- name: Depcheck
run: yarn bundle:depcheck

8
.gitignore vendored

@ -3,10 +3,18 @@
# generated contract types
/src/types/v3
/src/abis/types
/src/lib/locales/**/*.js
/src/lib/locales/**/en-US.po
/src/lib/locales/**/pseudo.po
/src/locales/**/*.js
/src/locales/**/en-US.po
/src/locales/**/pseudo.po
/src/state/data/generated.ts
# generated assets
/src/lib/assets/svg/*.tsx
/src/lib/assets/fonts/*.css
# dependencies
/node_modules

@ -1,7 +1,9 @@
{
"staticPath": "public",
"watchDirs": ["src"],
"watchDirs": [
"src"
],
"webpack": {
"configPath": "react-scripts/config/webpack.config"
"configPath": "react-scripts/config/webpack.config",
"overridePath": "cosmos.override.js"
}
}
}

14
cosmos.override.js Normal file

@ -0,0 +1,14 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const HtmlWebpackPlugin = require('html-webpack-plugin')
// Renders the cosmos fixtures in isolation, instead of using public/index.html.
module.exports = (webpackConfig) => ({
...webpackConfig,
plugins: webpackConfig.plugins.map((plugin) =>
plugin instanceof HtmlWebpackPlugin
? new HtmlWebpackPlugin({
templateContent: '<body></body>',
})
: plugin
),
})

28
depcheck.js Normal file

@ -0,0 +1,28 @@
#!/bin/node
/**
* Checks if any dependencies have been bundled with the interface library.
* Exits with non-zero status if dependencies are included in the bundle.
*/
/* eslint-disable */
const { readFile } = require('fs')
function checkDeps(err, sourcemap) {
if (err) {
console.error(err)
process.exit(1)
}
const includesDeps = sourcemap.includes('node_modules')
if (includesDeps) {
const deps = [...sourcemap.toString().matchAll(/node_modules[\\\/]([^\\\/]*)/g)].map(([, match]) => match)
console.error(`
Sourcemap includes node_modules folder(s). External deps must be bundled under "dependencies".
To fix, run: \`yarn add ${deps.join(' ')}\`
`)
process.exit(1)
}
}
readFile('dist/interface.esm.js.map', checkDeps)

@ -46,11 +46,13 @@ const linguiConfig = {
'vi-VN',
'zh-CN',
'zh-TW',
'pseudo',
],
orderBy: 'messageId',
rootDir: '.',
runtimeConfigModule: ['@lingui/core', 'i18n'],
sourceLocale: 'en-US',
pseudoLocale: 'pseudo',
}
export default linguiConfig

@ -18,14 +18,12 @@
"@graphql-codegen/typescript-operations": "^1.18.2",
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
"@lingui/cli": "^3.9.0",
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@metamask/jazzicon": "^2.0.0",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@svgr/cli": "^5.5.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^7.0.2",
@ -54,13 +52,13 @@
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"@uniswap/default-token-list": "^2.1.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/redux-multicall": "^1.0.0",
"@uniswap/router-sdk": "^1.0.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.5.7",
"@uniswap/smart-order-router": "^2.5.4",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
@ -84,6 +82,7 @@
"d3": "^7.0.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-better-styled-components": "^1.1.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
@ -99,32 +98,23 @@
"ms.macro": "^2.0.0",
"multicodec": "^3.0.1",
"multihashes": "^4.0.2",
"node-vibrant": "^3.2.1-alpha.1",
"polished": "^3.3.2",
"polyfill-object.fromentries": "^1.0.1",
"prettier": "^2.2.1",
"qs": "^6.9.4",
"react": "^17.0.1",
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.3",
"react-dom": "^17.0.1",
"react-feather": "^2.0.8",
"react-ga": "^2.5.7",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-popper": "^2.2.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.0.0",
"react-scripts": "^4.0.3",
"react-spring": "^8.0.27",
"react-use-gesture": "^6.0.14",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"rebass": "^4.0.7",
"redux-localstorage-simple": "^2.3.1",
"sass": "^1.45.1",
"serve": "^11.3.2",
"start-server-and-test": "^1.11.0",
"styled-components": "^5.3.0",
"typechain": "^5.0.0",
"typescript": "^4.2.3",
"ua-parser-js": "^0.7.28",
@ -147,13 +137,18 @@
"prei18n:extract": "touch src/locales/en-US.po",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
"postinstall": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"postinstall": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile && yarn assets:generate",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=./custom-test-env.js",
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
"bundle": "microbundle --tsconfig tsconfig.lib.json src/lib/index.tsx --format esm,cjs",
"cosmos": "open http://localhost:5000 && cross-env FAST_REFRESH=false cosmos"
"assets:generate": "yarn assets:svg:generate && yarn assets:font:generate",
"assets:svg:generate": "svgr -d src/lib/assets/svg --ext tsx --typescript src/lib/assets/svg && rm src/lib/assets/svg/index.tsx",
"assets:font:generate": "sass src/lib/assets/fonts/index.scss src/lib/assets/fonts/index.css --no-source-map -I node_modules",
"bundle": "microbundle --define process.env.REACT_APP_IS_WIDGET=true --tsconfig tsconfig.lib.json src/lib/index.tsx --format esm,cjs",
"bundle:depcheck": "node depcheck.js",
"cosmos": "cross-env FAST_REFRESH=false REACT_APP_IS_WIDGET=true cosmos"
},
"browserslist": {
"production": [
@ -168,5 +163,37 @@
]
},
"license": "GPL-3.0-or-later",
"dependencies": {}
"dependencies": {
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@lingui/core": "^3.9.0",
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@popperjs/core": "^2.4.4",
"@uniswap/redux-multicall": "^1.0.0",
"immer": "^9.0.6",
"jotai": "^1.3.7",
"lodash": "^4.17.21",
"make-plural": "^7.0.0",
"node-vibrant": "^3.2.1-alpha.1",
"polished": "^3.3.2",
"popper-max-size-modifier": "^0.2.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-feather": "^2.0.8",
"react-popper": "^2.2.3",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"rebass": "^4.0.7",
"redux": "^4.1.2",
"styled-components": "^5.3.0",
"tiny-invariant": "^1.2.0",
"wcag-contrast": "^3.0.0",
"wicg-inert": "^3.1.1",
"widgets-web3-react/core": "npm:@web3-react/core@8.0.15-alpha.0",
"widgets-web3-react/eip1193": "npm:@web3-react/eip1193@8.0.15-alpha.0",
"widgets-web3-react/metamask": "npm:@web3-react/metamask@8.0.15-alpha.0",
"widgets-web3-react/network": "npm:@web3-react/network@8.0.15-alpha.0",
"widgets-web3-react/types": "npm:@web3-react/types@8.0.15-alpha.0"
}
}

@ -4,6 +4,7 @@ import { useCallback, useContext } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
import styled, { ThemeContext } from 'styled-components/macro'
import { Connector } from 'widgets-web3-react/types'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, portis, walletlink } from '../../connectors'
@ -176,7 +177,7 @@ const IconWrapper = styled.div<{ size?: number }>`
`};
`
function WrappedStatusIcon({ connector }: { connector: AbstractConnector }) {
function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
return (
<IconWrapper size={16}>
<StatusIcon connector={connector} />

@ -1,4 +1,5 @@
import { AbstractConnector } from '@web3-react/abstract-connector'
import { Connector } from 'widgets-web3-react/types'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
@ -7,7 +8,7 @@ import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors'
import Identicon from '../Identicon'
export default function StatusIcon({ connector }: { connector: AbstractConnector }) {
export default function StatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
switch (connector) {
case injected:
return <Identicon />

@ -1,11 +1,10 @@
import { Options, Placement } from '@popperjs/core'
import Portal from '@reach/portal'
import useInterval from 'lib/hooks/useInterval'
import React, { useCallback, useMemo, useState } from 'react'
import { usePopper } from 'react-popper'
import styled from 'styled-components/macro'
import useInterval from '../../hooks/useInterval'
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};

@ -6,6 +6,7 @@ import { darken } from 'polished'
import { useMemo } from 'react'
import { Activity } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import { Connector } from 'widgets-web3-react/types'
import { NetworkContextName } from '../../constants/misc'
import useENSName from '../../hooks/useENSName'
@ -130,7 +131,7 @@ function Sock() {
)
}
function WrappedStatusIcon({ connector }: { connector: AbstractConnector }) {
function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
return (
<IconWrapper size={16}>
<StatusIcon connector={connector} />

@ -33,11 +33,12 @@ export const SUPPORTED_LOCALES = [
'zh-CN',
'zh-TW',
] as const
export type SupportedLocale = typeof SUPPORTED_LOCALES[number]
export type SupportedLocale = typeof SUPPORTED_LOCALES[number] | 'pseudo'
// eslint-disable-next-line import/first
import * as enUS from '../locales/en-US'
export const DEFAULT_LOCALE: SupportedLocale = 'en-US'
export { messages as DEFAULT_MESSAGES } from '../locales/en-US'
export const DEFAULT_CATALOG = enUS
export const LOCALE_LABEL: { [locale in SupportedLocale]: string } = {
'af-ZA': 'Afrikaans',
@ -72,4 +73,5 @@ export const LOCALE_LABEL: { [locale in SupportedLocale]: string } = {
'vi-VN': 'Tiếng Việt',
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
pseudo: 'ƥƨèúδô',
}

@ -0,0 +1,22 @@
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
import { NetworkContextName } from '../constants/misc'
export default function useActiveWeb3React() {
const widgetsContext = useWidgetsWeb3React()
const interfaceContext = useWeb3React<Web3Provider>()
const interfaceNetworkContext = useWeb3React<Web3Provider>(
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName
)
if (process.env.REACT_APP_IS_WIDGET) {
return widgetsContext
}
if (interfaceContext.active) {
return interfaceContext
}
return interfaceNetworkContext
}

@ -1,7 +1,6 @@
import useInterval from 'lib/hooks/useInterval'
import { useState } from 'react'
import useInterval from './useInterval'
const useMachineTimeMs = (updateInterval: number): number => {
const [now, setNow] = useState(Date.now())

@ -1,16 +1,12 @@
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import type { EthereumProvider } from 'lib/ethereum'
import { useEffect, useState } from 'react'
import { gnosisSafe, injected } from '../connectors'
import { IS_IN_IFRAME, NetworkContextName } from '../constants/misc'
import { IS_IN_IFRAME } from '../constants/misc'
import { isMobile } from '../utils/userAgent'
export function useActiveWeb3React() {
const context = useWeb3React<Web3Provider>()
const contextNetwork = useWeb3React<Web3Provider>(NetworkContextName)
return context.active ? context : contextNetwork
}
export { default as useActiveWeb3React } from './useActiveWeb3React'
export function useEagerConnect() {
const { activate, active } = useWeb3React()
@ -74,7 +70,7 @@ export function useInactiveListener(suppress = false) {
const { active, error, activate } = useWeb3React()
useEffect(() => {
const { ethereum } = window
const ethereum = window.ethereum as EthereumProvider | undefined
if (ethereum && ethereum.on && !active && !error && !suppress) {
const handleChainChanged = () => {

@ -1,110 +1,26 @@
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { DEFAULT_LOCALE, DEFAULT_MESSAGES, SupportedLocale } from 'constants/locales'
import { SupportedLocale } from 'constants/locales'
import { initialLocale, useActiveLocale } from 'hooks/useActiveLocale'
import {
af,
ar,
ca,
cs,
da,
de,
el,
en,
es,
fi,
fr,
he,
hu,
id,
it,
ja,
ko,
nl,
no,
pl,
PluralCategory,
pt,
ro,
ru,
sr,
sv,
sw,
tr,
uk,
vi,
zh,
} from 'make-plural/plurals'
import { useEffect } from 'react'
import { ReactNode } from 'react'
import { dynamicActivate, Provider } from 'lib/i18n'
import { ReactNode, useCallback } from 'react'
import { useUserLocaleManager } from 'state/user/hooks'
type LocalePlural = {
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
}
const plurals: LocalePlural = {
'af-ZA': af,
'ar-SA': ar,
'ca-ES': ca,
'cs-CZ': cs,
'da-DK': da,
'de-DE': de,
'el-GR': el,
'en-US': en,
'es-ES': es,
'fi-FI': fi,
'fr-FR': fr,
'he-IL': he,
'hu-HU': hu,
'id-ID': id,
'it-IT': it,
'ja-JP': ja,
'ko-KR': ko,
'nl-NL': nl,
'no-NO': no,
'pl-PL': pl,
'pt-BR': pt,
'pt-PT': pt,
'ro-RO': ro,
'ru-RU': ru,
'sr-SP': sr,
'sv-SE': sv,
'sw-TZ': sw,
'tr-TR': tr,
'uk-UA': uk,
'vi-VN': vi,
'zh-CN': zh,
'zh-TW': zh,
}
async function dynamicActivate(locale: SupportedLocale) {
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
const { messages } = locale === DEFAULT_LOCALE ? { messages: DEFAULT_MESSAGES } : await import(`locales/${locale}`)
i18n.load(locale, messages)
i18n.activate(locale)
}
dynamicActivate(initialLocale)
export function LanguageProvider({ children }: { children: ReactNode }) {
const locale = useActiveLocale()
const [, setUserLocale] = useUserLocaleManager()
useEffect(() => {
dynamicActivate(locale)
.then(() => {
document.documentElement.setAttribute('lang', locale)
setUserLocale(locale) // stores the selected locale to persist across sessions
})
.catch((error) => {
console.error('Failed to activate locale', locale, error)
})
}, [locale, setUserLocale])
const onActivate = useCallback(
(locale: SupportedLocale) => {
document.documentElement.setAttribute('lang', locale)
setUserLocale(locale) // stores the selected locale to persist across sessions
},
[setUserLocale]
)
return (
<I18nProvider forceRenderOnLocaleChange={false} i18n={i18n}>
<Provider locale={locale} forceRenderAfterLocaleChange={false} onActivate={onActivate}>
{children}
</I18nProvider>
</Provider>
)
}

22
src/lib/.eslintrc.json Normal file

@ -0,0 +1,22 @@
{
"extends": ["../../.eslintrc.json"],
"plugins": ["better-styled-components"],
"rules": {
"better-styled-components/sort-declarations-alphabetically": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "styled-components/macro",
"message": "Please import styled from lib/theme to get the correct typings."
},
{
"name": "react-feather",
"message": "Please import from lib/icons to ensure performant usage."
}
]
}
]
}
}

@ -0,0 +1,22 @@
// Use Inter mixin to set font-display: block.
@use "@fontsource/inter/scss/mixins" as Inter;
@include Inter.fontFace(
$fontName: 'Inter',
$weight: 400,
$display: block,
);
@include Inter.fontFace(
$fontName: 'Inter',
$weight: 500,
$display: block,
);
@include Inter.fontFace(
$fontName: 'Inter',
$weight: 600,
$display: block,
);
@include Inter.fontFaceVariable(
$display: block,
);
@import "~@fontsource/ibm-plex-mono/400.css";

@ -0,0 +1,4 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" />
<path d="M14 7L8.5 12.5L6 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg">
<polyline class="left" points="18 15 12 9"></polyline>
<polyline class="right" points="12 9 6 15"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 238 B

@ -0,0 +1,13 @@
<svg viewBox="0 0 14 15" fill="black" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M4.15217 1.55141C3.96412 1.52242 3.95619 1.51902 4.04468 1.5055C4.21427 1.47958 4.61472 1.51491 4.89067 1.58012C5.53489 1.73232 6.12109 2.12221 6.74683 2.81466L6.91307 2.99862L7.15088 2.96062C8.15274 2.8006 9.17194 2.92778 10.0244 3.31918C10.2589 3.42686 10.6287 3.64121 10.6749 3.69629C10.6896 3.71384 10.7166 3.82684 10.7349 3.94742C10.7982 4.36458 10.7665 4.68434 10.6382 4.92317C10.5683 5.05313 10.5644 5.09432 10.6114 5.20554C10.6489 5.2943 10.7534 5.35999 10.8569 5.35985C11.0687 5.35956 11.2968 5.0192 11.4024 4.54561L11.4444 4.3575L11.5275 4.45109C11.9835 4.96459 12.3417 5.66488 12.4032 6.16335L12.4192 6.29332L12.3426 6.17517C12.2107 5.97186 12.0781 5.83346 11.9084 5.72183C11.6024 5.52062 11.2789 5.45215 10.4222 5.40727C9.64839 5.36675 9.21045 5.30106 8.77621 5.16032C8.03738 4.9209 7.66493 4.60204 6.78729 3.4576C6.39748 2.94928 6.15654 2.66804 5.91687 2.44155C5.37228 1.92691 4.83716 1.65701 4.15217 1.55141Z"/>
<path d="M10.8494 2.68637C10.8689 2.34575 10.9153 2.12108 11.0088 1.9159C11.0458 1.83469 11.0804 1.76822 11.0858 1.76822C11.0911 1.76822 11.075 1.82816 11.05 1.90142C10.9821 2.10054 10.9709 2.3729 11.0177 2.68978C11.0771 3.09184 11.1109 3.14985 11.5385 3.58416C11.739 3.78788 11.9723 4.0448 12.0568 4.15511L12.2106 4.35568L12.0568 4.21234C11.8688 4.03705 11.4364 3.6952 11.3409 3.64633C11.2768 3.61356 11.2673 3.61413 11.2278 3.65321C11.1914 3.68922 11.1837 3.74333 11.1787 3.99915C11.1708 4.39786 11.1161 4.65377 10.9842 4.90965C10.9128 5.04805 10.9015 5.01851 10.9661 4.8623C11.0143 4.74566 11.0192 4.69439 11.0189 4.30842C11.0181 3.53291 10.9255 3.34647 10.3823 3.02709C10.2447 2.94618 10.0179 2.8295 9.87839 2.76778C9.73887 2.70606 9.62805 2.6523 9.63208 2.64828C9.64746 2.63307 10.1772 2.78675 10.3905 2.86828C10.7077 2.98954 10.76 3.00526 10.7985 2.99063C10.8244 2.98082 10.8369 2.90608 10.8494 2.68637Z"/>
<path d="M4.51745 4.01304C4.13569 3.49066 3.89948 2.68973 3.95062 2.091L3.96643 1.90572L4.05333 1.92148C4.21652 1.95106 4.49789 2.05515 4.62964 2.13469C4.9912 2.35293 5.14773 2.64027 5.30697 3.37811C5.35362 3.59423 5.41482 3.8388 5.44298 3.9216C5.48831 4.05487 5.65962 4.36617 5.7989 4.56834C5.89922 4.71395 5.83258 4.78295 5.61082 4.76305C5.27215 4.73267 4.8134 4.41799 4.51745 4.01304Z"/>
<path d="M10.3863 7.90088C8.60224 7.18693 7.97389 6.56721 7.97389 5.52157C7.97389 5.36769 7.97922 5.24179 7.98571 5.24179C7.99221 5.24179 8.06124 5.29257 8.1391 5.35465C8.50088 5.64305 8.906 5.76623 10.0275 5.92885C10.6875 6.02455 11.0589 6.10185 11.4015 6.21477C12.4904 6.57371 13.1641 7.30212 13.3248 8.29426C13.3715 8.58255 13.3441 9.12317 13.2684 9.4081C13.2087 9.63315 13.0263 10.0388 12.9779 10.0544C12.9645 10.0587 12.9514 10.0076 12.9479 9.93809C12.9296 9.56554 12.7402 9.20285 12.4221 8.93116C12.0604 8.62227 11.5745 8.37633 10.3863 7.90088Z"/>
<path d="M9.13385 8.19748C9.11149 8.06527 9.07272 7.89643 9.04769 7.82228L9.00217 7.68748L9.08672 7.7818C9.20374 7.91233 9.2962 8.07937 9.37457 8.30185C9.43438 8.47165 9.44111 8.52215 9.44066 8.79807C9.4402 9.06896 9.43273 9.12575 9.3775 9.27858C9.29042 9.51959 9.18233 9.69048 9.00097 9.87391C8.67507 10.2036 8.25607 10.3861 7.65143 10.4618C7.54633 10.4749 7.24 10.4971 6.97069 10.511C6.292 10.5461 5.84531 10.6186 5.44393 10.7587C5.38623 10.7788 5.3347 10.7911 5.32947 10.7859C5.31323 10.7698 5.58651 10.6079 5.81223 10.4998C6.1305 10.3474 6.44733 10.2643 7.15719 10.1468C7.50785 10.0887 7.86998 10.0183 7.96194 9.99029C8.83033 9.72566 9.27671 9.04276 9.13385 8.19748Z"/>
<path d="M9.95169 9.64109C9.71465 9.13463 9.66022 8.64564 9.79009 8.18961C9.80399 8.14088 9.82632 8.101 9.83976 8.101C9.85319 8.101 9.90913 8.13105 9.96404 8.16777C10.0733 8.24086 10.2924 8.36395 10.876 8.68023C11.6043 9.0749 12.0196 9.3805 12.302 9.72965C12.5493 10.0354 12.7023 10.3837 12.776 10.8084C12.8177 11.0489 12.7932 11.6277 12.7311 11.8699C12.5353 12.6337 12.0802 13.2336 11.4311 13.5837C11.336 13.635 11.2506 13.6771 11.2414 13.6773C11.2321 13.6775 11.2668 13.5899 11.3184 13.4827C11.5367 13.029 11.5616 12.5877 11.3965 12.0965C11.2954 11.7957 11.0893 11.4287 10.6732 10.8084C10.1893 10.0873 10.0707 9.89539 9.95169 9.64109Z"/>
<path d="M3.25046 12.3737C3.91252 11.8181 4.73629 11.4234 5.48666 11.3022C5.81005 11.25 6.34877 11.2707 6.64823 11.3469C7.12824 11.469 7.55763 11.7425 7.78094 12.0683C7.99918 12.3867 8.09281 12.6642 8.19029 13.2816C8.22875 13.5252 8.27057 13.7697 8.28323 13.8251C8.35644 14.1451 8.4989 14.4008 8.67544 14.5293C8.95583 14.7333 9.43865 14.7459 9.91362 14.5618C9.99423 14.5305 10.0642 14.5089 10.0691 14.5138C10.0864 14.5308 9.84719 14.6899 9.67847 14.7737C9.45143 14.8864 9.2709 14.93 9.03102 14.93C8.59601 14.93 8.23486 14.7101 7.9335 14.2616C7.87419 14.1733 7.7409 13.909 7.63729 13.6741C7.3191 12.9528 7.16199 12.7331 6.79255 12.4926C6.47104 12.2834 6.05641 12.2459 5.74449 12.3979C5.33475 12.5976 5.22043 13.118 5.51389 13.4478C5.63053 13.5789 5.84803 13.6919 6.02588 13.7139C6.35861 13.7551 6.64455 13.5035 6.64455 13.1696C6.64455 12.9528 6.56071 12.8291 6.34966 12.7344C6.0614 12.6051 5.75156 12.7562 5.75304 13.0254C5.75368 13.1402 5.80396 13.2122 5.91971 13.2643C5.99397 13.2977 5.99569 13.3003 5.93514 13.2878C5.67066 13.2333 5.6087 12.9164 5.82135 12.706C6.07667 12.4535 6.60461 12.5649 6.78591 12.9097C6.86208 13.0545 6.87092 13.3429 6.80451 13.517C6.6559 13.9068 6.22256 14.1117 5.78297 14.0002C5.48368 13.9242 5.36181 13.842 5.00097 13.4726C4.37395 12.8306 4.13053 12.7062 3.22657 12.566L3.05335 12.5391L3.25046 12.3737Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.308383 0.883984C2.40235 3.40996 3.84457 4.45213 4.00484 4.67231C4.13717 4.85412 4.08737 5.01757 3.86067 5.14567C3.7346 5.21689 3.47541 5.28905 3.34564 5.28905C3.19887 5.28905 3.14847 5.23278 3.14847 5.23278C3.06337 5.15255 3.01544 5.16658 2.5784 4.39555C1.97166 3.45981 1.46389 2.68357 1.45004 2.67057C1.41801 2.64052 1.41856 2.64153 2.51654 4.59413C2.69394 5.0011 2.55182 5.15049 2.55182 5.20845C2.55182 5.32636 2.51946 5.38834 2.37311 5.55059C2.12914 5.8211 2.02008 6.12505 1.94135 6.7541C1.8531 7.45926 1.60492 7.95737 0.917156 8.80989C0.514562 9.30893 0.448686 9.4004 0.3471 9.60153C0.219144 9.85482 0.183961 9.99669 0.169701 10.3165C0.154629 10.6547 0.183983 10.8732 0.287934 11.1965C0.378939 11.4796 0.473932 11.6665 0.716778 12.0403C0.926351 12.3629 1.04702 12.6027 1.04702 12.6965C1.04702 12.7711 1.06136 12.7712 1.38611 12.6983C2.16328 12.5239 2.79434 12.2171 3.14925 11.8411C3.36891 11.6084 3.42048 11.4799 3.42215 11.1611C3.42325 10.9525 3.41587 10.9088 3.35914 10.7888C3.2668 10.5935 3.09869 10.4311 2.72817 10.1794C2.2427 9.84953 2.03534 9.58398 1.97807 9.21878C1.93108 8.91913 1.98559 8.70771 2.25416 8.14825C2.53214 7.56916 2.60103 7.32239 2.64763 6.73869C2.67773 6.36158 2.71941 6.21286 2.82842 6.09348C2.94212 5.969 3.04447 5.92684 3.32584 5.88863C3.78457 5.82635 4.07667 5.70839 4.31677 5.48849C4.52505 5.29772 4.61221 5.11391 4.62558 4.8372L4.63574 4.62747L4.51934 4.49259C4.09783 4.00411 0.0261003 0.5 0.000160437 0.5C-0.00538105 0.5 0.133325 0.672804 0.308383 0.883984ZM1.28364 10.6992C1.37894 10.5314 1.3283 10.3158 1.16889 10.2104C1.01827 10.1109 0.78428 10.1578 0.78428 10.2875C0.78428 10.3271 0.806303 10.3559 0.855937 10.3813C0.939514 10.424 0.945581 10.4721 0.879823 10.5703C0.81323 10.6698 0.818604 10.7573 0.894991 10.8167C1.0181 10.9125 1.19237 10.8598 1.28364 10.6992Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.92523 5.99865C4.70988 6.06439 4.50054 6.29124 4.43574 6.5291C4.39621 6.67421 4.41864 6.92875 4.47785 7.00736C4.57351 7.13433 4.66602 7.16778 4.91651 7.16603C5.40693 7.16263 5.83327 6.95358 5.88284 6.69224C5.92347 6.47801 5.73622 6.18112 5.4783 6.05078C5.34521 5.98355 5.06217 5.95688 4.92523 5.99865ZM5.49853 6.44422C5.57416 6.33741 5.54107 6.22198 5.41245 6.14391C5.1675 5.99525 4.79708 6.11827 4.79708 6.34826C4.79708 6.46274 4.99025 6.58765 5.16731 6.58765C5.28516 6.58765 5.44644 6.5178 5.49853 6.44422Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

@ -0,0 +1,18 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask">
<rect width="24" height="24" fill="white" stroke-width="0" />
<rect width="12" height="12" fill="black" stroke-width="0" />
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
</mask>
<circle
id="circle"
cx="12"
cy="12"
r="10"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
mask="url(#mask)"
/>
</svg>

After

Width:  |  Height:  |  Size: 608 B

@ -0,0 +1,85 @@
import { AlertTriangle, LargeIcon } from 'lib/icons'
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
import { ReactNode } from 'react'
import Button from './Button'
import Row from './Row'
const StyledButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-grow: 1;
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
`
const UpdateRow = styled(Row)``
const grow = keyframes`
from {
opacity: 0;
width: 0;
}
to {
opacity: 1;
width: max-content;
}
`
const updatedCss = css`
border: 1px solid ${({ theme }) => theme.outline};
padding: calc(0.25em - 1px);
padding-left: calc(0.75em - 1px);
${UpdateRow} {
animation: ${grow} 0.25s ease-in;
white-space: nowrap;
}
${StyledButton} {
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
flex-grow: 0;
padding: 1em;
}
`
export const Overlay = styled(Row)<{ updated?: boolean }>`
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-direction: row-reverse;
min-height: 3.5em;
transition: padding 0.25s ease-out;
${({ updated }) => updated && updatedCss}
`
export interface ActionButtonProps {
color?: Color
disabled?: boolean
updated?: { message: ReactNode; action: ReactNode }
onClick: () => void
onUpdate?: () => void
children: ReactNode
}
export default function ActionButton({
color = 'accent',
disabled,
updated,
onClick,
onUpdate,
children,
}: ActionButtonProps) {
return (
<Overlay updated={Boolean(updated)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={updated ? onUpdate : onClick}>
<ThemedText.TransitionButton buttonSize={updated ? 'medium' : 'large'} color="currentColor">
{updated ? updated.action : children}
</ThemedText.TransitionButton>
</StyledButton>
{updated && (
<UpdateRow gap={0.5}>
<LargeIcon icon={AlertTriangle} />
<ThemedText.Subhead2>{updated?.message}</ThemedText.Subhead2>
</UpdateRow>
)}
</Overlay>
)
}

@ -0,0 +1,64 @@
import { Icon } from 'lib/icons'
import styled, { Color } from 'lib/theme'
import { ComponentProps } from 'react'
export const BaseButton = styled.button`
background-color: transparent;
border: none;
border-radius: 0.5em;
color: currentColor;
cursor: pointer;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
margin: 0;
padding: 0;
:disabled {
cursor: initial;
filter: saturate(0) opacity(0.4);
}
`
export default styled(BaseButton)<{ color?: Color }>`
color: ${({ color = 'interactive', theme }) => color === 'interactive' && theme.onInteractive};
:enabled {
background-color: ${({ color = 'interactive', theme }) => theme[color]};
}
:enabled:hover {
background-color: ${({ color = 'interactive', theme }) => theme.onHover(theme[color])};
}
:disabled {
border: 1px solid ${({ theme }) => theme.outline};
color: ${({ theme }) => theme.secondary};
cursor: initial;
}
`
const transparentButton = (defaultColor: Color) => styled(BaseButton)<{ color?: Color }>`
color: ${({ color = defaultColor, theme }) => theme[color]};
:enabled:hover {
color: ${({ color = defaultColor, theme }) => theme.onHover(theme[color])};
}
`
export const TextButton = transparentButton('accent')
const SecondaryButton = transparentButton('secondary')
interface IconButtonProps {
icon: Icon
iconProps?: ComponentProps<Icon>
}
export function IconButton({ icon: Icon, iconProps, ...props }: IconButtonProps & ComponentProps<typeof BaseButton>) {
return (
<SecondaryButton {...props}>
<Icon {...iconProps} />
</SecondaryButton>
)
}

@ -0,0 +1,29 @@
import styled, { Color, css, Theme } from 'lib/theme'
const Column = styled.div<{
align?: string
color?: Color
justify?: string
gap?: number
padded?: true
flex?: true
grow?: true
theme: Theme
css?: ReturnType<typeof css>
}>`
align-items: ${({ align }) => align ?? 'center'};
background-color: inherit;
color: ${({ color, theme }) => color && theme[color]};
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
flex-direction: column;
flex-grow: ${({ grow }) => grow && 1};
gap: ${({ gap }) => gap && `${gap}em`};
grid-auto-flow: row;
grid-template-columns: 1fr;
justify-content: ${({ justify }) => justify ?? 'space-between'};
padding: ${({ padded }) => padded && '0.75em'};
${({ css }) => css}
`
export default Column

@ -0,0 +1,116 @@
import 'wicg-inert'
import useUnmount from 'lib/hooks/useUnmount'
import { X } from 'lib/icons'
import styled, { Color, Layer, ThemeProvider } from 'lib/theme'
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { IconButton } from './Button'
import Column from './Column'
import { default as BaseHeader } from './Header'
import Rule from './Rule'
// Include inert from wicg-inert
declare global {
interface HTMLElement {
inert?: boolean
}
}
const Context = createContext({
element: null as HTMLElement | null,
active: false,
setActive: (active: boolean) => undefined as void,
})
interface ProviderProps {
value: HTMLElement | null
children: ReactNode
}
export function Provider({ value, children }: ProviderProps) {
// If a Dialog is active, mark the main content inert
const ref = useRef<HTMLDivElement>(null)
const [active, setActive] = useState(false)
const context = { element: value, active, setActive }
useEffect(() => {
if (ref.current) {
ref.current.inert = active
}
}, [active])
return (
<div ref={ref}>
<Context.Provider value={context}>{children}</Context.Provider>
</div>
)
}
const OnCloseContext = createContext<() => void>(() => void 0)
interface HeaderProps {
title?: ReactElement
ruled?: boolean
children?: ReactNode
}
export function Header({ title, children, ruled }: HeaderProps) {
return (
<>
<Column>
<BaseHeader title={title}>
{children}
<IconButton color="primary" onClick={useContext(OnCloseContext)} icon={X} />
</BaseHeader>
{ruled && <Rule padded />}
</Column>
</>
)
}
export const Modal = styled.div<{ color: Color }>`
background-color: ${({ color, theme }) => theme[color]};
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
display: flex;
flex-direction: column;
height: calc(100% - 0.5em);
left: 0;
margin: 0.25em;
overflow: hidden;
position: absolute;
top: 0;
width: calc(100% - 0.5em);
z-index: ${Layer.DIALOG};
`
interface DialogProps {
color: Color
children: ReactNode
onClose?: () => void
}
export default function Dialog({ color, children, onClose = () => void 0 }: DialogProps) {
const context = useContext(Context)
useEffect(() => {
context.setActive(true)
return () => context.setActive(false)
}, [context])
const dialog = useRef<HTMLDivElement>(null)
useUnmount(dialog)
useEffect(() => {
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
document.addEventListener('keydown', close, true)
return () => document.removeEventListener('keydown', close, true)
}, [onClose])
return (
context.element &&
createPortal(
<ThemeProvider>
<Modal className="dialog" color={color} ref={dialog}>
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
</Modal>
</ThemeProvider>,
context.element
)
)
}

@ -0,0 +1,46 @@
import { Trans } from '@lingui/macro'
import React, { ErrorInfo } from 'react'
import Dialog from '../Dialog'
import ErrorDialog from './ErrorDialog'
export type ErrorHandler = (error: Error, info: ErrorInfo) => void
interface ErrorBoundaryProps {
onError?: ErrorHandler
}
type ErrorBoundaryState = {
error: Error | null
}
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { error: null }
}
static getDerivedStateFromError(error: Error) {
return { error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError?.(error, errorInfo)
}
render() {
if (this.state.error) {
return (
<Dialog color="dialog">
<ErrorDialog
error={this.state.error}
header={<Trans>Reload the page to try again</Trans>}
action={<Trans>Reload the page</Trans>}
onAction={() => window.location.reload()}
/>
</Dialog>
)
}
return this.props.children
}
}

@ -0,0 +1,127 @@
import { Trans } from '@lingui/macro'
import useScrollbar from 'lib/hooks/useScrollbar'
import { AlertTriangle, Expando, Icon, Info, LargeIcon } from 'lib/icons'
import styled, { Color, ThemedText } from 'lib/theme'
import { ReactNode, useState } from 'react'
import ActionButton from '../ActionButton'
import { IconButton } from '../Button'
import Column from '../Column'
import Row from '../Row'
import Rule from '../Rule'
const HeaderIcon = styled(LargeIcon)`
flex-grow: 1;
svg {
transition: height 0.25s, width 0.25s;
}
`
interface StatusHeaderProps {
icon: Icon
iconColor?: Color
iconSize?: number
children: ReactNode
}
export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }: StatusHeaderProps) {
return (
<>
<Column flex style={{ flexGrow: 1 }}>
<HeaderIcon icon={Icon} color={iconColor} size={iconSize} />
<Column gap={0.75} flex style={{ textAlign: 'center' }}>
{children}
</Column>
</Column>
<Rule />
</>
)
}
const ErrorHeader = styled(Column)<{ open: boolean }>`
transition: gap 0.25s;
div:last-child {
max-height: ${({ open }) => (open ? 0 : 60 / 14)}em; // 3 * line-height
overflow-y: hidden;
transition: max-height 0.25s;
}
`
const ErrorColumn = styled(Column)``
const ExpandoColumn = styled(Column)<{ open: boolean }>`
flex-grow: ${({ open }) => (open ? 2 : 0)};
transition: flex-grow 0.25s, gap 0.25s;
${Rule} {
margin-bottom: ${({ open }) => (open ? 0 : 0.75)}em;
transition: margin-bottom 0.25s;
}
${ErrorColumn} {
flex-basis: 0;
flex-grow: ${({ open }) => (open ? 1 : 0)};
overflow-y: hidden;
position: relative;
transition: flex-grow 0.25s;
${Column} {
height: 100%;
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
}
}
`
interface ErrorDialogProps {
header?: ReactNode
error: Error
action: ReactNode
onAction: () => void
}
export default function ErrorDialog({ header, error, action, onAction }: ErrorDialogProps) {
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={AlertTriangle} iconColor="error" iconSize={open ? 3 : 4}>
<ErrorHeader gap={open ? 0 : 0.75} open={open}>
<ThemedText.Subhead1>
<Trans>Something went wrong.</Trans>
</ThemedText.Subhead1>
<ThemedText.Body2>{header}</ThemedText.Body2>
</ErrorHeader>
</StatusHeader>
<Row>
<Row gap={0.5}>
<Info color="secondary" />
<ThemedText.Subhead2 color="secondary">
<Trans>Error details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch" open={open}>
<Rule />
<ErrorColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<ThemedText.Code>{error.message}</ThemedText.Code>
</Column>
</ErrorColumn>
<ActionButton onClick={onAction}>{action}</ActionButton>
</ExpandoColumn>
</Column>
)
}

@ -0,0 +1,51 @@
import { largeIconCss, Logo } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ReactElement, ReactNode } from 'react'
import Row from './Row'
const UniswapA = styled.a`
cursor: pointer;
${Logo} {
fill: ${({ theme }) => theme.secondary};
height: 1.5em;
transition: transform 0.25s ease;
width: 1.5em;
will-change: transform;
:hover {
fill: ${({ theme }) => theme.onHover(theme.secondary)};
transform: rotate(-5deg);
}
}
`
const HeaderRow = styled(Row)`
height: 1.75em;
margin: 0 0.75em 0.75em;
padding-top: 0.5em;
${largeIconCss}
`
export interface HeaderProps {
title?: ReactElement
logo?: boolean
children: ReactNode
}
export default function Header({ title, logo, children }: HeaderProps) {
return (
<HeaderRow iconSize={1.2}>
<Row gap={0.5}>
{logo && (
<UniswapA href={`https://app.uniswap.org/`}>
<Logo />
</UniswapA>
)}
{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}
</Row>
<Row gap={1}>{children}</Row>
</HeaderRow>
)
}

@ -0,0 +1,166 @@
import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
const Input = styled.input`
-webkit-appearance: textfield;
background-color: transparent;
border: none;
color: currentColor;
font-family: inherit;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
margin: 0;
outline: none;
overflow: hidden;
padding: 0;
text-align: left;
text-overflow: ellipsis;
width: 100%;
::-webkit-search-decoration {
-webkit-appearance: none;
}
[type='number'] {
-moz-appearance: textfield;
}
::-webkit-outer-spin-button,
::-webkit-inner-spin-button {
-webkit-appearance: none;
}
::placeholder {
color: ${({ theme }) => theme.secondary};
}
`
export default Input
interface StringInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
value: string
onChange: (input: string) => void
}
export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(function StringInput(
{ value, onChange, ...props }: StringInputProps,
ref
) {
return (
<Input
value={value}
onChange={(e) => onChange(e.target.value)}
// universal input options
inputMode="text"
autoComplete="off"
autoCorrect="off"
// text-specific options
type="text"
placeholder={props.placeholder || '-'}
minLength={1}
spellCheck="false"
ref={ref as any}
{...props}
/>
)
})
interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
value: number | undefined
onChange: (input: number | undefined) => void
}
interface EnforcedNumericInputProps extends NumericInputProps {
// Validates nextUserInput; returns stringified value or undefined if valid, or null if invalid
enforcer: (nextUserInput: string) => string | undefined | null
}
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
ref
) {
// Allow value/onChange to use number by preventing a trailing decimal separator from triggering onChange
const [state, setState] = useState(value ?? '')
useEffect(() => {
if (+state !== value) {
setState(value ?? '')
}
}, [value, state, setState])
const validateChange = useCallback(
(event) => {
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
if (nextInput !== null) {
setState(nextInput ?? '')
if (nextInput === undefined || +nextInput !== value) {
onChange(nextInput === undefined ? undefined : +nextInput)
}
}
},
[value, onChange, enforcer]
)
return (
<Input
value={state}
onChange={validateChange}
// universal input options
inputMode="decimal"
autoComplete="off"
autoCorrect="off"
// text-specific options
type="text"
pattern={pattern}
placeholder={props.placeholder || '0'}
minLength={1}
spellCheck="false"
ref={ref as any}
{...props}
/>
)
})
const integerRegexp = /^\d*$/
const integerEnforcer = (nextUserInput: string) => {
if (nextUserInput === '' || integerRegexp.test(nextUserInput)) {
const nextInput = parseInt(nextUserInput)
return isNaN(nextInput) ? undefined : nextInput.toString()
}
return null
}
export const IntegerInput = forwardRef(function IntegerInput(props: NumericInputProps, ref) {
return <NumericInput pattern="^[0-9]*$" enforcer={integerEnforcer} ref={ref as any} {...props} />
})
const decimalRegexp = /^\d*(?:[.])?\d*$/
const decimalEnforcer = (nextUserInput: string) => {
if (nextUserInput === '') {
return undefined
} else if (nextUserInput === '.') {
return '0.'
} else if (decimalRegexp.test(nextUserInput)) {
return nextUserInput
}
return null
}
export const DecimalInput = forwardRef(function DecimalInput(props: NumericInputProps, ref) {
return <NumericInput pattern="^[0-9]*[.,]?[0-9]*$" enforcer={decimalEnforcer} ref={ref as any} {...props} />
})
export const inputCss = css`
background-color: ${({ theme }) => theme.container};
border: 1px solid ${({ theme }) => theme.container};
border-radius: ${({ theme }) => theme.borderRadius}em;
cursor: text;
padding: calc(0.75em - 1px);
:hover:not(:focus-within) {
background-color: ${({ theme }) => theme.onHover(theme.container)};
border-color: ${({ theme }) => theme.onHover(theme.container)};
}
:focus-within {
border-color: ${({ theme }) => theme.active};
}
`

@ -0,0 +1,143 @@
import { Options, Placement } from '@popperjs/core'
import styled, { Layer } from 'lib/theme'
import maxSize from 'popper-max-size-modifier'
import React, { createContext, useContext, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { usePopper } from 'react-popper'
const BoundaryContext = createContext<HTMLDivElement | null>(null)
export const BoundaryProvider = BoundaryContext.Provider
const PopoverContainer = styled.div<{ show: boolean }>`
background-color: ${({ theme }) => theme.dialog};
border-radius: 0.5em;
opacity: ${(props) => (props.show ? 1 : 0)};
padding: 8px;
transition: visibility 0.25s linear, opacity 0.25s linear;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
z-index: ${Layer.TOOLTIP};
`
const Reference = styled.div`
display: inline-block;
`
const Arrow = styled.div`
height: 8px;
width: 8px;
z-index: ${Layer.TOOLTIP};
::before {
background: ${({ theme }) => theme.dialog};
border: 1px solid ${({ theme }) => theme.outline};
content: '';
height: 8px;
position: absolute;
transform: rotate(45deg);
width: 8px;
}
&.arrow-top {
bottom: -5px;
::before {
border-left: none;
border-top: none;
}
}
&.arrow-bottom {
top: -5px;
::before {
border-bottom: none;
border-right: none;
}
}
&.arrow-left {
right: -5px;
::before {
border-bottom: none;
border-left: none;
}
}
&.arrow-right {
left: -5px;
::before {
border-right: none;
border-top: none;
}
}
`
export interface PopoverProps {
content: React.ReactNode
show: boolean
children: React.ReactNode
placement: Placement
contained?: true
}
export default function Popover({ content, show, children, placement, contained }: PopoverProps) {
const boundary = useContext(BoundaryContext)
const reference = useRef<HTMLDivElement>(null)
// Use callback refs to be notified when instantiated
const [popover, setPopover] = useState<HTMLDivElement | null>(null)
const [arrow, setArrow] = useState<HTMLDivElement | null>(null)
const options = useMemo((): Options => {
const modifiers: Options['modifiers'] = [
{ name: 'offset', options: { offset: [5, 5] } },
{ name: 'arrow', options: { element: arrow, padding: 6 } },
]
if (contained) {
modifiers.push(
{ name: 'preventOverflow', options: { boundary, padding: 8 } },
{ name: 'flip', options: { boundary, padding: 8 } },
{ ...maxSize, options: { boundary, padding: 8 } },
{
name: 'applyMaxSize',
enabled: true,
phase: 'beforeWrite',
requires: ['maxSize'],
fn({ state }) {
const { width } = state.modifiersData.maxSize
state.styles.popper = {
...state.styles.popper,
maxWidth: `${width}px`,
}
},
}
)
}
return {
placement,
strategy: 'absolute',
modifiers,
}
}, [arrow, boundary, placement, contained])
const { styles, attributes } = usePopper(reference.current, popover, options)
return (
<>
<Reference ref={reference}>{children}</Reference>
{boundary &&
createPortal(
<PopoverContainer show={show} ref={setPopover} style={styles.popper} {...attributes.popper}>
{content}
<Arrow
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
ref={setArrow}
style={styles.arrow}
{...attributes.arrow}
/>
</PopoverContainer>,
boundary
)}
</>
)
}

@ -0,0 +1,113 @@
import { Trans } from '@lingui/macro'
import { AlertTriangle, ArrowRight, CheckCircle, Spinner, Trash2 } from 'lib/icons'
import { DAI, ETH, UNI, USDC } from 'lib/mocks'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useMemo, useState } from 'react'
import Button from './Button'
import Column from './Column'
import { Header } from './Dialog'
import Row from './Row'
import TokenImg from './TokenImg'
interface ITokenAmount {
value: number
token: Token
}
export enum TransactionStatus {
SUCCESS = 0,
ERROR,
PENDING,
}
interface ITransaction {
input: ITokenAmount
output: ITokenAmount
status: TransactionStatus
}
// TODO: integrate with web3-react context
export const mockTxs: ITransaction[] = [
{
input: { value: 4170.15, token: USDC },
output: { value: 4167.44, token: DAI },
status: TransactionStatus.SUCCESS,
},
{
input: { value: 1.23, token: ETH },
output: { value: 4125.02, token: DAI },
status: TransactionStatus.PENDING,
},
{
input: { value: 10, token: UNI },
output: { value: 2125.02, token: ETH },
status: TransactionStatus.ERROR,
},
]
const TransactionRow = styled(Row)`
padding: 0.5em 1em;
:first-of-type {
padding-top: 1em;
}
`
function TokenAmount({ value: { value, token } }: { value: ITokenAmount }) {
return (
<Row gap={0.375}>
<TokenImg token={token} />
<ThemedText.Body2>
{value.toLocaleString('en')} {token.symbol}
</ThemedText.Body2>
</Row>
)
}
function Transaction({ tx }: { tx: ITransaction }) {
const statusIcon = useMemo(() => {
switch (tx.status) {
case TransactionStatus.SUCCESS:
return <CheckCircle color="success" />
case TransactionStatus.ERROR:
return <AlertTriangle color="error" />
case TransactionStatus.PENDING:
return <Spinner />
}
}, [tx.status])
return (
<TransactionRow grow>
<Row gap={0.75}>
<Row flex gap={0.5}>
<TokenAmount value={tx.input} />
<Row flex justify="flex-end" gap={0.5} grow>
<ArrowRight />
<TokenAmount value={tx.output} />
</Row>
</Row>
{statusIcon}
</Row>
</TransactionRow>
)
}
export default function RecentTransactionsDialog() {
const [txs, setTxs] = useState(mockTxs)
return (
<>
<Header title={<Trans>Recent transactions</Trans>} ruled>
<Button>
<Trash2 onClick={() => setTxs([])} />
</Button>
</Header>
<Column>
{txs.map((tx, key) => (
<Transaction tx={tx} key={key} />
))}
</Column>
</>
)
}

@ -0,0 +1,27 @@
import styled, { Color, Theme } from 'lib/theme'
import { Children, ReactNode } from 'react'
const Row = styled.div<{
color?: Color
align?: string
justify?: string
pad?: number
gap?: number
flex?: true
grow?: true
children?: ReactNode
theme: Theme
}>`
align-items: ${({ align }) => align ?? 'center'};
color: ${({ color, theme }) => color && theme[color]};
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
flex-flow: wrap;
flex-grow: ${({ grow }) => grow && 1};
gap: ${({ gap }) => gap && `${gap}em`};
grid-auto-flow: column;
grid-template-columns: ${({ grow, children }) => (grow ? `repeat(${Children.count(children)}, 1fr)` : '')};
justify-content: ${({ justify }) => justify ?? 'space-between'};
padding: ${({ pad }) => pad && `0 ${pad}em`};
`
export default Row

@ -0,0 +1,11 @@
import styled from 'lib/theme'
const Rule = styled.hr<{ padded?: true; scrollingEdge?: 'top' | 'bottom' }>`
border: none;
border-bottom: 1px solid ${({ theme }) => theme.outline};
margin: 0 ${({ padded }) => (padded ? '0.75em' : 0)};
margin-bottom: ${({ scrollingEdge }) => (scrollingEdge === 'bottom' ? -1 : 0)}px;
margin-top: ${({ scrollingEdge }) => (scrollingEdge !== 'bottom' ? -1 : 0)}px;
`
export default Rule

@ -0,0 +1,64 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode } from 'react'
import Column from '../Column'
import Row from '../Row'
import TokenImg from '../TokenImg'
import { inputAtom, useUpdateInputToken, useUpdateInputValue } from './state'
import TokenInput from './TokenInput'
const mockBalance = 123.45
const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em;
position: relative;
${TokenImg} {
filter: ${({ approved }) => (approved ? undefined : 'saturate(0) opacity(0.4)')};
transition: filter 0.25s;
}
`
interface InputProps {
disabled?: boolean
children: ReactNode
}
export default function Input({ disabled, children }: InputProps) {
const input = useAtomValue(inputAtom)
const setValue = useUpdateInputValue(inputAtom)
const setToken = useUpdateInputToken(inputAtom)
const balance = mockBalance
return (
<InputColumn gap={0.5} approved={input.approved !== false}>
<Row>
<ThemedText.Subhead2 color="secondary">
<Trans>Trading</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput
input={input}
disabled={disabled}
onMax={balance ? () => setValue(balance) : undefined}
onChangeInput={setValue}
onChangeToken={setToken}
>
<ThemedText.Body2 color="secondary">
<Row>
{input.usdc ? `~ $${input.usdc.toLocaleString('en')}` : '-'}
{balance && (
<ThemedText.Body2 color={input.value && input.value > balance ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{balance}</span>
</ThemedText.Body2>
)}
</Row>
</ThemedText.Body2>
</TokenInput>
<Row />
{children}
</InputColumn>
)
}

@ -0,0 +1,87 @@
import { Trans } from '@lingui/macro'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import useColor, { usePrefetchColor } from 'lib/hooks/useColor'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { ReactNode, useMemo } from 'react'
import Column from '../Column'
import Row from '../Row'
import { inputAtom, outputAtom, useUpdateInputToken, useUpdateInputValue } from './state'
import TokenInput from './TokenInput'
export const colorAtom = atom<string | undefined>(undefined)
const OutputColumn = styled(Column)<{ hasColor: boolean | null }>`
background-color: ${({ theme }) => theme.module};
border-radius: ${({ theme }) => theme.borderRadius - 0.25}em;
padding: 0.75em;
position: relative;
// Set transitions to reduce color flashes when switching color/token.
// When color loads, transition the background so that it transitions from the empty or last state, but not _to_ the empty state.
transition: ${({ hasColor }) => (hasColor ? 'background-color 0.25s ease-out' : undefined)};
* {
// When color is loading, delay the color/stroke so that it seems to transition from the last state.
transition: ${({ hasColor }) => (hasColor === null ? 'color 0.25s ease-in, stroke 0.25s ease-in' : undefined)};
}
`
interface OutputProps {
disabled?: boolean
children: ReactNode
}
export default function Output({ disabled, children }: OutputProps) {
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const setValue = useUpdateInputValue(outputAtom)
const setToken = useUpdateInputToken(outputAtom)
const balance = 123.45
const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useColor(output.token)
usePrefetchColor(input.token) // extract eagerly in case of reversal
const color = overrideColor || dynamicColor
const hasColor = output.token ? Boolean(color) || null : false
const change = useMemo(() => {
if (input.usdc && output.usdc) {
const change = output.usdc / input.usdc - 1
const percent = (change * 100).toPrecision(3)
return change > 0 ? ` (+${percent}%)` : `(${percent}%)`
}
return ''
}, [input, output])
const usdc = useMemo(() => {
if (output.usdc) {
return `~ $${output.usdc.toLocaleString('en')}${change}`
}
return '-'
}, [change, output])
return (
<DynamicThemeProvider color={color}>
<OutputColumn hasColor={hasColor} gap={0.5}>
<Row>
<ThemedText.Subhead2 color="secondary">
<Trans>For</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput input={output} disabled={disabled} onChangeInput={setValue} onChangeToken={setToken}>
<ThemedText.Body2 color="secondary">
<Row>
{usdc}
{balance && (
<span>
Balance: <span style={{ userSelect: 'text' }}>{balance}</span>
</span>
)}
</Row>
</ThemedText.Body2>
</TokenInput>
{children}
</OutputColumn>
</DynamicThemeProvider>
)
}

@ -0,0 +1,72 @@
import { useAtom } from 'jotai'
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons'
import styled, { Layer } from 'lib/theme'
import { useCallback, useState } from 'react'
import Button from '../Button'
import Row from '../Row'
import { stateAtom } from './state'
const ReverseRow = styled(Row)`
bottom: -1.5em;
position: absolute;
width: 100%;
z-index: ${Layer.OVERLAY};
`
const ArrowUp = styled(ArrowUpIcon)`
left: calc(50% - 0.37em);
position: absolute;
top: calc(50% - 0.82em);
`
const ArrowDown = styled(ArrowDownIcon)`
bottom: calc(50% - 0.82em);
position: absolute;
right: calc(50% - 0.37em);
`
const Overlay = styled.div`
background-color: ${({ theme }) => theme.container};
border-radius: ${({ theme }) => theme.borderRadius}em;
padding: 0.25em;
`
const StyledReverseButton = styled(Button)<{ turns: number }>`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
height: 2.5em;
position: relative;
width: 2.5em;
div {
transform: rotate(${({ turns }) => turns / 2}turn);
transition: transform 0.25s ease-in-out;
will-change: transform;
}
`
export default function ReverseButton({ disabled }: { disabled?: boolean }) {
const [state, setState] = useAtom(stateAtom)
const [turns, setTurns] = useState(0)
const onClick = useCallback(() => {
const { input, output } = state
setState((state) => {
state.input = output
state.output = input
})
setTurns((turns) => ++turns)
}, [state, setState])
return (
<ReverseRow justify="center">
<Overlay>
<StyledReverseButton disabled={disabled} onClick={onClick} turns={turns}>
<div>
<ArrowUp strokeWidth={3} />
<ArrowDown strokeWidth={3} />
</div>
</StyledReverseButton>
</Overlay>
</ReverseRow>
)
}

@ -0,0 +1,8 @@
import { Modal } from '../Dialog'
import { SettingsDialog } from './Settings'
export default (
<Modal color="module">
<SettingsDialog />
</Modal>
)

@ -0,0 +1,91 @@
import { t, Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { Check, LargeIcon } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useRef } from 'react'
import { BaseButton, TextButton } from '../../Button'
import Column from '../../Column'
import { DecimalInput, inputCss } from '../../Input'
import Row from '../../Row'
import { MaxSlippage, maxSlippageAtom } from '../state'
import { Label, optionCss } from './components'
const tooltip = (
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
)
const StyledOption = styled(TextButton)<{ selected: boolean }>`
${({ selected }) => optionCss(selected)}
`
const StyledInputOption = styled(BaseButton)<{ selected: boolean }>`
${({ selected }) => optionCss(selected)}
${inputCss}
border-color: ${({ selected, theme }) => (selected ? theme.active : 'transparent')} !important;
padding: calc(0.5em - 1px) 0.625em;
`
interface OptionProps<T> {
value: T
selected: boolean
onSelect: (value: T) => void
}
function Option<T>({ value, selected, onSelect }: OptionProps<T>) {
return (
<StyledOption selected={selected} onClick={() => onSelect(value)}>
<Row>
<ThemedText.Subhead2>{value}%</ThemedText.Subhead2>
{selected && <LargeIcon icon={Check} />}
</Row>
</StyledOption>
)
}
function InputOption<T>({ value, children, selected, onSelect }: OptionProps<T> & { children: ReactNode }) {
return (
<StyledInputOption color="container" selected={selected} onClick={() => onSelect(value)}>
<ThemedText.Subhead2>
<Row>{children}</Row>
</ThemedText.Subhead2>
</StyledInputOption>
)
}
export default function MaxSlippageSelect() {
const { P01, P05, CUSTOM } = MaxSlippage
const [{ value: maxSlippage, custom }, setMaxSlippage] = useAtom(maxSlippageAtom)
const input = useRef<HTMLInputElement>(null)
const focus = useCallback(() => input.current?.focus(), [input])
const onInputSelect = useCallback(
(custom) => {
focus()
if (custom !== undefined) {
setMaxSlippage({ value: CUSTOM, custom })
}
},
[CUSTOM, focus, setMaxSlippage]
)
return (
<Column gap={0.75}>
<Label name={<Trans>Max slippage</Trans>} tooltip={tooltip} />
<Row gap={0.5} grow>
<Option value={P01} onSelect={setMaxSlippage} selected={maxSlippage === P01} />
<Option value={P05} onSelect={setMaxSlippage} selected={maxSlippage === P05} />
<InputOption value={custom} onSelect={onInputSelect} selected={maxSlippage === CUSTOM}>
<DecimalInput
size={custom === undefined ? undefined : 5}
value={custom}
onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom })}
placeholder={t`Custom`}
ref={input}
/>
%
</InputOption>
</Row>
</Column>
)
}

@ -0,0 +1,17 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import Row from '../../Row'
import Toggle from '../../Toggle'
import { mockTogglableAtom } from '../state'
import { Label } from './components'
export default function MockToggle() {
const [mockTogglable, toggleMockTogglable] = useAtom(mockTogglableAtom)
return (
<Row>
<Label name={<Trans>Mock Toggle</Trans>} />
<Toggle checked={mockTogglable} onToggle={toggleMockTogglable} />
</Row>
)
}

@ -0,0 +1,37 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import styled, { ThemedText } from 'lib/theme'
import { useRef } from 'react'
import Column from '../../Column'
import { inputCss, IntegerInput } from '../../Input'
import Row from '../../Row'
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from '../state'
import { Label } from './components'
const tooltip = <Trans>Your transaction will revert if it has been pending for longer than this period of time.</Trans>
const Input = styled(Row)`
${inputCss}
`
export default function TransactionTtlInput() {
const [transactionTtl, setTransactionTtl] = useAtom(transactionTtlAtom)
const input = useRef<HTMLInputElement>(null)
return (
<Column gap={0.75}>
<Label name={<Trans>Transaction deadline</Trans>} tooltip={tooltip} />
<ThemedText.Body1>
<Input onClick={() => input.current?.focus()}>
<IntegerInput
placeholder={TRANSACTION_TTL_DEFAULT.toString()}
value={transactionTtl}
onChange={(value) => setTransactionTtl(value ?? 0)}
ref={input}
/>
<Trans>minutes</Trans>
</Input>
</ThemedText.Body1>
</Column>
)
}

@ -0,0 +1,43 @@
import styled, { css, ThemedText } from 'lib/theme'
import { ReactNode } from 'react'
import { AnyStyledComponent } from 'styled-components'
import Row from '../../Row'
import Tooltip from '../../Tooltip'
export const optionCss = (selected: boolean) => css`
border: 1px solid ${({ theme }) => (selected ? theme.active : theme.outline)};
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
color: ${({ theme }) => theme.primary} !important;
display: grid;
grid-gap: 0.25em;
padding: 0.5em 0.625em;
:enabled:hover {
border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)};
}
`
export function value(Value: AnyStyledComponent) {
return styled(Value)<{ selected?: boolean; cursor?: string }>`
cursor: ${({ cursor }) => cursor ?? 'pointer'};
`
}
interface LabelProps {
name: ReactNode
tooltip?: ReactNode
}
export function Label({ name, tooltip }: LabelProps) {
return (
<Row gap={0.5} justify="flex-start">
<ThemedText.Subhead2>{name}</ThemedText.Subhead2>
{tooltip && (
<Tooltip placement="top" contained>
<ThemedText.Caption>{tooltip}</ThemedText.Caption>
</Tooltip>
)}
</Row>
)
}

@ -0,0 +1,67 @@
import { Trans } from '@lingui/macro'
import { useResetAtom } from 'jotai/utils'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Settings as SettingsIcon } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import React, { useState } from 'react'
import { IconButton, TextButton } from '../../Button'
import Column from '../../Column'
import Dialog, { Header } from '../../Dialog'
import { BoundaryProvider } from '../../Popover'
import { settingsAtom } from '../state'
import MaxSlippageSelect from './MaxSlippageSelect'
import TransactionTtlInput from './TransactionTtlInput'
export function SettingsDialog() {
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(boundary, { padded: true })
const resetSettings = useResetAtom(settingsAtom)
return (
<>
<Header title={<Trans>Settings</Trans>} ruled>
<TextButton onClick={resetSettings}>
<ThemedText.ButtonSmall>
<Trans>Reset</Trans>
</ThemedText.ButtonSmall>
</TextButton>
</Header>
<Column gap={1} style={{ paddingTop: '1em' }} ref={setBoundary} padded css={scrollbar}>
<BoundaryProvider value={boundary}>
<MaxSlippageSelect />
<TransactionTtlInput />
</BoundaryProvider>
</Column>
</>
)
}
const SettingsButton = styled(IconButton)<{ hover: boolean }>`
${SettingsIcon} {
transform: ${({ hover }) => hover && 'rotate(45deg)'};
transition: ${({ hover }) => hover && 'transform 0.25s'};
will-change: transform;
}
`
export default function Settings({ disabled }: { disabled?: boolean }) {
const [open, setOpen] = useState(false)
const [hover, setHover] = useState(false)
return (
<>
<SettingsButton
disabled={disabled}
hover={hover}
onClick={() => setOpen(true)}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
icon={SettingsIcon}
/>
{open && (
<Dialog color="module" onClose={() => setOpen(false)}>
<SettingsDialog />
</Dialog>
)}
</>
)
}

@ -0,0 +1,60 @@
import { useUpdateAtom } from 'jotai/utils'
import { DAI, ETH } from 'lib/mocks'
import { useEffect } from 'react'
import { useSelect } from 'react-cosmos/fixture'
import invariant from 'tiny-invariant'
import { Modal } from '../Dialog'
import { transactionAtom } from './state'
import { StatusDialog } from './Status'
function Fixture() {
const setTransaction = useUpdateAtom(transactionAtom)
const [state] = useSelect('state', {
options: ['PENDING', 'ERROR', 'SUCCESS'],
})
useEffect(() => {
setTransaction({
input: { token: ETH, value: 1 },
output: { token: DAI, value: 4200 },
receipt: '',
timestamp: Date.now(),
})
}, [setTransaction])
useEffect(() => {
switch (state) {
case 'PENDING':
setTransaction({
input: { token: ETH, value: 1 },
output: { token: DAI, value: 4200 },
receipt: '',
timestamp: Date.now(),
})
break
case 'ERROR':
setTransaction((tx) => {
invariant(tx)
tx.status = new Error(
'Swap failed: Unknown error: "Error: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent pulvinar, risus eu pretium condimentum, tellus dui fermentum turpis, id gravida metus justo ac lorem. Etiam vitae dapibus eros, nec elementum ipsum. Duis condimentum, felis vel tempor ultricies, eros diam tempus odio, at tempor urna odio id massa. Aliquam laoreet turpis justo, auctor accumsan est pellentesque at. Integer et dolor feugiat, sodales tortor non, cursus augue. Phasellus id suscipit justo, in ultricies tortor. Aenean libero nibh, egestas sit amet vehicula sit amet, tempor ac ligula. Cras at tempor lectus. Mauris sollicitudin est velit, nec consectetur lorem dapibus ut. Praesent magna ex, faucibus ac fermentum malesuada, molestie at ex. Phasellus bibendum lorem nec dolor dignissim eleifend. Nam dignissim varius velit, at volutpat justo pretium id."'
)
tx.elapsedMs = Date.now() - tx.timestamp
})
break
case 'SUCCESS':
setTransaction((tx) => {
invariant(tx)
tx.status = true
tx.elapsedMs = Date.now() - tx.timestamp
})
break
}
}, [setTransaction, state])
return <StatusDialog onClose={() => void 0} />
}
export default (
<Modal color="dialog">
<Fixture />
</Modal>
)

@ -0,0 +1,110 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
import Row from '../../Row'
import { Transaction, transactionAtom } from '../state'
import Summary from '../Summary'
const errorMessage = (
<Trans>
Try increasing your slippage tolerance.
<br />
NOTE: Fee on transfer and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`
function ElapsedTime({ tx }: { tx: Transaction | null }) {
const [elapsedMs, setElapsedMs] = useState(0)
useInterval(
() => {
if (tx?.elapsedMs) {
setElapsedMs(tx.elapsedMs)
} else if (tx?.timestamp) {
setElapsedMs(Date.now() - tx.timestamp)
}
},
elapsedMs === tx?.elapsedMs ? null : 1000
)
const toElapsedTime = useCallback((ms: number) => {
let sec = Math.floor(ms / 1000)
const min = Math.floor(sec / 60)
sec = sec % 60
if (min) {
return (
<Trans>
{min}m {sec}s
</Trans>
)
} else {
return <Trans>{sec}s</Trans>
}
}, [])
return (
<Row gap={0.5}>
<Clock />
<ThemedText.Body2>{toElapsedTime(elapsedMs)}</ThemedText.Body2>
</Row>
)
}
const EtherscanA = styled.a`
color: ${({ theme }) => theme.accent};
text-decoration: none;
`
interface TransactionStatusProps extends StatusProps {
tx: Transaction | null
}
function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
const Icon = useMemo(() => {
return tx?.status ? CheckCircle : Spinner
}, [tx?.status])
const heading = useMemo(() => {
return tx?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx?.status])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx?.status && 'success'}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
{tx ? <Summary input={tx.input} output={tx.output} /> : <div style={{ height: '1.25em' }} />}
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>
<EtherscanA href="//etherscan.io" target="_blank">
<Trans>View on Etherscan</Trans>
</EtherscanA>
</ThemedText.ButtonSmall>
<ElapsedTime tx={tx} />
</TransactionRow>
<ActionButton onClick={onClose}>
<Trans>Close</Trans>
</ActionButton>
</Column>
)
}
interface StatusProps {
onClose: () => void
}
export default function TransactionStatusDialog({ onClose }: StatusProps) {
const tx = useAtomValue(transactionAtom)
return tx?.status instanceof Error ? (
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
) : (
<TransactionStatus tx={tx} onClose={onClose} />
)
}

@ -0,0 +1 @@
export { default as StatusDialog } from './StatusDialog'

@ -0,0 +1,43 @@
import { useUpdateAtom } from 'jotai/utils'
import { DAI, ETH } from 'lib/mocks'
import { useEffect, useState } from 'react'
import { useValue } from 'react-cosmos/fixture'
import { Modal } from '../Dialog'
import { Field, outputAtom, stateAtom } from './state'
import { SummaryDialog } from './Summary'
function Fixture() {
const setState = useUpdateAtom(stateAtom)
const [, setInitialized] = useState(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
setState({
activeInput: Field.INPUT,
input: { token: ETH, value: 1, usdc: 4195 },
output: { token: DAI, value: 4200, usdc: 4200 },
swap: {
lpFee: 0.0005,
integratorFee: 0.00025,
priceImpact: 0.01,
slippageTolerance: 0.5,
minimumReceived: 4190,
},
})
setInitialized(true)
})
const setOutput = useUpdateAtom(outputAtom)
const [price] = useValue('output value', { defaultValue: 4200 })
useEffect(() => {
setState((state) => ({ ...state, output: { token: DAI, value: price, usdc: price } }))
}, [price, setOutput, setState])
return (
<Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} />
</Modal>
)
}
export default <Fixture />

@ -0,0 +1,58 @@
import { t } from '@lingui/macro'
import { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useMemo } from 'react'
import Row from '../../Row'
import { State } from '../state'
interface DetailProps {
label: string
value: string
}
function Detail({ label, value }: DetailProps) {
return (
<ThemedText.Caption>
<Row gap={2}>
<span>{label}</span>
<span style={{ whiteSpace: 'nowrap' }}>{value}</span>
</Row>
</ThemedText.Caption>
)
}
interface DetailsProps {
swap: Required<State>['swap']
input: Token
output: Token
}
export default function Details({
input: { symbol: inputSymbol },
output: { symbol: outputSymbol },
swap,
}: DetailsProps) {
const integrator = window.location.hostname
const details = useMemo((): [string, string][] => {
return [
[t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, swap.integratorFee && `${swap.integratorFee} ${inputSymbol}`],
[t`Price impact`, `${swap.priceImpact}%`],
[t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`],
[t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`],
[t`Slippage tolerance`, `${swap.slippageTolerance}%`],
].filter(isDetail)
function isDetail(detail: unknown[]): detail is [string, string] {
return Boolean(detail[1])
}
}, [inputSymbol, outputSymbol, swap, integrator])
return (
<>
{details.map(([label, detail]) => (
<Detail key={label} label={label} value={detail} />
))}
</>
)
}

@ -0,0 +1,69 @@
import { ArrowRight } from 'lib/icons'
import styled from 'lib/theme'
import { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import Column from '../../Column'
import Row from '../../Row'
import TokenImg from '../../TokenImg'
import { Input } from '../state'
const Percent = styled.span<{ gain: boolean }>`
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
`
interface TokenValueProps {
input: Required<Pick<Input, 'token' | 'value'>> & Input
usdc?: boolean
change?: number
}
function TokenValue({ input, usdc, change }: TokenValueProps) {
const percent = useMemo(() => {
if (change) {
const percent = (change * 100).toPrecision(3)
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
}
return undefined
}, [change])
return (
<Column justify="flex-start">
<Row gap={0.375} justify="flex-start">
<TokenImg token={input.token} />
<ThemedText.Body2>
{input.value} {input.token.symbol}
</ThemedText.Body2>
</Row>
{usdc && input.usdc && (
<Row justify="flex-start">
<ThemedText.Caption color="secondary">
~ ${input.usdc.toLocaleString('en')}
{change && <Percent gain={change > 0}> {percent}</Percent>}
</ThemedText.Caption>
</Row>
)}
</Column>
)
}
interface SummaryProps {
input: Required<Pick<Input, 'token' | 'value'>> & Input
output: Required<Pick<Input, 'token' | 'value'>> & Input
usdc?: boolean
}
export default function Summary({ input, output, usdc }: SummaryProps) {
const change = useMemo(() => {
if (usdc && input.usdc && output.usdc) {
return output.usdc / input.usdc - 1
}
return undefined
}, [usdc, input.usdc, output.usdc])
return (
<Row gap={usdc ? 1 : 0.25}>
<TokenValue input={input} usdc={usdc} />
<ArrowRight />
<TokenValue input={output} usdc={usdc} change={change} />
</Row>
)
}

@ -0,0 +1,154 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import { IconButton } from 'lib/components/Button'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando, Info } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useMemo, useState } from 'react'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
import { Header } from '../../Dialog'
import Row from '../../Row'
import Rule from '../../Rule'
import { Input, inputAtom, outputAtom, swapAtom } from '../state'
import Details from './Details'
import Summary from './Summary'
export default Summary
function asInput(input: Input): (Required<Pick<Input, 'token' | 'value'>> & Input) | undefined {
return input.token && input.value ? (input as Required<Pick<Input, 'token' | 'value'>>) : undefined
}
const updated = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
const SummaryColumn = styled(Column)``
const ExpandoColumn = styled(Column)``
const DetailsColumn = styled(Column)``
const Estimate = styled(ThemedText.Caption)``
const Body = styled(Column)<{ open: boolean }>`
height: calc(100% - 2.5em);
${SummaryColumn} {
flex-grow: ${({ open }) => (open ? 0 : 1)};
transition: flex-grow 0.25s;
}
${ExpandoColumn} {
flex-grow: ${({ open }) => (open ? 1 : 0)};
transition: flex-grow 0.25s;
${DetailsColumn} {
flex-basis: ${({ open }) => (open ? 7 : 0)}em;
overflow-y: hidden;
position: relative;
transition: flex-basis 0.25s;
${Column} {
height: 100%;
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
}
}
${Estimate} {
max-height: ${({ open }) => (open ? 0 : 56 / 12)}em; // 2 * line-height + padding
overflow-y: hidden;
padding: ${({ open }) => (open ? 0 : '1em 0')};
transition: ${({ open }) =>
open
? 'max-height 0.1s ease-out, padding 0.25s ease-out'
: 'flex-grow 0.25s ease-out, max-height 0.1s ease-in, padding 0.25s ease-out'};
}
}
`
interface SummaryDialogProps {
onConfirm: () => void
}
export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
const swap = useAtomValue(swapAtom)
const partialInput = useAtomValue(inputAtom)
const partialOutput = useAtomValue(outputAtom)
const input = asInput(partialInput)
const output = asInput(partialOutput)
const price = useMemo(() => {
return input && output ? output.value / input.value : undefined
}, [input, output])
const [confirmedPrice, confirmPrice] = useState(price)
const [open, setOpen] = useState(true)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
if (!(input && output && swap)) {
return null
}
return (
<>
<Header title={<Trans>Swap summary</Trans>} ruled />
<Body flex align="stretch" gap={0.75} padded open={open}>
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={input} output={output} usdc={true} />
<ThemedText.Caption>
1 {input.token.symbol} = {price} {output.token.symbol}
</ThemedText.Caption>
</SummaryColumn>
<Rule />
<Row>
<Row gap={0.5}>
<Info color="secondary" />
<ThemedText.Subhead2 color="secondary">
<Trans>Swap details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch">
<Rule />
<DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details input={input.token} output={output.token} swap={swap} />
</Column>
</DetailsColumn>
<Estimate color="secondary">
<Trans>Output is estimated.</Trans>{' '}
{swap?.minimumReceived && (
<Trans>
You will receive at least {swap.minimumReceived} {output.token.symbol} or the transaction will revert.
</Trans>
)}
{swap?.maximumSent && (
<Trans>
You will send at most {swap.maximumSent} {input.token.symbol} or the transaction will revert.
</Trans>
)}
</Estimate>
<ActionButton
onClick={onConfirm}
onUpdate={() => confirmPrice(price)}
updated={price === confirmedPrice ? undefined : updated}
>
<Trans>Confirm swap</Trans>
</ActionButton>
</ExpandoColumn>
</Body>
</>
)
}

@ -0,0 +1,67 @@
import { useAtom } from 'jotai'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
import { useValue } from 'react-cosmos/fixture'
import Swap from '.'
import { colorAtom } from './Output'
import { inputAtom, outputAtom, swapAtom } from './state'
const validateColor = (() => {
const validator = document.createElement('div').style
return (color: string) => {
validator.color = ''
validator.color = color
return validator.color !== ''
}
})()
function Fixture() {
const [input, setInput] = useAtom(inputAtom)
const [output, setOutput] = useAtom(outputAtom)
const [swap, setSwap] = useAtom(swapAtom)
const [priceFetched] = useValue('price fetched', { defaultValue: false })
useEffect(() => {
if (priceFetched && input.token && output.token) {
const inputValue = input.value || 1
const inputUsdc = input.usdc || inputValue
const outputValue = output.value || 1
const outputUsdc = output.usdc || outputValue
if (!(inputValue === input.value && inputUsdc === input.usdc)) {
setInput({ ...input, value: inputValue, usdc: inputUsdc })
}
if (!(outputValue === output.value && outputUsdc === output.usdc)) {
setOutput({ ...output, value: outputValue, usdc: outputUsdc })
}
if (!swap || swap.minimumReceived !== outputValue * 0.995) {
setSwap({
lpFee: 0.0005,
priceImpact: 0.01,
slippageTolerance: 0.5,
minimumReceived: outputValue * 0.995,
})
}
} else if (swap) {
setSwap(undefined)
}
}, [input, output, priceFetched, setInput, setOutput, setSwap, swap])
const [tokenApproved] = useValue('token approved', { defaultValue: true })
useEffect(() => {
if (tokenApproved !== input.approved) {
setInput({ ...input, approved: tokenApproved })
}
}, [input, setInput, tokenApproved])
const setColor = useUpdateAtom(colorAtom)
const [color] = useValue('token color', { defaultValue: '' })
useEffect(() => {
if (!color || validateColor(color)) {
setColor(color)
}
}, [color, setColor])
return <Swap />
}
export default <Fixture />

@ -0,0 +1,58 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import { useCallback, useMemo, useState } from 'react'
import ActionButton from '../ActionButton'
import Dialog from '../Dialog'
import { inputAtom, outputAtom, swapAtom } from './state'
import { StatusDialog } from './Status'
import { SummaryDialog } from './Summary'
const mockBalance = 123.45
enum Mode {
NONE,
SUMMARY,
STATUS,
}
export default function SwapButton() {
const swap = useAtomValue(swapAtom)
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const balance = mockBalance
const [mode, setMode] = useState(Mode.NONE)
const actionProps = useMemo(() => {
if (swap && input.token && input.value && output.token && output.value && input.value <= balance) {
if (input.approved) {
return {}
} else {
return {
updated: { message: <Trans>Approve {input.token.symbol} first</Trans>, action: <Trans>Approve</Trans> },
}
}
}
return { disabled: true }
}, [balance, input.approved, input.token, input.value, output.token, output.value, swap])
const onConfirm = useCallback(() => {
// TODO: Send the tx to the connected wallet.
setMode(Mode.STATUS)
}, [])
return (
<>
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={() => void 0} {...actionProps}>
<Trans>Review swap</Trans>
</ActionButton>
{mode >= Mode.SUMMARY && (
<Dialog color="dialog" onClose={() => setMode(Mode.NONE)}>
<SummaryDialog onConfirm={onConfirm} />
</Dialog>
)}
{mode >= Mode.STATUS && (
<Dialog color="dialog">
<StatusDialog onClose={() => setMode(Mode.NONE)} />
</Dialog>
)}
</>
)
}

@ -0,0 +1,95 @@
import { Trans } from '@lingui/macro'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
import Button from '../Button'
import Column from '../Column'
import { DecimalInput } from '../Input'
import Row from '../Row'
import TokenSelect from '../TokenSelect'
import { Input } from './state'
const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;
`
const ValueInput = styled(DecimalInput)`
color: ${({ theme }) => theme.primary};
:hover:not(:focus-within) {
color: ${({ theme }) => theme.onHover(theme.primary)};
}
:hover:not(:focus-within)::placeholder {
color: ${({ theme }) => theme.onHover(theme.secondary)};
}
`
const delayedFadeIn = keyframes`
0% {
opacity: 0;
}
25% {
opacity: 0;
}
100% {
opacity: 1;
}
`
const MaxButton = styled(Button)`
animation: ${delayedFadeIn} 0.25s linear;
border-radius: 0.75em;
padding: 0.5em;
`
interface TokenInputProps {
input: Input
disabled?: boolean
onMax?: () => void
onChangeInput: (input: number | undefined) => void
onChangeToken: (token: Token) => void
children: ReactNode
}
export default function TokenInput({
input: { value, token },
disabled,
onMax,
onChangeInput,
onChangeToken,
children,
}: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null)
const [showMax, setShowMax] = useState(false)
const onFocus = useCallback(() => setShowMax(Boolean(onMax)), [onMax])
const onBlur = useCallback((e: FocusEvent) => {
if (e.relatedTarget !== max.current) {
setShowMax(false)
}
}, [])
return (
<Column gap={0.25}>
<TokenInputRow gap={0.5} onBlur={onBlur}>
<ThemedText.H2>
<ValueInput
value={value}
onFocus={onFocus}
onChange={onChangeInput}
disabled={disabled || !token}
></ValueInput>
</ThemedText.H2>
{showMax && (
<MaxButton onClick={onMax} ref={max}>
<ThemedText.ButtonMedium>
<Trans>Max</Trans>
</ThemedText.ButtonMedium>
</MaxButton>
)}
<TokenSelect value={token} collapsed={showMax} disabled={disabled} onSelect={onChangeToken} />
</TokenInputRow>
{children}
</Column>
)
}

@ -0,0 +1,124 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons'
import styled, { ThemedText, ThemeProvider } from 'lib/theme'
import { useMemo, useState } from 'react'
import { TextButton } from '../Button'
import Row from '../Row'
import Rule from '../Rule'
import Tooltip from '../Tooltip'
import { Field, Input, inputAtom, outputAtom, stateAtom, swapAtom } from './state'
const mockBalance = 123.45
function RoutingTooltip() {
return (
<Tooltip icon={Info} placement="bottom">
<ThemeProvider>
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
</ThemeProvider>
</Tooltip>
)
}
type FilledInput = Input & Required<Pick<Input, 'token' | 'value'>>
function asFilledInput(input: Input): FilledInput | undefined {
return input.token && input.value ? (input as FilledInput) : undefined
}
interface LoadedStateProps {
input: FilledInput
output: FilledInput
}
function LoadedState({ input, output }: LoadedStateProps) {
const [flip, setFlip] = useState(true)
const ratio = useMemo(() => {
const [a, b] = flip ? [output, input] : [input, output]
const ratio = `1 ${a.token.symbol} = ${b.value / a.value} ${b.token.symbol}`
const usdc = a.usdc && ` ($${(a.usdc / a.value).toLocaleString('en')})`
return (
<Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
)
}, [flip, input, output])
return (
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
)
}
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { activeInput } = useAtomValue(stateAtom)
const swap = useAtomValue(swapAtom)
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const balance = mockBalance
const caption = useMemo(() => {
const filledInput = asFilledInput(input)
const filledOutput = asFilledInput(output)
if (disabled) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Connect wallet to swap</Trans>
</>
)
}
if (activeInput === Field.INPUT ? filledInput && output.token : filledOutput && input.token) {
if (!swap) {
return (
<>
<Spinner color="secondary" />
<Trans>Fetching best price</Trans>
</>
)
}
if (filledInput && filledInput.value > balance) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Insufficient {filledInput.token.symbol}</Trans>
</>
)
}
if (filledInput && filledOutput) {
return (
<>
<RoutingTooltip />
<LoadedState input={filledInput} output={filledOutput} />
</>
)
}
}
return (
<>
<Info color="secondary" />
<Trans>Enter an amount</Trans>
</>
)
}, [activeInput, balance, disabled, input, output, swap])
return (
<>
<Rule />
<ThemedText.Caption>
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
{caption}
</ToolbarRow>
</ThemedText.Caption>
</>
)
}

@ -0,0 +1,37 @@
import { Trans } from '@lingui/macro'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useState } from 'react'
import Header from '../Header'
import { BoundaryProvider } from '../Popover'
import Wallet from '../Wallet'
import Input from './Input'
import Output from './Output'
import ReverseButton from './ReverseButton'
import Settings from './Settings'
import SwapButton from './SwapButton'
import Toolbar from './Toolbar'
export default function Swap() {
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
const { active, account } = useActiveWeb3React()
return (
<>
<Header logo title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} />}
<Settings disabled={!active} />
</Header>
<div ref={setBoundary}>
<BoundaryProvider value={boundary}>
<Input disabled={!active}>
<ReverseButton disabled={!active} />
</Input>
<Output disabled={!active}>
<Toolbar disabled={!active} />
<SwapButton />
</Output>
</BoundaryProvider>
</div>
</>
)
}

@ -0,0 +1,117 @@
import { atom, WritableAtom } from 'jotai'
import { atomWithImmer } from 'jotai/immer'
import { useUpdateAtom } from 'jotai/utils'
import { atomWithReset } from 'jotai/utils'
import { ETH } from 'lib/mocks'
import { Token } from 'lib/types'
import { Customizable, pickAtom, setCustomizable, setTogglable } from 'lib/utils/atoms'
import { useMemo } from 'react'
/** Max slippage, as a percentage. */
export enum MaxSlippage {
P01 = 0.1,
P05 = 0.5,
// Members to satisfy CustomizableEnum; see setCustomizable
CUSTOM = -1,
DEFAULT = P05,
}
export const TRANSACTION_TTL_DEFAULT = 40
export interface Settings {
maxSlippage: Customizable<MaxSlippage>
transactionTtl: number | undefined
mockTogglable: boolean
}
const initialSettings: Settings = {
maxSlippage: { value: MaxSlippage.DEFAULT },
transactionTtl: undefined,
mockTogglable: true,
}
export const settingsAtom = atomWithReset(initialSettings)
export const maxSlippageAtom = pickAtom(settingsAtom, 'maxSlippage', setCustomizable(MaxSlippage))
export const transactionTtlAtom = pickAtom(settingsAtom, 'transactionTtl')
export const mockTogglableAtom = pickAtom(settingsAtom, 'mockTogglable', setTogglable)
export enum Field {
INPUT = 'input',
OUTPUT = 'output',
}
export interface Input {
value?: number
token?: Token
usdc?: number
}
export interface State {
activeInput: Field
[Field.INPUT]: Input & { approved?: boolean }
[Field.OUTPUT]: Input
swap?: {
lpFee: number
priceImpact: number
slippageTolerance: number
integratorFee?: number
maximumSent?: number
minimumReceived?: number
}
}
export const stateAtom = atomWithImmer<State>({
activeInput: Field.INPUT,
input: { token: ETH },
output: {},
})
export const swapAtom = pickAtom(stateAtom, 'swap')
export const inputAtom = atom(
(get) => get(stateAtom).input,
(get, set, update: Input & { approved?: boolean }) => {
set(stateAtom, (state) => {
state.activeInput = Field.INPUT
state.input = update
state.swap = undefined
})
}
)
export const outputAtom = atom(
(get) => get(stateAtom).output,
(get, set, update: Input) => {
set(stateAtom, (state) => {
state.activeInput = Field.OUTPUT
state.output = update
state.swap = undefined
})
}
)
export function useUpdateInputValue(inputAtom: WritableAtom<Input, Input>) {
return useUpdateAtom(
useMemo(
() => atom(null, (get, set, value: Input['value']) => set(inputAtom, { token: get(inputAtom).token, value })),
[inputAtom]
)
)
}
export function useUpdateInputToken(inputAtom: WritableAtom<Input, Input>) {
return useUpdateAtom(
useMemo(() => atom(null, (get, set, token: Input['token']) => set(inputAtom, { token })), [inputAtom])
)
}
export interface Transaction {
input: Required<Pick<Input, 'token' | 'value'>>
output: Required<Pick<Input, 'token' | 'value'>>
receipt: string
timestamp: number
elapsedMs?: number
status?: true | Error
}
export const transactionAtom = atomWithImmer<Transaction | null>(null)

@ -0,0 +1,90 @@
import { t } from '@lingui/macro'
import styled, { ThemedText } from 'lib/theme'
import { transparentize } from 'polished'
import { KeyboardEvent, useCallback } from 'react'
const Input = styled.input<{ text: string }>`
-moz-appearance: none;
-webkit-appearance: none;
align-items: center;
appearance: none;
background: ${({ theme }) => theme.interactive};
border: none;
border-radius: ${({ theme }) => theme.borderRadius * 1.25}em;
cursor: pointer;
display: flex;
font-size: inherit;
font-weight: inherit;
height: 2em;
margin: 0;
padding: 0;
position: relative;
width: 4.5em;
:before {
background-color: ${({ theme }) => theme.secondary};
border-radius: ${({ theme }) => theme.borderRadius * 50}%;
content: '';
display: inline-block;
height: 1.5em;
margin-left: 0.25em;
position: absolute;
width: 1.5em;
}
:hover:before {
background-color: ${({ theme }) => transparentize(0.3, theme.secondary)};
}
:checked:before {
background-color: ${({ theme }) => theme.accent};
margin-left: 2.75em;
}
:hover:checked:before {
background-color: ${({ theme }) => transparentize(0.3, theme.accent)};
}
:after {
content: '${({ text }) => text}';
margin-left: 1.75em;
text-align: center;
width: 2.75em;
}
:checked:after {
margin-left: 0;
}
:before {
transition: margin 0.25s ease;
}
`
interface ToggleProps {
checked: boolean
onToggle: () => void
}
export default function Toggle({ checked, onToggle }: ToggleProps) {
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Enter') {
onToggle()
}
},
[onToggle]
)
return (
<ThemedText.ButtonMedium>
<Input
type="checkbox"
checked={checked}
text={checked ? t`ON` : t`OFF`}
onChange={() => onToggle()}
onKeyDown={onKeyDown}
/>
</ThemedText.ButtonMedium>
)
}

@ -0,0 +1,33 @@
import useNativeEvent from 'lib/hooks/useNativeEvent'
import styled from 'lib/theme'
import { Token } from 'lib/types'
import { useState } from 'react'
interface TokenImgProps {
className?: string
token: Token
}
const TRANSPARENT_SRC = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
function TokenImg({ className, token }: TokenImgProps) {
const [img, setImg] = useState<HTMLImageElement | null>(null)
useNativeEvent(img, 'error', () => {
if (img) {
// Use a local transparent gif to avoid the browser-dependent broken img icon.
// The icon may still flash, but using a native event further reduces the duration.
img.src = TRANSPARENT_SRC
}
})
return <img className={className} src={token.logoURI} alt={token.name || token.symbol} ref={setImg} />
}
export default styled(TokenImg)<{ size?: number }>`
// radial-gradient calculates distance from the corner, not the edge: divide by sqrt(2)
background: radial-gradient(
${({ theme }) => theme.module} calc(100% / ${Math.sqrt(2)} - 1.5px),
${({ theme }) => theme.outline} calc(100% / ${Math.sqrt(2)} - 1.5px)
);
border-radius: 100%;
height: ${({ size }) => size || 1}em;
width: ${({ size }) => size || 1}em;
`

@ -0,0 +1,8 @@
import { Modal } from './Dialog'
import { TokenSelectDialog } from './TokenSelect'
export default (
<Modal color="module">
<TokenSelectDialog onSelect={() => void 0} />
</Modal>
)

@ -0,0 +1,29 @@
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import Button from '../Button'
import Row from '../Row'
import TokenImg from '../TokenImg'
const TokenButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
padding: 0.25em 0.75em 0.25em 0.25em;
`
interface TokenBaseProps {
value: Token
onClick: (value: Token) => void
}
export default function TokenBase({ value, onClick }: TokenBaseProps) {
return (
<TokenButton onClick={() => onClick(value)}>
<ThemedText.ButtonMedium>
<Row gap={0.5}>
<TokenImg token={value} size={1.5} />
{value.symbol}
</Row>
</ThemedText.ButtonMedium>
</TokenButton>
)
}

@ -0,0 +1,54 @@
import { Trans } from '@lingui/macro'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import Button from '../Button'
import Row from '../Row'
import TokenImg from '../TokenImg'
const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
border-radius: ${({ theme }) => theme.borderRadius}em;
padding: 0.25em;
padding-left: ${({ empty }) => (empty ? 0.75 : 0.25)}em;
:disabled {
// prevents border from expanding the button's box size
padding: calc(0.25em - 1px);
padding-left: calc(${({ empty }) => (empty ? 0.75 : 0.25)}em - 1px);
}
`
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
height: 1.2em;
max-width: ${({ collapsed }) => (collapsed ? '1.2' : '8.2')}em;
overflow-x: hidden;
transition: max-width 0.25s linear;
`
interface TokenButtonProps {
value?: Token
collapsed: boolean
disabled?: boolean
onClick: () => void
}
export default function TokenButton({ value, collapsed, disabled, onClick }: TokenButtonProps) {
return (
<StyledTokenButton onClick={onClick} empty={!value} color={value ? 'interactive' : 'accent'} disabled={disabled}>
<ThemedText.ButtonLarge color="onInteractive">
<TokenButtonRow gap={0.4} collapsed={Boolean(value) && collapsed}>
{value ? (
<>
<TokenImg token={value} size={1.2} />
{value.symbol}
</>
) : (
<Trans>Select a token</Trans>
)}
<ChevronDown color="onInteractive" strokeWidth={3} />
</TokenButtonRow>
</ThemedText.ButtonLarge>
</StyledTokenButton>
)
}

@ -0,0 +1,232 @@
import useNativeEvent from 'lib/hooks/useNativeEvent'
import useScrollbar from 'lib/hooks/useScrollbar'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import {
ComponentClass,
CSSProperties,
forwardRef,
KeyboardEvent,
memo,
SyntheticEvent,
useCallback,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { areEqual, FixedSizeList, FixedSizeListProps } from 'react-window'
import invariant from 'tiny-invariant'
import { BaseButton } from '../Button'
import Column from '../Column'
import Row from '../Row'
import TokenImg from '../TokenImg'
const TokenButton = styled(BaseButton)`
border-radius: 0;
outline: none;
padding: 0.5em 0.75em;
`
const ITEM_SIZE = 56
type ItemData = Token[]
interface FixedSizeTokenList extends FixedSizeList<ItemData>, ComponentClass<FixedSizeListProps<ItemData>> {}
const TokenList = styled(FixedSizeList as unknown as FixedSizeTokenList)<{
hover: number
scrollbar?: ReturnType<typeof useScrollbar>
}>`
${TokenButton}[data-index='${({ hover }) => hover}'] {
background-color: ${({ theme }) => theme.onHover(theme.module)};
}
${({ scrollbar }) => scrollbar}
overscroll-behavior: none; // prevent Firefox's bouncy overscroll effect (because it does not trigger the scroll handler)
`
const OnHover = styled.div<{ hover: number }>`
background-color: ${({ theme }) => theme.onHover(theme.module)};
height: ${ITEM_SIZE}px;
left: 0;
position: absolute;
top: ${({ hover }) => hover * ITEM_SIZE}px;
width: 100%;
`
interface TokenOptionProps {
index: number
value: Token
style: CSSProperties
}
interface BubbledEvent extends SyntheticEvent {
index?: number
token?: Token
ref?: HTMLButtonElement
}
function TokenOption({ index, value, style }: TokenOptionProps) {
const ref = useRef<HTMLButtonElement>(null)
// Annotate the event to be handled later instead of passing in handlers to avoid rerenders.
// This prevents token logos from reloading and flashing on the screen.
const onEvent = (e: BubbledEvent) => {
e.index = index
e.token = value
e.ref = ref.current ?? undefined
}
return (
<TokenButton
data-index={index}
style={style}
onClick={onEvent}
onBlur={onEvent}
onFocus={onEvent}
onMouseMove={onEvent}
onKeyDown={onEvent}
ref={ref}
>
<ThemedText.Body1>
<Row>
<Row gap={0.5}>
<TokenImg token={value} size={1.5} />
<Column flex gap={0.125} align="flex-start">
<ThemedText.Subhead1>{value.symbol}</ThemedText.Subhead1>
<ThemedText.Caption color="secondary">{value.name}</ThemedText.Caption>
</Column>
</Row>
1.234
</Row>
</ThemedText.Body1>
</TokenButton>
)
}
const itemKey = (index: number, tokens: ItemData) => tokens[index]?.address
const ItemRow = memo(function ItemRow({
data: tokens,
index,
style,
}: {
data: ItemData
index: number
style: CSSProperties
}) {
return <TokenOption index={index} value={tokens[index]} style={style} />
},
areEqual)
interface TokenOptionsHandle {
onKeyDown: (e: KeyboardEvent) => void
blur: () => void
}
interface TokenOptionsProps {
tokens: Token[]
onSelect: (token: Token) => void
}
const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function TokenOptions(
{ tokens, onSelect }: TokenOptionsProps,
ref
) {
const [focused, setFocused] = useState(false)
const [hover, setHover] = useState(-1)
useEffect(() => setHover(-1), [tokens])
const list = useRef<FixedSizeList>(null)
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (e.key === 'ArrowDown' && hover < tokens.length - 1) {
scrollTo(hover + 1)
} else if (e.key === 'ArrowUp' && hover > 0) {
scrollTo(hover - 1)
} else if (e.key === 'ArrowUp' && hover === -1) {
scrollTo(tokens.length - 1)
}
e.preventDefault()
}
if (e.key === 'Enter' && hover) {
onSelect(tokens[hover])
}
function scrollTo(index: number) {
list.current?.scrollToItem(index)
setHover(index)
}
},
[hover, onSelect, tokens]
)
const blur = useCallback(() => setHover(-1), [])
useImperativeHandle(ref, () => ({ onKeyDown, blur }), [blur, onKeyDown])
const onClick = useCallback(({ token }: BubbledEvent) => token && onSelect(token), [onSelect])
const onFocus = useCallback(({ index }: BubbledEvent) => {
if (index !== undefined) {
setHover(index)
setFocused(true)
}
}, [])
const onBlur = useCallback(() => setFocused(false), [])
const onMouseMove = useCallback(
({ index, ref }: BubbledEvent) => {
if (index !== undefined) {
setHover(index)
if (focused) {
ref?.focus()
}
}
},
[focused]
)
const [element, setElement] = useState<HTMLElement | null>(null)
const scrollbar = useScrollbar(element, { padded: true })
const onHover = useRef<HTMLDivElement>(null)
// use native onscroll handler to capture Safari's bouncy overscroll effect
useNativeEvent(element, 'scroll', (e) => {
invariant(element)
if (onHover.current) {
// must be set synchronously to avoid jank (avoiding useState)
onHover.current.style.marginTop = `${-element.scrollTop}px`
}
})
return (
<Column
align="unset"
grow
onKeyDown={onKeyDown}
onClick={onClick}
onBlur={onBlur}
onFocus={onFocus}
onMouseMove={onMouseMove}
style={{ overflow: 'hidden' }}
>
{/* OnHover is a workaround to Safari's incorrect (overflow: overlay) implementation */}
<OnHover hover={hover} ref={onHover} />
<AutoSizer disableWidth>
{({ height }) => (
<TokenList
hover={hover}
height={height}
width="100%"
itemCount={tokens.length}
itemData={tokens}
itemKey={itemKey}
itemSize={ITEM_SIZE}
className="scrollbar"
ref={list}
outerRef={setElement}
scrollbar={scrollbar}
>
{ItemRow}
</TokenList>
)}
</AutoSizer>
</Column>
)
})
export default TokenOptions

@ -0,0 +1,92 @@
import { t, Trans } from '@lingui/macro'
import { DAI, ETH, UNI, USDC } from 'lib/mocks'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { ElementRef, useCallback, useEffect, useRef, useState } from 'react'
import Column from '../Column'
import Dialog, { Header } from '../Dialog'
import { inputCss, StringInput } from '../Input'
import Row from '../Row'
import Rule from '../Rule'
import TokenBase from './TokenBase'
import TokenButton from './TokenButton'
import TokenOptions from './TokenOptions'
// TODO: integrate with web3-react context
const mockTokens = [DAI, ETH, UNI, USDC]
const SearchInput = styled(StringInput)`
${inputCss}
`
export function TokenSelectDialog({ onSelect }: { onSelect: (token: Token) => void }) {
const baseTokens = [DAI, ETH, UNI, USDC]
const tokens = mockTokens
const [search, setSearch] = useState('')
const input = useRef<HTMLInputElement>(null)
useEffect(() => input.current?.focus(), [input])
const [options, setOptions] = useState<ElementRef<typeof TokenOptions> | null>(null)
return (
<>
<Header title={<Trans>Select a token</Trans>} />
<Column gap={0.75}>
<Row pad={0.75} grow>
<ThemedText.Body1>
<SearchInput
value={search}
onChange={setSearch}
placeholder={t`Search by token name or address`}
onKeyDown={options?.onKeyDown}
onBlur={options?.blur}
ref={input}
/>
</ThemedText.Body1>
</Row>
{Boolean(baseTokens.length) && (
<>
<Row pad={0.75} gap={0.25} justify="flex-start" flex>
{baseTokens.map((token) => (
<TokenBase value={token} onClick={onSelect} key={token.address} />
))}
</Row>
<Rule padded />
</>
)}
</Column>
<TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} />
</>
)
}
interface TokenSelectProps {
value?: Token
collapsed: boolean
disabled?: boolean
onSelect: (value: Token) => void
}
export default function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
const [open, setOpen] = useState(false)
const selectAndClose = useCallback(
(value: Token) => {
onSelect(value)
setOpen(false)
},
[onSelect, setOpen]
)
return (
<>
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={() => setOpen(true)} />
{open && (
<Dialog color="module" onClose={() => setOpen(false)}>
<TokenSelectDialog onSelect={selectAndClose} />
</Dialog>
)}
</>
)
}

@ -0,0 +1,40 @@
import { Placement } from '@popperjs/core'
import { HelpCircle, Icon } from 'lib/icons'
import styled from 'lib/theme'
import { ReactNode, useState } from 'react'
import { IconButton } from './Button'
import Popover from './Popover'
const IconTooltip = styled(IconButton)`
:hover {
cursor: help;
}
`
interface TooltipInterface {
icon?: Icon
children: ReactNode
placement: Placement
contained?: true
}
export default function Tooltip({
icon: Icon = HelpCircle,
children,
placement = 'auto',
contained,
}: TooltipInterface) {
const [show, setShow] = useState(false)
return (
<Popover content={children} show={show} placement={placement} contained={contained}>
<IconTooltip
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
onFocus={() => setShow(true)}
onBlur={() => setShow(false)}
icon={Icon}
/>
</Popover>
)
}

@ -0,0 +1,15 @@
import { CreditCard } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import Row from './Row'
export default function Wallet({ disabled }: { disabled?: boolean }) {
return disabled ? (
<ThemedText.Caption color="secondary">
<Row gap={0.25}>
<CreditCard />
Connect wallet to swap
</Row>
</ThemedText.Caption>
) : null
}

@ -0,0 +1,41 @@
import { SetStateAction } from 'jotai'
import { RESET, useUpdateAtom } from 'jotai/utils'
import { injectedAtom, networkAtom } from 'lib/state'
import { ReactNode, useEffect, useMemo } from 'react'
import { initializeConnector, Web3ReactHooks } from 'widgets-web3-react/core'
import { EIP1193 } from 'widgets-web3-react/eip1193'
import { Network } from 'widgets-web3-react/network'
import { Actions, Connector, Provider as EthProvider } from 'widgets-web3-react/types'
interface Web3ProviderProps {
jsonRpcEndpoint?: string
provider?: EthProvider
children: ReactNode
}
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
Connector: T,
initializer: I | undefined,
setContext: (update: typeof RESET | SetStateAction<[Connector, Web3ReactHooks]>) => void
) {
return useEffect(() => {
if (initializer) {
const [connector, hooks] = initializeConnector((actions) => new Connector(actions, initializer))
setContext([connector, hooks])
} else {
setContext(RESET)
}
}, [Connector, initializer, setContext])
}
export default function Web3Provider({ jsonRpcEndpoint, provider, children }: Web3ProviderProps) {
const setNetwork = useUpdateAtom(networkAtom)
// TODO(zzmp): Network should take a string, not a urlMap.
const urlMap = useMemo(() => jsonRpcEndpoint && { 1: jsonRpcEndpoint }, [jsonRpcEndpoint])
useConnector(Network, urlMap, setNetwork)
const setInjected = useUpdateAtom(injectedAtom)
useConnector(EIP1193, provider, setInjected)
return <>{children}</>
}

@ -0,0 +1,111 @@
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { Provider as I18nProvider } from 'lib/i18n'
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
import { ReactNode, StrictMode, useRef } from 'react'
import { Provider as EthProvider } from 'widgets-web3-react/types'
import { Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import Web3Provider from './Web3Provider'
const slideDown = keyframes`
to {
top: calc(100% - 0.25em);
}
`
const slideUp = keyframes`
from {
top: calc(100% - 0.25em);
}
`
const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
background-color: ${({ theme }) => theme.container};
border-radius: ${({ theme }) => theme.borderRadius}em;
color: ${({ theme }) => theme.primary};
display: flex;
flex-direction: column;
font-feature-settings: 'ss01' on, 'ss02' on, 'cv01' on, 'cv03' on;
font-size: 16px;
font-smooth: always;
font-variant: none;
height: 348px;
min-width: 300px;
overflow-y: hidden;
padding: 0.25em;
position: relative;
width: ${({ width }) => width && (isNaN(Number(width)) ? width : `${width}px`)};
@supports (overflow: clip) {
overflow-y: clip;
}
* {
box-sizing: border-box;
font-family: ${({ theme }) => theme.fontFamily};
user-select: none;
@supports (font-variation-settings: normal) {
font-family: ${({ theme }) => theme.fontFamilyVariable};
}
}
.dialog {
animation: ${slideUp} 0.25s ease-in-out;
}
.dialog.${UNMOUNTING} {
animation: ${slideDown} 0.25s ease-in-out;
}
`
export interface WidgetProps {
children: ReactNode
theme?: Theme
locale?: SupportedLocale
provider?: EthProvider
jsonRpcEndpoint?: string
width?: string | number
dialog?: HTMLElement | null
className?: string
onError?: ErrorHandler
}
export default function Widget({
children,
theme,
locale = DEFAULT_LOCALE,
provider,
jsonRpcEndpoint,
width = 360,
dialog,
className,
onError,
}: WidgetProps) {
const wrapper = useRef<HTMLDivElement>(null)
return (
<StrictMode>
<I18nProvider locale={locale}>
<ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className} ref={wrapper}>
<DialogProvider value={dialog || wrapper.current}>
<ErrorBoundary onError={onError}>
<AtomProvider>
<Web3Provider provider={provider} jsonRpcEndpoint={jsonRpcEndpoint}>
{children}
</Web3Provider>
</AtomProvider>
</ErrorBoundary>
</DialogProvider>
</WidgetWrapper>
</ThemeProvider>
</I18nProvider>
</StrictMode>
)
}

@ -0,0 +1,16 @@
import { JSXElementConstructor, ReactElement } from 'react'
import Row from './components/Row'
import Widget from './cosmos/components/Widget'
export default function WidgetDecorator({
children,
}: {
children: ReactElement<any, string | JSXElementConstructor<any>>
}) {
return (
<Row justify="center">
<Widget>{children}</Widget>
</Row>
)
}

@ -0,0 +1,55 @@
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from 'constants/locales'
import Widget from 'lib/components/Widget'
import { darkTheme, defaultTheme, lightTheme } from 'lib/theme'
import { ReactNode, useEffect, useMemo } from 'react'
import { useSelect, useValue } from 'react-cosmos/fixture'
import { metaMask } from '../connectors/metaMask'
import { URLS } from '../connectors/network'
export default function Wrapper({ children }: { children: ReactNode }) {
const [width] = useValue('width', { defaultValue: 360 })
const [locale] = useSelect('locale', { defaultValue: DEFAULT_LOCALE, options: ['pseudo', ...SUPPORTED_LOCALES] })
const [darkMode] = useValue('dark mode', { defaultValue: false })
const [theme, setTheme] = useValue('theme', { defaultValue: { ...defaultTheme, ...lightTheme } })
useEffect(() => {
setTheme({ ...defaultTheme, ...(darkMode ? darkTheme : lightTheme) })
// cosmos does not maintain referential equality for setters
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [darkMode])
const NO_JSON_RPC = 'None'
const [jsonRpcEndpoint] = useSelect('JSON-RPC', {
defaultValue: URLS[SupportedChainId.MAINNET][0] || NO_JSON_RPC,
options: [NO_JSON_RPC, ...Object.values(URLS).flat()],
})
const NO_PROVIDER = 'None'
const META_MASK = 'MetaMask'
const [providerType] = useSelect('Provider', {
defaultValue: NO_PROVIDER,
options: [NO_PROVIDER, META_MASK],
})
const provider = useMemo(() => {
switch (providerType) {
case META_MASK:
metaMask.activate()
return metaMask.provider
default:
return undefined
}
}, [providerType])
return (
<Widget
width={width}
theme={theme}
locale={locale}
jsonRpcEndpoint={jsonRpcEndpoint === NO_JSON_RPC ? undefined : jsonRpcEndpoint}
provider={provider}
>
{children}
</Widget>
)
}

@ -0,0 +1,4 @@
import { initializeConnector } from 'widgets-web3-react/core'
import { MetaMask } from 'widgets-web3-react/metamask'
export const [metaMask, hooks] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))

@ -0,0 +1,14 @@
import { SupportedChainId } from 'constants/chains'
const ALCHEMY_KEY = '-mzwnEVG3Ssm75WVbmsEpYiekfTF3W1z'
const alchemyUrl = (network: string) => `https://${network}.alchemyapi.io/v2/${ALCHEMY_KEY}`
export const URLS = {
[SupportedChainId.MAINNET]: [alchemyUrl('eth-mainnet')],
[SupportedChainId.ROPSTEN]: [alchemyUrl('eth-ropsten')],
[SupportedChainId.RINKEBY]: [alchemyUrl('eth-rinkeby')],
[SupportedChainId.GOERLI]: [alchemyUrl('eth-goerli')],
[SupportedChainId.KOVAN]: [alchemyUrl('eth-kovan')],
[SupportedChainId.OPTIMISM]: [alchemyUrl('optimism-mainnet')],
[SupportedChainId.ARBITRUM_ONE]: [alchemyUrl('arbitrum-mainnet')],
}

11
src/lib/ethereum.d.ts vendored Normal file

@ -0,0 +1,11 @@
export interface EthereumProvider {
on?: (...args: any[]) => void
removeListener?: (...args: any[]) => void
autoRefreshOnNetworkChange?: boolean
}
declare global {
interface Window {
ethereum?: EthereumProvider
}
}

@ -0,0 +1,8 @@
import { multicall } from 'lib/state'
export const {
useMultipleContractSingleData,
useSingleContractMultipleData,
useSingleContractWithCallData,
useSingleCallResult,
} = multicall.hooks

@ -0,0 +1,19 @@
import { useAtomValue } from 'jotai/utils'
import { injectedAtom, networkAtom, Web3ReactState } from 'lib/state'
import { Web3ReactHooks } from 'widgets-web3-react/core'
export function useActiveWeb3ReactState(): Web3ReactState {
const injected = useAtomValue(injectedAtom)
const network = useAtomValue(networkAtom)
return injected[1].useIsActive() ? injected : network
}
export function useActiveWeb3ReactHooks(): Web3ReactHooks {
const [, hooks] = useActiveWeb3ReactState()
return hooks
}
export default function useActiveWeb3React() {
const { useProvider, useWeb3React } = useActiveWeb3ReactHooks()
return useWeb3React(useProvider())
}

80
src/lib/hooks/useColor.ts Normal file

@ -0,0 +1,80 @@
import { useTheme } from 'lib/theme'
import { Token } from 'lib/types'
import uriToHttp from 'lib/utils/uriToHttp'
import Vibrant from 'node-vibrant/lib/bundle'
import { useLayoutEffect, useState } from 'react'
const colors = new Map<string, string>()
function UriForEthToken(address: string) {
return `https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/${address}/logo.png?color`
}
/**
* Extracts the prominent color from a token.
* NB: If cached, this function returns synchronously; using a callback allows sync or async returns.
*/
async function getColorFromToken(token: Token, cb: (color: string | undefined) => void = () => void 0) {
const { address, chainId, logoURI } = token
// Color extraction must use a CORS-compatible resource, but the resource is already cached.
// Add a dummy parameter to force a different browser resource cache entry.
// Without this, color extraction prevents resource caching.
const uri = uriToHttp(logoURI)[0] + '?color'
let color = colors.get(uri)
if (color) {
return cb(color)
}
color = await getColorFromUriPath(uri)
if (!color && chainId === 1) {
const fallbackUri = UriForEthToken(address)
color = await getColorFromUriPath(fallbackUri)
}
if (color) {
colors.set(uri, color)
}
return cb(color)
}
async function getColorFromUriPath(uri: string): Promise<string | undefined> {
try {
const palette = await Vibrant.from(uri).getPalette()
return palette.Vibrant?.hex
} catch {}
return
}
export function usePrefetchColor(token?: Token) {
const theme = useTheme()
if (theme.tokenColorExtraction && token) {
getColorFromToken(token)
}
}
export default function useColor(token?: Token) {
const [color, setColor] = useState<string | undefined>(undefined)
const theme = useTheme()
useLayoutEffect(() => {
let stale = false
if (theme.tokenColorExtraction && token) {
getColorFromToken(token, (color) => {
if (!stale && color) {
setColor(color)
}
})
}
return () => {
stale = true
setColor(undefined)
}
}, [token, theme])
return color
}

@ -1,5 +1,11 @@
import { useEffect, useRef } from 'react'
/**
* Invokes callback repeatedly over an interval defined by the delay
* @param callback
* @param delay if null, the callback will not be invoked
* @param leading if true, the callback will be invoked immediately (on the leading edge); otherwise, it will be invoked after delay
*/
export default function useInterval(callback: () => void, delay: null | number, leading = true) {
const savedCallback = useRef<() => void>()
@ -11,7 +17,7 @@ export default function useInterval(callback: () => void, delay: null | number,
// Set up the interval.
useEffect(() => {
function tick() {
const current = savedCallback.current
const { current } = savedCallback
current && current()
}
@ -20,6 +26,6 @@ export default function useInterval(callback: () => void, delay: null | number,
const id = setInterval(tick, delay)
return () => clearInterval(id)
}
return undefined
return
}, [delay, leading])
}

@ -0,0 +1,11 @@
import { useEffect } from 'react'
export default function useNativeEvent(
element: HTMLElement | null,
...eventListener: Parameters<HTMLElement['addEventListener']>
) {
useEffect(() => {
element?.addEventListener(...eventListener)
return () => element?.removeEventListener(...eventListener)
}, [element, eventListener])
}

@ -0,0 +1,64 @@
import { css } from 'lib/theme'
import { useEffect, useMemo, useState } from 'react'
import useNativeEvent from './useNativeEvent'
const overflowCss = css`
overflow-y: scroll;
`
/** Customizes the scrollbar for vertical overflow. */
const scrollbarCss = (padded: boolean) => css`
overflow-y: scroll;
::-webkit-scrollbar {
width: 1.25em;
}
::-webkit-scrollbar-thumb {
background: radial-gradient(
closest-corner at 0.25em 0.25em,
${({ theme }) => theme.interactive} 0.25em,
transparent 0.25em
),
linear-gradient(
to bottom,
#ffffff00 0.25em,
${({ theme }) => theme.interactive} 0.25em,
${({ theme }) => theme.interactive} calc(100% - 0.25em),
#ffffff00 calc(100% - 0.25em)
),
radial-gradient(
closest-corner at 0.25em calc(100% - 0.25em),
${({ theme }) => theme.interactive} 0.25em,
#ffffff00 0.25em
);
background-clip: padding-box;
border: none;
${padded ? 'border-right' : 'border-left'}: 0.75em solid transparent;
}
@supports not selector(::-webkit-scrollbar-thumb) {
scrollbar-color: ${({ theme }) => theme.interactive} transparent;
}
`
interface ScrollbarOptions {
padded?: boolean
}
export default function useScrollbar(element: HTMLElement | null, { padded = false }: ScrollbarOptions = {}) {
const [overflow, setOverflow] = useState(true)
useEffect(() => {
setOverflow(hasOverflow(element))
}, [element])
useNativeEvent(element, 'transitionend', () => setOverflow(hasOverflow(element)))
return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded])
function hasOverflow(element: HTMLElement | null) {
if (!element) {
return true
}
return element.scrollHeight > element.clientHeight
}
}

@ -0,0 +1,33 @@
import { RefObject, useEffect } from 'react'
export const UNMOUNTING = 'unmounting'
/**
* Delays a node's unmounting so that an animation may be applied.
* An animation *must* be applied, or the node will not unmount.
*/
export default function useUnmount(node: RefObject<HTMLElement>) {
useEffect(() => {
const current = node.current
const parent = current?.parentElement
const removeChild = parent?.removeChild
if (parent && removeChild) {
parent.removeChild = function <T extends Node>(child: T) {
if ((child as Node) === current) {
current.classList.add(UNMOUNTING)
current.onanimationend = () => {
removeChild.call(parent, child)
}
return child
} else {
return removeChild.call(parent, child) as T
}
}
}
return () => {
if (parent && removeChild) {
parent.removeChild = removeChild
}
}
}, [node])
}

109
src/lib/i18n.tsx Normal file

@ -0,0 +1,109 @@
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { DEFAULT_CATALOG, DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import {
af,
ar,
ca,
cs,
da,
de,
el,
en,
es,
fi,
fr,
he,
hu,
id,
it,
ja,
ko,
nl,
no,
pl,
pt,
ro,
ru,
sr,
sv,
sw,
tr,
uk,
vi,
zh,
} from 'make-plural/plurals'
import { PluralCategory } from 'make-plural/plurals'
import { ReactNode, useEffect } from 'react'
type LocalePlural = {
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
}
const plurals: LocalePlural = {
'af-ZA': af,
'ar-SA': ar,
'ca-ES': ca,
'cs-CZ': cs,
'da-DK': da,
'de-DE': de,
'el-GR': el,
'en-US': en,
'es-ES': es,
'fi-FI': fi,
'fr-FR': fr,
'he-IL': he,
'hu-HU': hu,
'id-ID': id,
'it-IT': it,
'ja-JP': ja,
'ko-KR': ko,
'nl-NL': nl,
'no-NO': no,
'pl-PL': pl,
'pt-BR': pt,
'pt-PT': pt,
'ro-RO': ro,
'ru-RU': ru,
'sr-SP': sr,
'sv-SE': sv,
'sw-TZ': sw,
'tr-TR': tr,
'uk-UA': uk,
'vi-VN': vi,
'zh-CN': zh,
'zh-TW': zh,
pseudo: en,
}
export async function dynamicActivate(locale: SupportedLocale) {
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
// There are no default messages in production; instead, bundle the default to save a network request:
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
const catalog = locale === DEFAULT_LOCALE ? DEFAULT_CATALOG : await import(`locales/${locale}`)
i18n.load(locale, catalog.messages)
i18n.activate(locale)
}
interface ProviderProps {
locale: SupportedLocale
forceRenderAfterLocaleChange?: boolean
onActivate?: (locale: SupportedLocale) => void
children: ReactNode
}
export function Provider({ locale, forceRenderAfterLocaleChange = true, onActivate, children }: ProviderProps) {
useEffect(() => {
dynamicActivate(locale)
.then(() => onActivate?.(locale))
.catch((error) => {
console.error('Failed to activate locale', locale, error)
})
}, [locale, onActivate])
return (
<I18nProvider forceRenderOnLocaleChange={forceRenderAfterLocaleChange} i18n={i18n}>
{children}
</I18nProvider>
)
}

123
src/lib/icons/index.tsx Normal file

@ -0,0 +1,123 @@
/* eslint-disable no-restricted-imports */
import CheckIcon from 'lib/assets/svg/Check'
import ExpandoIcon from 'lib/assets/svg/Expando'
import LogoIcon from 'lib/assets/svg/Logo'
import SpinnerIcon from 'lib/assets/svg/Spinner'
import styled, { Color, css, keyframes } from 'lib/theme'
import { FunctionComponent, SVGProps } from 'react'
import { Icon as FeatherIcon } from 'react-feather'
import {
AlertTriangle as AlertTriangleIcon,
ArrowDown as ArrowDownIcon,
ArrowRight as ArrowRightIcon,
ArrowUp as ArrowUpIcon,
CheckCircle as CheckCircleIcon,
ChevronDown as ChevronDownIcon,
Clock as ClockIcon,
CreditCard as CreditCardIcon,
HelpCircle as HelpCircleIcon,
Info as InfoIcon,
Settings as SettingsIcon,
Trash2 as Trash2Icon,
X as XIcon,
} from 'react-feather'
type SVGIcon = FunctionComponent<SVGProps<SVGSVGElement>>
function icon(Icon: FeatherIcon | SVGIcon) {
return styled(Icon)<{ color?: Color }>`
clip-path: stroke-box;
height: 1em;
stroke: ${({ color = 'currentColor', theme }) => theme[color]};
width: 1em;
`
}
export type Icon = ReturnType<typeof icon>
export const largeIconCss = css<{ iconSize: number }>`
display: flex;
svg {
align-self: center;
height: ${({ iconSize }) => iconSize}em;
width: ${({ iconSize }) => iconSize}em;
}
`
const LargeWrapper = styled.div<{ iconSize: number }>`
height: 1em;
${largeIconCss}
`
interface LargeIconProps {
icon: Icon
color?: Color
size?: number
className?: string
}
export function LargeIcon({ icon: Icon, color, size = 1.2, className }: LargeIconProps) {
return (
<LargeWrapper color={color} iconSize={size} className={className}>
<Icon color={color} />
</LargeWrapper>
)
}
export const AlertTriangle = icon(AlertTriangleIcon)
export const ArrowDown = icon(ArrowDownIcon)
export const ArrowRight = icon(ArrowRightIcon)
export const ArrowUp = icon(ArrowUpIcon)
export const CheckCircle = icon(CheckCircleIcon)
export const ChevronDown = icon(ChevronDownIcon)
export const Clock = icon(ClockIcon)
export const CreditCard = icon(CreditCardIcon)
export const HelpCircle = icon(HelpCircleIcon)
export const Info = icon(InfoIcon)
export const Settings = icon(SettingsIcon)
export const Trash2 = icon(Trash2Icon)
export const X = icon(XIcon)
export const Check = styled(icon(CheckIcon))`
circle {
fill: ${({ theme }) => theme.active};
stroke: none;
}
`
export const Expando = styled(icon(ExpandoIcon))<{ open: boolean }>`
path {
transition: transform 0.25s ease-in-out;
will-change: transform;
&:first-child {
transform: ${({ open }) => open && 'translateX(-25%)'};
}
&:last-child {
transform: ${({ open }) => open && 'translateX(25%)'};
}
}
`
export const Logo = styled(icon(LogoIcon))`
fill: ${({ theme }) => theme.secondary};
stroke: none;
`
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
export const Spinner = styled(icon(SpinnerIcon))<{ color?: Color }>`
animation: 2s ${rotate} linear infinite;
stroke: ${({ color = 'active', theme }) => theme[color]};
stroke-linecap: round;
stroke-width: 2;
`

@ -1,7 +0,0 @@
import './'
describe('lib', () => {
it('exists', () => {
expect(true).toBeTruthy()
})
})

@ -1 +1,12 @@
export {}
import Swap from './components/Swap'
import Widget, { WidgetProps } from './components/Widget'
type SwapWidgetProps = Omit<WidgetProps, 'children'>
export function SwapWidget(props: SwapWidgetProps) {
return (
<Widget {...props}>
<Swap />
</Widget>
)
}

33
src/lib/mocks.ts Normal file

@ -0,0 +1,33 @@
export const USDC = {
name: 'USDCoin',
symbol: 'USDC',
chainId: 1,
decimals: 18,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
logoURI:
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
}
export const DAI = {
name: 'DaiStablecoin',
symbol: 'DAI',
chainId: 1,
decimals: 18,
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
logoURI: 'https://gemini.com/images/currencies/icons/default/dai.svg',
}
export const ETH = {
name: 'Ether',
symbol: 'ETH',
chainId: 1,
decimals: 18,
address: 'ETHER',
logoURI: 'https://raw.githubusercontent.com/Uniswap/interface/main/src/assets/images/ethereum-logo.png',
}
export const UNI = {
name: 'Uniswap',
symbol: 'UNI',
chainId: 1,
decimals: 18,
address: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984',
logoURI: 'https://gemini.com/images/currencies/icons/default/uni.svg',
}

26
src/lib/state/index.ts Normal file

@ -0,0 +1,26 @@
import { createMulticall } from '@uniswap/redux-multicall'
import { atomWithStore } from 'jotai/redux'
import { atomWithDefault } from 'jotai/utils'
import { createStore } from 'redux'
import { Web3ReactHooks } from 'widgets-web3-react/core'
import { initializeConnector } from 'widgets-web3-react/core'
import { Connector } from 'widgets-web3-react/types'
// TODO(zzmp): EmptyConnector singleton should come from 'widgets-web3-react/empty'
const EMPTY_CONNECTOR = initializeConnector<Connector>(
(actions) =>
new (class EmptyConnector extends Connector {
activate() {
void 0
}
})(actions)
)
export type Web3ReactState = [Connector, Web3ReactHooks]
export const networkAtom = atomWithDefault<Web3ReactState>(() => EMPTY_CONNECTOR)
export const injectedAtom = atomWithDefault<Web3ReactState>(() => EMPTY_CONNECTOR)
export const multicall = createMulticall()
const multicallStore = createStore(multicall.reducer)
export const multicallStoreAtom = atomWithStore(multicallStore)

79
src/lib/theme/dynamic.tsx Normal file

@ -0,0 +1,79 @@
import { darken, lighten, opacify, transparentize } from 'polished'
import { readableColor } from 'polished'
import { ReactNode, useMemo } from 'react'
import { hex } from 'wcag-contrast'
import { ThemedProvider, useTheme } from './styled'
import { Colors, ComputedTheme } from './theme'
type DynamicColors = Pick<Colors, 'interactive' | 'outline' | 'primary' | 'secondary' | 'onInteractive'>
const black = '#000000'
const white = '#FFFFFF'
const light: DynamicColors = {
// surface
interactive: transparentize(1 - 0.54, black),
outline: transparentize(1 - 0.24, black),
// text
primary: black,
secondary: transparentize(1 - 0.64, black),
onInteractive: white,
}
const dark: DynamicColors = {
// surface
interactive: transparentize(1 - 0.48, white),
outline: transparentize(1 - 0.12, white),
// text
primary: white,
secondary: transparentize(1 - 0.6, white),
onInteractive: black,
}
export function getDynamicTheme(theme: ComputedTheme, color: string): ComputedTheme {
const colors = { light, dark }[readableColor(color, 'light', 'dark', false) as 'light' | 'dark']
return {
...theme,
...colors,
module: color,
onHover: (color: string) => (color === colors.primary ? transparentize(0.4, colors.primary) : opacify(0.25, color)),
}
}
function getAccessibleColor(theme: ComputedTheme, color: string) {
const dynamic = getDynamicTheme(theme, color)
let { primary } = dynamic
let AAscore = hex(color, primary)
const contrastify = hex(color, '#000') > hex(color, '#fff') ? darken : lighten
while (AAscore < 3) {
color = contrastify(0.005, color)
primary = getDynamicTheme(theme, color).primary
AAscore = hex(color, primary)
}
return color
}
interface DynamicThemeProviderProps {
color?: string
children: ReactNode
}
export function DynamicThemeProvider({ color, children }: DynamicThemeProviderProps) {
const theme = useTheme()
const value = useMemo(() => {
if (!color) {
return theme
}
const accessibleColor = getAccessibleColor(theme, color)
return getDynamicTheme(theme, accessibleColor)
}, [theme, color])
return (
<ThemedProvider theme={value}>
<div style={{ color: value.primary }}>{children}</div>
</ThemedProvider>
)
}

118
src/lib/theme/index.tsx Normal file

@ -0,0 +1,118 @@
import 'lib/assets/fonts/index.css'
import { mix, transparentize } from 'polished'
import { createContext, ReactNode, useContext, useMemo, useState } from 'react'
import styled, { ThemedProvider } from './styled'
import { Colors, ComputedTheme, Theme } from './theme'
export type { Color, Colors, Theme } from './theme'
export default styled
export * from './dynamic'
export * from './layer'
export * from './styled'
export * as ThemedText from './type'
export const lightTheme: Colors = {
// surface
accent: '#FF007A',
container: '#F7F8FA',
module: '#E2E3E9',
interactive: '#CED0D9',
outline: '#C3C5CB',
dialog: '#FFFFFF',
// text
primary: '#000000',
secondary: '#565A69',
hint: '#888D9B',
onInteractive: '#000000',
// state
active: '#2172E5',
success: '#27AE60',
warning: '#F3B71E',
error: '#FD4040',
currentColor: 'currentColor',
}
export const darkTheme: Colors = {
// surface
accent: '#2172E5',
container: '#191B1F',
module: '#2C2F36',
interactive: '#40444F',
outline: '#565A69',
dialog: '#000000',
// text
primary: '#FFFFFF',
secondary: '#888D9B',
hint: '#6C7284',
onInteractive: '#FFFFFF',
// state
active: '#2172E5',
success: '#27AE60',
warning: '#F3B71E',
error: '#FD4040',
currentColor: 'currentColor',
}
export const defaultTheme = {
borderRadius: 1,
fontFamily: '"Inter", sans-serif',
fontFamilyVariable: '"InterVariable", sans-serif',
fontFamilyCode: 'IBM Plex Mono',
tokenColorExtraction: true,
...lightTheme,
}
export function useSystemTheme() {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
const [systemTheme, setSystemTheme] = useState(prefersDark.matches ? darkTheme : lightTheme)
prefersDark.addEventListener('change', (e) => {
setSystemTheme(e.matches ? darkTheme : lightTheme)
})
return systemTheme
}
const ThemeContext = createContext<ComputedTheme>(toComputedTheme(defaultTheme))
interface ThemeProviderProps {
theme?: Theme
children: ReactNode
}
export function ThemeProvider({ theme, children }: ThemeProviderProps) {
const contextTheme = useContext(ThemeContext)
const value = useMemo(() => {
return toComputedTheme({
...contextTheme,
...theme,
} as Required<Theme>)
}, [contextTheme, theme])
return (
<ThemeContext.Provider value={value}>
<ThemedProvider theme={value}>{children}</ThemedProvider>
</ThemeContext.Provider>
)
}
function toComputedTheme(theme: Required<Theme>): ComputedTheme {
return {
...theme,
borderRadius: clamp(
Number.isFinite(theme.borderRadius) ? (theme.borderRadius as number) : theme.borderRadius ? 1 : 0
),
onHover: (color: string) =>
color === theme.primary ? transparentize(0.4, theme.primary) : mix(0.16, theme.primary, color),
}
function clamp(value: number) {
return Math.min(Math.max(value, 0), 1)
}
}

5
src/lib/theme/layer.ts Normal file

@ -0,0 +1,5 @@
export enum Layer {
OVERLAY = 100,
DIALOG = 1000,
TOOLTIP = 2000,
}

18
src/lib/theme/styled.ts Normal file

@ -0,0 +1,18 @@
/* eslint-disable no-restricted-imports */
import styled, {
css as styledCss,
keyframes as styledKeyframes,
ThemedBaseStyledInterface,
ThemedCssFunction,
ThemeProvider as StyledProvider,
ThemeProviderComponent,
useTheme as useStyled,
} from 'styled-components/macro'
import { ComputedTheme } from './theme'
export default styled as unknown as ThemedBaseStyledInterface<ComputedTheme>
export const css = styledCss as unknown as ThemedCssFunction<ComputedTheme>
export const keyframes = styledKeyframes
export const useTheme = useStyled as unknown as () => ComputedTheme
export const ThemedProvider = StyledProvider as unknown as ThemeProviderComponent<ComputedTheme>

40
src/lib/theme/theme.d.ts vendored Normal file

@ -0,0 +1,40 @@
export interface Colors {
// surface
accent: string
container: string
module: string
interactive: string
outline: string
dialog: string
// text
primary: string
secondary: string
hint: string
onInteractive: string
// state
active: string
success: string
warning: string
error: string
currentColor: 'currentColor'
}
export type Color = keyof Colors
export interface Attributes {
borderRadius: boolean | number
fontFamily: string
fontFamilyVariable: string
fontFamilyCode: string
tokenColorExtraction: boolean
}
export interface Theme extends Partial<Attributes>, Partial<Colors> {}
export interface ComputedTheme extends Omit<Attributes, 'borderRadius'>, Colors {
borderRadius: number
onHover: (color: string) => string
}

91
src/lib/theme/type.tsx Normal file

@ -0,0 +1,91 @@
import { Text, TextProps as TextPropsWithCss } from 'rebass'
import styled, { useTheme } from './styled'
import { Color } from './theme'
type TextProps = Omit<TextPropsWithCss, 'css' | 'color'> & { color?: Color }
const TextWrapper = styled(Text)<{ color?: Color }>`
color: ${({ color = 'currentColor', theme }) => theme[color as Color]};
`
const TransitionTextWrapper = styled(TextWrapper)`
transition: font-size 0.25s ease-out, line-height 0.25s ease-out;
`
export function H1(props: TextProps) {
return <TextWrapper className="headline headline-1" fontSize={36} fontWeight={400} lineHeight="36px" {...props} />
}
export function H2(props: TextProps) {
return <TextWrapper className="headline headline-2" fontSize={24} fontWeight={400} lineHeight="32px" {...props} />
}
export function H3(props: TextProps) {
return <TextWrapper className="headline headline-3" fontSize={20} fontWeight={400} lineHeight="20px" {...props} />
}
export function Subhead1(props: TextProps) {
return <TextWrapper className="subhead subhead-1" fontSize={16} fontWeight={500} lineHeight="16px" {...props} />
}
export function Subhead2(props: TextProps) {
return <TextWrapper className="subhead subhead-2" fontSize={14} fontWeight={500} lineHeight="14px" {...props} />
}
export function Body1(props: TextProps) {
return <TextWrapper className="body body-1" fontSize={16} fontWeight={400} lineHeight="24px" {...props} />
}
export function Body2(props: TextProps) {
return <TextWrapper className="body body-2" fontSize={14} fontWeight={400} lineHeight="20px" {...props} />
}
export function Caption(props: TextProps) {
return <TextWrapper className="caption" fontSize={12} fontWeight={400} lineHeight="16px" {...props} />
}
export function Badge(props: TextProps) {
return <TextWrapper className="badge" fontSize={8} fontWeight={600} lineHeight="8px" {...props} />
}
export function ButtonLarge(props: TextProps) {
return <TextWrapper className="button button-large" fontSize={20} fontWeight={500} lineHeight="20px" {...props} />
}
export function ButtonMedium(props: TextProps) {
return <TextWrapper className="button button-medium" fontSize={16} fontWeight={500} lineHeight="16px" {...props} />
}
export function ButtonSmall(props: TextProps) {
return <TextWrapper className="button button-small" fontSize={14} fontWeight={500} lineHeight="14px" {...props} />
}
export function TransitionButton(props: TextProps & { buttonSize: 'small' | 'medium' | 'large' }) {
const className = `button button-${props.buttonSize}`
const fontSize = { small: 14, medium: 16, large: 20 }[props.buttonSize]
const lineHeight = `${fontSize}px`
return (
<TransitionTextWrapper
className={className}
fontSize={fontSize}
fontWeight={500}
lineHeight={lineHeight}
{...props}
/>
)
}
export function Code(props: TextProps) {
const { fontFamilyCode } = useTheme()
return (
<TextWrapper
className="code"
fontSize={12}
fontWeight={400}
lineHeight="16px"
fontFamily={fontFamilyCode}
{...props}
/>
)
}

8
src/lib/types.d.ts vendored Normal file

@ -0,0 +1,8 @@
export interface Token {
name: string
symbol: string
chainId: number
decimals: number
address: string
logoURI: string
}

77
src/lib/utils/atoms.ts Normal file

@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/ban-types */
import { Draft } from 'immer'
import { atom, WritableAtom } from 'jotai'
import { withImmer } from 'jotai/immer'
/**
* Creates a derived atom whose value is the picked object property.
* By default, the setter acts as a primitive atom's, changing the original atom.
* A custom setter may also be passed, which uses an Immer Draft so that it may be mutated directly.
*/
export function pickAtom<Value, Key extends keyof Value & keyof Draft<Value>, Update>(
anAtom: WritableAtom<Value, Value>,
key: Key,
setter: (draft: Draft<Value>[Key], update: Update) => Draft<Value>[Key]
): WritableAtom<Value[Key], Update>
export function pickAtom<Value, Key extends keyof Value & keyof Draft<Value>, Update extends Value[Key]>(
anAtom: WritableAtom<Value, Value>,
key: Key,
setter?: (draft: Draft<Value>[Key], update: Update) => Draft<Value>[Key]
): WritableAtom<Value[Key], Update>
export function pickAtom<Value, Key extends keyof Value & keyof Draft<Value>, Update extends Value[Key]>(
anAtom: WritableAtom<Value, Value>,
key: Key,
setter: (draft: Draft<Value>[Key], update: Update) => Draft<Value>[Key] = (draft, update) =>
update as Draft<Value>[Key]
): WritableAtom<Value[Key], Update> {
return atom(
(get) => get(anAtom)[key],
(get, set, update: Update) =>
set(withImmer(anAtom), (value) => {
const derived = setter(value[key], update)
value[key] = derived
})
)
}
/**
* Typing for a customizable enum; see setCustomizable.
* This is not exported because an enum may not extend another interface.
*/
interface CustomizableEnum<T extends number> {
CUSTOM: -1
DEFAULT: T
}
/**
* Typing for a customizable enum; see setCustomizable.
* The first value is used, unless it is CUSTOM, in which case the second is used.
*/
export type Customizable<T> = { value: T; custom?: number }
/** Sets a customizable enum, validating the tuple and falling back to the default. */
export function setCustomizable<T extends number, Enum extends CustomizableEnum<T>>(customizable: Enum) {
return (draft: Customizable<T>, update: T | Customizable<T>) => {
// normalize the update
if (typeof update === 'number') {
update = { value: update }
}
draft.value = update.value
if (draft.value === customizable.CUSTOM) {
draft.custom = update.custom
// prevent invalid state
if (draft.custom === undefined) {
draft.value = customizable.DEFAULT
}
}
return draft
}
}
/** Sets a togglable atom to invert its state at the next render. */
export function setTogglable(draft: boolean) {
return !draft
}

@ -0,0 +1,28 @@
import uriToHttp from './uriToHttp'
describe('uriToHttp', () => {
it('returns .eth.link for ens names', () => {
expect(uriToHttp('t2crtokens.eth')).toEqual([])
})
it('returns https first for http', () => {
expect(uriToHttp('http://test.com')).toEqual(['https://test.com', 'http://test.com'])
})
it('returns https for https', () => {
expect(uriToHttp('https://test.com')).toEqual(['https://test.com'])
})
it('returns ipfs gateways for ipfs:// urls', () => {
expect(uriToHttp('ipfs://QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ')).toEqual([
'https://cloudflare-ipfs.com/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/',
'https://ipfs.io/ipfs/QmV8AfDE8GFSGQvt3vck8EwAzsPuNTmtP8VcQJE3qxRPaZ/',
])
})
it('returns ipns gateways for ipns:// urls', () => {
expect(uriToHttp('ipns://app.uniswap.org')).toEqual([
'https://cloudflare-ipfs.com/ipns/app.uniswap.org/',
'https://ipfs.io/ipns/app.uniswap.org/',
])
})
it('returns empty array for invalid scheme', () => {
expect(uriToHttp('blah:test')).toEqual([])
})
})

@ -0,0 +1,21 @@
/**
* Given a URI that may be ipfs, ipns, http, or https protocol, return the fetch-able http(s) URLs for the same content
* @param uri to convert to fetch-able http url
*/
export default function uriToHttp(uri: string): string[] {
const protocol = uri.split(':')[0].toLowerCase()
switch (protocol) {
case 'https':
return [uri]
case 'http':
return ['https' + uri.substr(4), uri]
case 'ipfs':
const hash = uri.match(/^ipfs:(\/\/)?(.*)$/i)?.[2]
return [`https://cloudflare-ipfs.com/ipfs/${hash}/`, `https://ipfs.io/ipfs/${hash}/`]
case 'ipns':
const name = uri.match(/^ipns:(\/\/)?(.*)$/i)?.[2]
return [`https://cloudflare-ipfs.com/ipns/${name}/`, `https://ipfs.io/ipns/${name}/`]
default:
return []
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More