Slippage warning (#292)
* add rudimentary slippage warning fix address checksum bug debounce address input alway show tx details if valid trade * fix estimated copy
This commit is contained in:
parent
c6e165eaf7
commit
e27cd92cd2
@ -26,6 +26,7 @@
|
|||||||
"unlockTokenCont": "Please unlock token to continue.",
|
"unlockTokenCont": "Please unlock token to continue.",
|
||||||
"transactionDetails": "Transaction Details",
|
"transactionDetails": "Transaction Details",
|
||||||
"hideDetails": "Hide Details",
|
"hideDetails": "Hide Details",
|
||||||
|
"slippageWarning": "Slippage Warning",
|
||||||
"youAreSelling": "You are selling",
|
"youAreSelling": "You are selling",
|
||||||
"orTransFail": "or the transaction will fail.",
|
"orTransFail": "or the transaction will fail.",
|
||||||
"youWillReceive": "You will receive at least",
|
"youWillReceive": "You will receive at least",
|
||||||
|
@ -4,20 +4,23 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
import { isAddress } from '../../utils'
|
import { isAddress } from '../../utils'
|
||||||
|
import { useDebounce } from '../../hooks'
|
||||||
// import QrCode from '../QrCode' // commented out pending further review
|
// import QrCode from '../QrCode' // commented out pending further review
|
||||||
|
|
||||||
import './address-input-panel.scss'
|
import './address-input-panel.scss'
|
||||||
|
|
||||||
export default function AddressInputPanel({ title, onChange = () => {}, onError = () => {} }) {
|
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { library } = useWeb3Context()
|
const { library } = useWeb3Context()
|
||||||
|
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState(initialInput)
|
||||||
|
const debouncedInput = useDebounce(input, 150)
|
||||||
|
|
||||||
const [data, setData] = useState({ address: undefined, name: undefined })
|
const [data, setData] = useState({ address: undefined, name: undefined })
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
// keep stuff in sync
|
// keep data and errors in sync
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange({ address: data.address, name: data.name })
|
onChange({ address: data.address, name: data.name })
|
||||||
}, [onChange, data.address, data.name])
|
}, [onChange, data.address, data.name])
|
||||||
@ -25,29 +28,30 @@ export default function AddressInputPanel({ title, onChange = () => {}, onError
|
|||||||
onError(error)
|
onError(error)
|
||||||
}, [onError, error])
|
}, [onError, error])
|
||||||
|
|
||||||
|
// run parser on debounced input
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let stale = false
|
let stale = false
|
||||||
|
|
||||||
if (isAddress(input)) {
|
if (isAddress(debouncedInput)) {
|
||||||
library.lookupAddress(input).then(name => {
|
library.lookupAddress(debouncedInput).then(name => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
// if an ENS name exists, set it as the destination
|
// if an ENS name exists, set it as the destination
|
||||||
if (name) {
|
if (name) {
|
||||||
setInput(name)
|
setInput(name)
|
||||||
} else {
|
} else {
|
||||||
setData({ address: input, name: '' })
|
setData({ address: debouncedInput, name: '' })
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (input !== '') {
|
if (debouncedInput !== '') {
|
||||||
try {
|
try {
|
||||||
library.resolveName(input).then(address => {
|
library.resolveName(debouncedInput).then(address => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
// if the input name resolves to an address
|
// if the debounced input name resolves to an address
|
||||||
if (address) {
|
if (address) {
|
||||||
setData({ address: address, name: input })
|
setData({ address: address, name: debouncedInput })
|
||||||
setError(null)
|
setError(null)
|
||||||
} else {
|
} else {
|
||||||
setError(true)
|
setError(true)
|
||||||
@ -62,10 +66,20 @@ export default function AddressInputPanel({ title, onChange = () => {}, onError
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stale = true
|
stale = true
|
||||||
|
}
|
||||||
|
}, [debouncedInput, library, onChange, onError])
|
||||||
|
|
||||||
|
function onInput(event) {
|
||||||
|
if (data.address !== undefined || data.name !== undefined) {
|
||||||
setData({ address: undefined, name: undefined })
|
setData({ address: undefined, name: undefined })
|
||||||
|
}
|
||||||
|
if (error !== undefined) {
|
||||||
setError()
|
setError()
|
||||||
}
|
}
|
||||||
}, [input, library, onChange, onError])
|
const input = event.target.value
|
||||||
|
const checksummedInput = isAddress(input)
|
||||||
|
setInput(checksummedInput || input)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="currency-input-panel">
|
<div className="currency-input-panel">
|
||||||
@ -83,11 +97,15 @@ export default function AddressInputPanel({ title, onChange = () => {}, onError
|
|||||||
<div className="currency-input-panel__input-row">
|
<div className="currency-input-panel__input-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
className={classnames('address-input-panel__input', {
|
className={classnames('address-input-panel__input', {
|
||||||
'address-input-panel__input--error': input !== '' && error
|
'address-input-panel__input--error': input !== '' && error
|
||||||
})}
|
})}
|
||||||
placeholder="0x1234..."
|
placeholder="0x1234..."
|
||||||
onChange={e => setInput(e.target.value)}
|
onChange={onInput}
|
||||||
value={input}
|
value={input}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
42
src/components/ContextualInfoNew/contextual-info.scss
Normal file
42
src/components/ContextualInfoNew/contextual-info.scss
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
@import '../../variables.scss';
|
||||||
|
|
||||||
|
.contextual-info {
|
||||||
|
&__summary-wrapper {
|
||||||
|
color: $dove-gray;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
color: $salmon-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details {
|
||||||
|
background-color: $concrete-gray;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__open-details-container {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@extend %row-nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: $royal-blue;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 0.75rem;
|
||||||
|
width: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
src/components/ContextualInfoNew/index.js
Normal file
55
src/components/ContextualInfoNew/index.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import c from 'classnames'
|
||||||
|
|
||||||
|
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
|
||||||
|
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
|
||||||
|
|
||||||
|
import './contextual-info.scss'
|
||||||
|
|
||||||
|
const WrappedDropup = ({ isError, ...rest }) => <Dropup {...rest} />
|
||||||
|
const ColoredDropup = styled(WrappedDropup)`
|
||||||
|
path {
|
||||||
|
stroke: ${props => props.isError && props.theme.salmonRed};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const WrappedDropdown = ({ isError, ...rest }) => <Dropdown {...rest} />
|
||||||
|
const ColoredDropdown = styled(WrappedDropdown)`
|
||||||
|
path {
|
||||||
|
stroke: ${props => props.isError && props.theme.salmonRed};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function ContextualInfo({
|
||||||
|
openDetailsText = 'Transaction Details',
|
||||||
|
closeDetailsText = 'Hide Details',
|
||||||
|
contextualInfo = '',
|
||||||
|
allowExpand = false,
|
||||||
|
renderTransactionDetails = () => {},
|
||||||
|
isError = false
|
||||||
|
}) {
|
||||||
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
|
||||||
|
return !allowExpand ? (
|
||||||
|
<div className={c({ 'contextual-info--error': isError }, 'contextual-info__summary-wrapper')}>
|
||||||
|
<div>{contextualInfo}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key="open-details"
|
||||||
|
className="contextual-info__summary-wrapper contextual-info__open-details-container"
|
||||||
|
onClick={() => setShowDetails(s => !s)}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span className={c({ 'contextual-info--error': isError })}>
|
||||||
|
{contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText}
|
||||||
|
</span>
|
||||||
|
{showDetails ? <ColoredDropup isError={isError} /> : <ColoredDropdown isError={isError} />}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
{showDetails && <div className="contextual-info__details">{renderTransactionDetails()}</div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -281,6 +281,12 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
function onInput(event) {
|
||||||
|
const input = event.target.value
|
||||||
|
const checksummedInput = isAddress(input)
|
||||||
|
setSearchQuery(checksummedInput || input)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={_onClose}>
|
<Modal onClose={_onClose}>
|
||||||
<CSSTransitionGroup
|
<CSSTransitionGroup
|
||||||
@ -298,9 +304,7 @@ function CurrencySelectModal({ onClose, onTokenSelect }) {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder={t('searchOrPaste')}
|
placeholder={t('searchOrPaste')}
|
||||||
className="token-modal__search-input"
|
className="token-modal__search-input"
|
||||||
onChange={e => {
|
onChange={onInput}
|
||||||
setSearchQuery(e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
|
<img src={SearchIcon} className="token-modal__search-icon" alt="search" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,30 @@
|
|||||||
import { useMemo, useCallback, useEffect } from 'react'
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
||||||
import { useWeb3Context } from 'web3-react'
|
import { useWeb3Context } from 'web3-react'
|
||||||
|
|
||||||
import ERC20_ABI from '../abi/erc20'
|
import ERC20_ABI from '../abi/erc20'
|
||||||
import { getContract, getFactoryContract, getExchangeContract } from '../utils'
|
import { getContract, getFactoryContract, getExchangeContract } from '../utils'
|
||||||
|
|
||||||
|
// modified from https://usehooks.com/useDebounce/
|
||||||
|
export function useDebounce(value, delay) {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update debounced value after delay
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value)
|
||||||
|
}, delay)
|
||||||
|
|
||||||
|
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||||
|
// This is how we prevent debounced value from updating if value is changed ...
|
||||||
|
// .. within the delay period. Timeout gets cleared and restarted.
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler)
|
||||||
|
}
|
||||||
|
}, [value, delay])
|
||||||
|
|
||||||
|
return debouncedValue
|
||||||
|
}
|
||||||
|
|
||||||
// modified from https://usehooks.com/useKeyPress/
|
// modified from https://usehooks.com/useKeyPress/
|
||||||
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
|
export function useBodyKeyDown(targetKey, onKeyDown, suppressOnKeyDown = false) {
|
||||||
const downHandler = useCallback(
|
const downHandler = useCallback(
|
||||||
|
@ -297,7 +297,7 @@ export default function AddLiquidity() {
|
|||||||
|
|
||||||
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
|
const deadline = Math.ceil(Date.now() / 1000) + DEADLINE_FROM_NOW
|
||||||
const estimatedGasLimit = await exchangeContract.estimate.addLiquidity(
|
const estimatedGasLimit = await exchangeContract.estimate.addLiquidity(
|
||||||
isNewExchange ? inputValueParsed : liquidityTokensMin,
|
isNewExchange ? ethers.constants.Zero : liquidityTokensMin,
|
||||||
isNewExchange ? outputValueParsed : outputValueMax,
|
isNewExchange ? outputValueParsed : outputValueMax,
|
||||||
deadline,
|
deadline,
|
||||||
{
|
{
|
||||||
@ -307,7 +307,7 @@ export default function AddLiquidity() {
|
|||||||
|
|
||||||
exchangeContract
|
exchangeContract
|
||||||
.addLiquidity(
|
.addLiquidity(
|
||||||
isNewExchange ? inputValueParsed : liquidityTokensMin,
|
isNewExchange ? ethers.constants.Zero : liquidityTokensMin,
|
||||||
isNewExchange ? outputValueParsed : outputValueMax,
|
isNewExchange ? outputValueParsed : outputValueMax,
|
||||||
deadline,
|
deadline,
|
||||||
{
|
{
|
||||||
@ -476,7 +476,7 @@ export default function AddLiquidity() {
|
|||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
title={t('deposit')}
|
title={t('deposit')}
|
||||||
description={isNewExchange ? `(${t('estimated')})` : ''}
|
description={isNewExchange ? '' : outputValue ? `(${t('estimated')})` : ''}
|
||||||
extraText={outputBalance && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))}
|
extraText={outputBalance && formatBalance(amountFormatter(outputBalance, decimals, Math.min(decimals, 4)))}
|
||||||
selectedTokenAddress={outputCurrency}
|
selectedTokenAddress={outputCurrency}
|
||||||
onCurrencySelected={outputCurrency => {
|
onCurrencySelected={outputCurrency => {
|
||||||
|
@ -18,7 +18,7 @@ function CreateExchange({ history, location }) {
|
|||||||
const factory = useFactoryContract()
|
const factory = useFactoryContract()
|
||||||
|
|
||||||
const [tokenAddress, setTokenAddress] = useState({
|
const [tokenAddress, setTokenAddress] = useState({
|
||||||
address: (location.state && location.state.tokenAddress) || '',
|
address: '',
|
||||||
name: ''
|
name: ''
|
||||||
})
|
})
|
||||||
const [tokenAddressError, setTokenAddressError] = useState()
|
const [tokenAddressError, setTokenAddressError] = useState()
|
||||||
@ -74,7 +74,12 @@ function CreateExchange({ history, location }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AddressInputPanel title={t('tokenAddress')} onChange={setTokenAddress} onError={setTokenAddressError} />
|
<AddressInputPanel
|
||||||
|
title={t('tokenAddress')}
|
||||||
|
initialInput={(location.state && location.state.tokenAddress) || ''}
|
||||||
|
onChange={setTokenAddress}
|
||||||
|
onError={setTokenAddressError}
|
||||||
|
/>
|
||||||
<OversizedPanel hideBottom>
|
<OversizedPanel hideBottom>
|
||||||
<div className="pool__summary-panel">
|
<div className="pool__summary-panel">
|
||||||
<div className="pool__exchange-rate-wrapper">
|
<div className="pool__exchange-rate-wrapper">
|
||||||
|
@ -284,7 +284,7 @@ export default function RemoveLiquidity() {
|
|||||||
</OversizedPanel>
|
</OversizedPanel>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
title={t('output')}
|
title={t('output')}
|
||||||
description={`(${t('estimated')})`}
|
description={ethWithdrawn && tokenWithdrawn ? `(${t('estimated')})` : ''}
|
||||||
key="remove-liquidity-input"
|
key="remove-liquidity-input"
|
||||||
renderInput={() =>
|
renderInput={() =>
|
||||||
ethWithdrawn && tokenWithdrawn ? (
|
ethWithdrawn && tokenWithdrawn ? (
|
||||||
|
@ -5,7 +5,7 @@ import { useWeb3Context } from 'web3-react'
|
|||||||
import { ethers } from 'ethers'
|
import { ethers } from 'ethers'
|
||||||
|
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import ContextualInfo from '../../components/ContextualInfo'
|
import NewContextualInfo from '../../components/ContextualInfoNew'
|
||||||
import OversizedPanel from '../../components/OversizedPanel'
|
import OversizedPanel from '../../components/OversizedPanel'
|
||||||
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
|
import ArrowDownBlue from '../../assets/images/arrow-down-blue.svg'
|
||||||
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
|
import ArrowDownGrey from '../../assets/images/arrow-down-grey.svg'
|
||||||
@ -425,18 +425,16 @@ export default function Swap() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const percentSlippage =
|
const percentSlippage =
|
||||||
exchangeRate &&
|
exchangeRate && marketRate
|
||||||
marketRate &&
|
? exchangeRate
|
||||||
amountFormatter(
|
|
||||||
exchangeRate
|
|
||||||
.sub(marketRate)
|
.sub(marketRate)
|
||||||
.abs()
|
.abs()
|
||||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
|
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
|
||||||
.div(marketRate)
|
.div(marketRate)
|
||||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15)))),
|
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
||||||
16,
|
: undefined
|
||||||
2
|
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
||||||
)
|
const slippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.1')) // 10%
|
||||||
|
|
||||||
const isValid = exchangeRate && inputError === null && independentError === null
|
const isValid = exchangeRate && inputError === null && independentError === null
|
||||||
|
|
||||||
@ -479,7 +477,7 @@ export default function Swap() {
|
|||||||
{t('orTransFail')}
|
{t('orTransFail')}
|
||||||
</div>
|
</div>
|
||||||
<div className="send__last-summary-text">
|
<div className="send__last-summary-text">
|
||||||
{t('priceChange')} {b(`${percentSlippage}%`)}.
|
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -509,7 +507,7 @@ export default function Swap() {
|
|||||||
{t('orTransFail')}
|
{t('orTransFail')}
|
||||||
</div>
|
</div>
|
||||||
<div className="send__last-summary-text">
|
<div className="send__last-summary-text">
|
||||||
{t('priceChange')} {b(`${percentSlippage}%`)}.
|
{t('priceChange')} {b(`${percentSlippageFormatted}%`)}.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -533,10 +531,11 @@ export default function Swap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextualInfo
|
<NewContextualInfo
|
||||||
openDetailsText={t('transactionDetails')}
|
openDetailsText={t('transactionDetails')}
|
||||||
closeDetailsText={t('hideDetails')}
|
closeDetailsText={t('hideDetails')}
|
||||||
contextualInfo={contextualInfo}
|
contextualInfo={contextualInfo ? contextualInfo : slippageWarning ? t('slippageWarning') : ''}
|
||||||
|
allowExpand={!!(inputCurrency && outputCurrency && inputValueParsed && outputValueParsed)}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
renderTransactionDetails={renderTransactionDetails}
|
renderTransactionDetails={renderTransactionDetails}
|
||||||
/>
|
/>
|
||||||
|
@ -4,6 +4,7 @@ import { ThemeProvider as StyledComponentsThemeProvider, createGlobalStyle } fro
|
|||||||
const theme = {
|
const theme = {
|
||||||
uniswapPink: '#DC6BE5',
|
uniswapPink: '#DC6BE5',
|
||||||
royalBlue: '#2f80ed',
|
royalBlue: '#2f80ed',
|
||||||
|
salmonRed: '#ff6871',
|
||||||
white: '#FFF',
|
white: '#FFF',
|
||||||
black: '#000'
|
black: '#000'
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,17 @@ export const ERROR_CODES = ['TOKEN_NAME', 'TOKEN_SYMBOL', 'TOKEN_DECIMALS'].redu
|
|||||||
)
|
)
|
||||||
|
|
||||||
export function safeAccess(object, path) {
|
export function safeAccess(object, path) {
|
||||||
return path.reduce(
|
return object
|
||||||
|
? path.reduce(
|
||||||
(accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
|
(accumulator, currentValue) => (accumulator && accumulator[currentValue] ? accumulator[currentValue] : null),
|
||||||
object
|
object
|
||||||
)
|
)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAddress(value) {
|
export function isAddress(value) {
|
||||||
try {
|
try {
|
||||||
ethers.utils.getAddress(value)
|
return ethers.utils.getAddress(value.toLowerCase())
|
||||||
return true
|
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user