Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/components/SearchModal/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export default function TokenList({
otherToken,
showSendWithSwap,
onRemoveAddedToken,
otherSelectedText
otherSelectedText,
hideRemove
}: {
tokens: Token[]
selectedToken: string
Expand All @@ -38,6 +39,7 @@ export default function TokenList({
otherToken: string
showSendWithSwap?: boolean
otherSelectedText: string
hideRemove?: boolean
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
Expand Down Expand Up @@ -80,15 +82,16 @@ export default function TokenList({
</Text>
<FadedSpan>
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
{customAdded && (
<div
{customAdded && !hideRemove && (
<LinkStyledButton
onClick={event => {
event.stopPropagation()
onRemoveAddedToken(chainId, address)
}}
style={{ marginLeft: '4px', fontWeight: 400 }}
>
<LinkStyledButton style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</LinkStyledButton>
</div>
(Remove)
</LinkStyledButton>
)}
</FadedSpan>
</Column>
Expand Down
1 change: 1 addition & 0 deletions src/components/SearchModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ function SearchModal({
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
showSendWithSwap={showSendWithSwap}
hideRemove={Boolean(isAddress(searchQuery))}
/>
) : (
<PairList
Expand Down
4 changes: 4 additions & 0 deletions src/constants/abis/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Interface } from '@ethersproject/abi'
import ERC20_ABI from './erc20.json'
import ERC20_BYTES32_ABI from './erc20_bytes32.json'

const ERC20_INTERFACE = new Interface(ERC20_ABI)

const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI)

export default ERC20_INTERFACE
export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI }
100 changes: 11 additions & 89 deletions src/constants/abis/erc20_bytes32.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,12 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "approve",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"name": "transferFrom",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
Expand All @@ -61,48 +17,14 @@
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "transfer",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
"name": "allowance",
"outputs": [{ "name": "", "type": "uint256" }],
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
}
]
92 changes: 71 additions & 21 deletions src/hooks/Tokens.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens'
import { useAddUserToken, useFetchTokenByAddress, useUserAddedTokens } from '../state/user/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useAddUserToken, useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils'

import { useActiveWeb3React } from './index'
import { useBytes32TokenContract, useTokenContract } from './useContract'

export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React()
Expand Down Expand Up @@ -35,36 +38,83 @@ export function useAllTokens(): { [address: string]: Token } {
}, [userAddedTokens, chainId])
}

export function useToken(tokenAddress?: string): Token | undefined {
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
return str && str.length > 0
? str
: bytes32 && BYTES32_REGEX.test(bytes32)
? parseBytes32String(bytes32)
: defaultValue
}

// undefined if invalid or does not exist
// null if loading
// otherwise returns the token
export function useToken(tokenAddress?: string): Token | undefined | null {
const { chainId } = useActiveWeb3React()
const tokens = useAllTokens()

const address = isAddress(tokenAddress)

const tokenContract = useTokenContract(address ? address : undefined, false)
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
const token: Token | undefined = address ? tokens[address] : undefined

const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
const tokenNameBytes32 = useSingleCallResult(
token ? undefined : tokenContractBytes32,
'name',
undefined,
NEVER_RELOAD
)
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)

return useMemo(() => {
const validatedAddress = isAddress(tokenAddress)
if (!validatedAddress) return
return tokens[validatedAddress]
}, [tokens, tokenAddress])
if (token) return token
if (!chainId || !address) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
return new Token(
chainId,
address,
decimals.result[0],
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
)
}
return undefined
}, [
address,
chainId,
decimals.loading,
decimals.result,
symbol.loading,
symbol.result,
symbolBytes32.result,
token,
tokenName.loading,
tokenName.result,
tokenNameBytes32.result
])
}

// gets token information by address (typically user input) and
// automatically adds it for the user if the token address is valid
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined {
const fetchTokenByAddress = useFetchTokenByAddress()
// automatically adds it for the user if it's a valid token address
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined | null {
const addToken = useAddUserToken()
const token = useToken(tokenAddress)
const { chainId } = useActiveWeb3React()
const allTokens = useAllTokens()

useEffect(() => {
if (!chainId || !isAddress(tokenAddress)) return
const weth = WETH[chainId as ChainId]
if (weth && weth.address === isAddress(tokenAddress)) return

if (tokenAddress && !token) {
fetchTokenByAddress(tokenAddress).then(token => {
if (token !== null) {
addToken(token)
}
})
}
}, [tokenAddress, token, fetchTokenByAddress, addToken, chainId])
if (!chainId || !token) return
if (WETH[chainId as ChainId]?.address === token.address) return
if (allTokens[token.address]) return
addToken(token)
}, [token, addToken, chainId, allTokens])

return token
}
5 changes: 5 additions & 0 deletions src/hooks/useContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
import ERC20_ABI from '../constants/abis/erc20.json'
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
Expand Down Expand Up @@ -41,6 +42,10 @@ export function useTokenContract(tokenAddress?: string, withSignerIfPossible = t
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
}

export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}

export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
}
Expand Down
9 changes: 6 additions & 3 deletions src/state/application/updater.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
import { useActiveWeb3React } from '../../hooks'
import useDebounce from '../../hooks/useDebounce'
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { updateBlockNumber } from './actions'
import { useDispatch } from 'react-redux'
Expand Down Expand Up @@ -45,10 +46,12 @@ export default function Updater() {
}
}, [dispatch, chainId, library, blockNumberCallback, windowVisible])

const debouncedState = useDebounce(state, 100)

useEffect(() => {
if (!state.chainId || !state.blockNumber || !windowVisible) return
dispatch(updateBlockNumber({ chainId: state.chainId, blockNumber: state.blockNumber }))
}, [windowVisible, dispatch, state.blockNumber, state.chainId])
if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return
dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber }))
}, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId])

return null
}
51 changes: 51 additions & 0 deletions src/state/multicall/actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { parseCallKey, toCallKey } from './actions'

describe('actions', () => {
describe('#parseCallKey', () => {
it('throws for invalid address', () => {
expect(() => parseCallKey('0x-0x')).toThrow('Invalid address: 0x')
})
it('throws for invalid calldata', () => {
expect(() => parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-abc')).toThrow('Invalid hex: abc')
})
it('throws for invalid format', () => {
expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc')
})
it('throws for uppercase hex', () => {
expect(() => parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toThrow('Invalid hex: 0xabcD')
})
it('parses pieces into address', () => {
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd')).toEqual({
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
callData: '0xabcd'
})
})
})

describe('#toCallKey', () => {
it('throws for invalid address', () => {
expect(() => toCallKey({ callData: '0x', address: '0x' })).toThrow('Invalid address: 0x')
})
it('throws for invalid calldata', () => {
expect(() =>
toCallKey({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: 'abc'
})
).toThrow('Invalid hex: abc')
})
it('throws for uppercase hex', () => {
expect(() =>
toCallKey({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: '0xabcD'
})
).toThrow('Invalid hex: 0xabcD')
})
it('concatenates address to data', () => {
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
'0x6B175474E89094C44Da98b954EedeAC495271d0F-0xabcd'
)
})
})
})
Loading