Skip to content

Commit 104be83

Browse files
authored
perf(multicall): use single call to get token information (#855)
* use single call to get token information * delete the bytes32 overload * console log statement * add a bunch of tests to actions.ts for multicall * fix to work with bytes32 symbols/names * only include name/symbol * enforce lowercase calldata
1 parent 24c7079 commit 104be83

File tree

13 files changed

+201
-203
lines changed

13 files changed

+201
-203
lines changed

src/components/SearchModal/TokenList.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export default function TokenList({
2828
otherToken,
2929
showSendWithSwap,
3030
onRemoveAddedToken,
31-
otherSelectedText
31+
otherSelectedText,
32+
hideRemove
3233
}: {
3334
tokens: Token[]
3435
selectedToken: string
@@ -38,6 +39,7 @@ export default function TokenList({
3839
otherToken: string
3940
showSendWithSwap?: boolean
4041
otherSelectedText: string
42+
hideRemove?: boolean
4143
}) {
4244
const { t } = useTranslation()
4345
const { account, chainId } = useActiveWeb3React()
@@ -80,15 +82,16 @@ export default function TokenList({
8082
</Text>
8183
<FadedSpan>
8284
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
83-
{customAdded && (
84-
<div
85+
{customAdded && !hideRemove && (
86+
<LinkStyledButton
8587
onClick={event => {
8688
event.stopPropagation()
8789
onRemoveAddedToken(chainId, address)
8890
}}
91+
style={{ marginLeft: '4px', fontWeight: 400 }}
8992
>
90-
<LinkStyledButton style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</LinkStyledButton>
91-
</div>
93+
(Remove)
94+
</LinkStyledButton>
9295
)}
9396
</FadedSpan>
9497
</Column>

src/components/SearchModal/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ function SearchModal({
189189
otherToken={otherSelectedTokenAddress}
190190
selectedToken={hiddenToken}
191191
showSendWithSwap={showSendWithSwap}
192+
hideRemove={Boolean(isAddress(searchQuery))}
192193
/>
193194
) : (
194195
<PairList

src/constants/abis/erc20.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Interface } from '@ethersproject/abi'
22
import ERC20_ABI from './erc20.json'
3+
import ERC20_BYTES32_ABI from './erc20_bytes32.json'
34

45
const ERC20_INTERFACE = new Interface(ERC20_ABI)
56

7+
const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI)
8+
69
export default ERC20_INTERFACE
10+
export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI }

src/constants/abis/erc20_bytes32.json

Lines changed: 11 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,12 @@
33
"constant": true,
44
"inputs": [],
55
"name": "name",
6-
"outputs": [{ "name": "", "type": "bytes32" }],
7-
"payable": false,
8-
"stateMutability": "view",
9-
"type": "function"
10-
},
11-
{
12-
"constant": false,
13-
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
14-
"name": "approve",
15-
"outputs": [{ "name": "", "type": "bool" }],
16-
"payable": false,
17-
"stateMutability": "nonpayable",
18-
"type": "function"
19-
},
20-
{
21-
"constant": true,
22-
"inputs": [],
23-
"name": "totalSupply",
24-
"outputs": [{ "name": "", "type": "uint256" }],
25-
"payable": false,
26-
"stateMutability": "view",
27-
"type": "function"
28-
},
29-
{
30-
"constant": false,
31-
"inputs": [
32-
{ "name": "_from", "type": "address" },
33-
{ "name": "_to", "type": "address" },
34-
{ "name": "_value", "type": "uint256" }
6+
"outputs": [
7+
{
8+
"name": "",
9+
"type": "bytes32"
10+
}
3511
],
36-
"name": "transferFrom",
37-
"outputs": [{ "name": "", "type": "bool" }],
38-
"payable": false,
39-
"stateMutability": "nonpayable",
40-
"type": "function"
41-
},
42-
{
43-
"constant": true,
44-
"inputs": [],
45-
"name": "decimals",
46-
"outputs": [{ "name": "", "type": "uint8" }],
47-
"payable": false,
48-
"stateMutability": "view",
49-
"type": "function"
50-
},
51-
{
52-
"constant": true,
53-
"inputs": [{ "name": "_owner", "type": "address" }],
54-
"name": "balanceOf",
55-
"outputs": [{ "name": "balance", "type": "uint256" }],
5612
"payable": false,
5713
"stateMutability": "view",
5814
"type": "function"
@@ -61,48 +17,14 @@
6117
"constant": true,
6218
"inputs": [],
6319
"name": "symbol",
64-
"outputs": [{ "name": "", "type": "bytes32" }],
65-
"payable": false,
66-
"stateMutability": "view",
67-
"type": "function"
68-
},
69-
{
70-
"constant": false,
71-
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
72-
"name": "transfer",
73-
"outputs": [{ "name": "", "type": "bool" }],
74-
"payable": false,
75-
"stateMutability": "nonpayable",
76-
"type": "function"
77-
},
78-
{
79-
"constant": true,
80-
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
81-
"name": "allowance",
82-
"outputs": [{ "name": "", "type": "uint256" }],
20+
"outputs": [
21+
{
22+
"name": "",
23+
"type": "bytes32"
24+
}
25+
],
8326
"payable": false,
8427
"stateMutability": "view",
8528
"type": "function"
86-
},
87-
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
88-
{
89-
"anonymous": false,
90-
"inputs": [
91-
{ "indexed": true, "name": "owner", "type": "address" },
92-
{ "indexed": true, "name": "spender", "type": "address" },
93-
{ "indexed": false, "name": "value", "type": "uint256" }
94-
],
95-
"name": "Approval",
96-
"type": "event"
97-
},
98-
{
99-
"anonymous": false,
100-
"inputs": [
101-
{ "indexed": true, "name": "from", "type": "address" },
102-
{ "indexed": true, "name": "to", "type": "address" },
103-
{ "indexed": false, "name": "value", "type": "uint256" }
104-
],
105-
"name": "Transfer",
106-
"type": "event"
10729
}
10830
]

src/hooks/Tokens.ts

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { parseBytes32String } from '@ethersproject/strings'
12
import { ChainId, Token, WETH } from '@uniswap/sdk'
23
import { useEffect, useMemo } from 'react'
34
import { ALL_TOKENS } from '../constants/tokens'
4-
import { useAddUserToken, useFetchTokenByAddress, useUserAddedTokens } from '../state/user/hooks'
5+
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
6+
import { useAddUserToken, useUserAddedTokens } from '../state/user/hooks'
57
import { isAddress } from '../utils'
68

79
import { useActiveWeb3React } from './index'
10+
import { useBytes32TokenContract, useTokenContract } from './useContract'
811

912
export function useAllTokens(): { [address: string]: Token } {
1013
const { chainId } = useActiveWeb3React()
@@ -35,36 +38,83 @@ export function useAllTokens(): { [address: string]: Token } {
3538
}, [userAddedTokens, chainId])
3639
}
3740

38-
export function useToken(tokenAddress?: string): Token | undefined {
41+
// parse a name or symbol from a token response
42+
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
43+
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
44+
return str && str.length > 0
45+
? str
46+
: bytes32 && BYTES32_REGEX.test(bytes32)
47+
? parseBytes32String(bytes32)
48+
: defaultValue
49+
}
50+
51+
// undefined if invalid or does not exist
52+
// null if loading
53+
// otherwise returns the token
54+
export function useToken(tokenAddress?: string): Token | undefined | null {
55+
const { chainId } = useActiveWeb3React()
3956
const tokens = useAllTokens()
57+
58+
const address = isAddress(tokenAddress)
59+
60+
const tokenContract = useTokenContract(address ? address : undefined, false)
61+
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
62+
const token: Token | undefined = address ? tokens[address] : undefined
63+
64+
const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
65+
const tokenNameBytes32 = useSingleCallResult(
66+
token ? undefined : tokenContractBytes32,
67+
'name',
68+
undefined,
69+
NEVER_RELOAD
70+
)
71+
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
72+
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
73+
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
74+
4075
return useMemo(() => {
41-
const validatedAddress = isAddress(tokenAddress)
42-
if (!validatedAddress) return
43-
return tokens[validatedAddress]
44-
}, [tokens, tokenAddress])
76+
if (token) return token
77+
if (!chainId || !address) return undefined
78+
if (decimals.loading || symbol.loading || tokenName.loading) return null
79+
if (decimals.result) {
80+
return new Token(
81+
chainId,
82+
address,
83+
decimals.result[0],
84+
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
85+
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
86+
)
87+
}
88+
return undefined
89+
}, [
90+
address,
91+
chainId,
92+
decimals.loading,
93+
decimals.result,
94+
symbol.loading,
95+
symbol.result,
96+
symbolBytes32.result,
97+
token,
98+
tokenName.loading,
99+
tokenName.result,
100+
tokenNameBytes32.result
101+
])
45102
}
46103

47104
// gets token information by address (typically user input) and
48-
// automatically adds it for the user if the token address is valid
49-
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined {
50-
const fetchTokenByAddress = useFetchTokenByAddress()
105+
// automatically adds it for the user if it's a valid token address
106+
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined | null {
51107
const addToken = useAddUserToken()
52108
const token = useToken(tokenAddress)
53109
const { chainId } = useActiveWeb3React()
110+
const allTokens = useAllTokens()
54111

55112
useEffect(() => {
56-
if (!chainId || !isAddress(tokenAddress)) return
57-
const weth = WETH[chainId as ChainId]
58-
if (weth && weth.address === isAddress(tokenAddress)) return
59-
60-
if (tokenAddress && !token) {
61-
fetchTokenByAddress(tokenAddress).then(token => {
62-
if (token !== null) {
63-
addToken(token)
64-
}
65-
})
66-
}
67-
}, [tokenAddress, token, fetchTokenByAddress, addToken, chainId])
113+
if (!chainId || !token) return
114+
if (WETH[chainId as ChainId]?.address === token.address) return
115+
if (allTokens[token.address]) return
116+
addToken(token)
117+
}, [token, addToken, chainId, allTokens])
68118

69119
return token
70120
}

src/hooks/useContract.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Contract } from '@ethersproject/contracts'
22
import { ChainId } from '@uniswap/sdk'
33
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
44
import { useMemo } from 'react'
5+
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
56
import ERC20_ABI from '../constants/abis/erc20.json'
67
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
78
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
@@ -41,6 +42,10 @@ export function useTokenContract(tokenAddress?: string, withSignerIfPossible = t
4142
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
4243
}
4344

45+
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
46+
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
47+
}
48+
4449
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
4550
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
4651
}

