Compare commits

...

15 Commits

Author SHA1 Message Date
Christine Legge
99ab581a87 refactor: migrate state/user to createSlice (#3779)
* use slice in transactions reducer

* update transaction reducer tests

* update user reducer to use slice

* fix merge conflicts
2022-05-02 15:37:44 -04:00
Will Hennessy
fc571d0f63 chore: update compliance email test (#3788) 2022-05-02 14:37:51 -04:00
David Mihal
2de43a8cdb feat: take tick range from URL (#3208)
* Take tick range from URL

* Keep minPrice/maxPrice in the URL
2022-05-02 12:10:27 -05:00
Jordan Frankfurt
5383436c88 feat(widgets): empty token list on network alert (#3627)
* feat(widgets): empty token list on network alert

* make it work

* pr review

* split dialog header out of tokenselect

* correctly filter token list case

* find -> some

* pr feedback

* clean up query hooks
2022-05-02 10:47:27 -05:00
Jordan Frankfurt
521f3aae04 chore(monitoring): trm cleanup (#3783)
* remove old monitoring code

* cleanup

* remove unneeded .then
2022-04-29 15:42:09 -05:00
0xlucius
9318c1204b feat: Add on-hover tooltips for tx details (#3178)
* Add on-hover tooltips for tx details

* Change tooltips to use <Trans> macro instead of t

* fix: remove info tooltip on transaction popup

* fix: update getting the nativeCurrencyVariable

* use getNativeCurrency() instead of chainInfo const

Co-authored-by: Christine Legge <christine.legge@uniswap.org>
2022-04-25 12:43:41 -04:00
Jordan Frankfurt
5055695b9b feat(optimism): update to new bridge app (#3771) 2022-04-25 10:44:00 -05:00
Christine Legge
ae8c0377de refactor: move state/transactions to createSlice (#3758)
* use slice in transactions reducer

* update transaction reducer tests

* chore: move state/transactions types into their own folder

* fix: fix broken transaction/reducer tests
2022-04-25 09:22:31 -04:00
Jordan Frankfurt
8eaf1f4964 feat(analytics): add a GA event on risk block (#3768)
* feat(analytics): add a GA event on risk block

* Update src/hooks/useAccountRiskCheck.ts

Co-authored-by: Will Hennessy <hennessywill@gmail.com>

Co-authored-by: Will Hennessy <hennessywill@gmail.com>
2022-04-21 21:44:34 -05:00
Zach Pomerantz
f717bf4a49 chore: bump to 1.0.7 (#3753) 2022-04-19 13:14:58 -04:00
Zach Pomerantz
dcbd4e475d chore: rm "with no slippage" (#3752) 2022-04-19 13:13:33 -04:00
Jordan Frankfurt
b704bdac94 feat(compliance): risk screening (#3714)
* feat(compliance): risk screening

* add api endpoint

* hosted app only

* add help center link and click-to-copy email address

* only show on app.uniswap.org and fix spacing nits

* 12px for bottom section
2022-04-19 10:12:28 -05:00
Zach Pomerantz
00d3df95c0 fix: rm console logs (#3743) 2022-04-15 15:19:34 -04:00
Ian Lapham
251b8b703a update list (#3737) 2022-04-15 15:19:11 -04:00
Zach Pomerantz
ef8432437d fix: missing token img (#3727) 2022-04-15 14:19:38 -04:00
54 changed files with 568 additions and 459 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@uniswap/widgets",
"version": "1.0.6",
"version": "1.0.7",
"description": "Uniswap Interface",
"homepage": ".",
"files": [

View File

@@ -1,49 +1,58 @@
import { Trans } from '@lingui/macro'
import React from 'react'
import useCopyClipboard from 'hooks/useCopyClipboard'
import React, { useCallback } from 'react'
import { CheckCircle, Copy } from 'react-feather'
import styled from 'styled-components/macro'
import useCopyClipboard from '../../hooks/useCopyClipboard'
import { LinkStyledButton } from '../../theme'
import { LinkStyledButton } from 'theme'
const CopyIcon = styled(LinkStyledButton)`
color: ${({ theme }) => theme.text3};
color: ${({ color, theme }) => color || theme.text3};
flex-shrink: 0;
display: flex;
text-decoration: none;
font-size: 0.825rem;
font-size: 12px;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ theme }) => theme.text2};
color: ${({ color, theme }) => color || theme.text2};
}
`
const TransactionStatusText = styled.span`
margin-left: 0.25rem;
font-size: 0.825rem;
font-size: 12px;
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
`
export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) {
interface BaseProps {
toCopy: string
color?: string
}
export type CopyHelperProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
export default function CopyHelper({ color, toCopy, children }: CopyHelperProps) {
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(toCopy)
}, [toCopy, setCopied])
return (
<CopyIcon onClick={() => setCopied(props.toCopy)}>
<CopyIcon onClick={copy} color={color}>
{isCopied ? '' : children}
&nbsp;
{isCopied ? (
<TransactionStatusText>
<CheckCircle size={'16'} />
<CheckCircle size={'12'} />
<TransactionStatusText>
<Trans>Copied</Trans>
</TransactionStatusText>
</TransactionStatusText>
) : (
<TransactionStatusText>
<Copy size={'16'} />
<Copy size={'12'} />
</TransactionStatusText>
)}
{isCopied ? '' : props.children}
</CopyIcon>
)
}

View File

@@ -25,7 +25,7 @@ import {
VoteTransactionInfo,
WithdrawLiquidityStakingTransactionInfo,
WrapTransactionInfo,
} from '../../state/transactions/actions'
} from '../../state/transactions/types'
function formatAmount(amountRaw: string, decimals: number, sigFigs: number): string {
return new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs)

View File

@@ -10,7 +10,7 @@ import { AbstractConnector } from 'web3-react-abstract-connector'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, walletlink } from '../../connectors'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { clearAllTransactions } from '../../state/transactions/actions'
import { clearAllTransactions } from '../../state/transactions/reducer'
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
import { shortenAddress } from '../../utils'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'

View File

@@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { ReactNode, useMemo } from 'react'
// SDN OFAC addresses
const BLOCKED_ADDRESSES: string[] = [
'0x7Db418b5D567A4e0E8c59Ad71BE1FcE48f3E6107',
'0x72a5843cc08275C8171E582972Aa4fDa8C397B2A',
@@ -24,16 +23,18 @@ const BLOCKED_ADDRESSES: string[] = [
'0x6acdfba02d390b97ac2b2d42a63e85293bcc1',
'0x48549a34ae37b12f6a30566245176994e17c6',
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e',
'0x3cbded43efdaf0fc77b9c55f6fc9988fcc9b757d',
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
'0x3CBdeD43EFdAf0FC77b9C55F6fC9988fCC9b757d',
'0x67d40EE1A85bf4a4Bb7Ffae16De985e8427B6b45',
'0x6f1ca141a28907f78ebaa64fb83a9088b02a8352',
'0x6acdfba02d390b97ac2b2d42a63e85293bcc160e',
'0x6F1cA141A28907F78Ebaa64fb83A9088b02A8352',
'0x6aCDFBA02D390b97Ac2b2d42A63E85293BCc160e',
'0x48549a34ae37b12f6a30566245176994e17c6b4a',
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
'0x098B716B8Aaf21512996dC57EB0615e2383E2f96',
'0xfEC8A60023265364D066a1212fDE3930F6Ae8da7',
]
export default function Blocklist({ children }: { children: ReactNode }) {

View File

@@ -0,0 +1,55 @@
import { Trans } from '@lingui/macro'
import CopyHelper from 'components/AccountDetails/Copy'
import Column from 'components/Column'
import useTheme from 'hooks/useTheme'
import { AlertOctagon } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import Modal from '../Modal'
const ContentWrapper = styled(Column)`
align-items: center;
margin: 32px;
text-align: center;
`
const WarningIcon = styled(AlertOctagon)`
min-height: 22px;
min-width: 22px;
color: ${({ theme }) => theme.warning};
`
interface ConnectedAccountBlockedProps {
account: string | null | undefined
isOpen: boolean
}
export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedProps) {
const theme = useTheme()
return (
<Modal isOpen={props.isOpen} onDismiss={Function.prototype()}>
<ContentWrapper>
<WarningIcon />
<ThemedText.LargeHeader lineHeight={2} marginBottom={1} marginTop={1}>
<Trans>Blocked Address</Trans>
</ThemedText.LargeHeader>
<ThemedText.DarkGray fontSize={12} marginBottom={12}>
{props.account}
</ThemedText.DarkGray>
<ThemedText.Main fontSize={14} marginBottom={12}>
<Trans>This address is blocked on the Uniswap Labs interface because it is associated with one or more</Trans>{' '}
<ExternalLink href="https://help.uniswap.org/en/articles/6149816">
<Trans>blocked activities</Trans>
</ExternalLink>
.
</ThemedText.Main>
<ThemedText.Main fontSize={12}>
<Trans>If you believe this is an error, please send an email including your address to </Trans>{' '}
</ThemedText.Main>
<CopyHelper toCopy="compliance@uniswap.org" color={theme.primary1}>
compliance@uniswap.org.
</CopyHelper>
</ContentWrapper>
</Modal>
)
}

View File

@@ -146,7 +146,7 @@ const BridgeLabel = ({ chainId }: { chainId: SupportedChainId }) => {
return <Trans>Arbitrum Bridge</Trans>
case SupportedChainId.OPTIMISM:
case SupportedChainId.OPTIMISTIC_KOVAN:
return <Trans>Optimism Gateway</Trans>
return <Trans>Optimism Bridge</Trans>
case SupportedChainId.POLYGON:
case SupportedChainId.POLYGON_MUMBAI:
return <Trans>Polygon Bridge</Trans>

View File

@@ -18,6 +18,7 @@ export const TooltipContainer = styled.div`
interface TooltipProps extends Omit<PopoverProps, 'content'> {
text: ReactNode
disableHover?: boolean // disable the hover and content display
}
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
@@ -29,19 +30,20 @@ interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
}
export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
return <Popover content={text && <TooltipContainer>{text}</TooltipContainer>} {...rest} />
}
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
}
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
/** Standard text tooltip. */
export function MouseoverTooltip({ text, disableHover, children, ...rest }: Omit<TooltipProps, 'show'>) {
const [show, setShow] = useState(false)
const open = useCallback(() => setShow(true), [setShow])
const close = useCallback(() => setShow(false), [setShow])
return (
<Tooltip {...rest} show={show}>
<Tooltip {...rest} show={show} text={disableHover ? null : text}>
<div onMouseEnter={open} onMouseLeave={close}>
{children}
</div>
@@ -49,6 +51,7 @@ export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show
)
}
/** Tooltip that displays custom content. */
export function MouseoverTooltipContent({
content,
children,

View File

@@ -0,0 +1,23 @@
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
export default function TopLevelModals() {
const addressClaimOpen = useModalOpen(ApplicationModal.ADDRESS_CLAIM)
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const blockedAccountModalOpen = useModalOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useActiveWeb3React()
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
return (
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
</>
)
}

View File

@@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
import { AutoColumn } from 'components/Column'
import { PrivacyPolicy } from 'components/PrivacyPolicy'
import Row, { AutoRow, RowBetween } from 'components/Row'
import { useWalletConnectMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useEffect, useState } from 'react'
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
import ReactGA from 'react-ga4'
@@ -151,8 +150,6 @@ export default function WalletModal({
const previousAccount = usePrevious(account)
const logMonitoringEvent = useWalletConnectMonitoringEventCallback()
// close on connection, when logged out before
useEffect(() => {
if (account && !previousAccount && walletModalOpen) {
@@ -200,18 +197,13 @@ export default function WalletModal({
}
connector &&
activate(connector, undefined, true)
.then(async () => {
const walletAddress = await connector.getAccount()
logMonitoringEvent({ walletAddress })
})
.catch((error) => {
if (error instanceof UnsupportedChainIdError) {
activate(connector) // a little janky...can't use setError because the connector isn't set
} else {
setPendingError(true)
}
})
activate(connector, undefined, true).catch((error) => {
if (error instanceof UnsupportedChainIdError) {
activate(connector) // a little janky...can't use setError because the connector isn't set
} else {
setPendingError(true)
}
})
}
// close wallet modal if fortmatic modal is active

View File

@@ -13,7 +13,7 @@ import useENSName from '../../hooks/useENSName'
import { useHasSocks } from '../../hooks/useSocksBalance'
import { useWalletModalToggle } from '../../state/application/hooks'
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
import { TransactionDetails } from '../../state/transactions/reducer'
import { TransactionDetails } from '../../state/transactions/types'
import { shortenAddress } from '../../utils'
import { ButtonSecondary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'

View File

@@ -7,8 +7,8 @@ import styled from 'styled-components/macro'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'

View File

@@ -12,8 +12,8 @@ import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallbac
import { useContract, usePairContract, useV2RouterContract } from '../../hooks/useContract'
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { formatCurrencyAmount } from '../../utils/formatCurrencyAmount'
import { maxAmountSpend } from '../../utils/maxAmountSpend'

View File

@@ -7,8 +7,8 @@ import styled from 'styled-components/macro'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'

View File

@@ -4,6 +4,7 @@ import Card from 'components/Card'
import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useContext, useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import styled, { ThemeContext } from 'styled-components/macro'
@@ -12,6 +13,7 @@ import { Separator, ThemedText } from '../../theme'
import { computeRealizedLPFeePercent } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
import FormattedPriceImpact from './FormattedPriceImpact'
const StyledCard = styled(Card)`
@@ -23,6 +25,7 @@ interface AdvancedSwapDetailsProps {
allowedSlippage: Percent
syncing?: boolean
hideRouteDiagram?: boolean
hideInfoTooltips?: boolean
}
function TextWithLoadingPlaceholder({
@@ -43,9 +46,15 @@ function TextWithLoadingPlaceholder({
)
}
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
export function AdvancedSwapDetails({
trade,
allowedSlippage,
syncing = false,
hideInfoTooltips = false,
}: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
const { chainId } = useActiveWeb3React()
const nativeCurrency = useNativeCurrency()
const { expectedOutputAmount, priceImpact } = useMemo(() => {
if (!trade) return { expectedOutputAmount: undefined, priceImpact: undefined }
@@ -60,9 +69,19 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
<AutoColumn gap="8px">
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Expected Output</Trans>
</ThemedText.SubHeader>
<MouseoverTooltip
text={
<Trans>
The amount you expect to receive at the current market price. You may receive less or more if the
market price changes while your transaction is pending.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Expected Output</Trans>
</ThemedText.SubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.Black textAlign="right" fontSize={14}>
@@ -74,9 +93,14 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</RowBetween>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</ThemedText.SubHeader>
<MouseoverTooltip
text={<Trans>The impact your trade has on the market price of this pool.</Trans>}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</ThemedText.SubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14}>
@@ -87,14 +111,24 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
<Separator />
<RowBetween>
<RowFixed style={{ marginRight: '20px' }}>
<ThemedText.SubHeader color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}{' '}
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
</ThemedText.SubHeader>
<MouseoverTooltip
text={
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
will revert.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}{' '}
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
</ThemedText.SubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
@@ -106,9 +140,18 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</RowBetween>
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<RowBetween>
<ThemedText.SubHeader color={theme.text3}>
<Trans>Network Fee</Trans>
</ThemedText.SubHeader>
<MouseoverTooltip
text={
<Trans>
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text3}>
<Trans>Network Fee</Trans>
</ThemedText.SubHeader>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
~${trade.gasUseEstimateUSD.toFixed(2)}

View File

@@ -147,7 +147,12 @@ export default function SwapDetailsDropdown({
content={
<ResponsiveTooltipContainer origin="top right" style={{ padding: '0' }}>
<Card padding="12px">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
<AdvancedSwapDetails
trade={trade}
allowedSlippage={allowedSlippage}
syncing={syncing}
hideInfoTooltips={true}
/>
</Card>
</ResponsiveTooltipContainer>
}

View File

@@ -94,7 +94,7 @@ export const CHAIN_INFO: ChainInfoMap = {
[SupportedChainId.OPTIMISM]: {
networkType: NetworkType.L2,
blockWaitMsBeforeWarning: ms`25m`,
bridge: 'https://gateway.optimism.io/?chainId=1',
bridge: 'https://app.optimism.io/bridge',
defaultListUrl: OPTIMISM_LIST,
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',
@@ -108,7 +108,7 @@ export const CHAIN_INFO: ChainInfoMap = {
[SupportedChainId.OPTIMISTIC_KOVAN]: {
networkType: NetworkType.L2,
blockWaitMsBeforeWarning: ms`25m`,
bridge: 'https://gateway.optimism.io/',
bridge: 'https://app.optimism.io/bridge',
defaultListUrl: OPTIMISM_LIST,
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',

View File

@@ -0,0 +1,33 @@
import { useEffect } from 'react'
import ReactGA from 'react-ga4'
import { ApplicationModal, setOpenModal } from 'state/application/reducer'
import { useAppDispatch } from 'state/hooks'
export default function useAccountRiskCheck(account: string | null | undefined) {
const dispatch = useAppDispatch()
useEffect(() => {
if (account && window.location.hostname === 'app.uniswap.org') {
const headers = new Headers({ 'Content-Type': 'application/json' })
fetch('https://screening-worker.uniswap.workers.dev', {
method: 'POST',
headers,
body: JSON.stringify({ address: account }),
})
.then((res) => res.json())
.then((data) => {
if (data.block) {
dispatch(setOpenModal(ApplicationModal.BLOCKED_ACCOUNT))
ReactGA.event({
category: 'Address Screening',
action: 'blocked',
label: account,
})
}
})
.catch(() => {
dispatch(setOpenModal(null))
})
}
}, [account, dispatch])
}

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { useAppDispatch } from 'state/hooks'
import { updateUserExpertMode } from '../state/user/actions'
import { updateUserExpertMode } from '../state/user/reducer'
import useParsedQueryString from './useParsedQueryString'
export default function ApeModeQueryParamReader(): null {

View File

@@ -6,8 +6,8 @@ import useSwapApproval, { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/u
import { ApprovalState, useApproval } from 'lib/hooks/useApproval'
import { useCallback } from 'react'
import { TransactionType } from '../state/transactions/actions'
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
export { ApprovalState } from 'lib/hooks/useApproval'
function useGetAndTrackApproval(getApproval: ReturnType<typeof useApproval>[1]) {

View File

@@ -1,98 +0,0 @@
import { TransactionResponse } from '@ethersproject/providers'
import { initializeApp } from 'firebase/app'
import { getDatabase, push, ref } from 'firebase/database'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback } from 'react'
import { TransactionInfo, TransactionType } from 'state/transactions/actions'
type PartialTransactionResponse = Pick<TransactionResponse, 'hash' | 'v' | 'r' | 's'>
const SUPPORTED_TRANSACTION_TYPES = [
TransactionType.ADD_LIQUIDITY_V2_POOL,
TransactionType.ADD_LIQUIDITY_V3_POOL,
TransactionType.CREATE_V3_POOL,
TransactionType.REMOVE_LIQUIDITY_V3,
TransactionType.SWAP,
]
const FIREBASE_API_KEY = process.env.REACT_APP_FIREBASE_KEY
const firebaseEnabled = typeof FIREBASE_API_KEY !== 'undefined'
if (firebaseEnabled) initializeFirebase()
function useMonitoringEventCallback() {
const { chainId } = useActiveWeb3React()
return useCallback(
async function log(
type: string,
{
transactionResponse,
walletAddress,
}: { transactionResponse: PartialTransactionResponse; walletAddress: string | undefined }
) {
if (!firebaseEnabled) return
const db = getDatabase()
if (!walletAddress) {
console.debug('Wallet address required to log monitoring events.')
return
}
try {
push(ref(db, 'trm'), {
chainId,
origin: window.location.origin,
timestamp: Date.now(),
tx: transactionResponse,
type,
walletAddress,
})
} catch (e) {
console.debug('Error adding document: ', e)
}
},
[chainId]
)
}
export function useTransactionMonitoringEventCallback() {
const { account } = useActiveWeb3React()
const log = useMonitoringEventCallback()
return useCallback(
(info: TransactionInfo, transactionResponse: TransactionResponse) => {
if (SUPPORTED_TRANSACTION_TYPES.includes(info.type)) {
log(TransactionType[info.type], {
transactionResponse: (({ hash, v, r, s }: PartialTransactionResponse) => ({ hash, v, r, s }))(
transactionResponse
),
walletAddress: account ?? undefined,
})
}
},
[account, log]
)
}
export function useWalletConnectMonitoringEventCallback() {
const log = useMonitoringEventCallback()
return useCallback(
(walletAddress) => {
log('WALLET_CONNECTED', { transactionResponse: { hash: '', r: '', s: '', v: -1 }, walletAddress })
},
[log]
)
}
function initializeFirebase() {
initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_KEY,
authDomain: 'interface-monitoring.firebaseapp.com',
databaseURL: 'https://interface-monitoring-default-rtdb.firebaseio.com',
projectId: 'interface-monitoring',
storageBucket: 'interface-monitoring.appspot.com',
messagingSenderId: '968187720053',
appId: '1:968187720053:web:acedf72dce629d470be33c',
})
}

View File

@@ -4,8 +4,8 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
import { ReactNode, useMemo } from 'react'
import { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
import { currencyId } from '../utils/currencyId'
import useENS from './useENS'
import { SignatureData } from './useERC20Permit'

View File

@@ -6,8 +6,8 @@ import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../constants/tokens'
import { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
import { useCurrencyBalance } from '../state/wallet/hooks'
import { useWETHContract } from './useContract'

View File

@@ -1,5 +1,9 @@
import { tokens } from '@uniswap/default-token-list'
import { TokenInfo } from '@uniswap/token-lists'
import { SupportedChainId } from 'constants/chains'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { TokenListProvider } from 'lib/hooks/useTokenList'
import { useEffect } from 'react'
import { useSelect, useValue } from 'react-cosmos/fixture'
@@ -56,16 +60,47 @@ function Fixture() {
})
const [defaultOutputAmount] = useValue('defaultOutputAmount', { defaultValue: 0 })
const tokenListNameMap: Record<string, TokenInfo[] | string> = {
'default list': tokens,
'mainnet only': tokens.filter((token) => SupportedChainId.MAINNET === token.chainId),
'arbitrum only': [
{
logoURI: 'https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734',
chainId: 42161,
address: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
name: 'Dai Stablecoin',
symbol: 'DAI',
decimals: 18,
},
{
logoURI: 'https://assets.coingecko.com/coins/images/6319/thumb/USD_Coin_icon.png?1547042389',
chainId: 42161,
address: '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
name: 'USD Coin (Arb1)',
symbol: 'USDC',
decimals: 6,
},
],
}
const tokenListOptions = Object.keys(tokenListNameMap)
const [tokenListName] = useSelect('tokenList', {
options: tokenListOptions,
defaultValue: tokenListOptions[0],
})
return (
<Swap
convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
defaultInputAmount={defaultInputAmount}
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
defaultOutputAmount={defaultOutputAmount}
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
/>
<TokenListProvider list={tokenListNameMap[tokenListName]}>
<Swap
convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
defaultInputAmount={defaultInputAmount}
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
defaultOutputAmount={defaultOutputAmount}
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
/>
</TokenListProvider>
)
}

View File

@@ -2,6 +2,7 @@ import 'setimmediate'
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import TokenSelect from 'lib/components/TokenSelect'
import { loadingTransitionCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -10,7 +11,6 @@ import Button from '../Button'
import Column from '../Column'
import { DecimalInput } from '../Input'
import Row from '../Row'
import TokenSelect from '../TokenSelect'
const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;

View File

@@ -86,7 +86,7 @@ export function WrapCurrency({ inputCurrency, outputCurrency }: { inputCurrency:
const Text = useCallback(
() => (
<Trans>
Convert {inputCurrency.symbol} to {outputCurrency.symbol} with no slippage
Convert {inputCurrency.symbol} to {outputCurrency.symbol}
</Trans>
),
[inputCurrency.symbol, outputCurrency.symbol]

View File

@@ -17,21 +17,22 @@ function TokenImg({ token, ...rest }: TokenImgProps) {
// Use the wrapped token info so that it includes the logoURI.
const tokenInfo = useToken(token.isToken ? token.wrapped.address : undefined) ?? token
// TODO(zzmp): TokenImg takes a frame to switch.
const srcs = useCurrencyLogoURIs(tokenInfo)
const [attempt, setAttempt] = useState(0)
const onError = useCallback((e) => {
if (e.target.src) badSrcs.add(e.target.src)
setAttempt((attempt) => ++attempt)
}, [])
const src = useMemo(() => {
// Trigger a re-render when an error occurs.
void attempt
return srcs.find((src) => !badSrcs.has(src))
}, [attempt, srcs])
const onError = useCallback(
(e) => {
if (src) badSrcs.add(src)
setAttempt((attempt) => ++attempt)
},
[src]
)
if (!src) return <MissingToken color="secondary" {...rest} />

View File

@@ -8,7 +8,7 @@ export default function Fixture() {
return (
<Modal color="module">
<TokenListProvider list={DEFAULT_TOKEN_LIST.tokens}>
<TokenSelectDialog onSelect={() => void 0} />
<TokenSelectDialog onSelect={() => void 0} onClose={() => void 0} />
</TokenListProvider>
</Modal>
)

View File

@@ -0,0 +1,30 @@
import { Trans } from '@lingui/macro'
import { HelpCircle } from 'lib/icons'
import styled, { css, ThemedText } from 'lib/theme'
import Column from '../Column'
const HelpCircleIcon = styled(HelpCircle)`
height: 64px;
margin-bottom: 12px;
stroke: ${({ theme }) => theme.secondary};
width: 64px;
`
const wrapperCss = css`
display: flex;
height: 80%;
text-align: center;
width: 100%;
`
export default function NoTokensAvailableOnNetwork() {
return (
<Column align="center" justify="center" css={wrapperCss}>
<HelpCircleIcon />
<ThemedText.Body1 color="primary">
<Trans>No tokens are available on this network. Please switch to another network.</Trans>
</ThemedText.Body1>
</Column>
)
}

View File

@@ -1,19 +1,19 @@
import { t, Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { Header as DialogHeader } from 'lib/components/Dialog'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import useTokenList, { useIsTokenListLoaded, useQueryCurrencies } from 'lib/hooks/useTokenList'
import useTokenList, { useIsTokenListLoaded, useQueryTokens } from 'lib/hooks/useTokenList'
import styled, { ThemedText } from 'lib/theme'
import { ElementRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { currencyId } from 'utils/currencyId'
import Column from '../Column'
import Dialog, { Header } from '../Dialog'
import Dialog from '../Dialog'
import { inputCss, StringInput } from '../Input'
import Row from '../Row'
import Rule from '../Rule'
import TokenBase from './TokenBase'
import NoTokensAvailableOnNetwork from './NoTokensAvailableOnNetwork'
import TokenButton from './TokenButton'
import TokenOptions from './TokenOptions'
import TokenOptionsSkeleton from './TokenOptionsSkeleton'
@@ -42,12 +42,13 @@ function useAreBalancesLoaded(): boolean {
interface TokenSelectDialogProps {
value?: Currency
onSelect: (token: Currency) => void
onClose: () => void
}
export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
export function TokenSelectDialog({ value, onSelect, onClose }: TokenSelectDialogProps) {
const [query, setQuery] = useState('')
const queriedTokens = useQueryCurrencies(query)
const tokens = useMemo(() => queriedTokens?.filter((token) => token !== value), [queriedTokens, value])
const list = useTokenList()
const tokens = useQueryTokens(query, list)
const isTokenListLoaded = useIsTokenListLoaded()
const areBalancesLoaded = useAreBalancesLoaded()
@@ -65,16 +66,26 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
[query, areBalancesLoaded, isTokenListLoaded]
)
const baseTokens: Currency[] = [] // TODO(zzmp): Add base tokens to token list functionality
const input = useRef<HTMLInputElement>(null)
useEffect(() => input.current?.focus({ preventScroll: true }), [input])
const [options, setOptions] = useState<ElementRef<typeof TokenOptions> | null>(null)
const { chainId } = useActiveWeb3React()
const listHasTokens = useMemo(() => list.some((token) => token.chainId === chainId), [chainId, list])
if (!listHasTokens && isLoaded) {
return (
<Dialog color="module" onClose={onClose}>
<DialogHeader title={<Trans>Select a token</Trans>} />
<NoTokensAvailableOnNetwork />
</Dialog>
)
}
return (
<>
<Header title={<Trans>Select a token</Trans>} />
<Dialog color="module" onClose={onClose}>
<DialogHeader title={<Trans>Select a token</Trans>} />
<Column gap={0.75}>
<Row pad={0.75} grow>
<ThemedText.Body1>
@@ -88,13 +99,6 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
/>
</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={currencyId(token)} />
))}
</Row>
)}
<Rule padded />
</Column>
{isLoaded ? (
@@ -112,7 +116,7 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
) : (
<TokenOptionsSkeleton />
)}
</>
</Dialog>
)
}
@@ -138,11 +142,7 @@ export default memo(function TokenSelect({ value, collapsed, disabled, onSelect
return (
<>
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={onOpen} />
{open && (
<Dialog color="module" onClose={() => setOpen(false)}>
<TokenSelectDialog value={value} onSelect={selectAndClose} />
</Dialog>
)}
{open && <TokenSelectDialog value={value} onSelect={selectAndClose} onClose={() => setOpen(false)} />}
</>
)
})

View File

@@ -1,4 +1,4 @@
import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { Token } from '@uniswap/sdk-core'
import { TokenInfo, TokenList } from '@uniswap/token-lists'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
@@ -6,10 +6,11 @@ import { createContext, PropsWithChildren, useCallback, useContext, useEffect, u
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import fetchTokenList from './fetchTokenList'
import { useQueryTokens } from './querying'
import { ChainTokenMap, tokensToChainTokenMap } from './utils'
import { validateTokens } from './validateTokenList'
export { useQueryTokens } from './useQueryTokens'
export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
const MISSING_PROVIDER = Symbol()
@@ -52,10 +53,6 @@ export function useTokenMap(): TokenMap {
}, [tokenMap])
}
export function useQueryCurrencies(query = ''): (WrappedTokenInfo | NativeCurrency)[] {
return useQueryTokens(query, useTokenList())
}
export function TokenListProvider({
list = DEFAULT_TOKEN_LIST,
children,

View File

@@ -5,6 +5,7 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { FeeAmount, NonfungiblePositionManager } from '@uniswap/v3-sdk'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useParsedQueryString from 'hooks/useParsedQueryString'
import { useCallback, useContext, useEffect, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import ReactGA from 'react-ga4'
@@ -47,8 +48,8 @@ import { useUSDCValue } from '../../hooks/useUSDCPrice'
import { useV3PositionFromTokenId } from '../../hooks/useV3Positions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Bound, Field } from '../../state/mint/v3/actions'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { ExternalLink, ThemedText } from '../../theme'
import approveAmountCalldata from '../../utils/approveAmountCalldata'
@@ -86,6 +87,7 @@ export default function AddLiquidity({
const expertMode = useIsExpertMode()
const addTransaction = useTransactionAdder()
const positionManager = useV3NFTPositionManagerContract()
const parsedQs = useParsedQueryString()
// check for existing position if tokenId in url
const { position: existingPositionDetails, loading: positionLoading } = useV3PositionFromTokenId(
@@ -107,7 +109,8 @@ export default function AddLiquidity({
baseCurrency && currencyB && baseCurrency.wrapped.equals(currencyB.wrapped) ? undefined : currencyB
// mint state
const { independentField, typedValue, startPriceTypedValue } = useV3MintState()
const { independentField, typedValue, startPriceTypedValue, rightRangeTypedValue, leftRangeTypedValue } =
useV3MintState()
const {
pool,
@@ -150,6 +153,24 @@ export default function AddLiquidity({
useEffect(() => setShowCapitalEfficiencyWarning(false), [baseCurrency, quoteCurrency, feeAmount])
useEffect(() => {
if (
typeof parsedQs.minPrice === 'string' &&
parsedQs.minPrice !== leftRangeTypedValue &&
!isNaN(parsedQs.minPrice as any)
) {
onLeftRangeInput(parsedQs.minPrice)
}
if (
typeof parsedQs.maxPrice === 'string' &&
parsedQs.maxPrice !== rightRangeTypedValue &&
!isNaN(parsedQs.maxPrice as any)
) {
onRightRangeInput(parsedQs.maxPrice)
}
}, [parsedQs, rightRangeTypedValue, leftRangeTypedValue, onRightRangeInput, onLeftRangeInput])
// txn values
const deadline = useTransactionDeadline() // custom from users settings

View File

@@ -32,8 +32,8 @@ import { PairState } from '../../hooks/useV2Pairs'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/mint/actions'
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { useIsExpertMode, useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { ThemedText } from '../../theme'
import { calculateGasMargin } from '../../utils/calculateGasMargin'

View File

@@ -1,18 +1,16 @@
import Loader from 'components/Loader'
import TopLevelModals from 'components/TopLevelModals'
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { lazy, Suspense } from 'react'
import { Redirect, Route, Switch } from 'react-router-dom'
import styled from 'styled-components/macro'
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import AddressClaimModal from '../components/claim/AddressClaimModal'
import ErrorBoundary from '../components/ErrorBoundary'
import Header from '../components/Header'
import Polling from '../components/Header/Polling'
import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
import { useModalOpen, useToggleModal } from '../state/application/hooks'
import { ApplicationModal } from '../state/application/reducer'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity'
import { RedirectDuplicateTokenIds } from './AddLiquidity/redirects'
@@ -65,12 +63,6 @@ const Marginer = styled.div`
margin-top: 5rem;
`
function TopLevelModals() {
const open = useModalOpen(ApplicationModal.ADDRESS_CLAIM)
const toggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
return <AddressClaimModal isOpen={open} onDismiss={toggle} />
}
export default function App() {
return (
<ErrorBoundary>

View File

@@ -44,7 +44,7 @@ import { useToken } from '../../hooks/Tokens'
import { usePairContract, useV2MigratorContract } from '../../hooks/useContract'
import useIsArgentWallet from '../../hooks/useIsArgentWallet'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { TransactionType } from '../../state/transactions/actions'
import { TransactionType } from '../../state/transactions/types'
import { useTokenBalance } from '../../state/wallet/hooks'
import { BackArrow, ExternalLink, ThemedText } from '../../theme'
import { isAddress } from '../../utils'

View File

@@ -42,7 +42,7 @@ import RateToggle from '../../components/RateToggle'
import { SwitchLocaleLink } from '../../components/SwitchLocaleLink'
import { usePositionTokenURI } from '../../hooks/usePositionTokenURI'
import useTheme from '../../hooks/useTheme'
import { TransactionType } from '../../state/transactions/actions'
import { TransactionType } from '../../state/transactions/types'
import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { LoadingRows } from './styleds'

View File

@@ -34,7 +34,7 @@ import { ThemedText } from 'theme'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import { WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
import { TransactionType } from '../../state/transactions/actions'
import { TransactionType } from '../../state/transactions/types'
import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { currencyId } from '../../utils/currencyId'
import AppBody from '../AppBody'

View File

@@ -33,8 +33,8 @@ import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/burn/actions'
import { useBurnActionHandlers, useBurnState, useDerivedBurnInfo } from '../../state/burn/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { useUserSlippageToleranceWithDefault } from '../../state/user/hooks'
import { StyledInternalLink, ThemedText } from '../../theme'
import { calculateGasMargin } from '../../utils/calculateGasMargin'

View File

@@ -14,17 +14,18 @@ export type PopupContent =
}
export enum ApplicationModal {
WALLET,
SETTINGS,
SELF_CLAIM,
ADDRESS_CLAIM,
BLOCKED_ACCOUNT,
DELEGATE,
CLAIM_POPUP,
MENU,
DELEGATE,
VOTE,
POOL_OVERVIEW_OPTIONS,
NETWORK_SELECTOR,
POOL_OVERVIEW_OPTIONS,
PRIVACY_POLICY,
SELF_CLAIM,
SETTINGS,
VOTE,
WALLET,
}
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>

View File

@@ -11,8 +11,8 @@ import { UNI } from '../../constants/tokens'
import { useContract } from '../../hooks/useContract'
import { isAddress } from '../../utils'
import { calculateGasMargin } from '../../utils/calculateGasMargin'
import { TransactionType } from '../transactions/actions'
import { useTransactionAdder } from '../transactions/hooks'
import { TransactionType } from '../transactions/types'
function useMerkleDistributorContract() {
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)

View File

@@ -32,8 +32,8 @@ import {
} from '../../constants/proposals'
import { UNI } from '../../constants/tokens'
import { useLogs } from '../logs/hooks'
import { TransactionType } from '../transactions/actions'
import { useTransactionAdder } from '../transactions/hooks'
import { TransactionType } from '../transactions/types'
import { VoteOption } from './types'
function useGovernanceV0Contract(): Contract | null {

View File

@@ -16,8 +16,10 @@ import { usePool } from 'hooks/usePools'
import JSBI from 'jsbi'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useCallback, useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { getTickToPrice } from 'utils/getTickToPrice'
import { replaceURLParam } from 'utils/routes'
import { BIG_INT_ZERO } from '../../../constants/misc'
import { PoolState } from '../../../hooks/usePools'
@@ -46,6 +48,7 @@ export function useV3MintActionHandlers(noLiquidity: boolean | undefined): {
onStartPriceInput: (typedValue: string) => void
} {
const dispatch = useAppDispatch()
const history = useHistory()
const onFieldAInput = useCallback(
(typedValue: string) => {
@@ -64,15 +67,17 @@ export function useV3MintActionHandlers(noLiquidity: boolean | undefined): {
const onLeftRangeInput = useCallback(
(typedValue: string) => {
dispatch(typeLeftRangeInput({ typedValue }))
history.replace({ search: replaceURLParam(history.location.search, 'minPrice', typedValue) })
},
[dispatch]
[dispatch, history]
)
const onRightRangeInput = useCallback(
(typedValue: string) => {
dispatch(typeRightRangeInput({ typedValue }))
history.replace({ search: replaceURLParam(history.location.search, 'maxPrice', typedValue) })
},
[dispatch]
[dispatch, history]
)
const onStartPriceInput = useCallback(

View File

@@ -1,20 +1,17 @@
import { TransactionResponse } from '@ethersproject/providers'
import { Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useTransactionMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useCallback, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { addTransaction, TransactionInfo, TransactionType } from './actions'
import { TransactionDetails } from './reducer'
import { addTransaction } from './reducer'
import { TransactionDetails, TransactionInfo, TransactionType } from './types'
// helper that can take a ethers library transaction response and add it to the list of transactions
export function useTransactionAdder(): (response: TransactionResponse, info: TransactionInfo) => void {
const { chainId, account } = useActiveWeb3React()
const dispatch = useAppDispatch()
const logMonitoringEvent = useTransactionMonitoringEventCallback()
return useCallback(
(response: TransactionResponse, info: TransactionInfo) => {
if (!account) return
@@ -25,10 +22,8 @@ export function useTransactionAdder(): (response: TransactionResponse, info: Tra
throw Error('No transaction hash found.')
}
dispatch(addTransaction({ hash, from: account, info, chainId }))
logMonitoringEvent(info, response)
},
[account, chainId, dispatch, logMonitoringEvent]
[account, chainId, dispatch]
)
}

View File

@@ -1,14 +1,15 @@
import { createStore, Store } from 'redux'
import { updateVersion } from '../global/actions'
import {
import reducer, {
addTransaction,
checkedTransaction,
clearAllTransactions,
finalizeTransaction,
TransactionType,
} from './actions'
import reducer, { initialState, TransactionState } from './reducer'
initialState,
TransactionState,
} from './reducer'
import { TransactionType } from './types'
describe('transaction reducer', () => {
let store: Store<TransactionState>

View File

@@ -1,27 +1,10 @@
import { createReducer } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { updateVersion } from '../global/actions'
import {
addTransaction,
checkedTransaction,
clearAllTransactions,
finalizeTransaction,
SerializableTransactionReceipt,
TransactionInfo,
} from './actions'
import { TransactionDetails } from './types'
const now = () => new Date().getTime()
export interface TransactionDetails {
hash: string
receipt?: SerializableTransactionReceipt
lastCheckedBlockNumber?: number
addedTime: number
confirmedTime?: number
from: string
info: TransactionInfo
}
export interface TransactionState {
[chainId: number]: {
[txHash: string]: TransactionDetails
@@ -30,9 +13,44 @@ export interface TransactionState {
export const initialState: TransactionState = {}
export default createReducer(initialState, (builder) =>
builder
.addCase(updateVersion, (transactions) => {
const transactionSlice = createSlice({
name: 'transactions',
initialState,
reducers: {
addTransaction(transactions, { payload: { chainId, from, hash, info } }) {
if (transactions[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
const txs = transactions[chainId] ?? {}
txs[hash] = { hash, info, from, addedTime: now() }
transactions[chainId] = txs
},
clearAllTransactions(transactions, { payload: { chainId } }) {
if (!transactions[chainId]) return
transactions[chainId] = {}
},
checkedTransaction(transactions, { payload: { chainId, hash, blockNumber } }) {
const tx = transactions[chainId]?.[hash]
if (!tx) {
return
}
if (!tx.lastCheckedBlockNumber) {
tx.lastCheckedBlockNumber = blockNumber
} else {
tx.lastCheckedBlockNumber = Math.max(blockNumber, tx.lastCheckedBlockNumber)
}
},
finalizeTransaction(transactions, { payload: { hash, chainId, receipt } }) {
const tx = transactions[chainId]?.[hash]
if (!tx) {
return
}
tx.receipt = receipt
tx.confirmedTime = now()
},
},
extraReducers: (builder) => {
builder.addCase(updateVersion, (transactions) => {
// in case there are any transactions in the store with the old format, remove them
Object.keys(transactions).forEach((chainId) => {
const chainTransactions = transactions[chainId as unknown as number]
@@ -44,35 +62,9 @@ export default createReducer(initialState, (builder) =>
})
})
})
.addCase(addTransaction, (transactions, { payload: { chainId, from, hash, info } }) => {
if (transactions[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
const txs = transactions[chainId] ?? {}
txs[hash] = { hash, info, from, addedTime: now() }
transactions[chainId] = txs
})
.addCase(clearAllTransactions, (transactions, { payload: { chainId } }) => {
if (!transactions[chainId]) return
transactions[chainId] = {}
})
.addCase(checkedTransaction, (transactions, { payload: { chainId, hash, blockNumber } }) => {
const tx = transactions[chainId]?.[hash]
if (!tx) {
return
}
if (!tx.lastCheckedBlockNumber) {
tx.lastCheckedBlockNumber = blockNumber
} else {
tx.lastCheckedBlockNumber = Math.max(blockNumber, tx.lastCheckedBlockNumber)
}
})
.addCase(finalizeTransaction, (transactions, { payload: { hash, chainId, receipt } }) => {
const tx = transactions[chainId]?.[hash]
if (!tx) {
return
}
tx.receipt = receipt
tx.confirmedTime = now()
})
)
},
})
export const { addTransaction, clearAllTransactions, checkedTransaction, finalizeTransaction } =
transactionSlice.actions
export default transactionSlice.reducer

View File

@@ -1,9 +1,8 @@
import { createAction } from '@reduxjs/toolkit'
import { TradeType } from '@uniswap/sdk-core'
import { VoteOption } from '../governance/types'
export interface SerializableTransactionReceipt {
interface SerializableTransactionReceipt {
to: string
from: string
contractAddress: string
@@ -171,20 +170,12 @@ export type TransactionInfo =
| RemoveLiquidityV3TransactionInfo
| SubmitProposalTransactionInfo
export const addTransaction = createAction<{
chainId: number
export interface TransactionDetails {
hash: string
receipt?: SerializableTransactionReceipt
lastCheckedBlockNumber?: number
addedTime: number
confirmedTime?: number
from: string
info: TransactionInfo
}>('transactions/addTransaction')
export const clearAllTransactions = createAction<{ chainId: number }>('transactions/clearAllTransactions')
export const finalizeTransaction = createAction<{
chainId: number
hash: string
receipt: SerializableTransactionReceipt
}>('transactions/finalizeTransaction')
export const checkedTransaction = createAction<{
chainId: number
hash: string
blockNumber: number
}>('transactions/checkedTransaction')
}

View File

@@ -6,7 +6,7 @@ import { useAppDispatch, useAppSelector } from 'state/hooks'
import { L2_CHAIN_IDS } from '../../constants/chains'
import { useAddPopup } from '../application/hooks'
import { checkedTransaction, finalizeTransaction } from './actions'
import { checkedTransaction, finalizeTransaction } from './reducer'
export default function Updater() {
const { chainId } = useActiveWeb3React()

View File

@@ -1,35 +0,0 @@
import { createAction } from '@reduxjs/toolkit'
import { SupportedLocale } from 'constants/locales'
export interface SerializedToken {
chainId: number
address: string
decimals: number
symbol?: string
name?: string
}
export interface SerializedPair {
token0: SerializedToken
token1: SerializedToken
}
export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('user/updateMatchesDarkMode')
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('user/updateUserDarkMode')
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('user/updateUserExpertMode')
export const updateUserLocale = createAction<{ userLocale: SupportedLocale }>('user/updateUserLocale')
export const updateShowSurveyPopup = createAction<{ showSurveyPopup: boolean }>('user/updateShowSurveyPopup')
export const updateShowDonationLink = createAction<{ showDonationLink: boolean }>('user/updateShowDonationLink')
export const updateUserClientSideRouter = createAction<{ userClientSideRouter: boolean }>(
'user/updateUserClientSideRouter'
)
export const updateHideClosedPositions = createAction<{ userHideClosedPositions: boolean }>('user/hideClosedPositions')
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number | 'auto' }>(
'user/updateUserSlippageTolerance'
)
export const updateUserDeadline = createAction<{ userDeadline: number }>('user/updateUserDeadline')
export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('user/addSerializedToken')
export const removeSerializedToken = createAction<{ chainId: number; address: string }>('user/removeSerializedToken')
export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('user/addSerializedPair')
export const removeSerializedPair =
createAction<{ chainId: number; tokenAAddress: string; tokenBAddress: string }>('user/removeSerializedPair')

View File

@@ -18,8 +18,6 @@ import {
addSerializedPair,
addSerializedToken,
removeSerializedToken,
SerializedPair,
SerializedToken,
updateHideClosedPositions,
updateShowDonationLink,
updateShowSurveyPopup,
@@ -29,7 +27,8 @@ import {
updateUserExpertMode,
updateUserLocale,
updateUserSlippageTolerance,
} from './actions'
} from './reducer'
import { SerializedPair, SerializedToken } from './types'
function serializeToken(token: Token): SerializedToken {
return {

View File

@@ -1,26 +1,9 @@
import { createReducer } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { SupportedLocale } from 'constants/locales'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
import { updateVersion } from '../global/actions'
import {
addSerializedPair,
addSerializedToken,
removeSerializedPair,
removeSerializedToken,
SerializedPair,
SerializedToken,
updateHideClosedPositions,
updateMatchesDarkMode,
updateShowDonationLink,
updateShowSurveyPopup,
updateUserClientSideRouter,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserLocale,
updateUserSlippageTolerance,
} from './actions'
import { SerializedPair, SerializedToken } from './types'
const currentTimestamp = () => new Date().getTime()
@@ -91,9 +74,84 @@ export const initialState: UserState = {
showDonationLink: true,
}
export default createReducer(initialState, (builder) =>
builder
.addCase(updateVersion, (state) => {
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
updateUserDarkMode(state, action) {
state.userDarkMode = action.payload.userDarkMode
state.timestamp = currentTimestamp()
},
updateMatchesDarkMode(state, action) {
state.matchesDarkMode = action.payload.matchesDarkMode
state.timestamp = currentTimestamp()
},
updateUserExpertMode(state, action) {
state.userExpertMode = action.payload.userExpertMode
state.timestamp = currentTimestamp()
},
updateUserLocale(state, action) {
state.userLocale = action.payload.userLocale
state.timestamp = currentTimestamp()
},
updateUserSlippageTolerance(state, action) {
state.userSlippageTolerance = action.payload.userSlippageTolerance
state.timestamp = currentTimestamp()
},
updateUserDeadline(state, action) {
state.userDeadline = action.payload.userDeadline
state.timestamp = currentTimestamp()
},
updateUserClientSideRouter(state, action) {
state.userClientSideRouter = action.payload.userClientSideRouter
},
updateHideClosedPositions(state, action) {
state.userHideClosedPositions = action.payload.userHideClosedPositions
},
updateShowSurveyPopup(state, action) {
state.showSurveyPopup = action.payload.showSurveyPopup
},
updateShowDonationLink(state, action) {
state.showDonationLink = action.payload.showDonationLink
},
addSerializedToken(state, { payload: { serializedToken } }) {
if (!state.tokens) {
state.tokens = {}
}
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken
state.timestamp = currentTimestamp()
},
removeSerializedToken(state, { payload: { address, chainId } }) {
if (!state.tokens) {
state.tokens = {}
}
state.tokens[chainId] = state.tokens[chainId] || {}
delete state.tokens[chainId][address]
state.timestamp = currentTimestamp()
},
addSerializedPair(state, { payload: { serializedPair } }) {
if (
serializedPair.token0.chainId === serializedPair.token1.chainId &&
serializedPair.token0.address !== serializedPair.token1.address
) {
const chainId = serializedPair.token0.chainId
state.pairs[chainId] = state.pairs[chainId] || {}
state.pairs[chainId][pairKey(serializedPair.token0.address, serializedPair.token1.address)] = serializedPair
}
state.timestamp = currentTimestamp()
},
removeSerializedPair(state, { payload: { chainId, tokenAAddress, tokenBAddress } }) {
if (state.pairs[chainId]) {
// just delete both keys if either exists
delete state.pairs[chainId][pairKey(tokenAAddress, tokenBAddress)]
delete state.pairs[chainId][pairKey(tokenBAddress, tokenAAddress)]
}
state.timestamp = currentTimestamp()
},
},
extraReducers: (builder) => {
builder.addCase(updateVersion, (state) => {
// slippage isnt being tracked in local storage, reset to default
// noinspection SuspiciousTypeOfGuard
if (
@@ -126,75 +184,23 @@ export default createReducer(initialState, (builder) =>
state.lastUpdateVersionTimestamp = currentTimestamp()
})
.addCase(updateUserDarkMode, (state, action) => {
state.userDarkMode = action.payload.userDarkMode
state.timestamp = currentTimestamp()
})
.addCase(updateMatchesDarkMode, (state, action) => {
state.matchesDarkMode = action.payload.matchesDarkMode
state.timestamp = currentTimestamp()
})
.addCase(updateUserExpertMode, (state, action) => {
state.userExpertMode = action.payload.userExpertMode
state.timestamp = currentTimestamp()
})
.addCase(updateUserLocale, (state, action) => {
state.userLocale = action.payload.userLocale
state.timestamp = currentTimestamp()
})
.addCase(updateUserSlippageTolerance, (state, action) => {
state.userSlippageTolerance = action.payload.userSlippageTolerance
state.timestamp = currentTimestamp()
})
.addCase(updateUserDeadline, (state, action) => {
state.userDeadline = action.payload.userDeadline
state.timestamp = currentTimestamp()
})
.addCase(updateUserClientSideRouter, (state, action) => {
state.userClientSideRouter = action.payload.userClientSideRouter
})
.addCase(updateHideClosedPositions, (state, action) => {
state.userHideClosedPositions = action.payload.userHideClosedPositions
})
.addCase(updateShowSurveyPopup, (state, action) => {
state.showSurveyPopup = action.payload.showSurveyPopup
})
.addCase(updateShowDonationLink, (state, action) => {
state.showDonationLink = action.payload.showDonationLink
})
.addCase(addSerializedToken, (state, { payload: { serializedToken } }) => {
if (!state.tokens) {
state.tokens = {}
}
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken
state.timestamp = currentTimestamp()
})
.addCase(removeSerializedToken, (state, { payload: { address, chainId } }) => {
if (!state.tokens) {
state.tokens = {}
}
state.tokens[chainId] = state.tokens[chainId] || {}
delete state.tokens[chainId][address]
state.timestamp = currentTimestamp()
})
.addCase(addSerializedPair, (state, { payload: { serializedPair } }) => {
if (
serializedPair.token0.chainId === serializedPair.token1.chainId &&
serializedPair.token0.address !== serializedPair.token1.address
) {
const chainId = serializedPair.token0.chainId
state.pairs[chainId] = state.pairs[chainId] || {}
state.pairs[chainId][pairKey(serializedPair.token0.address, serializedPair.token1.address)] = serializedPair
}
state.timestamp = currentTimestamp()
})
.addCase(removeSerializedPair, (state, { payload: { chainId, tokenAAddress, tokenBAddress } }) => {
if (state.pairs[chainId]) {
// just delete both keys if either exists
delete state.pairs[chainId][pairKey(tokenAAddress, tokenBAddress)]
delete state.pairs[chainId][pairKey(tokenBAddress, tokenAAddress)]
}
state.timestamp = currentTimestamp()
})
)
},
})
export const {
addSerializedPair,
addSerializedToken,
removeSerializedPair,
removeSerializedToken,
updateHideClosedPositions,
updateMatchesDarkMode,
updateShowDonationLink,
updateShowSurveyPopup,
updateUserClientSideRouter,
updateUserDarkMode,
updateUserDeadline,
updateUserExpertMode,
updateUserLocale,
updateUserSlippageTolerance,
} = userSlice.actions
export default userSlice.reducer

12
src/state/user/types.ts Normal file
View File

@@ -0,0 +1,12 @@
export interface SerializedToken {
chainId: number
address: string
decimals: number
symbol?: string
name?: string
}
export interface SerializedPair {
token0: SerializedToken
token1: SerializedToken
}

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { useAppDispatch } from 'state/hooks'
import { updateMatchesDarkMode } from './actions'
import { updateMatchesDarkMode } from './reducer'
export default function Updater(): null {
const dispatch = useAppDispatch()

View File

@@ -3,7 +3,7 @@ import { useEffect } from 'react'
import { RouteComponentProps } from 'react-router-dom'
import { useAppDispatch } from 'state/hooks'
import { updateUserDarkMode } from '../state/user/actions'
import { updateUserDarkMode } from '../state/user/reducer'
export default function DarkModeQueryParamReader({ location: { search } }: RouteComponentProps): null {
const dispatch = useAppDispatch()