src/state/application/updater.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useCallback, useEffect, useState } from 'react'
22
import { useActiveWeb3React } from '../../hooks'
3+
import useDebounce from '../../hooks/useDebounce'
34
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
45
import { updateBlockNumber } from './actions'
56
import { useDispatch } from 'react-redux'
@@ -45,10 +46,12 @@ export default function Updater() {
4546
}
4647
}, [dispatch, chainId, library, blockNumberCallback, windowVisible])
4748

49+
const debouncedState = useDebounce(state, 100)
50+
4851
useEffect(() => {
49-
if (!state.chainId || !state.blockNumber || !windowVisible) return
50-
dispatch(updateBlockNumber({ chainId: state.chainId, blockNumber: state.blockNumber }))
51-
}, [windowVisible, dispatch, state.blockNumber, state.chainId])
52+
if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return
53+
dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber }))
54+
}, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId])
5255

5356
return null
5457
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { parseCallKey, toCallKey } from './actions'
2+
3+
describe('actions', () => {
4+
describe('#parseCallKey', () => {
5+
it('throws for invalid address', () => {
6+
expect(() => parseCallKey('0x-0x')).toThrow('Invalid address: 0x')
7+
})
8+
it('throws for invalid calldata', () => {
9+
expect(() => parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-abc')).toThrow('Invalid hex: abc')
10+
})
11+
it('throws for invalid format', () => {
12+
expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc')
13+
})
14+
it('throws for uppercase hex', () => {
15+
expect(() => parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toThrow('Invalid hex: 0xabcD')
16+
})
17+
it('parses pieces into address', () => {
18+
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd')).toEqual({
19+
address: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
20+
callData: '0xabcd'
21+
})
22+
})
23+
})
24+
25+
describe('#toCallKey', () => {
26+
it('throws for invalid address', () => {
27+
expect(() => toCallKey({ callData: '0x', address: '0x' })).toThrow('Invalid address: 0x')
28+
})
29+
it('throws for invalid calldata', () => {
30+
expect(() =>
31+
toCallKey({
32+
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
33+
callData: 'abc'
34+
})
35+
).toThrow('Invalid hex: abc')
36+
})
37+
it('throws for uppercase hex', () => {
38+
expect(() =>
39+
toCallKey({
40+
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
41+
callData: '0xabcD'
42+
})
43+
).toThrow('Invalid hex: 0xabcD')
44+
})
45+
it('concatenates address to data', () => {
46+
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
47+
'0x6B175474E89094C44Da98b954EedeAC495271d0F-0xabcd'
48+
)
49+
})
50+
})
51+
})

0 commit comments

Comments
 (0)